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

namespace StardewModdingAPI.Framework.Reflection
{
    /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
    internal class InterfaceProxyBuilder
    {
        /*********
        ** Properties
        *********/
        /// <summary>The CLR module in which to create proxy classes.</summary>
        private readonly ModuleBuilder ModuleBuilder;

        /// <summary>The generated proxy types.</summary>
        private readonly IDictionary<string, Type> GeneratedTypes = new Dictionary<string, Type>();


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        public InterfaceProxyBuilder()
        {
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
            this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
        }

        /// <summary>Create an API proxy.</summary>
        /// <typeparam name="TInterface">The interface through which to access the API.</typeparam>
        /// <param name="instance">The API instance to access.</param>
        /// <param name="sourceModID">The unique ID of the mod consuming the API.</param>
        /// <param name="targetModID">The unique ID of the mod providing the API.</param>
        public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
            where TInterface : class
        {
            // validate
            if (instance == null)
                throw new InvalidOperationException("Can't proxy access to a null API.");
            if (!typeof(TInterface).IsInterface)
                throw new InvalidOperationException("The proxy type must be an interface, not a class.");

            // get proxy type
            Type targetType = instance.GetType();
            string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>";
            if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type))
            {
                type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType);
                this.GeneratedTypes[proxyTypeName] = type;
            }

            // create instance
            ConstructorInfo constructor = type.GetConstructor(new[] { targetType });
            if (constructor == null)
                throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen
            return (TInterface)constructor.Invoke(new[] { instance });
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Define a class which proxies access to a target type through an interface.</summary>
        /// <param name="proxyTypeName">The name of the proxy type to generate.</param>
        /// <param name="interfaceType">The interface type through which to access the target.</param>
        /// <param name="targetType">The target type to access.</param>
        private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType)
        {
            // define proxy type
            TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class);
            proxyBuilder.AddInterfaceImplementation(interfaceType);

            // create field to store target instance
            FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);

            // create constructor which accepts target instance
            {
                ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType });
                ILGenerator il = constructor.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0); // this
                // ReSharper disable once AssignNullToNotNullAttribute -- never null
                il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor
                il.Emit(OpCodes.Ldarg_0);      // this
                il.Emit(OpCodes.Ldarg_1);      // load argument
                il.Emit(OpCodes.Stfld, field); // set field to loaded argument
                il.Emit(OpCodes.Ret);
            }

            // proxy methods
            foreach (MethodInfo proxyMethod in interfaceType.GetMethods())
            {
                var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray());
                if (targetMethod == null)
                    throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API.");

                this.ProxyMethod(proxyBuilder, targetMethod, field);
            }

            // create type
            return proxyBuilder.CreateType();
        }

        /// <summary>Define a method which proxies access to a method on the target.</summary>
        /// <param name="proxyBuilder">The proxy type being generated.</param>
        /// <param name="target">The target method.</param>
        /// <param name="instanceField">The proxy field containing the API instance.</param>
        private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField)
        {
            Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray();

            // create method
            MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual);
            methodBuilder.SetParameters(argTypes);
            methodBuilder.SetReturnType(target.ReturnType);

            // create method body
            {
                ILGenerator il = methodBuilder.GetILGenerator();

                // load target instance
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, instanceField);

                // invoke target method on instance
                for (int i = 0; i < argTypes.Length; i++)
                    il.Emit(OpCodes.Ldarg, i + 1);
                il.Emit(OpCodes.Call, target);

                // return result
                il.Emit(OpCodes.Ret);
            }
        }
    }
}