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

namespace ZeroLevel.Services
{
    public delegate object DecorateMethodCallHandler(string methodName, params object[] args);

    public class ProxyTypeBuilder
    {
        private enum MethodType
        {
            DirectProxy,
            AbstractOverrideProxy
        }

        #region Fields
        private readonly AssemblyBuilder _assemblyBuilder;
        private readonly ModuleBuilder _moduleBuilder;
        private readonly TypeBuilder _typeBuilder;
        private readonly FieldBuilder _callbackField;
        private readonly IDictionary<string, FieldBuilder> _fields = new Dictionary<string, FieldBuilder>();

        private readonly Type _parentType;
        private readonly Type[] _interfaces;
        #endregion

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="typeName">Название создаваемого типа</param>
        /// <param name="parentType">Тип от которого должен наследоваться создаваемый тип, если null, наследование от object</param>
        /// <param name="interfaces">Интерфейсы которые должен реализовать создаваемый тип</param>
        public ProxyTypeBuilder(string typeName, Type parentType, params Type[] interfaces)
        {
            var newAssemblyName = new AssemblyName(Guid.NewGuid().ToString());
            _assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(newAssemblyName, AssemblyBuilderAccess.Run);
            _moduleBuilder = _assemblyBuilder.DefineDynamicModule(newAssemblyName.Name);
            _typeBuilder = _moduleBuilder.DefineType(typeName,
                TypeAttributes.Public |
                TypeAttributes.Class |
                TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass |
                TypeAttributes.BeforeFieldInit | TypeAttributes.Serializable |
                TypeAttributes.AutoLayout, parentType ?? typeof(object), interfaces);
            // Поле для хранения метода обратного вызова
            _callbackField = _typeBuilder.DefineField("_callbackHandler", CreateDecorateMethodCallHandlerDelegate(_moduleBuilder), FieldAttributes.Private);
            _interfaces = interfaces;
            _parentType = parentType!;
        }
        /// <summary>
        /// Собирает конечный тип
        /// </summary>
        /// <returns>Готовый тип</returns>
        public Type Complete()
        {
            // Создание конструктора
            CreateConstructor();
            // Реализация методов интерфейсов
            ProceedInterfaces();
            // Реализация абстрактных методов родительского класса
            ProceedParentAbstractMethods();
            // Сборка типа
            var type = _typeBuilder.CreateType();
            // Результат
            return type;
        }

        #region Private members
        private readonly Dictionary<string, MethodBuilder> _methodsBuilderCachee = new Dictionary<string, MethodBuilder>();
        /// <summary>
        /// Создает конструктор принимающий метод обратного вызова
        /// </summary>
        private void CreateConstructor()
        {
            Type objType = Type.GetType("System.Object");
            ConstructorInfo objCtor = objType.GetConstructor(new Type[0]);
            ConstructorBuilder cBuilder =
                _typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                    CallingConventions.Standard,
                    new Type[1] { typeof(DecorateMethodCallHandler) });

            ILGenerator cil = cBuilder.GetILGenerator();
            cil.Emit(OpCodes.Ldarg_0);
            cil.Emit(OpCodes.Call, objCtor);
            cil.Emit(OpCodes.Nop);
            cil.Emit(OpCodes.Nop);
            // Присвоение полю класса _callbackHandler аргумента конструктора
            cil.Emit(OpCodes.Ldarg_0);
            cil.Emit(OpCodes.Ldarg_1);
            cil.Emit(OpCodes.Stfld, _callbackField);
            cil.Emit(OpCodes.Nop);
            cil.Emit(OpCodes.Ret);
        }
        /// <summary>
        /// Создает декортаторы для вызовов интерфейсных методов
        /// </summary>
        private void ProceedInterfaces()
        {
            var list = new List<Type>();
            if (_interfaces != null && _interfaces.Length > 0)
            {
                list.AddRange(_interfaces);
                list.AddRange(GetInterfaces(_interfaces));
            }
            if (_parentType != null!)
            {
                list.AddRange(GetInterfaces(new Type[] { _parentType }));
            }
            list = list.Distinct().ToList();
            foreach (var interfaceType in list)
            {
                var properties = interfaceType.GetProperties();
                foreach (var property in properties)
                {
                    _fields.Add(property.Name, _typeBuilder.DefineField("_" + property.Name, property.PropertyType, FieldAttributes.Private));
                }
                foreach (var method in interfaceType.GetMethods())
                {
                    if (properties.Any(p => p.GetGetMethod() == method || p.GetSetMethod() == method))
                    {
                        continue;
                        //CreateProxyMethod(_typeBuilder, method, MethodType.DirectProxy, true);
                    }
                    else
                    {
                        CreateProxyMethod(_typeBuilder, method, MethodType.DirectProxy);
                    }
                }
                foreach (var propertyInfo in properties)
                {
                    AddProperty(_typeBuilder, propertyInfo.Name, propertyInfo.PropertyType);
                }
            }
        }

