// Apple IIgs serial HAL -- direct, polled register access.
//
// Three endpoints behind one API, selected at open:
//   JL_SERIAL_MODEM   -- modem port   = Zilog 8530 SCC channel B
//   JL_SERIAL_PRINTER -- printer port = Zilog 8530 SCC channel A
//   JL_SERIAL_SLOT    -- 6551 ACIA on a Super Serial Card in cfg.unit's slot
//   JL_SERIAL_DEFAULT -- the modem port (SCC channel B)
//
// Channel-to-port assignment: MEASURED against MAME 0.264's apple2gs (its
// machine config wires out_txda -> "printer" and out_txdb -> "modem"); with
// the old modem=A mapping every TX byte left the unconnected printer port and
// the modem-port peer saw silence.  If real silicon turns out to disagree,
// pick the other port at open (MODEM <-> PRINTER) rather than editing this
// table.
//
// Direct register pokes through 24-bit long pointers to $00C0xx, exactly like
// input.c / the video + DOC pokes -- no GS/OS driver, no ROM firmware, lowest
// latency, and it does not fight the GS/OS heartbeat (JoeyLib's stated policy
// is to avoid ISRs that do). RX is drained on jlSerialPoll() into a bank-0
// ring; TX is a bounded poll on the transmit-empty status bit. The app owns
// the port exclusively between jlSerialOpen and jlSerialClose.
//
// SCC access uses the register-pointer protocol: to reach WRn/RRn, first write
// n to the channel's command register, then write (WRn) or read (RRn) it; RR0
// (status) and WR0 need no pointer step. The two writes must stay adjacent
// (the pointer is not re-entrant), which they do here -- no serial ISR runs.
//
// CAVEAT: the SSC 6551 window offset (+8 within the slot's 16-byte I/O space,
// the classic Apple Super Serial Card decode) and the slot's Control-Panel
// "Your Card" setting should be confirmed against the target hardware/emulator
// before relying on the SSC path.

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


// ----- SCC (modem/printer) registers -- 24-bit long pointers -----

#define SCC_B_CTRL   ((volatile uint8_t *)0x00C038L)   // modem port   command/control (ch B)
#define SCC_A_CTRL   ((volatile uint8_t *)0x00C039L)   // printer port command/control (ch A)
#define SCC_B_DATA   ((volatile uint8_t *)0x00C03AL)   // modem port   data (ch B)
#define SCC_A_DATA   ((volatile uint8_t *)0x00C03BL)   // printer port data (ch A)

// WR9 bits 7-6 are the reset command: 00 none, 01 channel B, 10 channel A,
// 11 FORCE HARDWARE RESET.  Only ever issue the one for the channel being
// opened: 0xC0 also wipes the OTHER channel's registers, FIFOs, baud generator
// and DTR/RTS, and on a IIgs that is the port AppleTalk runs on.
#define SCC_WR9_RESET_A    0x80u
#define SCC_WR9_RESET_B    0x40u

// RR0 (status) bits, read directly from the command register.
#define SCC_RR0_RX_AVAIL   0x01
#define SCC_RR0_TX_EMPTY   0x04

// RR1 bit 5: RX overrun - the 3-byte hardware FIFO was full when another byte finished, and that byte
// is GONE.  Latched until an Error Reset (WR0 command 0x30), so it survives to the next pump pass.
#define SCC_RR1_RX_OVRN    0x20
#define SCC_WR0_ERR_RESET  0x30

// 6551 status bits.
#define ACIA_ST_RX_FULL    0x08
#define ACIA_ST_TX_EMPTY   0x10
#define ACIA_ST_RX_OVRN    0x04

// SCC baud time constant: BRG output = PCLK(3.6864MHz) / (2*(TC+2)) with the
// /16 clock mode, so baud = 115200 / (TC+2) -> TC = 115200/baud - 2.
#define SCC_TC_NUM   115200L

// SSC 6551 lives at the high half of slot s's I/O window: $C080 + s*16 + 8.
#define SSC_BASE(_slot)   ((volatile uint8_t *)(0x00C088L + (long)(_slot) * 16L))

// Bank-0 RX ring (power of two).
#define RX_RING_SIZE   512u
#define RX_RING_MASK   (RX_RING_SIZE - 1u)


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

