



using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace InteropDotNet
{
    static class InteropRuntimeImplementer
    {
        public static T CreateInstance<T>() where T : class
        {
            var interfaceType = typeof(T);
            if (!typeof(T).IsInterface)
                throw new Exception($"The type {interfaceType.Name} should be an interface");
            if (!interfaceType.IsPublic)
                throw new Exception($"The interface {interfaceType.Name} should be public");

            var assemblyName = GetAssemblyName(interfaceType);
            
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName);

            var typeName = GetImplementationTypeName(assemblyName, interfaceType);
            var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public,
                typeof(object), new[] { interfaceType });
            var methods = BuildMethods(interfaceType);

            ImplementDelegates(assemblyName, moduleBuilder, methods);
            ImplementFields(typeBuilder, methods);
            ImplementMethods(typeBuilder, methods);
            ImplementConstructor(typeBuilder, methods);

            var implementationType = typeBuilder.CreateType();
            return (T)Activator.CreateInstance(implementationType, LibraryLoader.Instance);
        }

        #region Main steps

        private static MethodItem[] BuildMethods(Type interfaceType)
        {
            var methodInfoArray = interfaceType.GetMethods();
            var methods = new MethodItem[methodInfoArray.Length];
            for (int i = 0; i < methodInfoArray.Length; i++)
            {
                methods[i] = new MethodItem { Info = methodInfoArray[i] };
                var attribute = GetRuntimeDllImportAttribute(methodInfoArray[i]);
                if (attribute == null)
                    throw new Exception(
                        $"Method '{methodInfoArray[i].Name}' of interface '{interfaceType.Name}' should be marked with the RuntimeDllImport attribute");
                methods[i].DllImportAttribute = attribute;
            }
            return methods;
        }

        private static void ImplementDelegates(string assemblyName, ModuleBuilder moduleBuilder, IEnumerable<MethodItem> methods)
        {
            foreach (var method in methods)
                method.DelegateType = ImplementMethodDelegate(assemblyName, moduleBuilder, method);
        }

        private static Type ImplementMethodDelegate(string assemblyName, ModuleBuilder moduleBuilder, MethodItem method)
        {
            
            const MethodAttributes methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig |
                MethodAttributes.NewSlot | MethodAttributes.Virtual;

            
            var delegateName = GetDelegateName(assemblyName, method.Info);
            var delegateBuilder = moduleBuilder.DefineType(delegateName,
                TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.Sealed, typeof(MulticastDelegate));

            
            var importAttribute = method.DllImportAttribute;
            var attributeCtor = typeof(UnmanagedFunctionPointerAttribute).GetConstructor(new[] { typeof(CallingConvention) });
            if (attributeCtor == null)
                throw new Exception("There is no the target constructor of the UnmanagedFunctionPointerAttribute");
            var attributeBuilder = new CustomAttributeBuilder(attributeCtor, new object[] { importAttribute.CallingConvention },
                new[]
                {
                    typeof(UnmanagedFunctionPointerAttribute).GetField("CharSet"),
                    typeof(UnmanagedFunctionPointerAttribute).GetField("BestFitMapping"),
                    typeof(UnmanagedFunctionPointerAttribute).GetField("ThrowOnUnmappableChar"),
                    typeof(UnmanagedFunctionPointerAttribute).GetField("SetLastError")
                },
                new object[]
                {
                    importAttribute.CharSet,
                    importAttribute.BestFitMapping,
                    importAttribute.ThrowOnUnmappableChar,
                    importAttribute.SetLastError
                });
            delegateBuilder.SetCustomAttribute(attributeBuilder);


            
            var ctorBuilder = delegateBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig |
                MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard,
                new[] { typeof(object), typeof(IntPtr) });
            ctorBuilder.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);
            ctorBuilder.DefineParameter(1, ParameterAttributes.HasDefault, "object");
            ctorBuilder.DefineParameter(2, ParameterAttributes.HasDefault, "method");

            
            var parameters = GetParameterInfoArray(method.Info);
            var methodBuilder = DefineMethod(delegateBuilder, "Invoke", methodAttributes, method.ReturnType, parameters);
            methodBuilder.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);

            
            parameters = GetParameterInfoArray(method.Info, InfoArrayMode.BeginInvoke);
            methodBuilder = DefineMethod(delegateBuilder, "BeginInvoke", methodAttributes, typeof(IAsyncResult), parameters);
            methodBuilder.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);

            
            parameters = GetParameterInfoArray(method.Info, InfoArrayMode.EndInvoke);
            methodBuilder = DefineMethod(delegateBuilder, "EndInvoke", methodAttributes, method.ReturnType, parameters);
            methodBuilder.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);

            
            return delegateBuilder.CreateType();
        }

        private static void ImplementFields(TypeBuilder typeBuilder, IEnumerable<MethodItem> methods)
        {
            foreach (var method in methods)
            {
                var fieldName = method.Info.Name + "Field";
                var fieldBuilder = typeBuilder.DefineField(fieldName, method.DelegateType, FieldAttributes.Private);
                method.FieldInfo = fieldBuilder;
            }
        }

        private static void ImplementMethods(TypeBuilder typeBuilder, IEnumerable<MethodItem> methods)
        {
            foreach (var method in methods)
            {
                var infoArray = GetParameterInfoArray(method.Info);
                var methodBuilder = DefineMethod(typeBuilder, method.Name,
                    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot |
                    MethodAttributes.Final | MethodAttributes.Virtual,
                    method.ReturnType, infoArray);

                var ilGen = methodBuilder.GetILGenerator();

                
                ilGen.Emit(OpCodes.Ldarg_0);
                
                ilGen.Emit(OpCodes.Ldfld, method.FieldInfo);
                
                for (int i = 0; i < infoArray.Length; i++)
                    LdArg(ilGen, i + 1);
                
                ilGen.Emit(OpCodes.Callvirt, method.DelegateType.GetMethod("Invoke"));
                
                ilGen.Emit(OpCodes.Ret);

                
                typeBuilder.DefineMethodOverride(methodBuilder, method.Info);
            }
        }

        private static void ImplementConstructor(TypeBuilder typeBuilder, MethodItem[] methods)
        {
            
            var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public,
                CallingConventions.Standard, new[] { typeof(LibraryLoader) });
            ctorBuilder.DefineParameter(1, ParameterAttributes.HasDefault, "loader");
            if (typeBuilder.BaseType == null)
                throw new Exception("There is no a BaseType of typeBuilder");
            var baseCtor = typeBuilder.BaseType.GetConstructor(new Type[0]);
            if (baseCtor == null)
                throw new Exception("There is no a default constructor of BaseType of typeBuilder");

            
            var libraries = new List<string>();
            foreach (var method in methods)
            {
                var libraryName = method.DllImportAttribute.LibraryFileName;
                if (!libraries.Contains(libraryName))
                    libraries.Add(libraryName);
            }

            
            var ilGen = ctorBuilder.GetILGenerator();
            
            for (int i = 0; i < libraries.Count; i++)
                ilGen.DeclareLocal(typeof(IntPtr));
            
            ilGen.DeclareLocal(typeof(IntPtr));
            
            ilGen.Emit(OpCodes.Ldarg_0);
            
            ilGen.Emit(OpCodes.Call, baseCtor);
            for (int i = 0; i < libraries.Count; i++)
            {
                
                var library = libraries[i];
                
                ilGen.Emit(OpCodes.Ldarg_1);
                
                ilGen.Emit(OpCodes.Ldstr, library);
                
                ilGen.Emit(OpCodes.Ldnull);
                
                ilGen.Emit(OpCodes.Callvirt, typeof(LibraryLoader).GetMethod("LoadLibrary"));
                
                ilGen.Emit(OpCodes.Stloc, i);
            }
            foreach (var method in methods)
            {
                
                var libraryIndex = libraries.IndexOf(method.DllImportAttribute.LibraryFileName);
                var methodName = method.DllImportAttribute.EntryPoint ?? method.Info.Name;
                
                ilGen.Emit(OpCodes.Ldarg_1);
                
                ilGen.Emit(OpCodes.Ldloc, libraryIndex);
                
                ilGen.Emit(OpCodes.Ldstr, methodName);
                
                ilGen.Emit(OpCodes.Callvirt, typeof(LibraryLoader).GetMethod("GetProcAddress"));
                
                ilGen.Emit(OpCodes.Stloc, libraries.Count);
                
                ilGen.Emit(OpCodes.Ldarg_0);
                
                ilGen.Emit(OpCodes.Ldloc_1);
                
                ilGen.Emit(OpCodes.Ldtoken, method.DelegateType);
                
                ilGen.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"));
                
                ilGen.Emit(OpCodes.Call, typeof(Marshal).GetMethod("GetDelegateForFunctionPointer",
                    new[] { typeof(IntPtr), typeof(Type) }));
                
                ilGen.Emit(OpCodes.Castclass, method.DelegateType);
                
                ilGen.Emit(OpCodes.Stfld, method.FieldInfo);
            }
            
            ilGen.Emit(OpCodes.Ret);
        }

        #endregion

        #region Reflection and emit helpers

        private static RuntimeDllImportAttribute GetRuntimeDllImportAttribute(MethodInfo methodInfo)
        {
            var attributes = methodInfo.GetCustomAttributes(typeof(RuntimeDllImportAttribute), true);
            if (attributes.Length == 0)
                throw new Exception($"RuntimeDllImportAttribute for method '{methodInfo.Name}' not found");
            return (RuntimeDllImportAttribute)attributes[0];
        }

        private static void LdArg(ILGenerator ilGen, int index)
        {
            switch (index)
            {
                case 0:
                    ilGen.Emit(OpCodes.Ldarg_0);
                    break;
                case 1:
                    ilGen.Emit(OpCodes.Ldarg_1);
                    break;
                case 2:
                    ilGen.Emit(OpCodes.Ldarg_2);
                    break;
                case 3:
                    ilGen.Emit(OpCodes.Ldarg_3);
                    break;
                default:
                    ilGen.Emit(OpCodes.Ldarg, index);
                    break;
            }
        }

        private static MethodBuilder DefineMethod(TypeBuilder typeBuilder, string name,
           MethodAttributes attributes, Type returnType, LightParameterInfo[] infoArray)
        {
            var methodBuilder = typeBuilder.DefineMethod(name, attributes, returnType, GetParameterTypeArray(infoArray));
            for (int parameterIndex = 0; parameterIndex < infoArray.Length; parameterIndex++)
                methodBuilder.DefineParameter(parameterIndex + 1,
                    infoArray[parameterIndex].Attributes, infoArray[parameterIndex].Name);
            return methodBuilder;
        }

        #endregion

        #region Method helpers

        private class MethodItem
        {
            public MethodInfo Info { get; set; }
            public RuntimeDllImportAttribute DllImportAttribute { get; set; }

            public Type DelegateType { get; set; }
            public FieldInfo FieldInfo { get; set; }

            public string Name { get { return Info.Name; } }
            public Type ReturnType { get { return Info.ReturnType; } }
        }

        private class LightParameterInfo
        {
            public LightParameterInfo(ParameterInfo info)
            {
                Type = info.ParameterType;
                Name = info.Name;
                Attributes = info.Attributes;
            }

            public LightParameterInfo(Type type, string name)
            {
                Type = type;
                Name = name;
                Attributes = ParameterAttributes.HasDefault;
            }

            public Type Type { get; }
            public string Name { get; }
            public ParameterAttributes Attributes { get; }
        }

        private enum InfoArrayMode
        {
            Invoke, BeginInvoke, EndInvoke
        }

        private static LightParameterInfo[] GetParameterInfoArray(MethodInfo methodInfo, InfoArrayMode mode = InfoArrayMode.Invoke)
        {
            var parameters = methodInfo.GetParameters();
            var infoList = new List<LightParameterInfo>();
            foreach (var t in parameters)
                if (mode != InfoArrayMode.EndInvoke || t.ParameterType.IsByRef)
                    infoList.Add(new LightParameterInfo(t));

            switch (mode)
            {
                case InfoArrayMode.BeginInvoke:
                    infoList.Add(new LightParameterInfo(typeof(AsyncCallback), "callback"));
                    infoList.Add(new LightParameterInfo(typeof(object), "object"));
                    break;
                case InfoArrayMode.EndInvoke:
                    infoList.Add(new LightParameterInfo(typeof(IAsyncResult), "result"));
                    break;
            }

            var infoArray = new LightParameterInfo[infoList.Count];
            for (var i = 0; i < infoList.Count; i++)
                infoArray[i] = infoList[i];
            return infoArray;
        }

        private static Type[] GetParameterTypeArray(LightParameterInfo[] infoArray)
        {
            var typeArray = new Type[infoArray.Length];
            for (int i = 0; i < infoArray.Length; i++)
                typeArray[i] = infoArray[i].Type;
            return typeArray;
        }

        #endregion

        #region Name helpers

        private static string GetAssemblyName(Type interfaceType)
        {
            return $"InteropRuntimeImplementer.{GetSubstantialName(interfaceType)}Instance";
        }

        private static string GetImplementationTypeName(string assemblyName, Type interfaceType)
        {
            return $"{assemblyName}.{GetSubstantialName(interfaceType)}Implementation";
        }

        private static string GetSubstantialName(Type interfaceType)
        {
            var name = interfaceType.Name;
            if (name.StartsWith("I"))
                name = name.Substring(1);
            return name;
        }

        private static string GetDelegateName(string assemblyName, MethodInfo methodInfo)
        {
            return $"{assemblyName}.{methodInfo.Name}Delegate";
        }

        #endregion
    }
}
