#include "global.h"

#define RSP_DONE_MSG 667
#define RDP_DONE_MSG 668
#define ENTRY_MSG 670
#define RDP_AUDIO_CANCEL_MSG 671
#define RSP_GFX_CANCEL_MSG 672

FaultClient sSchedFaultClient;

OSTime sRSPGFXStartTime;
OSTime sRSPAudioStartTime;
OSTime sRSPOtherStartTime;
OSTime sRDPStartTime;

u64* gAudioSPDataPtr;
u32 gAudioSPDataSize;

void Sched_SwapFramebuffer(CfbInfo* cfbInfo) {
    s32 one = 1;

    if (cfbInfo->swapBuffer != NULL) {
        osViSwapBuffer(cfbInfo->swapBuffer);
        cfbInfo->updateRate2 = cfbInfo->updateRate;

        if (SREG(62) == 0 && cfbInfo->viMode != NULL) {
            D_80096B20 = one;
            osViSetMode(cfbInfo->viMode);
            osViSetSpecialFeatures(cfbInfo->features);
            osViSetXScale(cfbInfo->xScale);
            osViSetYScale(cfbInfo->yScale);
            cfbInfo->viMode = NULL;
        }
    }
    cfbInfo->unk_10 = 0;
}

void Sched_RetraceUpdateFramebuffer(SchedContext* sched, CfbInfo* cfbInfo) {
    if (sched->shouldUpdateVi) {
        sched->shouldUpdateVi = false;

        if (gIrqMgrResetStatus == 0) {
            ViConfig_UpdateVi(0);
        }
    }
    Sched_SwapFramebuffer(cfbInfo);
}

void Sched_HandleReset(SchedContext* sched) {
}

void Sched_HandleStop(SchedContext* sched) {
    ViConfig_UpdateVi(1);
}

/**
 * Attempt to stop the RSP, if it is not already halted, by setting the halt bit in the
 * SP status register and waiting. Regardless of the result, the scheduler will send an
 * RSP_DONE_MSG back to itself.
 * If there was no currently running audio task, it will dequeue the currently waiting
 * audio task and notify the sender if the task is associated with a message queue.
 */
void Sched_HandleAudioCancel(SchedContext* sched) {
    s32 i;

    // AUDIO SP Cancel
    osSyncPrintf("AUDIO SP キャンセルします\n");

    if (sched->curRSPTask != NULL && sched->curRSPTask->list.t.type == M_AUDTASK) {
        if (!(HW_REG(SP_STATUS_REG, u32) & SP_STATUS_HALT)) {
            // Attempts to stop AUDIO SP
            osSyncPrintf("AUDIO SP止めようとします\n");

            HW_REG(SP_STATUS_REG, u32) = SP_SET_HALT;

            i = 0;
            while (!(HW_REG(SP_STATUS_REG, u32) & SP_STATUS_HALT)) {
                if (i++ > 100) {
                    // AUDIO SP did not stop (10ms timeout)
                    osSyncPrintf("AUDIO SP止まりませんでした(10msタイムアウト)\n");
                    goto send_mesg;
                }
                Sleep_Usec(100);
            }
            // AUDIO SP stopped (% d * 100us)
            osSyncPrintf("AUDIO SP止まりました(%d * 100us)\n", i);
        } else {
            // AUDIO SP seems to be stopped
            osSyncPrintf("AUDIO SP止まっているようです\n");
        }
    send_mesg:
        osSendMesg(&sched->interruptQ, RSP_DONE_MSG, OS_MESG_NOBLOCK);
        return;
    }

    if (sched->audioListHead != NULL) {
        OSScTask* cur = sched->audioListHead;
        OSScTask* next = cur->next;

        sched->audioListHead = next;
        if (next == NULL) {
            sched->audioListTail = NULL;
        }
        if (cur->msgQ != NULL) {
            osSendMesg(cur->msgQ, cur->msg, OS_MESG_BLOCK);
        }
        // Removed AUDIO SP task from pending list
        osSyncPrintf("AUDIO SP タスクを実行待ちリストから削除しました\n");
        return;
    }

    // There are no AUDIO SP tasks to cancel
    osSyncPrintf("キャンセルすべき AUDIO SP タスクがありません\n");
}

