// Atari ST keyboard + mouse input: replaces the TOS ikbdsys vector in
// the KBDVECS table so every byte from the IKBD ACIA comes through our
// packet-aware handler.
//
// The IKBD protocol mixes keyboard scan codes with mouse/joy packets.
// Bytes 0x00-0x72 (with optional 0x80 break bit -> 0x80..0xF2) are
// keyboard scan codes; 0xF6-0xFF are packet headers that announce a
// fixed number of trailing bytes. We dispatch based on header:
//
//   0xF8..0xFB  Relative mouse packet (3 bytes total). The header's
//               low two bits are the button state (bit 1 = left,
//               bit 0 = right). Next two bytes are signed dx, dy.
//   Other       Discarded (joystick / clock / status etc.) -- we just
//               consume the documented byte count to stay in sync.
//
// The ISR writes into private gIsrState (keyboard) and gIsrMouse*
// buffers; jlpInputPoll copies them into the public globals.
// jlKeyPressed edge detection requires that public gKeyState only
// advance during jlpInputPoll, never at interrupt time -- jlInputPoll
// snapshots gKeyState into gKeyPrev before jlpInputPoll runs.
//
// Typed characters: replacing ikbdsys means TOS's own keyboard
// processing never runs, so Bconin never sees our keys. Instead the
// ISR translates make codes with the TOS keyboard tables from
// Keytbl() -- the OS keymap, so national layouts are honored -- into
// a private ring that jlpInputPoll drains into the core typed-
// character queue. Shift/caps come from the ISR's own key state
// (caps-lock toggles on scan 0x3A); Ctrl chords are commands, not
// text, and produce no character. The IKBD sends one make per press
// (TOS's software auto-repeat is gone with ikbdsys), so held keys do
// not repeat characters.

#include <string.h>

#include <mint/osbind.h>
#include <mint/ostruct.h>

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

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

// IKBD ACIA data register; reading also clears the ACIA's interrupt
// so the MFP ACIA line drops.
#define ST_ACIA_DATA        ((volatile uint8_t *)0xFFFFFC02L)

#define SCAN_BREAK_BIT      0x80
#define SCAN_CODE_MASK      0x7F
#define SCAN_TABLE_SIZE     128
#define SCAN_CAPS_LOCK      0x3A

// Cursor/edit cluster scan codes. Their UNSHIFTED keytable entries are
// 0 (no character), but TOS's SHIFT table maps them to legacy numeric
// digits ('8' for shift+Up etc.), so the typed-character path must
// skip them outright. Keypad '-' (0x4A) and '+' (0x4E) sit between
// these codes and DO type.
#define SCAN_CLR_HOME       0x47
#define SCAN_CURSOR_UP      0x48
#define SCAN_CURSOR_LEFT    0x4B
#define SCAN_CURSOR_RIGHT   0x4D
#define SCAN_CURSOR_DOWN    0x50
#define SCAN_INSERT         0x52

// Kbshift(-1) state bits (TOS BIOS): bit 4 = caps lock engaged.
#define KBSHIFT_CAPS_BIT    0x10

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

#define PKT_STATUS          0xF6    // + 7 bytes
#define PKT_ABS_MOUSE       0xF7    // + 5 bytes
#define PKT_REL_MOUSE_MIN   0xF8    // 0xF8..0xFB + 2 bytes
#define PKT_REL_MOUSE_MAX   0xFB
#define PKT_TIME            0xFC    // + 6 bytes
#define PKT_JOY_BOTH        0xFD    // + 2 bytes
#define PKT_JOY0            0xFE    // + 1 byte
#define PKT_JOY1            0xFF    // + 1 byte

// Bit layout of the IKBD relative-mouse packet header:
#define MOUSE_HDR_LEFT_BTN  0x02
#define MOUSE_HDR_RIGHT_BTN 0x01

// IKBD joystick byte: bit set = direction held / fire pressed.
#define JOY_BIT_UP          0x01
#define JOY_BIT_DOWN        0x02
#define JOY_BIT_LEFT        0x04
#define JOY_BIT_RIGHT       0x08
#define JOY_BIT_FIRE        0x80

