// Sharp X68000 serial HAL: RS-232C via the IOCS polled API.
//
// Structurally the Atari ST HAL (src/atarist/serial.c): ride the OS's polled
// interface, hijack no vector, buffer nothing ourselves. The IOCS calls map
// one-for-one onto the ST's XBIOS ones --
//     _iocs_set232c   <- Rsconf     (baud + framing, one packed word)
//     _iocs_isns232c  <- Bconstat   (input status)
//     _iocs_inp232c   <- Bconin     (read one byte, BLOCKS if empty)
//     _iocs_osns232c  <- Bcostat    (transmitter ready)
//     _iocs_out232c   <- Bconout    (write one byte)
//
// The machine has one RS-232C line (SCC channel A); channel B is the mouse and
// is not reachable here, so jlSerialDeviceE is accepted and ignored exactly as
// on the ST.

#include <x68k/iocs.h>

#include "port.h"


// LINE CONFIGURATION IS DELIBERATELY NOT APPLIED YET.
//
// _iocs_set232c takes a single packed mode word, and the packing could not be
// confirmed. What IS confirmed, from XEiJ's RS232CTerminal.java (which reads
// the live mode word out of the RSDRV.SYS work area):
//     bit 7  RTS/CTS flow control
//     bit 9  XON/XOFF flow control
// and its baud-rate table is
//     0=75 1=150 2=300 3=600 4=1200 5=2400 6=4800 7=9600 8=19200 9=31250
//     10=38400 11=50000 12=57600 13=76800 14=115200 15=230400
// i.e. the rate is a FOUR-bit field, so it cannot sit at bits 0-2 with parity
// at bit 3. The positions of the data/stop/parity fields remain unknown.
//
// Writing a guessed mode word is worse than writing none: a wrong framing
// setting still transmits, it just corrupts every byte, which is exactly the
// failure this port hit first time out (every received byte came back as 0x00
// or 0x80 -- only the MSB surviving).
//
// So jlpSerialOpen inherits whatever RSDRV.SYS configured at boot. The Human68k
// "RS-232C DRIVER for X68000 version 2.02" banner appears during startup and
// leaves the port at its default, which pairs with MAME's null_modem default
// of 9600 8N1. Honour jlSerialConfigT properly once the layout is confirmed
// against hardware documentation.

// Bounded transmitter-ready spin, so a stalled link (no CTS) reports a partial
// write instead of hanging the frame. Same guard the ST HAL uses.
#define X68K_TX_SPIN_LIMIT      200000ul

// ----- Soft receive ring -----------------------------------------------------
//
// RSDRV.SYS's own receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a
// 203-byte frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the
// signature of a 64-entry ring filling while the client was busy and discarding until drained.  Every
// other JoeyLib target rides a big interrupt-fed ring (DOS hardware IRQ ring, ST XBIOS IOREC, and the
// 8-bit clients' IRQ rings), so only this port could lose mid-frame bytes whenever the caller went
// deaf for longer than ~66 ms at 9600 baud -- and jlpPresent's full-stage blit, jlpWaitVBL's V-DISP
// spin, and jlpInputPoll's IOCS trap storm all can.  The fix: drain the IOCS ring into this soft ring
// from INSIDE those long-running paths (x68kSerialPump below), so the 64-byte hardware-side window
// only ever has to cover the gap between pumps, never a whole present.  No ISR is involved -- the
// pump is a plain call -- so none of the vdispst-style vector hazards apply.
//
// SIZED FOR THE FASTEST LINE, not just the 9600 default: this port inherits RSDRV.SYS's boot rate
// (the mode word is deliberately not written), and RSDRV's table reaches 230400, where the 64-byte
// IOCS ring covers only ~2.8 ms -- so every pump site keeps its interval near 1 ms (present: every
// 2 blitted rows; input poll: every bitsns group; VBL wait: every 64 spins).  4 KB here holds ~178 ms
// of full-rate 230400 traffic, longer than the worst deaf window (a >100 ms all-dirty present), so
// even an unpaced burst -- the server's flowWait timeout degrade streams a whole screen without acks
// -- fits while the loop is busy.  It is plain RAM on a 4 MB machine; do not shrink it to save bytes.
#define X68K_RX_RING_CAP        4096u

// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer
// (writes gX68kRxTail), the mainline is the consumer (writes gX68kRxHead).  The byte is stored
// before the tail is published and the 68000 is strictly in-order, so volatile indices are the
// whole interlock.  When the ISR could not be installed, x68kSerialPump below produces instead.
uint8_t           gX68kRxRing[X68K_RX_RING_CAP];
volatile uint16_t gX68kRxHead = 0u;   // pop side
volatile uint16_t gX68kRxTail = 0u;   // push side

// ----- SCC channel A RX interrupt takeover -----------------------------------
//
// The proper fix for the tiny RSDRV ring: own the receive interrupt, the way every other JoeyLib
// target does (DOS UART IRQ ring, the 8-bit ACIA rings).  serial.c reads the SCC's programmed
// vector base (RR2 on channel A returns it UNmodified) and steals the two channel A receive vectors
// -- base|0x0C "character available" and base|0x0E "special condition" (Z8530 status-affects-vector
// puts the source in bits 3-1) -- pointing them at the raw handlers in x68kSccRxIsr.s.  RSDRV's own
// RX handler simply never runs again; its 64-byte ring starves and the polled pump becomes a no-op.
// If the base looks wrong (VIS off, unexpected layout) nothing is stolen and the polled pump keeps
// carrying the port exactly as before -- the fallback is automatic.
#define X68K_SCC_A_CTRL  ((volatile uint8_t *)0xE98005L)
#define X68K_VECTOR(v)   ((volatile uint32_t *)((uint32_t)(v) << 2))

