// DOS serial HAL -- 8250/16550 UART on a COM port, interrupt-driven RX ring.
//
// BIOS INT 14h is polled and unbuffered, so a byte arriving mid-frame is lost
// when the UART's holding register overruns -- unusable for a game loop. This
// HAL instead drives the UART directly (inportb/outportb) and installs a
// receive IRQ that pushes bytes into a background ring, exactly the shape the
// SB DMA IRQ (audio.c) and keyboard ISR (input.c) already use: the ISR does
// the minimum (drain RBR into the ring, EOI) and the main loop drains the ring
// non-blocking via jlSerialRead/Available. Transmit is a bounded poll on THRE.
//
// The RX ring is a lock-free single-producer (ISR) / single-consumer (main)
// queue: uint16 head/tail are read/written atomically on x86, so no cli/sti is
// needed around the drain. Everything the ISR touches is page-locked (a page
// fault at interrupt time hangs the machine under DJGPP paging).

#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <stdint.h>

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


// ----- UART register offsets (from the COM base I/O port) -----

#define UART_RBR   0   // read: received data          (DLAB=0)
#define UART_THR   0   // write: transmit holding       (DLAB=0)
#define UART_DLL   0   // divisor latch low             (DLAB=1)
#define UART_IER   1   // interrupt enable              (DLAB=0)
#define UART_DLM   1   // divisor latch high            (DLAB=1)
#define UART_IIR   2   // read: interrupt identification
#define UART_FCR   2   // write: FIFO control (16550+)
#define UART_LCR   3   // line control (bit 7 = DLAB)
#define UART_MCR   4   // modem control (bit 3 = OUT2 gates the IRQ!)
#define UART_LSR   5   // line status  (bit 0 = data ready, bit 5 = THRE)
#define UART_MSR   6   // modem status

#define LCR_DLAB       0x80
#define IER_RX_AVAIL   0x01
#define MCR_DTR        0x01
#define MCR_RTS        0x02
#define MCR_OUT2       0x08   // physically gates the UART IRQ to the PIC
#define LSR_DATA_READY 0x01
#define LSR_THRE       0x20
#define FCR_ENABLE_14  0xC7   // enable FIFO + clear RX/TX + 14-byte trigger (ignored by 8250)

// UART base clock 1.8432 MHz -> divisor = 115200 / baud.
#define UART_BAUD_BASE 115200uL

// PIC (mirrors src/dos/audio.c).
#define PIC1_CMD   0x20
#define PIC1_IMR   0x21
#define PIC2_CMD   0xA0
#define PIC2_IMR   0xA1
#define PIC_EOI    0x20

// Background RX ring. Power-of-two so head/tail wrap with a mask.
#define RX_RING_SIZE  2048u
#define RX_RING_MASK  (RX_RING_SIZE - 1u)

// Enough of serialRxIsr to page-lock (over-estimates, harmless).
#define ISR_LOCK_SIZE 4096u


// ----- Module state (volatile: shared with the RX ISR) -----

static volatile uint8_t  gRxRing[RX_RING_SIZE];
static volatile uint16_t gRxHead;   // consumer (main loop) advances
static volatile uint16_t gRxTail;   // producer (ISR) advances
static volatile uint16_t gComBase;
static volatile uint8_t  gComIrq;

static bool                    gOpen = false;
static bool                    gIrqHooked = false;
static _go32_dpmi_seginfo      gOldIrqVec;
static _go32_dpmi_seginfo      gNewIrqVec;


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

static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq);
static int  picVectorForIrq(uint8_t irq);
static bool serialResolvePort(jlSerialDeviceE device, const jlSerialConfigT *cfg, uint16_t *outBase, uint8_t *outIrq);
static void serialRxIsr(void);


// ----- PIC helpers (mirror src/dos/audio.c) -----

static int picVectorForIrq(uint8_t irq) {
    int  i = (int)irq;

    return (i < 8) ? (0x08 + i) : (0x70 + i - 8);
}


