#define _CRT_SECURE_NO_WARNINGS 1 // for fopen

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>

#include "../src/libsm64.h"

extern "C"
{
    #define SDL_MAIN_HANDLED
    #include "level.h"
    #include "context.h"
    #include "renderer.h"
    #include "gl33core/gl33core_renderer.h"
    #include "gl20/gl20_renderer.h"
}

#include "imgui/imgui.h"
#include "imgui/backends/imgui_impl_sdl2.h"
#include "imgui/backends/imgui_impl_opengl3.h"

#include "audio.h"
#include "sm64.h"

#include "../src/decomp/include/types.h"

struct FlagEntry
{
    uint32_t bit;
    const char *name;
};

static const FlagEntry sMarioFlags[] = {
    {MARIO_NORMAL_CAP, "MARIO_NORMAL_CAP"},
    {MARIO_VANISH_CAP, "MARIO_VANISH_CAP"},
    {MARIO_METAL_CAP, "MARIO_METAL_CAP"},
    {MARIO_WING_CAP, "MARIO_WING_CAP"},
    {MARIO_CAP_ON_HEAD, "MARIO_CAP_ON_HEAD"},
    {MARIO_CAP_IN_HAND, "MARIO_CAP_IN_HAND"},
    {MARIO_METAL_SHOCK, "MARIO_METAL_SHOCK"},
    {MARIO_TELEPORTING, "MARIO_TELEPORTING"},
    {MARIO_UNKNOWN_08, "MARIO_UNKNOWN_08"},
    {MARIO_UNKNOWN_13, "MARIO_UNKNOWN_13"},
    {MARIO_ACTION_SOUND_PLAYED, "MARIO_ACTION_SOUND_PLAYED"},
    {MARIO_MARIO_SOUND_PLAYED, "MARIO_MARIO_SOUND_PLAYED"},
    {MARIO_UNKNOWN_18, "MARIO_UNKNOWN_18"},
    {MARIO_PUNCHING, "MARIO_PUNCHING"},
    {MARIO_KICKING, "MARIO_KICKING"},
    {MARIO_TRIPPING, "MARIO_TRIPPING"},
    {MARIO_UNKNOWN_25, "MARIO_UNKNOWN_25"},
    {MARIO_UNKNOWN_30, "MARIO_UNKNOWN_30"},
    {MARIO_UNKNOWN_31, "MARIO_UNKNOWN_31"},
};

static const FlagEntry sParticleFlags[] = {
    {PARTICLE_DUST, "PARTICLE_DUST"},
    {PARTICLE_VERTICAL_STAR, "PARTICLE_VERTICAL_STAR"},
    {PARTICLE_2, "PARTICLE_2"},
    {PARTICLE_SPARKLES, "PARTICLE_SPARKLES"},
    {PARTICLE_HORIZONTAL_STAR, "PARTICLE_HORIZONTAL_STAR"},
    {PARTICLE_BUBBLE, "PARTICLE_BUBBLE"},
    {PARTICLE_WATER_SPLASH, "PARTICLE_WATER_SPLASH"},
    {PARTICLE_IDLE_WATER_WAVE, "PARTICLE_IDLE_WATER_WAVE"},
    {PARTICLE_SHALLOW_WATER_WAVE, "PARTICLE_SHALLOW_WATER_WAVE"},
    {PARTICLE_PLUNGE_BUBBLE, "PARTICLE_PLUNGE_BUBBLE"},
    {PARTICLE_WAVE_TRAIL, "PARTICLE_WAVE_TRAIL"},
    {PARTICLE_FIRE, "PARTICLE_FIRE"},
    {PARTICLE_SHALLOW_WATER_SPLASH, "PARTICLE_SHALLOW_WATER_SPLASH"},
    {PARTICLE_LEAF, "PARTICLE_LEAF"},
    {PARTICLE_SNOW, "PARTICLE_SNOW"},
    {PARTICLE_DIRT, "PARTICLE_DIRT"},
    {PARTICLE_MIST_CIRCLE, "PARTICLE_MIST_CIRCLE"},
    {PARTICLE_BREATH, "PARTICLE_BREATH"},
    {PARTICLE_TRIANGLE, "PARTICLE_TRIANGLE"},
    {PARTICLE_19, "PARTICLE_19"},
};

