using System;
using System.Collections.Generic;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace dtdfixer
{
	class MainClass {
		public static void Main (string[] args) {
			ModuleDefinition module = ModuleDefinition.ReadModule ("Assembly-CSharp.dll");

			TypeDefinition type = module.GetType ("GameManager");
			if (isPatched (type)) {
				Console.WriteLine ("Assembly already patched");
				return;
			}
			markTypePatched (module, type);

			mappingPatch (module);
			consoleOutputPatch (module);
			telnetPatch (module);
			connectLogPatch (module);
			playerDataPatch (module);

			module.Write ("Assembly-CSharp.patched.dll");
			Console.WriteLine ("Done");

		}

		private static void mappingPatch (ModuleDefinition module) {
			TypeDefinition type = module.GetType ("Chunk");
			addHook (type, "CalcMapColors", true, 0, true, typeof(AllocsFixes.Mods).GetMethod ("CallCalcMapColors"));
		}

		private static void consoleOutputPatch (ModuleDefinition module) {
			TypeDefinition type = module.GetType ("ConsoleSdtd");
			replaceMethod (type, "ExecuteCmdFromClient", true, 4, typeof(AllocsFixes.NetConnections.ConsoleOutputSeparator).GetMethod ("C_ExecuteCmdFromClient"));
			addHook (type, "Run", true, 0, true, typeof(AllocsFixes.NetConnections.ConsoleOutputSeparator).GetMethod ("C_Run"));
			replaceMethod (type, "SendResult", true, 1, typeof(AllocsFixes.NetConnections.ConsoleOutputSeparator).GetMethod ("C_SendResult"));
		}

		private static void playerDataPatch (ModuleDefinition module) {
			TypeDefinition type = module.GetType ("GameManager");
			addHook (type, "SavePlayerData", true, 2, true, typeof(AllocsFixes.PlayerDataStuff).GetMethod ("GM_SavePlayerData"));
			addHook (type, "Awake", true, 0, true, typeof(AllocsFixes.StateManager).GetMethod ("Awake"));
			addHook (type, "Cleanup", true, 0, false, typeof(AllocsFixes.StateManager).GetMethod ("Shutdown"));
		}

		private static void connectLogPatch (ModuleDefinition module) {
			TypeDefinition type = module.GetType ("GameManager");
			addHook (type, "RequestToSpawnPlayer", true, 4, true, typeof(AllocsFixes.AllocsLogFunctions).GetMethod ("RequestToSpawnPlayer"));
			type = module.GetType ("ConnectionManager");
			addHook (type, "DisconnectClient", true, 2, false, typeof(AllocsFixes.AllocsLogFunctions).GetMethod ("PlayerDisconnected"));
		}

		private static void telnetPatch (ModuleDefinition module) {
			TypeDefinition type = module.GetType ("NetTelnetServer");
			replaceMethod (type, ".ctor", false, 1, typeof(AllocsFixes.NetConnections.NetTelnetServer).GetMethod ("init"));
			replaceMethod (type, "Disconnect", false, 0, typeof(AllocsFixes.NetConnections.NetTelnetServer).GetMethod ("Disconnect"));
			replaceMethod (type, "SetConsole", false, 1, typeof(AllocsFixes.NetConnections.NetTelnetServer).GetMethod ("SetConsole"));
			replaceMethod (type, "WriteToClient", false, 1, typeof(AllocsFixes.NetConnections.NetTelnetServer).GetMethod ("WriteToClient"));
		}

		private static void markTypePatched (ModuleDefinition module, TypeDefinition type) {
			type.Fields.Add (new FieldDefinition ("AllocsPatch", Mono.Cecil.FieldAttributes.Private | Mono.Cecil.FieldAttributes.SpecialName, module.Import (typeof(int))));
		}

		private static void addHook (TypeDefinition type, string methodName, bool addThisRef, int opCount, bool atEnd, MethodBase targetMethod) {
			MethodDefinition method = findMethod (type, methodName, opCount);
			if (method != null) {
				var il = method.Body.GetILProcessor ();
				var call = il.Create (OpCodes.Call, method.Module.Import (targetMethod));
				if (atEnd) {
					int insBefore = method.Body.Instructions.Count;
					if (addThisRef) {
						il.Append (il.Create (OpCodes.Ldarg, 0));
					}
					for (int op = 0; op < opCount; op++) {
						il.Append (il.Create (OpCodes.Ldarg, op + 1));
					}
					il.Append (call);
					il.Remove (method.Body.Instructions [insBefore - 1]);
					il.Append (il.Create (OpCodes.Ret));
				} else {
					var i = 0;
					if (addThisRef) {
						il.InsertBefore (method.Body.Instructions [i++], il.Create (OpCodes.Ldarg, 0));
					}
					for (int op = 0; op < opCount; op++) {
						il.InsertBefore (method.Body.Instructions [i++], il.Create (OpCodes.Ldarg, op + 1));
					}
					il.InsertBefore (method.Body.Instructions [i++], call);
				}
				return;
			}
		}

		private static void replaceMethod (TypeDefinition type, string methodName, bool addThisRef, int opCount, MethodBase targetMethod) {
			MethodDefinition method = findMethod (type, methodName, opCount);
			if (method != null) {
				var il = method.Body.GetILProcessor ();
				var call = il.Create (OpCodes.Call, method.Module.Import (targetMethod));
				var i = 0;
				if (addThisRef) {
					il.InsertBefore (method.Body.Instructions [i++], il.Create (OpCodes.Ldarg, 0));
				}
				for (int op = 0; op < opCount; op++) {
					il.InsertBefore (method.Body.Instructions [i++], il.Create (OpCodes.Ldarg, op + 1));
				}
				il.InsertBefore (method.Body.Instructions [i++], call);
				il.InsertBefore (method.Body.Instructions [i++], il.Create (OpCodes.Ret));
				return;
			}
		}

		private static MethodDefinition findMethod (TypeDefinition type, string methodName, int opCount) {
			foreach (MethodDefinition method in type.Methods) {
				if (method.Name.Equals (methodName) && method.Parameters.Count == opCount) {
					return method;
				}
			}
			Console.WriteLine ("ERROR: Did not find " + type.Name + "." + methodName + "() with " + opCount + " parameters");
			return null;
		}

		private static bool isPatched (TypeDefinition type) {
			foreach (FieldDefinition fd in type.Fields) {
				if (fd.Name.Equals ("AllocsPatch")) {
					return true;
				}
			}
			return false;
		}
	}
}
