/*
    Credits
        Sebastien S. (SystemFailu.re) : Creating main script, reversing engine.
        ellomenop : Doing original splits, helping test & misc. bug fixes.
        Museus: Routed, midbiome, enter boss arena, overhaul of logic and sig scanning
        cgull: House splits + splits on boss kill, revamp for Hades II.
        iDeathHD: solved the 23 length string crisis of 2024
        Sam C. (froggy) : Olympic & Warsong updates
*/

state("Hades2")
{
    /*
        There's nothing here because I don't want to use static instance addresses..
        Please refer to `init` to see the signature scanning.
    */
}

startup
{
    vars.Log = (Action<object>)((output) => print("[Hades 2 ASL] " + output));
    vars.InitComplete = false;

    settings.Add("multiWep", false, "Multi Weapon Run");
    settings.Add("houseSplits", false, "Use Crossroads Splits", "multiWep");
    settings.Add("enterBossArena", true, "Split when entering boss arena");
    settings.Add("splitOnBossKill", true, "Split on Boss Kills");
    settings.Add("midbiome", false, "Split when exiting inter-biome");
    settings.Add("routed", false, "Routed (per chamber)");
}

init
{
    vars.InitComplete = false;
    vars.CancelSource = new CancellationTokenSource();
    vars.quick_restart_mod = false;

    System.Threading.Tasks.Task.Run(async () =>
    {
        task_start: try {
            while (true)
            {
                // DX = EngineWin64s.dll, VK = EngineWin64sv.dll
                // Have to use game.ModulesWow64Safe() because modules variable doesn't update inside Tasks
                var engine = game.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.StartsWith("Hades2"));
                if (engine == null){
                    vars.Log("Engine not loaded yet, trying again.");
                    await System.Threading.Tasks.Task.Delay(1000, vars.CancelSource.Token);
                    continue;
                }
                vars.Log("Found engine!");

                var signature_scanner = new SignatureScanner(game, engine.BaseAddress, engine.ModuleMemorySize);

                /* Signatures */
                var app_signature_target = new SigScanTarget(3, "48 8B 05 ?? ?? ?? ?? 48 8B B8 ?? ?? ?? ?? 48 8B 4F"); // rip = 7
                var world_signature_target = new SigScanTarget(3, "48 8B 05 ?? ?? ?? ?? 48 8D 8E ?? ?? ?? ?? 33 D2");
                var player_manager_signature_target = new SigScanTarget(3, "48 8B 15 ?? ?? ?? ?? 48 FF C3 E9 ?? ?? ?? ?? 0F 29 74 24");

                var signature_targets = new [] {
                    app_signature_target,
                    world_signature_target,
                    player_manager_signature_target,
                };

                foreach (var target in signature_targets) {
                    target.OnFound = (process, _, pointer) => process.ReadPointer(pointer + 0x4 + process.ReadValue<int>(pointer));
                }

                IntPtr app = signature_scanner.Scan(app_signature_target);
                vars.world = signature_scanner.Scan(world_signature_target);
                IntPtr player_manager = signature_scanner.Scan(player_manager_signature_target);

                vars.screen_manager = game.ReadPointer(app + 0x4F0); // 48 8B 8F ? ? ? ? E8 ? ? ? ? 48 8B D8 48 85 C0 0F 84 ? ? ? ? 48 8B 88
                vars.current_player = game.ReadPointer(game.ReadPointer(player_manager + 0x18));
                // vars.current_block_count = game.ReadValue<int>((IntPtr)vars.current_player + 0x50);

                vars.InitComplete = true;
                break;
            }

            vars.CancelSource.Cancel();
        }
        catch (ArgumentException) {
            // Hopefully will be fixed by https://github.com/LiveSplit/LiveSplit/pull/2203
            goto task_start;
        }
        catch (Exception ex) {
            vars.Log("Task abort.\n" + ex);
        }
    });

    current.run_time = "0:0.1";
    current.map = "";

    current.total_seconds = 0.5f;
    old.total_seconds = 0.5f;

    vars.time_split = current.run_time.Split(':', '.');
    vars.has_beat_final_boss = false;
    vars.boss_killed = false;

    vars.still_in_arena = false;

    vars.game_ui = IntPtr.Zero;
}