// Packet kinds for the ISR's small state machine.
#define PKT_KIND_NONE       0
#define PKT_KIND_DISCARD    1
#define PKT_KIND_REL_MOUSE  2
#define PKT_KIND_JOY0       3   // 0xFE -- 1 byte for ST joy port 0 (mouse port)
#define PKT_KIND_JOY1       4   // 0xFF -- 1 byte for ST joy port 1 (dedicated)
#define PKT_KIND_JOY_BOTH   5   // 0xFD -- 2 bytes (joy0 then joy1)

// Mapping from ST joystick port -> jlJoystickE. Port 1 is the
// dedicated stick that retro games use; expose it as the primary
// JOYSTICK_0. Port 0 (mouse port) becomes JOYSTICK_1.
#define ST_PORT0_AS_JS      JOYSTICK_1
#define ST_PORT1_AS_JS      JOYSTICK_0

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

static long ikbdHandler(void);
static long patchIkbdVector(void);
static long restoreIkbdVector(void);
static bool scanIsCursorCluster(uint8_t code);

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

// ST IKBD scan codes. Identical encoding to the PC AT set-1 for most
// keys, which is why the table mirrors the DOS scan map.
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 _KBDVECS        *gKbdvecs     = NULL;
static long           (*gOldIkbdsys)(void) = NULL;
static volatile uint8_t gPacketRemaining   = 0;
static volatile uint8_t gPacketKind        = PKT_KIND_NONE;
static volatile uint8_t gMousePacketByte   = 0;  // bytes consumed in current packet
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];

// Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the
// poll routine clamps the running absolute position into the surface
// rectangle. Buttons are latched in the header byte and stay live in
// gIsrMouseButtons until the next packet rewrites them.
static volatile int32_t gIsrMouseDx       = 0;
static volatile int32_t gIsrMouseDy       = 0;
static volatile uint8_t gIsrMouseButtons  = 0;
static          int16_t gMouseAbsX        = SURFACE_WIDTH  / 2;
static          int16_t gMouseAbsY        = SURFACE_HEIGHT / 2;

// Latched joystick state, written by the IKBD ISR. The byte stays
// unchanged between packets while a direction or fire is held, so
// jlpInputPoll can simply read the latest value.
static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT];

// TOS keyboard translation tables (Keytbl), cached at init so the ISR
// only does array lookups. 128 bytes each, indexed by scan code; 0
// means the key types nothing.
static const uint8_t *gKeyTabUnshift = NULL;
static const uint8_t *gKeyTabShift   = NULL;
static const uint8_t *gKeyTabCaps    = NULL;

// ISR-tracked caps-lock toggle (scan 0x3A make flips it). uint8_t so
// the element size is pinned like the other ISR state.
static volatile uint8_t gIsrCapsLock = 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;

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

