using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using HarmonyLib;
using LudeonTK;
using Multiplayer.Client.Desyncs;
using Multiplayer.Common;
using Multiplayer.Client.Util;
using Multiplayer.Client.Windows;
using RimWorld;
using RimWorld.Planet;
using Steamworks;
using UnityEngine;
using Verse;
using Debug = UnityEngine.Debug;

namespace Multiplayer.Client
{
    static class MpDebugActions
    {
        public const string MultiplayerCategory = "Multiplayer";
        // Actions in this category are not synced and only happen on the client.
        public const string MultiplayerLocalCategory = "Multiplayer local";

        [DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolWorld, allowedGameStates = AllowedGameStates.PlayingOnWorld)]
        public static void SpawnCaravans()
        {
            for (int a = 0; a < 10; a++)
            {
                int num = GenWorld.MouseTile();
                if (Find.WorldGrid[num].biome.impassable)
                {
                    return;
                }

                List<Pawn> list = new List<Pawn>();
                int num2 = Rand.RangeInclusive(1, 10);
                for (int i = 0; i < num2; i++)
                {
                    Pawn pawn = PawnGenerator.GeneratePawn(Faction.OfPlayer.def.basicMemberKind, Faction.OfPlayer);
                    list.Add(pawn);
                    if (!pawn.WorkTagIsDisabled(WorkTags.Violent))
                    {
                        ThingDef thingDef = DefDatabase<ThingDef>.AllDefs.Where((ThingDef def) =>
                                def.IsWeapon && !def.weaponTags.NullOrEmpty() &&
                                (def.weaponTags.Contains("SimpleGun") ||
                                 def.weaponTags.Contains(
                                     "IndustrialGunAdvanced") ||
                                 def.weaponTags.Contains("SpacerGun") ||
                                 def.weaponTags.Contains(
                                     "MedievalMeleeAdvanced") ||
                                 def.weaponTags.Contains(
                                     "NeolithicRangedBasic") ||
                                 def.weaponTags.Contains(
                                     "NeolithicRangedDecent") ||
                                 def.weaponTags.Contains(
                                     "NeolithicRangedHeavy")))
                            .RandomElementWithFallback();
                        pawn.equipment.AddEquipment(
                            (ThingWithComps)ThingMaker.MakeThing(thingDef, GenStuff.RandomStuffFor(thingDef)));
                    }
                }

                int num3 = Rand.RangeInclusive(-4, 10);
                for (int j = 0; j < num3; j++)
                {
                    Pawn item = PawnGenerator.GeneratePawn(
                        DefDatabase<PawnKindDef>.AllDefs
                            .Where((PawnKindDef d) => d.RaceProps.Animal && d.race.GetStatValueAbstract(StatDefOf.Wildness) < 1f).RandomElement(),
                        Faction.OfPlayer);
                    list.Add(item);
                }

                Caravan caravan =
                    CaravanMaker.MakeCaravan(list, Faction.OfPlayer, num, addToWorldPawnsIfNotAlready: true);

                List<Thing> list2 = ThingSetMakerDefOf.DebugCaravanInventory.root.Generate();
                for (int k = 0; k < list2.Count; k++)
                {
                    Thing thing = list2[k];
                    if (!(thing.GetStatValue(StatDefOf.Mass) * (float)thing.stackCount >
                          caravan.MassCapacity - caravan.MassUsage))
                    {
                        CaravanInventoryUtility.GiveThing(caravan, thing);
                        continue;
                    }

                    break;
                }
            }
        }

        [DebugAction(MultiplayerCategory, "Set faction (rect)", actionType = DebugActionType.Action, allowedGameStates = AllowedGameStates.PlayingOnMap, displayPriority = 100)]
        private static void SetFaction()
        {
        	DebugToolsGeneral.GenericRectTool("Set faction (rect)", rect =>
        	{
                List<FloatMenuOption> factionOptions = new List<FloatMenuOption>();
                foreach (Faction faction in Find.FactionManager.AllFactionsInViewOrder)
                {
                    FloatMenuOption item = new FloatMenuOption(faction.Name, () =>
                    {
                        foreach (Thing thing in rect.SelectMany(c => Find.CurrentMap.thingGrid.ThingsAt(c)))
                        {
                            if (thing.def.CanHaveFaction)
                                thing.SetFaction(faction);

                            if (thing is IThingHolder holder && holder.GetDirectlyHeldThings() != null)
                                foreach (var heldThing in holder.GetDirectlyHeldThings())
                                    if (heldThing.def.CanHaveFaction)
                                        heldThing.SetFaction(faction);
                        }
                    });
                    factionOptions.Add(item);
                }
                Find.WindowStack.Add(new FloatMenu(factionOptions));
        	});
        }