static const char *GetActionName(uint32_t action)
{
    switch (action)
    {
        case ACT_UNINITIALIZED:
            return "ACT_UNINITIALIZED";
        // stationary
        case ACT_IDLE:
            return "ACT_IDLE";
        case ACT_START_SLEEPING:
            return "ACT_START_SLEEPING";
        case ACT_SLEEPING:
            return "ACT_SLEEPING";
        case ACT_WAKING_UP:
            return "ACT_WAKING_UP";
        case ACT_PANTING:
            return "ACT_PANTING";
        case ACT_HOLD_PANTING_UNUSED:
            return "ACT_HOLD_PANTING_UNUSED";
        case ACT_HOLD_IDLE:
            return "ACT_HOLD_IDLE";
        case ACT_HOLD_HEAVY_IDLE:
            return "ACT_HOLD_HEAVY_IDLE";
        case ACT_STANDING_AGAINST_WALL:
            return "ACT_STANDING_AGAINST_WALL";
        case ACT_COUGHING:
            return "ACT_COUGHING";
        case ACT_SHIVERING:
            return "ACT_SHIVERING";
        case ACT_IN_QUICKSAND:
            return "ACT_IN_QUICKSAND";
        case ACT_UNKNOWN_0002020E:
            return "ACT_UNKNOWN_0002020E";
        case ACT_CROUCHING:
            return "ACT_CROUCHING";
        case ACT_START_CROUCHING:
            return "ACT_START_CROUCHING";
        case ACT_STOP_CROUCHING:
            return "ACT_STOP_CROUCHING";
        case ACT_START_CRAWLING:
            return "ACT_START_CRAWLING";
        case ACT_STOP_CRAWLING:
            return "ACT_STOP_CRAWLING";
        case ACT_SLIDE_KICK_SLIDE_STOP:
            return "ACT_SLIDE_KICK_SLIDE_STOP";
        case ACT_SHOCKWAVE_BOUNCE:
            return "ACT_SHOCKWAVE_BOUNCE";
        case ACT_FIRST_PERSON:
            return "ACT_FIRST_PERSON";
        case ACT_BACKFLIP_LAND_STOP:
            return "ACT_BACKFLIP_LAND_STOP";
        case ACT_JUMP_LAND_STOP:
            return "ACT_JUMP_LAND_STOP";
        case ACT_DOUBLE_JUMP_LAND_STOP:
            return "ACT_DOUBLE_JUMP_LAND_STOP";
        case ACT_FREEFALL_LAND_STOP:
            return "ACT_FREEFALL_LAND_STOP";
        case ACT_SIDE_FLIP_LAND_STOP:
            return "ACT_SIDE_FLIP_LAND_STOP";
        case ACT_HOLD_JUMP_LAND_STOP:
            return "ACT_HOLD_JUMP_LAND_STOP";
        case ACT_HOLD_FREEFALL_LAND_STOP:
            return "ACT_HOLD_FREEFALL_LAND_STOP";
        case ACT_AIR_THROW_LAND:
            return "ACT_AIR_THROW_LAND";
        case ACT_TWIRL_LAND:
            return "ACT_TWIRL_LAND";
        case ACT_LAVA_BOOST_LAND:
            return "ACT_LAVA_BOOST_LAND";
        case ACT_TRIPLE_JUMP_LAND_STOP:
            return "ACT_TRIPLE_JUMP_LAND_STOP";
        case ACT_LONG_JUMP_LAND_STOP:
            return "ACT_LONG_JUMP_LAND_STOP";
        case ACT_GROUND_POUND_LAND:
            return "ACT_GROUND_POUND_LAND";
        case ACT_BRAKING_STOP:
            return "ACT_BRAKING_STOP";
        case ACT_BUTT_SLIDE_STOP:
            return "ACT_BUTT_SLIDE_STOP";
        case ACT_HOLD_BUTT_SLIDE_STOP:
            return "ACT_HOLD_BUTT_SLIDE_STOP";
        // moving
        case ACT_WALKING:
            return "ACT_WALKING";
        case ACT_HOLD_WALKING:
            return "ACT_HOLD_WALKING";
        case ACT_TURNING_AROUND:
            return "ACT_TURNING_AROUND";
        case ACT_FINISH_TURNING_AROUND:
            return "ACT_FINISH_TURNING_AROUND";
        case ACT_BRAKING:
            return "ACT_BRAKING";
        case ACT_RIDING_SHELL_GROUND:
            return "ACT_RIDING_SHELL_GROUND";
        case ACT_HOLD_HEAVY_WALKING:
            return "ACT_HOLD_HEAVY_WALKING";
        case ACT_CRAWLING:
            return "ACT_CRAWLING";
        case ACT_BURNING_GROUND:
            return "ACT_BURNING_GROUND";
        case ACT_DECELERATING:
            return "ACT_DECELERATING";
        case ACT_HOLD_DECELERATING:
            return "ACT_HOLD_DECELERATING";
        case ACT_BEGIN_SLIDING:
            return "ACT_BEGIN_SLIDING";
        case ACT_HOLD_BEGIN_SLIDING:
            return "ACT_HOLD_BEGIN_SLIDING";
        case ACT_BUTT_SLIDE:
            return "ACT_BUTT_SLIDE";
        case ACT_STOMACH_SLIDE:
            return "ACT_STOMACH_SLIDE";
        case ACT_HOLD_BUTT_SLIDE:
            return "ACT_HOLD_BUTT_SLIDE";
        case ACT_HOLD_STOMACH_SLIDE:
            return "ACT_HOLD_STOMACH_SLIDE";
        case ACT_DIVE_SLIDE:
            return "ACT_DIVE_SLIDE";
        case ACT_MOVE_PUNCHING:
            return "ACT_MOVE_PUNCHING";
        case ACT_CROUCH_SLIDE:
            return "ACT_CROUCH_SLIDE";
        case ACT_SLIDE_KICK_SLIDE:
            return "ACT_SLIDE_KICK_SLIDE";
        case ACT_HARD_BACKWARD_GROUND_KB:
            return "ACT_HARD_BACKWARD_GROUND_KB";
        case ACT_HARD_FORWARD_GROUND_KB:
            return "ACT_HARD_FORWARD_GROUND_KB";
        case ACT_BACKWARD_GROUND_KB:
            return "ACT_BACKWARD_GROUND_KB";
        case ACT_FORWARD_GROUND_KB:
            return "ACT_FORWARD_GROUND_KB";
        case ACT_SOFT_BACKWARD_GROUND_KB:
            return "ACT_SOFT_BACKWARD_GROUND_KB";
        case ACT_SOFT_FORWARD_GROUND_KB:
            return "ACT_SOFT_FORWARD_GROUND_KB";
        case ACT_GROUND_BONK:
            return "ACT_GROUND_BONK";
        case ACT_DEATH_EXIT_LAND:
            return "ACT_DEATH_EXIT_LAND";
        case ACT_JUMP_LAND:
            return "ACT_JUMP_LAND";
        case ACT_FREEFALL_LAND:
            return "ACT_FREEFALL_LAND";
        case ACT_DOUBLE_JUMP_LAND:
            return "ACT_DOUBLE_JUMP_LAND";
        case ACT_SIDE_FLIP_LAND:
            return "ACT_SIDE_FLIP_LAND";
        case ACT_HOLD_JUMP_LAND:
            return "ACT_HOLD_JUMP_LAND";
        case ACT_HOLD_FREEFALL_LAND:
            return "ACT_HOLD_FREEFALL_LAND";
        case ACT_QUICKSAND_JUMP_LAND:
            return "ACT_QUICKSAND_JUMP_LAND";
        case ACT_HOLD_QUICKSAND_JUMP_LAND:
            return "ACT_HOLD_QUICKSAND_JUMP_LAND";
        case ACT_TRIPLE_JUMP_LAND:
            return "ACT_TRIPLE_JUMP_LAND";
        case ACT_LONG_JUMP_LAND:
            return "ACT_LONG_JUMP_LAND";
        case ACT_BACKFLIP_LAND:
            return "ACT_BACKFLIP_LAND";
        // airborne
        case ACT_JUMP:
            return "ACT_JUMP";
        case ACT_DOUBLE_JUMP:
            return "ACT_DOUBLE_JUMP";
        case ACT_TRIPLE_JUMP:
            return "ACT_TRIPLE_JUMP";
        case ACT_BACKFLIP:
            return "ACT_BACKFLIP";
        case ACT_STEEP_JUMP:
            return "ACT_STEEP_JUMP";
        case ACT_WALL_KICK_AIR:
            return "ACT_WALL_KICK_AIR";
        case ACT_SIDE_FLIP:
            return "ACT_SIDE_FLIP";
        case ACT_LONG_JUMP:
            return "ACT_LONG_JUMP";
        case ACT_WATER_JUMP:
            return "ACT_WATER_JUMP";
        case ACT_DIVE:
            return "ACT_DIVE";
        case ACT_FREEFALL:
            return "ACT_FREEFALL";
        case ACT_TOP_OF_POLE_JUMP:
            return "ACT_TOP_OF_POLE_JUMP";
        case ACT_BUTT_SLIDE_AIR:
            return "ACT_BUTT_SLIDE_AIR";
        case ACT_FLYING_TRIPLE_JUMP:
            return "ACT_FLYING_TRIPLE_JUMP";
        case ACT_SHOT_FROM_CANNON:
            return "ACT_SHOT_FROM_CANNON";
        case ACT_FLYING:
            return "ACT_FLYING";
        case ACT_RIDING_SHELL_JUMP:
            return "ACT_RIDING_SHELL_JUMP";
        case ACT_RIDING_SHELL_FALL:
            return "ACT_RIDING_SHELL_FALL";
        case ACT_VERTICAL_WIND:
            return "ACT_VERTICAL_WIND";
        case ACT_HOLD_JUMP:
            return "ACT_HOLD_JUMP";
        case ACT_HOLD_FREEFALL:
            return "ACT_HOLD_FREEFALL";
        case ACT_HOLD_BUTT_SLIDE_AIR:
            return "ACT_HOLD_BUTT_SLIDE_AIR";
        case ACT_HOLD_WATER_JUMP:
            return "ACT_HOLD_WATER_JUMP";
        case ACT_TWIRLING:
            return "ACT_TWIRLING";
        case ACT_FORWARD_ROLLOUT:
            return "ACT_FORWARD_ROLLOUT";
        case ACT_AIR_HIT_WALL:
            return "ACT_AIR_HIT_WALL";
        case ACT_RIDING_HOOT:
            return "ACT_RIDING_HOOT";
        case ACT_GROUND_POUND:
            return "ACT_GROUND_POUND";
        case ACT_SLIDE_KICK:
            return "ACT_SLIDE_KICK";
        case ACT_AIR_THROW:
            return "ACT_AIR_THROW";
        case ACT_JUMP_KICK:
            return "ACT_JUMP_KICK";
        case ACT_BACKWARD_ROLLOUT:
            return "ACT_BACKWARD_ROLLOUT";
        case ACT_CRAZY_BOX_BOUNCE:
            return "ACT_CRAZY_BOX_BOUNCE";
        case ACT_SPECIAL_TRIPLE_JUMP:
            return "ACT_SPECIAL_TRIPLE_JUMP";
        case ACT_BACKWARD_AIR_KB:
            return "ACT_BACKWARD_AIR_KB";
        case ACT_FORWARD_AIR_KB:
            return "ACT_FORWARD_AIR_KB";
        case ACT_HARD_FORWARD_AIR_KB:
            return "ACT_HARD_FORWARD_AIR_KB";
        case ACT_HARD_BACKWARD_AIR_KB:
            return "ACT_HARD_BACKWARD_AIR_KB";
        case ACT_BURNING_JUMP:
            return "ACT_BURNING_JUMP";
        case ACT_BURNING_FALL:
            return "ACT_BURNING_FALL";
        case ACT_SOFT_BONK:
            return "ACT_SOFT_BONK";
        case ACT_LAVA_BOOST:
            return "ACT_LAVA_BOOST";
        case ACT_GETTING_BLOWN:
            return "ACT_GETTING_BLOWN";
        case ACT_THROWN_FORWARD:
            return "ACT_THROWN_FORWARD";
        case ACT_THROWN_BACKWARD:
            return "ACT_THROWN_BACKWARD";
        // submerged
        case ACT_WATER_IDLE:
            return "ACT_WATER_IDLE";
        case ACT_HOLD_WATER_IDLE:
            return "ACT_HOLD_WATER_IDLE";
        case ACT_WATER_ACTION_END:
            return "ACT_WATER_ACTION_END";
        case ACT_HOLD_WATER_ACTION_END:
            return "ACT_HOLD_WATER_ACTION_END";
        case ACT_DROWNING:
            return "ACT_DROWNING";
        case ACT_BACKWARD_WATER_KB:
            return "ACT_BACKWARD_WATER_KB";
        case ACT_FORWARD_WATER_KB:
            return "ACT_FORWARD_WATER_KB";
        case ACT_WATER_DEATH:
            return "ACT_WATER_DEATH";
        case ACT_WATER_SHOCKED:
            return "ACT_WATER_SHOCKED";
        case ACT_BREASTSTROKE:
            return "ACT_BREASTSTROKE";
        case ACT_SWIMMING_END:
            return "ACT_SWIMMING_END";
        case ACT_FLUTTER_KICK:
            return "ACT_FLUTTER_KICK";
        case ACT_HOLD_BREASTSTROKE:
            return "ACT_HOLD_BREASTSTROKE";
        case ACT_HOLD_SWIMMING_END:
            return "ACT_HOLD_SWIMMING_END";
        case ACT_HOLD_FLUTTER_KICK:
            return "ACT_HOLD_FLUTTER_KICK";
        case ACT_WATER_SHELL_SWIMMING:
            return "ACT_WATER_SHELL_SWIMMING";
        case ACT_WATER_THROW:
            return "ACT_WATER_THROW";
        case ACT_WATER_PUNCH:
            return "ACT_WATER_PUNCH";
        case ACT_WATER_PLUNGE:
            return "ACT_WATER_PLUNGE";
        case ACT_CAUGHT_IN_WHIRLPOOL:
            return "ACT_CAUGHT_IN_WHIRLPOOL";
        case ACT_METAL_WATER_STANDING:
            return "ACT_METAL_WATER_STANDING";
        case ACT_HOLD_METAL_WATER_STANDING:
            return "ACT_HOLD_METAL_WATER_STANDING";
        case ACT_METAL_WATER_WALKING:
            return "ACT_METAL_WATER_WALKING";
        case ACT_HOLD_METAL_WATER_WALKING:
            return "ACT_HOLD_METAL_WATER_WALKING";
        case ACT_METAL_WATER_FALLING:
            return "ACT_METAL_WATER_FALLING";
        case ACT_HOLD_METAL_WATER_FALLING:
            return "ACT_HOLD_METAL_WATER_FALLING";
        case ACT_METAL_WATER_FALL_LAND:
            return "ACT_METAL_WATER_FALL_LAND";
        case ACT_HOLD_METAL_WATER_FALL_LAND:
            return "ACT_HOLD_METAL_WATER_FALL_LAND";
        case ACT_METAL_WATER_JUMP:
            return "ACT_METAL_WATER_JUMP";
        case ACT_HOLD_METAL_WATER_JUMP:
            return "ACT_HOLD_METAL_WATER_JUMP";
        case ACT_METAL_WATER_JUMP_LAND:
            return "ACT_METAL_WATER_JUMP_LAND";
        case ACT_HOLD_METAL_WATER_JUMP_LAND:
            return "ACT_HOLD_METAL_WATER_JUMP_LAND";
        // cutscene
        case ACT_DISAPPEARED:
            return "ACT_DISAPPEARED";
        case ACT_INTRO_CUTSCENE:
            return "ACT_INTRO_CUTSCENE";
        case ACT_STAR_DANCE_EXIT:
            return "ACT_STAR_DANCE_EXIT";
        case ACT_STAR_DANCE_WATER:
            return "ACT_STAR_DANCE_WATER";
        case ACT_FALL_AFTER_STAR_GRAB:
            return "ACT_FALL_AFTER_STAR_GRAB";
        case ACT_READING_AUTOMATIC_DIALOG:
            return "ACT_READING_AUTOMATIC_DIALOG";
        case ACT_READING_NPC_DIALOG:
            return "ACT_READING_NPC_DIALOG";
        case ACT_STAR_DANCE_NO_EXIT:
            return "ACT_STAR_DANCE_NO_EXIT";
        case ACT_READING_SIGN:
            return "ACT_READING_SIGN";
        case ACT_JUMBO_STAR_CUTSCENE:
            return "ACT_JUMBO_STAR_CUTSCENE";
        case ACT_WAITING_FOR_DIALOG:
            return "ACT_WAITING_FOR_DIALOG";
        case ACT_DEBUG_FREE_MOVE:
            return "ACT_DEBUG_FREE_MOVE";
        case ACT_STANDING_DEATH:
            return "ACT_STANDING_DEATH";
        case ACT_QUICKSAND_DEATH:
            return "ACT_QUICKSAND_DEATH";
        case ACT_ELECTROCUTION:
            return "ACT_ELECTROCUTION";
        case ACT_SUFFOCATION:
            return "ACT_SUFFOCATION";
        case ACT_DEATH_ON_STOMACH:
            return "ACT_DEATH_ON_STOMACH";
        case ACT_DEATH_ON_BACK:
            return "ACT_DEATH_ON_BACK";
        case ACT_EATEN_BY_BUBBA:
            return "ACT_EATEN_BY_BUBBA";
        case ACT_END_PEACH_CUTSCENE:
            return "ACT_END_PEACH_CUTSCENE";
        case ACT_CREDITS_CUTSCENE:
            return "ACT_CREDITS_CUTSCENE";
        case ACT_END_WAVING_CUTSCENE:
            return "ACT_END_WAVING_CUTSCENE";
        case ACT_PULLING_DOOR:
            return "ACT_PULLING_DOOR";
        case ACT_PUSHING_DOOR:
            return "ACT_PUSHING_DOOR";
        case ACT_WARP_DOOR_SPAWN:
            return "ACT_WARP_DOOR_SPAWN";
        case ACT_EMERGE_FROM_PIPE:
            return "ACT_EMERGE_FROM_PIPE";
        case ACT_SPAWN_SPIN_AIRBORNE:
            return "ACT_SPAWN_SPIN_AIRBORNE";
        case ACT_SPAWN_SPIN_LANDING:
            return "ACT_SPAWN_SPIN_LANDING";
        case ACT_EXIT_AIRBORNE:
            return "ACT_EXIT_AIRBORNE";
        case ACT_EXIT_LAND_SAVE_DIALOG:
            return "ACT_EXIT_LAND_SAVE_DIALOG";
        case ACT_DEATH_EXIT:
            return "ACT_DEATH_EXIT";
        case ACT_UNUSED_DEATH_EXIT:
            return "ACT_UNUSED_DEATH_EXIT";
        case ACT_FALLING_DEATH_EXIT:
            return "ACT_FALLING_DEATH_EXIT";
        case ACT_SPECIAL_EXIT_AIRBORNE:
            return "ACT_SPECIAL_EXIT_AIRBORNE";
        case ACT_SPECIAL_DEATH_EXIT:
            return "ACT_SPECIAL_DEATH_EXIT";
        case ACT_FALLING_EXIT_AIRBORNE:
            return "ACT_FALLING_EXIT_AIRBORNE";
        case ACT_UNLOCKING_KEY_DOOR:
            return "ACT_UNLOCKING_KEY_DOOR";
        case ACT_UNLOCKING_STAR_DOOR:
            return "ACT_UNLOCKING_STAR_DOOR";
        case ACT_ENTERING_STAR_DOOR:
            return "ACT_ENTERING_STAR_DOOR";
        case ACT_SPAWN_NO_SPIN_AIRBORNE:
            return "ACT_SPAWN_NO_SPIN_AIRBORNE";
        case ACT_SPAWN_NO_SPIN_LANDING:
            return "ACT_SPAWN_NO_SPIN_LANDING";
        case ACT_BBH_ENTER_JUMP:
            return "ACT_BBH_ENTER_JUMP";
        case ACT_BBH_ENTER_SPIN:
            return "ACT_BBH_ENTER_SPIN";
        case ACT_TELEPORT_FADE_OUT:
            return "ACT_TELEPORT_FADE_OUT";
        case ACT_TELEPORT_FADE_IN:
            return "ACT_TELEPORT_FADE_IN";
        case ACT_SHOCKED:
            return "ACT_SHOCKED";
        case ACT_SQUISHED:
            return "ACT_SQUISHED";
        case ACT_HEAD_STUCK_IN_GROUND:
            return "ACT_HEAD_STUCK_IN_GROUND";
        case ACT_BUTT_STUCK_IN_GROUND:
            return "ACT_BUTT_STUCK_IN_GROUND";
        case ACT_FEET_STUCK_IN_GROUND:
            return "ACT_FEET_STUCK_IN_GROUND";
        case ACT_PUTTING_ON_CAP:
            return "ACT_PUTTING_ON_CAP";
        // automatic
        case ACT_HOLDING_POLE:
            return "ACT_HOLDING_POLE";
        case ACT_GRAB_POLE_SLOW:
            return "ACT_GRAB_POLE_SLOW";
        case ACT_GRAB_POLE_FAST:
            return "ACT_GRAB_POLE_FAST";
        case ACT_CLIMBING_POLE:
            return "ACT_CLIMBING_POLE";
        case ACT_TOP_OF_POLE_TRANSITION:
            return "ACT_TOP_OF_POLE_TRANSITION";
        case ACT_TOP_OF_POLE:
            return "ACT_TOP_OF_POLE";
        case ACT_START_HANGING:
            return "ACT_START_HANGING";
        case ACT_HANGING:
            return "ACT_HANGING";
        case ACT_HANG_MOVING:
            return "ACT_HANG_MOVING";
        case ACT_LEDGE_GRAB:
            return "ACT_LEDGE_GRAB";
        case ACT_LEDGE_CLIMB_SLOW_1:
            return "ACT_LEDGE_CLIMB_SLOW_1";
        case ACT_LEDGE_CLIMB_SLOW_2:
            return "ACT_LEDGE_CLIMB_SLOW_2";
        case ACT_LEDGE_CLIMB_DOWN:
            return "ACT_LEDGE_CLIMB_DOWN";
        case ACT_LEDGE_CLIMB_FAST:
            return "ACT_LEDGE_CLIMB_FAST";
        case ACT_GRABBED:
            return "ACT_GRABBED";
        case ACT_IN_CANNON:
            return "ACT_IN_CANNON";
        case ACT_TORNADO_TWIRLING:
            return "ACT_TORNADO_TWIRLING";
        // object
        case ACT_PUNCHING:
            return "ACT_PUNCHING";
        case ACT_PICKING_UP:
            return "ACT_PICKING_UP";
        case ACT_DIVE_PICKING_UP:
            return "ACT_DIVE_PICKING_UP";
        case ACT_STOMACH_SLIDE_STOP:
            return "ACT_STOMACH_SLIDE_STOP";
        case ACT_PLACING_DOWN:
            return "ACT_PLACING_DOWN";
        case ACT_THROWING:
            return "ACT_THROWING";
        case ACT_HEAVY_THROW:
            return "ACT_HEAVY_THROW";
        case ACT_PICKING_UP_BOWSER:
            return "ACT_PICKING_UP_BOWSER";
        case ACT_HOLDING_BOWSER:
            return "ACT_HOLDING_BOWSER";
        case ACT_RELEASING_BOWSER:
            return "ACT_RELEASING_BOWSER";
        default:
            return "ACT_UNKNOWN";
    }
}