static void picMaskIrq(uint8_t irq) {
    uint16_t port = (irq < 8u) ? PIC1_IMR : PIC2_IMR;
    uint8_t  bit  = (uint8_t)(1u << (irq & 7u));

    outportb(port, (uint8_t)(inportb(port) | bit));
}


static void picUnmaskIrq(uint8_t irq) {
    uint16_t port = (irq < 8u) ? PIC1_IMR : PIC2_IMR;
    uint8_t  bit  = (uint8_t)(1u << (irq & 7u));

    outportb(port, (uint8_t)(inportb(port) & ~bit));
    if (irq >= 8u) {
        // Slave PIC reaches the CPU only if the cascade line (IRQ2) is open.
        outportb(PIC1_IMR, (uint8_t)(inportb(PIC1_IMR) & ~(1u << 2)));
    }
}


// ----- RX interrupt handler (page-locked, no libc) -----

static void serialRxIsr(void) {
    uint8_t  iir;

    // Drain every pending UART source so a shared IRQ line cannot wedge.
    while (((iir = inportb((int)(gComBase + UART_IIR))) & 0x01u) == 0u) {
        uint8_t  src = (uint8_t)((iir >> 1) & 0x07u);

        if (src == 0x02u || src == 0x06u) {          // RX data / RX FIFO timeout
            while (inportb((int)(gComBase + UART_LSR)) & LSR_DATA_READY) {
                uint8_t  b    = (uint8_t)inportb((int)(gComBase + UART_RBR));
                uint16_t next = (uint16_t)((gRxTail + 1u) & RX_RING_MASK);

                if (next != gRxHead) {               // drop on full rather than block
                    gRxRing[gRxTail] = b;
                    gRxTail = next;
                }
            }
        } else if (src == 0x03u) {                   // line status: read LSR to clear
            (void)inportb((int)(gComBase + UART_LSR));
        } else {                                     // modem status: read MSR to clear
            (void)inportb((int)(gComBase + UART_MSR));
        }
    }

    if (gComIrq >= 8u) {
        outportb(PIC2_CMD, PIC_EOI);
    }
    outportb(PIC1_CMD, PIC_EOI);
}


// ----- Port resolution -----

// DEFAULT/MODEM/PRINTER -> COM1; SLOT -> COM<unit> (1..4). The canonical COM
// base/IRQ map is a convention (the real map lives at BIOS 0040:0000), but it
// is correct on every emulator + standard PC; a future revision could read the
// BIOS table.
static bool serialResolvePort(jlSerialDeviceE device, const jlSerialConfigT *cfg, uint16_t *outBase, uint8_t *outIrq) {
    uint8_t  com = 1u;   // COM1 default

    if (device == JL_SERIAL_SLOT) {
        com = cfg->unit;
    }
    switch (com) {
        case 1u:  *outBase = 0x3F8u; *outIrq = 4u; return true;
        case 2u:  *outBase = 0x2F8u; *outIrq = 3u; return true;
        case 3u:  *outBase = 0x3E8u; *outIrq = 4u; return true;
        case 4u:  *outBase = 0x2E8u; *outIrq = 3u; return true;
        default:  return false;
    }
}


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

uint16_t jlpSerialAvailable(void) {
    // Lock-free SPSC: both indices are atomic uint16 reads on x86.
    return (uint16_t)((gRxTail - gRxHead) & RX_RING_MASK);
}


void jlpSerialClose(void) {
    if (!gOpen) {
        return;
    }
    if (gIrqHooked) {
        int  vector = picVectorForIrq(gComIrq);

        picMaskIrq(gComIrq);
        outportb((int)(gComBase + UART_IER), 0);   // disable UART interrupts
        outportb((int)(gComBase + UART_MCR), 0);   // drop DTR/RTS/OUT2
        _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gOldIrqVec);
        _go32_dpmi_free_iret_wrapper(&gNewIrqVec);
        gIrqHooked = false;
    }
    gOpen = false;
}


