// Atari ST serial HAL -- the RS-232 Modem port via TOS.
//
// The base ST's one RS-232 line is the Modem port, driven by the 68901 MFP
// USART and wrapped by TOS as BIOS logical device 1 (AUX) with a receive-
// buffered IOREC. This HAL rides that: XBIOS Rsconf sets baud/flow/format,
// Bconin/Bconout move bytes, Bconstat/Bcostat gate them. All are GEMDOS/BIOS/
// XBIOS traps that switch to supervisor internally, so -- unlike the direct
// $FFFFFAxx pokes elsewhere in the ST HAL -- serial needs no supervisor
// handling and no new headers. RX is buffered by TOS, so jlpSerialPoll is a
// no-op. (Caveat: TOS's default RS-232 IOREC is small ~256 bytes and Bconin
// blocks, so callers gate on Available; sustained high-baud input can overrun
// -- a future revision could grow the IOREC or install an MFP RX ISR.)

#include <mint/osbind.h>

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


// BIOS logical device 1 = RS-232 / Modem port.
#define ST_AUX_DEV       1

// 68901 MFP USART control register (UCR) bit fields, built by serialUcr:
//   bit 7   clock divide (1 = /16, standard async)
//   bits6-5 word length  (00=8, 01=7, 10=6, 11=5)  -> (8 - dataBits)
//   bits4-3 stop bits    (01 = 1 stop, 11 = 2 stop)
//   bit 2   parity enable
//   bit 1   parity even (1) / odd (0)
#define ST_UCR_CLK_DIV16 0x80

// Rsconf sentinels. -1 in any slot means "leave that field alone" (TOS writes a
// field only when the value it is handed is in range). -2 in the SPEED slot is
// the separate, documented baud inquire (mint/ostruct.h BAUD_INQUIRE): EmuTOS's
// rsconf_mfp returns iorec->baudrate and returns BEFORE touching any register,
// so it is side-effect free. Do NOT use -1 to inquire; -1 is "no change".
#define ST_RSCONF_NO_CHANGE  ((int16_t)-1)
#define ST_RSCONF_INQUIRE    ((int16_t)-2)
#define ST_BAUD_CODE_MAX     15


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

// What jlpSerialOpen took away, so jlpSerialClose can hand it back. Both sit at
// ST_RSCONF_NO_CHANGE when there is nothing to restore, so a close with no
// matching open -- or a second close -- lowers to an Rsconf that writes nothing.
static int16_t gSavedBaudCode = ST_RSCONF_NO_CHANGE;
static int16_t gSavedUcr      = ST_RSCONF_NO_CHANGE;


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

static int16_t serialBaudCode(uint32_t baud);
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr);
static int16_t serialUcr(const jlSerialConfigT *cfg);


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

// Rsconf speed codes (XBIOS): 0 is fastest, 15 slowest. Unknown -> 9600.
static int16_t serialBaudCode(uint32_t baud) {
    switch (baud) {
        case 19200u: return 0;
        case 9600u:  return 1;
        case 4800u:  return 2;
        case 3600u:  return 3;
        case 2400u:  return 4;
        case 2000u:  return 5;
        case 1800u:  return 6;
        case 1200u:  return 7;
        case 600u:   return 8;
        case 300u:   return 9;
        case 200u:   return 10;
        case 150u:   return 11;
        case 134u:   return 12;
        case 110u:   return 13;
        case 75u:    return 14;
        case 50u:    return 15;
        default:     return 1;
    }
}


// XBIOS 15 (Rsconf) done by hand, because mint's Rsconf() macro CANNOT be
// trusted with computed arguments.
//
// osbind.h's own change log admits it: the seven-word trap macro relaxed its
// operand constraints from "r" to "g" on the grounds that "these args will
// never be expressions". With "g", gcc is free to leave an argument in a stack
// temp -- and the macro then pushes seven words with movew ...,sp@-, moving sp
// out from under that temp, so the push reads the wrong offset. The disassembly
// showed exactly that: the baud code was parked at sp@(14) and pushed after sp
// had already moved. MEASURED under EmuTOS/Hatari: every requested rate (19200,
// 9600, 4800, 2400, 1200, 300) programmed 9600, and shuffling the C around only
// changed WHICH wrong code came out. The UCR happened to survive in a register.
//
// Pushing the words here, with the three variable ones bound to registers,
// removes the sp-relative temp entirely. rsr/tsr/scr are immediates: no caller
// wants them, and leaving them "no change" means there is nothing to undo.
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr) {
    // Bound to d0 the way osbind.h's own trap_14_* helpers bind theirs: XBIOS
    // returns in d0, and an unbound output operand would leave gcc free to pick
    // another register that the asm never writes (which silently returned
    // garbage here until the probe caught it).
    register int32_t ret __asm__("d0");

    __asm__ volatile (
        "movew #-1,%%sp@-\n\t"      /* scr: no change */
        "movew #-1,%%sp@-\n\t"      /* tsr: no change */
        "movew #-1,%%sp@-\n\t"      /* rsr: no change */
        "movew %3,%%sp@-\n\t"       /* ucr  */
        "movew %2,%%sp@-\n\t"       /* flow */
        "movew %1,%%sp@-\n\t"       /* baud */
        "movew #15,%%sp@-\n\t"      /* XBIOS 15 = Rsconf */
        "trap  #14\n\t"
        "lea   %%sp@(14),%%sp"
        : "=d" (ret)
        : "d" (baud), "d" (flow), "d" (ucr)
        : "d1", "d2", "a0", "a1", "cc", "memory");
    return ret;
}