static void DrawBitFlags(
    const char *label, uint32_t value,
    const FlagEntry *table, int count)
{
    ImGui::Text("%s: 0x%08X", label, value);
    ImGui::Indent();
    for (int i = 0; i < count; i++)
    {
        bool set = (value & table[i].bit) != 0;
        ImGui::TextColored(
            set
                ? ImVec4(0.2f, 1.0f, 0.4f, 1.0f)
                : ImVec4(0.4f, 0.4f, 0.4f, 1.0f),
            "[%c] %s", set ? 'X' : ' ', table[i].name);
    }
    ImGui::Unindent();
}

static void DrawAnimInfo(const AnimInfo *anim)
{
    if (!anim)
    {
        ImGui::TextDisabled("animInfo: (null)");
        return;
    }

    if (ImGui::TreeNode("animInfo"))
    {
        ImGui::Text("animID:               %d", anim->animID);
        ImGui::Text("animYTrans:           %d", anim->animYTrans);
        ImGui::Text("curAnim:              %p", (void *)anim->curAnim);
        ImGui::Text("animFrame:            %d", anim->animFrame);
        ImGui::Text("animTimer:            %u", anim->animTimer);
        ImGui::Text("animFrameAccelAssist: %d", anim->animFrameAccelAssist);
        ImGui::Text("animAccel:            %d", anim->animAccel);
        ImGui::TreePop();
    }
}

