// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// RS-232 Serial Port Library for DJGPP
// Ported from DOS Serial Library 1.4 by Karl Stenerud (MIT License)

#include <stdint.h>
#include <stdbool.h>
#include <pc.h>
#include <sys/farptr.h>
#include <go32.h>
#include <dpmi.h>
#include "rs232.h"


// ========================================================================
// Defines and macros
// ========================================================================

// Port I/O helpers that return the written value. outportb/outportw are void
// in DJGPP, but our macros (UART_WRITE_IER, etc.) use comma expressions to
// cache the written value in the port state struct for later reads without
// hitting the hardware again.
#define OUTP(a, b)  (outportb((a), (b)), (b))
#define OUTPW(a, w) (outportw((a), (w)), (w))

// Buffer sizes must be power of 2 so (index & MASK) wraps correctly.
// 2048 bytes balances memory usage (8KB per port for both buffers) against
// burst tolerance at 115200 bps.
#define RX_BUFFER_BITS          11 // 2048
#define TX_BUFFER_BITS          11 // 2048
#define RX_BUFFER_SIZE          (1L << RX_BUFFER_BITS)
#define TX_BUFFER_SIZE          (1L << TX_BUFFER_BITS)
#define RX_BUFFER_MASK          ((1U << RX_BUFFER_BITS) - 1)
#define TX_BUFFER_MASK          ((1U << TX_BUFFER_BITS) - 1)

// Flow control watermarks (percentage of buffer).  Only the RX side has
// watermark consumers (RX_LOWATER/RX_HIWATER); the TX side has none.
#define RX_HIGH_PERCENT         80
#define RX_LOW_PERCENT          20
#define RX_HIGH_WATER           (RX_BUFFER_SIZE * RX_HIGH_PERCENT / 100UL)
#define RX_LOW_WATER            (RX_BUFFER_SIZE * RX_LOW_PERCENT / 100UL)

// Ring buffer macros. These advance head/tail BEFORE accessing the slot
// (pre-increment style). The +1-before-mask pattern means slot 0 is never
// used and the full/empty distinction works: head==tail means empty,
// (head+1)&mask==tail means full (one slot wasted to distinguish states).
#define RX_READ(C)              (C)->rxBuff[(C)->rxTail = ((C)->rxTail + 1) & RX_BUFFER_MASK]
#define RX_WRITE(C, D)          (C)->rxBuff[(C)->rxHead = ((C)->rxHead + 1) & RX_BUFFER_MASK] = (D)
#define RX_INIT(C)              (C)->rxHead = (C)->rxTail = 0
#define RX_EMPTY(C)             ((C)->rxHead == (C)->rxTail)
#define RX_FULL(C)              ((((C)->rxHead + 1) & RX_BUFFER_MASK) == (C)->rxTail)
#define RX_COUNT(C)             (((C)->rxHead - (C)->rxTail) & RX_BUFFER_MASK)
#define RX_LOWATER(C)           (RX_COUNT(C) < RX_LOW_WATER)
#define RX_HIWATER(C)           (RX_COUNT(C) > RX_HIGH_WATER)

// TX buffer operations
#define TX_READ(C)              (C)->txBuff[(C)->txTail = ((C)->txTail + 1) & TX_BUFFER_MASK]
#define TX_WRITE(C, D)          (C)->txBuff[(C)->txHead = ((C)->txHead + 1) & TX_BUFFER_MASK] = (D)
#define TX_INIT(C)              (C)->txHead = (C)->txTail = 0
#define TX_EMPTY(C)             ((C)->txHead == (C)->txTail)
#define TX_FULL(C)              ((((C)->txHead + 1) & TX_BUFFER_MASK) == (C)->txTail)
#define TX_COUNT(C)             (((C)->txHead - (C)->txTail) & TX_BUFFER_MASK)

// XON/XOFF
#define XON                     0x11
#define XOFF                    0x13

// Default IRQs
#define COM1_DEFAULT_IRQ        4
#define COM2_DEFAULT_IRQ        3
#define COM3_DEFAULT_IRQ        4
#define COM4_DEFAULT_IRQ        3

#define COM_MIN                 RS232_COM1
#define COM_MAX                 RS232_COM4

#define IRQ_MIN                 3
#define IRQ_MAX                 15
#define IRQ_NONE                0xFF

// Max valid UART port base (12-bit I/O address).
#define UART_MAX_BASE           0xFFF

// BIOS data area at 0040:0000 holds up to 4 COM port base addresses as
// 16-bit words at offsets 0x400-0x407 (linear, in DOS low memory).
#define BIOS_COM_DATA_AREA      0x400

// UART type detection constants.
#define UART_SCRATCH_TEST_A     0xA5  // first value written to UART_SCR
#define UART_SCRATCH_TEST_B     0x5A  // second value written to UART_SCR
#define IIR_FIFO_STATUS_MASK    0xC0  // IIR bits 7:6 encode FIFO capability
#define IIR_FIFO_WORKING        0xC0  // 16550A: FIFO enabled and functional
#define IIR_FIFO_PRESENT        0x80  // 16550: FIFO advertised but broken

// PIC registers
#define INT_VECTOR_OFFSET       8
#define PIC_MASTER              0x20
#define PIC_SLAVE               0xA0
#define PIC_EOI                 0x20
#define PIC_RR                  0x02
#define PIC_MASTER_IRQ_MAX      7   // highest IRQ on the master 8259
#define SLAVE_IRQ_BASE          8   // IRQ8 is slave bit 0 (IRQ8..15 -> slave IMR bit 0..7)

#define PIC_WRITE_IMR(P, D)     outportb((P) | 1, (D))
#define PIC_WRITE_OCW2(P, D)    outportb((P), (D))
#define PIC_WRITE_OCW3(P, D)    outportb((P), (D) | 8)
#define PIC_READ_IMR(P)         inportb((P) | 1)
#define PIC_END_IRQ(P)          PIC_WRITE_OCW2((P), PIC_EOI)