static uint8_t          gRxRing[RX_RING_SIZE];

// Flow counters for the RetroNet RXSTAT diagnostic (read via extern): bytes moved SCC->ring and
// ring->caller since open.  Cheap enough to keep unconditionally.
uint16_t gIigsRxPumped = 0u;
uint16_t gIigsRxRead   = 0u;

// Receive-loss counters, same read-via-extern contract.  Overruns = loss EVENTS the hardware latched
// (SCC RR1 overrun / 6551 status bit 2) - one latch observation may stand for SEVERAL destroyed bytes,
// so this is a loss indicator, not a byte count.  Before it existed a pump-discipline gap was invisible
// except as a frame CRC drop and a retransmit stall.  Dropped = bytes the SOFT ring refused because it
// was full (read from the chip, then discarded); saturating, like the overrun count.
uint16_t gIigsRxOverruns = 0u;
uint16_t gIigsRxDropped  = 0u;
static uint16_t         gRxHead;
static uint16_t         gRxTail;
static bool             gOpen;
static bool             gIsScc;             // true: 8530 SCC; false: 6551 ACIA
static volatile uint8_t *gStatus;           // SCC command reg (RR0) / 6551 status reg
static volatile uint8_t *gData;             // data register (both)


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

extern uint8_t jlpIoRead8(const volatile uint8_t *p);
static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg);
static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg);
static bool serialTxReady(void);
static uint8_t sccRead(volatile uint8_t *ctrl, uint8_t reg);
static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val);


// ----- Guaranteed 8-bit I/O read -----
//
// llvm816 widens a volatile uint8_t LOAD through a variable long pointer to a 16-bit `lda [dp],y`
// (m=0), so every such register read also performs a phantom bus read of address+1 - and the SCC's
// four registers are adjacent, so a data read on one channel phantom-pops the OTHER channel's data
// FIFO ($C03A reads $C03B: channel A, the AppleTalk port).  Constant-address loads are sep-wrapped
// correctly; only the pointer form is affected, and STORES are always sep-wrapped.  Filed in
// llvm816/LLVM816-ASKS.md; until the toolchain honours volatile width, every serial register READ
// goes through this hand-asm helper - a global function so no call site can be inlined back into
// the widened form.  ABI from a compiled probe: pointer lo16 in A, bank in X; return in A (high
// byte cleared), X zeroed; $e0-$e3 are call-clobbered imaginary-register scratch, D points at the
// imaginary page on entry (the same assumption every compiled function makes).
__asm__(
    ".section .text.jlpIoRead8,\"ax\",@progbits\n"
    ".globl jlpIoRead8\n"
    "jlpIoRead8:\n"
    "\trep #$30\n"                  // known width state: 16-bit A and index
    "\tsta $e0\n"                   // pointer low 16 -> $e0-$e1
    "\ttxa\n"
    "\tsta $e2\n"                   // bank -> $e2 ($e3 catches the junk high byte - scratch)
    "\tldy #0\n"
    "\tsep #$20\n"                  // 8-bit accumulator: the actual fix
    "\tlda [$e0],y\n"               // ONE byte on the bus - no phantom companion read
    "\trep #$30\n"
    "\tand #$ff\n"                  // the hidden B accumulator held junk; clear it for the return
    "\tldx #0\n"
    "\trtl\n"
);


// ----- SCC helpers -----

// Register-pointer protocol: point at WRn (if not WR0), then write the value.
static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val) {
    if (reg != 0u) {
        *ctrl = reg;
    }
    *ctrl = val;
}


// Point at RRn, then read it.  Same adjacency rule as sccWrite: nothing may touch the SCC between the
// pointer write and the read (safe here - WR1=0 means this channel never interrupts, and the HAL is
// single-threaded polled).
static uint8_t sccRead(volatile uint8_t *ctrl, uint8_t reg) {
    if (reg != 0u) {
        *ctrl = reg;
    }
    return jlpIoRead8(ctrl);
}


