using System;
using System.Text.RegularExpressions;
using Mono.Cecil.Cil;
using MonoMod.Cil;
using MoreSlugcats;
using SlugBase;
using SlugBase.Features;
using static SlugBase.Features.FeatureTypes;
using static SSOracleBehavior;
using SSAction = SSOracleBehavior.Action;
using SubBehavID = SSOracleBehavior.SubBehavior.SubBehavID;

namespace JilloSlug.Story;

internal static class Iterators {
    public static readonly PlayerFeature<string[]> PebblesLines = PlayerStrings("jillo/pebbles_lines");
    public static readonly PlayerFeature<string[]> PebblesLines2 = PlayerStrings("jillo/pebbles_lines_2");
    public static readonly PlayerFeature<string[]> MoonLines = PlayerStrings("jillo/moon_lines");

    public static readonly SSAction MeetJilloSS_Init = new SSAction("MeetJilloSS_Init", register: true);
    public static readonly SSAction MeetJilloSS_Fakeout = new SSAction("MeetJilloSS_Fakeout", register: true);
    public static readonly SSAction MeetJilloSS_End = new SSAction("MeetJilloSS_End", register: true);
    public static readonly SSAction MeetJilloDM_Init = new SSAction("MeetJilloDM_Init", register: true);
    public static readonly SSAction MeetJilloDM_Done = new SSAction("MeetJilloDM_Done", register: true);

    public static readonly SubBehavID MeetJilloSS = new SubBehavID("MeetJilloSS", register: true);
    public static readonly SubBehavID MeetJilloDM = new SubBehavID("MeetJilloDM", register: true);

    public static readonly Conversation.ID Pebbles_Jillo = new Conversation.ID("Pebbles_Jillo", register: true);
    public static readonly Conversation.ID Pebbles_Jillo_End = new Conversation.ID("Pebbles_Jillo_End", register: true);
    public static readonly Conversation.ID Moon_Jillo = new Conversation.ID("Moon_Jillo", register: true);

    public class SSOracleMeetJillo : ConversationBehavior {
        public override bool Gravity {
            get {
                return base.action != MeetJilloSS_Fakeout;
            }
        }

        public SSOracleMeetJillo(SSOracleBehavior owner) : base(owner, MeetJilloSS, Pebbles_Jillo) {
            owner.getToWorking = 0f;
            if (ModManager.MMF && owner.oracle.room.game.IsStorySession && owner.oracle.room.game.GetStorySession.saveState.miscWorldSaveData.memoryArraysFrolicked && base.oracle.room.world.rainCycle.timer > base.oracle.room.world.rainCycle.cycleLength / 4) {
                base.oracle.room.world.rainCycle.timer = base.oracle.room.world.rainCycle.cycleLength / 4;
                base.oracle.room.world.rainCycle.dayNightCounter = 0;
            }
        }

        public override void Update() {
            base.Update();
            owner.LockShortcuts();
            if (base.action == MeetJilloSS_Init) {
                base.movementBehavior = MovementBehavior.KeepDistance;
                if (base.inActionCounter > 180) {
                    owner.NewAction(SSAction.General_MarkTalk);
                }
            } else if (base.action == SSAction.General_MarkTalk) {
                base.movementBehavior = MovementBehavior.Talk;
                if (base.inActionCounter == 15 && (owner.conversation == null || owner.conversation.id != convoID)) {
                    owner.InitateConversation(convoID, this);
                }
                if (owner.conversation != null && owner.conversation.id == convoID && owner.conversation.slatedForDeletion) {
                    owner.conversation = null;
                    owner.NewAction(MeetJilloSS_Fakeout);
                }
            } else if (base.action == MeetJilloSS_Fakeout) {
                base.movementBehavior = MovementBehavior.KeepDistance;
                if (base.inActionCounter > 240) {
                    owner.NewAction(MeetJilloSS_End);
                }
            } else if (base.action == MeetJilloSS_End) {
                base.movementBehavior = MovementBehavior.Talk;
                if (base.inActionCounter == 80 && (owner.conversation == null || owner.conversation.id != Pebbles_Jillo_End)) {
                    owner.InitateConversation(Pebbles_Jillo_End, this);
                }
                if (owner.conversation != null && owner.conversation.id == Pebbles_Jillo_End && owner.conversation.slatedForDeletion) {
                    owner.conversation = null;
                    owner.NewAction(SSAction.ThrowOut_ThrowOut);
                }
            }
        }
    }