extern void x68kSccRxIsr(void);
extern void x68kSccSpIsr(void);

static uint16_t x68kIrqMask(void);
static void     x68kIrqRestore(uint16_t sr);
static void     sccIsrInstall(void);
static void     sccIsrUninstall(void);

static bool     gSccIsrInstalled = false;
static uint8_t  gSccRxVec        = 0u;
static uint32_t gSccOldRx        = 0ul;
static uint32_t gSccOldSp        = 0ul;


static uint16_t x68kIrqMask(void) {
    uint16_t sr;

    __asm__ volatile("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "cc");
    return sr;
}


static void x68kIrqRestore(uint16_t sr) {
    __asm__ volatile("move.w %0,%%sr" : : "d"(sr) : "cc");
}


static void sccIsrInstall(void) {
    uint16_t sr;
    uint8_t  base;

    if (gSccIsrInstalled) {
        return;
    }
    // RR2 read is a two-access pointer sequence on the shared SCC, so no interrupt (the channel B
    // mouse) may interleave.  We run supervisor (jlpInit's _dos_super), so the SR mask is legal.
    sr = x68kIrqMask();
    *X68K_SCC_A_CTRL = 2u;
    base = *X68K_SCC_A_CTRL;
    x68kIrqRestore(sr);
    // With status-affects-vector the base must have bits 3-1 clear; anything else means the layout
    // is not what we expect, and the polled pump stays in charge.
    if (base == 0u || (base & 0x0Eu) != 0u) {
        return;
    }
    gSccRxVec = (uint8_t)(base | 0x0Cu);
    sr = x68kIrqMask();
    gSccOldRx = *X68K_VECTOR(gSccRxVec);
    gSccOldSp = *X68K_VECTOR(gSccRxVec + 2u);
    *X68K_VECTOR(gSccRxVec)      = (uint32_t)x68kSccRxIsr;
    *X68K_VECTOR(gSccRxVec + 2u) = (uint32_t)x68kSccSpIsr;
    x68kIrqRestore(sr);
    gSccIsrInstalled = true;
}


static void sccIsrUninstall(void) {
    uint16_t sr;

    if (!gSccIsrInstalled) {
        return;
    }
    sr = x68kIrqMask();
    *X68K_VECTOR(gSccRxVec)      = gSccOldRx;
    *X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp;
    x68kIrqRestore(sr);
    gSccIsrInstalled = false;
}


// Drain everything the IOCS ring holds into the soft ring.  Called from the HAL's own long-running
// paths (present / VBL wait / input poll) and from every serial entry point.  With the SCC ISR
// installed this is a cheap no-op guard (RSDRV's ring never fills); without it, it is the polled
// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead
// of RSDRV's ~64 bytes.
void x68kSerialPump(void) {
    if (gSccIsrInstalled) {
        return;
    }
    while (_iocs_isns232c() != 0) {
        uint16_t next = (uint16_t)((gX68kRxTail + 1u) % X68K_RX_RING_CAP);

        if (next == gX68kRxHead) {
            break;   // soft ring full: leave the rest in the IOCS ring rather than drop it here
        }
        gX68kRxRing[gX68kRxTail] = (uint8_t)(_iocs_inp232c() & 0xFF);
        gX68kRxTail              = next;
    }
}


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

uint16_t jlpSerialAvailable(void) {
    x68kSerialPump();
    return (gX68kRxHead != gX68kRxTail) ? 1u : 0u;
}


void jlpSerialClose(void) {
    sccIsrUninstall();   // hand channel A RX back to RSDRV before the vectors dangle
}


void jlpSerialFlush(void) {
    while (_iocs_isns232c() != 0) {
        (void)_iocs_inp232c();
    }
    gX68kRxHead = gX68kRxTail;   // the ISR may be live: only the consumer side may move
}


bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
    (void)device;   // one RS-232C line; channel B is the mouse.
    if (config == NULL) {
        return false;
    }
    // Line config is accepted but NOT applied -- see the mode-word note above. The
    // port keeps whatever RSDRV.SYS set at boot.  (An RTS/CTS enable via an
    // _iocs_set232c(-1) read-modify-write was tried and REVERTED: the -1-as-query
    // idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the
    // RSDRV work area instead -- so the write could not be trusted.)
    (void)config;
    sccIsrInstall();   // own the RX interrupt; falls back to the polled pump if the vectors look wrong
    return true;
}


void jlpSerialPoll(void) {
    x68kSerialPump();
}


uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
    uint16_t n;

    x68kSerialPump();
    n = 0u;
    while (n < max && gX68kRxHead != gX68kRxTail) {
        buf[n]      = gX68kRxRing[gX68kRxHead];
        gX68kRxHead = (uint16_t)((gX68kRxHead + 1u) % X68K_RX_RING_CAP);
        n++;
    }
    return n;
}


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

    for (n = 0u; n < len; n++) {
        guard = 0ul;
        while (_iocs_osns232c() == 0) {
            guard++;
            if (guard > X68K_TX_SPIN_LIMIT) {
                return n;
            }
        }
        _iocs_out232c((int)buf[n]);
    }
    return n;
}
