/**
 * @file z_std_dma.c
 *
 * This file implements a system for structuring the ROM image and retrieving data. It is designed to have the same
 * external interface regardless of whether the ROM segments are compressed or not.
 *
 * The ROM image is partitioned into regions that are entered into the DMA data table `gDmaDataTable`. External code
 * does not directly address locations on the ROM image directly, instead a "Virtual ROM" addressing is used. Virtual
 * ROM is defined to be the ROM address of a segment in a totally uncompressed ROM. For uncompressed ROMs, "physical"
 * ROM and VROM addresses coincide. The DMA data table converts VROM to ROM addresses so that code may exclusively use
 * VROM addresses even if the ROM is compressed.
 *
 * External code typically submits requests to the DMA Manager asking for a transfer in terms of Virtual ROM; the DMA
 * Manager translates this to the physical ROM address, performs the transfer to RAM and decompresses the data if
 * required.
 * Requests are processed in the order they are received and may be submitted both synchronously and asynchronously.
 *
 * There are some additional provisions to ensure that audio DMA is particularly high-speed, the audio data is assumed
 * to be uncompressed and the request queue and address translation is skipped.
 */
#include "z64dma.h"

#include "carthandle.h"
#include "fault.h"
#include "macros.h"
#include "segment_symbols.h"
#include "stack.h"
#include "libu64/stackcheck.h"
#include "yaz0.h"
#include "z64thread.h"

size_t gDmaMgrDmaBuffSize = DMAMGR_DEFAULT_BUFSIZE;

StackEntry sDmaMgrStackInfo;
u16 sNumDmaEntries;
OSMesgQueue sDmaMgrMsgQueue;
OSMesg sDmaMgrMsgBuf[32];
OSThread sDmaMgrThread;
STACK(sDmaMgrStack, 0x500);

/**
 * Transfer `size` bytes from physical ROM address `rom` to `ram`.
 *
 * This function is intended for internal use only, however it is possible to use this function externally in which
 * case it behaves as a synchronous transfer, data is available as soon as this function returns.
 *
 * Transfers are divided into chunks based on the current value of `gDmaMgrDmaBuffSize` to avoid congestion of the PI
 * so that higher priority transfers can still be carried out in a timely manner. The transfers are sent in a queue to
 * the OS PI Manager which performs the transfer.
 *
 * @return 0 if successful, -1 if the DMA could not be queued with the PI Manager.
 */
s32 DmaMgr_DmaRomToRam(uintptr_t rom, void* ram, size_t size) {
    OSIoMesg ioMsg;
    OSMesgQueue queue;
    OSMesg msg[1];
    s32 ret;
    size_t buffSize = gDmaMgrDmaBuffSize;

    osInvalDCache(ram, size);
    osCreateMesgQueue(&queue, msg, ARRAY_COUNT(msg));

    if (buffSize != 0) {
        while (buffSize < size) {
            ioMsg.hdr.pri = OS_MESG_PRI_NORMAL;
            ioMsg.hdr.retQueue = &queue;
            ioMsg.devAddr = rom;
            ioMsg.dramAddr = ram;
            ioMsg.size = buffSize;
            ret = osEPiStartDma(gCartHandle, &ioMsg, OS_READ);
            if (ret != 0) {
                goto end;
            }

            osRecvMesg(&queue, NULL, OS_MESG_BLOCK);
            size -= buffSize;
            rom += buffSize;
            ram = (u8*)ram + buffSize;
        }
    }

    ioMsg.hdr.pri = OS_MESG_PRI_NORMAL;
    ioMsg.hdr.retQueue = &queue;
    ioMsg.devAddr = rom;
    ioMsg.dramAddr = ram;
    ioMsg.size = size;
    ret = osEPiStartDma(gCartHandle, &ioMsg, OS_READ);
    if (ret != 0) {
        goto end;
    }

    osRecvMesg(&queue, NULL, OS_MESG_BLOCK);

    osInvalDCache(ram, size);

end:
    return ret;
}

