#include <Windows.h>

#include "../comdevice.h"
#include "../common.h"
#include "../input.h"
#include "../key_config.h"
#include "mx.h"

#define JVS_SENSE (g_MiceJvsNumBoards == 0 ? FALSE : _MiceJvsBoards[0].m_SenseOut)

JVS_STATUS MiceJvsWrapper(PMICE_JVS this, DWORD nMaxRead) {
    JVS_STATUS status = JVS_STATUS_OK;
    // -1, because we don't want to treat the checksum as a command!
    while (this->m_nInIdx < nMaxRead - 1) {
        JVS_STATUS subStatus = this->vftable->transact(this, MiceJvsRead(this));
        if (subStatus > status) status = subStatus;
    }

    return status;
}

void jvs_send_status(unsigned char status, unsigned char* outData, LPDWORD outCount) {
    short respIndex = 4;

    outData[0] = JVS_SYNC;
    outData[1] = JVS_NODE_MASTER;
    outData[2] = 2;
    if (status == JVS_MARK || status == JVS_SYNC) {
        outData[3] = JVS_MARK;
        outData[4] = status - 1;
        outData[5] = JVS_NODE_MASTER + 2 + status;
        *outCount = 6;
    } else {
        outData[3] = status;
        outData[4] = JVS_NODE_MASTER + 2 + status;
        *outCount = 5;
    }
}

static float jvsRateAccum;
static float jvsDTHistory[60];
static size_t jvsDTHIndex = 0;
static size_t jvsDTHCount = 0;
float jvsRate = 0;

