#ifndef Z64GAME_H
#define Z64GAME_H

#include "ultra64.h"

#include "stdbool.h"
#include "stdint.h"
#include "gamealloc.h"
#include "padmgr.h"
#include "libu64/pad.h"
#include "romfile.h"
#include "tha.h"
#include "unk.h"

struct GraphicsContext;
struct GameState;


#define DEFINE_GAMESTATE_INTERNAL(typeName, enumName) enumName,
#define DEFINE_GAMESTATE(typeName, enumName, name) DEFINE_GAMESTATE_INTERNAL(typeName, enumName)

typedef enum GameStateId {
#include "tables/gamestate_table.h"
    /* 0x07 */ GAMESTATE_ID_MAX
} GameStateId;

#undef DEFINE_GAMESTATE
#undef DEFINE_GAMESTATE_INTERNAL


typedef void (*GameStateFunc)(struct GameState* gameState);

typedef struct GameStateOverlay {
    /* 0x00 */ void*         loadedRamAddr;
    /* 0x04 */ RomFile       file;      // if applicable
    /* 0x0C */ void*         vramStart; // if applicable
    /* 0x10 */ void*         vramEnd;   // if applicable
    /* 0x14 */ UNK_PTR       unk_14;
    /* 0x18 */ GameStateFunc init;    // initializes and executes the given context
    /* 0x1C */ GameStateFunc destroy; // deconstructs the context, and sets the next context to load
    /* 0x20 */ UNK_PTR       unk_20;
    /* 0x24 */ UNK_PTR       unk_24;
    /* 0x28 */ UNK_TYPE      unk_28;
    /* 0x2C */ size_t        instanceSize;
} GameStateOverlay; // size = 0x30

typedef struct GameState {
    /* 0x00 */ struct GraphicsContext* gfxCtx;
    /* 0x04 */ GameStateFunc main;
    /* 0x08 */ GameStateFunc destroy;
    /* 0x0C */ GameStateFunc init; // Usually the current game state's init, though after stopping, the graph thread will look here to determine the next game state to load.
    /* 0x10 */ size_t size;
    /* 0x14 */ Input input[MAXCONTROLLERS];
    /* 0x74 */ TwoHeadArena tha;
    /* 0x84 */ GameAlloc alloc;
    /* 0x98 */ UNK_TYPE1 pad98[0x3];
    /* 0x9B */ u8 running; // If 0, switch to next game state
    /* 0x9C */ u32 frames;
    /* 0xA0 */ u8 padA0[0x2];
    /* 0xA2 */ u8 framerateDivisor;
    /* 0xA3 */ u8 unk_A3;
} GameState; // size = 0xA4


void GameState_UpdateFramerateDivisors(s32 divisor);
void GameState_SetFramerateDivisor(GameState* gameState, s32 divisor);
void GameState_SetFBFilter(Gfx** gfxP, void* zbuffer);
void GameState_Noop(GameState* gameState);
void GameState_Draw(GameState* gameState, struct GraphicsContext* gfxCtx);
void GameState_SetFrameBuffer(struct GraphicsContext* gfxCtx);
void GameState_DrawEnd(struct GraphicsContext* gfxCtx);
void GameState_GetInput(GameState* gameState);
void GameState_Update(GameState* gameState);
void GameState_IncrementFrameCount(GameState* gameState);
void GameState_InitArena(GameState* gameState, size_t size);
void GameState_Realloc(GameState* gameState, size_t size);
void GameState_Init(GameState* gameState, GameStateFunc init, struct GraphicsContext* gfxCtx);
void GameState_Destroy(GameState* gameState);
GameStateFunc GameState_GetInit(GameState* gameState);
size_t GameState_GetSize(GameState* gameState);
u32 GameState_IsRunning(GameState* gameState);
s32 GameState_GetArenaSize(GameState* gameState);
s32 func_80173B48(GameState* gameState);

extern s32 gFramerateDivisor;
extern f32 gFramerateDivisorF;
extern f32 gFramerateDivisorHalf;
extern f32 gFramerateDivisorThird;


extern GameStateOverlay gGameStateOverlayTable[GAMESTATE_ID_MAX];
extern GameStateId gGraphNumGameStates;


#define CONTROLLER1(gameState) (&(gameState)->input[0])
#define CONTROLLER2(gameState) (&(gameState)->input[1])
#define CONTROLLER3(gameState) (&(gameState)->input[2])
#define CONTROLLER4(gameState) (&(gameState)->input[3])

#define STOP_GAMESTATE(curState)     \
    do {                             \
        GameState* state = curState; \
                                     \
        state->running = false;      \
    } while(0)

#define SET_NEXT_GAMESTATE(curState, nextInit, nextSize) \
    do {                                                 \
        GameState* state = curState;                     \
                                                         \
        (state)->init = nextInit;                        \
        (state)->size = nextSize;                        \
    } while (0)


#endif
