/**
 * @file mempak.c
 *
 * This file implements an interface for a controller memory pak filesystem (pfs), with operations to create, delete,
 * read, write and check the size of files, as well as obtain a listing of which files currently exist.
 *
 * Each file is assigned an uppercase ASCII letter as an identifier, the game name for each is marked as
 * 'ZELDA DEMO TOOL', encoded according to the N64 Font Code described in section 26.3 of the N64 Programming Manual.
 */
#include "mempak.h"
#include "padmgr.h"
#include "printf.h"

#define MEMPAK_MAX_FILES 11

OSPfs sMempakPfsHandle;
s32 sMempakFreeBytes;
s32 sMempakFiles[MEMPAK_MAX_FILES];

u16 sMempakCompanyCode = 1;
u32 sMempakGameCode = 1;

// NCH is a heavily abbreviated "N64 font code CHaracter"
// Conversion from A-Z to N64 Font Code
#define NCH(c) ((c)-0x27)
// Conversion from spaces to N64 Font Code
#define NCH_SPC (0x0F)

u8 sMempakGameName[PFS_FILE_NAME_LEN] = {
    NCH('Z'), NCH('E'), NCH('L'), NCH('D'), NCH('A'), NCH_SPC,  NCH('D'), NCH('E'),
    NCH('M'), NCH('O'), NCH_SPC,  NCH('T'), NCH('O'), NCH('O'), NCH('L'), NCH_SPC,
};

u8 sMempakExtName[PFS_FILE_EXT_LEN] = { 0 };

/**
 * Initializes the memory pak filesystem for a memory pak in the controller plugged into the specified port. Subsequent
 * memory pak operations will use the same controller port.
 *
 * @return true if the operation completed successfully, false otherwise
 */
s32 Mempak_Init(s32 controllerNum) {
    OSMesgQueue* serialEventQueue;
    s32 pad;
    s32 ret = false;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    if (osPfsInitPak(serialEventQueue, &sMempakPfsHandle, controllerNum) == 0) {
        ret = true;
    }

    osPfsFreeBlocks(&sMempakPfsHandle, &sMempakFreeBytes);
    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    return ret;
}

s32 Mempak_GetFreeBytes(s32 controllerNum) {
    return sMempakFreeBytes;
}

/**
 * Checks if the files identified by letters between `start` and `end` (inclusive) exist on the memory pak.
 *
 * This must be called before performing any individual file operations.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param start Start file letter
 * @param end End file letter (inclusive)
 * @return a bitfield where set bits indicate that the file exists
 */
s32 Mempak_FindFiles(s32 controllerNum, char start, char end) {
    OSMesgQueue* serialEventQueue;
    s32 error;
    char letter;
    u32 bit = 1;
    s32 bits = 0;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    for (letter = start; letter <= end; letter++) {
        sMempakExtName[0] = NCH(letter);

        error = osPfsFindFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName, sMempakExtName,
                              &sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)]);
        if (error == 0) {
            bits |= bit;
        } else {
            sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)] = -1;
        }

        bit <<= 1;
        PRINTF("mempak: find '%c' (%d)\n", letter, error);
    }

    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    PRINTF("mempak: find '%c' - '%c' %02x\n", start, end, bits);
    return bits;
}

/**
 * Writes data to the file identified with `letter`.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param letter Memory pak file letter, in the range 'A' to ('A' + MEMPAK_MAX_FILES)
 * @param buffer Buffer containing data to write
 * @param offset Offset into the file to write to
 * @param size Size in bytes
 * @return true if the operation completed successfully, false otherwise
 */
s32 Mempak_Write(s32 controllerNum, char letter, void* buffer, s32 offset, s32 size) {
    OSMesgQueue* serialEventQueue;
    s32 error;
    s32 ret = false;
    s32 pad;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    if (size < sMempakFreeBytes) {
        error = osPfsReadWriteFile(&sMempakPfsHandle, sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)], PFS_WRITE, offset,
                                   size, buffer);
        if (error == 0) {
            ret = true;
        }
        PRINTF("mempak: write %d byte '%c' (%d)->%d\n", size, letter, sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)],
               error);
    }
    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    return ret;
}

/**
 * Reads data from the file identified with `letter`.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param letter Memory pak file letter, in the range 'A' to ('A' + MEMPAK_MAX_FILES)
 * @param buffer Buffer to read data into
 * @param offset Offset into the file to read from
 * @param size Size in bytes
 * @return true if the operation completed successfully, false otherwise
 */
