using System;
using System.Threading;
using System.Reflection;

using System.Reflection.Emit;
using System.Collections.Generic;
using NLua.Method;

namespace NLua
{
    class CodeGeneration
    {
        private readonly Dictionary<Type, LuaClassType> _classCollection = new Dictionary<Type, LuaClassType>();
        private readonly Dictionary<Type, Type> _delegateCollection = new Dictionary<Type, Type>();

#if !NETSTANDARD && !WINDOWS_UWP
        private Dictionary<Type, Type> eventHandlerCollection = new Dictionary<Type, Type>();
        private Type eventHandlerParent = typeof(LuaEventHandler);
        private Type delegateParent = typeof(LuaDelegate);
        private Type classHelper = typeof(LuaClassHelper);
        private AssemblyBuilder newAssembly;
        private ModuleBuilder newModule;
        private int luaClassNumber = 1;
#endif

        static CodeGeneration()
        {
        }

        private CodeGeneration()
        {
            // Create an assembly name
            var assemblyName = new AssemblyName();
            assemblyName.Name = "NLua_generatedcode";
            // Create a new assembly with one module.
#if NETCOREAPP
            newAssembly = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            newModule = newAssembly.DefineDynamicModule("NLua_generatedcode");
#elif !NETSTANDARD && !WINDOWS_UWP
            newAssembly = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            newModule = newAssembly.DefineDynamicModule("NLua_generatedcode");
#endif
        }

        /*
         * Singleton instance of the class
         */
        public static CodeGeneration Instance { get; } = new CodeGeneration();

        /*
         *  Generates an event handler that calls a Lua function
         */
        private Type GenerateEvent(Type eventHandlerType)
        {
#if NETSTANDARD || WINDOWS_UWP
            throw new NotImplementedException(" Emit not available on .NET Standard ");
#else
            string typeName;
            lock (this)
            {
                typeName = "LuaGeneratedClass" + luaClassNumber;
                luaClassNumber++;
            }

            // Define a public class in the assembly, called typeName
            var myType = newModule.DefineType(typeName, TypeAttributes.Public, eventHandlerParent);

            // Defines the handler method. Its signature is void(object, <subclassofEventArgs>)
            var paramTypes = new Type[2];
            paramTypes[0] = typeof(object);
            paramTypes[1] = eventHandlerType;
            var returnType = typeof(void);
            var handleMethod = myType.DefineMethod("HandleEvent", MethodAttributes.Public | MethodAttributes.HideBySig, returnType, paramTypes);

            // Emits the IL for the method. It loads the arguments
            // and calls the handleEvent method of the base class
            ILGenerator generator = handleMethod.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldarg_1);
            generator.Emit(OpCodes.Ldarg_2);
            var miGenericEventHandler = eventHandlerParent.GetMethod("HandleEvent");
            generator.Emit(OpCodes.Call, miGenericEventHandler);
            // returns
            generator.Emit(OpCodes.Ret);
            // creates the new type
            return myType.CreateType();
#endif
        }

