// Amiga keyboard + mouse input: opens a backdrop borderless window on
// the JoeyLib CUSTOMSCREEN to receive IDCMP_RAWKEY, IDCMP_MOUSEMOVE,
// and IDCMP_MOUSEBUTTONS events, then drains those messages each
// jlInputPoll.
//
// RAWKEY is the lowest-level IDCMP event Intuition exposes: ie_Code
// carries the Amiga raw key code (press in 0x00..0x7F, release with
// bit 7 set), which we map straight to jlKeyE entries. The backdrop
// borderless window covers the whole screen without drawing over our
// planar bitmap (c2p writes to Screen->BitMap->Planes directly), and
// WFLG_ACTIVATE ensures keyboard focus lands on us from the start.
//
// MOUSEMOVE messages carry MouseX/MouseY in window-relative pixels;
// since the window is at (0,0) covering the whole screen, those are
// also surface-space coordinates. WA_ReportMouse = TRUE is required
// for MOUSEMOVE events to fire continuously rather than only on button
// transitions. WFLG_RMBTRAP keeps the right mouse button from opening
// Intuition's screen menu so right-clicks come to us as MENUDOWN.
//
// Typed characters: RAWKEY press events are also run through
// keymap.library's MapRawKey with the message qualifier and dead-key
// context, which applies the user's keymap (shift, caps, national
// layouts, dead keys) and yields translated bytes for the core typed-
// character queue. Intuition delivers key repeats as further RAWKEY
// events, so OS auto-repeat produces repeated characters for free.

#include <string.h>

#include <exec/types.h>
#include <exec/ports.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <devices/inputevent.h>
#include <libraries/lowlevel.h>

#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/keymap.h>
#include <proto/lowlevel.h>

#include "port.h"
#include "inputInternal.h"

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

#define AMIGA_KEY_TABLE_SIZE   128
#define AMIGA_KEY_RELEASE_BIT  0x80
#define AMIGA_KEY_CODE_MASK    0x7F

// MapRawKey output buffer. A single keystroke maps to at most a few
// bytes (dead-key compositions); 8 is comfortably above any keymap's
// output.
#define MAP_RAW_KEY_BUF_SIZE   8

// Keymaps encode arrows / F-keys / Help as ANSI control sequences
// beginning with CSI (0x9B). The CSI byte itself would be filtered,
// but the sequence's TAIL bytes are printable ('A' for cursor up,
// "0~" for F1, ...) and would leak into the typed-character queue as
// stray text -- so a mapping that starts with CSI is discarded whole.
#define AMIGA_CSI              0x9B

// keymap.library base referenced by the MapRawKey inline stub. Defined
// here and opened explicitly (not left to libnix auto-open) so a
// system without the library degrades to "no typed characters" instead
// of failing at startup.
struct Library *KeymapBase = NULL;

// ----- External from hal.c -----

extern struct Screen *gScreen;

// ----- Empty pointer image -----
//
// Full-screen custom-screen games conventionally hide the system
// pointer; we install an all-zero sprite via SetPointer so Workbench's
// arrow doesn't drift over the playfield. Game code that needs a
// cursor draws its own from jlMouseX/Y.
//
// Amiga hardware sprite data: 2 leading control words (VSTART/HSTART/
// VSTOP), one line of (planeA, planeB) data words, 2 trailing zero
// words as terminator. All zeros yields a transparent pointer. Must
// live in chip RAM since sprite DMA reads it.

#define EMPTY_POINTER_BYTES (6 * sizeof(UWORD))

static UWORD *gEmptyPointer = NULL;

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

static void drainMessages(void);
static void pollJoysticks(void);

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

