//
// https://github.com/ServiceStack/ServiceStack.Text
// ServiceStack.Text: .NET C# POCO JSON, JSV and CSV Text Serializers.
//
// Authors:
//   Demis Bellot (demis.bellot@gmail.com)
//
// Copyright 2012 ServiceStack, Inc. All Rights Reserved.
//
// Licensed under the same terms of ServiceStack.
//

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using ServiceStack.Text.Support;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;

using ServiceStack.Text;

namespace ServiceStack;

public delegate EmptyCtorDelegate EmptyCtorFactoryDelegate(Type type);
public delegate object EmptyCtorDelegate();

public static class ReflectionExtensions
{
    public static TypeCode GetTypeCode(this Type type)
    {
        return Type.GetTypeCode(type);
    }        

    public static bool IsInstanceOf(this Type type, Type thisOrBaseType)
    {
        while (type != null)
        {
            if (type == thisOrBaseType)
                return true;

            type = type.BaseType;
        }

        return false;
    }

    public static bool HasGenericType(this Type type)
    {
        while (type != null)
        {
            if (type.IsGenericType)
                return true;

            type = type.BaseType;
        }
        return false;
    }

    public static Type FirstGenericType(this Type type)
    {
        while (type != null)
        {
            if (type.IsGenericType)
                return type;

            type = type.BaseType;
        }
        return null;
    }

    public static Type GetTypeWithGenericTypeDefinitionOfAny(this Type type, params Type[] genericTypeDefinitions)
    {
        foreach (var genericTypeDefinition in genericTypeDefinitions)
        {
            var genericType = type.GetTypeWithGenericTypeDefinitionOf(genericTypeDefinition);
            if (genericType == null && type == genericTypeDefinition)
            {
                genericType = type;
            }

            if (genericType != null)
                return genericType;
        }
        return null;
    }

    public static bool IsOrHasGenericInterfaceTypeOf(this Type type, Type genericTypeDefinition) =>
        type.IsOrHasGenericTypeOf(genericTypeDefinition);

    public static bool IsOrHasGenericTypeOf(this Type type, Type genericTypeDefinition)
    {
        return type.GetTypeWithGenericTypeDefinitionOf(genericTypeDefinition) != null
               || type == genericTypeDefinition;
    }

    public static Type GetTypeWithGenericTypeDefinitionOf(this Type type, Type genericTypeDefinition)
    {
        foreach (var t in type.GetInterfaces())
        {
            if (t.IsGenericType && t.GetGenericTypeDefinition() == genericTypeDefinition)
            {
                return t;
            }
        }

        var genericType = type.FirstGenericType();
        if (genericType != null && genericType.GetGenericTypeDefinition() == genericTypeDefinition)
        {
            return genericType;
        }

        return null;
    }

    public static Type FirstGenericArg(this Type type) => type.IsGenericType || type.IsGenericTypeDefinition
        ? type.GetGenericArguments().FirstOrDefault()
        : null;

    public static Type GetTypeWithInterfaceOf(this Type type, Type interfaceType)
    {
        if (type == interfaceType) return interfaceType;

        foreach (var t in type.GetInterfaces())
        {
            if (t == interfaceType)
                return t;
        }

        return null;
    }

    public static bool HasInterface(this Type type, Type interfaceType)
    {
#if NET6_0_OR_GREATER        
        return type.IsAssignableTo(interfaceType);
#else
        foreach (var t in type.GetInterfaces())
        {
            if (t == interfaceType)
                return true;
        }
        return false;
#endif
    }

    public static bool HasAnyInterface(this Type type, Type[] interfaceTypes)
    {
        foreach (var t in type.GetInterfaces())
        {
            foreach (var interfaceType in interfaceTypes)
            {
                if (t == interfaceType)
                    return true;
            }
        }
        return false;
    }

    public static bool AllHaveInterfacesOfType(
        this Type assignableFromType, params Type[] types)
    {
        foreach (var type in types)
        {
            if (assignableFromType.GetTypeWithInterfaceOf(type) == null) return false;
        }
        return true;
    }

    public static bool IsNullableType(this Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
    }

    public static bool IsAssignableToNull(this PropertyInfo pi)
    {
        var propType = pi.PropertyType;
        if (propType.IsValueType)
            return propType.IsNullableType();
        
#if NET6_0_OR_GREATER
        return new NullabilityInfoContext().Create(pi).WriteState is NullabilityState.Nullable;
#else
        return IsAssignableToNull(propType, pi.DeclaringType, pi.CustomAttributes);
#endif
    }

