#include "mxparallel.h"

#include "../../sysconf.h"

BYTE parallel_flags = 0x00;
BYTE parallel_ctrl = 0x00;
BYTE parallel_status = 0x00;
BYTE parallel_data = 0x00;

BYTE KEYCHIP_ID[16] = KEY_ID;
BYTE _MAIN_ID[16] = MAIN_ID;

appboot_t APPBOOT = {
    .format = 1,
    .game_id = GAME_ID,
    .region = 8 | 4 | 2 | 1,
    .model_type = 2,
    // Bitfield
    // 1 = use flash for appboot
    .system_flag = 0x24,
    ._ = 0,
    .platform_id = HW_ID,
    .dvd_flag = 1,
    .network_addr = (192 << 0) | (168 << 8) | (103 << 16) | (0 << 24),
    .__ = 0,
    .seed = 1,
};
billing_t BILLING = {
    .playlimit = 21046,
    .nearfull = 512,
};
uint32_t BILLING_PLAYCOUNT = 69420;

#define overlappedComplete(len)               \
    do {                                      \
        if (lpOverlapped) {                   \
            SetLastError(ERROR_SUCCESS);      \
            lpOverlapped->Internal = 0;       \
            lpOverlapped->InternalHigh = len; \
            SetEvent(lpOverlapped->hEvent);   \
        }                                     \
    } while (0)
#define outLen(len)                                  \
    do {                                             \
        if (lpBytesReturned) *lpBytesReturned = len; \
        overlappedComplete(len);                     \
    } while (0)

BOOL WINAPI mxparallel_DeviceIoControl(file_context_t* ctx, DWORD dwIoControlCode, LPVOID lpInBuffer,
                                DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize,
                                LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped) {
    log_trace(plfMxParallel, "DeviceIoControl(<mxparallel>, 0x%08x, 0x%p, 0x%x, -, 0x%x, -, -)",
              dwIoControlCode, lpInBuffer, nInBufferSize, nOutBufferSize);

    switch (dwIoControlCode) {
        case IOCTL_MXPARALLEL_READ_FLAGS:
            ((LPBYTE)lpOutBuffer)[0] = parallel_flags;
            outLen(1);
            return TRUE;
        case IOCTL_MXPARALLEL_WRITE_FLAGS:
            parallel_flags = ((LPBYTE)lpInBuffer)[0];
            outLen(0);
            return TRUE;

        case IOCTL_MXPARALLEL_READ_CTRL_PORT:
            ((LPBYTE)lpOutBuffer)[0] = parallel_ctrl;
            outLen(1);
            return TRUE;
        case IOCTL_MXPARALLEL_WRITE_CTRL_PORT:
            parallel_ctrl = ((LPBYTE)lpInBuffer)[0];
            // log_warning(plfMxParallel, "Write ctrl %08x", parallel_ctrl);
            outLen(0);
            return TRUE;

        case IOCTL_MXPARALLEL_READ_STATUS:
            ((LPBYTE)lpOutBuffer)[0] = parallel_status;
            outLen(1);
            return TRUE;
        case IOCTL_MXPARALLEL_WRITE_STATUS:
            parallel_status = ((LPBYTE)lpInBuffer)[0];
            outLen(0);
            return TRUE;

        case IOCTL_MXPARALLEL_READ_DATA:
            ((LPBYTE)lpOutBuffer)[0] = parallel_data;
            outLen(1);
            return TRUE;
        case IOCTL_MXPARALLEL_WRITE_DATA:
            parallel_data = ((LPBYTE)lpInBuffer)[0];
            outLen(0);
            return TRUE;
    }
    return FALSE;
}

// * Our implementation needs to use the opposite keys to mxkeychip