/**
 * Callback function to facilitate audio DMA. Audio DMA does not use the request queue as audio data is often needed
 * very soon after the request is sent, requiring a higher priority method for enqueueing a DMA on the OS PI command
 * queue.
 *
 * @param pihandle Cartridge ROM PI Handle.
 * @param mb IO Message describing the transfer.
 * @param direction Read or write.
 * @return 0 if the IO Message was successfully put on the OS PI command queue, < 0 otherwise
 */
s32 DmaMgr_AudioDmaHandler(OSPiHandle* pihandle, OSIoMesg* mb, s32 direction) {
    s32 ret = osEPiStartDma(pihandle, mb, direction);

    if (ret != 0) {
        PRINTF("OOPS!!\n");
    }

    return ret;
}

DmaEntry* DmaMgr_FindDmaEntry(uintptr_t vrom) {
    DmaEntry* entry;

    for (entry = gDmaDataTable; entry->file.vromEnd != 0; entry++) {
        if ((vrom >= entry->file.vromStart) && (vrom < entry->file.vromEnd)) {
            return entry;
        }
    }

    return NULL;
}

s32 DmaMgr_TranslateVromToRom(uintptr_t vrom) {
    DmaEntry* entry = DmaMgr_FindDmaEntry(vrom);

    if (entry != NULL) {
        if (entry->romEnd == 0) {
            return vrom + entry->romStart - entry->file.vromStart;
        }

        if (vrom == entry->file.vromStart) {
            return entry->romStart;
        }

        return -1;
    }

    return -1;
}

s32 DmaMgr_FindDmaIndex(uintptr_t vrom) {
    DmaEntry* entry = DmaMgr_FindDmaEntry(vrom);

    if (entry != NULL) {
        return entry - gDmaDataTable;
    }

    return -1;
}

const char* func_800809F4(uintptr_t vrom) {
    return "??";
}

void DmaMgr_ProcessRequest(DmaRequest* req) {
    uintptr_t vrom = req->vromAddr;
    void* ram = req->dramAddr;
    size_t size = req->size;
    uintptr_t romStart;
    size_t romSize;
    DmaEntry* entry;
    s32 index = DmaMgr_FindDmaIndex(vrom);

    if ((index >= 0) && (index < sNumDmaEntries)) {
        entry = &gDmaDataTable[index];
        if (entry->romEnd == 0) {
            // romEnd of 0 indicates that the file is uncompressed. Files that are stored uncompressed can have
            // only part of their content loaded into RAM, so DMA only the requested region.
            if (entry->file.vromEnd < (vrom + size)) {
                // Error, vrom + size ends up in a different file than it started in
#if MM_VERSION >= N64_US
                Fault_AddHungupAndCrash("../z_std_dma.c", 499);
#else
                Fault_AddHungupAndCrash("../z_std_dma.c", 496);
#endif
            }
            DmaMgr_DmaRomToRam((entry->romStart + vrom) - entry->file.vromStart, ram, size);
        } else {
            // File is compressed. Files that are stored compressed must be loaded into RAM all at once.

            romSize = entry->romEnd - entry->romStart;
            romStart = entry->romStart;

            if (vrom != entry->file.vromStart) {
                // Error, requested vrom is not the start of a file
#if MM_VERSION >= N64_US
                Fault_AddHungupAndCrash("../z_std_dma.c", 518);
#else
                Fault_AddHungupAndCrash("../z_std_dma.c", 515);
#endif
            }

            if (size != (entry->file.vromEnd - entry->file.vromStart)) {
                // Error, only part of the file was requested
#if MM_VERSION >= N64_US
                Fault_AddHungupAndCrash("../z_std_dma.c", 525);
#else
                Fault_AddHungupAndCrash("../z_std_dma.c", 522);
#endif
            }

            // Reduce the thread priority and decompress the file, the decompression routine handles the DMA
            // in chunks. Restores the thread priority when done.
            osSetThreadPri(NULL, Z_PRIORITY_DMAMGR_LOW);
            Yaz0_Decompress(romStart, ram, romSize);
            osSetThreadPri(NULL, Z_PRIORITY_DMAMGR);
        }
    } else {
        // Error, invalid index
#if MM_VERSION >= N64_US
        Fault_AddHungupAndCrash("../z_std_dma.c", 558);
#else
        Fault_AddHungupAndCrash("../z_std_dma.c", 555);
#endif
    }
}