        /*
         * Generates a type that can be used for instantiating a delegate
         * of the provided type, given a Lua function.
         */
        private Type GenerateDelegate(Type delegateType)
        {
#if NETSTANDARD || WINDOWS_UWP
            throw new NotImplementedException("GenerateDelegate is not available on Windows Store, please register your LuaDelegate type with Lua.RegisterLuaDelegateType( yourDelegate, theLuaDelegateHandler) ");
#else
            string typeName;
            lock (this)
            {
                typeName = "LuaGeneratedClass" + luaClassNumber;
                luaClassNumber++;
            }

            // Define a public class in the assembly, called typeName
            var myType = newModule.DefineType(typeName, TypeAttributes.Public, delegateParent);

            // Defines the delegate method with the same signature as the
            // Invoke method of delegateType
            var invokeMethod = delegateType.GetMethod("Invoke");
            var paramInfo = invokeMethod.GetParameters();
            var paramTypes = new Type[paramInfo.Length];
            var returnType = invokeMethod.ReturnType;

            // Counts out and ref params, for use later
            int nOutParams = 0;
            int nOutAndRefParams = 0;

            for (int i = 0; i < paramTypes.Length; i++)
            {
                paramTypes[i] = paramInfo[i].ParameterType;

                if ((!paramInfo[i].IsIn) && paramInfo[i].IsOut)
                    nOutParams++;

                if (paramTypes[i].IsByRef)
                    nOutAndRefParams++;
            }

            int[] refArgs = new int[nOutAndRefParams];
            var delegateMethod = myType.DefineMethod("CallFunction", invokeMethod.Attributes, returnType, paramTypes);

            // Generates the IL for the method
            ILGenerator generator = delegateMethod.GetILGenerator();
            generator.DeclareLocal(typeof(object[])); // original arguments
            generator.DeclareLocal(typeof(object[])); // with out-only arguments removed
            generator.DeclareLocal(typeof(int[])); // indexes of out and ref arguments

            if (!(returnType == typeof(void)))  // return value
                generator.DeclareLocal(returnType);
            else
                generator.DeclareLocal(typeof(object));

            // Initializes local variables
            generator.Emit(OpCodes.Ldc_I4, paramTypes.Length);
            generator.Emit(OpCodes.Newarr, typeof(object));
            generator.Emit(OpCodes.Stloc_0);
            generator.Emit(OpCodes.Ldc_I4, paramTypes.Length - nOutParams);
            generator.Emit(OpCodes.Newarr, typeof(object));
            generator.Emit(OpCodes.Stloc_1);
            generator.Emit(OpCodes.Ldc_I4, nOutAndRefParams);
            generator.Emit(OpCodes.Newarr, typeof(int));
            generator.Emit(OpCodes.Stloc_2);

            // Stores the arguments in the local variables
            for (int iArgs = 0, iInArgs = 0, iOutArgs = 0; iArgs < paramTypes.Length; iArgs++)
            {
                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ldc_I4, iArgs);
                generator.Emit(OpCodes.Ldarg, iArgs + 1);

                if (paramTypes[iArgs].IsByRef)
                {
                    if (paramTypes[iArgs].GetElementType().IsValueType)
                    {
                        generator.Emit(OpCodes.Ldobj, paramTypes[iArgs].GetElementType());
                        generator.Emit(OpCodes.Box, paramTypes[iArgs].GetElementType());
                    }
                    else
                        generator.Emit(OpCodes.Ldind_Ref);
                }
                else
                {
                    if (paramTypes[iArgs].IsValueType)
                        generator.Emit(OpCodes.Box, paramTypes[iArgs]);
                }

                generator.Emit(OpCodes.Stelem_Ref);

                if (paramTypes[iArgs].IsByRef)
                {
                    generator.Emit(OpCodes.Ldloc_2);
                    generator.Emit(OpCodes.Ldc_I4, iOutArgs);
                    generator.Emit(OpCodes.Ldc_I4, iArgs);
                    generator.Emit(OpCodes.Stelem_I4);
                    refArgs[iOutArgs] = iArgs;
                    iOutArgs++;
                }

                if (paramInfo[iArgs].IsIn || (!paramInfo[iArgs].IsOut))
                {
                    generator.Emit(OpCodes.Ldloc_1);
                    generator.Emit(OpCodes.Ldc_I4, iInArgs);
                    generator.Emit(OpCodes.Ldarg, iArgs + 1);

                    if (paramTypes[iArgs].IsByRef)
                    {
                        if (paramTypes[iArgs].GetElementType().IsValueType)
                        {
                            generator.Emit(OpCodes.Ldobj, paramTypes[iArgs].GetElementType());
                            generator.Emit(OpCodes.Box, paramTypes[iArgs].GetElementType());
                        }
                        else
                            generator.Emit(OpCodes.Ldind_Ref);
                    }
                    else
                    {
                        if (paramTypes[iArgs].IsValueType)
                            generator.Emit(OpCodes.Box, paramTypes[iArgs]);
                    }

                    generator.Emit(OpCodes.Stelem_Ref);
                    iInArgs++;
                }
            }

            // Calls the callFunction method of the base class
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldloc_1);
            generator.Emit(OpCodes.Ldloc_2);
            var miGenericEventHandler = delegateParent.GetMethod("CallFunction");
            generator.Emit(OpCodes.Call, miGenericEventHandler);

            // Stores return value
            if (returnType == typeof(void))
            {
                generator.Emit(OpCodes.Pop);
                generator.Emit(OpCodes.Ldnull);
            }
            else if (returnType.IsValueType)
            {
                generator.Emit(OpCodes.Unbox, returnType);
                generator.Emit(OpCodes.Ldobj, returnType);
            }
            else
                generator.Emit(OpCodes.Castclass, returnType);

            generator.Emit(OpCodes.Stloc_3);

            // Stores new value of out and ref params
            for (int i = 0; i < refArgs.Length; i++)
            {
                generator.Emit(OpCodes.Ldarg, refArgs[i] + 1);
                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ldc_I4, refArgs[i]);
                generator.Emit(OpCodes.Ldelem_Ref);

                if (paramTypes[refArgs[i]].GetElementType().IsValueType)
                {
                    generator.Emit(OpCodes.Unbox, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Ldobj, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Stobj, paramTypes[refArgs[i]].GetElementType());
                }
                else
                {
                    generator.Emit(OpCodes.Castclass, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Stind_Ref);
                }
            }

            // Returns
            if (!(returnType == typeof(void)))
                generator.Emit(OpCodes.Ldloc_3);

            generator.Emit(OpCodes.Ret);
            return myType.CreateType(); // creates the new type
