// Apple IIgs keyboard + mouse input via classic Apple II softswitches.
//
// Keyboard: $C000 (data) and $C010 (clear strobe). The Event Manager
// would be the "modern" approach, but it requires a full ToolBox
// bring-up that our S16 binary does not perform; calling GetNextEvent
// uninitialized corrupted KEGS' emulation state. Softswitches have no
// such dependency: they are live memory-mapped hardware.
//
// The typed-character queue is fed from the same $C000 drain: the ADB
// firmware translates shift/caps/layout before the byte reaches the
// strobe register, so each drained event doubles as a typed character
// (jlInputCharPush) with no extra keymap work.
//
// Tradeoff: $C000 reports the *last* key pressed, not a per-key matrix.
// Holding multiple non-modifier keys simultaneously cannot be observed;
// the demo and any game using this port sees one typable key at a time,
// plus live shift/ctrl/option state from the modifier register at
// $C025. This matches what every Apple II game ever shipped does, and
// it is enough for feature parity with the other platforms on typical
// "press a key, act on it" flows.
//
// Release detection uses the IIe-inherited "any key currently down"
// live flag at $C010 bit 7 (set by the keyboard scanner independently
// of the strobe). Each jlpInputPoll drains pending strobe events to
// pick up presses, then samples $C010: bit 7 == 0 means no
// non-modifier key is physically held, and we wholesale-clear
// gKeyState. readModifierKeys then re-asserts the modifiers from
// $C025's live state, so shift/ctrl/option stay accurate. Avoids
// the inferred-release lag the old TTL-decay scheme had, and works
// on every IIgs (real or stealth) without ToolBox / ADB Tool init.
//
// Mouse: $C024 (delta data) and $C027 (status). Each $C024 read
// returns one signed 7-bit delta; $C027 bit 1 indicates whether the
// next read will return X (0) or Y (1). On the Y read, $C024 bit 7
// also encodes inverted button state (0 = pressed). We do exactly two
// $C024 reads per jlpInputPoll, accumulating the deltas onto an
// absolute position which is clamped to the surface rectangle. The
// IIgs ADB MCU autopolls the mouse and queues fifos behind these
// softswitches, so the per-frame two-read cadence keeps up with
// normal motion; bursts may lag by a frame.

#include <string.h>

#include "port.h"
#include "inputInternal.h"
#include "joey/surface.h"
#include "surfaceInternal.h"   // iigsByteFill (hunt-3 rank 5)


// ----- Hardware registers -----

#define IIGS_KBD             ((volatile uint8_t *)0x00C000L)
#define IIGS_KBDSTRB         ((volatile uint8_t *)0x00C010L)
#define IIGS_MOUSEDATA       ((volatile uint8_t *)0x00C024L)
#define IIGS_MODIFIERS       ((volatile uint8_t *)0x00C025L)
#define IIGS_KMSTATUS        ((volatile uint8_t *)0x00C027L)

// Joystick / paddle softswitches.
#define IIGS_BTN0            ((volatile uint8_t *)0x00C061L)
#define IIGS_BTN1            ((volatile uint8_t *)0x00C062L)
#define IIGS_PADDLE0         ((volatile uint8_t *)0x00C064L)
#define IIGS_PADDLE1         ((volatile uint8_t *)0x00C065L)
#define IIGS_PTRIG           ((volatile uint8_t *)0x00C070L)
#define IIGS_BUTTON_BIT      0x80
#define IIGS_PADDLE_BUSY     0x80
#define PADDLE_TIMEOUT       256
#define PADDLE_LO_THRESHOLD  64
#define PADDLE_HI_THRESHOLD  192

#define KBD_STROBE_BIT       0x80
#define KBD_ASCII_MASK       0x7F

// $C010 RDKBDSTRB: reading clears the keyboard strobe at $C000 and
// returns the live "any key currently held" flag in bit 7 (set by
// the keyboard scanner / ADB MCU independently of the strobe). Used
// to drive immediate release detection without an inferred-release
// TTL counter.
#define KBD_ANY_KEY_DOWN_BIT 0x80

// Cap on the per-poll keyboard-FIFO drain. The IIgs ADB queue is
// small in practice; this is purely a defensive bound so a stuck
// strobe can't spin jlpInputPoll forever.
#define KBD_DRAIN_GUARD      32u

// $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl,
// bit 6 = option (Closed-Apple), bit 7 = command (Open-Apple).
#define MOD_SHIFT            0x01
#define MOD_CONTROL          0x02
#define MOD_OPTION           0x40