void micexkTransportSend(unsigned char* send_data, int nbytes) {
    setAck;
    for (int i = 0; i < nbytes; i++) {
        while (_Strobe) YieldProcessor();
        setBusy;
        parallel_data = send_data[i];
        while (!_Strobe) YieldProcessor();
        clearBusy;
        while (_Strobe) YieldProcessor();
    }
    clearAck;
}
void micexkSendPacket(unsigned char* send_data) {
    BYTE encrypted[16];
    memset(encrypted, 0, sizeof encrypted);
    mxkCryptEncryptData(encrypted, send_data);
    micexkTransportSend(encrypted, 16);
}

void micexkTransportRecv(unsigned char* buffer, int nbytes) {
    for (int i = 0; i < nbytes; i++) {
        clearBusy;
        while (!_Strobe) YieldProcessor();
        buffer[i] = parallel_data;
        setBusy;
        while (_Strobe) YieldProcessor();
        clearBusy;
    }
}
void micexkRecvPacket(unsigned char* packet) {
    unsigned char buffer[16];
    micexkTransportRecv(buffer, 16);
    mxkCryptDecryptData(buffer, packet);
}

BYTE nvram[0x4000];
BYTE eeprom[0x1000];
BYTE flash[0x100000];
void init_nv_storage() {
    memset(nvram, 0xff, sizeof nvram);
    memset(eeprom, 0xff, sizeof eeprom);
    memset(flash, 0xff, sizeof flash);

    // NVRAM
    nvram_data_block_t block;
    ZeroMemory(&block, sizeof block);
    block.length = sizeof BILLING_PUBKEY;
    memcpy(block.data, BILLING_PUBKEY, sizeof BILLING_PUBKEY);
    memcpy(&nvram[0x1800], &block, sizeof block);

    block.length = sizeof BILLING_CACERT;
    memcpy(block.data, BILLING_CACERT, sizeof BILLING_CACERT);
    memcpy(&nvram[0x1c00], &block, sizeof block);

    // Flash
    billing_t billing_info;
    ZeroMemory(&billing_info, sizeof billing_info);
    billing_info.playlimit = 21046;
    billing_info.nearfull = 512;
    mxkSignValue(billing_info.nearfull, billing_info.nearfull_sig);
    mxkSignValue(billing_info.playlimit, billing_info.playlimit_sig);
    billing_info.crc = amiCrc32RCalc(sizeof billing_info - 4, (unsigned char*)&billing_info + 4, 0);

    memcpy(&flash[0x7a000], &billing_info, sizeof billing_info);
    memcpy(&flash[0x7b000], &billing_info, sizeof billing_info);
}

void dump_nv_storage() {
    FILE* fp;
    fopen_s(&fp, "dev/kc/nvram.bin", "wb");
    fwrite(nvram, 1, sizeof nvram, fp);
    fclose(fp);
    fopen_s(&fp, "dev/kc/eeprom.bin", "wb");
    fwrite(eeprom, 1, sizeof eeprom, fp);
    fclose(fp);
    fopen_s(&fp, "dev/kc/flash.bin", "wb");
    fwrite(flash, 1, sizeof flash, fp);
    fclose(fp);
}

/*
addr = (0x70 + block) * 0x1000 + offset

Eeprom:
    Appears to store entries detailing tracedata metadata
    [4:crc32] [12:?]

    one copy at 0x000, one copy at 0x100
    16 entries
    first dword: CRC
    second dword: ??
    third dword: ??
    fourth dword:
        word 1: ??
        word 2: 0xffff

Nvram:
    2x 0x400 blocks
    [4:length] [length:data]
    first block: billing pubkey
    second block: cacert

Flash (25DF041A):
    nvram0: reads 070000<->070fff
    nvram1: reads 071000<->071fff
    nvram2: reads 072000<->072fff
    nvram3: reads 073000<->073fff
    nvram4: reads 074000<->074fff
    nvram5: reads 075000<->075fff
    nvram6: reads 076000<->076fff
    nvram7: reads 077000<->077fff
    nvram8: reads 078000<->078fff
    nvram9: reads 079000<->079fff

    billing info a: 0x7a000
    billing info b: 0x7b000

    - The first 0x80 bytes of each tracedata sector is a bitfield indicating which blocks have been
        used
    - There are 1022 0x40 blocks per tracedata sector (the last two are always unsed, to account for
        the 0x80 of metadata)
    - There are 7 tracedata sectors, at 0x00000, 0x10000, 0x20000, ..., 0x60000
    - Each slot is DES ECB encrypted, with key 4D77F1748D6D1094

*/