static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg) {
    uint8_t  wr4;
    uint8_t  wr3;
    uint8_t  wr5;
    int32_t  tc;

    (void)data;

    // WR4: /16 clock, stop bits, parity.
    wr4 = 0x40u;                                   // /16 clock rate
    wr4 |= (cfg->stopBits == 2u) ? 0x0Cu : 0x04u;  // 2 or 1 stop bit
    if (cfg->parity == JL_SERIAL_PARITY_ODD) {
        wr4 |= 0x01u;                              // parity enable, odd
    } else if (cfg->parity == JL_SERIAL_PARITY_EVEN) {
        wr4 |= 0x03u;                              // parity enable, even
    }

    // Rx / Tx bits-per-char.
    switch (cfg->dataBits) {
        case 5u:  wr3 = 0x00u; wr5 = 0x00u; break;
        case 6u:  wr3 = 0x80u; wr5 = 0x40u; break;
        case 7u:  wr3 = 0x40u; wr5 = 0x20u; break;
        default:  wr3 = 0xC0u; wr5 = 0x60u; break;   // 8
    }

    // baud = PCLK(3.6864MHz) / (2 * (TC+2) * 16) with the WR4 /16 clock mode -- the real-hardware
    // formula, which MAME's z80scc implements faithfully.  Stock MAME 0.264 apple2gs feeds the SCC
    // the WRONG clock (14.318MHz/2 instead of the 3.6864MHz PCLK) and pins RX at rxc/16 = 230400 via
    // configure_channels; that is fixed in the DRIVER (patches/mame-0.264-apple2gs-scc-clock.patch),
    // not here.
    tc = SCC_TC_NUM / (int32_t)cfg->baud - 2;
    if (tc < 0) {
        tc = 0;
    }

    // Reset THIS CHANNEL ONLY, first: on real silicon the firmware has already reset the chip at boot,
    // but MAME's z80scc powers up unreset and its TX path stays dead without it (measured: every
    // register write landed - DTR visibly rose at the far end - yet no data byte ever left the chip).
    // This used to be WR9 = 0xC0, a force-hardware-reset that took the OTHER channel down with it.
    //
    // Not fully solved, and it cannot be from here: WR9 is a single chip-wide register AND is
    // write-only, so the reset command unavoidably rewrites the chip's interrupt-control bits
    // (MIE/VIS/NV) with our zeroes, and there is no way to read the previous value back to preserve
    // them.  A driver on the other channel that takes interrupts -- AppleTalk -- is therefore still
    // disturbed, just no longer wiped.  Sharing the SCC properly would mean going through GS/OS's
    // serial drivers, which this HAL deliberately bypasses (see the file header).
    sccWrite(ctrl, 9u, reset);
    sccWrite(ctrl, 1u, 0x00u);                      // no interrupts from THIS channel -- WR1 is per-channel, WR9 is not
    sccWrite(ctrl, 10u, 0x00u);                     // NRZ, no loop/mark-idle: a CHANNEL reset preserves the
                                                    // encoding bits the old chip-wide reset cleared, so a
                                                    // channel left in FM0 by LocalTalk would stay there.
    sccWrite(ctrl, 4u, wr4);
    sccWrite(ctrl, 3u, wr3);                        // Rx bits, not yet enabled
    sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x82u));     // Tx bits + DTR + RTS, not yet enabled
    sccWrite(ctrl, 11u, 0x50u);                     // Rx & Tx clock = baud rate generator
    sccWrite(ctrl, 12u, (uint8_t)(tc & 0xFFu));     // time constant low
    sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
    sccWrite(ctrl, 14u, 0x03u);                     // BRG enable, source = PCLK
    sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u));     // Rx enable
    sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x8Au));     // Tx enable + DTR + RTS
}


// ----- 6551 (Super Serial Card) helper -----