update
{
    if (!(vars.InitComplete))
        return false;

    IntPtr hash_table = game.ReadPointer((IntPtr) vars.current_player + 0x48);
    for(int i = 0; i < 5; i++)
    {
        IntPtr block = game.ReadPointer(hash_table + 0x8 * i);
        if(block == IntPtr.Zero)
            continue;

        string block_name = "";

        bool isChonosKill = (game.ReadValue<byte>(block + 23) & 0x0F) == 0x0F;
        bool isSSOString = (game.ReadValue<byte>(block + 23) & 0x80) == 0;

        if (isChonosKill)
        {
            block = game.ReadPointer(block + 0x18);
            block_name = game.ReadString(block, 128);

            if (block_name == null)
                continue;

            // vars.Log("(update) chronos block_name: " + block_name);
        }
        else if (isSSOString)
        {
            block_name = game.ReadString(block, 24);

            if (block_name == null)
                continue;

            //vars.Log("(update) sso block_name: " + block_name);
        }
        else
        {
            block = game.ReadPointer(block);
            block_name = game.ReadString(block, 128);
            
            if (block_name == null)
                continue;

            //vars.Log("(update) block_name: " + block_name);
        }

        // vars.Log("(update) Encountered block: " + block_name);

        var boss_killed_block = block_name == "GenericBossKillPresentation" || block_name == "HecateKillPresentation" || block_name == "PrometheusKillPresentation";
        var ignored_boss_map = (
            current.map == "G_MiniBoss02" || current.map == "O_MiniBoss01" || // Uh-Oh! || Charybdis
            current.map == "P_MiniBoss01" || current.map == "Q_MiniBoss02" || // Talos || _ of Typhon
            current.map == "Q_MiniBoss03" || current.map == "Q_MiniBoss05" || // _ of Typhon
            current.map == "I_Boss01" || current.map == "Q_Boss01" // Chronos - handled by has_beat_final_boss Typhon - handled by has_beat_final_boss
        );
        if (!vars.boss_killed && boss_killed_block && !ignored_boss_map)
        {
            vars.Log("(update) Detected boss kill");
            vars.boss_killed = true;
        }

        if (!vars.has_beat_final_boss &&
            (block_name == "ChronosKillPresentation" || (block_name == "GenericBossKillPresentation" && current.map == "Q_Boss01")))
        {
            vars.Log("(update) Detected Chronos/Typhon kill");
            vars.has_beat_final_boss = true;
        }
    }

    // Get the array of screen IntPtrs and iterate to find InGameUI screen
    if (vars.screen_manager != IntPtr.Zero)
    {
        IntPtr screen_vector_begin = game.ReadPointer((IntPtr)vars.screen_manager + 0x48); // sgg::ScreenManager mScreens
        IntPtr screen_vector_end = game.ReadPointer((IntPtr)vars.screen_manager + 0x50);

        var num_screens = (screen_vector_end.ToInt64() - screen_vector_begin.ToInt64()) / 8;

        // Maybe only loop once to find game_ui, not sure if pointer is destructed anytime.
        for (int i = 0; i < num_screens; i++)
        {
            IntPtr current_screen = game.ReadPointer(screen_vector_begin + 0x8 * i);
            if (current_screen == IntPtr.Zero)
                continue;

            IntPtr screen_vtable = game.ReadPointer(current_screen); // Deref to get vtable
            IntPtr get_type_method = game.ReadPointer(screen_vtable + 0x50); // sgg::GameScreen_vtbl GetType
            int screen_type = game.ReadValue<int>(get_type_method + 0x1); // sgg::ScreenType

            // InGameUI is screen type 7
            if ((screen_type & 0x7) == 7) {
                vars.game_ui = current_screen;
                break;
            }
        }
    }

    /* Get our current run time */
    if (vars.game_ui != IntPtr.Zero) // sgg::InGameUI
    {
        IntPtr runtime_component = game.ReadPointer((IntPtr)vars.game_ui + 0x2F8); // 0x2F8 = mElapsedRunTimeText
        if (runtime_component != IntPtr.Zero)
        {
            /* This might break if the run goes over 99 minutes T_T */
            current.run_time = game.ReadString(game.ReadPointer(runtime_component + 0x6B0), 0x8); // 48 8D 8E ? ? ? ? 48 8D 05 ? ? ? ? 4C 8B C0 66 0F 1F 44 00
            if (current.run_time == "PauseScr")
                current.run_time = "0:0.10";
        }
    }

    /* Get our current map name */
    if(vars.world != IntPtr.Zero)
    {
        IntPtr map_data = game.ReadPointer((IntPtr)vars.world + 0x90); // 0x70 + 0x20
        if(map_data != IntPtr.Zero)
            current.map = game.ReadString(map_data, 0x10);
            if (current.map != old.map)
                vars.Log("(update) Map change: " + old.map + " -> " + current.map);

            if (vars.still_in_arena && current.map != old.map)
            {
                vars.still_in_arena = false;
                vars.boss_killed = false;
                vars.has_beat_final_boss = false;
            }
    }

    vars.time_split = current.run_time.Split(':', '.');
    /* Convert the string time to singles */
    current.total_seconds =
        int.Parse(vars.time_split[0]) * 60 +
        int.Parse(vars.time_split[1]) +
        float.Parse(vars.time_split[2]) / 100;
}

onStart
{
    vars.boss_killed = false;
    vars.has_beat_final_boss = false;
    vars.still_in_arena = false;
}

