// 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.
//
// 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 "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

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

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

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

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

static void keyboardIsr(void) {
    uint8_t scan;
    uint8_t code;
    uint8_t key;
    bool    isBreak;

    scan = inportb(KB_DATA_PORT);

    if (scan != SCAN_EXTENDED) {
        isBreak = (scan & SCAN_BREAK_BIT) != 0;
        code    = (uint8_t)(scan & SCAN_CODE_MASK);
        key     = gScanToKey[code];
        if (key != KEY_NONE) {
            gIsrState[key] = !isBreak;
        }
    }

    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) {
    memset(gKeyState,          0, sizeof(gKeyState));
    memset(gKeyPrev,           0, sizeof(gKeyPrev));
    memset((void *)gIsrState,  0, sizeof(gIsrState));

    _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_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();
    gJoystickPresent = joystickInit();
}


void jlpInputPoll(void) {
    disable();
    memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
    enable();
    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;
}
