// DOS keyboard + mouse input.
//
// Keyboard: hooks INT 9 to capture AT set-1 scan codes from port 0x60.
// The ISR reads the scan code, maps it to a jlKeyE, updates a private
// gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots
// gIsrState into gKeyState with interrupts disabled.
//
// Typed characters: because the hook replaces the BIOS INT 9 handler
// outright (no chaining, no EOI from the BIOS), INT 16h never sees our
// keys, so the ISR translates make codes itself with a US-layout
// scancode->ASCII pair of tables (normal/shifted) plus caps-lock
// tracking, seeded from the BIOS shift-flag byte at 0040:0017. The
// translated bytes land in a private locked ring (gIsrCharQueue) that
// jlpInputPoll drains into the core typed-character queue. Keys held
// with Ctrl or Alt are not characters and are skipped. The hard-coded
// US layout matches the port's existing hard-coded scan map; KEYB
// layouts are not applied (they hook the BIOS handler we replace).
//
// The two-buffer split is required for jlKeyPressed edge detection.
// jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll
// runs, so whatever gKeyState holds at that moment becomes gKeyPrev.
// If the ISR wrote directly to gKeyState, press/release events that
// happened between polls would already have landed there -- the memcpy
// would copy the new value into gKeyPrev and edge detection would see
// gKeyState == gKeyPrev every frame.
//
// Because DJGPP runs in protected mode with paging, the ISR code and
// any data it touches must be locked or a page fault at interrupt time
// will hang the machine.
//
// Mouse: uses INT 33h, the standard MS DOS mouse driver interface.
// jlpInputInit calls function 0 (reset/detect), then 7/8 to clamp the
// driver's coordinate range to the surface dimensions. jlpInputPoll
// calls function 3 once per frame to read absolute X/Y and the button
// bitmask. INT 33h is provided by every common DOS mouse driver and by
// DOSBox / DOSBox-X.

#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <string.h>
#include <sys/farptr.h>

#include "port.h"
#include "inputInternal.h"
#include "joey/surface.h"

// ----- Constants -----

#define KB_DATA_PORT     0x60
#define PIC_CMD_PORT     0x20
#define PIC_EOI          0x20
#define SCAN_BREAK_BIT   0x80
#define SCAN_CODE_MASK   0x7F
#define SCAN_EXTENDED    0xE0
#define SCAN_TABLE_SIZE  128
#define ISR_LOCK_SIZE    4096

// Caps/num-lock make codes, and the BIOS keyboard shift-flag byte in
// the BIOS data area (0040:0017); bit 6 = caps-lock active, bit 5 =
// num-lock active. Read once at init to seed gIsrCapsLock/gIsrNumLock
// -- the BIOS can't update the byte after we take INT 9, so from then
// on the ISR tracks the toggles itself.
#define SCAN_CAPS_LOCK       0x3A
#define SCAN_NUM_LOCK        0x45
#define BIOS_KB_FLAG_ADDR    0x417
#define BIOS_KB_FLAG_CAPS    0x40
#define BIOS_KB_FLAG_NUM     0x20

// Numeric keypad scan range (set 1). With num-lock active these type
// digits and '.'; the grey arrow cluster shares the codes but arrives
// behind an 0xE0 prefix, which the ISR tracks to keep them apart.
#define SCAN_KEYPAD_FIRST    0x47
#define SCAN_KEYPAD_LAST     0x53

// AT keyboards bracket extended keys with fake-shift sequences for XT
// compatibility: 0xE0 0x2A / 0xE0 0xAA (fake left shift), and with
// right shift physically held, 0xE0 0xB6 / 0xE0 0x36 (fake right
// shift). All four decode to these codes after the break bit is
// masked and must be ignored, or the fakes corrupt the real shift
// state while an extended key is held.
#define SCAN_FAKE_LSHIFT     0x2A
#define SCAN_FAKE_RSHIFT     0x36