#define PIC_ENABLE_IRQ(I) \
    { if ((I) <= PIC_MASTER_IRQ_MAX) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) & ~(1 << (I))); } \
      else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) & ~(1 << ((I) - SLAVE_IRQ_BASE))); } }

#define PIC_DISABLE_IRQ(I) \
    { if ((I) <= PIC_MASTER_IRQ_MAX) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) | (1 << (I))); } \
      else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) | (1 << ((I) - SLAVE_IRQ_BASE))); } }

// UART register offsets
#define UART_RX                 0
#define UART_TX                 0
#define UART_IER                1
#define UART_DLW                0 // divisor latch word (when DLAB=1)
#define UART_IIR                2
#define UART_FCR                2
#define UART_LCR                3
#define UART_MCR                4
#define UART_LSR                5
#define UART_MSR                6
#define UART_SCR                7 // scratch register (not present on 8250)

// UART read macros
#define UART_READ_DATA(C)       inportb((C)->base + UART_RX)
#define UART_READ_IER(C)        inportb((C)->base + UART_IER)
#define UART_READ_IIR(C)        inportb((C)->base + UART_IIR)
#define UART_READ_LCR(C)        inportb((C)->base + UART_LCR)
#define UART_READ_MCR(C)        inportb((C)->base + UART_MCR)
#define UART_READ_LSR(C)        ((C)->lsr = inportb((C)->base + UART_LSR))
#define UART_READ_MSR(C)        ((C)->msr = inportb((C)->base + UART_MSR))
#define UART_READ_BPS(C, D) \
    { outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) | LCR_DLAB); \
      (D) = (uint16_t)inportw((C)->base + UART_DLW); \
      outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) & ~LCR_DLAB); }

// UART write macros
#define UART_WRITE_DATA(C, D)   outportb((C)->base + UART_TX, (D))
#define UART_WRITE_IER(C, D)    ((C)->ier = OUTP((C)->base + UART_IER, (D)))
#define UART_WRITE_FCR(C, D)    ((C)->fcr = OUTP((C)->base + UART_FCR, (D)))
#define UART_WRITE_LCR(C, D)    ((C)->lcr = OUTP((C)->base + UART_LCR, (D)))
#define UART_WRITE_MCR(C, D)    ((C)->mcr = OUTP((C)->base + UART_MCR, (D)))
#define UART_WRITE_BPS(C, D) \
    { outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) | LCR_DLAB); \
      (C)->dlatch = OUTPW((C)->base + UART_DLW, (D)); \
      outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) & ~LCR_DLAB); }

// IER bits
#define IER_DATA_READY          0x01
#define IER_TX_HOLD_EMPTY       0x02
#define IER_ERRORS              0x04
#define IER_MODEM_STATUS        0x08

// IIR bits
#define IIR_NO_INT_PENDING      0x01
#define IIR_ID0                 0x02
#define IIR_ID1                 0x04
#define IIR_ID2                 0x08
#define IIR_MASK                (IIR_NO_INT_PENDING | IIR_ID0 | IIR_ID1)
#define IIR_MODEM_STATUS        0x00
#define IIR_TX_HOLD_EMPTY       IIR_ID0
#define IIR_DATA_READY          IIR_ID1
#define IIR_LINE_STATUS         (IIR_ID0 | IIR_ID1)
#define IIR_NO_INTERRUPT        IIR_NO_INT_PENDING

// FCR bits
#define FCR_ENABLE              0x01
#define FCR_RX_RESET            0x02
#define FCR_TX_RESET            0x04
#define FCR_DMA_SELECT          0x08
#define FCR_TRIGGER_0           0x40
#define FCR_TRIGGER_1           0x80
#define FCR_TRIGGER_AT_1        0x00
#define FCR_TRIGGER_AT_4        FCR_TRIGGER_0
#define FCR_TRIGGER_AT_8        FCR_TRIGGER_1
#define FCR_TRIGGER_AT_14       (FCR_TRIGGER_0 | FCR_TRIGGER_1)
#define FIFO_DEFAULT_THRESHOLD  14

// LCR bits
#define LCR_DLAB                0x80
#define LCR_BREAK               0x40
#define LCR_STICK_PARITY        0x20
#define LCR_EVEN_PARITY         0x10
#define LCR_PARITY_ENABLE       0x08
#define LCR_STOP                0x04
#define LCR_WORD1               0x02
#define LCR_WORD0               0x01

#define DATA_5                  0x00
#define DATA_6                  LCR_WORD0
#define DATA_7                  LCR_WORD1
#define DATA_8                  (LCR_WORD0 | LCR_WORD1)
#define DATA_MASK               (LCR_WORD0 | LCR_WORD1)

#define PARITY_NONE             0x00
#define PARITY_ODD              LCR_PARITY_ENABLE
#define PARITY_EVEN             (LCR_PARITY_ENABLE | LCR_EVEN_PARITY)
#define PARITY_MARK             (LCR_PARITY_ENABLE | LCR_STICK_PARITY)
#define PARITY_SPACE            (LCR_PARITY_ENABLE | LCR_EVEN_PARITY | LCR_STICK_PARITY)
#define PARITY_MASK             (LCR_PARITY_ENABLE | LCR_EVEN_PARITY | LCR_STICK_PARITY)

#define STOP_1                  0x00
#define STOP_2                  LCR_STOP
#define STOP_MASK               LCR_STOP

// MCR bits
#define MCR_DTR                 0x01
#define MCR_RTS                 0x02
#define MCR_OUT1                0x04
#define MCR_OUT2                0x08
#define MCR_LOOPBACK            0x10
#define MCR_MASK                (MCR_DTR | MCR_RTS | MCR_OUT1 | MCR_OUT2 | MCR_LOOPBACK)

// LSR bits
#define LSR_DATA_READY          0x01
#define LSR_OVERRUN             0x02
#define LSR_PARITY_ERR          0x04
#define LSR_FRAME_ERR           0x08
#define LSR_BREAK               0x10
#define LSR_TX_HOLD_EMPTY       0x20
#define LSR_TX_EMPTY            0x40
#define LSR_FIFO_ERR            0x80