void jlpSerialFlush(void) {
    // Discard buffered input by snapping the consumer index up to the producer.
    gRxHead = gRxTail;
}


bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
    uint16_t  base;
    uint8_t   irq;
    uint16_t  divisor;
    uint8_t   lcr;
    int       vector;

    if (gOpen || !serialResolvePort(device, config, &base, &irq)) {
        return false;
    }

    divisor = (uint16_t)(UART_BAUD_BASE / config->baud);
    if (divisor == 0u) {
        divisor = 1u;
    }
    // Line control: word length (dataBits-5), stop bit, parity.
    lcr = (uint8_t)(config->dataBits - 5u);
    if (config->stopBits == 2u) {
        lcr |= 0x04u;
    }
    if (config->parity == JL_SERIAL_PARITY_ODD) {
        lcr |= 0x08u;                    // parity enable, odd
    } else if (config->parity == JL_SERIAL_PARITY_EVEN) {
        lcr |= 0x18u;                    // parity enable, even
    }

    gComBase = base;
    gComIrq  = irq;
    gRxHead  = 0u;
    gRxTail  = 0u;

    // Program the UART: baud divisor, line format, FIFO, then RX interrupt +
    // OUT2 (which physically gates the IRQ line -- the classic "IRQ never
    // fires" bug is a cleared OUT2).
    outportb((int)(base + UART_IER), 0);
    outportb((int)(base + UART_LCR), (uint8_t)(lcr | LCR_DLAB));
    outportb((int)(base + UART_DLL), (uint8_t)(divisor & 0xFFu));
    outportb((int)(base + UART_DLM), (uint8_t)((divisor >> 8) & 0xFFu));
    outportb((int)(base + UART_LCR), lcr);
    outportb((int)(base + UART_FCR), FCR_ENABLE_14);
    outportb((int)(base + UART_MCR), (uint8_t)(MCR_DTR | MCR_RTS | MCR_OUT2));

    // Hook the receive interrupt.
    vector = picVectorForIrq(irq);
    _go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
    gNewIrqVec.pm_offset   = (unsigned long)serialRxIsr;
    gNewIrqVec.pm_selector = _go32_my_cs();
    if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
        outportb((int)(base + UART_MCR), 0);
        return false;
    }
    // Page-lock the ISR + every object it touches (fault at IRQ time hangs).
    _go32_dpmi_lock_code(serialRxIsr,       ISR_LOCK_SIZE);
    _go32_dpmi_lock_data((void *)gRxRing,   sizeof(gRxRing));
    _go32_dpmi_lock_data((void *)&gRxHead,  sizeof(gRxHead));
    _go32_dpmi_lock_data((void *)&gRxTail,  sizeof(gRxTail));
    _go32_dpmi_lock_data((void *)&gComBase, sizeof(gComBase));
    _go32_dpmi_lock_data((void *)&gComIrq,  sizeof(gComIrq));

    _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
    gIrqHooked = true;

    // Clear any stale UART status, then enable the RX interrupt + unmask PIC.
    (void)inportb((int)(base + UART_LSR));
    (void)inportb((int)(base + UART_RBR));
    (void)inportb((int)(base + UART_MSR));
    (void)inportb((int)(base + UART_IIR));
    outportb((int)(base + UART_IER), IER_RX_AVAIL);
    picUnmaskIrq(irq);

    gOpen = true;
    return true;
}


void jlpSerialPoll(void) {
    // The RX IRQ fills the ring in the background; nothing to pump.
}


uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
    uint16_t  n;
    uint16_t  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++) {
        uint32_t  guard = 0u;

        // Bounded wait for the transmit holding register to empty.
        while ((inportb((int)(gComBase + UART_LSR)) & LSR_THRE) == 0) {
            guard++;
            if (guard > 200000ul) {
                return n;
            }
        }
        outportb((int)(gComBase + UART_THR), buf[n]);
    }
    return n;
}
