#include <ultra64.h>

#include "sm64.h"
#include "engine/behavior_script.h"
#include "object_helpers.h"
#include "audio/external.h"
#include "print.h"
#include "engine/surface_collision.h"
#include "mario.h"
#include "game_init.h"
#include "main.h"
#include "debug.h"
#include "object_list_processor.h"
#include "behavior_data.h"

#define DEBUG_INFO_NOFLAGS (0 << 0)
#define DEBUG_INFO_FLAG_DPRINT (1 << 0)
#define DEBUG_INFO_FLAG_LSELECT (1 << 1)
#define DEBUG_INFO_FLAG_ALL 0xFF

s16 gDebugPrintState1[6]; // prints top-down?
s16 gDebugPrintState2[6]; // prints bottom-up?

enum DebugPrintStateInfo {
    DEBUG_PSTATE_DISABLED,
    DEBUG_PSTATE_X_CURSOR,
    DEBUG_PSTATE_Y_CURSOR,
    DEBUG_PSTATE_MIN_Y_CURSOR,
    DEBUG_PSTATE_MAX_X_CURSOR,
    DEBUG_PSTATE_LINE_Y_OFFSET
};

// DEBUG_SYS_EFFECTINFO
const char *sDebugEffectStringInfo[] = {
    "  a0 %d", "  a1 %d", "  a2 %d", "  a3 %d", "  a4 %d", "  a5 %d", "  a6 %d", "  a7 %d",
    "A" // cursor
};

// DEBUG_SYS_ENEMYINFO
const char *sDebugEnemyStringInfo[] = {
    "  b0 %d", "  b1 %d", "  b2 %d", "  b3 %d", "  b4 %d", "  b5 %d", "  b6 %d", "  b7 %d",
    "B" // cursor
};

s32 sDebugInfoDPadMask = 0;
s32 sDebugInfoDPadUpdID = 0;
s8 sDebugLvSelectCheckFlag = FALSE;

#define DEBUG_PAGE_MIN DEBUG_PAGE_OBJECTINFO
#define DEBUG_PAGE_MAX DEBUG_PAGE_ENEMYINFO

s8 sDebugPage = DEBUG_PAGE_MIN;
s8 sNoExtraDebug = FALSE;
s8 sDebugStringArrPrinted = FALSE;
s8 sDebugSysCursor = 0;
s8 sDebugInfoButtonSeqID = 0;
s16 sDebugInfoButtonSeq[] = { U_CBUTTONS, L_CBUTTONS, D_CBUTTONS, R_CBUTTONS, -1 };

// most likely present in an ifdef DEBUG build. TODO: check DD version?
void stub_debug_1(void) {
}

void stub_debug_2(void) {
}

void stub_debug_3(void) {
}

void stub_debug_4(void) {
}

/*
 * These 2 functions are called from the object list processor in regards to cycle
 * counts. They likely have stubbed out code that calculated the clock count and
 * its difference for consecutive calls.
 */
s64 get_current_clock(void) {
    s64 wtf = 0;

    return wtf;
}

s64 get_clock_difference(UNUSED s64 arg0) {
    s64 wtf = 0;

    return wtf;
}

/*
 * Set the print state info given a pointer to a print state and the relevent
 * information. Note the reset of the printing boolean. For all intenses and
 * purposes this creates/formats a new print state.
 */
void set_print_state_info(s16 *printState, s16 xCursor, s16 yCursor, s16 minYCursor, s16 maxXCursor,
                          s16 lineYOffset) {
    printState[DEBUG_PSTATE_DISABLED] = FALSE;
    printState[DEBUG_PSTATE_X_CURSOR] = xCursor;
    printState[DEBUG_PSTATE_Y_CURSOR] = yCursor;
    printState[DEBUG_PSTATE_MIN_Y_CURSOR] = minYCursor;
    printState[DEBUG_PSTATE_MAX_X_CURSOR] = maxXCursor;
    printState[DEBUG_PSTATE_LINE_Y_OFFSET] = lineYOffset;
}

/*
 * Take a print state array, string, and the number to print, and use its information to print
 * the next entry in the list. If the current print state array is too far down the list, this
 * will print "DPRINT OVER" instead, signaling that the print state overflowed.
 */