// MSR bits
#define MSR_DLT_CTS             0x01
#define MSR_DLT_DSR             0x02
#define MSR_NEW_RING            0x04
#define MSR_DLT_DCD             0x08
#define MSR_CTS                 0x10
#define MSR_DSR                 0x20
#define MSR_RI                  0x40
#define MSR_DCD                 0x80

#define FIFO_SIZE               16

#define ISR_SIZE                2048


// ========================================================================
// Types
// ========================================================================

typedef struct {
    uint8_t  port;
    uint8_t  defaultIrq;
    uint8_t  irq;
    uint8_t  isOpen;
    uint8_t  ier;
    uint8_t  fcr;
    uint8_t  lcr;
    uint8_t  mcr;
    uint8_t  lsr;
    uint8_t  msr;
    uint8_t  flowMode;
    uint8_t  rxFlowOn;
    uint8_t  txFlowOn;
    uint8_t  rxBuff[(uint16_t)RX_BUFFER_SIZE];
    uint8_t  txBuff[(uint16_t)TX_BUFFER_SIZE];
    uint32_t base;
    uint32_t dlatch;
    uint32_t rxHead;
    uint32_t txHead;
    uint32_t rxTail;
    uint32_t txTail;
} Rs232StateT;


typedef struct {
    int32_t  bps;
    uint16_t divisor;
} Rs232BpsMapT;


// ========================================================================
// Static globals
// ========================================================================

static _go32_dpmi_seginfo sOldIsrs[16];
static uint8_t            sIsrsTaken[16] = {0};
static _go32_dpmi_seginfo sIsrAddr;
static uint32_t           sIsrsCount = 0;

static Rs232StateT sComPorts[COM_MAX + 1] = {
    { .port = 0, .defaultIrq = COM1_DEFAULT_IRQ, .irq = IRQ_NONE },
    { .port = 1, .defaultIrq = COM2_DEFAULT_IRQ, .irq = IRQ_NONE },
    { .port = 2, .defaultIrq = COM3_DEFAULT_IRQ, .irq = IRQ_NONE },
    { .port = 3, .defaultIrq = COM4_DEFAULT_IRQ, .irq = IRQ_NONE }
};

// BPS rate to divisor mapping (1.8432 MHz crystal)
static const Rs232BpsMapT sBpsMap[] = {
    { 115200,    1 },
    {  57600,    2 },
    {  38400,    3 },
    {  19200,    6 },
    {   9600,   12 },
    {   7200,   16 },
    {   4800,   24 },
    {   3600,   32 },
    {   2400,   48 },
    {   1800,   64 },
    {   1200,   96 },
    {    600,  192 },
    {    300,  384 },
    {    150,  768 },
    {    110, 1047 },
    {     75, 1536 },
    {     50, 2304 }
};

#define BPS_MAP_COUNT (sizeof(sBpsMap) / sizeof(sBpsMap[0]))


// ========================================================================
// Prototypes (alphabetical)
// ========================================================================

static int32_t  bpsToDivisor(int32_t bps);
static void     comGeneralIsr(void);
static int32_t  divisorToBps(uint16_t divisor);
static void     dpmiGetPvect(int vector, _go32_dpmi_seginfo *info);
static int      dpmiLockMemory(void);
static void     dpmiSetPvect(int vector, _go32_dpmi_seginfo *info);
static void     dpmiUnlockMemory(void);
static int      findIrq(int com);
static void     freeIrq(int com);
static int      installIrqHandler(int irq);
static void     irqRestore(uint32_t flags);
static uint32_t irqSave(void);
static uint8_t  picReadIrr(uint16_t port);
static void     removeIrqHandler(int irq);
int            rs232ClearRxBuffer(int com);
int            rs232ClearTxBuffer(int com);
int            rs232Close(int com);
int            rs232GetBase(int com);
int32_t        rs232GetBps(int com);
int            rs232GetCts(int com);
int            rs232GetData(int com);
int            rs232GetDsr(int com);
int            rs232GetDtr(int com);
int            rs232GetHandshake(int com);
int            rs232GetIrq(int com);
int            rs232GetLsr(int com);
int            rs232GetMcr(int com);
int            rs232GetMsr(int com);
char           rs232GetParity(int com);
int            rs232GetRts(int com);
int            rs232GetRxBuffered(int com);
int            rs232GetStop(int com);
int            rs232GetTxBuffered(int com);
int            rs232GetUartType(int com);
int            rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake);
int            rs232Read(int com, char *data, int len);
int            rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake);
int            rs232SetBase(int com, int base);
int            rs232SetBps(int com, int32_t bps);
int            rs232SetData(int com, int dataBits);
int            rs232SetDtr(int com, bool dtr);
int            rs232SetFifoThreshold(int com, int threshold);
int            rs232SetHandshake(int com, int handshake);
int            rs232SetIrq(int com, int irq);
int            rs232SetMcr(int com, int mcr);
int            rs232SetParity(int com, char parity);
int            rs232SetRts(int com, bool rts);
int            rs232SetStop(int com, int stopBits);
int            rs232Write(int com, const char *data, int len);
int            rs232WriteBuf(int com, const char *data, int len);


// ========================================================================
// Interrupt service routine
// ========================================================================

static int32_t bpsToDivisor(int32_t bps) {
    uint32_t i;

    for (i = 0; i < BPS_MAP_COUNT; i++) {
        if (sBpsMap[i].bps == bps) {
            return sBpsMap[i].divisor;
        }
    }
    return RS232_ERR_INVALID_BPS;
}


static int32_t divisorToBps(uint16_t divisor) {
    uint32_t i;

    for (i = 0; i < BPS_MAP_COUNT; i++) {
        if (sBpsMap[i].divisor == divisor) {
            return sBpsMap[i].bps;
        }
    }
    return RS232_ERR_INVALID_BPS;
}