// Pause sends 0xE1 0x1D 0x45 / 0xE1 0x9D 0xC5 -- an 0xE1 prefix plus
// two payload bytes that would otherwise read as Ctrl and NumLock
// events. Swallow the payload to keep the tracked lock state honest.
#define SCAN_PAUSE_PREFIX    0xE1
#define PAUSE_PAYLOAD_BYTES  2

// Extended two-byte keys that still type a character: keypad Enter
// (0xE0 0x1C) and keypad '/' (0xE0 0x35).
#define SCAN_ENTER           0x1C
#define SCAN_SLASH           0x35

// Private ISR->poll ring for translated characters. Power of two so
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
#define ISR_CHAR_QUEUE_SIZE  32

// INT 33h mouse driver functions and button bits.
#define MOUSE_INT             0x33
#define MOUSE_FN_RESET        0x0000
#define MOUSE_FN_GET_STATE    0x0003
#define MOUSE_FN_SET_X_RANGE  0x0007
#define MOUSE_FN_SET_Y_RANGE  0x0008
#define MOUSE_FN_SET_POS      0x0004
#define MOUSE_BTN_LEFT        0x0001
#define MOUSE_BTN_RIGHT       0x0002
#define MOUSE_BTN_MIDDLE      0x0004

// INT 15h game-port BIOS functions:
//   AH=84h, DX=0: read switches  (AL bits 4-7 = buttons, 0=pressed)
//   AH=84h, DX=1: read positions (AX/BX/CX/DX = stickA X/Y, stickB X/Y)
// The position values are raw R-C timer counts; magnitude depends on
// the joystick's potentiometers and the host BIOS calibration. We
// threshold around a nominal center to derive digital direction; for
// DOSBox-mapped sticks 128/384 covers the default emulated range and
// gives clean L/R/U/D snaps.
#define BIOS_INT                  0x15
#define BIOS_FN_GAMEPORT          0x84
#define BIOS_GAMEPORT_DX_SWITCHES 0x0000
#define BIOS_GAMEPORT_DX_POS      0x0001
#define BIOS_FLAGS_CARRY          0x0001
#define BIOS_BTN_A1               0x10
#define BIOS_BTN_A2               0x20
#define BIOS_BTN_B1               0x40
#define BIOS_BTN_B2               0x80
#define JOY_AXIS_LO_THRESHOLD     128
#define JOY_AXIS_HI_THRESHOLD     384

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

static bool   joystickInit(void);
static void   joystickPoll(void);
static void   keyboardIsr(void);
static bool   mouseInit(void);
static void   mousePoll(void);
static int8_t scaleAxis(uint16_t v);

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

static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = {
    [0x01] = KEY_ESCAPE,
    [0x02] = KEY_1,
    [0x03] = KEY_2,
    [0x04] = KEY_3,
    [0x05] = KEY_4,
    [0x06] = KEY_5,
    [0x07] = KEY_6,
    [0x08] = KEY_7,
    [0x09] = KEY_8,
    [0x0A] = KEY_9,
    [0x0B] = KEY_0,
    [0x0E] = KEY_BACKSPACE,
    [0x0F] = KEY_TAB,
    [0x10] = KEY_Q,
    [0x11] = KEY_W,
    [0x12] = KEY_E,
    [0x13] = KEY_R,
    [0x14] = KEY_T,
    [0x15] = KEY_Y,
    [0x16] = KEY_U,
    [0x17] = KEY_I,
    [0x18] = KEY_O,
    [0x19] = KEY_P,
    [0x1C] = KEY_RETURN,
    [0x1D] = KEY_LCTRL,
    [0x1E] = KEY_A,
    [0x1F] = KEY_S,
    [0x20] = KEY_D,
    [0x21] = KEY_F,
    [0x22] = KEY_G,
    [0x23] = KEY_H,
    [0x24] = KEY_J,
    [0x25] = KEY_K,
    [0x26] = KEY_L,
    [0x2A] = KEY_LSHIFT,
    [0x2C] = KEY_Z,
    [0x2D] = KEY_X,
    [0x2E] = KEY_C,
    [0x2F] = KEY_V,
    [0x30] = KEY_B,
    [0x31] = KEY_N,
    [0x32] = KEY_M,
    [0x36] = KEY_RSHIFT,
    [0x38] = KEY_LALT,
    [0x39] = KEY_SPACE,
    [0x3B] = KEY_F1,
    [0x3C] = KEY_F2,
    [0x3D] = KEY_F3,
    [0x3E] = KEY_F4,
    [0x3F] = KEY_F5,
    [0x40] = KEY_F6,
    [0x41] = KEY_F7,
    [0x42] = KEY_F8,
    [0x43] = KEY_F9,
    [0x44] = KEY_F10,
    [0x48] = KEY_UP,
    [0x4B] = KEY_LEFT,
    [0x4D] = KEY_RIGHT,
    [0x50] = KEY_DOWN,
};