void print_text_array_info(s16 *printState, const char *str, s32 number) {
    if (!printState[DEBUG_PSTATE_DISABLED]) {
        if ((printState[DEBUG_PSTATE_Y_CURSOR] < printState[DEBUG_PSTATE_MIN_Y_CURSOR])
            || (printState[DEBUG_PSTATE_MAX_X_CURSOR] < printState[DEBUG_PSTATE_Y_CURSOR])) {
            print_text(printState[DEBUG_PSTATE_X_CURSOR], printState[DEBUG_PSTATE_Y_CURSOR],
                       "DPRINT OVER");
            printState[DEBUG_PSTATE_DISABLED] += 1; // why not just = TRUE...
        } else {
            print_text_fmt_int(printState[DEBUG_PSTATE_X_CURSOR], printState[DEBUG_PSTATE_Y_CURSOR],
                               str, number);
            printState[DEBUG_PSTATE_Y_CURSOR] += printState[DEBUG_PSTATE_LINE_Y_OFFSET];
        }
    }
}

void set_text_array_x_y(s32 xOffset, s32 yOffset) {
    s16 *printState = gDebugPrintState1;

    printState[DEBUG_PSTATE_X_CURSOR] += xOffset;
    printState[DEBUG_PSTATE_Y_CURSOR] =
        yOffset * printState[DEBUG_PSTATE_LINE_Y_OFFSET] + printState[DEBUG_PSTATE_Y_CURSOR];
}

/*
 * These series of dprint functions print methods depending on the context of the
 * current debug mode as well as the printer array (down to up vs up to down).
 */
void print_debug_bottom_up(const char *str, s32 number) {
    if (gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT) {
        print_text_array_info(gDebugPrintState2, str, number);
    }
}

void print_debug_top_down_objectinfo(const char *str, s32 number) {
    if ((gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT) && sDebugPage == DEBUG_PAGE_OBJECTINFO) {
        print_text_array_info(gDebugPrintState1, str, number);
    }
}

void print_debug_top_down_mapinfo(const char *str, s32 number) {
    if (sNoExtraDebug) { // how come this is the only instance of the sNoExtraDebug check?
        return;
    }

    if (gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT) {
        print_text_array_info(gDebugPrintState1, str, number);
    }
}

void print_debug_top_down_normal(const char *str, s32 number) {
    if (gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT) {
        print_text_array_info(gDebugPrintState1, str, number);
    }
}

#ifndef VERSION_EU
void print_mapinfo(void) {
    struct Surface *pfloor;
    f32 bgY;
    f32 water;
    s32 area;
    s32 angY;

    angY = gCurrentObject->oMoveAngleYaw / 182.044000;
    area = ((s32) gCurrentObject->oPosX + 0x2000) / 1024
           + ((s32) gCurrentObject->oPosZ + 0x2000) / 1024 * 16;

    bgY = find_floor(gCurrentObject->oPosX, gCurrentObject->oPosY, gCurrentObject->oPosZ, &pfloor);
    water = find_water_level(gCurrentObject->oPosX, gCurrentObject->oPosZ);

    print_debug_top_down_normal("mapinfo", 0);
    print_debug_top_down_mapinfo("area %x", area);
    print_debug_top_down_mapinfo("wx   %d", gCurrentObject->oPosX);
    //! Fat finger: programmer hit tab instead of space. Japanese
    // thumb shift keyboards had the tab key next to the spacebar,
    // so this was likely the reason.
    print_debug_top_down_mapinfo("wy\t  %d", gCurrentObject->oPosY);
    print_debug_top_down_mapinfo("wz   %d", gCurrentObject->oPosZ);
    print_debug_top_down_mapinfo("bgY  %d", bgY);
    print_debug_top_down_mapinfo("angY %d", angY);

    if (pfloor) // not null
    {
        print_debug_top_down_mapinfo("bgcode   %d", pfloor->type);
        print_debug_top_down_mapinfo("bgstatus %d", pfloor->flags);
        print_debug_top_down_mapinfo("bgarea   %d", pfloor->room);
    }

    if (gCurrentObject->oPosY < water) {
        print_debug_top_down_mapinfo("water %d", water);
    }
}
#else
void print_mapinfo(void) {
    // EU mostly stubbed this function out.
    struct Surface *pfloor;
    UNUSED f32 bgY;
    UNUSED f32 water;
    UNUSED s32 area;
    // s32 angY;
    //
    // angY = gCurrentObject->oMoveAngleYaw / 182.044000;
    // area  = ((s32)gCurrentObject->oPosX + 0x2000) / 1024
    //      + ((s32)gCurrentObject->oPosZ + 0x2000) / 1024 * 16;
    //
    bgY = find_floor(gCurrentObject->oPosX, gCurrentObject->oPosY, gCurrentObject->oPosZ, &pfloor);
    water = find_water_level(gCurrentObject->oPosX, gCurrentObject->oPosZ);

    print_debug_top_down_normal("mapinfo", 0);
    // print_debug_top_down_mapinfo("area %x", area);
    // print_debug_top_down_mapinfo("wx   %d", gCurrentObject->oPosX);
    // print_debug_top_down_mapinfo("wy\t  %d", gCurrentObject->oPosY);
    // print_debug_top_down_mapinfo("wz   %d", gCurrentObject->oPosZ);
    // print_debug_top_down_mapinfo("bgY  %d", bgY);
    // print_debug_top_down_mapinfo("angY %d", angY);
    //
    // if(pfloor) // not null
    //{
    //    print_debug_top_down_mapinfo("bgcode   %d", pfloor->type);
    //    print_debug_top_down_mapinfo("bgstatus %d", pfloor->flags);
    //    print_debug_top_down_mapinfo("bgarea   %d", pfloor->room);
    //}
    //
    // if(gCurrentObject->oPosY < water)
    //    print_debug_top_down_mapinfo("water %d", water);
}
#endif