// $C027 layout (IIgs Hardware Reference / ADB MCU):
#define KMSTATUS_MOUSE_DATA  0x80   // mouse data available
#define KMSTATUS_MOUSE_COORD 0x02   // 0 = next $C024 read is X, 1 = Y

// $C024 mouse-data layout: bit 7 on Y reads encodes button (0=down).
// Bit 6 carries the sign of the 7-bit delta; bits 5-0 the magnitude.
#define MOUSE_DELTA_MASK     0x7F
#define MOUSE_DELTA_SIGN_BIT 0x40
#define MOUSE_BUTTON_INV     0x80

#define ASCII_TABLE_SIZE     128

// Apple II arrow-key ASCII conventions.
#define ASCII_LEFT           0x08
#define ASCII_RIGHT          0x15
#define ASCII_UP             0x0B
#define ASCII_DOWN           0x0A
#define ASCII_RETURN         0x0D
#define ASCII_TAB            0x09
#define ASCII_ESCAPE         0x1B
#define ASCII_DELETE         0x7F
#define ASCII_SPACE          0x20

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

static void    buildAsciiTable(void);
static void    pollJoystick(void);
static void    pollMouse(void);
static void    readModifierKeys(void);
static int8_t  signExtend7(uint8_t raw);
static int8_t  thresholdPaddle(uint8_t v);

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

// ASCII -> jlKeyE, filled once at jlpInputInit. Runtime fill keeps
// lookup O(1) instead of a 40-plus-case switch.
static uint8_t gAsciiToKey[ASCII_TABLE_SIZE];

static int16_t gMouseAbsX = SURFACE_WIDTH  / 2;
static int16_t gMouseAbsY = SURFACE_HEIGHT / 2;

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

static void buildAsciiTable(void) {
    uint16_t i;

    memset(gAsciiToKey, 0, sizeof(gAsciiToKey));

    for (i = 'A'; i <= 'Z'; i++) {
        gAsciiToKey[i]               = (uint8_t)(KEY_A + (i - 'A'));
        gAsciiToKey[i - 'A' + 'a']   = (uint8_t)(KEY_A + (i - 'A'));
    }
    for (i = '0'; i <= '9'; i++) {
        gAsciiToKey[i] = (uint8_t)(KEY_0 + (i - '0'));
    }

    gAsciiToKey[ASCII_SPACE]  = KEY_SPACE;
    gAsciiToKey[ASCII_ESCAPE] = KEY_ESCAPE;
    gAsciiToKey[ASCII_RETURN] = KEY_RETURN;
    gAsciiToKey[ASCII_TAB]    = KEY_TAB;
    gAsciiToKey[ASCII_DELETE] = KEY_BACKSPACE;
    // The left-arrow key produces 0x08, which is also ASCII backspace
    // on a classic Apple II. Prefer the arrow interpretation since
    // there is a dedicated Delete key that reports 0x7F.
    gAsciiToKey[ASCII_LEFT]   = KEY_LEFT;
    gAsciiToKey[ASCII_RIGHT]  = KEY_RIGHT;
    gAsciiToKey[ASCII_UP]     = KEY_UP;
    gAsciiToKey[ASCII_DOWN]   = KEY_DOWN;
}


static void readModifierKeys(void) {
    uint8_t mods;

    mods = *IIGS_MODIFIERS;
    gKeyState[KEY_LSHIFT] = (mods & MOD_SHIFT)   != 0;
    gKeyState[KEY_LCTRL]  = (mods & MOD_CONTROL) != 0;
    gKeyState[KEY_LALT]   = (mods & MOD_OPTION)  != 0;
}


// Sign-extend a 7-bit two's-complement number stored in bits 0-6.
static int8_t signExtend7(uint8_t raw) {
    uint8_t v;

    v = (uint8_t)(raw & MOUSE_DELTA_MASK);
    if (v & MOUSE_DELTA_SIGN_BIT) {
        return (int8_t)(v | 0x80);
    }
    return (int8_t)v;
}


// Map a raw 0..255 paddle reading to JOYSTICK_AXIS_MIN..MAX, using the
// stick's calibrated center (captured by jlJoystickReset) and a
// dead-zone band around it. Returns 0 if reading is within deadZone of
// the center; otherwise the offset from center, clamped to int8_t.
static int8_t analogPaddle(uint8_t v, uint8_t center, uint8_t deadZone) {
    int16_t delta;

    delta = (int16_t)v - (int16_t)center;
    if (delta < 0) {
        if ((-delta) <= (int16_t)deadZone) {
            return 0;
        }
        if (delta < (int16_t)JOYSTICK_AXIS_MIN) {
            return JOYSTICK_AXIS_MIN;
        }
    } else {
        if (delta <= (int16_t)deadZone) {
            return 0;
        }
        if (delta > (int16_t)JOYSTICK_AXIS_MAX) {
            return JOYSTICK_AXIS_MAX;
        }
    }
    return (int8_t)delta;
}


