require "common"

cb.setCrankSoundsDisabled()
cb.force_pref("crank_mode", 5)
cb.force_pref("crank_undock_button", 0)
cb.force_pref("crank_dock_button", 0)

-- decide version based on name in header
if cb.rom_peek(0x13C) == 0x52 then
    POKERED = true
    POKEVER = "R"
end
if cb.rom_peek(0x13C) == 0x42 then
    POKEBLUE = true
    POKEVER = "B"
end
if cb.rom_peek(0x13C) == 0x59 then
    POKEYELLOW = true
    POKEVER = "Y"
end

XRAM_IN_MENU_DOCK_TO_EXIT = 0xFEA0
XRAM_TMP_0 = 0xFEA1
XRAM_TMP_1 = 0xFEA2
XRAM_TMP_2 = 0xFEA3
XRAM_BLOCK_MENU = 0xFEA4
XRAM_TRAINER = 0xFEA5

-- 2: enable xram [0xFEA0-FEFF]
-- 4: enable crank menu mode
cb.ram_poke(0xFF57, 0x06)

if bint(POKERED) + bint(POKEBLUE) + bint(POKEYELLOW) ~= 1 then
    error("Unrecognized gen1 ROM")
    return 1
else
    print("Gen1 version: ", POKEVER)
end

local cave_rom_start, cave_size = find_code_cave(1)
local cave_bank = math.floor(cave_rom_start / 0x4000)
cave_start = ((cave_bank == 0) and 0 or 0x4000) | (cave_rom_start % 0x4000)

if (cave_size < 0x180) then
    error("unable to find sufficiently large code cave")
end

-- margins
cave_start = cave_start + 8
cave_rom_start = cave_rom_start + 8
cave_size = cave_size - 16

print("Special thanks to pret for disasm reference")
printf("Code cave: %x:%x +%x", cave_bank, cave_start, cave_size)
local addr = ({
    R = {
        Bankswitch = 0x0035d6,
        CheckPauseMenu = 0x00044F-8,
        PauseMenuInput = 0x002af2,
        hJoyHeld = 0xffb4,
        hJoyPressed = 0xffb3,
        hJoy5 = 0xffb5,
        hJoy7 = 0xffb7,
        hJoyInput = 0xfff8,
        wCurrentMenuItem = 0xcc26,
        wLastMenuItem = 0xcc2a,
        wJoyIgnore = 0xcd6b,
        wStatusFlags5 = 0xd730,
        WrapMenuItemID = 0x2B10,
        CheckIfPastBottom = 0x002B29,
        HandleMenuInput = 0x3abe,
        HandleMenuInput_GetJoypadState = 0x3ae8+1,
        PauseMenuInputCheckDown = 0x002b18,
        JoypadLowSensitivity = 0x003831,
        Joypad = 0x00019A,
        DrawStartMenu = 0x01710B,
        MenuExitDraw = 0x01717A,
        SkipDrawBorder = 0x0170BD,
    },
    B = {
        Bankswitch = 0x0035d6,
        CheckPauseMenu = 0x00044F-8,
        PauseMenuInput = 0x002af2,
        hJoyHeld = 0xffb4,
        hJoyPressed = 0xffb3,
        hJoy5 = 0xffb5,
        hJoy7 = 0xffb7,
        hJoyInput = 0xfff8,
        wStatusFlags5 = 0xd730,
        wCurrentMenuItem = 0xcc26,
        wLastMenuItem = 0xcc2a,
        wJoyIgnore = 0xcd6b,
        WrapMenuItemID = 0x2B10,
        CheckIfPastBottom = 0x002B29,
        HandleMenuInput = 0x3abe,
        HandleMenuInput_GetJoypadState = 0x3ae8+1,
        PauseMenuInputCheckDown = 0x002b18,
        JoypadLowSensitivity = 0x003831,
        Joypad = 0x00019A,
        DrawStartMenu = 0x01710B,
        MenuExitDraw = 0x01717A,
        SkipDrawBorder = 0x0170BD,
    },
    Y = {
        Bankswitch = 0x003e84,
        CheckPauseMenu = 0x000290-8,
        PauseMenuInput = 0x0029f4,
        hJoyHeld = 0xffb4,
        hJoyPressed = 0xffb3,
        hJoy5 = 0xffb5,
        hJoy7 = 0xffb7,
        hJoyInput = 0xfff5,
        wStatusFlags5 = 0xd72f,
        wCurrentMenuItem = 0xcc26,
        wLastMenuItem = 0xcc2a,
        wJoyIgnore = 0xcd6b,
        WrapMenuItemID = 0x2A12,
        CheckIfPastBottom = 0x002A2B,
        HandleMenuInput = 0x3aab,
        HandleMenuInput_GetJoypadState = 0x3ad5+1,
        PauseMenuInputCheckDown = 0x002a1a,
        JoypadLowSensitivity = 0x00381e,
        Joypad = 0x0001B9,
        DrawStartMenu = 0x016F80,
        MenuExitDraw = 0x016FED,
        SkipDrawBorder = 0x016F33,
    }
})[POKEVER]