    public static bool IsAssignableToNull(Type memberType, MemberInfo declaringType, IEnumerable<CustomAttributeData> customAttributes)
    {
        if (memberType.IsValueType)
            return Nullable.GetUnderlyingType(memberType) != null;

        var nullable = customAttributes
            .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
        if (nullable is { ConstructorArguments.Count: 1 })
        {
            var attributeArgument = nullable.ConstructorArguments[0];
            if (attributeArgument.ArgumentType == typeof(byte[]))
            {
                var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value!;
                if (args.Count > 0 && args[0].ArgumentType == typeof(byte))
                {
                    return (byte)args[0].Value! == 2;
                }
            }
            else if (attributeArgument.ArgumentType == typeof(byte))
            {
                return (byte)attributeArgument.Value! == 2;
            }
        }

        for (var type = declaringType; type != null; type = type.DeclaringType)
        {
            var context = type.CustomAttributes
                .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
            if (context is { ConstructorArguments.Count: 1 } &&
                context.ConstructorArguments[0].ArgumentType == typeof(byte))
            {
                return (byte)context.ConstructorArguments[0].Value! == 2;
            }
        }

        return false;
    }

    public static TypeCode GetUnderlyingTypeCode(this Type type)
    {
        return GetTypeCode(Nullable.GetUnderlyingType(type) ?? type);
    }

    public static bool IsNumericType(this Type type)
    {
        if (type == null) return false;

        if (type.IsEnum) //TypeCode can be TypeCode.Int32
        {
            return JsConfig.TreatEnumAsInteger || type.IsEnumFlags();
        }

        switch (GetTypeCode(type))
        {
            case TypeCode.Byte:
            case TypeCode.Decimal:
            case TypeCode.Double:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.SByte:
            case TypeCode.Single:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
                return true;

            case TypeCode.Object:
                if (type.IsNullableType())
                {
                    return IsNumericType(Nullable.GetUnderlyingType(type));
                }
                if (type.IsEnum)
                {
                    return JsConfig.TreatEnumAsInteger || type.IsEnumFlags();
                }
                return false;
        }
        return false;
    }

    public static bool IsIntegerType(this Type type)
    {
        if (type == null) return false;

        switch (GetTypeCode(type))
        {
            case TypeCode.Byte:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.SByte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
                return true;

            case TypeCode.Object:
                if (type.IsNullableType())
                {
                    return IsNumericType(Nullable.GetUnderlyingType(type));
                }
                return false;
        }
        return false;
    }

    public static bool IsRealNumberType(this Type type)
    {
        if (type == null) return false;

        switch (GetTypeCode(type))
        {
            case TypeCode.Decimal:
            case TypeCode.Double:
            case TypeCode.Single:
                return true;

            case TypeCode.Object:
                if (type.IsNullableType())
                {
                    return IsNumericType(Nullable.GetUnderlyingType(type));
                }
                return false;
        }
        return false;
    }

    public static Type GetTypeWithGenericInterfaceOf(this Type type, Type genericInterfaceType)
    {
        foreach (var t in type.GetInterfaces())
        {
            if (t.IsGenericType && t.GetGenericTypeDefinition() == genericInterfaceType)
                return t;
        }

        if (!type.IsGenericType) return null;

        var genericType = type.FirstGenericType();
        return genericType.GetGenericTypeDefinition() == genericInterfaceType
            ? genericType
            : null;
    }

    public static bool HasAnyTypeDefinitionsOf(this Type genericType, params Type[] theseGenericTypes)
    {
        if (!genericType.IsGenericType) return false;

        var genericTypeDefinition = genericType.GetGenericTypeDefinition();

        foreach (var thisGenericType in theseGenericTypes)
        {
            if (genericTypeDefinition == thisGenericType)
                return true;
        }

        return false;
    }

    public static Type[] GetGenericArgumentsIfBothHaveSameGenericDefinitionTypeAndArguments(
        this Type assignableFromType, Type typeA, Type typeB)
    {
        var typeAInterface = typeA.GetTypeWithGenericInterfaceOf(assignableFromType);
        if (typeAInterface == null) return null;

        var typeBInterface = typeB.GetTypeWithGenericInterfaceOf(assignableFromType);
        if (typeBInterface == null) return null;

        var typeAGenericArgs = typeAInterface.GetGenericArguments();
        var typeBGenericArgs = typeBInterface.GetGenericArguments();

        if (typeAGenericArgs.Length != typeBGenericArgs.Length) return null;

        for (var i = 0; i < typeBGenericArgs.Length; i++)
        {
            if (typeAGenericArgs[i] != typeBGenericArgs[i])
            {
                return null;
            }
        }

        return typeAGenericArgs;
    }