void print_checkinfo(void) {
    print_debug_top_down_normal("checkinfo", 0);
}

void print_surfaceinfo(void) {
    debug_surface_list_info(gMarioObject->oPosX, gMarioObject->oPosZ);
}

void print_stageinfo(void) {
    print_debug_top_down_normal("stageinfo", 0);
    print_debug_top_down_normal("stage param %d", gTTCSpeedSetting);
}

/*
 * Common printer function for effectinfo and enemyinfo. This function
 * also prints the cursor functionality intended to be used with modifying
 * gDebugInfo to set enemy/effect behaviors.
 */
void print_string_array_info(const char **strArr) {
    s32 i;

    if (sDebugStringArrPrinted == FALSE) {
        sDebugStringArrPrinted += 1; // again, why not = TRUE...
        for (i = 0; i < 8; i++) {
            // sDebugPage is assumed to be 4 or 5 here.
            print_debug_top_down_mapinfo(strArr[i], gDebugInfo[sDebugPage][i]);
        }
        // modify the cursor position so the cursor prints at the correct location.
        // this is equivalent to (sDebugSysCursor - 8)
        set_text_array_x_y(0, -1 - (u32)(7 - sDebugSysCursor));
        print_debug_top_down_mapinfo(strArr[8], 0); // print the cursor
        set_text_array_x_y(0, 7 - sDebugSysCursor);
    }
}

void print_effectinfo(void) {
    print_debug_top_down_normal("effectinfo", 0);
    print_string_array_info(sDebugEffectStringInfo);
}

void print_enemyinfo(void) {
    print_debug_top_down_normal("enemyinfo", 0);
    print_string_array_info(sDebugEnemyStringInfo);
}

void update_debug_dpadmask(void) {
    s32 dPadMask = gPlayer1Controller->buttonDown & (U_JPAD | D_JPAD | L_JPAD | R_JPAD);

    if (!dPadMask) {
        sDebugInfoDPadUpdID = 0;
        sDebugInfoDPadMask = 0;
    } else {
        // to prevent stuttering of mask updates, the first time is updated 6
        // frames from start, and then every 2 frames when held down.
        if (sDebugInfoDPadUpdID == 0) {
            sDebugInfoDPadMask = dPadMask;
        } else if (sDebugInfoDPadUpdID == 6) {
            sDebugInfoDPadMask = dPadMask;
        } else {
            sDebugInfoDPadMask = 0;
        }
        sDebugInfoDPadUpdID += 1;
        if (sDebugInfoDPadUpdID >= 8) {
            sDebugInfoDPadUpdID = 6; // rapidly set to 6 from 8 as long as dPadMask is being set.
        }
    }
}

void debug_unknown_level_select_check(void) {
    if (!sDebugLvSelectCheckFlag) {
        sDebugLvSelectCheckFlag += 1; // again, just do = TRUE...

        if (!gDebugLevelSelect) {
            gDebugInfoFlags = DEBUG_INFO_NOFLAGS;
        } else {
            gDebugInfoFlags = DEBUG_INFO_FLAG_LSELECT;
        }

        gNumCalls.floor = 0;
        gNumCalls.ceil = 0;
        gNumCalls.wall = 0;
    }
}