/**
 * Attempt to stop the RSP, if it is not already halted, by setting the halt bit in the
 * SP status register and waiting. Regardless of the result, the scheduler will send an
 * RSP_DONE_MSG back to itself and attempt to stop the RDP.
 * If there was no currently running gfx task, it will dequeue the currently waiting gfx
 * task and notify the sender if the task is associated with a message queue.
 * If there is an RDP task, the output buffer will be cleared and the scheduler will send
 * an RDP_DONE_MSG back to itself.
 */
void Sched_HandleGfxCancel(SchedContext* sched) {
    s32 i;

    // GRAPH SP Cancel
    osSyncPrintf("GRAPH SP キャンセルします\n");

    if (sched->curRSPTask != NULL && sched->curRSPTask->list.t.type == M_GFXTASK) {
        if (!(HW_REG(SP_STATUS_REG, u32) & SP_STATUS_HALT)) {
            // GRAPH SP tries to stop
            osSyncPrintf("GRAPH SP止めようとします\n");

            HW_REG(SP_STATUS_REG, u32) = SP_SET_HALT;

            i = 0;
            while (!(HW_REG(SP_STATUS_REG, u32) & SP_STATUS_HALT)) {
                if (i++ > 100) {
                    // GRAPH SP did not stop (10ms timeout)
                    osSyncPrintf("GRAPH SP止まりませんでした(10msタイムアウト)\n");
                    goto send_mesg;
                }
                Sleep_Usec(100);
            }
            // GRAPH SP stopped (%d * 100us)
            osSyncPrintf("GRAPH SP止まりました(%d * 100us)\n", i);
        } else {
            // GRAPH SP seems to be stopped
            osSyncPrintf("GRAPH SP止まっているようです\n");
        }
    send_mesg:
        osSendMesg(&sched->interruptQ, RSP_DONE_MSG, OS_MESG_NOBLOCK);
        goto halt_rdp;
    }

    if (sched->gfxListHead != NULL) {
        OSScTask* cur = sched->gfxListHead;
        OSScTask* next = cur->next;

        sched->gfxListHead = next;
        if (next == NULL) {
            sched->gfxListTail = NULL;
        }
        if (cur->msgQ != NULL) {
            osSendMesg(cur->msgQ, cur->msg, OS_MESG_BLOCK);
        }
        goto halt_rdp;
    }

    // There are no GRAPH SP tasks to cancel
    osSyncPrintf("キャンセルすべき GRAPH SP タスクがありません\n");

halt_rdp:
    if (sched->curRDPTask != NULL) {
        OSTask_t* dpTask = &sched->curRDPTask->list.t;

        if (dpTask->type == M_GFXTASK) {
            // Try to stop DP
            osSyncPrintf("DP止めようとします\n");
            bzero(dpTask->outputBuff, (u32)dpTask->outputBuffSize - (u32)dpTask->outputBuff);
            osSendMesg(&sched->interruptQ, RDP_DONE_MSG, OS_MESG_NOBLOCK);
        }
    }
}

/**
 * Adds a scheduler task to the appropriate linked list.
 */
void Sched_QueueTask(SchedContext* sched, OSScTask* task) {
    s32 type = task->list.t.type;

    if (type == M_AUDTASK) {
        if (sched->audioListTail != NULL) {
            sched->audioListTail->next = task;
        } else {
            sched->audioListHead = task;
        }
        sched->audioListTail = task;
    } else {
        if (sched->gfxListTail != NULL) {
            sched->gfxListTail->next = task;
        } else {
            sched->gfxListHead = task;
        }
        sched->gfxListTail = task;
    }
    task->next = NULL;
    task->state = task->flags & OS_SC_RCP_MASK;
}