// US-layout set-1 scancode -> ASCII, normal and shifted. Only make
// codes reach these; slots left at 0 produce no character. Letters
// are lowercase here -- the ISR flips to the shifted table when the
// effective shift (shift XOR caps, letters only) is active.
static const uint8_t gScanAsciiNormal[SCAN_TABLE_SIZE] = {
    [0x01] = JL_CHAR_ESCAPE,
    [0x02] = '1',  [0x03] = '2', [0x04] = '3',  [0x05] = '4',
    [0x06] = '5',  [0x07] = '6', [0x08] = '7',  [0x09] = '8',
    [0x0A] = '9',  [0x0B] = '0', [0x0C] = '-',  [0x0D] = '=',
    [0x0E] = JL_CHAR_BACKSPACE, [0x0F] = JL_CHAR_TAB,
    [0x10] = 'q',  [0x11] = 'w', [0x12] = 'e',  [0x13] = 'r',
    [0x14] = 't',  [0x15] = 'y', [0x16] = 'u',  [0x17] = 'i',
    [0x18] = 'o',  [0x19] = 'p', [0x1A] = '[',  [0x1B] = ']',
    [0x1C] = JL_CHAR_RETURN,
    [0x1E] = 'a',  [0x1F] = 's', [0x20] = 'd',  [0x21] = 'f',
    [0x22] = 'g',  [0x23] = 'h', [0x24] = 'j',  [0x25] = 'k',
    [0x26] = 'l',  [0x27] = ';', [0x28] = '\'', [0x29] = '`',
    [0x2B] = '\\',
    [0x2C] = 'z',  [0x2D] = 'x', [0x2E] = 'c',  [0x2F] = 'v',
    [0x30] = 'b',  [0x31] = 'n', [0x32] = 'm',  [0x33] = ',',
    [0x34] = '.',  [0x35] = '/',
    [0x37] = '*',  [0x39] = ' ',
    [0x4A] = '-',  [0x4E] = '+',
};

static const uint8_t gScanAsciiShift[SCAN_TABLE_SIZE] = {
    [0x01] = JL_CHAR_ESCAPE,
    [0x02] = '!',  [0x03] = '@', [0x04] = '#',  [0x05] = '$',
    [0x06] = '%',  [0x07] = '^', [0x08] = '&',  [0x09] = '*',
    [0x0A] = '(',  [0x0B] = ')', [0x0C] = '_',  [0x0D] = '+',
    [0x0E] = JL_CHAR_BACKSPACE, [0x0F] = JL_CHAR_TAB,
    [0x10] = 'Q',  [0x11] = 'W', [0x12] = 'E',  [0x13] = 'R',
    [0x14] = 'T',  [0x15] = 'Y', [0x16] = 'U',  [0x17] = 'I',
    [0x18] = 'O',  [0x19] = 'P', [0x1A] = '{',  [0x1B] = '}',
    [0x1C] = JL_CHAR_RETURN,
    [0x1E] = 'A',  [0x1F] = 'S', [0x20] = 'D',  [0x21] = 'F',
    [0x22] = 'G',  [0x23] = 'H', [0x24] = 'J',  [0x25] = 'K',
    [0x26] = 'L',  [0x27] = ':', [0x28] = '"',  [0x29] = '~',
    [0x2B] = '|',
    [0x2C] = 'Z',  [0x2D] = 'X', [0x2E] = 'C',  [0x2F] = 'V',
    [0x30] = 'B',  [0x31] = 'N', [0x32] = 'M',  [0x33] = '<',
    [0x34] = '>',  [0x35] = '?',
    [0x37] = '*',  [0x39] = ' ',
    [0x4A] = '-',  [0x4E] = '+',
};