    public static TypePair GetGenericArgumentsIfBothHaveConvertibleGenericDefinitionTypeAndArguments(
        this Type assignableFromType, Type typeA, Type typeB)
    {
        var typeAInterface = typeA.GetTypeWithGenericInterfaceOf(assignableFromType);
        if (typeAInterface == null) return null;

        var typeBInterface = typeB.GetTypeWithGenericInterfaceOf(assignableFromType);
        if (typeBInterface == null) return null;

        var typeAGenericArgs = typeAInterface.GetGenericArguments();
        var typeBGenericArgs = typeBInterface.GetGenericArguments();

        if (typeAGenericArgs.Length != typeBGenericArgs.Length) return null;

        for (var i = 0; i < typeBGenericArgs.Length; i++)
        {
            if (!AreAllStringOrValueTypes(typeAGenericArgs[i], typeBGenericArgs[i]))
            {
                return null;
            }
        }

        return new TypePair(typeAGenericArgs, typeBGenericArgs);
    }

    public static bool AreAllStringOrValueTypes(params Type[] types)
    {
        foreach (var type in types)
        {
            if (!(type == typeof(string) || type.IsValueType)) return false;
        }
        return true;
    }

    static Dictionary<Type, EmptyCtorDelegate> ConstructorMethods = new();
    public static EmptyCtorDelegate GetConstructorMethod(Type type)
    {
        if (ConstructorMethods.TryGetValue(type, out var emptyCtorFn)) 
            return emptyCtorFn;

        emptyCtorFn = GetConstructorMethodToCache(type);

        Dictionary<Type, EmptyCtorDelegate> snapshot, newCache;
        do
        {
            snapshot = ConstructorMethods;
            newCache = new Dictionary<Type, EmptyCtorDelegate>(ConstructorMethods) {
                [type] = emptyCtorFn
            };
        } while (!ReferenceEquals(
                     Interlocked.CompareExchange(ref ConstructorMethods, newCache, snapshot), snapshot));

        return emptyCtorFn;
    }

    static Dictionary<string, EmptyCtorDelegate> TypeNamesMap = new();
    public static EmptyCtorDelegate GetConstructorMethod(string typeName)
    {
        if (TypeNamesMap.TryGetValue(typeName, out var emptyCtorFn)) 
            return emptyCtorFn;

        var type = JsConfig.TypeFinder(typeName);
        if (type == null) return null;
        emptyCtorFn = GetConstructorMethodToCache(type);

        Dictionary<string, EmptyCtorDelegate> snapshot, newCache;
        do
        {
            snapshot = TypeNamesMap;
            newCache = new Dictionary<string, EmptyCtorDelegate>(TypeNamesMap) {
                [typeName] = emptyCtorFn
            };

        } while (!ReferenceEquals(
                     Interlocked.CompareExchange(ref TypeNamesMap, newCache, snapshot), snapshot));

        return emptyCtorFn;
    }

    public static EmptyCtorDelegate GetConstructorMethodToCache(Type type)
    {
        if (type == typeof(string))
            return () => string.Empty;

        if (type.IsInterface)
        {
            if (type.HasGenericType())
            {
                var genericType = type.GetTypeWithGenericTypeDefinitionOfAny(
                    typeof(IDictionary<,>));

                if (genericType != null)
                {
                    var keyType = genericType.GetGenericArguments()[0];
                    var valueType = genericType.GetGenericArguments()[1];
                    return GetConstructorMethodToCache(typeof(Dictionary<,>).MakeGenericType(keyType, valueType));
                }

                genericType = type.GetTypeWithGenericTypeDefinitionOfAny(
                    typeof(IEnumerable<>),
                    typeof(ICollection<>),
                    typeof(IList<>));

                if (genericType != null)
                {
                    var elementType = genericType.GetGenericArguments()[0];
                    return GetConstructorMethodToCache(typeof(List<>).MakeGenericType(elementType));
                }
            }
        }
        else if (type.IsArray)
        {
            return () => Array.CreateInstance(type.GetElementType(), 0);
        }
        else if (type.IsGenericTypeDefinition)
        {
            var genericArgs = type.GetGenericArguments();
            var typeArgs = new Type[genericArgs.Length];
            for (var i = 0; i < genericArgs.Length; i++)
                typeArgs[i] = typeof(object);

            var realizedType = type.MakeGenericType(typeArgs);

            return realizedType.CreateInstance;
        }

        return ReflectionOptimizer.Instance.CreateConstructor(type);
    }

