using System;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace NeighPatcher
{
    [AttributeUsage(AttributeTargets.Method)]
    internal class NeighPatchAttribute : Attribute
    {
        public string[] ArgTypes;
        public string AssemblyName;
        public string MethodName;
        public string TypeName;

        public NeighPatchAttribute(string assemblyName, string typeName, string methodName, params string[] argTypes)
        {
            AssemblyName = assemblyName;
            TypeName = typeName;
            MethodName = methodName;
            ArgTypes = argTypes;
        }
    }

    internal static class NeighPatcher
    {
        public static void PatchAll(AssemblyDefinition targetAssembly, string patchTypeName)
        {
            var hookAd = AssemblyDefinition.ReadAssembly(Assembly.GetExecutingAssembly().Location);
            var patchType = hookAd.MainModule.GetType(patchTypeName);

            Console.WriteLine($"[NeighPatcher] Patching {targetAssembly.Name.Name} with {patchType.FullName}");

            foreach (var methodDefinition in patchType.Methods.Where(m => m.HasCustomAttributes))
            {
                var patchAttributes = methodDefinition
                                     .CustomAttributes
                                     .Where(c => c.HasConstructorArguments &&
                                                 c.Constructor.DeclaringType.Name == nameof(NeighPatchAttribute))
                                     .ToList();

                if (patchAttributes.Count == 0)
                    continue;

                Console.WriteLine(
                    $"[NeighPatcher] Applying {methodDefinition.FullName} with {patchAttributes.Count} patch attributes");

                var patchAttrs = patchAttributes.Select(p => new NeighPatchAttribute(
                                                            p.ConstructorArguments[0].Value as string,
                                                            p.ConstructorArguments[1].Value as string,
                                                            p.ConstructorArguments[2].Value as string,
                                                            ((CustomAttributeArgument[])p.ConstructorArguments[3].Value)
                                                           .Select(c => c.Value as string).ToArray())).ToList();

                foreach (var neighPatchAttribute in patchAttrs)
                {
                    Console.WriteLine(
                        $"[NeighPatcher] Patching [{neighPatchAttribute.AssemblyName}]{neighPatchAttribute.TypeName}::{neighPatchAttribute.MethodName} with {neighPatchAttribute.ArgTypes.Length} arg types");

                    if (targetAssembly.Name.Name != neighPatchAttribute.AssemblyName)
                        continue;

                    var t = targetAssembly.MainModule.GetType(neighPatchAttribute.TypeName);

                    Console.WriteLine($"[NeighPatcher] Found type {t}");

                    var method = t?.Methods.FirstOrDefault(m => m.Name == neighPatchAttribute.MethodName &&
                                                                (neighPatchAttribute.ArgTypes.Length == 0 ||
                                                                 neighPatchAttribute.ArgTypes.Length != 0 &&
                                                                 neighPatchAttribute.ArgTypes.Length ==
                                                                 m.Parameters.Count &&
                                                                 m.Parameters.Select(p => p.ParameterType.FullName)
                                                                  .SequenceEqual(neighPatchAttribute.ArgTypes)));

                    Console.WriteLine($"[NeighPatcher] Found method {method}");

                    if (method == null)
                        continue;

                    var il = method.Body.GetILProcessor();
                    var ins = method.Body.Instructions.First();

                    foreach (var mParam in methodDefinition.Parameters)
                        if (mParam.Name == "__instance")
                            il.InsertBefore(ins, il.Create(OpCodes.Ldarg_0));
                        else if (mParam.Name.StartsWith("___"))
                        {
                            string realName = mParam.Name.Substring(3);
                            var targetField = t.Fields.FirstOrDefault(ff => ff.Name == realName);
                            if (targetField == null)
                                throw new Exception($"Field {realName} not found from type {t.FullName}!");

                            if (!method.IsStatic)
                            {
                                il.InsertBefore(ins, il.Create(OpCodes.Ldarg_0));
                                il.InsertBefore(ins, il.Create(OpCodes.Ldflda, targetField));
                            }
                            else
                                il.InsertBefore(ins, il.Create(OpCodes.Ldsflda, targetField));
                        }
                        else
                        {
                            var pIndex = method.Parameters.FirstOrDefault(p => p.Name == mParam.Name);

                            if (pIndex == null)
                                throw new Exception(
                                    $"Parameter with name {mParam.Name} does not exist in {method.FullName}");
                            il.InsertBefore(ins, il.Create(OpCodes.Ldarg, pIndex));
                        }

                    il.InsertBefore(
                        ins, il.Create(OpCodes.Call, targetAssembly.MainModule.ImportReference(methodDefinition)));
                }
            }
        }
    }
}