BYTE kc_aes_iv[16];

void mxparallel_process_packet(BYTE* request) {
    BYTE response[16];
    memset(response, 10, sizeof response);

    response[0] = request[0];
    switch (request[0]) {
        // We're pretending to be the keychip, so S and R are swapped for us!
        case SetKeyS:
            log_info(plfMxParallel, "SetKeyS");
            micexkRecvPacket(request);
            mxkSetKeyR(request);
            micexkSendPacket(response);
            break;
        case SetKeyR:
            log_info(plfMxParallel, "SetKeyR");
            micexkRecvPacket(request);
            mxkSetKeyS(request);
            micexkSendPacket(response);
            break;

        case Encrypt:
            log_info(plfMxParallel, "Encrypt");
            micexkRecvPacket(request);
            micexkSendPacket(request);
            break;
        case Decrypt:
            log_info(plfMxParallel, "Decrypt");
            micexkRecvPacket(request);
            micexkSendPacket(request);
            break;
        case SetIV:
            log_info(plfMxParallel, "SetIV");
            ZeroMemory(kc_aes_iv, sizeof kc_aes_iv);
            micexkSendPacket(response);
            break;
        case GetAppBootInfo:
            log_info(plfMxParallel, "GetAppBootInfo");
            if (request[1] != 0x00) {
                log_warning(plfMxParallel, "GetAppBootInfo[%d] unexpected!", request[1]);
            }

            APPBOOT.crc = amiCrc32RCalc(sizeof APPBOOT - 4, (unsigned char*)&APPBOOT + 4, 0);
            for (int i = 0; i < sizeof APPBOOT; i += 16) {
                micexkSendPacket((unsigned char*)(&APPBOOT) + i);
            }
            break;

        case KcGetVersion:
            log_info(plfMxParallel, "GetVersion");
            response[0] = 0x01;
            response[1] = 0x04;
            micexkSendPacket(response);
            break;

        case FlashRead: {
            uint32_t addr = request[1] | (request[2] << 8) | (request[3] << 16);
            uint32_t nbytes = request[4] | (request[5] << 8) | (request[6] << 16);
            log_info(plfMxParallel, "FlashRead: %06x/%06x", addr, nbytes);

            if (addr + nbytes <= sizeof flash)
                micexkTransportSend(&flash[addr], nbytes);
            else
                log_error(plfMxParallel, "Flash read would exceed storage!");

            break;
        }
        case FlashErase: {
            uint32_t addr = request[1] | (request[2] << 8) | (request[3] << 16);
            log_info(plfMxParallel, "FlashErase: %06x", addr);

            micexkSendPacket(response);
            break;
        }
        case FlashWrite: {
            uint32_t addr = request[1] | (request[2] << 8) | (request[3] << 16);
            uint32_t nbytes = request[4] | (request[5] << 8) | (request[6] << 16);
            log_info(plfMxParallel, "FlashWrite: %06x/%06x", addr, nbytes);

            if (addr + nbytes <= sizeof flash)
                micexkTransportRecv(&flash[addr], nbytes);
            else
                log_error(plfMxParallel, "Flash write would exceed storage!");

            micexkSendPacket(response);
            dump_nv_storage();
            break;
        }

        // EEPROM?
        case EepromWrite: {
            // TODO: What is this? Appears to be some sort of EEPROM write
            uint8_t offset = request[1];
            log_info(plfMxParallel, "EepromWrite: %02x", offset);

            if (offset * 16 + 16 <= sizeof eeprom) {
                micexkRecvPacket(&eeprom[offset * 16]);
            } else {
                log_error(plfMxParallel, "EEPROM write would exceed storage!");
            }
            micexkSendPacket(response);
            dump_nv_storage();
            break;
        }
        case EepromRead: {
            // TODO: What is this? Appears to be some sort of EEPROM read
            uint8_t offset = request[1];
            log_info(plfMxParallel, "EepromRead: %02x", offset);

            if (offset * 16 + 16 <= sizeof eeprom) {
                micexkSendPacket(&eeprom[offset * 16]);
            } else {
                log_error(plfMxParallel, "EEPROM read would exceed storage!");
            }
            break;
        }

        // NVRAM?
        case NvramWrite: {
            // TODO: What is this? Appears to be some sort of memory write
            uint16_t addr = request[1] | (request[2] << 8);
            uint8_t blocks = request[3];
            log_info(plfMxParallel, "NvramWrite: %04x (%02x)", addr, blocks);

            if (addr + blocks * 16 <= sizeof nvram) {
                for (byte i = 0; i < blocks; i++) {
                    micexkRecvPacket(&(nvram[addr + (i * 16)]));
                }
                micexkSendPacket(response);
            } else {
                log_error(plfMxParallel, "NVRAM write would exceed storage!");
            }
            dump_nv_storage();
            break;
        }
        case NvramRead: {
            // TODO: What is this? Appears to be some sort of memory read
            uint16_t addr = request[1] | (request[2] << 8);
            uint8_t blocks = request[3];
            log_info(plfMxParallel, "NvramRead: %04x (%02x)", addr, blocks);

            if (addr + blocks * 16 <= sizeof nvram) {
                for (byte i = 0; i < blocks; i++) {
                    micexkSendPacket(&(nvram[addr + (i * 16)]));
                }
            } else {
                log_error(plfMxParallel, "NVRAM read would exceed storage!");
            }
            break;
        }

        case AddPlayCount:
            log_info(plfMxParallel, "AddPlayCount");
            BILLING_PLAYCOUNT++;
            micexkSendPacket(response);
            break;

        case SetMainId:
            log_info(plfMxParallel, "SetMainId");

            // micexkRecvPacket(_MAIN_ID);
            micexkRecvPacket(request);
            response[0] = 0xff;
            micexkSendPacket(response);
            break;
        case GetMainId:
            log_info(plfMxParallel, "GetMainId");
            micexkSendPacket(_MAIN_ID);
            break;
        case SetKeyId:
            log_info(plfMxParallel, "SetKeyId");
            micexkRecvPacket(KEYCHIP_ID);
            micexkSendPacket(response);
            break;
        case GetKeyId:
            log_info(plfMxParallel, "GetKeyId");
            micexkSendPacket(KEYCHIP_ID);
            break;

        case GetPlayCounter:
            log_info(plfMxParallel, "GetPlayCounter");
            ((uint32_t*)response)[0] = BILLING_PLAYCOUNT;
            micexkSendPacket(response);
            break;

        default:
            log_error(plfMxParallel, "Unhandled opcode: %d", request[0]);
            for (byte i = 0; i < 16; i++) {
                printf("%02x ", request[i]);
            }
            puts("");

            break;
    }
}

DWORD WINAPI mxparallel_thread(LPVOID _) {
    log_info(plfMxParallel, "Parallel device thread spawned");
    clearBusy;
    clearAck;

    BYTE last_strobe = 0x00;
    while (1) {
        unsigned char packet[16];
        micexkRecvPacket(packet);
        mxparallel_process_packet(packet);
    }
}

void setup_mxparallel() {
    // We're pretending to be a keychip, so need to swap the keys!
    mxkSwapKeys();

    init_nv_storage();

    file_hook_t* mxparallel = new_file_hook(L"\\\\.\\mxparallel");
    mxparallel->DeviceIoControl = &mxparallel_DeviceIoControl;
    hook_file(mxparallel);

    CreateThread(NULL, 0, mxparallel_thread, NULL, 0, NULL);
}