// Threshold a 0..255 paddle reading into a digital direction so the
// IIgs analog stick presents the same axis semantics as the digital
// sticks on ST/Amiga/DOS. Center range is treated as zero. Used
// before jlJoystickReset has been called -- once the app calibrates,
// we switch to analogPaddle for finer control.
static int8_t thresholdPaddle(uint8_t v) {
    if (v < PADDLE_LO_THRESHOLD) {
        return JOYSTICK_AXIS_MIN;
    }
    if (v > PADDLE_HI_THRESHOLD) {
        return JOYSTICK_AXIS_MAX;
    }
    return 0;
}


// Read the Apple IIgs joystick (paddle 0/1 + buttons 0/1). Buttons at
// $C061/$C062 are tied to the Open-Apple/Closed-Apple keys, so holding
// either modifier key looks like a fire press -- intentional Apple
// behavior, accept it. Only one stick is exposed; the IIgs second
// "stick" wiring (paddles 2/3) is rarely used by retro games.
//
// Each paddle read triggers an RC scan via $C070 and then polls the
// paddle softswitch until bit 7 clears; the iteration count
// approximates the paddle's 0..255 position (the Apple firmware
// PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to
// charge at full deflection; if NO joystick is wired up, the BUSY bit
// stays set forever and the busy-wait runs the full PADDLE_TIMEOUT
// every frame -- ~3 ms wasted per frame on a stick that isn't there.
//
// After JOY_DISCONNECT_THRESHOLD consecutive timeouts we latch the
// stick as absent and stop polling entirely. The app calls
// jlJoystickReset to clear the latch and resume polling.
#define JOY_DISCONNECT_THRESHOLD 60u

static uint16_t gJoyConsecutiveTimeouts = 0;
static bool     gJoyDisconnectLatched   = false;

// Analog calibration: gJoyCenterX/Y hold the raw paddle reading we
// captured the last time the user called jlJoystickReset. Until
// that's called, gJoyCenterValid is false and pollJoystick falls back
// to the digital threshold mapping. gJoyRecalibrate is set by
// jlpJoystickReset and cleared on the next successful poll, which
// captures the new center.
// uint8_t (not bool) so the per-element stride is a known 1 byte.
// Storage is still 0 or 1 either way.
static uint8_t gJoyCenterX     [JOYSTICK_COUNT];
static uint8_t gJoyCenterY     [JOYSTICK_COUNT];
static uint8_t gJoyCenterValid [JOYSTICK_COUNT];
static uint8_t gJoyRecalibrate [JOYSTICK_COUNT];


void jlpJoystickReset(jlJoystickE js) {
    if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) {
        return;
    }
    // Re-enable polling and arm a fresh center capture for the next
    // poll. The dead-zone value lives in core's gJoyDeadZone[js].
    gJoyConsecutiveTimeouts = 0;
    gJoyDisconnectLatched   = false;
    gJoyRecalibrate[js]     = true;
}

// Asm paddle reader (joeyDraw.s, John Brooks' 1 MHz GetJoyXY). Switches
// the CPU to 1 MHz for the read so paddle counts match what every other
// IIgs/Apple II joystick game produces (a busy-wait at 2.8 MHz inflates
// counts). Returns the paddle read packed as a uint32_t in A:X (register
// return -- asm writes to a global do not reach the C-read address in
// this toolchain):
//   resolved = ret & 0xFF (bit0: JoyX valid, bit1: JoyY valid)
//   px       = (ret >> 8)  & 0xFF (JoyX 0..255)
//   py       = (ret >> 16) & 0xFF (JoyY 0..255)
extern uint32_t iigsPollJoystickInner(void);