// Amiga raw-key code -> jlKeyE. Codes are layout-independent; the
// OS has a separate keymap for producing characters.
static const uint8_t gRawKeyToKey[AMIGA_KEY_TABLE_SIZE] = {
    [0x01] = KEY_1,
    [0x02] = KEY_2,
    [0x03] = KEY_3,
    [0x04] = KEY_4,
    [0x05] = KEY_5,
    [0x06] = KEY_6,
    [0x07] = KEY_7,
    [0x08] = KEY_8,
    [0x09] = KEY_9,
    [0x0A] = KEY_0,
    [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,
    [0x20] = KEY_A,
    [0x21] = KEY_S,
    [0x22] = KEY_D,
    [0x23] = KEY_F,
    [0x24] = KEY_G,
    [0x25] = KEY_H,
    [0x26] = KEY_J,
    [0x27] = KEY_K,
    [0x28] = KEY_L,
    [0x31] = KEY_Z,
    [0x32] = KEY_X,
    [0x33] = KEY_C,
    [0x34] = KEY_V,
    [0x35] = KEY_B,
    [0x36] = KEY_N,
    [0x37] = KEY_M,
    [0x40] = KEY_SPACE,
    [0x41] = KEY_BACKSPACE,
    [0x42] = KEY_TAB,
    [0x44] = KEY_RETURN,
    [0x45] = KEY_ESCAPE,
    [0x4C] = KEY_UP,
    [0x4D] = KEY_DOWN,
    [0x4E] = KEY_RIGHT,
    [0x4F] = KEY_LEFT,
    [0x50] = KEY_F1,
    [0x51] = KEY_F2,
    [0x52] = KEY_F3,
    [0x53] = KEY_F4,
    [0x54] = KEY_F5,
    [0x55] = KEY_F6,
    [0x56] = KEY_F7,
    [0x57] = KEY_F8,
    [0x58] = KEY_F9,
    [0x59] = KEY_F10,
    [0x60] = KEY_LSHIFT,
    [0x61] = KEY_RSHIFT,
    [0x63] = KEY_LCTRL,
    [0x64] = KEY_LALT,
};

static struct Window  *gWindow      = NULL;
static struct Library *gLowLevelBase = NULL;

// AmigaOS exposes joystick port 1 (the dedicated stick) via lowlevel
// ReadJoyPort(1); port 0 shares the mouse port. We map our public
// JOYSTICK_0 to Amiga port 1 so the primary stick is what retro games
// expect.
#define AMIGA_PORT_FOR_JS0  1
#define AMIGA_PORT_FOR_JS1  0

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

static void drainMessages(void) {
    struct IntuiMessage *msg;
    struct InputEvent    ie;
    UBYTE                mapBuf[MAP_RAW_KEY_BUF_SIZE];
    WORD                 mapCount;
    WORD                 mapIdx;
    UWORD                msgClass;
    UWORD                msgCode;
    int16_t              msgMouseX;
    int16_t              msgMouseY;
    uint8_t              code;
    uint8_t              key;
    bool                 isRelease;

    if (gWindow == NULL) {
        return;
    }
    while ((msg = (struct IntuiMessage *)GetMsg(gWindow->UserPort)) != NULL) {
        msgClass  = msg->Class;
        msgCode   = msg->Code;
        msgMouseX = (int16_t)msg->MouseX;
        msgMouseY = (int16_t)msg->MouseY;

        // Typed-character translation must run BEFORE ReplyMsg: the
        // qualifier and the dead-key context behind IAddress belong
        // to the message and may be recycled once replied. Release
        // events (bit 7) type nothing.
        mapCount = 0;
        if (msgClass == IDCMP_RAWKEY &&
            (msgCode & AMIGA_KEY_RELEASE_BIT) == 0 &&
            KeymapBase != NULL) {
            ie.ie_NextEvent    = NULL;
            ie.ie_Class        = IECLASS_RAWKEY;
            ie.ie_SubClass     = 0;
            ie.ie_Code         = msgCode;
            ie.ie_Qualifier    = msg->Qualifier;
            // Dead-key context: IAddress points at the previous-code
            // storage MapRawKey wants in ie_EventAddress (RKM recipe).
            ie.ie_EventAddress = (APTR)*((ULONG *)msg->IAddress);
            mapCount = MapRawKey(&ie, (STRPTR)mapBuf, MAP_RAW_KEY_BUF_SIZE, NULL);
            if (mapCount > 0 && mapBuf[0] == AMIGA_CSI) {
                mapCount = 0;
            }
        }

        ReplyMsg((struct Message *)msg);

        switch (msgClass) {
            case IDCMP_RAWKEY:
                isRelease = (msgCode & AMIGA_KEY_RELEASE_BIT) != 0;
                code      = (uint8_t)(msgCode & AMIGA_KEY_CODE_MASK);
                key       = gRawKeyToKey[code];
                if (key != KEY_NONE) {
                    gKeyState[key] = !isRelease;
                }
                // MapRawKey returns -1 on overflow (composition longer
                // than the buffer) -- nothing usable, skip.
                for (mapIdx = 0; mapIdx < mapCount; mapIdx++) {
                    jlInputCharPush((uint8_t)mapBuf[mapIdx]);
                }
                break;
            case IDCMP_MOUSEMOVE:
                gMouseX = msgMouseX;
                gMouseY = msgMouseY;
                break;
            case IDCMP_MOUSEBUTTONS:
                switch (msgCode) {
                    case SELECTDOWN: gMouseButtonState[MOUSE_BUTTON_LEFT]   = true;  break;
                    case SELECTUP:   gMouseButtonState[MOUSE_BUTTON_LEFT]   = false; break;
                    case MENUDOWN:   gMouseButtonState[MOUSE_BUTTON_RIGHT]  = true;  break;
                    case MENUUP:     gMouseButtonState[MOUSE_BUTTON_RIGHT]  = false; break;
                    case MIDDLEDOWN: gMouseButtonState[MOUSE_BUTTON_MIDDLE] = true;  break;
                    case MIDDLEUP:   gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; break;
                    default: break;
                }
                // MOUSEBUTTONS messages also carry the mouse position.
                gMouseX = msgMouseX;
                gMouseY = msgMouseY;
                break;
            default:
                break;
        }
    }
}


// Read both joystick ports via lowlevel.library. Devices that report
// JP_TYPE_JOYSTK or JP_TYPE_GAMECTLR populate gJoy*; anything else
// (mouse on port 0, no device, unknown) leaves the stick disconnected
// with zeroed state.
static void pollJoysticks(void) {
    static const int amigaPortForStick[JOYSTICK_COUNT] = {
        AMIGA_PORT_FOR_JS0,
        AMIGA_PORT_FOR_JS1
    };
    ULONG state;
    ULONG type;
    int   stick;

    if (gLowLevelBase == NULL) {
        return;
    }
    for (stick = 0; stick < JOYSTICK_COUNT; stick++) {
        state = ReadJoyPort((UWORD)amigaPortForStick[stick]);
        type  = state & JP_TYPE_MASK;
        if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) {
            gJoyConnected[stick] = false;
            gJoyAxisX[stick]     = 0;
            gJoyAxisY[stick]     = 0;
            gJoyButtonState[stick][JOY_BUTTON_0] = false;
            gJoyButtonState[stick][JOY_BUTTON_1] = false;
            continue;
        }
        gJoyConnected[stick] = true;

        gJoyAxisX[stick] = 0;
        if (state & JPF_JOY_LEFT)  { gJoyAxisX[stick] = JOYSTICK_AXIS_MIN; }
        if (state & JPF_JOY_RIGHT) { gJoyAxisX[stick] = JOYSTICK_AXIS_MAX; }
        gJoyAxisY[stick] = 0;
        if (state & JPF_JOY_UP)    { gJoyAxisY[stick] = JOYSTICK_AXIS_MIN; }
        if (state & JPF_JOY_DOWN)  { gJoyAxisY[stick] = JOYSTICK_AXIS_MAX; }

        // Standard 1-button stick reports JPF_BUTTON_RED only. CD32
        // game controllers also report BLUE for the second face button.
        gJoyButtonState[stick][JOY_BUTTON_0] = (state & JPF_BUTTON_RED)  != 0;
        gJoyButtonState[stick][JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0;
    }
}


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

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


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

    if (gScreen == NULL) {
        return;
    }

    gWindow = OpenWindowTags(NULL,
        (ULONG)WA_CustomScreen, (ULONG)gScreen,
        (ULONG)WA_Left,         (ULONG)0,
        (ULONG)WA_Top,          (ULONG)0,
        (ULONG)WA_Width,        (ULONG)gScreen->Width,
        (ULONG)WA_Height,       (ULONG)gScreen->Height,
        (ULONG)WA_Flags,        (ULONG)(WFLG_BACKDROP
                                        | WFLG_BORDERLESS
                                        | WFLG_ACTIVATE
                                        | WFLG_RMBTRAP
                                        | WFLG_NOCAREREFRESH
                                        | WFLG_REPORTMOUSE),
        (ULONG)WA_IDCMP,        (ULONG)(IDCMP_RAWKEY
                                        | IDCMP_MOUSEMOVE
                                        | IDCMP_MOUSEBUTTONS),
        TAG_DONE);

    if (gWindow != NULL) {
        gMouseAttached = true;         // Intuition delivers IDCMP mouse events on this window
        gMouseX = (int16_t)(gWindow->Width  / 2);
        gMouseY = (int16_t)(gWindow->Height / 2);

        gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES,
                                          (ULONG)(MEMF_CHIP | MEMF_CLEAR));
        if (gEmptyPointer != NULL) {
            SetPointer(gWindow, gEmptyPointer, 1, 16, 0, 0);
        }
    }

    // lowlevel.library shipped with AmigaOS 2.05+. If absent (very old
    // Kickstart, or stripped AROS), the joystick API silently reports
    // disconnected sticks rather than failing init.
    gLowLevelBase = OpenLibrary((CONST_STRPTR)"lowlevel.library", 0);

    // keymap.library (OS 2.0+) drives the typed-character path. If
    // absent, jlInputGetChar just stays empty; the predicates still
    // work off the raw codes.
    KeymapBase = OpenLibrary((CONST_STRPTR)"keymap.library", 0);
}


void jlpInputPoll(void) {
    drainMessages();
    pollJoysticks();
}


void jlpInputShutdown(void) {
    if (KeymapBase != NULL) {
        CloseLibrary(KeymapBase);
        KeymapBase = NULL;
    }
    if (gLowLevelBase != NULL) {
        CloseLibrary(gLowLevelBase);
        gLowLevelBase = NULL;
    }
    if (gWindow == NULL) {
        return;
    }
    drainMessages();
    if (gEmptyPointer != NULL) {
        // Restore the system pointer before freeing our image so
        // Intuition isn't holding a stale chip-RAM reference.
        ClearPointer(gWindow);
        FreeMem(gEmptyPointer, (ULONG)EMPTY_POINTER_BYTES);
        gEmptyPointer = NULL;
    }
    CloseWindow(gWindow);
    gWindow = NULL;
}