// Keypad digits/'.' typed when num-lock is active (0x47..0x53). The
// '-'/'+' slots stay 0: those keys type regardless of num-lock and
// live in the main tables instead.
static const uint8_t gKeypadAscii[SCAN_KEYPAD_LAST - SCAN_KEYPAD_FIRST + 1] = {
    '7', '8', '9', 0, '4', '5', '6', 0, '1', '2', '3', '0', '.'
};

static _go32_dpmi_seginfo gOldHandler;
static _go32_dpmi_seginfo gNewHandler;
static bool               gHooked    = false;
// uint8_t (not bool) so element size matches gKeyState's. See
// src/core/inputInternal.h for the full rationale.
static volatile uint8_t   gIsrState[KEY_COUNT];

// ISR-tracked lock/prefix state. uint8_t (not bool) so the locked-data
// regions have known one-byte elements.
static volatile uint8_t   gIsrExtended = 0;   // last byte was 0xE0
static volatile uint8_t   gIsrE1Skip   = 0;   // Pause payload bytes left to swallow
static volatile uint8_t   gIsrCapsLock = 0;
static volatile uint8_t   gIsrCapsHeld = 0;   // suppress typematic re-toggle
static volatile uint8_t   gIsrNumLock  = 0;
static volatile uint8_t   gIsrNumHeld  = 0;

// Translated characters, ISR producer -> poll consumer. Single-
// producer single-consumer: the ISR only writes head + slots, the
// poll drain only writes tail, so no interrupt masking is needed.
static volatile uint8_t   gIsrCharQueue[ISR_CHAR_QUEUE_SIZE];
static volatile uint8_t   gIsrCharHead = 0;
static volatile uint8_t   gIsrCharTail = 0;

static bool gMousePresent    = false;
static bool gJoystickPresent = false;

// ----- Internal helpers -----