// Single shared ISR for all COM ports. ISR sharing is necessary because
// COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3. Having one ISR
// that polls all ports avoids the complexity of per-IRQ handlers.
//
// The ISR design follows a careful protocol:
//   1. Mask all COM IRQs on the PIC to prevent re-entry
//   2. STI to allow higher-priority interrupts (timer, keyboard) through
//   3. Loop over all open ports, draining each UART's pending interrupts
//   4. CLI, send EOI to PIC, re-enable COM IRQs, STI before IRET
//
// This "mask-then-STI" pattern is standard for slow device ISRs on PC
// hardware  -- it prevents the same IRQ from re-entering while still allowing
// the timer tick and keyboard to function during potentially long UART
// processing.
static void comGeneralIsr(void) {
    Rs232StateT *comMin        = &sComPorts[0];
    Rs232StateT *comMax        = &sComPorts[COM_MAX];
    uint8_t      intId;
    uint8_t      data;
    uint8_t      slaveTriggered = 0;
    Rs232StateT *com;
    for (com = comMin; com <= comMax; com++) {
        if (com->isOpen) {
            if (com->irq > 7) {
                slaveTriggered = 1;
            }
            PIC_DISABLE_IRQ(com->irq);
        }
    }
    asm("STI");

    // Process all pending interrupts on all open ports
    for (com = comMin; com <= comMax; com++) {
        if (com->isOpen) {
            while ((intId = UART_READ_IIR(com) & IIR_MASK) != IIR_NO_INTERRUPT) {
                switch (intId) {
                    case IIR_DATA_READY:
                        // Read all available data from the UART
                        while (UART_READ_LSR(com) & LSR_DATA_READY) {
                            data = UART_READ_DATA(com);

                            // Handle XON/XOFF flow control (TX direction)
                            if (com->flowMode == RS232_HANDSHAKE_XONXOFF && (data == XOFF || data == XON)) {
                                com->txFlowOn = (data == XON);
                                if (!TX_EMPTY(com) && com->txFlowOn) {
                                    UART_WRITE_IER(com, UART_READ_IER(com) | IER_TX_HOLD_EMPTY);
                                }
                            } else if (!RX_FULL(com)) {
                                // Store data if room in buffer
                                RX_WRITE(com, data);

                                // RX flow control: turn off if buffer almost full
                                if (com->rxFlowOn && RX_HIWATER(com)) {
                                    com->rxFlowOn = 0;
                                    switch (com->flowMode) {
                                        case RS232_HANDSHAKE_XONXOFF:
                                            UART_WRITE_DATA(com, XOFF);
                                            break;
                                        case RS232_HANDSHAKE_RTSCTS:
                                            UART_WRITE_MCR(com, UART_READ_MCR(com) & ~MCR_RTS);
                                            break;
                                        case RS232_HANDSHAKE_DTRDSR:
                                            UART_WRITE_MCR(com, UART_READ_MCR(com) & ~MCR_DTR);
                                            break;
                                        case RS232_HANDSHAKE_NONE:
                                            break;
                                    }
                                }
                            }
                        }
                        break;

                    case IIR_LINE_STATUS:
                        UART_READ_LSR(com);
                        break;

                    case IIR_MODEM_STATUS:
                        UART_READ_MSR(com);
                        // Handle RTS/CTS or DTR/DSR flow control (TX direction)
                        if (com->flowMode == RS232_HANDSHAKE_RTSCTS) {
                            com->txFlowOn = (com->msr & MSR_CTS) != 0;
                        } else if (com->flowMode == RS232_HANDSHAKE_DTRDSR) {
                            com->txFlowOn = (com->msr & MSR_DSR) != 0;
                        }
                        if (!TX_EMPTY(com) && com->txFlowOn) {
                            UART_WRITE_IER(com, UART_READ_IER(com) | IER_TX_HOLD_EMPTY);
                        }
                        break;

                    case IIR_TX_HOLD_EMPTY: {
                        // Drain up to FIFO_SIZE bytes from TX ring buffer to UART.
                        // Writing more than one byte is only safe on 16550A+ (has FIFO);
                        // on 8250/16450 FIFO_SIZE effectively limits to 1 byte anyway.
                        int cnt;
                        for (cnt = 0; cnt < FIFO_SIZE && com->txFlowOn && !TX_EMPTY(com); cnt++) {
                            UART_WRITE_DATA(com, TX_READ(com));
                        }
                        if (TX_EMPTY(com) || !com->txFlowOn) {
                            UART_WRITE_IER(com, UART_READ_IER(com) & ~IER_TX_HOLD_EMPTY);
                        }
                        break;
                    }
                }
            }
        }
    }

    asm("CLI");

    // End the interrupt on the PIC
    if (slaveTriggered) {
        PIC_END_IRQ(PIC_SLAVE);
    }
    PIC_END_IRQ(PIC_MASTER);

    // Re-enable all COM port interrupts
    for (com = comMin; com <= comMax; com++) {
        if (com->isOpen) {
            PIC_ENABLE_IRQ(com->irq);
        }
    }

    // Must explicitly STI before IRET because under DPMI (and some
    // virtualizers like DOSBox/86Box), IRET's implicit IF restore from
    // the flags on the stack doesn't always work as expected.
    asm("STI");
}


// ========================================================================
// DPMI utility functions
// ========================================================================

// These functions wrap DJGPP's DPMI interface for installing protected-mode
// interrupt handlers. Under DPMI, the ISR code and data must be locked in
// physical memory to prevent page faults during interrupt handling  -- a page
// fault inside an ISR would be fatal. The IRET wrapper is also allocated
// by DPMI to handle the real-mode-to-protected-mode transition.

static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info) {
    _go32_dpmi_get_protected_mode_interrupt_vector(vector, info);
}