void Sched_Yield(SchedContext* sched) {
    // Don't yield audio tasks
    if (sched->curRSPTask->list.t.type == M_AUDTASK) {
        // A new audio task has been entered even though the previous audio task has not been completed yet
        osSyncPrintf("まだ前回のオーディオタスクが完了していないのに新たなオーディオタスクがエントリされた\n");
    } else if (!(sched->curRSPTask->state & OS_SC_YIELD)) {
        sched->curRSPTask->state |= OS_SC_YIELD;
        osSpTaskYield();
    }
}

s32 Sched_TaskCheckFramebuffers(SchedContext* sched, OSScTask* task) {
    void* nextFB = osViGetNextFramebuffer();
    void* curFB = osViGetCurrentFramebuffer();

    if (task == NULL || sched->pendingSwapBuf1 != NULL || (curFB == TASK_FRAMEBUFFER(task)->fb1 && curFB != nextFB)) {
        return 0;
    }
    return 1;
}

/**
 * Pops the next task from the appropriate linked list and returns it through spTask and dpTask.
 */
s32 Sched_Schedule(SchedContext* sched, OSScTask** spTask, OSScTask** dpTask, s32 state) {
    s32 ret = state;
    OSScTask* gfxTask = sched->gfxListHead;
    OSScTask* audioTask = sched->audioListHead;

    if ((ret & OS_SC_SP) && sched->audioListHead != NULL) {
        *spTask = audioTask;
        ret &= ~OS_SC_SP;
        sched->audioListHead = sched->audioListHead->next;
        if (sched->audioListHead == NULL) {
            sched->audioListTail = NULL;
        }
    } else if (gfxTask != NULL) {
        if (gfxTask->state & OS_SC_YIELDED || !(gfxTask->flags & OS_SC_NEEDS_RDP)) {
            if (ret & OS_SC_SP) {
                *spTask = gfxTask;
                ret &= ~OS_SC_SP;
                sched->gfxListHead = sched->gfxListHead->next;
                if (sched->gfxListHead == NULL) {
                    sched->gfxListTail = NULL;
                }
            }
        } else if (ret == (OS_SC_SP | OS_SC_DP)) {
            if (TASK_FRAMEBUFFER(gfxTask) == NULL || Sched_TaskCheckFramebuffers(sched, gfxTask)) {
                *spTask = *dpTask = gfxTask;
                ret &= ~(OS_SC_SP | OS_SC_DP);
                sched->gfxListHead = sched->gfxListHead->next;
                if (sched->gfxListHead == NULL) {
                    sched->gfxListTail = NULL;
                }
            }
        }
    }
    return ret;
}

void Sched_TaskUpdateFramebuffer(SchedContext* sched, OSScTask* task) {
    sched->pendingSwapBuf1 = TASK_FRAMEBUFFER(task);

    if (sched->curBuf != NULL && sched->curBuf->updateRate2 > 0) {
        return;
    }
    Sched_RetraceUpdateFramebuffer(sched, sched->pendingSwapBuf1);
}

/**
 * If the task has been marked as completed, notify the sender through the task's
 * associated message queue (if it has one) that the task has been completed. If the task
 * flags dictate it should swap the framebuffer, do so.
 */
void Sched_NotifyDone(SchedContext* sched, OSScTask* task) {
    if (!(task->state & (OS_SC_DP | OS_SC_SP))) {
        if (task->msgQ != NULL) {
            osSendMesg(task->msgQ, task->msg, OS_MESG_BLOCK);
        }
        if (task->flags & OS_SC_SWAPBUFFER) {
            Sched_TaskUpdateFramebuffer(sched, task);
        }
    }
}