static void keyboardIsr(void) {
    uint8_t scan;
    uint8_t code;
    uint8_t key;
    uint8_t extended;
    uint8_t ch;
    uint8_t next;
    bool    isBreak;
    bool    useShift;

    scan = inportb(KB_DATA_PORT);

    // Pause's 0xE1-prefixed payload would otherwise read as Ctrl and
    // NumLock events (0x1D 0x45 / 0x9D 0xC5) -- swallow it whole.
    if (gIsrE1Skip != 0) {
        gIsrE1Skip = (uint8_t)(gIsrE1Skip - 1);
        outportb(PIC_CMD_PORT, PIC_EOI);
        return;
    }
    if (scan == SCAN_PAUSE_PREFIX) {
        gIsrE1Skip = PAUSE_PAYLOAD_BYTES;
        outportb(PIC_CMD_PORT, PIC_EOI);
        return;
    }

    if (scan == SCAN_EXTENDED) {
        gIsrExtended = 1;
        outportb(PIC_CMD_PORT, PIC_EOI);
        return;
    }
    extended     = gIsrExtended;
    gIsrExtended = 0;

    isBreak = (scan & SCAN_BREAK_BIT) != 0;
    code    = (uint8_t)(scan & SCAN_CODE_MASK);

    // Fake shifts bracket extended keys (arrows etc.) on AT keyboards;
    // treating them as real shift makes would corrupt the live shift
    // state while an arrow is held. Drop both variants entirely.
    if (extended && (code == SCAN_FAKE_LSHIFT || code == SCAN_FAKE_RSHIFT)) {
        outportb(PIC_CMD_PORT, PIC_EOI);
        return;
    }

    key = gScanToKey[code];
    if (key != KEY_NONE) {
        gIsrState[key] = !isBreak;
    }

    if (!isBreak) {
        // Lock-key toggles fire once per press; typematic repeat of a
        // held lock key must not flap the state (gIsr*Held mirrors the
        // BIOS's held-bit for exactly this).
        if (code == SCAN_CAPS_LOCK) {
            if (!gIsrCapsHeld) {
                gIsrCapsLock = (uint8_t)!gIsrCapsLock;
            }
            gIsrCapsHeld = 1;
        } else if (code == SCAN_NUM_LOCK) {
            if (!gIsrNumHeld) {
                gIsrNumLock = (uint8_t)!gIsrNumLock;
            }
            gIsrNumHeld = 1;
        } else if (!gIsrState[KEY_LCTRL] && !gIsrState[KEY_LALT]) {
            // Typed-character translation. Ctrl/Alt chords are
            // commands, not text, so they produce no character.
            ch = 0;
            if (extended) {
                // Of the 0xE0 pairs only keypad Enter and keypad '/'
                // type; the rest (arrows, Home/End, ...) do not.
                if (code == SCAN_ENTER || code == SCAN_SLASH) {
                    ch = gScanAsciiNormal[code];
                }
            } else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST &&
                       gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) {
                ch = gKeypadAscii[code - SCAN_KEYPAD_FIRST];
            } else {
                useShift = gIsrState[KEY_LSHIFT] || gIsrState[KEY_RSHIFT];
                if (gIsrCapsLock &&
                    gScanAsciiNormal[code] >= 'a' && gScanAsciiNormal[code] <= 'z') {
                    useShift = !useShift;
                }
                ch = useShift ? gScanAsciiShift[code] : gScanAsciiNormal[code];
            }
            if (ch != 0) {
                next = (uint8_t)((gIsrCharHead + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
                if (next != gIsrCharTail) {
                    gIsrCharQueue[gIsrCharHead] = ch;
                    gIsrCharHead                = next;
                }
            }
        }
    } else {
        if (code == SCAN_CAPS_LOCK) {
            gIsrCapsHeld = 0;
        } else if (code == SCAN_NUM_LOCK) {
            gIsrNumHeld = 0;
        }
    }

    outportb(PIC_CMD_PORT, PIC_EOI);
}


// Reset the mouse driver, detect presence, clamp coordinate range to
// the surface, and center the pointer. AX=0xFFFF after reset means a
// driver is loaded; anything else means no mouse and we leave gMouse*
// at their zero defaults.
static bool mouseInit(void) {
    __dpmi_regs r;

    memset(&r, 0, sizeof(r));
    r.x.ax = MOUSE_FN_RESET;
    __dpmi_int(MOUSE_INT, &r);
    if (r.x.ax != 0xFFFF) {
        return false;
    }

    memset(&r, 0, sizeof(r));
    r.x.ax = MOUSE_FN_SET_X_RANGE;
    r.x.cx = 0;
    r.x.dx = SURFACE_WIDTH - 1;
    __dpmi_int(MOUSE_INT, &r);

    memset(&r, 0, sizeof(r));
    r.x.ax = MOUSE_FN_SET_Y_RANGE;
    r.x.cx = 0;
    r.x.dx = SURFACE_HEIGHT - 1;
    __dpmi_int(MOUSE_INT, &r);

    memset(&r, 0, sizeof(r));
    r.x.ax = MOUSE_FN_SET_POS;
    r.x.cx = SURFACE_WIDTH  / 2;
    r.x.dx = SURFACE_HEIGHT / 2;
    __dpmi_int(MOUSE_INT, &r);

    return true;
}


static int8_t scaleAxis(uint16_t v) {
    if (v < JOY_AXIS_LO_THRESHOLD) {
        return JOYSTICK_AXIS_MIN;
    }
    if (v > JOY_AXIS_HI_THRESHOLD) {
        return JOYSTICK_AXIS_MAX;
    }
    return 0;
}


// Detect a game-port BIOS handler. Some BIOSes return CF=1 for "no
// joystick port" -- we treat that as no joystick. DOSBox-Staging with
// any host stick mapped will return CF=0.
static bool joystickInit(void) {
    __dpmi_regs r;

    memset(&r, 0, sizeof(r));
    r.h.ah = BIOS_FN_GAMEPORT;
    r.x.dx = BIOS_GAMEPORT_DX_SWITCHES;
    __dpmi_int(BIOS_INT, &r);
    if (r.x.flags & BIOS_FLAGS_CARRY) {
        return false;
    }
    return true;
}


static void joystickPoll(void) {
    __dpmi_regs r;
    uint8_t     btn;

    if (!gJoystickPresent) {
        return;
    }

    memset(&r, 0, sizeof(r));
    r.h.ah = BIOS_FN_GAMEPORT;
    r.x.dx = BIOS_GAMEPORT_DX_SWITCHES;
    __dpmi_int(BIOS_INT, &r);
    if (r.x.flags & BIOS_FLAGS_CARRY) {
        gJoystickPresent = false;
        return;
    }
    btn = r.h.al;
    // Active-low: bit set in AL means button is *not* pressed.
    gJoyButtonState[JOYSTICK_0][JOY_BUTTON_0] = (btn & BIOS_BTN_A1) == 0;
    gJoyButtonState[JOYSTICK_0][JOY_BUTTON_1] = (btn & BIOS_BTN_A2) == 0;
    gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0;
    gJoyButtonState[JOYSTICK_1][JOY_BUTTON_1] = (btn & BIOS_BTN_B2) == 0;

    // Announce presence from the successful switches read that produced
    // live button state, independent of the axis decode below -- matches
    // the ST/IIgs convention. Doing it here (not after the position read)
    // keeps connected status consistent with the button state.
    gJoyConnected[JOYSTICK_0] = true;
    gJoyConnected[JOYSTICK_1] = true;

    memset(&r, 0, sizeof(r));
    r.h.ah = BIOS_FN_GAMEPORT;
    r.x.dx = BIOS_GAMEPORT_DX_POS;
    __dpmi_int(BIOS_INT, &r);
    if (r.x.flags & BIOS_FLAGS_CARRY) {
        // A failed position read leaves the stick connected (buttons are
        // live), but report centered axes rather than stale values.
        gJoyAxisX[JOYSTICK_0] = 0;
        gJoyAxisY[JOYSTICK_0] = 0;
        gJoyAxisX[JOYSTICK_1] = 0;
        gJoyAxisY[JOYSTICK_1] = 0;
        return;
    }
    gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax);
    gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx);
    gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx);
    gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx);
}