void DmaMgr_ThreadEntry(void* arg) {
    OSMesg msg;
    DmaRequest* req;

    PRINTF(T("ＤＭＡマネージャスレッド実行開始\n", "DMA manager thread execution start\n"));

    while (true) {
        // Wait for DMA Requests to arrive from other threads
        osRecvMesg(&sDmaMgrMsgQueue, &msg, OS_MESG_BLOCK);

        if (msg == NULL) {
            break;
        }

        req = (DmaRequest*)msg;

        DmaMgr_ProcessRequest(req);

        // Notify the sender that the request has been processed
        if (req->notifyQueue != NULL) {
            osSendMesg(req->notifyQueue, req->notifyMsg, OS_MESG_NOBLOCK);
        }
    }

    PRINTF(T("ＤＭＡマネージャスレッド実行終了\n", "DMA manager thread execution end\n"));
}

/**
 * Submit an asynchronous DMA request. Unlike other DMA requests, this will not block the
 * current thread. Data arrival is not immediate however, ensure that the request has completed by awaiting a message
 * sent to `queue` when the DMA operation has completed.
 *
 * @param req DMA request, filled out internally.
 * @param ram Location in DRAM for data to be written.
 * @param vrom Virtual ROM location for data to be read.
 * @param size Transfer size.
 * @param queue Message queue to notify with `msg` once the transfer is complete.
 * @param msg Message to send to `queue` once the transfer is complete.
 * @return -2 if Irq is in NMI reset state, 0 otherwise
 */
s32 DmaMgr_RequestAsync(DmaRequest* req, void* ram, uintptr_t vrom, size_t size, UNK_TYPE4 unused, OSMesgQueue* queue,
                        OSMesg msg) {
    if (gIrqMgrResetStatus >= IRQ_RESET_STATUS_NMI) {
        return -2;
    }

    req->vromAddr = vrom;
    req->dramAddr = ram;
    req->size = size;
    req->unk14 = 0;
    req->notifyQueue = queue;
    req->notifyMsg = msg;

    osSendMesg(&sDmaMgrMsgQueue, (OSMesg)req, OS_MESG_BLOCK);

    return 0;
}

/**
 * Submit a synchronous DMA request. This will block the current thread until the requested transfer is complete. Data
 * is immediately available as soon as this function returns.
 *
 * @param ram Location in DRAM for data to be written.
 * @param vrom Virtual ROM location for data to be read.
 * @param size Transfer size.
 * @return 0
 */
s32 DmaMgr_RequestSync(void* ram, uintptr_t vrom, size_t size) {
    DmaRequest req;
    OSMesgQueue queue;
    OSMesg msg[1];
    s32 ret;

    osCreateMesgQueue(&queue, msg, ARRAY_COUNT(msg));

    ret = DmaMgr_RequestAsync(&req, ram, vrom, size, 0, &queue, NULL);
    if (ret == -1) { // DmaMgr_RequestAsync does not return -1
        return ret;
    }

    osRecvMesg(&queue, NULL, OS_MESG_BLOCK);

    return 0;
}

void DmaMgr_Init(void) {
    DmaMgr_DmaRomToRam(SEGMENT_ROM_START(dmadata), gDmaDataTable, SEGMENT_ROM_SIZE(dmadata));

    {
        DmaEntry* entry = gDmaDataTable;
        s32 index = 0;

        while (entry->file.vromEnd != 0) {
            entry++;
            index++;
        }

        sNumDmaEntries = index;
    }

    osCreateMesgQueue(&sDmaMgrMsgQueue, sDmaMgrMsgBuf, ARRAY_COUNT(sDmaMgrMsgBuf));
    StackCheck_Init(&sDmaMgrStackInfo, sDmaMgrStack, STACK_TOP(sDmaMgrStack), 0, 0x100, "dmamgr");
    osCreateThread(&sDmaMgrThread, Z_THREAD_ID_DMAMGR, DmaMgr_ThreadEntry, NULL, STACK_TOP(sDmaMgrStack),
                   Z_PRIORITY_DMAMGR);

    osStartThread(&sDmaMgrThread);
}

void DmaMgr_Stop(void) {
    osSendMesg(&sDmaMgrMsgQueue, NULL, OS_MESG_BLOCK);
}