void Sched_RunTask(SchedContext* sched, OSScTask* spTask, OSScTask* dpTask) {
    u64 time;

    if (spTask != NULL) {
        if (spTask->list.t.type == M_NULTASK) {
            if (spTask->flags & OS_SC_NEEDS_RSP) {
                spTask->state &= ~OS_SC_SP;
                sched->curRSPTask = NULL;
            }
            if (spTask->flags & OS_SC_NEEDS_RDP) {
                spTask->state &= ~OS_SC_DP;
                sched->curRDPTask = NULL;
            }
            Sched_NotifyDone(sched, spTask);
            return;
        }
        // Write back the data cache to ensure imminent SP DMA does not miss anything
        if (spTask->list.t.type != M_AUDTASK && !(spTask->state & OS_SC_YIELDED)) {
            osWritebackDCacheAll();
        }
        spTask->state &= ~(OS_SC_YIELD | OS_SC_YIELDED);
        // Have the RSP download the task and prepare the RSP program counter
        osSpTaskLoad(&spTask->list);

        // Log the start time based on the type of task
        time = osGetTime();
        switch (spTask->list.t.type) {
            case M_AUDTASK:
                sRSPAudioStartTime = time;
                break;
            case M_GFXTASK:
                sRSPGFXStartTime = time;
                break;
            default:
                if (1) {}
                sRSPOtherStartTime = time;
                break;
        }

        if (spTask->list.t.type == M_AUDTASK) {
            // Set global pointers to audio task data for use in audio processing
            gAudioSPDataPtr = spTask->list.t.dataPtr;
            gAudioSPDataSize = spTask->list.t.dataSize;
        }

        // Begin task execution
        osSpTaskStartGo(&spTask->list);
        sched->curRSPTask = spTask;
        if (spTask == dpTask && sched->curRDPTask == NULL) {
            sched->curRDPTask = dpTask;
            sRDPStartTime = sRSPGFXStartTime;
        }
    }
}

/**
 * Enqueues any tasks that have been sent to the scheduler down the command queue.
 */
void Sched_HandleEntry(SchedContext* sched) {
    OSScTask* spTask = NULL;
    OSScTask* dpTask = NULL;
    OSMesg msg = NULL;
    s32 state;

    // Fetch and enqueue waiting tasks
    while (osRecvMesg(&sched->cmdQ, &msg, OS_MESG_NOBLOCK) != -1) {
        Sched_QueueTask(sched, msg);
    }
    // If there is an audio task pending and an RSP task is running, yield the current task.
    if (sched->audioListHead != NULL && sched->curRSPTask != NULL) {
        Sched_Yield(sched);
        return;
    }
    // Schedule and run the next task
    state = ((sched->curRSPTask == NULL) << 1) | (sched->curRDPTask == NULL);
    if (Sched_Schedule(sched, &spTask, &dpTask, state) != state) {
        Sched_RunTask(sched, spTask, dpTask);
    }
}

void Sched_HandleRetrace(SchedContext* sched) {
    ViConfig_UpdateBlack();
    sched->retraceCount++;

    if (osViGetCurrentFramebuffer() ==
        (void*)((sched->pendingSwapBuf1 != NULL) ? sched->pendingSwapBuf1->swapBuffer : NULL)) {
        if (sched->curBuf != NULL) {
            sched->curBuf->unk_10 = 0;
        }
        if (sched->pendingSwapBuf1 != NULL) {
            sched->pendingSwapBuf1->unk_10 = 0;
        }
        sched->curBuf = sched->pendingSwapBuf1;
        sched->pendingSwapBuf1 = NULL;
    }
    if (sched->curBuf != NULL) {
        if (sched->curBuf->updateRate2 > 0) {
            sched->curBuf->updateRate2--;
        }
        if (sched->curBuf->updateRate2 <= 0 && sched->pendingSwapBuf1 != NULL) {
            Sched_RetraceUpdateFramebuffer(sched, sched->pendingSwapBuf1);
        }
    }
    Sched_HandleEntry(sched);
}

