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

namespace COM3D2.CacheEditMenu.Patcher
{
    public static class Patcher
    {
        public static readonly string[] TargetAssemblyNames = { "Assembly-CSharp.dll" };

        public static readonly string SybarisPath = Path.GetDirectoryName(typeof(Patcher).Assembly.Location);

        private static void PatchHelper(MethodDefinition target, MethodReference prefix, MethodReference postix, params int[] ldargs)
        {
            var il = target.Body.GetILProcessor();
            var resultVar = target.ReturnType.FullName != "System.Void" ? new VariableDefinition(target.Module.ImportReference(target.ReturnType)) : null;
            if(resultVar != null)
                target.Body.Variables.Add(resultVar);
            
            var ins = target.Body.Instructions[0];

            if(resultVar != null)
                il.InsertBefore(ins, il.Create(OpCodes.Ldloca, resultVar));
            foreach (var ldarg in ldargs)
                il.InsertBefore(ins, il.Create(OpCodes.Ldarg, ldarg));
            il.InsertBefore(ins, il.Create(OpCodes.Call, target.Module.ImportReference(prefix)));
            il.InsertBefore(ins, il.Create(OpCodes.Brtrue, ins));
            if(resultVar != null)
                il.InsertBefore(ins, il.Create(OpCodes.Ldloc, resultVar));
            il.InsertBefore(ins, il.Create(OpCodes.Ret));

            if (postix == null)
                return;

            ins = il.Create(OpCodes.Nop);

            foreach (var bodyInstruction in target.Body.Instructions.Where(bodyInstruction => bodyInstruction.OpCode == OpCodes.Ret).Skip(1))
            {
                bodyInstruction.OpCode = OpCodes.Br;
                bodyInstruction.Operand = ins;
            }

            il.Append(ins);
            ins = il.Create(OpCodes.Ret);
            il.Append(ins);

            foreach (var ldarg in ldargs)
                il.InsertBefore(ins, il.Create(OpCodes.Ldarg, ldarg));
            il.InsertBefore(ins, il.Create(OpCodes.Call, target.Module.ImportReference(postix)));
        }

        public static void Patch(AssemblyDefinition ad)
        {
            var hookAd = AssemblyDefinition.ReadAssembly(
                typeof(Patcher).Assembly.GetManifestResourceStream(
                    "COM3D2.CacheEditMenu.Patcher.COM3D2.CacheEditMenu.dll"));

            var sceneEdit = ad.MainModule.GetType("SceneEdit");
            var getMenuItemSetUp = sceneEdit.Methods.FirstOrDefault(m => m.Name == "GetMenuItemSetUP");

            var importCm = ad.MainModule.GetType("ImportCM");
            var createTexture = importCm.Methods.FirstOrDefault(m => m.Name == "CreateTexture" && m.Parameters.Count == 1);

            var hookType = hookAd.MainModule.GetType("COM3D2.CacheEditMenu.Hooks");
            var getMenuItemSetUpPrefix = hookType.Methods.FirstOrDefault(m => m.Name == "Prefix");
            var getMenuItemSetUpPostfix = hookType.Methods.FirstOrDefault(m => m.Name == "Postfix");

            var createTexturePrefix = hookType.Methods.FirstOrDefault(m => m.Name == "CreateTexturePrefix");

            PatchHelper(getMenuItemSetUp, getMenuItemSetUpPrefix, getMenuItemSetUpPostfix, 0, 1);
            PatchHelper(createTexture, createTexturePrefix, null, 0);

            var buffer = new byte[4096];
            using(var s = typeof(Patcher).Assembly.GetManifestResourceStream(
                "COM3D2.CacheEditMenu.Patcher.COM3D2.CacheEditMenu.dll"))
                using (var ms = new MemoryStream())
                {
                    int read;
                    while ((read = s.Read(buffer, 0, buffer.Length)) > 0)
                        ms.Write(buffer, 0, read);

                    Assembly.Load(ms.ToArray());
                }
        }
    }
}