#endif
        }

        void GetReturnTypesFromClass(Type klass, out Type[][] returnTypes)
        {
            var classMethods = klass.GetMethods();
            returnTypes = new Type[classMethods.Length][];

            int i = 0;

            foreach (var method in classMethods)
            {
                if (klass.IsInterface)
                {
                    GetReturnTypesFromMethod(method, out returnTypes[i]);
                    i++;
                }
                else if (!method.IsPrivate && !method.IsFinal && method.IsVirtual)
                {
                    GetReturnTypesFromMethod(method, out returnTypes[i]);
                    i++;
                }
            }
        }

        /*
         * Generates an implementation of klass, if it is an interface, or
         * a subclass of klass that delegates its virtual methods to a Lua table.
         */
        public void GenerateClass(Type klass, out Type newType, out Type[][] returnTypes)
        {

#if NETSTANDARD || WINDOWS_UWP
            throw new NotImplementedException (" Emit not available on .NET Standard ");
#else
            string typeName;
            lock (this)
            {
                typeName = "LuaGeneratedClass" + luaClassNumber;
                luaClassNumber++;
            }

            TypeBuilder myType;
            // Define a public class in the assembly, called typeName
            if (klass.IsInterface)
                myType = newModule.DefineType(typeName, TypeAttributes.Public, typeof(object), new Type[] {
                    klass,
                    typeof(ILuaGeneratedType)
                });
            else
                myType = newModule.DefineType(typeName, TypeAttributes.Public, klass, new Type[] { typeof(ILuaGeneratedType) });

            // Field that stores the Lua table
            var luaTableField = myType.DefineField("__luaInterface_luaTable", typeof(LuaTable), FieldAttributes.Public);
            // Field that stores the return types array
            var returnTypesField = myType.DefineField("__luaInterface_returnTypes", typeof(Type[][]), FieldAttributes.Public);
            // Generates the constructor for the new type, it takes a Lua table and an array
            // of return types and stores them in the respective fields
            var constructor = myType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] {
                typeof(LuaTable),
                typeof(Type[][])
            });
            ILGenerator generator = constructor.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);

            if (klass.IsInterface)
                generator.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
            else
                generator.Emit(OpCodes.Call, klass.GetConstructor(Type.EmptyTypes));

            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldarg_1);
            generator.Emit(OpCodes.Stfld, luaTableField);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldarg_2);
            generator.Emit(OpCodes.Stfld, returnTypesField);
            generator.Emit(OpCodes.Ret);
            // Generates overriden versions of the klass' public virtual methods
            var classMethods = klass.GetMethods();
            returnTypes = new Type[classMethods.Length][];
            int i = 0;

            foreach (var method in classMethods)
            {
                if (klass.IsInterface)
                {
                    GenerateMethod(myType, method, MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot,
                        i, luaTableField, returnTypesField, false, out returnTypes[i]);
                    i++;
                }
                else
                {
                    if (!method.IsPrivate && !method.IsFinal && method.IsVirtual)
                    {
                        GenerateMethod(myType, method, (method.Attributes | MethodAttributes.NewSlot) ^ MethodAttributes.NewSlot, i,
                            luaTableField, returnTypesField, true, out returnTypes[i]);
                        i++;
                    }
                }
            }

            // Generates an implementation of the luaInterfaceGetLuaTable method
            var returnTableMethod = myType.DefineMethod("LuaInterfaceGetLuaTable",
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, typeof(LuaTable), new Type[0]);
            myType.DefineMethodOverride(returnTableMethod, typeof(ILuaGeneratedType).GetMethod("LuaInterfaceGetLuaTable"));
            generator = returnTableMethod.GetILGenerator();
            generator.Emit(OpCodes.Ldfld, luaTableField);
            generator.Emit(OpCodes.Ret);
            newType = myType.CreateType(); // Creates the type