void Sched_HandleRSPDone(SchedContext* sched) {
    OSScTask* curRSP;
    OSScTask* nextRSP = NULL;
    OSScTask* nextRDP = NULL;
    s32 state;
    u64 time;

    if (sched->curRSPTask == NULL) {
        osSyncPrintf("__scHandleRSP:sc->curRSPTask == NULL\n");
        return;
    }

    // Log the time based on the type of task
    time = osGetTime();
    switch (sched->curRSPTask->list.t.type) {
        case M_AUDTASK:
            gRSPAudioTotalTime += time - sRSPAudioStartTime;
            break;
        case M_GFXTASK:
            sRSPGFXTotalTime += time - sRSPGFXStartTime;
            break;
        default:
            if (1) {}
            sRSPOtherTotalTime += time - sRSPOtherStartTime;
            break;
    }

    curRSP = sched->curRSPTask;
    sched->curRSPTask = NULL;

    if (curRSP->list.t.type == M_AUDTASK) {
        // Reset the global audio task data pointers
        gAudioSPDataPtr = NULL;
        gAudioSPDataSize = 0;
    }

    if ((curRSP->state & OS_SC_YIELD) && osSpTaskYielded(&curRSP->list)) {
        // If the task was yielded, re-queue the task
        curRSP->state |= OS_SC_YIELDED;
        curRSP->next = sched->gfxListHead;
        sched->gfxListHead = curRSP;
        if (sched->gfxListTail == NULL) {
            sched->gfxListTail = curRSP;
        }
    } else {
        // Mark task completed
        curRSP->state &= ~OS_SC_SP;
        Sched_NotifyDone(sched, curRSP);
    }

    // Schedule and run next task
    state = ((sched->curRSPTask == NULL) << 1) | (sched->curRDPTask == NULL);
    if (Sched_Schedule(sched, &nextRSP, &nextRDP, state) != state) {
        Sched_RunTask(sched, nextRSP, nextRDP);
    }
}

void Sched_HandleRDPDone(SchedContext* sched) {
    OSScTask* curRDP;
    OSScTask* nextRSP = NULL;
    OSScTask* nextRDP = NULL;
    s32 state;

    if (sched->curRDPTask == NULL) {
        osSyncPrintf("__scHandleRDP:sc->curRDPTask == NULL\n");
        return;
    }

    // Log run time
    gRDPTotalTime = osGetTime() - sRDPStartTime;

    // Mark task done
    curRDP = sched->curRDPTask;
    sched->curRDPTask = NULL;
    curRDP->state &= ~OS_SC_DP;

    Sched_NotifyDone(sched, curRDP);

    // Schedule and run next task
    state = ((sched->curRSPTask == NULL) << 1) | (sched->curRDPTask == NULL);
    if (Sched_Schedule(sched, &nextRSP, &nextRDP, state) != state) {
        Sched_RunTask(sched, nextRSP, nextRDP);
    }
}

/**
 * Sends a message to the scheduler to inform it that a new task has
 * been sent down the command queue.
 */
void Sched_SendEntryMsg(SchedContext* sched) {
    osSendMesg(&sched->interruptQ, ENTRY_MSG, OS_MESG_BLOCK);
}

/**
 * Sends a message to the scheduler to inform it that it should attempt
 * to stop the last dispatched audio task.
 */
void Sched_SendAudioCancelMsg(SchedContext* sched) {
    osSendMesg(&sched->interruptQ, RDP_AUDIO_CANCEL_MSG, OS_MESG_BLOCK);
}

/**
 * Sends a message to the scheduler to inform it that it should attempt
 * to stop the last dispatched gfx task.
 */
void Sched_SendGfxCancelMsg(SchedContext* sched) {
    osSendMesg(&sched->interruptQ, RSP_GFX_CANCEL_MSG, OS_MESG_BLOCK);
}

/**
 * Fault Client for the scheduler. Reports information about the state of the scheduler
 * and any current tasks in the crash debugger.
 */