static int16_t serialUcr(const jlSerialConfigT *cfg) {
    int16_t  ucr;

    ucr  = ST_UCR_CLK_DIV16;
    ucr |= (int16_t)((8u - cfg->dataBits) << 5);
    ucr |= (int16_t)((cfg->stopBits == 2u ? 3u : 1u) << 3);
    if (cfg->parity != JL_SERIAL_PARITY_NONE) {
        ucr |= (int16_t)(1u << 2);
        if (cfg->parity == JL_SERIAL_PARITY_EVEN) {
            ucr |= (int16_t)(1u << 1);
        }
    }
    return ucr;
}


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

uint16_t jlpSerialAvailable(void) {
    // Bconstat reports presence (-1/0), not a count: 1 == at least one byte.
    return Bconstat(ST_AUX_DEV) ? 1u : 0u;
}


// The trap path hijacks no vector, but jlpSerialOpen did change the line
// settings, and those live in the MFP and TOS's iorec -- both of which outlive
// the program. So hand back the two fields TOS lets us recover: the baud code
// (from the -2 inquire) and the UCR (the high byte of what Rsconf returned when
// it installed ours). rsr/tsr/scr were never written -- open passes -1 for them
// -- so they need no undo, and scr is not in the return value anyway.
//
// Flow control is deliberately NOT restored: Rsconf has no inquire for it (TOS
// keeps it in iorec->flowctrl with no way back out), so the only options are
// leaving it or guessing, and guessing a value onto a working port is the
// mistake the X68000 _SET232C note in that port's serial.c warns about. A user
// who had hardware handshaking on keeps the app's setting until something sets
// it again; that is a documented limitation, not a silent corruption.
void jlpSerialClose(void) {
    (void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr);
    gSavedBaudCode = ST_RSCONF_NO_CHANGE;
    gSavedUcr      = ST_RSCONF_NO_CHANGE;
}


void jlpSerialFlush(void) {
    while (Bconstat(ST_AUX_DEV)) {
        (void)Bconin(ST_AUX_DEV);
    }
}


bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
    int16_t  flow;
    int16_t  baudCode;
    int16_t  ucr;
    int32_t  inquired;
    uint32_t previous;

    (void)device;   // the base ST has one RS-232 line: the Modem port.
    switch (config->flow) {
        case JL_SERIAL_FLOW_RTSCTS:  flow = 2; break;
        case JL_SERIAL_FLOW_XONXOFF: flow = 1; break;
        default:                     flow = 0; break;
    }

    // EVALUATE THE HELPERS INTO LOCALS FIRST -- never inline a call into the
    // Rsconf() argument list. mint/osbind.h's own change log says so: the
    // seven-word trap macro relaxed its operand constraints from "r" to "g"
    // and notes that is "ok since these args will never be expressions", so
    // gcc may materialise an argument from a stack slot AFTER the macro has
    // moved sp out from under it. Passing serialBaudCode(...) inline was
    // exactly that, and it lost the baud code: MEASURED under EmuTOS/Hatari,
    // every requested rate -- 19200, 9600, 4800, 2400, 1200, 300 -- programmed
    // code 1 (9600). serialUcr's result survived; the baud one did not.
    baudCode = serialBaudCode(config->baud);
    ucr      = serialUcr(config);

    // Snapshot the baud code BEFORE installing ours -- the inquire reports the
    // rate currently in force. Range-gate the answer: a TOS that did not
    // implement the -2 inquire would fall through to the normal path, write
    // nothing (every field is out of range), and hand back the packed
    // ucr/rsr/tsr instead, which is >= 0x01000000 for any non-zero UCR.
    inquired = serialRsconf(ST_RSCONF_INQUIRE, ST_RSCONF_NO_CHANGE, ST_RSCONF_NO_CHANGE);
    if (inquired >= 0 && inquired <= ST_BAUD_CODE_MAX) {
        gSavedBaudCode = (int16_t)inquired;
    }

    // The return is the PREVIOUS (ucr << 24) | (rsr << 16) | (tsr << 8), so the
    // same call that installs our line settings also reports the UCR we displaced.
    previous  = (uint32_t)serialRsconf(baudCode, flow, ucr);
    gSavedUcr = (int16_t)((previous >> 24) & 0xFFu);
    return true;
}


void jlpSerialPoll(void) {
    // TOS buffers RX in its IOREC; nothing to pump.
}


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

    n = 0u;
    // Gate each read on Bconstat first: Bconin blocks on an empty buffer.
    while (n < max && Bconstat(ST_AUX_DEV)) {
        buf[n] = (uint8_t)Bconin(ST_AUX_DEV);
        n++;
    }
    return n;
}


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

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

        // Bounded transmitter-ready spin (Bcostat -1 = ready) so a stalled
        // link (no CTS) reports a partial write instead of hanging the frame.
        guard = 0u;
        while (!Bcostat(ST_AUX_DEV)) {
            guard++;
            if (guard > 200000ul) {
                return n;
            }
        }
        Bconout(ST_AUX_DEV, (int16_t)buf[n]);
    }
    return n;
}