BYTE jvsInUnmasked[MICE_JVS_MAX];
BYTE jvsOutUnmasked[MICE_JVS_MAX];
BYTE jvsOutMasked[MICE_JVS_MASKED];
void mxjvs_handle(unsigned char* lpMaskedIn, short nMaskedCount, unsigned char* outData,
                  short maxOut, LPDWORD outCount) {
    // Poll rate calculation
    static amtime_t last_tick = { .seconds = 0, .microseconds = 0 };
    amtime_t now;
    amiTimerGet(&now);

    float dt = (float)amiTimerDiffUsec(&last_tick, &now) / (float)1000000;
    if (last_tick.seconds != 0 || last_tick.microseconds != 0) {
        jvsRateAccum += dt - jvsDTHistory[jvsDTHIndex];
        jvsDTHistory[jvsDTHIndex] = dt;
        if ((++jvsDTHIndex) == _countof(jvsDTHistory)) jvsDTHIndex = 0;
        if ((++jvsDTHCount) > _countof(jvsDTHistory)) jvsDTHCount = _countof(jvsDTHistory);

        jvsRate = (jvsRateAccum > 0.0f) ? (1.0f / (jvsRateAccum / (float)jvsDTHCount)) : FLT_MAX;

        // log_game(plfAime, "jvs rate: %.2fHz", jvsRate);
    }
    last_tick = now;

    // Actual handler start

    SHORT nIn = 0;
    BOOL wasMark = FALSE;
    for (int i = 0; i < nMaskedCount; i++) {
        if (lpMaskedIn[i] == JVS_MARK) {
            wasMark = TRUE;
            continue;
        }
        if (wasMark)
            jvsInUnmasked[nIn++] = lpMaskedIn[i] + 1;
        else
            jvsInUnmasked[nIn++] = lpMaskedIn[i];
    }
    PJVS_PACKET lpPacket = (PJVS_PACKET)jvsInUnmasked;

    // JVS frame is 4 bytes in total
    if (nIn < 4) {
        log_error(plfMxJvs, "inCount impossibly small: %d", nIn);
        jvs_send_status(JVS_STATUS_UNKNOWN, outData, outCount);
        return;
    }
    // This isn't a JVS packet. We should scan for SYNC, but I don't think we'll ever need to
    if (lpPacket->m_Sync != JVS_SYNC) {
        log_error(plfMxJvs, "SYNC missing. Saw 0x%02x", lpPacket->m_Sync);
        jvs_send_status(JVS_STATUS_UNKNOWN, outData, outCount);
        return;
    }

    // Validate the checksum before proceeding
    unsigned char sum = 0;
    bool escape = false;
    for (int i = 1; i < nIn - 1; i++) sum += jvsInUnmasked[i];
    if (sum != jvsInUnmasked[nIn - 1]) {
        log_error(plfMxJvs, "Checksum failed. Computed 0x%02x, expected 0x%02x", sum,
                  jvsInUnmasked[nIn - 1]);
        jvs_send_status(JVS_STATUS_SUM, outData, outCount);
        return;
    }

    DWORD nBoardCount = g_MiceJvsNumBoards;
    if (nBoardCount > _countof(_MiceJvsBoards)) nBoardCount = _countof(_MiceJvsBoards);

    // Not broadcast, not master-bound, and within bounds
    if (lpPacket->m_Dst != 0xff && (lpPacket->m_Dst == 0 || lpPacket->m_Dst > nBoardCount)) {
        log_warning(plfMxJvs, "JVS bound for %02x ignored", lpPacket->m_Dst);
        *outCount = 0;
        return;
    }

    JVS_STATUS status = JVS_STATUS_UNKNOWN;
    DWORD nBytesWrote = 0;
    if (lpPacket->m_Dst == 0xff) {
        for (DWORD i = 0; i < nBoardCount; i++) {
            PMICE_JVS lpMiceJvs = &_MiceJvsBoards[i];

            lpMiceJvs->m_lpInData = jvsInUnmasked;
            lpMiceJvs->m_lpOutData = jvsOutUnmasked;
            lpMiceJvs->m_nInIdx = 3;
            lpMiceJvs->m_nOutIdx = 4;

            status = MiceJvsWrapper(lpMiceJvs, nIn);
            if (lpMiceJvs->m_nOutIdx > 4) {
                nBytesWrote = lpMiceJvs->m_nOutIdx - 4;
                break;
            }
        }
    } else {
        PMICE_JVS lpMiceJvs = &_MiceJvsBoards[nBoardCount - lpPacket->m_Dst];

        lpMiceJvs->m_lpInData = jvsInUnmasked;
        lpMiceJvs->m_lpOutData = jvsOutUnmasked;
        lpMiceJvs->m_nInIdx = 3;
        lpMiceJvs->m_nOutIdx = 4;

        status = MiceJvsWrapper(lpMiceJvs, nIn);
        nBytesWrote = lpMiceJvs->m_nOutIdx - 4;
    }

    if (status == JVS_STATUS_SILENCE) {
        *outCount = 0;
        return;
    }

    if (status == JVS_STATUS_OK) {
        ((PJVS_PACKET)&jvsOutUnmasked)->m_Dst = JVS_NODE_MASTER;
        ((PJVS_PACKET)&jvsOutUnmasked)->m_nBytes = (nBytesWrote + 2) & 0xff;
        jvsOutUnmasked[3] = status;

        sum = 0;
        for (DWORD i = 1; i < nBytesWrote + 4; i++) sum += jvsOutUnmasked[i];
        jvsOutUnmasked[nBytesWrote + 4] = sum;

        DWORD nPadded = 1;
        jvsOutMasked[0] = JVS_SYNC;
        for (DWORD i = 1; i <= nBytesWrote + 4; i++) {
            if (i == JVS_MARK || i == JVS_SYNC) {
                jvsOutMasked[nPadded++] = JVS_MARK;
                jvsOutMasked[nPadded++] = jvsOutUnmasked[i] - 1;
            } else
                jvsOutMasked[nPadded++] = jvsOutUnmasked[i];
        }

        *outCount = nPadded;
        memcpy_s(outData, maxOut, jvsOutMasked, nPadded);
    } else {
        log_error(plfMxJvs, "JVS board %d returned status %d", lpPacket->m_Dst, status);
        jvs_send_status(status, outData, outCount);
    }
}

BOOL WINAPI mxjvs_DeviceIoControl(file_context_t* ctx, DWORD dwIoControlCode, LPVOID lpInBuffer,
                                  DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize,
                                  LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped) {
    switch (dwIoControlCode) {
        case IOCTL_MXJVS_EXCHANGE:
            log_trace(plfMxJvs,
                      "DeviceIoControl(<mxjvs>, <exchange>, 0x%p, 0x%x, -, "
                      "0x%x, -, -)",
                      lpInBuffer, nInBufferSize, nOutBufferSize);

            mxjvs_handle(lpInBuffer, nInBufferSize & 0xffff, lpOutBuffer, nOutBufferSize & 0xFFFF,
                         lpBytesReturned);

            break;
        default:
            log_warning(plfMxJvs, "unhandled 0x%08x", dwIoControlCode);
            return FALSE;
    }

    return TRUE;
}

