using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using VCommon.Collections;
using VCommon.Reflection;

// ReSharper disable StaticMemberInGenericType

namespace VCommon.VAutoMapper.Internals
{
    //REF: StackOverflow.com/a/3862241/2430943

    internal static class MiddleClass
    {
        private static readonly string NameSpace = $"{nameof(VCommon)}.{nameof(VCommon.VAutoMapper)}.MiddleClasses.{nameof(MiddleClass)}";
        private static readonly ModuleBuilder ModuleBuilder;

        static MiddleClass()
        {
            var name = new AssemblyName(NameSpace);
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
            ModuleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
        }

        public static Type Create(Type entity, Type dto)
        {
            var midPropNames = dto.GetPublicInstanceProperties().Select(p => p.Name).ToArray();
            var entityProps = entity.GetPublicInstanceProperties().ToDictionary(p => p.Name);

            var typeName = $"{NameSpace}.{entity.Assembly.FullName}!{entity.FullName}->{dto.Assembly.FullName}!{dto.FullName}";

            var tb = ModuleBuilder.DefineType(typeName,
                TypeAttributes.Public |
                TypeAttributes.Class |
                TypeAttributes.AutoLayout,
                null);

            tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            foreach (var propName in midPropNames)
            {
                if (false == entityProps.TryGetValue(propName, out var field))
                    throw new VAutoMapperException($"Missing property [{propName}] in [{entity.FullName}] for [{dto.FullName}]");

                var propertyType = field.PropertyType;
                if (false == propertyType.IsPrimitive)
                {
                    var primitiveCheck = propertyType.In(typeof(string), typeof(DateTime), typeof(Guid), typeof(decimal), typeof(TimeSpan));
                    var genericCheck = propertyType.IsGenericType && propertyType.GetGenericTypeDefinition().In(typeof(Nullable<>));

                    if (genericCheck && primitiveCheck)
                    {
                        //TODO: 复杂类型支持(递归?)
                        throw new NotSupportedException($"Only primitive property support. [{field}] in [{field.DeclaringType?.FullName ?? field.DeclaringType?.Name ?? "Unknown class"}]");
                    }
                }

                CreateProperty(tb, field.Name, propertyType);
            }

            var middleType = tb.CreateType();

            VAutoMapper.ConfigMapping(entity, middleType);
            VAutoMapper.ConfigMapping(middleType, dto);
            VAutoMapper.FlushMapper();

            return middleType;
        }

        private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            var propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
            var getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
            var getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            var setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                    MethodAttributes.Public |
                    MethodAttributes.SpecialName |
                    MethodAttributes.HideBySig,
                    null, new[] { propertyType });

            var setIl = setPropMthdBldr.GetILGenerator();
            var modifyProperty = setIl.DefineLabel();
            var exitSet = setIl.DefineLabel();

            setIl.MarkLabel(modifyProperty);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.MarkLabel(exitSet);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }
    }
}