static void DrawMarioState(const SM64MarioState *m)
{
    ImGui::Begin("Mario State");

    if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
    {
        ImGui::Text("Position:     (%.2f, %.2f, %.2f)", m->position[0], m->position[1], m->position[2]);
        ImGui::Text("Velocity:     (%.2f, %.2f, %.2f)", m->velocity[0], m->velocity[1], m->velocity[2]);
        ImGui::Text("Face Angle:   %.4f rad  (%.1f deg)", m->faceAngle, m->faceAngle * (180.0f / 3.14159265f));
        ImGui::Text("Fwd Velocity: %.2f", m->forwardVelocity);
    }

    if (ImGui::CollapsingHeader("Status", ImGuiTreeNodeFlags_DefaultOpen))
    {
        int hpSegs = (m->health & 0xFF00) >> 8;
        ImGui::Text("Health:       0x%04X  (HP segments: %d / 8)", (uint16_t)m->health, hpSegs);
        ImGui::ProgressBar(hpSegs / 8.0f, ImVec2(-1, 0));
        ImGui::Text("Invinc Timer: %d", m->invincTimer);
    }

    if (ImGui::CollapsingHeader("Action", ImGuiTreeNodeFlags_DefaultOpen))
    {
        ImGui::Text("0x%08X  (%s)", m->action, GetActionName(m->action));
        ImGui::Indent();
        // group
        switch (m->action & ACT_GROUP_MASK)
        {
            case ACT_GROUP_STATIONARY:
                ImGui::TextDisabled("group: STATIONARY");
                break;
            case ACT_GROUP_MOVING:
                ImGui::TextColored({0.2f, 1.0f, 0.4f, 1.0f}, "group: MOVING");
                break;
            case ACT_GROUP_AIRBORNE:
                ImGui::TextColored({0.2f, 0.8f, 1.0f, 1.0f}, "group: AIRBORNE");
                break;
            case ACT_GROUP_SUBMERGED:
                ImGui::TextColored({0.2f, 0.4f, 1.0f, 1.0f}, "group: SUBMERGED");
                break;
            case ACT_GROUP_CUTSCENE:
                ImGui::TextColored({1.0f, 0.8f, 0.2f, 1.0f}, "group: CUTSCENE");
                break;
            case ACT_GROUP_AUTOMATIC:
                ImGui::TextColored({0.8f, 0.4f, 1.0f, 1.0f}, "group: AUTOMATIC");
                break;
            case ACT_GROUP_OBJECT:
                ImGui::TextColored({1.0f, 0.5f, 0.2f, 1.0f}, "group: OBJECT");
                break;
        }
        // relevant flags
        if (m->action & ACT_FLAG_AIR) ImGui::Text("[x] ACT_FLAG_AIR");
        if (m->action & ACT_FLAG_SWIMMING) ImGui::Text("[x] ACT_FLAG_SWIMMING");
        if (m->action & ACT_FLAG_ATTACKING) ImGui::Text("[x] ACT_FLAG_ATTACKING");
        if (m->action & ACT_FLAG_INVULNERABLE) ImGui::Text("[x] ACT_FLAG_INVULNERABLE");
        if (m->action & ACT_FLAG_INTANGIBLE) ImGui::Text("[x] ACT_FLAG_INTANGIBLE");
        if (m->action & ACT_FLAG_RIDING_SHELL) ImGui::Text("[x] ACT_FLAG_RIDING_SHELL");
        if (m->action & ACT_FLAG_ON_POLE) ImGui::Text("[x] ACT_FLAG_ON_POLE");
        if (m->action & ACT_FLAG_HANGING) ImGui::Text("[x] ACT_FLAG_HANGING");
        if (m->action & ACT_FLAG_DIVING) ImGui::Text("[x] ACT_FLAG_DIVING");
        if (m->action & ACT_FLAG_SWIMMING_OR_FLYING)ImGui::Text("[x] ACT_FLAG_SWIMMING_OR_FLYING");
        if (m->action & ACT_FLAG_IDLE) ImGui::Text("[x] ACT_FLAG_IDLE");
        if (m->action & ACT_FLAG_THROWING) ImGui::Text("[x] ACT_FLAG_THROWING");
        ImGui::Unindent();
    }

    if (ImGui::CollapsingHeader("Flags"))
        DrawBitFlags("flags", m->flags, sMarioFlags, (int)(sizeof(sMarioFlags) / sizeof(sMarioFlags[0])));

    if (ImGui::CollapsingHeader("Particle Flags"))
        DrawBitFlags("particleFlags", m->particleFlags, sParticleFlags, (int)(sizeof(sParticleFlags) / sizeof(sParticleFlags[0])));

    if (ImGui::CollapsingHeader("Animation"))
        DrawAnimInfo(m->animInfo);

    ImGui::End();
}