// Lock the ISR code and its data (sComPorts array) in physical memory.
// The IRET wrapper handles the stack frame that DPMI uses to dispatch
// hardware interrupts to protected-mode code.
static int dpmiLockMemory(void) {
    unsigned long dataAddr;
    unsigned long codeAddr;
    __dpmi_meminfo dataRegion;
    __dpmi_meminfo codeRegion;

    if (__dpmi_get_segment_base_address(_my_cs(), &codeAddr) == 0 &&
        __dpmi_get_segment_base_address(_my_ds(), &dataAddr) == 0) {
        codeRegion.handle  = 0;
        codeRegion.size    = ISR_SIZE;
        codeRegion.address = codeAddr + (unsigned long)comGeneralIsr;
        dataRegion.handle  = 0;
        dataRegion.size    = sizeof(sComPorts);
        dataRegion.address = dataAddr + (unsigned long)sComPorts;
        if (__dpmi_lock_linear_region(&codeRegion) == 0) {
            if (__dpmi_lock_linear_region(&dataRegion) == 0) {
                sIsrAddr.pm_offset   = (unsigned long)comGeneralIsr;
                sIsrAddr.pm_selector = _go32_my_cs();
                if (_go32_dpmi_allocate_iret_wrapper(&sIsrAddr) == 0) {
                    return 1;
                }
                __dpmi_unlock_linear_region(&dataRegion);
            }
            __dpmi_unlock_linear_region(&codeRegion);
        }
    }
    return 0;
}


static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info) {
    _go32_dpmi_set_protected_mode_interrupt_vector(vector, info);
}


static void dpmiUnlockMemory(void) {
    unsigned long baseAddr;
    __dpmi_meminfo region;

    if (__dpmi_get_segment_base_address(_my_ds(), &baseAddr) == 0) {
        region.handle  = 0;
        region.size    = sizeof(sComPorts);
        region.address = baseAddr + (unsigned long)sComPorts;
        __dpmi_unlock_linear_region(&region);
    }
    if (__dpmi_get_segment_base_address(_my_cs(), &baseAddr) == 0) {
        region.handle  = 0;
        region.size    = ISR_SIZE;
        region.address = baseAddr + (unsigned long)comGeneralIsr;
        __dpmi_unlock_linear_region(&region);
    }
    _go32_dpmi_free_iret_wrapper(&sIsrAddr);
}


// ========================================================================
// IRQ management
// ========================================================================

// Auto-detect which IRQ a UART is wired to by generating a TX hold empty
// interrupt and reading the PIC's IRR (Interrupt Request Register) to see
// which line went high. Uses a double-check pattern: enable interrupt,
// read IRR, disable, read IRR again, mask out persistent bits, then
// enable once more to confirm. Falls back to the default IRQ if detection
// fails (e.g., on virtualized hardware that doesn't model IRR accurately).
static int findIrq(int comport) {
    Rs232StateT *com   = &sComPorts[comport];
    uint8_t      imrM  = PIC_READ_IMR(PIC_MASTER);
    uint8_t      imrS  = PIC_READ_IMR(PIC_SLAVE);
    uint8_t      irrM;
    uint8_t      irrS;

    // Set up the UART
    UART_WRITE_MCR(com, MCR_OUT2);
    UART_WRITE_FCR(com, 0);

    // Wait until TX hold register is empty
    while ((UART_READ_LSR(com) & LSR_TX_HOLD_EMPTY) == 0) {
    }

    asm("CLI");

    // Allow any interrupt on PIC
    PIC_WRITE_IMR(PIC_MASTER, 0);
    PIC_WRITE_IMR(PIC_SLAVE, 0);

    // Initial polls to let things settle
    UART_WRITE_IER(com, IER_TX_HOLD_EMPTY);
    picReadIrr(PIC_MASTER);
    picReadIrr(PIC_SLAVE);
    UART_WRITE_IER(com, 0);
    picReadIrr(PIC_MASTER);
    picReadIrr(PIC_SLAVE);

    // Generate an interrupt and record all active IRQs
    UART_WRITE_IER(com, IER_TX_HOLD_EMPTY);
    irrM = picReadIrr(PIC_MASTER);
    irrS = picReadIrr(PIC_SLAVE);

    // Remove the interrupt and mask out all IRQs still active
    UART_WRITE_IER(com, 0);
    irrM &= ~picReadIrr(PIC_MASTER);
    irrS &= ~picReadIrr(PIC_SLAVE);

    // Interrupt again to confirm
    UART_WRITE_IER(com, IER_TX_HOLD_EMPTY);
    irrM &= picReadIrr(PIC_MASTER);
    irrS &= picReadIrr(PIC_SLAVE);

    // Restore everything
    PIC_WRITE_IMR(PIC_MASTER, imrM);
    PIC_WRITE_IMR(PIC_SLAVE, imrS);
    UART_WRITE_IER(com, 0);

    asm("STI");

    // IRQs 0-2 are never valid serial lines (timer, keyboard, cascade), so a
    // detected 0/1/2 is treated as detection failure; the caller then falls
    // back to the port's default IRQ.
    switch (irrM) {
        case 0x01: return RS232_ERR_IRQ_NOT_FOUND;
        case 0x02: return RS232_ERR_IRQ_NOT_FOUND;
        case 0x04:
            switch (irrS) {
                case 0x01: return 8;
                case 0x02: return 9;
                case 0x04: return 10;
                case 0x08: return 11;
                case 0x10: return 12;
                case 0x20: return 13;
                case 0x40: return 14;
                case 0x80: return 15;
                default:   return RS232_ERR_IRQ_NOT_FOUND;
            }
        case 0x08: return 3;
        case 0x10: return 4;
        case 0x20: return 5;
        case 0x40: return 6;
        case 0x80: return 7;
    }
    return RS232_ERR_IRQ_NOT_FOUND;
}