start
{
    // Start the timer if in the first room and the old timer is greater than the new (memory address holds the value from the previous run)
    var is_opening_chamber = (
        current.map == "F_Opening01" || current.map == "F_Opening02" || // Erebus
        current.map == "F_Opening03" || current.map=="N_Opening01" // Erebus || Ephyrya
    );
    if (old.total_seconds > current.total_seconds && is_opening_chamber)
    {
        return true;
    }
}

onSplit
{
    vars.boss_killed = false;
    vars.has_beat_final_boss = false;
}

split
{
    // Split on Chronos/Typhon Kill
    if (!vars.still_in_arena && vars.has_beat_final_boss)
    {
        // Disable boss kill detection until we leave the boss arena
        vars.still_in_arena = true;
        vars.Log(current.run_time + " (split) Splitting for Chronos/Typhon kill");
        return true;
    }

    // Split on Boss Kill
    if (settings["splitOnBossKill"] && !vars.still_in_arena && vars.boss_killed)
    {
        // Disable boss kill detection until we leave the boss arena
        vars.still_in_arena = true;

        vars.Log(current.run_time + " (splitOnBossKill) Splitting for boss kill");
        return true;
    }

    // Split on run start if Crossroads Splits are enabled
    if (settings["multiWep"] && settings["houseSplits"])
    {
        if (current.map == "Hub_PreRun" && (old.total_seconds > current.total_seconds))
        {
            vars.Log(current.run_time + " (multiWep && houseSplits) Splitting for house split");
            return true;
        }
    }

    var entered_new_room = current.map != old.map;

    // Split every chamber if Routed is enabled
    if (settings["routed"] && entered_new_room)
    {
        vars.Log(current.run_time +" (routed) Splitting for chamber transition: " + old.map + " -> " + current.map);
        return true;
    }

    // Split on room transition
    // TODO check in Chronos/Typhon room ??
    if (!settings["splitOnBossKill"] && entered_new_room)
    {
        if (current.map == "F_PostBoss01" || current.map == "G_PostBoss01" || // Erebus/Hecate || Oceanus/Sirens
            current.map == "H_PostBoss01" || current.map == "N_PostBoss01" || // Fields/Cerberus || Ephyra/Polyphemus
            current.map == "O_PostBoss01" || current.map == "P_PostBoss01" || // Rift/Eris || Olympus/Prometheus
            current.map == "I_Boss01" || current.map == "Q_Boss01") //  Tartarus/Chronos|| Summit/Typhon
        {
            vars.Log(current.run_time + " (!splitOnBossKill) Splitting for exiting boss arena: " + old.map);
            return true;
        }
    }

    // Split when leaving interbiome
    if (settings["midbiome"] && entered_new_room)
    {
        if (old.map == "F_PostBoss01" || old.map == "G_PostBoss01" || // Erebus/Hecate || Oceanus/Sirens
            old.map == "H_PostBoss01" || old.map == "N_PostBoss01" || // Fields/Cerberus || Ephyra/Polyphemus
            old.map == "O_PostBoss01" || old.map == "P_PostBoss01") // Rift/Eris || // Olympus/Prometheus
        {
            vars.Log(current.run_time + " (midbiome) Splitting for leaving interbiome: " + old.map);
            return true;
        }
    }

    // Split on entering boss arena
    if (settings["enterBossArena"] && entered_new_room)
    {
        if (current.map == "F_Boss01" || current.map == "G_Boss01" || // Erebus/Hecate || Oceanus/Sirens
            current.map == "H_Boss01" || current.map == "I_Boss01" || // Fields/Cerberus || Tartarus/Chronos
            current.map == "N_Boss01" || current.map == "O_Boss01" || // Ephyra/Polyphemus || Rift/Eris
            current.map == "P_Boss01" || current.map == "Q_Boss01") // Olympus/Prometheus || Summit/Typhon
        {
            vars.Log(current.run_time + " (enterBossArena) Splitting for entering boss arena: " + current.map);
            return true;
        }
    }
}

onReset
{
    vars.time_split = "0:0.1".Split(':', '.');
    current.total_seconds = 0.5f;
    old.total_seconds = 0.5f;

    vars.boss_killed = false;
    vars.has_beat_final_boss = false;
}

reset
{
    // Reset and clear state if Mel is currently in the courtyard.  Don't reset in multiweapon runs
    if(!settings["multiWep"] && current.map == "Hub_PreRun")
    {
        vars.Log("(reset) Resetting for courtyard");
        return true;
    }
}

gameTime
{
    return TimeSpan.FromSeconds(current.total_seconds);
}

isLoading
{
    /*
    Because we just override the gameTime constantly with in-game timer, we
    don't need a fancy load sensor. For a Loadless RTA setting, this will need
    to be actually evaluated, but for now just pretend we are always loading.
    */
    return true;
}

exit
{
    vars.CancelSource.Cancel();
    vars.InitComplete = false;
}

shutdown
{
    vars.CancelSource.Cancel();
}