#include "patches.h"
#include "sys_cfb.h"
#include "buffers.h"
#include "fault.h"
#include "audiomgr.h"
#include "z64speed_meter.h"
#include "z64vimode.h"
#include "z64viscvg.h"
#include "z64vismono.h"
#include "z64viszbuf.h"
#include "input.h"

void recomp_set_current_frame_poll_id();
void PadMgr_HandleRetrace(void);
void PadMgr_LockPadData(void);
void PadMgr_UnlockPadData(void);
void PadMgr_UpdateRumble(void);
void PadMgr_UpdateConnections(void);
void PadMgr_UpdateInputs(void);
void PadMgr_InitVoice(void);
OSMesgQueue* PadMgr_AcquireSerialEventQueue(void);
void PadMgr_ReleaseSerialEventQueue(OSMesgQueue* serialEventQueue);


extern PadMgr* sPadMgrInstance;
extern s32 sPadMgrRetraceCount;
extern FaultMgr gFaultMgr;
extern s32 sVoiceInitStatus;


typedef enum {
    /* 0 */ VOICE_INIT_FAILED, // voice initialization failed
    /* 1 */ VOICE_INIT_TRY,    // try to initialize voice
    /* 2 */ VOICE_INIT_SUCCESS // voice initialized
} VoiceInitStatus;

RECOMP_PATCH void PadMgr_HandleRetrace(void) {
    // Execute rumble callback
    if (sPadMgrInstance->rumbleRetraceCallback != NULL) {
        sPadMgrInstance->rumbleRetraceCallback(sPadMgrInstance->rumbleRetraceArg);
    }

    // Try and initialize a Voice Recognition Unit if not already attempted
    if (sVoiceInitStatus != VOICE_INIT_FAILED) {
        PadMgr_InitVoice();
    }

    // Rumble Pak
    if (gFaultMgr.msgId != 0) {
        // If fault is active, no rumble
        PadMgr_RumbleStop();
    } else if (sPadMgrInstance->rumbleOffTimer > 0) {
        // If the rumble off timer is active, no rumble
        --sPadMgrInstance->rumbleOffTimer;
        PadMgr_RumbleStop();
    } else if (sPadMgrInstance->rumbleOnTimer == 0) {
        // If the rumble on timer is inactive, no rumble
        PadMgr_RumbleStop();
    } else if (!sPadMgrInstance->isResetting) {
        // If not resetting, update rumble
        PadMgr_UpdateRumble();
        --sPadMgrInstance->rumbleOnTimer;
    }
}

extern u8 sOcarinaInstrumentId;

void poll_inputs(void) {
    OSMesgQueue* serialEventQueue = PadMgr_AcquireSerialEventQueue();
    // Begin reading controller data
    osContStartReadData(serialEventQueue);

    bool needs_right_stick = recomp_analog_cam_enabled() || recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_FORCE_RIGHT_STICK;
    // Suppress the right analog stick if analog camera is active unless the ocarina is in use.
    recomp_set_right_analog_suppressed(needs_right_stick && sOcarinaInstrumentId == OCARINA_INSTRUMENT_OFF);
    // Resets this flag for the next frame;
    recomp_aiming_override_mode = RECOMP_AIMING_OVERRIDE_OFF;

    // Wait for controller data
    osRecvMesg(serialEventQueue, NULL, OS_MESG_BLOCK);
    osContGetReadData(sPadMgrInstance->pads);

    // Clear all but controller 1
    bzero(&sPadMgrInstance->pads[1], sizeof(*sPadMgrInstance->pads) * (MAXCONTROLLERS - 1));

    // If in PreNMI, clear all controllers
    if (sPadMgrInstance->isResetting) {
        bzero(sPadMgrInstance->pads, sizeof(sPadMgrInstance->pads));
    }

    // Query controller statuses
    osContStartQuery(serialEventQueue);
    osRecvMesg(serialEventQueue, NULL, OS_MESG_BLOCK);
    osContGetQuery(sPadMgrInstance->padStatus);

    // Lock serial message queue
    PadMgr_ReleaseSerialEventQueue(serialEventQueue);

    // Update connections
    PadMgr_UpdateConnections();

    // Lock input data
    PadMgr_LockPadData();

    // Update input data
    PadMgr_UpdateInputs();

    // Execute input callback
    if (sPadMgrInstance->inputRetraceCallback != NULL) {
        sPadMgrInstance->inputRetraceCallback(sPadMgrInstance->inputRetraceArg);
    }

    // Unlock input data
    PadMgr_UnlockPadData();
    sPadMgrRetraceCount++;
}