// Release this port's IRQ. If another port shares the same IRQ line,
// the handler stays installed  -- only the last user removes it. This is
// critical for COM1/COM3 IRQ sharing.
static void freeIrq(int comport) {
    Rs232StateT *com    = &sComPorts[comport];
    Rs232StateT *comMin = &sComPorts[0];
    Rs232StateT *comMax = &sComPorts[COM_MAX];
    uint32_t     irq    = com->irq;
    Rs232StateT *ptr;

    if (irq == IRQ_NONE) {
        return;
    }

    asm("CLI");

    // Disable interrupts from the UART
    UART_WRITE_IER(com, 0);
    UART_WRITE_MCR(com, MCR_OUT2);

    // Clear the FIFO and RX registers
    UART_WRITE_FCR(com, 0);
    for (int i = 0; i < FIFO_DEFAULT_THRESHOLD; i++) {
        UART_READ_DATA(com);
    }
    UART_READ_IIR(com);
    UART_READ_LSR(com);
    UART_READ_DATA(com);
    UART_READ_IIR(com);
    UART_READ_MSR(com);
    UART_READ_IIR(com);
    UART_READ_LSR(com);
    UART_READ_IIR(com);

    com->irq = IRQ_NONE;

    asm("STI");

    // Check if any other port shares this IRQ
    for (ptr = comMin; ptr <= comMax; ptr++) {
        if (ptr != com && ptr->irq == irq) {
            return;
        }
    }

    // No other port uses this IRQ, so restore the old handler
    PIC_DISABLE_IRQ(irq);
    removeIrqHandler(irq);
}


static int installIrqHandler(int irq) {
    if (!sIsrsTaken[irq]) {
        if (sIsrsCount++ == 0) {
            // Lock memory used by interrupt handler in DPMI mode
            if (!dpmiLockMemory()) {
                --sIsrsCount;
                return RS232_ERR_LOCK_MEM;
            }
        }
        dpmiGetPvect(irq + INT_VECTOR_OFFSET, &sOldIsrs[irq]);
        dpmiSetPvect(irq + INT_VECTOR_OFFSET, &sIsrAddr);
        sIsrsTaken[irq] = 1;
    }
    return RS232_SUCCESS;
}


// Disable interrupts, returning the previous EFLAGS so the caller can
// restore the interrupt state it was entered with instead of blindly
// re-enabling with STI.
static uint32_t irqSave(void) {
    uint32_t flags;

    asm volatile("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");

    return flags;
}


static void irqRestore(uint32_t flags) {
    asm volatile("pushl %0\n\tpopfl" : : "r"(flags) : "memory", "cc");
}


static uint8_t picReadIrr(uint16_t port) {
    PIC_WRITE_OCW3(port, PIC_RR);
    return inportb(port);
}


static void removeIrqHandler(int irq) {
    if (sIsrsTaken[irq]) {
        dpmiSetPvect(irq + INT_VECTOR_OFFSET, &sOldIsrs[irq]);
        sIsrsTaken[irq] = 0;
        if (--sIsrsCount == 0) {
            dpmiUnlockMemory();
        }
    }
}


// ========================================================================
// Public functions (alphabetical)
// ========================================================================

int rs232ClearRxBuffer(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    asm("CLI");
    RX_INIT(port);
    asm("STI");

    return RS232_SUCCESS;
}


int rs232ClearTxBuffer(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    asm("CLI");
    TX_INIT(port);
    asm("STI");

    return RS232_SUCCESS;
}


int rs232Close(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    // Restore old interrupt handler
    freeIrq(com);

    // Turn everything off
    UART_WRITE_IER(port, 0);
    UART_WRITE_MCR(port, 0);
    rs232SetFifoThreshold(com, 0);

    port->isOpen = 0;

    return RS232_SUCCESS;
}


int rs232GetBase(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (port->base < 1 || port->base > UART_MAX_BASE) {
        return RS232_ERR_INVALID_BASE;
    }

    return port->base;
}


int32_t rs232GetBps(int com) {
    Rs232StateT *port = &sComPorts[com];
    uint16_t     divisor;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    // The ISR must not fire inside the DLAB=1 window: with DLAB set, its
    // data register access hits the divisor latch instead of RBR/THR.
    uint32_t flags = irqSave();
    UART_READ_BPS(port, divisor);
    irqRestore(flags);

    return divisorToBps(divisor);
}


int rs232GetCts(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return (port->msr & MSR_CTS) != 0;
}


int rs232GetData(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (UART_READ_LCR(port) & DATA_MASK) {
        case DATA_5: return 5;
        case DATA_6: return 6;
        case DATA_7: return 7;
        case DATA_8: return 8;
    }
    return RS232_ERR_INVALID_DATA;
}


int rs232GetDsr(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return (port->msr & MSR_DSR) != 0;
}


int rs232GetDtr(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return (port->mcr & MCR_DTR) != 0;
}


int rs232GetHandshake(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return port->flowMode;
}


int rs232GetIrq(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (port->irq < IRQ_MIN || port->irq > IRQ_MAX) {
        return RS232_ERR_INVALID_IRQ;
    }

    return port->irq;
}


int rs232GetLsr(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return port->lsr;
}


int rs232GetMcr(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return port->mcr;
}


int rs232GetMsr(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return port->msr;
}


char rs232GetParity(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (UART_READ_LCR(port) & PARITY_MASK) {
        case PARITY_NONE:  return 'n';
        case PARITY_EVEN:  return 'e';
        case PARITY_ODD:   return 'o';
        case PARITY_MARK:  return 'm';
        case PARITY_SPACE: return 's';
    }
    return RS232_ERR_INVALID_PARITY;
}


int rs232GetRts(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }

    return (port->mcr & MCR_RTS) != 0;
}


int rs232GetRxBuffered(int com) {
    Rs232StateT *port = &sComPorts[com];
    int          count;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    asm("CLI");
    count = RX_COUNT(port);
    asm("STI");

    return count;
}


int rs232GetStop(int com) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (UART_READ_LCR(port) & STOP_MASK) {
        case STOP_1: return 1;
        case STOP_2: return 2;
    }
    return RS232_ERR_INVALID_STOP;
}


int rs232GetTxBuffered(int com) {
    Rs232StateT *port = &sComPorts[com];
    int          count;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    asm("CLI");
    count = TX_COUNT(port);
    asm("STI");

    return count;
}