void reset_debug_objectinfo(void) {
    gNumFindFloorMisses = 0;
    gUnknownWallCount = 0;
    gObjectCounter = 0;
    sDebugStringArrPrinted = FALSE;
    D_8035FEE2 = 0;
    D_8035FEE4 = 0;

    set_print_state_info(gDebugPrintState1, 20, 185, 40, 200, -15);
    set_print_state_info(gDebugPrintState2, 180, 30, 0, 150, 15);
    update_debug_dpadmask();
}

/*
 * This function checks for a button sequence (C Up, C Left, C Down,
 * C Right) and then toggles the debug flags from FF to 2; 2 is unused,
 * despite so this has no effect, being called. (unused)
 */
static void check_debug_button_seq(void) {
    s16 *buttonArr;
    s16 cButtonMask;

    buttonArr = sDebugInfoButtonSeq;

    if (!(gPlayer1Controller->buttonDown & L_TRIG)) {
        sDebugInfoButtonSeqID = 0;
    } else {
        if ((s16)(cButtonMask = (gPlayer1Controller->buttonPressed & C_BUTTONS))) {
            if (buttonArr[sDebugInfoButtonSeqID] == cButtonMask) {
                sDebugInfoButtonSeqID += 1;
                if (buttonArr[sDebugInfoButtonSeqID] == -1) {
                    if (gDebugInfoFlags == DEBUG_INFO_FLAG_ALL) {
                        gDebugInfoFlags = DEBUG_INFO_FLAG_LSELECT;
                    } else {
                        gDebugInfoFlags = DEBUG_INFO_FLAG_ALL;
                    }
                }
            } else {
                sDebugInfoButtonSeqID = 0;
            }
        }
    }
}

/*
 * Poll the debug info flags and controller for appropriate presses that
 * control sDebugPage's range. (unused)
 */
static void try_change_debug_page(void) {
    if (gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT) {
        if ((gPlayer1Controller->buttonPressed & L_JPAD)
            && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) {
            sDebugPage += 1;
        }
        if ((gPlayer1Controller->buttonPressed & R_JPAD)
            && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) {
            sDebugPage -= 1;
        }
        if (sDebugPage >= (DEBUG_PAGE_MAX + 1)) {
            sDebugPage = DEBUG_PAGE_MIN;
        }
        if (sDebugPage < DEBUG_PAGE_MIN) {
            sDebugPage = DEBUG_PAGE_MAX;
        }
    }
}

/*
 * Use the controller to modify gDebugInfo and the sys cursor while
 * using the DPad. L/R DPad modifies gDebugInfo while U/D modifies
 * sDebugSysCursor. This is used to adjust enemy and effect behaviors
 * on the fly. (unused)
 */
#ifdef VERSION_SH
static
#endif
void try_modify_debug_controls(void) {
    s32 sp4;

    if (gPlayer1Controller->buttonPressed & Z_TRIG) {
        sNoExtraDebug ^= 1;
    }
    if (!(gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG)) && sNoExtraDebug == FALSE) {
        sp4 = 1;
        if (gPlayer1Controller->buttonDown & B_BUTTON) {
            sp4 = 100;
        }

        if (sDebugInfoDPadMask & U_JPAD) {
            sDebugSysCursor -= 1;
            if (sDebugSysCursor < 0) {
                sDebugSysCursor = 0;
            }
        }

        if (sDebugInfoDPadMask & D_JPAD) {
            sDebugSysCursor += 1;
            if (sDebugSysCursor >= 8) {
                sDebugSysCursor = 7;
            }
        }

        if (sDebugInfoDPadMask & L_JPAD) {
            // we allow the player while in this mode to modify the debug controls. This is
            // so the playtester can adjust enemy behavior and parameters on the fly, since
            // various behaviors try to update their behaviors from gDebugInfo[4] and [5].
            if (gPlayer1Controller->buttonDown & A_BUTTON) {
                gDebugInfo[sDebugPage][sDebugSysCursor] =
                    gDebugInfoOverwrite[sDebugPage][sDebugSysCursor];
            } else {
                gDebugInfo[sDebugPage][sDebugSysCursor] = gDebugInfo[sDebugPage][sDebugSysCursor] - sp4;
            }
        }

        if (sDebugInfoDPadMask & R_JPAD) {
            gDebugInfo[sDebugPage][sDebugSysCursor] = gDebugInfo[sDebugPage][sDebugSysCursor] + sp4;
        }
    }
}

// possibly a removed debug control (TODO: check DD)
void stub_debug_5(void) {
}

/*
 * If Mario's object exists, this function tries to print available object debug
 * information depending on the debug sys ID. Additional information (updated obj
 * count, floor misses, and an unknown wall counter) is also printed.
 */