    public class DMOracleMeetJillo : ConversationBehavior {
        public DMOracleMeetJillo(SSOracleBehavior owner) : base(owner, MeetJilloDM, Moon_Jillo) {
            base.owner.TurnOffSSMusic(abruptEnd: true);
            owner.getToWorking = 0f;
            if (base.owner.conversation != null) {
                base.owner.conversation.Destroy();
                base.owner.conversation = null;
            }
        }

        public override void Update() {
            base.Update();
            if (base.action == MeetJilloDM_Init) {
                owner.LockShortcuts();
                base.movementBehavior = MovementBehavior.Investigate;
                if (base.inActionCounter > 200) {
                    owner.NewAction(SSAction.General_MarkTalk);
                }
            } else if (base.action == SSAction.General_MarkTalk) {
                base.movementBehavior = MovementBehavior.Talk;
                if (base.inActionCounter == 15 && (owner.conversation == null || owner.conversation.id != convoID)) {
                    owner.InitateConversation(convoID, this);
                }
                if (owner.conversation != null && owner.conversation.id == convoID && owner.conversation.slatedForDeletion) {
                    owner.conversation = null;
                    owner.NewAction(MeetJilloDM_Done);
                }
            } else if (base.action == MeetJilloDM_Done) {
                owner.getToWorking = 1f;
                base.movementBehavior = MovementBehavior.Meditate;
                owner.UnlockShortcuts();

                // TODO: why will moon not read pearls?

                // we need Some savedata to use to indicate this, and this works Well Enough
                (owner.oracle.room.world.game.session as StoryGameSession).saveState.miscWorldSaveData.smPearlTagged = true;
            }
        }
    }

    public static void AddHooks() {
        // patch to load the correct dialog w/ the Conversation.IDs
        On.SSOracleBehavior.PebblesConversation.AddEvents += PebblesConversation_AddEvents;
        // patch to enter the Action as appropriate
        IL.SSOracleBehavior.SeePlayer += SSOracleBehavior_SeePlayer;
        // patch the action -> subbehavior conversion
        IL.SSOracleBehavior.NewAction += SSOracleBehavior_NewAction;
    }

    // highly references SLOracleBehaviorHasMark.NameForPlayer
    private static string NameForPlayer(SSOracleBehavior self, bool capitalized = false) {
        string titleRaw = "creature";
        if (UnityEngine.Random.value < 0.4f) {
            titleRaw = "friend";
        }
        if (self.rainWorld.inGameTranslator.currentLanguage == InGameTranslator.LanguageID.Portuguese) {
            string title = self.Translate(titleRaw);
            if (capitalized && InGameTranslator.LanguageID.UsesCapitals(self.rainWorld.inGameTranslator.currentLanguage)) {
                title = char.ToUpper(title[0]) + title.Substring(1);
            }
            return title;
        } else {
            string title = self.Translate(titleRaw);
            string prefix = self.Translate("little");
            if (UnityEngine.Random.value < 0.3f) {
                prefix = self.Translate("gelatinous");
            }
            if (capitalized && InGameTranslator.LanguageID.UsesCapitals(self.rainWorld.inGameTranslator.currentLanguage))
            {
                prefix = char.ToUpper(prefix[0]) + prefix.Substring(1);
            }
            return prefix + " " + title;
        }
    }