#endif
        }

        void GetReturnTypesFromMethod(MethodInfo method, out Type[] returnTypes)
        {
            var paramInfo = method.GetParameters();
            var paramTypes = new Type[paramInfo.Length];
            var returnTypesList = new List<Type>();

            // Counts out and ref parameters, for later use, 
            // and creates the list of return types
            int nOutParams = 0;
            int nOutAndRefParams = 0;
            var returnType = method.ReturnType;
            returnTypesList.Add(returnType);

            for (int i = 0; i < paramTypes.Length; i++)
            {
                paramTypes[i] = paramInfo[i].ParameterType;

                if (!paramInfo[i].IsIn && paramInfo[i].IsOut)
                {
                    nOutParams++;
                }

                if (paramTypes[i].IsByRef)
                {
                    returnTypesList.Add(paramTypes[i].GetElementType());
                    nOutAndRefParams++;
                }
            }

            returnTypes = returnTypesList.ToArray();
        }

#if !NETSTANDARD && !WINDOWS_UWP

        /*
         * Generates an overriden implementation of method inside myType that delegates
         * to a function in a Lua table with the same name, if the function exists. If it
         * doesn't the method calls the base method (or does nothing, in case of interface
         * implementations).
         */
        private void GenerateMethod(TypeBuilder myType, MethodInfo method, MethodAttributes attributes, int methodIndex,
            FieldInfo luaTableField, FieldInfo returnTypesField, bool generateBase, out Type[] returnTypes)
        {
            var paramInfo = method.GetParameters();
            var paramTypes = new Type[paramInfo.Length];
            var returnTypesList = new List<Type>();

            // Counts out and ref parameters, for later use, 
            // and creates the list of return types
            int nOutParams = 0;
            int nOutAndRefParams = 0;
            var returnType = method.ReturnType;
            returnTypesList.Add(returnType);

            for (int i = 0; i < paramTypes.Length; i++)
            {
                paramTypes[i] = paramInfo[i].ParameterType;
                if (!paramInfo[i].IsIn && paramInfo[i].IsOut)
                    nOutParams++;

                if (paramTypes[i].IsByRef)
                {
                    returnTypesList.Add(paramTypes[i].GetElementType());
                    nOutAndRefParams++;
                }
            }

            int[] refArgs = new int[nOutAndRefParams];
            returnTypes = returnTypesList.ToArray();

            // Generates a version of the method that calls the base implementation
            // directly, for use by the base field of the table
            if (generateBase)
            {
                var baseMethod = myType.DefineMethod("__luaInterface_base_" + method.Name,
                    MethodAttributes.Private | MethodAttributes.NewSlot | MethodAttributes.HideBySig,
                    returnType, paramTypes);
                ILGenerator generatorBase = baseMethod.GetILGenerator();
                generatorBase.Emit(OpCodes.Ldarg_0);

                for (int i = 0; i < paramTypes.Length; i++)
                    generatorBase.Emit(OpCodes.Ldarg, i + 1);

                generatorBase.Emit(OpCodes.Call, method);

                if (returnType == typeof(void))
                    generatorBase.Emit(OpCodes.Pop);

                generatorBase.Emit(OpCodes.Ret);
            }

            // Defines the method
            var methodImpl = myType.DefineMethod(method.Name, attributes, returnType, paramTypes);

            // If it's an implementation of an interface tells what method it
            // is overriding
            if (myType.BaseType.Equals(typeof(object)))
                myType.DefineMethodOverride(methodImpl, method);

            ILGenerator generator = methodImpl.GetILGenerator();
            generator.DeclareLocal(typeof(object[])); // original arguments
            generator.DeclareLocal(typeof(object[])); // with out-only arguments removed
            generator.DeclareLocal(typeof(int[])); // indexes of out and ref arguments

            if (!(returnType == typeof(void))) // return value
                generator.DeclareLocal(returnType);
            else
                generator.DeclareLocal(typeof(object));

            // Initializes local variables
            generator.Emit(OpCodes.Ldc_I4, paramTypes.Length);
            generator.Emit(OpCodes.Newarr, typeof(object));
            generator.Emit(OpCodes.Stloc_0);
            generator.Emit(OpCodes.Ldc_I4, paramTypes.Length - nOutParams + 1);
            generator.Emit(OpCodes.Newarr, typeof(object));
            generator.Emit(OpCodes.Stloc_1);
            generator.Emit(OpCodes.Ldc_I4, nOutAndRefParams);
            generator.Emit(OpCodes.Newarr, typeof(int));
            generator.Emit(OpCodes.Stloc_2);
            generator.Emit(OpCodes.Ldloc_1);
            generator.Emit(OpCodes.Ldc_I4_0);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, luaTableField);
            generator.Emit(OpCodes.Stelem_Ref);

            // Stores the arguments into the local variables, as needed
            for (int iArgs = 0, iInArgs = 1, iOutArgs = 0; iArgs < paramTypes.Length; iArgs++)
            {
                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ldc_I4, iArgs);
                generator.Emit(OpCodes.Ldarg, iArgs + 1);

                if (paramTypes[iArgs].IsByRef)
                {
                    if (paramTypes[iArgs].GetElementType().IsValueType)
                    {
                        generator.Emit(OpCodes.Ldobj, paramTypes[iArgs].GetElementType());
                        generator.Emit(OpCodes.Box, paramTypes[iArgs].GetElementType());
                    }
                    else
                        generator.Emit(OpCodes.Ldind_Ref);
                }
                else
                {
                    if (paramTypes[iArgs].IsValueType)
                        generator.Emit(OpCodes.Box, paramTypes[iArgs]);
                }

                generator.Emit(OpCodes.Stelem_Ref);

                if (paramTypes[iArgs].IsByRef)
                {
                    generator.Emit(OpCodes.Ldloc_2);
                    generator.Emit(OpCodes.Ldc_I4, iOutArgs);
                    generator.Emit(OpCodes.Ldc_I4, iArgs);
                    generator.Emit(OpCodes.Stelem_I4);
                    refArgs[iOutArgs] = iArgs;
                    iOutArgs++;
                }

                if (paramInfo[iArgs].IsIn || (!paramInfo[iArgs].IsOut))
                {
                    generator.Emit(OpCodes.Ldloc_1);
                    generator.Emit(OpCodes.Ldc_I4, iInArgs);
                    generator.Emit(OpCodes.Ldarg, iArgs + 1);

                    if (paramTypes[iArgs].IsByRef)
                    {
                        if (paramTypes[iArgs].GetElementType().IsValueType)
                        {
                            generator.Emit(OpCodes.Ldobj, paramTypes[iArgs].GetElementType());
                            generator.Emit(OpCodes.Box, paramTypes[iArgs].GetElementType());
                        }
                        else
                            generator.Emit(OpCodes.Ldind_Ref);
                    }
                    else
                    {
                        if (paramTypes[iArgs].IsValueType)
                            generator.Emit(OpCodes.Box, paramTypes[iArgs]);
                    }

                    generator.Emit(OpCodes.Stelem_Ref);
                    iInArgs++;
                }
            }

            // Gets the function the method will delegate to by calling
            // the getTableFunction method of class LuaClassHelper
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, luaTableField);
            generator.Emit(OpCodes.Ldstr, method.Name);
            generator.Emit(OpCodes.Call, classHelper.GetMethod("GetTableFunction"));
            var lab1 = generator.DefineLabel();
            generator.Emit(OpCodes.Dup);
            generator.Emit(OpCodes.Brtrue_S, lab1);
            // Function does not exist, call base method
            generator.Emit(OpCodes.Pop);

            if (!method.IsAbstract)
            {
                generator.Emit(OpCodes.Ldarg_0);

                for (int i = 0; i < paramTypes.Length; i++)
                    generator.Emit(OpCodes.Ldarg, i + 1);

                generator.Emit(OpCodes.Call, method);

                if (returnType == typeof(void))
                    generator.Emit(OpCodes.Pop);

                generator.Emit(OpCodes.Ret);
                generator.Emit(OpCodes.Ldnull);
            }
            else
                generator.Emit(OpCodes.Ldnull);

            var lab2 = generator.DefineLabel();
            generator.Emit(OpCodes.Br_S, lab2);
            generator.MarkLabel(lab1);
            // Function exists, call using method callFunction of LuaClassHelper
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, returnTypesField);
            generator.Emit(OpCodes.Ldc_I4, methodIndex);
            generator.Emit(OpCodes.Ldelem_Ref);
            generator.Emit(OpCodes.Ldloc_1);
            generator.Emit(OpCodes.Ldloc_2);
            generator.Emit(OpCodes.Call, classHelper.GetMethod("CallFunction"));
            generator.MarkLabel(lab2);

            // Stores the function return value
            if (returnType == typeof(void))
            {
                generator.Emit(OpCodes.Pop);
                generator.Emit(OpCodes.Ldnull);
            }
            else if (returnType.IsValueType)
            {
                generator.Emit(OpCodes.Unbox, returnType);
                generator.Emit(OpCodes.Ldobj, returnType);
            }
            else
                generator.Emit(OpCodes.Castclass, returnType);

            generator.Emit(OpCodes.Stloc_3);

            // Sets return values of out and ref parameters
            for (int i = 0; i < refArgs.Length; i++)
            {
                generator.Emit(OpCodes.Ldarg, refArgs[i] + 1);
                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ldc_I4, refArgs[i]);
                generator.Emit(OpCodes.Ldelem_Ref);

                if (paramTypes[refArgs[i]].GetElementType().IsValueType)
                {
                    generator.Emit(OpCodes.Unbox, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Ldobj, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Stobj, paramTypes[refArgs[i]].GetElementType());
                }
                else
                {
                    generator.Emit(OpCodes.Castclass, paramTypes[refArgs[i]].GetElementType());
                    generator.Emit(OpCodes.Stind_Ref);
                }
            }

            // Returns
            if (!(returnType == typeof(void)))
                generator.Emit(OpCodes.Ldloc_3);

            generator.Emit(OpCodes.Ret);
        }