static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) {
    uint8_t  control;
    uint8_t  command;
    uint8_t  baudCode;

    // 6551 internal baud rate codes (bits 3-0 of the control register).
    switch (cfg->baud) {
        case 50u:    baudCode = 0x01u; break;
        case 75u:    baudCode = 0x02u; break;
        case 110u:   baudCode = 0x03u; break;
        case 135u:   baudCode = 0x04u; break;
        case 150u:   baudCode = 0x05u; break;
        case 300u:   baudCode = 0x06u; break;
        case 600u:   baudCode = 0x07u; break;
        case 1200u:  baudCode = 0x08u; break;
        case 1800u:  baudCode = 0x09u; break;
        case 2400u:  baudCode = 0x0Au; break;
        case 3600u:  baudCode = 0x0Bu; break;
        case 4800u:  baudCode = 0x0Cu; break;
        case 7200u:  baudCode = 0x0Du; break;
        case 9600u:  baudCode = 0x0Eu; break;
        case 19200u: baudCode = 0x0Fu; break;
        default:     baudCode = 0x0Eu; break;   // 9600
    }

    // Control: baud | rx-clock=BRG (bit4) | word length (bits 6-5) | 2-stop (bit7).
    control  = (uint8_t)(baudCode | 0x10u);
    control |= (uint8_t)((8u - cfg->dataBits) << 5);   // 8->00,7->01,6->10,5->11
    if (cfg->stopBits == 2u) {
        control |= 0x80u;
    }

    // Command: DTR on (bit0) | rx IRQ disabled (bit1, polled) | RTS low, TX IRQ
    // off (bits 3-2 = 10) | parity.
    command = 0x0Bu;
    if (cfg->parity == JL_SERIAL_PARITY_ODD) {
        command |= 0x20u;                              // parity enable, odd
    } else if (cfg->parity == JL_SERIAL_PARITY_EVEN) {
        command |= 0x60u;                              // parity enable, even
    }

    base[1] = 0x00u;        // write to status register = software reset
    base[3] = control;      // control register
    base[2] = command;      // command register
}


// ----- Status polling -----

static bool serialTxReady(void) {
    if (gIsScc) {
        return (jlpIoRead8(gStatus) & SCC_RR0_TX_EMPTY) != 0u;
    }
    return (jlpIoRead8(gStatus) & ACIA_ST_TX_EMPTY) != 0u;
}


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

uint16_t jlpSerialAvailable(void) {
    return (uint16_t)((gRxTail - gRxHead) & RX_RING_MASK);
}


void jlpSerialClose(void) {
    gOpen = false;
    // Leave the chip configured; a polled port needs no teardown, and the app
    // owns it until re-opened.
}


void jlpSerialFlush(void) {
    gRxHead = gRxTail;
}


bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
    if (gOpen) {
        return false;
    }
    gRxHead = 0u;
    gRxTail = 0u;

    switch (device) {
        case JL_SERIAL_PRINTER:
            gIsScc  = true;
            gStatus = SCC_A_CTRL;
            gData   = SCC_A_DATA;
            serialInitScc(SCC_A_CTRL, SCC_A_DATA, SCC_WR9_RESET_A, config);
            break;
        case JL_SERIAL_SLOT: {
            volatile uint8_t *base;

            if (config->unit < 1u || config->unit > 7u) {
                return false;
            }
            base    = SSC_BASE(config->unit);
            gIsScc  = false;
            gStatus = &base[1];
            gData   = &base[0];
            serialInitAcia(base, config);
            break;
        }
        case JL_SERIAL_MODEM:
        case JL_SERIAL_DEFAULT:
        default:
            gIsScc  = true;
            gStatus = SCC_B_CTRL;
            gData   = SCC_B_DATA;
            serialInitScc(SCC_B_CTRL, SCC_B_DATA, SCC_WR9_RESET_B, config);
            break;
    }

    gOpen = true;
    return true;
}


// Drain the SCC hardware FIFO into the soft ring from ANY wait/blit/input site (the X68000 lesson:
// a polled port with a tiny hardware FIFO loses mid-frame bytes during every deaf window - the Z8530
// RX FIFO is 3 bytes, ~3ms at 9600, and the client's VBL-paced loop is deaf for ~16ms per pass).
// Safe to call at any time; no-ops when the port is closed.
void iigsSerialPump(void) {
    jlpSerialPoll();
}