        private static IEnumerable<Type> GetInterfaces(Type[] sourceTypes)
        {
            var interfaces = new List<Type>();
            IEnumerable<Type> subList = sourceTypes;
            while (subList.Count() != 0)
            {
                subList = subList.SelectMany(i => i.GetInterfaces());
                interfaces.AddRange(subList);
            }
            return interfaces;
        }
        /// <summary>
        /// Создает декораторы для вызовов абстрактных методов родительского класса
        /// </summary>
        private void ProceedParentAbstractMethods()
        {
            if (_parentType != null!)
            {
                foreach (var method in _parentType.GetMethods())
                {
                    if (method.IsAbstract)
                    {
                        CreateProxyMethod(_typeBuilder, method, MethodType.AbstractOverrideProxy);
                    }
                }
            }
        }
        /// <summary>
        /// Создание прокси-метода, вызывающего метод обратного вызова, передавая ему аргументы для реального метода
        /// </summary>
        private void CreateProxyMethod(TypeBuilder typeBuilder, MethodInfo method, MethodType methodType, bool isPropertyAccessor = false)
        {
            var methodName = method.Name;
            var returnType = method.ReturnType;
            var parametersTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
            var hasReturnValue = returnType != typeof(void);
            MethodBuilder dynamicMethod = null!;
            switch (methodType)
            {
                case MethodType.DirectProxy:
                    dynamicMethod = typeBuilder.DefineMethod(methodName,
                    method.Attributes ^ MethodAttributes.Abstract,
                    method.CallingConvention,
                    returnType,
                    method.ReturnParameter.GetRequiredCustomModifiers(),
                    method.ReturnParameter.GetOptionalCustomModifiers(),
                    parametersTypes,
                    method.GetParameters().Select(p => p.GetRequiredCustomModifiers()).ToArray(),
                    method.GetParameters().Select(p => p.GetOptionalCustomModifiers()).ToArray());
                    break;
                case MethodType.AbstractOverrideProxy:
                    dynamicMethod = typeBuilder.DefineMethod(methodName,
                    MethodAttributes.Public |
                    MethodAttributes.ReuseSlot |
                    MethodAttributes.Virtual |
                    MethodAttributes.HideBySig |
                    MethodAttributes.Final,
                    method.CallingConvention,
                    returnType,
                    method.ReturnParameter.GetRequiredCustomModifiers(),
                    method.ReturnParameter.GetOptionalCustomModifiers(),
                    parametersTypes,
                    method.GetParameters().Select(p => p.GetRequiredCustomModifiers()).ToArray(),
                    method.GetParameters().Select(p => p.GetOptionalCustomModifiers()).ToArray());
                    break;
            }
            int index = 1;
            foreach (var p in method.GetParameters())
            {
                dynamicMethod.DefineParameter(index, ParameterAttributes.In, p.Name);
                index++;
            }
            var il = dynamicMethod.GetILGenerator();
            il.Emit(OpCodes.Nop);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, _callbackField);