static SDL_Haptic *gHaptic = nullptr;

static void InitHaptics(SDL_GameController *controller)
{
    if (controller == nullptr || gHaptic != nullptr)
    {
        return;
    }

    SDL_Joystick *joy = SDL_GameControllerGetJoystick(controller);

    if (joy == nullptr)
    {
        return;
    }

    gHaptic = SDL_HapticOpenFromJoystick(joy);

    if (gHaptic == nullptr)
    {
        return;
    }

    if (SDL_HapticRumbleInit(gHaptic) != 0)
    {
        SDL_HapticClose(gHaptic);
        gHaptic = nullptr;
    }
}

uint8_t *utils_read_file_alloc(const char *path, size_t *fileLength)
{
    FILE *f = fopen(path, "rb");

    if (!f) return nullptr;

    fseek(f, 0, SEEK_END);
    size_t length = (size_t)ftell(f);
    rewind(f);
    uint8_t *buffer = (uint8_t *)malloc(length + 1);
    fread(buffer, 1, length, f);
    buffer[length] = 0;
    fclose(f);

    if (fileLength) *fileLength = length;

    return buffer;
}

static float read_axis(int16_t val)
{
    float result = (float)val / 32767.0f;

    if (result < 0.2f && result > -0.2f)
        return 0.0f;

    return result > 0.0f ? (result - 0.2f) / 0.8f : (result + 0.2f) / 0.8f;
}

