// 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 A
//   JL_SERIAL_PRINTER -- printer port = Zilog 8530 SCC channel B
//   JL_SERIAL_SLOT    -- 6551 ACIA on a Super Serial Card in cfg.unit's slot
//   JL_SERIAL_DEFAULT -- the modem port (SCC channel A)
//
// 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)   // printer port command/control (ch B)
#define SCC_A_CTRL   ((volatile uint8_t *)0x00C039L)   // modem port   command/control (ch A)
#define SCC_B_DATA   ((volatile uint8_t *)0x00C03AL)   // printer port data (ch B)
#define SCC_A_DATA   ((volatile uint8_t *)0x00C03BL)   // modem port   data (ch A)

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

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

// 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];
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 -----

static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg);
static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, const jlSerialConfigT *cfg);
static bool serialRxReady(void);
static bool serialTxReady(void);
static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val);


// ----- 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;
}


static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, 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
    }

    tc = SCC_TC_NUM / (int32_t)cfg->baud - 2;
    if (tc < 0) {
        tc = 0;
    }

    sccWrite(ctrl, 9u, 0x00u);                      // reset pointer / no interrupts (polled)
    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 serialRxReady(void) {
    if (gIsScc) {
        return (*gStatus & SCC_RR0_RX_AVAIL) != 0u;   // RR0 read directly
    }
    return (*gStatus & ACIA_ST_RX_FULL) != 0u;
}


static bool serialTxReady(void) {
    if (gIsScc) {
        return (*gStatus & SCC_RR0_TX_EMPTY) != 0u;
    }
    return (*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_B_CTRL;
            gData   = SCC_B_DATA;
            serialInitScc(SCC_B_CTRL, SCC_B_DATA, 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_A_CTRL;
            gData   = SCC_A_DATA;
            serialInitScc(SCC_A_CTRL, SCC_A_DATA, config);
            break;
    }

    gOpen = true;
    return true;
}


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;
    while (guard != 0u && serialRxReady()) {
        uint16_t  next = (uint16_t)((tail + 1u) & RX_RING_MASK);
        uint8_t   b    = *gData;

        if (next != gRxHead) {
            gRxRing[tail] = b;
            tail = next;
        }
        guard--;
    }
    gRxTail = tail;
}


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;
    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()) {
            guard++;
            if (guard == 0u) {   // wrapped 65536 spins -- link stalled
                return n;
            }
        }
        *gData = buf[n];
    }
    return n;
}