void jlpSerialPoll(void) {
    uint16_t  tail;
    uint16_t  guard;

    if (!gOpen) {
        return;
    }
    // Drain the hardware into the ring under a bound (like input.c's FIFO
    // drain). Local read-modify-write of the tail index dodges the `inc abs`
    // DBR trap that file-scope `++` lowers to on the 65816.
    tail  = gRxTail;
    guard = RX_RING_SIZE;
    // Counter updates below use the guarded local read-modify-write from input.c's joystick counter -
    // NOT because of the spelling (clang canonicalizes `x++` and `x = x + 1` identically) but because
    // the saturation compare keeps the loaded value LIVE, which is what stops the backend fusing the
    // update into a DBR-relative `inc abs`.  Saturating is also the right behaviour for a loss counter.
    while (guard != 0u) {
        uint16_t  next;
        uint8_t   st;
        uint8_t   b;

        st = jlpIoRead8(gStatus);                   // RR0 (SCC) or 6551 status
        if (gIsScc) {
            if ((st & SCC_RR0_RX_AVAIL) == 0u) {
                break;
            }
        } else {
            // The 6551 latches overrun ONLY while RDRF is still set, and the DATA read below retires
            // both bits together - so the overrun must be sampled HERE, from the same status read that
            // gates the byte.  (A post-loop check can never see it: the drain already cleared it.)
            if ((st & ACIA_ST_RX_OVRN) != 0u) {
                uint16_t ovr = gIigsRxOverruns;

                if (ovr < 0xFFFFu) {
                    gIigsRxOverruns = (uint16_t)(ovr + 1u);
                }
            }
            if ((st & ACIA_ST_RX_FULL) == 0u) {
                break;
            }
        }
        next = (uint16_t)((tail + 1u) & RX_RING_MASK);
        b    = jlpIoRead8(gData);
        if (next != gRxHead) {
            gRxRing[tail] = b;
            tail = next;
            gIigsRxPumped++;
        } else {
            uint16_t drop = gIigsRxDropped;

            if (drop < 0xFFFFu) {
                gIigsRxDropped = (uint16_t)(drop + 1u);
            }
        }
        guard--;
    }
    gRxTail = tail;
    // Harvest the SCC's own loss record: real silicon latches RX overrun in RR1 until an Error Reset,
    // so one check per pump names a deaf-window loss even though the pump never saw the byte.
    //
    // DEFAULT OFF - REAL HARDWARE ONLY, on two measured grounds: (1) under MAME the counter is INERT
    // (its z80scc never surfaces the flag into RR1), so enabling it under emulation buys nothing; and
    // (2) with it enabled the MAME 0.264 session goes RX-deaf after HELLO - 3 of 4 runs, INCLUDING one
    // through jlpIoRead8's true 8-bit reads, so the phantom companion read is NOT the mechanism (one
    // green run said otherwise and the next red run retracted it - the two-greens-prove-nothing rule).
    // Whatever the per-pass WR0-pointer/RR1 dance disturbs in MAME's SCC remains unpinned; it is not
    // worth more rig time for a diagnostic that cannot fire there.  JL_IIGS_RR1_HARVEST enables it
    // for silicon validation.
#ifdef JL_IIGS_RR1_HARVEST
    if (gIsScc) {
        if ((sccRead(gStatus, 1u) & SCC_RR1_RX_OVRN) != 0u) {
            uint16_t ovr = gIigsRxOverruns;

            if (ovr < 0xFFFFu) {
                gIigsRxOverruns = (uint16_t)(ovr + 1u);
            }
            sccWrite(gStatus, 0u, SCC_WR0_ERR_RESET);
        }
    }
#endif
}


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

    head = gRxHead;
    for (n = 0u; n < max && head != gRxTail; n++) {
        buf[n] = gRxRing[head];
        head = (uint16_t)((head + 1u) & RX_RING_MASK);
    }
    gRxHead = head;
    gIigsRxRead = (uint16_t)(gIigsRxRead + n);
    return n;
}


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

    for (n = 0u; n < len; n++) {
        uint16_t  guard = 0u;

        while (!serialTxReady()) {
            // Full-duplex: drain the RX FIFO while waiting for TX-ready.  Each TX byte at 9600 is a
            // ~1ms wait and the Z8530 RX FIFO is 3 bytes - an unpumped uplink mid-downlink-burst
            // measurably ate 4-byte clumps out of arriving frames.
            jlpSerialPoll();
            guard++;
            if (guard == 0u) {   // wrapped 65536 spins -- link stalled
                return n;
            }
        }
        *gData = buf[n];
    }
    jlpSerialPoll();
    return n;
}
