// Amiga serial HAL -- serial.device via exec I/O requests.
//
// The Amiga does not expose the Paula UART registers to apps; the OS owns them
// through serial.device, which maintains its own interrupt-driven RX buffer.
// This HAL rides that buffer instead of adding one: SDCMD_QUERY reports how
// many bytes are waiting and a same-size CMD_READ pulls them without blocking,
// so jlSerialPoll needs no work (serial.device fills the buffer in the
// background) and RX cannot overrun a game-loop cadence up to io_RBufLen.
//
// Transmit is asynchronous: one CMD_WRITE stays in flight via SendIO against a
// private staging buffer, and jlSerialWrite applies backpressure (returns 0)
// while it drains, so a stalled link never blocks the frame. Two IORequests
// share one MsgPort + one OpenDevice -- the write request is a copy of the
// opened read request, so it inherits io_Device/io_Unit.
//
// Uses exec.library CreateMsgPort/CreateIORequest (KS2.0+), so no amiga.lib.

#include <exec/types.h>
#include <exec/io.h>
#include <exec/memory.h>
#include <devices/serial.h>

#include <proto/exec.h>

#include "joey/serial.h"
#include "port.h"


// Largest single async transmit; a longer jlSerialWrite is split across frames.
#define TX_STAGE_SIZE   1024


// ----- Module state -----

static struct MsgPort   *gPort = NULL;
static struct IOExtSer  *gReadReq = NULL;
static struct IOExtSer  *gWriteReq = NULL;
static bool              gOpen = false;
static bool              gWritePending = false;
static UBYTE             gTxStage[TX_STAGE_SIZE];


// ----- Prototypes -----

static void serialReapWrite(void);
static void serialTeardown(void);


// ----- Helpers -----

// Collect a completed async write so the request + staging buffer free up.
static void serialReapWrite(void) {
    if (gWritePending && CheckIO((struct IORequest *)gWriteReq) != NULL) {
        WaitIO((struct IORequest *)gWriteReq);
        gWritePending = false;
    }
}


static void serialTeardown(void) {
    if (gWritePending) {
        AbortIO((struct IORequest *)gWriteReq);
        WaitIO((struct IORequest *)gWriteReq);
        gWritePending = false;
    }
    if (gReadReq != NULL && gReadReq->IOSer.io_Device != NULL) {
        CloseDevice((struct IORequest *)gReadReq);   // shared open; closes for both
    }
    if (gWriteReq != NULL) {
        DeleteIORequest((struct IORequest *)gWriteReq);
        gWriteReq = NULL;
    }
    if (gReadReq != NULL) {
        DeleteIORequest((struct IORequest *)gReadReq);
        gReadReq = NULL;
    }
    if (gPort != NULL) {
        DeleteMsgPort(gPort);
        gPort = NULL;
    }
}


// ----- HAL entry points (alphabetical) -----

uint16_t jlpSerialAvailable(void) {
    gReadReq->IOSer.io_Command = SDCMD_QUERY;
    DoIO((struct IORequest *)gReadReq);
    if (gReadReq->IOSer.io_Actual > 0xFFFFul) {
        return 0xFFFFu;
    }
    return (uint16_t)gReadReq->IOSer.io_Actual;
}


void jlpSerialClose(void) {
    serialTeardown();
    gOpen = false;
}


void jlpSerialFlush(void) {
    gReadReq->IOSer.io_Command = CMD_CLEAR;   // discard serial.device RX buffer
    DoIO((struct IORequest *)gReadReq);
}


// Why the last jlpSerialOpen failed, for host-side diagnosis: a silent false was unattributable
// through three swallowing layers (this file -> core gate -> the client's "non-fatal" open).
// stage: 1=CreateMsgPort 2=CreateIORequest 3=OpenDevice 4=SDCMD_SETPARAMS 5=write IORequest.
uint8_t gAmigaSerialFailStage  = 0u;
BYTE    gAmigaSerialFailErr    = 0;
BYTE    gAmigaSerialFailIoErr  = 0;
uint8_t gAmigaSerialFailDevice = 0u;


bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
    ULONG  unit;
    UBYTE  flags;
    BYTE   err;

    if (gOpen) {
        return false;
    }
    gAmigaSerialFailStage = 0u;
    gAmigaSerialFailErr   = 0;

    gPort = CreateMsgPort();
    if (gPort == NULL) {
        gAmigaSerialFailStage = 1u;
        return false;
    }
    gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
    if (gReadReq == NULL) {
        gAmigaSerialFailStage = 2u;
        serialTeardown();
        return false;
    }

    // io_SerFlags participates in OPENDEVICE, not just SETPARAMS -- the autodoc contract is "the
    // request must be zeroed EXCEPT io_SerFlags, set before open".  Setting the flags only after the
    // open (the old order) both violated that contract and left the unit opened EXCLUSIVE with
    // xon/xoff ENABLED until SETPARAMS ran.
    flags = 0u;
    if (config->flow != JL_SERIAL_FLOW_XONXOFF) {
        flags |= SERF_XDISABLED;              // no software flow control: 0x11/0x13 are DATA here
    }
    if (config->flow == JL_SERIAL_FLOW_RTSCTS) {
        flags |= SERF_7WIRE;                  // hardware RTS/CTS handshake
    }
    if (config->parity != JL_SERIAL_PARITY_NONE) {
        flags |= SERF_PARTY_ON;
        if (config->parity == JL_SERIAL_PARITY_ODD) {
            flags |= SERF_PARTY_ODD;
        }
    }
    gReadReq->io_SerFlags = flags;

    // Any device but SLOT maps to the built-in port (unit 0); SLOT picks a unit.
    unit = (device == JL_SERIAL_SLOT) ? (ULONG)config->unit : 0ul;
    err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul);
    if (err != 0) {
        // Exclusive access is refused if ANYONE holds the unit; fall back to a shared open, which is
        // fine for this HAL (it is the only serial user in the program).
        gReadReq->io_SerFlags = (UBYTE)(flags | SERF_SHARED);
        err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul);
    }
    if (err != 0) {
        gAmigaSerialFailStage = 3u;
        gAmigaSerialFailErr   = err;
        // Distinguish a REAL open failure from a lying return value: io_Error is the device's own
        // verdict and io_Device is non-NULL if the device actually attached.
        gAmigaSerialFailIoErr  = gReadReq->IOSer.io_Error;
        gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u;
        serialTeardown();
        return false;
    }
    gReadReq->io_Baud     = config->baud;
    gReadReq->io_ReadLen  = config->dataBits;
    gReadReq->io_WriteLen = config->dataBits;
    gReadReq->io_StopBits = config->stopBits;
    // CreateIORequest zero-fills the request, and serial.device REJECTS a SETPARAMS whose io_RBufLen
    // is below the documented 64-byte minimum -- so leaving it at 0 made every open fail (silently,
    // three swallowed layers up).  Ask for a real ring while at it: the X68000 port just proved a
    // ~64-byte receive window loses mid-frame bytes whenever the client blits or polls input.
    gReadReq->io_RBufLen  = 4096ul;
    // io_SerFlags already holds the value the successful open used (possibly including the
    // SERF_SHARED fallback) -- do not strip it here.
    gReadReq->IOSer.io_Command = SDCMD_SETPARAMS;
    if (DoIO((struct IORequest *)gReadReq) != 0) {
        gAmigaSerialFailStage = 4u;
        gAmigaSerialFailErr   = gReadReq->IOSer.io_Error;
        serialTeardown();
        return false;
    }

    // Second request sharing the same open device (copy carries io_Device/Unit).
    gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
    if (gWriteReq == NULL) {
        gAmigaSerialFailStage = 5u;
        serialTeardown();
        return false;
    }
    *gWriteReq = *gReadReq;

    gWritePending = false;
    gOpen = true;
    return true;
}


void jlpSerialPoll(void) {
    serialReapWrite();
    // RX arrives into serial.device's own buffer; nothing to pump here.
}


uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
    ULONG  avail;
    ULONG  want;

    gReadReq->IOSer.io_Command = SDCMD_QUERY;
    DoIO((struct IORequest *)gReadReq);
    avail = gReadReq->IOSer.io_Actual;
    if (avail == 0ul) {
        return 0u;
    }
    want = (avail < (ULONG)max) ? avail : (ULONG)max;

    // These bytes are already buffered, so CMD_READ returns without blocking.
    gReadReq->IOSer.io_Command = CMD_READ;
    gReadReq->IOSer.io_Length  = want;
    gReadReq->IOSer.io_Data    = (APTR)buf;
    DoIO((struct IORequest *)gReadReq);
    return (uint16_t)gReadReq->IOSer.io_Actual;
}


uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
    uint16_t  n;
    uint16_t  i;

    serialReapWrite();
    if (gWritePending) {
        return 0u;   // previous transmit still draining -- caller retries
    }

    n = (len > TX_STAGE_SIZE) ? (uint16_t)TX_STAGE_SIZE : len;
    for (i = 0u; i < n; i++) {
        gTxStage[i] = buf[i];
    }
    gWriteReq->IOSer.io_Command = CMD_WRITE;
    gWriteReq->IOSer.io_Length  = n;
    gWriteReq->IOSer.io_Data    = (APTR)gTxStage;
    SendIO((struct IORequest *)gWriteReq);
    gWritePending = true;
    return n;
}