        [DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)]
        public static void SpawnShuttleAcceptColonists()
        {
            var shuttle = ThingMaker.MakeThing(ThingDefOf.Shuttle);
            shuttle.TryGetComp<CompShuttle>().acceptColonists = true;
            GenPlace.TryPlaceThing(shuttle, UI.MouseCell(), Find.CurrentMap, ThingPlaceMode.Near);
        }

        [DebugAction(MultiplayerCategory, allowedGameStates = AllowedGameStates.PlayingOnMap)]
        public static void RegenerateEverythingNowWithTiming()
        {
            var drawer = Find.CurrentMap.mapDrawer;
            Dictionary<Type, long> timing = new();

            void RegenerateEverythingNow()
            {
                drawer.EnsureGlobalLayersInitialized();

                for (int i = 0; i < drawer.global.Count; i++)
                {
                    MapDrawLayer mapDrawLayer = drawer.global[i];
                    if (mapDrawLayer.Visible)
                    {
                        var stopwatch = Stopwatch.StartNew();
                        {
                            mapDrawLayer.Regenerate();
                            mapDrawLayer.RefreshSubMeshBounds();
                        }
                        if (timing.ContainsKey(mapDrawLayer.GetType()))
                            timing[mapDrawLayer.GetType()] += stopwatch.ElapsedMilliseconds;
                        else
                            timing[mapDrawLayer.GetType()] = stopwatch.ElapsedMilliseconds;
                    }
                }
                for (int j = 0; j < drawer.SectionCount.x; j++)
                {
                    for (int k = 0; k < drawer.SectionCount.z; k++)
                    {
                        RegenerateAllLayers(drawer.sections[j, k]);
                    }
                }
            }

            void RegenerateAllLayers(Section s)
            {
                s.bounds = s.CellRect;
                for (int i = 0; i < s.layers.Count; i++)
                {
                    if (!s.layers[i].Visible)
                    {
                        continue;
                    }
                    try
                    {
                        var stopwatch = Stopwatch.StartNew();
                        {
                            s.layers[i].Regenerate();
                            if (s.layers[i].Isnt<SectionLayer_Dynamic>())
                            {
                                s.bounds = s.bounds.Encapsulate(s.layers[i].GetBoundaryRect());
                            }
                        }
                        if (timing.ContainsKey(s.layers[i].GetType()))
                            timing[s.layers[i].GetType()] += stopwatch.ElapsedMilliseconds;
                        else
                            timing[s.layers[i].GetType()] = stopwatch.ElapsedMilliseconds;
                    }
                    catch (Exception arg)
                    {
                        Log.Error($"Could not regenerate layer {s.layers[i].ToStringSafe()}: {arg}");
                    }
                }
                for (int j = 0; j < s.layers.Count; j++)
                {
                    s.layers[j].RefreshSubMeshBounds();
                }
            }

            // RegenerateEverythingNow();
            Find.CurrentMap.mapDrawer.WholeMapChanged(~(ulong)MapMeshFlagDefOf.None);
            Log.Message(timing.ToStringFullContents());
        }

        [DebugAction(MultiplayerCategory, "Save Game", allowedGameStates = AllowedGameStates.Playing)]
        public static void SaveGame()
        {
            Game game = Current.Game;
            byte[] data = ScribeUtil.WriteExposable(game, "game", true);
            File.WriteAllBytes($"game_{Multiplayer.username}.xml", data);
        }

        [DebugAction(MultiplayerLocalCategory, name = "Trigger desync", allowedGameStates = AllowedGameStates.Playing)]
        public static void TriggerDesync()
        {
            var logItem = StackTraceLogItemRaw.GetFromPool();
            var trace = logItem.raw;
            int hash = 0;
            int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash);

            Multiplayer.game.asyncWorldTimeComp.randState++;
            Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash);
        }

        [DebugAction(MultiplayerLocalCategory, name = "Show desync window", allowedGameStates = AllowedGameStates.Playing)]
        public static void ShowDesync()
        {
            Find.WindowStack.Add(new DesyncedWindow(
                "Debug action",
                new SaveableDesyncInfo(Multiplayer.game.sync, new ClientSyncOpinion(0), new ClientSyncOpinion(0), 0, true)
            ));
        }

        [DebugAction(MultiplayerLocalCategory, name = "Show stack traces", allowedGameStates = AllowedGameStates.Playing)]
        public static void ShowStackTraces()
        {
            var syncCoordinator = Multiplayer.game.sync;
            var opinions = syncCoordinator.knownClientOpinions;
            if (opinions.Count == 0)
            {
                Log.Message("No opinions available");
                return;
            }
            var stackTrace = opinions[0].GetFormattedStackTracesForRange(-1);
            Log.Message($"Current opinion stack trace: \n{stackTrace}");
        }

        [DebugAction(MultiplayerCategory, name = "Show pending player", allowedGameStates = AllowedGameStates.Playing)]
        public static void ShowPendingPlayer()
        {
            PendingPlayerWindow.EnqueueJoinRequest(SteamUser.GetSteamID(), (_, _) => { });
        }

        [DebugAction(MultiplayerCategory, "Dump Sync Types", allowedGameStates = AllowedGameStates.Entry)]
        public static void DumpSyncTypes()
        {
            var dict = new Dictionary<string, Type[]>() {
                {"ThingComp", CompSerialization.thingCompTypes},
                {"AbilityComp", CompSerialization.abilityCompTypes},
                {"WorldObjectComp", CompSerialization.worldObjectCompTypes},
                {"HediffComp", CompSerialization.hediffCompTypes},

                {"GameComponent", CompSerialization.gameCompTypes},
                {"WorldComponent", CompSerialization.worldCompTypes},
                {"MapComponent", CompSerialization.mapCompTypes},
            };

            foreach (var syncWithImplType in Multiplayer.serialization.syncWithImplTypes)
                dict[syncWithImplType.Name] = Multiplayer.serialization.TypeHelper!.GetImplementations(syncWithImplType).ToArray();

            foreach(var kv in dict) {
                Log.Warning($"== {kv.Key} ==");
                Log.Message(
                    kv.Value
                    .Select(type => $"{type.Name}")
                    .Join(delimiter: "\n")
                );
            }
        }

        [DebugAction(MultiplayerCategory, "Print jitted methods", allowedGameStates = /* all */ AllowedGameStates.Invalid)]
        public static void PrintJittedMethods()
        {
            Log.Message(JittedMethods.GetJittedMethodsString());
        }

        [DebugAction(MultiplayerCategory, "Dump Def Types", allowedGameStates = AllowedGameStates.Entry)]
        public static void DumpDefTypes()
        {
            foreach (var defType in GenTypes.AllLeafSubclasses(typeof(Def)))
            {
                if (defType.Assembly != typeof(Game).Assembly) continue;
                if (MultiplayerData.IgnoredVanillaDefTypes.Contains(defType)) continue;

                Log.Warning($"== {defType.Name} ==");
                Log.Message(
                    GenDefDatabase.GetAllDefsInDatabaseForDef(defType)
                    .Select(def => $"{def.defName}")
                    .Join(delimiter: "\n")
                );
            }
        }

        [DebugAction(MultiplayerCategory, "Dump IRenameable Types", allowedGameStates = AllowedGameStates.Entry)]
        static void DumpIRenameableTypes()
        {
            var synced = new List<Type>();
            var unsynced = new List<Type>();
            var methods = typeof(IRenameable).AllImplementing()
                .Select(t => AccessTools.DeclaredPropertySetter(t, nameof(IRenameable.RenamableLabel)))
                .AllNotNull();

            foreach (var method in methods)
            {
                // Check if a method is synced or not
                if (Sync.methodBaseToInternalId.ContainsKey(method))
                    synced.Add(method.DeclaringType);
                else
                    unsynced.Add(method.DeclaringType);
            }

            Log.Warning("== Synced IRenameable types ==");
            Log.Message(!synced.Any() ? "No types" : synced.Select(GetNameWithNamespace).Join(delimiter: "\n"));

            Log.Warning("== Unsynced IRenameable types ==");
            Log.Message(!unsynced.Any() ? "No types" : unsynced.Select(GetNameWithNamespace).Join(delimiter: "\n"));

            static string GetNameWithNamespace(Type t) => t.Namespace.NullOrEmpty() ? t.Name : $"{t.Namespace}.{t.Name}";
        }

        [DebugAction(MultiplayerCategory, allowedGameStates = AllowedGameStates.Playing)]
        static void LogAllPatch()
        {
            var logger = new HarmonyMethod(typeof(MpDebugActions), nameof(MultiplayerMethodCallLogger));
            var refused = 0;

            foreach (var method in Assembly.GetExecutingAssembly().DefinedTypes.SelectMany(t => t.DeclaredMethods))
            {
                if (!InstrumentationTargets.ShouldInstrument(method))
                    continue;

                try
                {
                    Multiplayer.harmony.Patch(method, prefix: logger);
                }
                catch (Exception e)
                {
                    // The filter cannot anticipate every reason Harmony may refuse a method, and a trace
                    // missing one entry is worth far more than no trace at all. Counted rather than logged
                    // per method, so a systematic refusal does not bury the trace this action exists for.
                    refused++;
                    if (refused == 1)
                        Log.Warning($"MP: could not instrument {method.FullDescription()}: {e.GetBaseException().Message}");
                }
            }

            if (refused > 0)
                Log.Warning($"MP: {refused} method(s) could not be instrumented; the trace below is incomplete");
        }

        [DebugAction(MultiplayerCategory, allowedGameStates = AllowedGameStates.Entry)]
        static void LogAllPatchEntry()
        {
            LogAllPatch();
        }

        static void MultiplayerMethodCallLogger(MethodBase __originalMethod)
        {
            Debug.Log(__originalMethod.FullDescription());
        }

        [DebugAction(MultiplayerLocalCategory, "Dump addr info table", allowedGameStates = /* all */ AllowedGameStates.Invalid)]
        public static void DumpAddrInfoTable()
        {
            Log.Message($"Dumping address info table {DeferredStackTracingImpl.hashTable.Entries}");
            foreach (var addrInfo in DeferredStackTracingImpl.hashTable)
            {
                if (addrInfo.nameHash == 0) continue;
                var normalized = $"`{Native.MethodNameNormalizedFromAddr(addrInfo.addr, true)}`";
                var normal = Native.MethodNameFromAddr(addrInfo.addr, true);
                var hash = addrInfo.nameHash;
                Log.Message($"NORM: {normalized,-130} H:{hash,14} R:`{normal}`");
            }
        }

        [DebugAction(MultiplayerLocalCategory, "Show mod compat", allowedGameStates = AllowedGameStates.Playing)]
        public static void ShowModCompatDialog()
        {
            var window = new ModCompatWindow(null, true, false, null);
            Find.WindowStack.Add(window);
        }