BOOL mxjvs_SetupComm(void* com, DWORD dwInQueue, DWORD dwOutQueue) { return TRUE; }
BOOL mxjvs_PurgeComm(void* com, DWORD dwFlags) { return TRUE; }
BOOL mxjvs_GetCommState(void* com, LPDCB lpDCB) { return TRUE; }
BOOL mxjvs_SetCommTimeouts(void* com, LPCOMMTIMEOUTS lpDCB) { return TRUE; }

BOOL mxjvs_GetCommModemStatus(void* com, LPDWORD lpModelStat) {
    *lpModelStat = JVS_SENSE ? 0 : MS_DSR_ON;
    return TRUE;
}

BOOL mxjvs_SetCommState(void* com, LPDCB lpDCB) {
    char PARITY[] = { 'N', 'O', 'E', 'M', 'S' };
    char* STOP[] = { "1", "1.5", "2" };
    log_misc(plfMxJvs, "Switching to %d baud (%d%c%s)", lpDCB->BaudRate, lpDCB->ByteSize,
             PARITY[lpDCB->Parity], STOP[lpDCB->StopBits]);
    return TRUE;
}

void init_jvs_boards() {
    // new (MiceJvs837_14895, _MiceJvsBoards[0]);
    new (MiceJvs837_14572, _MiceJvsBoards[0]);

    // Init all the others
    for (int i = 1; i < _countof(_MiceJvsBoards); i++) {
        new (MiceJvs837_14572, _MiceJvsBoards[i]);
    }
}

DWORD __stdcall mxjvs_comdev_thread(com_device_t* com) {
    BYTE inBuffer[512];
    BYTE outBuffer[512];
    DWORD bytesReturned;

    while (1) {
        do {
            comdev_read_blocking(com, inBuffer, 1);
        } while (inBuffer[0] != 0xE0);
        comdev_read_blocking(com, inBuffer + 1, 2);
        BYTE nbytes = inBuffer[2];

        BYTE* bufferPtr = inBuffer + 3;
        if (nbytes == 0xD0) {
            comdev_read_blocking(com, bufferPtr++, 1);
            nbytes = *bufferPtr;
        }
        while (nbytes) {
            comdev_read_blocking(com, bufferPtr++, 1);
            if (bufferPtr[-1] == 0xD0) comdev_read_blocking(com, bufferPtr++, 1);
            nbytes--;
        }

        short packetSize = (bufferPtr - inBuffer) & 0xFFFF;
        mxjvs_handle(inBuffer, packetSize, outBuffer, _countof(outBuffer), &bytesReturned);
        com->modemStatus = JVS_SENSE ? 0 : MS_DSR_ON;
        comdev_write(com, outBuffer, bytesReturned & 0xFFFF);
    }
}

VIRTUAL_SERIAL_DEVICE jvsSerialDevice = {
    .m_Name = "JVS",
    .m_Entrypoint = mxjvs_comdev_thread,
};

void setup_mxjvs() {
    init_jvs_boards();

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

    com_hook_t* jvscom = new_com_hook(4);
    wcscpy_s(jvscom->wName, sizeof jvscom->wName, L"\\\\.\\mxjvs");
    wcscpy_s(jvscom->wDosName, sizeof jvscom->wDosName, L"\\\\.\\mxjvs");
    jvscom->GetCommState = mxjvs_GetCommState;
    jvscom->SetCommState = mxjvs_SetCommState;
    jvscom->SetCommTimeouts = mxjvs_SetCommTimeouts;
    jvscom->SetupComm = mxjvs_SetupComm;
    jvscom->PurgeComm = mxjvs_PurgeComm;
    jvscom->GetCommModemStatus = mxjvs_GetCommModemStatus;

    mxjvs->com_hook = jvscom;

    attach_com_device(4, &jvsSerialDevice);
}