static void pollJoystick(void) {
    uint32_t result;
    uint8_t  px;
    uint8_t  py;
    uint8_t  resolvedFlags;
    bool     xResolved;
    bool     yResolved;

    // Buttons are I/O reads -- always cheap, do them every frame.
    // Indexing through a (uint8_t *) cast collapses each
    // gJoyButtonState[i][j] write to a literal byte offset.
    ((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0]
        = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0;
    ((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_1]
        = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0;
    gJoyConnected[JOYSTICK_1] = false;

    // Once the stick has been latched as disconnected, only buttons
    // get polled. The app must call jlJoystickReset to resume axis
    // polling (e.g., when the user has just plugged in a stick).
    if (gJoyDisconnectLatched) {
        gJoyAxisX[JOYSTICK_0]     = 0;
        gJoyAxisY[JOYSTICK_0]     = 0;
        gJoyConnected[JOYSTICK_0] = false;
        return;
    }

    // Asm read at 1 MHz -- result returned packed in registers (A:X).
    result        = iigsPollJoystickInner();
    resolvedFlags = (uint8_t)(result & 0xFFu);
    px            = (uint8_t)((result >> 8) & 0xFFu);
    py            = (uint8_t)((result >> 16) & 0xFFu);
    xResolved     = (resolvedFlags & 0x01) != 0;
    yResolved     = (resolvedFlags & 0x02) != 0;

    gJoyConnected[JOYSTICK_0] = xResolved || yResolved;

    // Update auto-disconnect counter. Both axes failing => probably no
    // stick. One resolves => stick is present, reset the counter.
    //
    // gJoyConsecutiveTimeouts uses a local-var read-modify-write rather
    // than `++` so the update is DBR-independent: an `inc abs` on the
    // static would depend on DBR pointing at this static's bank, and a
    // cross-segment JSL doesn't update DBR, so a caller in a different
    // load segment could silently mutate the wrong byte. Long-mode
    // lda+sta avoids that.
    if (!xResolved && !yResolved) {
        uint16_t timeouts;

        timeouts = gJoyConsecutiveTimeouts;
        if (timeouts < 0xFFFFu) {
            timeouts = (uint16_t)(timeouts + 1u);
            gJoyConsecutiveTimeouts = timeouts;
        }
        if (timeouts >= JOY_DISCONNECT_THRESHOLD) {
            gJoyDisconnectLatched = true;
        }
        gJoyAxisX[JOYSTICK_0] = 0;
        gJoyAxisY[JOYSTICK_0] = 0;
        return;
    }

    gJoyConsecutiveTimeouts = 0;

    // Capture the resting position on recalibrate (one-shot).
    if (gJoyRecalibrate[JOYSTICK_0]) {
        gJoyCenterX    [JOYSTICK_0] = px;
        gJoyCenterY    [JOYSTICK_0] = py;
        gJoyCenterValid[JOYSTICK_0] = true;
        gJoyRecalibrate[JOYSTICK_0] = false;
    }

    // Calibrated => analog axis report (offset from center, dead-zone
    // clamped). Uncalibrated => the legacy 3-state digital threshold,
    // matching how the stick behaved before jlJoystickReset existed.
    if (gJoyCenterValid[JOYSTICK_0]) {
        gJoyAxisX[JOYSTICK_0] = analogPaddle(px,
                                             gJoyCenterX[JOYSTICK_0],
                                             gJoyDeadZone[JOYSTICK_0]);
        gJoyAxisY[JOYSTICK_0] = analogPaddle(py,
                                             gJoyCenterY[JOYSTICK_0],
                                             gJoyDeadZone[JOYSTICK_0]);
    } else {
        gJoyAxisX[JOYSTICK_0] = thresholdPaddle(px);
        gJoyAxisY[JOYSTICK_0] = thresholdPaddle(py);
    }
}


// Drain one X+Y delta pair from the ADB mouse FIFO. $C027 bit 1 tells
// us which coordinate the next $C024 read will return; we honor that
// rather than assuming an order, so we stay in sync even if a stray
// $C024 read happened between frames. The Y read also carries the
// inverted button state in bit 7 (0 = pressed).
static void pollMouse(void) {
    uint8_t status;
    uint8_t data;
    int8_t  delta;
    int16_t newPos;
    bool    isYRead;
    uint16_t i;

    for (i = 0; i < 2; i++) {
        status  = *IIGS_KMSTATUS;
        isYRead = (status & KMSTATUS_MOUSE_COORD) != 0;
        data    = *IIGS_MOUSEDATA;
        delta   = signExtend7(data);

        if (isYRead) {
            newPos = (int16_t)(gMouseAbsY + delta);
            if (newPos < 0)                  { newPos = 0; }
            if (newPos > SURFACE_HEIGHT - 1) { newPos = SURFACE_HEIGHT - 1; }
            gMouseAbsY = newPos;
            // Button bit only meaningful on Y reads. 0 = pressed.
            gMouseButtonState[MOUSE_BUTTON_LEFT] = (data & MOUSE_BUTTON_INV) == 0;
        } else {
            newPos = (int16_t)(gMouseAbsX + delta);
            if (newPos < 0)                  { newPos = 0; }
            if (newPos > SURFACE_WIDTH - 1)  { newPos = SURFACE_WIDTH - 1; }
            gMouseAbsX = newPos;
        }
    }

    gMouseX = gMouseAbsX;
    gMouseY = gMouseAbsY;
    // The ADB mouse only reports the single physical button; right
    // and middle stay false.
    gMouseButtonState[MOUSE_BUTTON_RIGHT]  = false;
    gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false;
}


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

void jlpInputInit(void) {
    memset(gKeyState, 0, sizeof(gKeyState));
    memset(gKeyPrev,  0, sizeof(gKeyPrev));
    buildAsciiTable();

    gMouseAttached = true;             // the ADB mouse is part of every IIgs
    gMouseAbsX = SURFACE_WIDTH  / 2;
    gMouseAbsY = SURFACE_HEIGHT / 2;
    gMouseX    = gMouseAbsX;
    gMouseY    = gMouseAbsY;

    // Clear any pending strobe from before we started.
    (void)*IIGS_KBDSTRB;
}


void jlpInputPoll(void) {
    // Keep the serial soft ring fed across the input scan (multiple I/O-page reads).
    {
        extern void iigsSerialPump(void);

        iigsSerialPump();
    }
    uint8_t  kbd;
    uint8_t  ascii;
    uint8_t  key;
    uint8_t  kbdStrb;
    uint16_t drainGuard;
    bool     strobeObserved;

    // The gKeyState/gKeyPrev/gMouseButtonPrev/gJoyButtonPrev snapshots
    // all happen earlier in jlInputPoll's call to iigsInputSnapshot
    // (asm). We just read the live hardware state here.

    // Drain the keyboard FIFO, not just the head. The IIgs ADB MCU
    // queues press + autorepeat events; consuming only one per poll
    // would leave queued events waiting to refresh state on later
    // polls. KBD_DRAIN_GUARD bounds the loop in case a stuck strobe
    // ever fails to clear.
    strobeObserved = false;
    for (drainGuard = 0; drainGuard < KBD_DRAIN_GUARD; drainGuard++) {
        {
            extern void iigsSerialPump(void);

            // Per-iteration pump (the X68000 per-group lesson): the FIFO drain + decode below is a
            // multi-ms deaf window at 9600 and the Z8530 FIFO is 3 bytes - a pump only at poll ENTRY
            // measurably lost a 4-byte clump mid-frame.
            iigsSerialPump();
        }
        kbd = *IIGS_KBD;
        if ((kbd & KBD_STROBE_BIT) == 0) {
            break;
        }
        strobeObserved = true;
        ascii = (uint8_t)(kbd & KBD_ASCII_MASK);
        key   = gAsciiToKey[ascii];
        if (key != KEY_NONE) {
            gKeyState[key] = true;
        }
        // Typed-character path: the ADB firmware already applies
        // shift/caps and the keyboard layout, so the strobe byte IS
        // the translated character. The Delete key (0x7F, the IIgs
        // backspace key) is normalized to JL_CHAR_BACKSPACE. A raw
        // 0x08 is the LEFT-ARROW key (Apple II heritage), and arrows
        // are not characters -- suppress it so cursor movement never
        // types a backspace; the other arrows' codes are stopped by
        // jlInputCharPush's filter anyway.
        if (ascii == ASCII_DELETE) {
            jlInputCharPush(JL_CHAR_BACKSPACE);
        } else if (ascii != ASCII_LEFT) {
            jlInputCharPush(ascii);
        }
        (void)*IIGS_KBDSTRB;
    }

    // $C010 bit 7 is the live "any non-modifier key currently held"
    // flag (IIe-inherited; updated by the keyboard scanner / ADB MCU
    // independently of the strobe). When 0 we know all non-modifier
    // keys are physically released, so wholesale-clear gKeyState and
    // let readModifierKeys re-assert the modifiers from $C025 below.
    //
    // strobeObserved guard: a press that arrived AND was released
    // between two polls would otherwise be set-then-cleared in a
    // single poll, losing the rising edge that jlKeyPressed needs.
    // Holding the press for one poll preserves it; the next poll's
    // bit-7 read will clear normally.
    kbdStrb = *IIGS_KBDSTRB;
    if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) {
        // iigsByteFill, not memset: llvm-mos lowers memset to a
        // far-call byte loop (finding #79), and this branch runs on
        // nearly EVERY real game frame (idle keyboard). Hunt-3 rank 5.
        iigsByteFill(gKeyState, 0u, (uint16_t)sizeof(gKeyState));
    }

    readModifierKeys();
    pollMouse();
    pollJoystick();
}


void jlpInputShutdown(void) {
    (void)*IIGS_KBDSTRB;
}