// Runs in MFP ACIA interrupt context. Reads one byte from the ACIA,
// dispatches to either keyboard handling, mouse-packet capture, or
// "discard remaining N bytes" for packets we do not yet care about.
static long ikbdHandler(void) {
    const uint8_t *tab;
    uint8_t        byte;
    uint8_t        code;
    uint8_t        key;
    uint8_t        ch;
    uint8_t        next;
    bool           isBreak;

    byte = *ST_ACIA_DATA;

    if (gPacketRemaining != 0) {
        switch (gPacketKind) {
            case PKT_KIND_REL_MOUSE:
                // mouse-packet payload: byte 0 = dx, byte 1 = dy
                if (gMousePacketByte == 0) {
                    gIsrMouseDx     += (int8_t)byte;
                    gMousePacketByte = 1;
                } else {
                    gIsrMouseDy     += (int8_t)byte;
                    gMousePacketByte = 0;
                }
                break;
            case PKT_KIND_JOY0:
                gIsrJoyByte[ST_PORT0_AS_JS] = byte;
                break;
            case PKT_KIND_JOY1:
                gIsrJoyByte[ST_PORT1_AS_JS] = byte;
                break;
            case PKT_KIND_JOY_BOTH:
                // 0xFD payload is joy0 then joy1.
                if (gMousePacketByte == 0) {
                    gIsrJoyByte[ST_PORT0_AS_JS] = byte;
                    gMousePacketByte             = 1;
                } else {
                    gIsrJoyByte[ST_PORT1_AS_JS] = byte;
                    gMousePacketByte             = 0;
                }
                break;
            default:
                break;
        }
        gPacketRemaining = (uint8_t)(gPacketRemaining - 1);
        if (gPacketRemaining == 0) {
            gPacketKind = PKT_KIND_NONE;
        }
        return 0;
    }

    if (byte >= PKT_STATUS) {
        gPacketKind      = PKT_KIND_DISCARD;
        gMousePacketByte = 0;
        switch (byte) {
            case PKT_STATUS:
                gPacketRemaining = 7;
                break;
            case PKT_ABS_MOUSE:
                gPacketRemaining = 5;
                break;
            case PKT_TIME:
                gPacketRemaining = 6;
                break;
            case PKT_JOY_BOTH:
                gPacketRemaining = 2;
                gPacketKind      = PKT_KIND_JOY_BOTH;
                break;
            case PKT_JOY0:
                gPacketRemaining = 1;
                gPacketKind      = PKT_KIND_JOY0;
                break;
            case PKT_JOY1:
                gPacketRemaining = 1;
                gPacketKind      = PKT_KIND_JOY1;
                break;
            default:
                if (byte >= PKT_REL_MOUSE_MIN && byte <= PKT_REL_MOUSE_MAX) {
                    gPacketRemaining = 2;
                    gPacketKind      = PKT_KIND_REL_MOUSE;
                    gIsrMouseButtons = (uint8_t)(byte & (MOUSE_HDR_LEFT_BTN | MOUSE_HDR_RIGHT_BTN));
                } else {
                    gPacketKind = PKT_KIND_NONE;
                }
                break;
        }
        return 0;
    }

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

    if (!isBreak) {
        if (code == SCAN_CAPS_LOCK) {
            gIsrCapsLock = (uint8_t)!gIsrCapsLock;
        } else if (!gIsrState[KEY_LCTRL] && !gIsrState[KEY_LALT] &&
                   gKeyTabUnshift != NULL && !scanIsCursorCluster(code)) {
            // Typed-character translation via the TOS keymap. Table
            // priority mirrors TOS: shift wins over caps, caps over
            // plain. A 0 entry types nothing (F-keys etc.); Ctrl/Alt
            // chords are commands, not text.
            if (gIsrState[KEY_LSHIFT] || gIsrState[KEY_RSHIFT]) {
                tab = gKeyTabShift;
            } else if (gIsrCapsLock) {
                tab = gKeyTabCaps;
            } else {
                tab = gKeyTabUnshift;
            }
            ch = tab[code];
            if (ch != 0) {
                next = (uint8_t)((gIsrCharHead + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
                if (next != gIsrCharTail) {
                    gIsrCharQueue[gIsrCharHead] = ch;
                    gIsrCharHead                = next;
                }
            }
        }
    }
    return 0;
}


static long patchIkbdVector(void) {
    gOldIkbdsys       = gKbdvecs->ikbdsys;
    gKbdvecs->ikbdsys = ikbdHandler;
    return 0;
}


static long restoreIkbdVector(void) {
    gKbdvecs->ikbdsys = gOldIkbdsys;
    return 0;
}


static bool scanIsCursorCluster(uint8_t code) {
    return code == SCAN_CLR_HOME  || code == SCAN_CURSOR_UP   ||
           code == SCAN_CURSOR_LEFT || code == SCAN_CURSOR_RIGHT ||
           code == SCAN_CURSOR_DOWN || code == SCAN_INSERT;
}


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

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


void jlpInputInit(void) {
    _KEYTAB *keyTab;

    gMouseAttached = true;             // the IKBD mouse line is always wired on an ST
    memset(gKeyState,            0, sizeof(gKeyState));
    memset(gKeyPrev,             0, sizeof(gKeyPrev));
    memset((void *)gIsrState,    0, sizeof(gIsrState));
    memset((void *)gIsrJoyByte,  0, sizeof(gIsrJoyByte));

    // Cache the TOS keyboard translation tables (the OS keymap) for
    // the ISR's typed-character path. -1 leaves the tables unchanged
    // and just returns the pointers.
    keyTab = (_KEYTAB *)Keytbl((void *)-1L, (void *)-1L, (void *)-1L);
    if (keyTab != NULL) {
        gKeyTabUnshift = (const uint8_t *)keyTab->unshift;
        gKeyTabShift   = (const uint8_t *)keyTab->shift;
        gKeyTabCaps    = (const uint8_t *)keyTab->caps;
    }
    // Seed caps lock from TOS: taking over ikbdsys freezes the OS's
    // own tracking, so an engaged caps lock at launch would otherwise
    // be invisible until the user toggles it once.
    gIsrCapsLock = (Kbshift(-1) & KBSHIFT_CAPS_BIT) != 0;
    gIsrCharHead = 0;
    gIsrCharTail = 0;

    gMouseAbsX        = SURFACE_WIDTH  / 2;
    gMouseAbsY        = SURFACE_HEIGHT / 2;
    gMouseX           = gMouseAbsX;
    gMouseY           = gMouseAbsY;
    gIsrMouseDx       = 0;
    gIsrMouseDy       = 0;
    gIsrMouseButtons  = 0;

    // Both ports always announce themselves on the ST -- packets
    // arrive even when no stick is plugged in (state stays at zero).
    gJoyConnected[JOYSTICK_0] = true;
    gJoyConnected[JOYSTICK_1] = true;

    gKbdvecs         = (_KBDVECS *)Kbdvbase();
    gPacketRemaining = 0;
    gPacketKind      = PKT_KIND_NONE;
    Supexec(patchIkbdVector);
    gHooked = true;
}


// The ACIA ISR writes gIsrState (keys), gIsrMouse* deltas, and
// gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially
// never racing a write. Worst case is a single key or one packet
// lagging one frame -- well under perceptible.
void jlpInputPoll(void) {
    int32_t dx;
    int32_t dy;
    int32_t newX;
    int32_t newY;
    uint8_t btn;
    uint8_t joy;
    uint8_t tail;
    uint16_t i;

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

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

    // Drain accumulated mouse deltas + latch button state.
    dx               = gIsrMouseDx;
    dy               = gIsrMouseDy;
    btn              = gIsrMouseButtons;
    gIsrMouseDx      = 0;
    gIsrMouseDy      = 0;

    newX = (int32_t)gMouseAbsX + dx;
    newY = (int32_t)gMouseAbsY + dy;
    if (newX < 0)                  { newX = 0; }
    if (newX > SURFACE_WIDTH  - 1) { newX = SURFACE_WIDTH  - 1; }
    if (newY < 0)                  { newY = 0; }
    if (newY > SURFACE_HEIGHT - 1) { newY = SURFACE_HEIGHT - 1; }
    gMouseAbsX = (int16_t)newX;
    gMouseAbsY = (int16_t)newY;

    gMouseX = gMouseAbsX;
    gMouseY = gMouseAbsY;

    gMouseButtonState[MOUSE_BUTTON_LEFT]   = (btn & MOUSE_HDR_LEFT_BTN)  != 0;
    gMouseButtonState[MOUSE_BUTTON_RIGHT]  = (btn & MOUSE_HDR_RIGHT_BTN) != 0;
    gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false;

    // Decode latest joystick byte for each stick. Direction is digital
    // on the ST, so axes snap to the JOYSTICK_AXIS_* extremes.
    for (i = 0; i < JOYSTICK_COUNT; i++) {
        joy = gIsrJoyByte[i];
        gJoyAxisX[i] = 0;
        gJoyAxisY[i] = 0;
        if (joy & JOY_BIT_LEFT)  { gJoyAxisX[i] = JOYSTICK_AXIS_MIN; }
        if (joy & JOY_BIT_RIGHT) { gJoyAxisX[i] = JOYSTICK_AXIS_MAX; }
        if (joy & JOY_BIT_UP)    { gJoyAxisY[i] = JOYSTICK_AXIS_MIN; }
        if (joy & JOY_BIT_DOWN)  { gJoyAxisY[i] = JOYSTICK_AXIS_MAX; }
        gJoyButtonState[i][JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0;
        gJoyButtonState[i][JOY_BUTTON_1] = false;  // ST has 1 fire button
    }
}


void jlpInputShutdown(void) {
    if (!gHooked) {
        return;
    }
    Supexec(restoreIkbdVector);
    gHooked = false;
}