local function sym_to_rom(sym)
    return (0x4000 * (sym >> 16)) | (sym % 0x4000)
end

local function sym_to_ram(sym)
    return ((0x4000 * (sym >> 16) == 0) and 0 or 0x4000) | (sym % 0x4000)
end

local function farcall(dstbank, dstaddr)
    return {
        OP_LD_B_d8, dstbank,
        OP_LD_HL_d16, word(dstaddr),
        OP_CALL, word(addr.Bankswitch)
    }
end

local trainer = {
"cavecall",
    -- store HL
    OP_LD_A_H,
    OP_LD_a16_A, word(XRAM_TMP_0),
    OP_LD_A_L,
    OP_LD_a16_A, word(XRAM_TMP_1),

    -- HL <- word at return address
    -- return address += 2
    OP_POP_HL,
    OP_LD_A_iHL,
    OP_INC_HL,
    OP_PUSH_HL,
    OP_DEC_HL,
    OP_LD_H_xHL,
    OP_LD_L_A,

    -- B <- dst bank
    OP_LD_B_d8, cave_bank,
    OP_CALL, word(addr.Bankswitch),

    -- A <- H
    OP_LD_A_H,

    OP_PUSH_AF,
        -- restore HL
        OP_LD_a16_A, word(XRAM_TMP_0),
        OP_LD_H_A,
        OP_LD_a16_A, word(XRAM_TMP_1),
        OP_LD_L_A,
    OP_POP_AF,
    OP_RET,
}
trainer_res = apply_patch(trainer, XRAM_TRAINER, XRAM_TRAINER, 0xFF00 - XRAM_TRAINER)

local patch = {
"CrankUndockPauseMenu",
    -- if we're in scripted movement, load [hJoyHeld] instead
    OP_LD_A_a16, word(addr.wStatusFlags5),
    OP_RLCA,
    OP_JR_nc, r8"CrankUndockPauseMenu_notSimulating",

    -- mark disabled menu input, lingers for 1 frame
    OP_XOR_A,
    OP_LD_a16_A, word(XRAM_BLOCK_MENU),

    OP_LD_A_a16, word(addr.hJoyHeld),
    OP_JR, r8"CrankUndockPauseMenu_ret",

"CrankUndockPauseMenu_notSimulating",

    -- if joypad input is ignored, load [hJoyHeld] instead
    OP_AND_d8, 0x40,
    OP_JR_nz, r8"CrankUndockPauseMenu_normal",

    -- if pause button is ignored, load [hJoyHeld] instead
    OP_LD_A_a16, word(addr.wJoyIgnore),
    OP_AND_d8, 0x8,
    OP_JR_nz, r8"CrankUndockPauseMenu_normal",

    OP_LD_A_a16, word(XRAM_BLOCK_MENU),
    OP_PUSH_AF,
        -- enable menu next frame
        OP_LD_A_d8, 1,
        OP_LD_a16_A, word(XRAM_BLOCK_MENU),
    OP_POP_AF,
    OP_AND_A,
    OP_JR_z, r8"CrankUndockPauseMenu_normal_noblock",

    -- check crank docked
    OP_LD_A_a16, word(IO_PD_CRANK_DOCKED),

    OP_RRCA,
    OP_JR_c, r8"CrankUndockPauseMenu_CrankDocked",

    -- undocked; input 'start'
    OP_LD_A_d8, 8,
    -- also, clear crank menu delta register
    OP_LD_a16_A, word(IO_PD_CRANK_hi),
    OP_LD_a16_A, word(IO_PD_CRANK_lo),
    OP_JR, r8"CrankUndockPauseMenu_ret",

"CrankUndockPauseMenu_CrankDocked",
    OP_LD_A_a16, word(addr.hJoyPressed),
    OP_AND_d8, (~8) & 0xFF, -- not start button

"CrankUndockPauseMenu_ret",
    OP_LD_H_A,
    OP_BIT3_A,
    OP_RET,

"CrankUndockPauseMenu_normal",
    -- mark disabled menu input, lingers for 1 frame
    OP_XOR_A,
    OP_LD_a16_A, word(XRAM_BLOCK_MENU),

"CrankUndockPauseMenu_normal_noblock",
    OP_LD_A_a16, word(addr.hJoyHeld),
    OP_JR, r8"CrankUndockPauseMenu_ret",

"CustomPauseMenuInput",
    OP_LD_A_d8, 1,
    OP_LD_a16_A, word(XRAM_IN_MENU_DOCK_TO_EXIT),
    OP_CALL, word(addr.HandleMenuInput),

    OP_PUSH_AF,
        OP_XOR_A,
        OP_LD_a16_A, word(XRAM_IN_MENU_DOCK_TO_EXIT),
    OP_POP_AF,

"CustomPauseMenuInput_Done",
    OP_LD_H_A,
    OP_BIT6_A,
    OP_JR_nz, r8"CustomPauseMenuInput_Done_Up",
    OP_SCF, OP_CCF, -- clear carry flag (indicates not pressing 'up')
    OP_RET,

-- had to steal some of this for detour
"CustomPauseMenuInput_Done_Up",
    OP_LD_A_a16, word(addr.wCurrentMenuItem),
    OP_LD_B_A,
    OP_LD_A_a16, word(addr.wLastMenuItem),
    OP_OR_B,
    OP_SCF,
    OP_RET,

"CustomMenuInput",
    OP_LD_A_a16, word(addr.hJoyInput),

    OP_PUSH_AF,
        OP_LD_A_a16, word(XRAM_IN_MENU_DOCK_TO_EXIT),
        OP_LD_B_A,
        OP_LD_A_a16, word(IO_PD_CRANK_DOCKED),
        OP_LD_C_A,
        OP_LD_A_a16, word(IO_PD_CRANK_lo),
        OP_LD_H_A,
        OP_LD_a16_A, word(IO_PD_CRANK_lo),
    OP_POP_AF,
    OP_RRC_B,
    OP_JR_nc, r8"menuDoctorInput",

"startMenuDoctorInput",
        -- suppress 'start' and 'B'
        OP_AND_d8, 0xFF & (~0xA),

        -- if crank docked, input 'B' to exit menu
        OP_RRC_C,

        OP_JR_nc, r8"menuDoctorInput",
        OP_OR_d8, 2,

"menuDoctorInput",

    OP_BIT0_H,
    OP_CALL_nz, a16"inputVerticalFromCrank",

    -- restore HL
    OP_PUSH_AF,
        OP_LD_A_a16, word(XRAM_TMP_0),
        OP_LD_H_A,
        OP_LD_A_a16, word(XRAM_TMP_1),
        OP_LD_L_A,
    OP_POP_AF,

    OP_LD_a16_A, word(addr.hJoyInput),
    OP_CALL, word(addr.JoypadLowSensitivity),
    OP_LD_A_a16, word(addr.hJoy5),
    OP_RET,

"inputVerticalFromCrank",
    OP_BIT7_H,
    OP_JR_nz, r8"inputUpFromCrank",

"inputDownFromCrank",
    OP_OR_d8, 0x80,
    OP_RET,

"inputUpFromCrank",
    OP_OR_d8, 0x40,
    OP_RET,
}