s32 Mempak_Read(s32 controllerNum, char letter, void* buffer, s32 offset, s32 size) {
    OSMesgQueue* serialEventQueue;
    s32 error;
    s32 ret = false;
    s32 pad;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    if (size < sMempakFreeBytes) {
        error = osPfsReadWriteFile(&sMempakPfsHandle, sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)], PFS_READ, offset,
                                   size, buffer);
        if (error == 0) {
            ret = true;
        }
        PRINTF("mempak: read %d byte '%c' (%d)<-%d\n", size, letter, sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)],
               error);
    }
    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    return ret;
}

/**
 * Creates a new file on the memory pak.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param letter Memory pak file letter, in the range 'A' to ('A' + MEMPAK_MAX_FILES).
 *      If this points to a valid file letter the new file will be created using that letter, otherwise it will create
 *      a file using the first free letter and return it through this argument. If no letters are free, the last letter
 *      ('A' + MEMPAK_MAX_FILES - 1) is used.
 * @param size File size
 * @return true if the operation completed successfully, false otherwise
 */
s32 Mempak_CreateFile(s32 controllerNum, char* letter, s32 size) {
    OSMesgQueue* serialEventQueue;
    s32 error;
    s32 ret = false;
    s32 i;
    s32 pad;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    if (*letter >= MEMPAK_INDEX_TO_LETTER(0) && *letter < MEMPAK_INDEX_TO_LETTER(MEMPAK_MAX_FILES)) {
        // Create file with specific letter

        sMempakExtName[0] = NCH(*letter);
        if (-1 == sMempakFiles[MEMPAK_LETTER_TO_INDEX(*letter)]) {
            // File does not already exist

            error = osPfsAllocateFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName,
                                      sMempakExtName, size, &sMempakFiles[MEMPAK_LETTER_TO_INDEX(*letter)]);
            if (error == 0) {
                ret = true;
            }
            PRINTF("mempak: alloc %d byte '%c' (%d)\n", size, *letter, error);
        } else {
            // File already exists, delete then alloc

            sMempakExtName[0] = NCH(*letter);
            if (osPfsDeleteFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName,
                                sMempakExtName) == 0) {
                ret = true;
            }
            error = osPfsAllocateFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName,
                                      sMempakExtName, size, &sMempakFiles[MEMPAK_LETTER_TO_INDEX(*letter)]);
            if (error == 0) {
                ret |= true;
            }
            PRINTF("mempak: resize %d byte '%c' (%d)\n", size, *letter, error);
        }
    } else {
        // Find first free letter and create a file identified by it
        for (i = 0; i < MEMPAK_MAX_FILES - 1; i++) {
            if (sMempakFiles[i] == -1) {
                break;
            }
        }
        *letter = MEMPAK_INDEX_TO_LETTER(i);

        sMempakExtName[0] = NCH(*letter);
        error = osPfsAllocateFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName,
                                  sMempakExtName, size, &sMempakFiles[i]);
        PRINTF("mempak: alloc %d byte '%c' (%d) with search\n", size, *letter, error);
        if (error == 0) {
            ret = true;
        }
    }
    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    return ret;
}

/**
 * Deletes the file identified with `letter`.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param letter Memory pak file letter, in the range 'A' to ('A' + MEMPAK_MAX_FILES)
 * @return true if the operation completed successfully, false otherwise
 */
s32 Mempak_DeleteFile(s32 controllerNum, char letter) {
    OSMesgQueue* serialEventQueue;
    s32 error;
    s32 ret = false;

    serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);

    sMempakExtName[0] = NCH(letter);
    error = osPfsDeleteFile(&sMempakPfsHandle, sMempakCompanyCode, sMempakGameCode, sMempakGameName, sMempakExtName);
    if (error == 0) {
        ret = true;
    }
    PRINTF("mempak: delete '%c' (%d)\n", letter, error);

    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);
    return ret;
}

/**
 * Gets the size of the file identified with `letter`.
 *
 * @param controllerNum Unused, the controller used is that which was last passed to `Mempak_Init`
 * @param letter Memory pak file letter, in the range 'A' to ('A' + MEMPAK_MAX_FILES)
 * @return the size of the file, or 0 if the operation failed for any reason
 */
s32 Mempak_GetFileSize(s32 controllerNum, char letter) {
    OSMesgQueue* serialEventQueue = PadMgr_AcquireSerialEventQueue(&gPadMgr);
    OSPfsState state;
    s32 error = osPfsFileState(&sMempakPfsHandle, sMempakFiles[MEMPAK_LETTER_TO_INDEX(letter)], &state);
    s32 pad;

    PadMgr_ReleaseSerialEventQueue(&gPadMgr, serialEventQueue);

    if (error != 0) {
        return 0;
    }
    return state.file_size;
}