static void mousePoll(void) {
    __dpmi_regs r;
    uint16_t    btn;

    if (!gMousePresent) {
        return;
    }
    memset(&r, 0, sizeof(r));
    r.x.ax = MOUSE_FN_GET_STATE;
    __dpmi_int(MOUSE_INT, &r);
    // Assumes a mode-13h-aware driver (DOSBox / DOSBox-X) that returns
    // native 0..SURFACE_WIDTH-1 X. Some real MS-compatible drivers keep
    // an internal 640-wide virtual space in mode 13h where fn 7/8 only
    // clamp min/max (not scale), so X comes back doubled and would need
    // a >>1; that scale-detection is not done here. Y is unaffected
    // (only X is virtualized in 320-wide modes).
    gMouseX = (int16_t)r.x.cx;
    gMouseY = (int16_t)r.x.dx;
    btn     = r.x.bx;
    gMouseButtonState[MOUSE_BUTTON_LEFT]   = (btn & MOUSE_BTN_LEFT)   != 0;
    gMouseButtonState[MOUSE_BUTTON_RIGHT]  = (btn & MOUSE_BTN_RIGHT)  != 0;
    gMouseButtonState[MOUSE_BUTTON_MIDDLE] = (btn & MOUSE_BTN_MIDDLE) != 0;
}


// ----- HAL API (alphabetical) -----

void jlpJoystickReset(jlJoystickE js) {
    // DOS sticks are digital -- no calibration to do.
    (void)js;
}