#if DEBUG

        [DebugOutput]
        public static unsafe void PrintInlined()
        {
            foreach (var m in Harmony.GetAllPatchedMethods())
            {
                // method->inline_info
                if ((*(byte*)(m.MethodHandle.Value + 32) & 1) == 1)
                    Log.Warning($"Mono inlined {m.FullDescription()} {*(byte*)(m.MethodHandle.Value + 32) & 1} {*((ushort*)(m.MethodHandle.Value) + 1) & (ushort)MethodImplOptions.NoInlining}");
            }
        }

        [DebugOutput(true)]
        public static void PrintThingListers()
        {
            var output = new StringBuilder();

            void PrintLister(ListerThings lister)
            {
                for (int i = 0; i < lister.listsByGroup.Length; i++)
                    if (lister.listsByGroup[i] != null)
                    {
                        output.Append((ThingRequestGroup)i).Append(" ");
                        foreach (var t in lister.listsByGroup[i])
                            output.Append(t).Append(",");
                        output.AppendLine();
                    }

                foreach (var kv in lister.listsByDef)
                {
                    output.Append(kv.Key).Append(" ");
                    foreach (var t in kv.Value)
                        output.Append(t).Append(",");
                    output.AppendLine();
                }
            }

            PrintLister(Find.CurrentMap.listerThings);

            foreach (var region in Find.CurrentMap.regionGrid.AllRegions)
            {
                output.AppendLine($"REGION {region.extentsClose} {region.AnyCell} {region.id}");
                PrintLister(region.ListerThings);
            }

            File.WriteAllText(Application.consoleLogPath + ".regioninfo", output.ToString());
        }

        [DebugAction(DebugActionCategories.Mods, "Print static fields (game)", allowedGameStates = AllowedGameStates.Entry)]
        public static void PrintStaticFields()
        {
            Log.Message(StaticFieldsToString(typeof(Game).Assembly, type => type.Namespace.StartsWith("RimWorld") || type.Namespace.StartsWith("Verse")));
        }

        [DebugAction(DebugActionCategories.Mods, "Print static fields (mods)", allowedGameStates = AllowedGameStates.Entry)]
        public static void AllModStatics()
        {
            var builder = new StringBuilder();

            foreach (var mod in LoadedModManager.RunningModsListForReading) {
                builder.AppendLine("======== ").Append(mod.Name).AppendLine();
                foreach (var asm in mod.assemblies.loadedAssemblies) {
                    builder.AppendLine(StaticFieldsToString(asm, t => !t.Namespace.StartsWith("Harmony") && !t.Namespace.StartsWith("Multiplayer")));
                }
            }

            Log.Message(builder.ToString());
        }

        static string StaticFieldsToString(Assembly asm, Predicate<Type> typeValidator)
        {
            var builder = new StringBuilder();

            object FieldValue(FieldInfo field)
            {
                if (!StaticFieldDump.TryReadStaticValue(field, out var value, out var failure))
                    return $"[unreadable: {failure}]";

                if (value is ICollection col)
                    return col.Count;
                if (field.Name.ToLowerInvariant().Contains("path") && value is string path && (path.Contains("/") || path.Contains("\\")))
                    return "[x]";
                return value;
            }

            foreach (var type in StaticFieldDump.TypesOf(asm))
                if (!type.IsGenericTypeDefinition && type.Namespace != null && typeValidator(type) && !type.HasAttribute<DefOf>() && !type.HasAttribute<CompilerGeneratedAttribute>())
                    foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly))
                        if (!field.IsLiteral && !field.IsInitOnly && !field.HasAttribute<CompilerGeneratedAttribute>())
                            builder.AppendLine($"{field.FieldType} {type}::{field.Name}: {FieldValue(field)}");

            return builder.ToString();
        }

        [DebugAction(MultiplayerCategory, "Print Patched Hashes", allowedGameStates = AllowedGameStates.Entry)]
        public static void SaveHashes()
        {
            var builder = new StringBuilder();

            // We only care about transpiled methods that aren't part of MP.
            var query = Multiplayer.harmony.GetPatchedMethods()
                .Where(m => !(m.DeclaringType.Namespace?.StartsWith("Multiplayer") ?? false)
                    && !Harmony.GetPatchInfo(m).Transpilers.NullOrEmpty());

            foreach (var method in query) {
                builder.Append(GetMethodHash(method));
                builder.Append(" ");
                builder.Append(method.DeclaringType.FullName);
                builder.Append(":");
                builder.Append(method.Name);
                builder.AppendLine();
            }

            Log.Message(builder.ToString());
        }

        static string GetMethodHash(MethodBase method)
        {
            var toHash = new List<byte>();
            foreach(var ins in PatchProcessor.GetOriginalInstructions(method)) {
                toHash.Add(Encoding.UTF8.GetBytes(ins.opcode.Name + ins.operand));
            }

            using (var sha256 = SHA256.Create()) {
                return Convert.ToBase64String(sha256.ComputeHash(toHash.ToArray()));
            }
        }
#endif

    }
}