// @recomp Patched to do the actual input polling.
RECOMP_PATCH void PadMgr_GetInput(Input* inputs, s32 gameRequest) {
    // @recomp Do an actual poll if gameRequest is true.
    if (gameRequest) {
        poll_inputs();
        // @recomp Tag the current frame's input polling id for latency tracking.
        recomp_set_current_frame_poll_id();
    }
    PadMgr_LockPadData();
    PadMgr_GetInputNoLock(inputs, gameRequest);
    PadMgr_UnlockPadData();
}

// @recomp Just call PadMgr_GetInput.
RECOMP_PATCH void PadMgr_GetInput2(Input* inputs, s32 gameRequest) {
    PadMgr_GetInput(inputs, gameRequest);
}

extern CfbInfo sGraphCfbInfos[3];
u32 recomp_time_us();
void recomp_measure_latency();
void* osViGetCurrentFramebuffer_recomp();

OSMesgQueue *rdp_queue_ptr = NULL;

// @recomp Immediately sends the graphics task instead of queueing it in the scheduler.
RECOMP_PATCH void Graph_TaskSet00(GraphicsContext* gfxCtx, GameState* gameState) {
    static s32 retryCount = 10;
    static s32 cfbIdx = 0;
    OSTask_t* task = &gfxCtx->task.list.t;
    OSScTask* scTask = &gfxCtx->task;
    OSTimer timer;
    OSMesg msg;
    CfbInfo* cfb;
    
    // @recomp Additional static members for extra scheduling purposes.
    static IrqMgrClient irq_client = {0};
    static OSMesgQueue vi_queue = {0};
    static OSMesg vi_buf[8] = {0};
    static bool created = false;
    if (!created) {
        created = true;
        osCreateMesgQueue(&vi_queue, vi_buf, ARRAY_COUNT(vi_buf));
        extern IrqMgr gIrqMgr;
        IrqMgr_AddClient(&gIrqMgr, &irq_client, &vi_queue);
    }

    // @recomp Disable the wait here so that it can be moved after task submission for minimizing latency.
// retry:
//     osSetTimer(&timer, OS_USEC_TO_CYCLES(3 * 1000 * 1000), 0, &gfxCtx->queue, (OSMesg)666);
//     osRecvMesg(&gfxCtx->queue, &msg, OS_MESG_BLOCK);
//     osStopTimer(&timer);

//     if (msg == (OSMesg)666) {
//         osSyncPrintf("GRAPH SP TIMEOUT\n");
//         if (retryCount >= 0) {
//             retryCount--;
//             Sched_SendGfxCancelMsg(&gSchedContext);
//             goto retry;
//         } else {
//             // graph.c: No more! die!
//             osSyncPrintf("graph.c:もうダメ！死ぬ！\n");
//             Fault_AddHungupAndCrashImpl("RCP is HUNG UP!!", "Oh! MY GOD!!");
//         }
//     }

    gfxCtx->masterList = gGfxMasterDL;
    if (gfxCtx->callback != NULL) {
        gfxCtx->callback(gfxCtx, gfxCtx->callbackArg);
    }

    task->type = M_GFXTASK;
    task->flags = OS_SC_DRAM_DLIST;
    task->ucodeBoot = SysUcode_GetUCodeBoot();
    task->ucodeBootSize = SysUcode_GetUCodeBootSize();
    task->ucode = SysUcode_GetUCode();
    task->ucodeData = SysUcode_GetUCodeData();
    task->ucodeSize = SP_UCODE_SIZE;
    task->ucodeDataSize = SP_UCODE_DATA_SIZE;
    task->dramStack = (u64*)gGfxSPTaskStack;
    task->dramStackSize = sizeof(gGfxSPTaskStack);
    task->outputBuff = gGfxSPTaskOutputBufferPtr;
    task->outputBuffSize = gGfxSPTaskOutputBufferEnd;
    task->dataPtr = (u64*)gGfxMasterDL;
    task->dataSize = 0;
    task->yieldDataPtr = (u64*)gGfxSPTaskYieldBuffer;
    task->yieldDataSize = sizeof(gGfxSPTaskYieldBuffer);

    scTask->next = NULL;
    scTask->flags = OS_SC_RCP_MASK | OS_SC_SWAPBUFFER | OS_SC_LAST_TASK;

    if (SREG(33) & 1) {
        SREG(33) &= ~1;
        scTask->flags &= ~OS_SC_SWAPBUFFER;
        gfxCtx->framebufferIndex--;
    }

    scTask->msgQ = &gfxCtx->queue;
    scTask->msg = NULL;

    { s32 pad; }

    cfb = &sGraphCfbInfos[cfbIdx];
    cfbIdx = (cfbIdx + 1) % ARRAY_COUNT(sGraphCfbInfos);

    cfb->fb1 = gfxCtx->curFrameBuffer;
    cfb->swapBuffer = gfxCtx->curFrameBuffer;

    if (gfxCtx->updateViMode) {
        gfxCtx->updateViMode = false;
        cfb->viMode = gfxCtx->viMode;
        cfb->features = gfxCtx->viConfigFeatures;
        cfb->xScale = gfxCtx->xScale;
        cfb->yScale = gfxCtx->yScale;
    } else {
        cfb->viMode = NULL;
    }
    cfb->unk_10 = 0;
    cfb->updateRate = gameState->framerateDivisor;

    scTask->framebuffer = cfb;

    while (gfxCtx->queue.validCount != 0) {
        osRecvMesg(&gfxCtx->queue, NULL, OS_MESG_NOBLOCK);
    }

    gfxCtx->schedMsgQ = &gSchedContext.cmdQ;
    osSendMesg(&gSchedContext.cmdQ, scTask, OS_MESG_BLOCK);
    Sched_SendEntryMsg(&gSchedContext);
    
    // @recomp Immediately wait on the task to complete to minimize latency for the next one.
    osRecvMesg(&gfxCtx->queue, &msg, OS_MESG_BLOCK);

    // @recomp Wait on the VI framebuffer to change if this task has a framebuffer swap.
    if (scTask->flags & OS_SC_SWAPBUFFER) {
        int viCounter = 0;
        while (osViGetCurrentFramebuffer() != cfb->fb1) {
            osRecvMesg(&vi_queue, NULL, OS_MESG_BLOCK);
            viCounter++;
        }
        
        // If we didn't wait the full number of VIs needed between frames then wait one extra VI afterwards.
        if (viCounter < gameState->framerateDivisor) {
            osRecvMesg(&vi_queue, NULL, OS_MESG_BLOCK);
        }
    }
    
    // @recomp Flush any extra messages from the VI queue.
    while (osRecvMesg(&vi_queue, NULL, OS_MESG_NOBLOCK) == 0) {
        ;
    }
}

extern SpeedMeter sGameSpeedMeter;
extern VisCvg sGameVisCvg;
extern VisZbuf sGameVisZbuf;
extern VisMono sGameVisMono;
extern ViMode sGameViMode;

RECOMP_PATCH void GameState_Destroy(GameState* gameState) {
    AudioMgr_StopAllSfxExceptSystem();
    Audio_Update();

    // @recomp The wait for the gfx task was moved to directly after submission, so it's not needed here.
    // osRecvMesg(&gameState->gfxCtx->queue, NULL, OS_MESG_BLOCK);

    if (gameState->destroy != NULL) {
        gameState->destroy(gameState);
    }

    Rumble_Destroy();
    SpeedMeter_Destroy(&sGameSpeedMeter);
    VisCvg_Destroy(&sGameVisCvg);
    VisZbuf_Destroy(&sGameVisZbuf);
    VisMono_Destroy(&sGameVisMono);
    ViMode_Destroy(&sGameViMode);
    THA_Destroy(&gameState->tha);
    GameAlloc_Cleanup(&gameState->alloc);
}