patch_res = apply_patch(patch, cave_rom_start, cave_start, cave_size, trainer_res.labels)

for label, addr in pairs(patch_res.labels) do
    printf("  %s: %x", label, addr)
end

-- detour to new check for pause menu
apply_patch(
    {
        farcall(cave_bank, patch_res.labels.CrankUndockPauseMenu),
        OP_LD_A_H,
        OP_NOP,
    },
    addr.CheckPauseMenu
)

-- detour to new pause menu input routine
apply_patch(
    {
        farcall(cave_bank, patch_res.labels.CustomPauseMenuInput),
        OP_LD_A_H,
        OP_LD_B_A,
        OP_JR_nc, r8(addr.PauseMenuInputCheckDown),
        OP_NOP, OP_NOP, OP_NOP,
        OP_NOP, OP_NOP, OP_NOP,
    },
    addr.PauseMenuInput
)

-- detour to new pause menu input routine
apply_patch(
    {
        OP_CALL, word(trainer_res.labels.cavecall),
        word(patch_res.labels.CustomMenuInput),
    },
    addr.HandleMenuInput_GetJoypadState
)

-- don't add "Exit" to menu
apply_patch(
    {
        -- number of options minus 1
        0x5
    },
    sym_to_rom(addr.WrapMenuItemID - 4), sym_to_ram(addr.WrapMenuItemID - 4)
)
apply_patch(
    {
        -- number of options
        0x6
    },
    sym_to_rom(addr.CheckIfPastBottom - 4), sym_to_ram(addr.CheckIfPastBottom - 4)
)
apply_patch(
    {
        -- window size
        0xC
    },
    sym_to_rom(addr.SkipDrawBorder - 14), sym_to_ram(addr.MenuExitDraw - 14)
)
apply_patch(
    {
        -- window size
        0xA
    },
    sym_to_rom(addr.SkipDrawBorder - 5), sym_to_ram(addr.MenuExitDraw - 5)
)
apply_patch(
    {
        -- window size
        0xC
    },
    sym_to_rom(addr.DrawStartMenu + 9), sym_to_ram(addr.MenuExitDraw + 9)
)
apply_patch(
    {
        -- window size (no pokédex)
        0xA
    },
    sym_to_rom(addr.DrawStartMenu + 18), sym_to_ram(addr.MenuExitDraw + 18)
)
apply_patch(
    {
        -- add menu item 'Exit'
        OP_NOP, OP_NOP, OP_NOP,
        OP_NOP, OP_NOP, OP_NOP,
    },
    sym_to_rom(addr.MenuExitDraw + 9), sym_to_ram(addr.MenuExitDraw + 9)
)