    // = SLOracleBehaviorHasMark.ReplaceParts
    public static string ReplaceParts(SSOracleBehavior self, string s) {
        s = Regex.Replace(s, "<PLAYERNAME>", NameForPlayer(self, capitalized: false));
        s = Regex.Replace(s, "<CAPPLAYERNAME>", NameForPlayer(self, capitalized: true));
        s = Regex.Replace(s, "<PlayerName>", NameForPlayer(self, capitalized: false));
        s = Regex.Replace(s, "<CapPlayerName>", NameForPlayer(self, capitalized: true));
        return s;
    }

    private static void AddLines(SSOracleBehavior.PebblesConversation self, string[] lines) {
        foreach (string line in lines) {
            if (line.StartsWith("!wait ")) {
                int wait = int.Parse(line.Split(' ')[1]);
                self.events.Add(new SSOracleBehavior.PebblesConversation.PauseAndWaitForStillEvent(self, self.convBehav, wait));
            } else {
                self.events.Add(new Conversation.TextEvent(self, 0, ReplaceParts(self.owner, self.Translate(line)), 0));
            }
        }
    }

    private static void PebblesConversation_AddEvents(On.SSOracleBehavior.PebblesConversation.orig_AddEvents orig, SSOracleBehavior.PebblesConversation self) {
        if (self.id == Pebbles_Jillo && PebblesLines.TryGet(self.convBehav.player, out var pebsLines)) {
            AddLines(self, pebsLines);
        } else if (self.id == Pebbles_Jillo_End && PebblesLines2.TryGet(self.convBehav.player, out var pebsLines2)) {
            AddLines(self, pebsLines2);
        } else if (self.id == Moon_Jillo && MoonLines.TryGet(self.convBehav.player, out var moonLines)) {
            AddLines(self, moonLines);
        } else {
            orig(self);
        }
    }

    private static void SSOracleBehavior_SeePlayer(ILContext il) {
        ILCursor c = new ILCursor(il);

        // matching for `oracle.ID == MoreSlugcatsEnums.OracleID.DM`
        c.GotoNext(MoveType.After,
            i => i.MatchLdarg(0),
            i => i.MatchLdfld<OracleBehavior>("oracle"),
            i => i.MatchLdfld<Oracle>("ID"),
            i => i.MatchLdsfld<MoreSlugcatsEnums.OracleID>("DM"),
            i => i.Match(OpCodes.Call), // this is a mess of generics; not matching this, but it's the equation call
            i => i.Match(OpCodes.Brfalse),

            i => i.MatchLdarg(0) // match the next call's ldarg to not remove the label by accident - this is messy!
        );

        // stuff inside this if will be for checking for different characters; let's override this
        // first let's grab the label to skip to once we're done

        ILCursor skipC = c.Clone();

        // matching for `NewAction(Action.MeetRed_Init);`
        skipC.GotoNext(MoveType.After,
            i => i.MatchLdarg(0),
            i => i.MatchLdsfld<SSAction>("MeetRed_Init"),
            i => i.MatchCall<SSOracleBehavior>("NewAction")
        );
        // the if is exited right after this statement
        skipC.GotoNext(MoveType.Before, i => i.Match(OpCodes.Br));
        // capture the label it goes to
        ILLabel skipLabel = skipC.Next.Operand as ILLabel;

        // now that we're done, we can do the usual delegate injection
        // note the ldarg captured during our first match - we don't need to put one down now

        //c.Emit(OpCodes.Ldarg_0); // implicit
        c.EmitDelegate<Func<SSOracleBehavior, bool>>(self => {
            // we get thrown in here if:
            // pebbles: first time meeting
            //  past the first meeting, you'll get the usual throw out behavior. this is Fine
            // moon: all times
            //  this is fine too; we want to tweak all idle lines of dialog

            if (self.oracle.ID != MoreSlugcatsEnums.OracleID.DM) {
                // pebbles
                if (PebblesLines.TryGet(self.player, out var lines)) {
                    self.NewAction(MeetJilloSS_Init);
                    return false; // skip
                }
            } else {
                // moon
                if (MoonLines.TryGet(self.player, out var lines)) {
                    if ((self.oracle.room.world.game.session as StoryGameSession).saveState.miscWorldSaveData.smPearlTagged) {
                        self.NewAction(MeetJilloDM_Done);
                    } else {
                        self.NewAction(MeetJilloDM_Init);
                    }
                    return false; // skip
                }
            }

            return true; // proceed as usual
        });
        c.Emit(OpCodes.Brfalse, skipLabel);

        // clean up after ourselves to account for the ldarg we captured
        c.Emit(OpCodes.Ldarg_0);
    }