void Sched_FaultClient(void* param1, void* param2) {
    SchedContext* sched = (SchedContext*)param1;
    OSScTask* spTask;
    OSScTask* dpTask;

    FaultDrawer_Printf("sched info\n", sched->gfxListHead, sched->gfxListTail, sched->audioListHead,
                       sched->audioListTail);
    FaultDrawer_Printf("GRAPH %08x %08x\n", sched->gfxListHead, sched->gfxListTail);
    FaultDrawer_Printf("AUDIO %08x %08x\n\n", sched->audioListHead, sched->audioListTail);

    spTask = sched->curRSPTask;
    if (spTask != NULL) {
        FaultDrawer_Printf("RSPTask %08x %08x %02x %02x\n%01x %08x %08x\n", spTask, spTask->next, spTask->state,
                           spTask->flags, spTask->list.t.type, spTask->list.t.dataPtr, spTask->list.t.dataSize);
    }

    dpTask = sched->curRDPTask;
    if (dpTask != NULL) {
        FaultDrawer_Printf("RDPTask %08x %08x %02x %02x\n", dpTask, dpTask->next, dpTask->state, dpTask->flags);
    }
}

/**
 * The main loop of the scheduler thread. Processes interrupt messages from
 * the IrqMgr received through its IrqClient and messages sent to it from other
 * threads or the OS.
 */
void Sched_ThreadEntry(void* arg) {
    OSMesg msg = NULL;
    SchedContext* sched = (SchedContext*)arg;

    while (true) {
        osRecvMesg(&sched->interruptQ, &msg, OS_MESG_BLOCK);

        // Check if it's a message from another thread or the OS
        switch ((s32)msg) {
            case RDP_AUDIO_CANCEL_MSG:
                Sched_HandleAudioCancel(sched);
                continue;
            case RSP_GFX_CANCEL_MSG:
                Sched_HandleGfxCancel(sched);
                continue;
            case ENTRY_MSG:
                Sched_HandleEntry(sched);
                continue;
            case RSP_DONE_MSG:
                Sched_HandleRSPDone(sched);
                continue;
            case RDP_DONE_MSG:
                Sched_HandleRDPDone(sched);
                continue;
        }
        // Check if it's a message from the IrqMgr
        switch (((OSScMsg*)msg)->type) {
            case OS_SC_RETRACE_MSG:
                Sched_HandleRetrace(sched);
                continue;
            case OS_SC_PRE_NMI_MSG:
                Sched_HandleReset(sched);
                continue;
            case OS_SC_NMI_MSG:
                Sched_HandleStop(sched);
                continue;
        }
    }
}

/**
 * Initializes the SchedContext and scheduler thread.
 * Registers an IrqClient for the thread and fault client for the SchedContext.
 * Directs the OS to send SP and DP OS messages to interruptQ when the RSP or RDP signal task completion.
 */
void Sched_Init(SchedContext* sched, void* stack, OSPri pri, UNK_TYPE arg3, UNK_TYPE arg4, IrqMgr* irqMgr) {
    bzero(sched, sizeof(SchedContext));

    sched->shouldUpdateVi = true;

    osCreateMesgQueue(&sched->interruptQ, sched->intBuf, ARRAY_COUNT(sched->intBuf));
    osCreateMesgQueue(&sched->cmdQ, sched->cmdMsgBuf, ARRAY_COUNT(sched->cmdMsgBuf));
    osSetEventMesg(OS_EVENT_SP, &sched->interruptQ, RSP_DONE_MSG);
    osSetEventMesg(OS_EVENT_DP, &sched->interruptQ, RDP_DONE_MSG);
    IrqMgr_AddClient(irqMgr, &sched->irqClient, &sched->interruptQ);
    Fault_AddClient(&sSchedFaultClient, Sched_FaultClient, sched, NULL);
    osCreateThread(&sched->thread, Z_THREAD_ID_SCHED, Sched_ThreadEntry, sched, stack, pri);
    osStartThread(&sched->thread);
}