void jlpInputInit(void) {
    uint8_t kbFlags;

    memset(gKeyState,          0, sizeof(gKeyState));
    memset(gKeyPrev,           0, sizeof(gKeyPrev));
    memset((void *)gIsrState,  0, sizeof(gIsrState));

    // Seed the lock toggles from the BIOS shift-flag byte -- the last
    // state the BIOS saw before we take INT 9 away from it.
    kbFlags      = _farpeekb(_dos_ds, BIOS_KB_FLAG_ADDR);
    gIsrCapsLock = (kbFlags & BIOS_KB_FLAG_CAPS) != 0;
    gIsrNumLock  = (kbFlags & BIOS_KB_FLAG_NUM)  != 0;
    gIsrExtended = 0;
    gIsrE1Skip   = 0;
    gIsrCapsHeld = 0;
    gIsrNumHeld  = 0;
    gIsrCharHead = 0;
    gIsrCharTail = 0;

    _go32_dpmi_lock_code(keyboardIsr, ISR_LOCK_SIZE);
    _go32_dpmi_lock_data((void *)gScanToKey,        sizeof(gScanToKey));
    _go32_dpmi_lock_data((void *)gIsrState,         sizeof(gIsrState));
    _go32_dpmi_lock_data((void *)gScanAsciiNormal,  sizeof(gScanAsciiNormal));
    _go32_dpmi_lock_data((void *)gScanAsciiShift,   sizeof(gScanAsciiShift));
    _go32_dpmi_lock_data((void *)gKeypadAscii,      sizeof(gKeypadAscii));
    _go32_dpmi_lock_data((void *)gIsrCharQueue,     sizeof(gIsrCharQueue));
    _go32_dpmi_lock_data((void *)&gIsrExtended,     sizeof(gIsrExtended));
    _go32_dpmi_lock_data((void *)&gIsrE1Skip,       sizeof(gIsrE1Skip));
    _go32_dpmi_lock_data((void *)&gIsrCapsLock,     sizeof(gIsrCapsLock));
    _go32_dpmi_lock_data((void *)&gIsrCapsHeld,     sizeof(gIsrCapsHeld));
    _go32_dpmi_lock_data((void *)&gIsrNumLock,      sizeof(gIsrNumLock));
    _go32_dpmi_lock_data((void *)&gIsrNumHeld,      sizeof(gIsrNumHeld));
    _go32_dpmi_lock_data((void *)&gIsrCharHead,     sizeof(gIsrCharHead));
    _go32_dpmi_lock_data((void *)&gIsrCharTail,     sizeof(gIsrCharTail));

    _go32_dpmi_get_protected_mode_interrupt_vector(9, &gOldHandler);

    gNewHandler.pm_offset   = (unsigned long)keyboardIsr;
    gNewHandler.pm_selector = _go32_my_cs();
    if (_go32_dpmi_allocate_iret_wrapper(&gNewHandler) != 0) {
        return;
    }
    _go32_dpmi_set_protected_mode_interrupt_vector(9, &gNewHandler);
    gHooked = true;

    gMousePresent    = mouseInit();
    gMouseAttached   = gMousePresent;
    gJoystickPresent = joystickInit();
}


void jlpInputPoll(void) {
    uint8_t tail;

    disable();
    memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
    enable();

    // Drain ISR-translated characters into the core queue. Single-
    // consumer side of the SPSC ring: only gIsrCharTail is written
    // here, so the ISR may keep producing concurrently.
    tail = gIsrCharTail;
    while (tail != gIsrCharHead) {
        jlInputCharPush(gIsrCharQueue[tail]);
        tail = (uint8_t)((tail + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
    }
    gIsrCharTail = tail;

    mousePoll();
    joystickPoll();
}


void jlpInputShutdown(void) {
    if (!gHooked) {
        return;
    }
    _go32_dpmi_set_protected_mode_interrupt_vector(9, &gOldHandler);
    _go32_dpmi_free_iret_wrapper(&gNewHandler);
    gHooked = false;
}