// Detect UART type by probing hardware features. The detection sequence:
//   1. Write/read scratch register  -- 8250 doesn't have one
//   2. Enable FIFO and check IIR bits 7:6  -- distinguishes 16450/16550/16550A
// This matters because only 16550A has a reliable 16-byte FIFO; the
// original 16550 FIFO is buggy and should not be enabled.
int rs232GetUartType(int com) {
    Rs232StateT *port = &sComPorts[com];
    uint8_t      scratch;
    uint8_t      iir;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    // Test scratch register  -- 8250 lacks it
    outportb(port->base + UART_SCR, UART_SCRATCH_TEST_A);
    scratch = inportb(port->base + UART_SCR);
    if (scratch != UART_SCRATCH_TEST_A) {
        return RS232_UART_8250;
    }
    outportb(port->base + UART_SCR, UART_SCRATCH_TEST_B);
    scratch = inportb(port->base + UART_SCR);
    if (scratch != UART_SCRATCH_TEST_B) {
        return RS232_UART_8250;
    }

    // Has scratch register  -- at least 16450. Try enabling FIFO.
    outportb(port->base + UART_FCR, FCR_ENABLE);
    iir = inportb(port->base + UART_IIR);

    // Restore original FCR
    outportb(port->base + UART_FCR, port->fcr);

    // Check IIR bits 7:6 for FIFO status
    switch (iir & IIR_FIFO_STATUS_MASK) {
        case IIR_FIFO_WORKING:
            return RS232_UART_16550A;
        case IIR_FIFO_PRESENT:
            return RS232_UART_16550;
        default:
            return RS232_UART_16450;
    }
}


int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
    Rs232StateT *port = &sComPorts[com];
    int          rc   = RS232_SUCCESS;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (port->isOpen) {
        return RS232_ERR_ALREADY_OPEN;
    }

    port->isOpen   = 1;
    port->rxFlowOn = 1;
    port->txFlowOn = 1;
    RX_INIT(port);
    TX_INIT(port);

    // Read COM base address from BIOS data area at 0040:0000. The BIOS
    // stores up to 4 COM port base addresses as 16-bit words at 40:00-40:07.
    // This is more reliable than hardcoding 0x3F8/0x2F8 because BIOS setup
    // may have remapped ports or detected them in a different order.
    if (rs232SetBase(com, _farpeekw(_dos_ds, BIOS_COM_DATA_AREA + (com << 1))) != RS232_SUCCESS) {
        // Clear the open flag set above so a later open isn't rejected with
        // ALREADY_OPEN against a port that never finished opening.  No IRQ is
        // hooked yet, so rs232Close (which would write to the bad base) is
        // not needed.
        port->isOpen = 0;
        return RS232_ERR_NO_UART;
    }

    // Turn off UART interrupts
    UART_WRITE_IER(port, 0);

    // Auto-detect IRQ
    rc = findIrq(com);
    if (rc < 0) {
        rc = port->defaultIrq;
    }

    rc = rs232SetIrq(com, rc);
    if (rc != RS232_SUCCESS) {
        // Same as above: no IRQ hooked here either, so just clear the flag.
        port->isOpen = 0;
        return rc;
    }

    // Turn off PIC interrupts for this IRQ
    PIC_DISABLE_IRQ(port->irq);

    // Configure the port
    rc = rs232Set(com, bps, dataBits, parity, stopBits, handshake);
    if (rc == RS232_SUCCESS) {
        UART_WRITE_MCR(port, MCR_DTR | MCR_RTS | MCR_OUT1 | MCR_OUT2);
        rc = rs232SetFifoThreshold(com, FIFO_DEFAULT_THRESHOLD);

        // Read initial status
        UART_READ_LSR(port);
        UART_READ_MSR(port);

        // Enable interrupts only after a successful configuration
        UART_WRITE_IER(port, IER_DATA_READY | IER_MODEM_STATUS | IER_ERRORS);
        PIC_ENABLE_IRQ(port->irq);
    }

    if (rc != RS232_SUCCESS) {
        rs232Close(com);
    }

    return rc;
}


int rs232Read(int com, char *data, int len) {
    Rs232StateT *port = &sComPorts[com];
    int          i;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }
    if (data == 0) {
        return RS232_ERR_NULL_PTR;
    }

    asm("CLI");

    for (i = 0; !RX_EMPTY(port) && i < len; i++) {
        data[i] = RX_READ(port);
    }

    // RX flow control: turn back on if buffer almost empty
    if (!port->rxFlowOn && RX_LOWATER(port)) {
        port->rxFlowOn = 1;
        if (port->flowMode == RS232_HANDSHAKE_XONXOFF) {
            UART_WRITE_DATA(port, XON);
        } else if (port->flowMode == RS232_HANDSHAKE_RTSCTS) {
            UART_WRITE_MCR(port, UART_READ_MCR(port) | MCR_RTS);
        } else if (port->flowMode == RS232_HANDSHAKE_DTRDSR) {
            UART_WRITE_MCR(port, UART_READ_MCR(port) | MCR_DTR);
        }
    }

    asm("STI");

    return i;
}


int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
    int rc;

    rc = rs232SetBps(com, bps);
    if (rc == RS232_SUCCESS) {
        rc = rs232SetData(com, dataBits);
    }
    if (rc == RS232_SUCCESS) {
        rc = rs232SetParity(com, parity);
    }
    if (rc == RS232_SUCCESS) {
        rc = rs232SetStop(com, stopBits);
    }
    if (rc == RS232_SUCCESS) {
        rc = rs232SetHandshake(com, handshake);
    }
    return rc;
}


int rs232SetBase(int com, int base) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (base < 1 || base > UART_MAX_BASE) {
        return RS232_ERR_INVALID_BASE;
    }

    port->base = base;
    return RS232_SUCCESS;
}