            ParameterInfo[] parameters = method.GetParameters();
            // Массив для параметров оригинального метода
            LocalBuilder argArray = il.DeclareLocal(typeof(object[]));
            il.Emit(OpCodes.Ldc_I4, parameters.Length);
            il.Emit(OpCodes.Newarr, typeof(object));
            il.Emit(OpCodes.Stloc, argArray);
            // Заполнение массива
            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo info = parameters[i];
                il.Emit(OpCodes.Ldloc, argArray);
                il.Emit(OpCodes.Ldc_I4, i);
                il.Emit(OpCodes.Ldarg_S, i + 1);
                if (info.ParameterType.IsPrimitive || info.ParameterType.IsValueType)
                    il.Emit(OpCodes.Box, info.ParameterType);
                il.Emit(OpCodes.Stelem_Ref);
            }
            // Аргументы прокси-метода

            il.Emit(OpCodes.Ldstr, method.Name);
            il.Emit(OpCodes.Ldloc, argArray);
            // Вызов прокси-метода
            il.Emit(OpCodes.Callvirt, typeof(DecorateMethodCallHandler).GetMethod("Invoke"));
            // Возврат результата
            if (hasReturnValue)
            {
                if (returnType.IsValueType)
                {
                    il.Emit(OpCodes.Unbox_Any, returnType);
                }
            }
            else
            {
                il.Emit(OpCodes.Pop);
            }
            il.Emit(OpCodes.Ret);
        }
        /// <summary>
        /// Создает делегат вида public delegate object DecorateMethodCallHandler(string methodName, params object[] args);
        /// </summary>
        private static Type CreateDecorateMethodCallHandlerDelegate(ModuleBuilder moduleBuilder)
        {
            TypeBuilder typeBuilder = moduleBuilder.DefineType("DecorateMethodCallHandler",
                TypeAttributes.Class |
                TypeAttributes.Public |
                TypeAttributes.Sealed |
                TypeAttributes.AnsiClass |
                TypeAttributes.AutoClass, typeof(MulticastDelegate));
            var constructorBuilder =
                typeBuilder.DefineConstructor(MethodAttributes.RTSpecialName |
                MethodAttributes.HideBySig |
                MethodAttributes.Public,
                CallingConventions.Standard, new Type[] { typeof(object), typeof(System.IntPtr) });
            constructorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);
            var methodBuilder = typeBuilder.DefineMethod("Invoke", MethodAttributes.Public |
                MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, typeof(object),
                new[] { typeof(string), typeof(object[]) });
            methodBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);
            return typeBuilder.CreateType();
        }

        private static void AddProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)
        {
            var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
            var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);

            var getMethod = typeBuilder.DefineMethod("get_" + propertyName,
                MethodAttributes.Public |
                MethodAttributes.SpecialName |
                MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
            var getMethodIL = getMethod.GetILGenerator();
            getMethodIL.Emit(OpCodes.Ldarg_0);
            getMethodIL.Emit(OpCodes.Ldfld, fieldBuilder);
            getMethodIL.Emit(OpCodes.Ret);

            var setMethod = typeBuilder.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public |
                  MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig,
                  null, new[] { propertyType });
            var setMethodIL = setMethod.GetILGenerator();
            Label modifyProperty = setMethodIL.DefineLabel();
            Label exitSet = setMethodIL.DefineLabel();

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

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

            propertyBuilder.SetGetMethod(getMethod);
            propertyBuilder.SetSetMethod(setMethod);
        }
        #endregion
    }

    public delegate object RemoteProcedureCallHandler(string contractName, string methodName, string returnTypeFullName, params object[] args);

    internal static class DynamicProxyGenerator
    {
        #region Private Fields
        private readonly static IDictionary<string, ModuleBuilder> _builders = new Dictionary<string, ModuleBuilder>();
        private readonly static IDictionary<Type, Type> _types = new Dictionary<Type, Type>();
        private readonly static object _lockObject = new object();
        #endregion
        /*
        #region Public Methods
        // Note that calling this method will cause any further
        // attempts to generate an interface to fail
        internal static void Save()
        {
            foreach (var builder in _builders.Select(b => b.Value))
            {
                var ass = (AssemblyBuilder)builder.Assembly;
                try
                {
                    ass.Save(ass.GetName().Name + ".dll");
                }
                catch { }
            }
        }
        #endregion
        */
        #region Private Methods
        /// <summary>
        /// Создание экземпляра прокси класса по интерфейсу <typeparamref name="T"/>
        /// </summary>
        /// <typeparam name="T">Интерфейс</typeparam>
        /// <param name="rpcHandler">Обработчик вызова метода</param>
        /// <returns>Экземпляр прокси класса</returns>
        internal static T CreateInterfaceInstance<T>(RemoteProcedureCallHandler rpcHandler)
        {
            var destType = GenerateInterfaceType<T>(rpcHandler);
            return (T)Activator.CreateInstance(destType);
        }
        /// <summary>
        /// Проверка корректности начальных условий
        /// </summary>
        /// <param name="sourceType">Тип интерфейса</param>
        /// <param name="mi">Метод-обработчик удаленного вызова</param>
        private static void Validate(Type sourceType, MethodInfo mi)
        {
            if (!sourceType.IsInterface)
                throw new ArgumentException("Type T is not an interface", "T");
            if ((mi.Attributes & MethodAttributes.Public) != MethodAttributes.Public)
                throw new ArgumentException("Method must be public.", "getter");
        }
        /// <summary>
        /// Получение динамического сбощика модуля
        /// </summary>
        /// <param name="sourceType">Тип интерфейса</param>
        /// <returns>ModuleBuilder</returns>
        private static ModuleBuilder CreateModuleBuilder(Type sourceType)
        {
            var orginalAssemblyName = sourceType.Assembly.GetName().Name;
            ModuleBuilder moduleBuilder;
            if (!_builders.TryGetValue(orginalAssemblyName, out moduleBuilder))
            {
                var newAssemblyName = new AssemblyName(Guid.NewGuid() + "." + orginalAssemblyName);
                var dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(newAssemblyName, AssemblyBuilderAccess.RunAndCollect);
                moduleBuilder = dynamicAssembly.DefineDynamicModule(newAssemblyName.Name);
                _builders.Add(orginalAssemblyName, moduleBuilder);
            }
            return moduleBuilder;
        }
        /// <summary>
        /// Создание списка списка интерфейсов, по которому требуется создать прокси класс
        /// В список входит указанный интерфейс <typeparamref name="T"/> и все интерфейсы от которых он унаследован
        /// </summary>
        /// <param name="sourceType">Тип интерфейса</param>
        /// <returns>Список интерфейсов</returns>
        internal static List<Type> GetDistinctInterfaces(Type sourceType)
        {
            var interfaces = new List<Type>();
            IEnumerable<Type> subList = new[] { sourceType };
            while (subList.Count() != 0)
            {
                interfaces.AddRange(subList);
                subList = subList.SelectMany(i => i.GetInterfaces());
            }
            return interfaces.Distinct().ToList();
        }
        /// <summary>
        /// Добавление нового метода в прокси тип
        /// </summary>
        /// <param name="typeBuilder">Сборщик типа</param>
        /// <param name="method">Описание метода</param>
        /// <param name="handler">Прокси метод</param>
        /// <param name="contractName">Контракт к которому относится метод (Тип интерфейса)</param>
        private static void AppendMethodToProxy(TypeBuilder typeBuilder, MethodInfo method, RemoteProcedureCallHandler handler, string contractName)
        {
            string methodName = method.Name;
            Type retType = method.ReturnType;
            bool hasReturnValue = retType != typeof(void);

            var newMethod = typeBuilder.DefineMethod(methodName,
                method.Attributes ^ MethodAttributes.Abstract,
                method.CallingConvention,
                retType,
                method.ReturnParameter.GetRequiredCustomModifiers(),
                method.ReturnParameter.GetOptionalCustomModifiers(),
                method.GetParameters().Select(p => p.ParameterType).ToArray(),
                method.GetParameters().Select(p => p.GetRequiredCustomModifiers()).ToArray(),
                method.GetParameters().Select(p => p.GetOptionalCustomModifiers()).ToArray()
                );

            var il = newMethod.GetILGenerator();

            /* Type exType = typeof(Exception);
             ConstructorInfo exCtorInfo = exType.GetConstructor(new Type[] { typeof(string) });
             MethodInfo exToStrMI = exType.GetMethod("ToString");
             MethodInfo writeLineMI = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string), typeof(object) });
             LocalBuilder tmp2 = il.DeclareLocal(exType);*/

            ParameterInfo[] parameters = method.GetParameters();
            // Массив для параметров оригинального метода
            LocalBuilder argArray = il.DeclareLocal(typeof(object[]));
            il.Emit(OpCodes.Ldc_I4, parameters.Length);
            il.Emit(OpCodes.Newarr, typeof(object));
            il.Emit(OpCodes.Stloc, argArray);
            // Заполнение массива
            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo info = parameters[i];
                il.Emit(OpCodes.Ldloc, argArray);
                il.Emit(OpCodes.Ldc_I4, i);
                il.Emit(OpCodes.Ldarg_S, i + 1);
                if (info.ParameterType.IsPrimitive || info.ParameterType.IsValueType)
                    il.Emit(OpCodes.Box, info.ParameterType);
                il.Emit(OpCodes.Stelem_Ref);
            }
            // Аргументы прокси-метода
            il.Emit(OpCodes.Ldstr, contractName);
            il.Emit(OpCodes.Ldstr, methodName);
            if (hasReturnValue)
            {
                il.Emit(OpCodes.Ldstr, retType.FullName);
            }
            else
            {
                il.Emit(OpCodes.Ldstr, typeof(void).FullName);
            }
            il.Emit(OpCodes.Ldloc, argArray);
            // Вызов прокси-метода

            // Label exBlock = il.BeginExceptionBlock();
            il.EmitCall(OpCodes.Call, handler.GetMethodInfo(), null);
            /*
            il.Emit(OpCodes.Stloc_S, tmp2);
            il.Emit(OpCodes.Ldstr, "Caught {0}");
            il.Emit(OpCodes.Ldloc_S, tmp2);
            il.EmitCall(OpCodes.Callvirt, exToStrMI, null);
            il.EmitCall(OpCodes.Call, writeLineMI, null);
            il.Emit(OpCodes.Ldc_I4_M1);
            il.EndExceptionBlock();   
            */
            // Возврат результата
            if (hasReturnValue)
            {
                if (retType.IsValueType)
                    il.Emit(OpCodes.Unbox_Any, retType);
            }
            else
            {
                il.Emit(OpCodes.Pop);
            }
            il.Emit(OpCodes.Ret);
        }
        /// <summary>
        /// Создание прокси типа для интерфейса <typeparamref name="T"/>
        /// </summary>
        /// <typeparam name="T">Интерфейс</typeparam>
        /// <param name="rpcHandler">Прокси метод, для обработки вызова реального метода</param>
        /// <returns>Созданный тип</returns>
        private static Type GenerateInterfaceType<T>(RemoteProcedureCallHandler rpcHandler)
        {
            var sourceType = typeof(T);
            Type newType;
            if (_types.TryGetValue(sourceType, out newType))
                return newType;

            string sourceContractFullName = sourceType.FullName;
            // Make sure the same interface isn't implemented twice
            lock (_lockObject)
            {
                if (_types.TryGetValue(sourceType, out newType))
                    return newType;
                // Validation
                Validate(sourceType, rpcHandler.Method);
                // Module and Assembly Creation
                var moduleBuilder = CreateModuleBuilder(sourceType);
                var assemblyName = moduleBuilder.Assembly.GetName();
                // Create the TypeBuilder
                var typeBuilder = moduleBuilder.DefineType(sourceType.FullName, TypeAttributes.Public | TypeAttributes.Class, typeof(object), new[] { sourceType });
                // Enumerate interface inheritance hierarchy
                var interfaces = GetDistinctInterfaces(sourceType);
                // Create the methods
                foreach (var method in interfaces.SelectMany(i => i.GetMethods()))
                {
                    AppendMethodToProxy(typeBuilder, method, rpcHandler, sourceContractFullName);
                }
                // Create and return the defined type
                newType = typeBuilder.CreateType();
                _types.Add(sourceType, newType);
                return newType;
            }
        }
        #endregion
    }
}