    private static void SSOracleBehavior_NewAction(ILContext il) {
        ILCursor c = new ILCursor(il);

        // match `currSubBehavior.NewAction(action, nextAction);`; this is the first line after the subbehavior decision
        c.GotoNext(MoveType.Before,
            i => i.MatchLdarg(0),
            i => i.MatchLdfld<SSOracleBehavior>("currSubBehavior"),
            i => i.MatchLdarg(0),
            i => i.MatchLdfld<SSOracleBehavior>("action"),
            i => i.MatchLdarg(1),
            i => i.MatchCallOrCallvirt<SSOracleBehavior.SubBehavior>("NewAction")
        );

        // we want to now set the subbehavior variable to whatever corresponds with the action

        c.Emit(OpCodes.Ldarg_1); // `nextAction`
        c.Emit(OpCodes.Ldloc_0); // the subbehavior variable
        c.EmitDelegate<Func<SSAction, SubBehavID, SubBehavID>>((action, id) => {
            if (action == MeetJilloSS_Init || action == MeetJilloSS_Fakeout || action == MeetJilloSS_End) {
                return MeetJilloSS;
            } else if (action == MeetJilloDM_Init || action == MeetJilloDM_Done) {
                return MeetJilloDM;
            } else {
                return id;
            }
        });
        // the returned value is the new id; let's set the variable to it
        c.Emit(OpCodes.Stloc_0);

        // unrelatedly, we also need to patch setting the subbehavior field based on the id!
        // match `subBehavior = new SSOracleMeetWhite(this);`
        c.GotoNext(MoveType.After,
            i => i.MatchLdarg(0),
            i => i.MatchNewobj<SSOracleBehavior.SSOracleMeetWhite>(),
            i => i.MatchStloc(1),
            i => i.Match(OpCodes.Br)
        );

        // capture the label it goes to once it's done
        ILLabel skipLabel = c.Prev.Operand as ILLabel;
        // capture an ldloc.0 to preserve labels
        c.GotoNext(MoveType.After, i => i.MatchLdloc(0));

        //c.Emit(OpCodes.Ldloc_0); // implicit; the subbehavior variable
        c.Emit(OpCodes.Ldarg_0); // this
        c.EmitDelegate<Func<SubBehavID, SSOracleBehavior, SSOracleBehavior.SubBehavior>>((id, self) => {
            if (id == MeetJilloSS) {
                return new SSOracleMeetJillo(self);
            } else if (id == MeetJilloDM) {
                return new DMOracleMeetJillo(self);
            } else {
                return null;
            }
        });

        // we either have null or a subbehavior!
        // let's set the subbehavior to whatever we got - setting it to null won't do anything, since it's already null by default
        c.Emit(OpCodes.Stloc_1);
        // now we skip the rest if it's null
        // stloc pops the value so we must load it back up (https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.stloc_0?view=net-7.0)
        c.Emit(OpCodes.Ldloc_1);
        // skip to our skiplabel if it's no longer null
        // the code actually does this the same way above at `if (subBehavior == null)`
        c.Emit(OpCodes.Brtrue, skipLabel);
        // past this point we know it's null; rebuild the bytecode state back to how the rest of it expects it to be
        // we put down our previously captured ldloc.0
        c.Emit(OpCodes.Ldloc_0);
        // and now we've cleaned everything up!
    }
}