int rs232SetBps(int com, int32_t bps) {
    Rs232StateT *port    = &sComPorts[com];
    int32_t      divisor = bpsToDivisor(bps);

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }
    if (divisor < 0) {
        return divisor;
    }

    // The ISR must not fire inside the DLAB=1 window (see rs232GetBps).
    uint32_t flags = irqSave();
    UART_WRITE_BPS(port, (uint16_t)divisor);
    irqRestore(flags);

    return RS232_SUCCESS;
}


int rs232SetData(int com, int dataBits) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (dataBits) {
        case 5: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_5); return RS232_SUCCESS;
        case 6: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_6); return RS232_SUCCESS;
        case 7: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_7); return RS232_SUCCESS;
        case 8: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_8); return RS232_SUCCESS;
    }
    return RS232_ERR_INVALID_DATA;
}


int rs232SetDtr(int com, bool dtr) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    if (dtr) {
        UART_WRITE_MCR(port, port->mcr | MCR_DTR);
    } else {
        UART_WRITE_MCR(port, port->mcr & ~MCR_DTR);
    }

    return RS232_SUCCESS;
}


int rs232SetFifoThreshold(int com, int threshold) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (threshold) {
        case 14:
            UART_WRITE_FCR(port, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | FCR_TRIGGER_AT_14);
            break;
        case 8:
            UART_WRITE_FCR(port, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | FCR_TRIGGER_AT_8);
            break;
        case 4:
            UART_WRITE_FCR(port, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | FCR_TRIGGER_AT_4);
            break;
        case 1:
            UART_WRITE_FCR(port, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | FCR_TRIGGER_AT_1);
            break;
        case 0:
            UART_WRITE_FCR(port, 0);
            break;
        default:
            return RS232_ERR_INVALID_FIFO;
    }

    // Clear any garbage in UART FIFO
    for (int i = 0; i < FIFO_SIZE; i++) {
        UART_READ_DATA(port);
    }
    UART_READ_LSR(port);

    return RS232_SUCCESS;
}


int rs232SetHandshake(int com, int handshake) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }
    if (handshake < RS232_HANDSHAKE_NONE || handshake > RS232_HANDSHAKE_DTRDSR) {
        return RS232_ERR_INVALID_HANDSHAKE;
    }

    port->txFlowOn = 1;
    port->rxFlowOn = 1;
    port->flowMode = handshake;

    return RS232_SUCCESS;
}


int rs232SetIrq(int com, int irq) {
    Rs232StateT *port = &sComPorts[com];
    int          rc;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (irq < IRQ_MIN || irq > IRQ_MAX) {
        return RS232_ERR_INVALID_IRQ;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    // Remove any ISRs on this port's current IRQ
    freeIrq(com);

    rc = installIrqHandler(irq);
    if (rc == RS232_SUCCESS) {
        port->irq = irq;
        PIC_ENABLE_IRQ(port->irq);
    }
    return rc;
}


int rs232SetMcr(int com, int mcr) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    UART_WRITE_MCR(port, mcr & MCR_MASK);

    return RS232_SUCCESS;
}


int rs232SetParity(int com, char parity) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (parity) {
        case 'n': case 'N': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE);  return RS232_SUCCESS;
        case 'e': case 'E': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN);  return RS232_SUCCESS;
        case 'o': case 'O': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD);   return RS232_SUCCESS;
        case 'm': case 'M': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK);  return RS232_SUCCESS;
        case 's': case 'S': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS;
    }
    return RS232_ERR_INVALID_PARITY;
}


int rs232SetRts(int com, bool rts) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    if (rts) {
        UART_WRITE_MCR(port, port->mcr | MCR_RTS);
    } else {
        UART_WRITE_MCR(port, port->mcr & ~MCR_RTS);
    }

    return RS232_SUCCESS;
}


int rs232SetStop(int com, int stopBits) {
    Rs232StateT *port = &sComPorts[com];

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }

    switch (stopBits) {
        case 1: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_1); return RS232_SUCCESS;
        case 2: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_2); return RS232_SUCCESS;
    }
    return RS232_ERR_INVALID_STOP;
}


// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte.
// This bypasses the TX ring buffer entirely and writes directly to the UART.
// Used by the packet layer for frame transmission where we want guaranteed
// delivery order and don't want ISR-driven TX reordering complications.
// The trade-off is CPU burn during the wait, but at 115200 bps each byte
// takes only ~87us, and frame sizes are small (< 520 bytes worst case).
int rs232Write(int com, const char *data, int len) {
    Rs232StateT *port = &sComPorts[com];
    int          i;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }
    if (data == 0) {
        return RS232_ERR_NULL_PTR;
    }

    for (i = 0; i < len; i++) {
        // Wait until we can write
        while (!(UART_READ_LSR(port) & LSR_TX_HOLD_EMPTY)) {
        }

        if (port->txFlowOn) {
            UART_WRITE_DATA(port, data[i]);
        } else {
            break;
        }
    }
    return i;
}


// Non-blocking buffered write: pushes data into the TX ring buffer and
// enables the TX_HOLD_EMPTY interrupt so the ISR drains it asynchronously.
// Returns the number of bytes actually buffered (may be less than len if
// the buffer is full). This is the ISR-driven counterpart to rs232Write.
int rs232WriteBuf(int com, const char *data, int len) {
    Rs232StateT *port = &sComPorts[com];
    int          i;

    if (com < COM_MIN || com > COM_MAX) {
        return RS232_ERR_INVALID_PORT;
    }
    if (!port->isOpen) {
        return RS232_ERR_NOT_OPEN;
    }
    if (data == 0) {
        return RS232_ERR_NULL_PTR;
    }

    asm("CLI");

    for (i = 0; i < len; i++) {
        if (TX_FULL(port)) {
            break;
        }
        TX_WRITE(port, data[i]);
    }

    // If there's data to send, enable TX_HOLD_EMPTY interrupt
    if (!TX_EMPTY(port)) {
        UART_WRITE_IER(port, UART_READ_IER(port) | IER_TX_HOLD_EMPTY);
    }

    asm("STI");

    return i;
}