    private static class TypeMeta<T>
    {
        public static readonly EmptyCtorDelegate EmptyCtorFn;
        static TypeMeta()
        {
            EmptyCtorFn = GetConstructorMethodToCache(typeof(T));
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static object CreateInstance<T>()
    {
        return TypeMeta<T>.EmptyCtorFn();
    }

    /// <summary>
    /// Creates a new instance of type. 
    /// First looks at JsConfig.ModelFactory before falling back to CreateInstance
    /// </summary>
    public static T New<T>(this Type type)
    {
        var factoryFn = JsConfig.ModelFactory(type)
                        ?? GetConstructorMethod(type);
        return (T)factoryFn();
    }

    /// <summary>
    /// Creates a new instance of type. 
    /// First looks at JsConfig.ModelFactory before falling back to CreateInstance
    /// </summary>
    public static object New(this Type type)
    {
        var factoryFn = JsConfig.ModelFactory(type)
                        ?? GetConstructorMethod(type);
        return factoryFn();
    }

    /// <summary>
    /// Creates a new instance from the default constructor of type
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static object CreateInstance(this Type type)
    {
        if (type == null)
            return null;

        var ctorFn = GetConstructorMethod(type);
        return ctorFn();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static T CreateInstance<T>(this Type type)
    {
        if (type == null)
            return default(T);

        var ctorFn = GetConstructorMethod(type);
        return (T)ctorFn();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static object CreateInstance(string typeName)
    {
        if (typeName == null)
            return null;

        var ctorFn = GetConstructorMethod(typeName);
        return ctorFn();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Module GetModule(this Type type)
    {
        if (type == null)
            return null;

        return type.Module;
    }

    public static PropertyInfo[] GetAllProperties(this Type type)
    {
        if (type.IsInterface)
        {
            var propertyInfos = new List<PropertyInfo>();

            var considered = new List<Type>();
            var queue = new Queue<Type>();
            considered.Add(type);
            queue.Enqueue(type);

            while (queue.Count > 0)
            {
                var subType = queue.Dequeue();
                foreach (var subInterface in subType.GetInterfaces())
                {
                    if (considered.Contains(subInterface)) continue;

                    considered.Add(subInterface);
                    queue.Enqueue(subInterface);
                }

                var typeProperties = subType.GetTypesProperties();

                var newPropertyInfos = typeProperties
                    .Where(x => !propertyInfos.Contains(x));

                propertyInfos.InsertRange(0, newPropertyInfos);
            }

            return propertyInfos.ToArray();
        }

        return type.GetTypesProperties()
            .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties
            .ToArray();
    }

    public static PropertyInfo[] GetPublicProperties(this Type type)
    {
        if (type.IsInterface)
        {
            var propertyInfos = new List<PropertyInfo>();

            var considered = new List<Type>();
            var queue = new Queue<Type>();
            considered.Add(type);
            queue.Enqueue(type);

            while (queue.Count > 0)
            {
                var subType = queue.Dequeue();
                foreach (var subInterface in subType.GetInterfaces())
                {
                    if (considered.Contains(subInterface)) continue;

                    considered.Add(subInterface);
                    queue.Enqueue(subInterface);
                }

                var typeProperties = subType.GetTypesPublicProperties();

                var newPropertyInfos = typeProperties
                    .Where(x => !propertyInfos.Contains(x));

                propertyInfos.InsertRange(0, newPropertyInfos);
            }

            return propertyInfos.ToArray();
        }

        return type.GetTypesPublicProperties()
            .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties
            .ToArray();
    }

    public const string DataMember = "DataMemberAttribute";

    internal static string[] IgnoreAttributesNamed = new[] {
        "IgnoreDataMemberAttribute",
        "JsonIgnoreAttribute"
    };

    internal static void Reset()
    {
        IgnoreAttributesNamed = new[] {
            "IgnoreDataMemberAttribute",
            "JsonIgnoreAttribute"
        };

        try
        {
            JsConfig<Type>.SerializeFn = x => x?.ToString();
            JsConfig<MethodInfo>.SerializeFn = x => x?.ToString();
            JsConfig<PropertyInfo>.SerializeFn = x => x?.ToString();
            JsConfig<FieldInfo>.SerializeFn = x => x?.ToString();
            JsConfig<MemberInfo>.SerializeFn = x => x?.ToString();
            JsConfig<ParameterInfo>.SerializeFn = x => x?.ToString();
        }
        catch (Exception e)
        {
            Tracer.Instance.WriteError("ReflectionExtensions JsConfig<Type>", e);
        }
    }

    /// <summary>
    /// Includes unfiltered serializable properties suitable for caching.
    /// Need to exclude JsConfig.ShouldExcludePropertyType() if serializing at runtime
    /// </summary>
    public static PropertyInfo[] GetAllSerializableProperties(this Type type)
    {
        var properties = type.IsDto()
            ? type.GetAllProperties()
            : type.GetPublicProperties();
        return properties.OnlySerializableProperties(type);
    }

    public static PropertyInfo[] GetSerializableProperties(this Type type) =>
        type.GetAllSerializableProperties().Where(x => !JsConfig.ShouldExcludePropertyType(x.PropertyType)).ToArray();

    public static PropertyInfo[] OnlySerializableProperties(this PropertyInfo[] properties, Type type = null)
    {
        var isDto = type.IsDto();
        var readableProperties = properties.Where(x => x.GetGetMethod(nonPublic: isDto) != null);

        if (isDto)
        {
            return readableProperties.Where(attr =>
                attr.HasAttribute<DataMemberAttribute>()).ToArray();
        }

        // else return those properties that are not decorated with IgnoreDataMember
        return readableProperties
            .Where(prop => prop.AllAttributes()
                .All(attr => {
                    var name = attr.GetType().Name;
                    return !IgnoreAttributesNamed.Contains(name);
                }))
            .ToArray();
    }

    public static Func<object, string, object, object> GetOnDeserializing<T>()
    {
        var method = typeof(T).GetMethodInfo("OnDeserializing");
        if (method == null || method.ReturnType != typeof(object))
            return null;
        var obj = (Func<T, string, object, object>)method.CreateDelegate(typeof(Func<T, string, object, object>));
        return (instance, memberName, value) => obj((T)instance, memberName, value);
    }

    public static FieldInfo[] GetSerializableFields(this Type type)
    {
        if (type.IsDto())
        {
            return type.GetAllFields().Where(f =>
                f.HasAttribute<DataMemberAttribute>()).ToArray();
        }

        var config = JsConfig.GetConfig();

        if (!config.IncludePublicFields)
            return TypeConstants.EmptyFieldInfoArray;

        var publicFields = type.GetPublicFields();

        // else return those properties that are not decorated with IgnoreDataMember
        return publicFields
            .Where(prop => prop.AllAttributes()
                .All(attr => !IgnoreAttributesNamed.Contains(attr.GetType().Name)))
            .Where(prop => !config.ExcludeTypes.Contains(prop.FieldType))
            .ToArray();
    }

    public static DataContractAttribute GetDataContract(this Type type)
    {
        var dataContract = type.FirstAttribute<DataContractAttribute>();

        if (dataContract == null && Env.IsMono)
            return PclExport.Instance.GetWeakDataContract(type);

        return dataContract;
    }

    public static DataMemberAttribute GetDataMember(this PropertyInfo pi)
    {
        var dataMember = pi.AllAttributes(typeof(DataMemberAttribute))
            .FirstOrDefault() as DataMemberAttribute;

        if (dataMember == null && Env.IsMono)
            return PclExport.Instance.GetWeakDataMember(pi);

        return dataMember;
    }

    public static DataMemberAttribute GetDataMember(this FieldInfo pi)
    {
        var dataMember = pi.AllAttributes(typeof(DataMemberAttribute))
            .FirstOrDefault() as DataMemberAttribute;

        if (dataMember == null && Env.IsMono)
            return PclExport.Instance.GetWeakDataMember(pi);

        return dataMember;
    }

    public static string GetDataMemberName(this PropertyInfo pi)
    {
        var attr = pi.GetDataMember();
        return attr?.Name;
    }

    public static string GetDataMemberName(this FieldInfo fi)
    {
        var attr = fi.GetDataMember();
        return attr?.Name;
    }
}