float lerp(float a, float b, float amount)
{
    return a + (b - a) * amount;
}

static void DebugPrint(const char *message)
{
    if (message == nullptr)
    {
        return;
    }

    printf("[SM64] %s\n", message);
    fflush(stdout);
}

static void Rumble(int32_t marioId, int16_t level, int16_t time)
{
    if (gHaptic == nullptr)
    {
        return;
    }

    float strength = (float)level;

    Uint32 duration = (Uint32)time * 2;

    SDL_HapticRumblePlay(gHaptic, strength, duration);
}

#ifdef WINDOWS_BUILD
constexpr float M_PI = 3.14159265358979323846f;
#endif

int main(void)
{
    SDL_SetMainReady();
    size_t romSize;

    uint8_t *rom = utils_read_file_alloc("baserom.us.z64", &romSize);

    if (rom == nullptr)
    {
        printf("\nFailed to read ROM file \"baserom.us.z64\"\n\n");
        return 1;
    }

    uint8_t *texture = (uint8_t *)malloc(4 * SM64_TEXTURE_WIDTH * SM64_TEXTURE_HEIGHT);

    sm64_global_terminate();
    sm64_global_init(rom, texture);
    sm64_audio_init(rom);

    // sm64_register_debug_print_function(DebugPrint);
    sm64_register_rumble_callback_function(Rumble);

    sm64_static_surfaces_load(surfaces, surfaces_count);
    int32_t marioId = sm64_mario_create(-6558, 0, 6464, true);

    int thing = sm64_fake_object_create(-6558.0f, 0.0f, 6200.0f, 1);
    sm64_fake_object_set_hitbox(thing, 32.0f, 800.0f * 2, 800);

    printf("MarioID: %i\n", marioId);
    free(rom);

    RenderState renderState;
    renderState.mario.index = nullptr;
    vec3 cameraPos = {0, 0, 0};
    float cameraRot = 0.0f;

    struct Renderer *renderer;

    int major, minor;
    #ifdef GL33_CORE
    major = 3; minor = 3;
    renderer = &gl33core_renderer;
    #else
    major = 2;
    minor = 0;
    renderer = &gl20_renderer;
    #endif

    context_init("libsm64", 1280, 720, major, minor);
    renderer->init(&renderState, texture);

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();

    // Setup Platform/Renderer backends
    ImGui_ImplSDL2_InitForOpenGL(context_get_window(), context_get_gl_context());
    ImGui_ImplOpenGL3_Init("#version 130"); // Use #version 130 for OpenGL 3.0+

    struct SM64MarioInputs marioInputs;
    struct SM64MarioState marioState;
    struct SM64MarioGeometryBuffers marioGeometry;

    // interpolation
    float lastPos[3], currPos[3];
    float lastGeoPos[9 * SM64_GEO_MAX_TRIANGLES], currGeoPos[9 * SM64_GEO_MAX_TRIANGLES];

    marioGeometry.position = (float *)malloc(sizeof(float) * 9 * SM64_GEO_MAX_TRIANGLES);
    marioGeometry.color = (float *)malloc(sizeof(float) * 9 * SM64_GEO_MAX_TRIANGLES);
    marioGeometry.normal = (float *)malloc(sizeof(float) * 9 * SM64_GEO_MAX_TRIANGLES);
    marioGeometry.uv = (float *)malloc(sizeof(float) * 6 * SM64_GEO_MAX_TRIANGLES);
    marioGeometry.numTrianglesUsed = 0;

    float tick = 0;
    uint32_t lastTicks = SDL_GetTicks();

    audio_init();

    sm64_play_music(0, 0x03, 0); // from decomp/include/seq_ids.h: SEQ_LEVEL_WATER | SEQ_VARIATION

    do
    {
        // --- ImGui frame setup ---
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplSDL2_NewFrame();
        ImGui::NewFrame();

        // --- Define ImGui content ---
        // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
        ImGui::SetNextWindowCollapsed(true, ImGuiCond_Appearing);
        ImGui::ShowDemoWindow();

        // --- Game logic and input ---
        float dt = (SDL_GetTicks() - lastTicks) / 1000.f;
        lastTicks = SDL_GetTicks();
        tick += dt;

        SDL_GameController *controller = context_get_controller();
        InitHaptics(controller);
        float x_axis, y_axis, x0_axis;

        if (!controller) // keyboard
        {
            const Uint8 *state = SDL_GetKeyboardState(nullptr);

            float dir;
            float spd = 0;
            if (state[SDL_SCANCODE_UP] && state[SDL_SCANCODE_RIGHT])
            {
                dir = -M_PI * 0.25f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_UP] && state[SDL_SCANCODE_LEFT])
            {
                dir = -M_PI * 0.75f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_DOWN] && state[SDL_SCANCODE_RIGHT])
            {
                dir = M_PI * 0.25f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_DOWN] && state[SDL_SCANCODE_LEFT])
            {
                dir = M_PI * 0.75f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_UP])
            {
                dir = -M_PI * 0.5f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_DOWN])
            {
                dir = M_PI * 0.5f;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_LEFT])
            {
                dir = M_PI;
                spd = 1;
            }
            else if (state[SDL_SCANCODE_RIGHT])
            {
                dir = 0;
                spd = 1;
            }

            x_axis = cosf(dir) * spd;
            y_axis = sinf(dir) * spd;
            x0_axis = state[SDL_SCANCODE_LSHIFT] ? 1 : state[SDL_SCANCODE_RSHIFT] ? -1 : 0;

            marioInputs.buttonA = state[SDL_SCANCODE_X];
            marioInputs.buttonB = state[SDL_SCANCODE_C];
            marioInputs.buttonZ = state[SDL_SCANCODE_Z];
        }
        else
        {
            x_axis = read_axis(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX));
            y_axis = read_axis(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY));
            x0_axis = read_axis(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX));

            marioInputs.buttonA = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A);
            marioInputs.buttonB = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X);
            marioInputs.buttonZ = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
        }

        cameraRot += x0_axis * dt * 2;
        cameraPos[0] = marioState.position[0] + 1000.0f * cosf(cameraRot);
        cameraPos[1] = marioState.position[1] + 200.0f;
        cameraPos[2] = marioState.position[2] + 1000.0f * sinf(cameraRot);

        marioInputs.camLookX = marioState.position[0] - cameraPos[0];
        marioInputs.camLookZ = marioState.position[2] - cameraPos[2];
        marioInputs.stickX = x_axis;
        marioInputs.stickY = y_axis;

        while (tick >= 1.f / 30)
        {
            memcpy(lastPos, currPos, sizeof(currPos));
            memcpy(lastGeoPos, currGeoPos, sizeof(currGeoPos));

            tick -= 1.f / 30;
            sm64_mario_tick(marioId, &marioInputs, &marioState, &marioGeometry);
            sm64_fake_object_tick(thing);

            memcpy(currPos, marioState.position, sizeof(currPos));
            memcpy(currGeoPos, marioGeometry.position, sizeof(currGeoPos));
        }

        for (int i = 0; i < 3; i++) marioState.position[i] = lerp(lastPos[i], currPos[i], tick / (1.f / 30));
        for (int i = 0; i < marioGeometry.numTrianglesUsed * 9; i++) marioGeometry.position[i] = lerp(lastGeoPos[i], currGeoPos[i], tick / (1.f / 30));

        // --- Game Rendering ---
        renderer->draw(&renderState, cameraPos, &marioState, &marioGeometry);

        DrawMarioState(&marioState);

        // --- ImGui Rendering ---
        ImGui::Render(); // Prepare draw data
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); // Render ImGui draw data
    }
    while (context_flip_frame_poll_events());

    sm64_stop_background_music(sm64_get_current_background_music());
    sm64_global_terminate();

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();

    context_terminate();

    if (gHaptic != nullptr)
    {
        SDL_HapticClose(gHaptic);
        gHaptic = nullptr;
    }

    return 0;
}