void try_print_debug_mario_object_info(void) {
    if (gMarioObject != NULL) {
        switch (sDebugPage) {
            case DEBUG_PAGE_CHECKSURFACEINFO:
                print_surfaceinfo();
                break;
            case DEBUG_PAGE_EFFECTINFO:
                print_effectinfo();
                break;
            case DEBUG_PAGE_ENEMYINFO:
                print_enemyinfo();
                break;
            default:
                break;
        }
    }

    print_debug_top_down_mapinfo("obj  %d", gObjectCounter);

    if (gNumFindFloorMisses) {
        print_debug_bottom_up("NULLBG %d", gNumFindFloorMisses);
    }

    if (gUnknownWallCount) {
        print_debug_bottom_up("WALL   %d", gUnknownWallCount);
    }
}

/*
 * Similar to above, but with level information. (checkinfo, mapinfo,
 * stageinfo)
 */
void try_print_debug_mario_level_info(void) {
    switch (sDebugPage) {
        case DEBUG_PAGE_OBJECTINFO:
            break; // no info list is printed for obj info.
        case DEBUG_PAGE_CHECKSURFACEINFO:
            print_checkinfo();
            break;
        case DEBUG_PAGE_MAPINFO:
            print_mapinfo();
            break;
        case DEBUG_PAGE_STAGEINFO:
            print_stageinfo();
            break;
        default:
            break;
    }
}

/*
 * One of the only remaining debug controls activatable from the
 * debug control array. This function lets you spawn 1 of 3
 * objects: A koopa shell, a jumping box, and an underwater koopa
 * shell. This can be reactivated by turning on modifying
 * debug controls with try_modify_debug_controls and setting
 * [5][7] (b7 in the string array) to 1 to enable debug spawn.
 */
void try_do_mario_debug_object_spawn(void) {
    UNUSED s32 unused;

    if (sDebugPage == DEBUG_PAGE_STAGEINFO && gDebugInfo[DEBUG_PAGE_ENEMYINFO][7] == 1) {
        if (gPlayer1Controller->buttonPressed & R_JPAD) {
            spawn_object_relative(0, 0, 100, 200, gCurrentObject, MODEL_KOOPA_SHELL, bhvKoopaShell);
        }
        if (gPlayer1Controller->buttonPressed & L_JPAD) {
            spawn_object_relative(0, 0, 100, 200, gCurrentObject, MODEL_BREAKABLE_BOX_SMALL,
                                  bhvJumpingBox);
        }
        if (gPlayer1Controller->buttonPressed & D_JPAD) {
            spawn_object_relative(0, 0, 100, 200, gCurrentObject, MODEL_KOOPA_SHELL,
                                  bhvKoopaShellUnderwater);
        }
    }
}

// TODO: figure out what this is
#ifdef VERSION_SH
static
#endif
void debug_print_obj_move_flags(void) {
#ifndef VERSION_EU // TODO: Is there a better way to diff this? static EU doesn't seem to work.
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_LANDED) {
        print_debug_top_down_objectinfo("BOUND   %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_ON_GROUND) {
        print_debug_top_down_objectinfo("TOUCH   %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_LEFT_GROUND) {
        print_debug_top_down_objectinfo("TAKEOFF %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_ENTERED_WATER) {
        print_debug_top_down_objectinfo("DIVE    %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_AT_WATER_SURFACE) {
        print_debug_top_down_objectinfo("S WATER %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_UNDERWATER_OFF_GROUND) {
        print_debug_top_down_objectinfo("U WATER %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_UNDERWATER_ON_GROUND) {
        print_debug_top_down_objectinfo("B WATER %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_IN_AIR) {
        print_debug_top_down_objectinfo("SKY     %x", gCurrentObject->oMoveFlags);
    }
    if (gCurrentObject->oMoveFlags & OBJ_MOVE_8) {
        print_debug_top_down_objectinfo("OUT SCOPE %x", gCurrentObject->oMoveFlags);
    }
#endif
}

// unused, what is this?
void debug_enemy_unknown(s16 *enemyArr) {
    // copy b1-b4 over to an unknown s16 array
    enemyArr[4] = gDebugInfo[DEBUG_PAGE_ENEMYINFO][1];
    enemyArr[5] = gDebugInfo[DEBUG_PAGE_ENEMYINFO][2];
    enemyArr[6] = gDebugInfo[DEBUG_PAGE_ENEMYINFO][3];
    enemyArr[7] = gDebugInfo[DEBUG_PAGE_ENEMYINFO][4];
}