#endif
        /*
         * Gets an event handler for the event type that delegates to the eventHandler Lua function.
         * Caches the generated type.
         */
        public LuaEventHandler GetEvent(Type eventHandlerType, LuaFunction eventHandler)
        {
#if NETSTANDARD || WINDOWS_UWP
            throw new NotImplementedException (" Emit not available on .NET Standard ");
#else
            Type eventConsumerType;

            if (eventHandlerCollection.ContainsKey(eventHandlerType))
                eventConsumerType = eventHandlerCollection[eventHandlerType];
            else
            {
                eventConsumerType = GenerateEvent(eventHandlerType);
                eventHandlerCollection[eventHandlerType] = eventConsumerType;
            }

            var luaEventHandler = (LuaEventHandler)Activator.CreateInstance(eventConsumerType);
            luaEventHandler.Handler = eventHandler;
            return luaEventHandler;
#endif
        }

        public void RegisterLuaDelegateType(Type delegateType, Type luaDelegateType)
        {
            _delegateCollection[delegateType] = luaDelegateType;
        }

        public void RegisterLuaClassType(Type klass, Type luaClass)
        {
            var luaClassType = new LuaClassType();
            luaClassType.klass = luaClass;
            GetReturnTypesFromClass(klass, out luaClassType.returnTypes);
            _classCollection[klass] = luaClassType;
        }
        /*
         * Gets a delegate with delegateType that calls the luaFunc Lua function
         * Caches the generated type.
         */
        public Delegate GetDelegate(Type delegateType, LuaFunction luaFunc)
        {
            var returnTypes = new List<Type>();
            Type luaDelegateType;

            if (_delegateCollection.ContainsKey(delegateType))
            {
                luaDelegateType = _delegateCollection[delegateType];
            }
            else
            {
                luaDelegateType = GenerateDelegate(delegateType);
                _delegateCollection[delegateType] = luaDelegateType;
            }

            var methodInfo = delegateType.GetMethod("Invoke");
            returnTypes.Add(methodInfo.ReturnType);

            foreach (ParameterInfo paramInfo in methodInfo.GetParameters())
            {
                if (paramInfo.ParameterType.IsByRef)
                    returnTypes.Add(paramInfo.ParameterType);
            }

            var luaDelegate = (LuaDelegate)Activator.CreateInstance(luaDelegateType);
            luaDelegate.Function = luaFunc;
            luaDelegate.ReturnTypes = returnTypes.ToArray();
            return Delegate.CreateDelegate(delegateType, luaDelegate, "CallFunction");
        }

        /*
         * Gets an instance of an implementation of the klass interface or
         * subclass of klass that delegates public virtual methods to the
         * luaTable table.
         * Caches the generated type.
         */
        public object GetClassInstance(Type klass, LuaTable luaTable)
        {
            LuaClassType luaClassType;

            if (_classCollection.ContainsKey(klass))
                luaClassType = _classCollection[klass];
            else
            {
                luaClassType = new LuaClassType();
                GenerateClass(klass, out luaClassType.klass, out luaClassType.returnTypes);
                _classCollection[klass] = luaClassType;
            }

            return Activator.CreateInstance(luaClassType.klass, new object[] {
                luaTable,
                luaClassType.returnTypes
            });
        }
    }
}
