// Sharp X68000 HAL -- bring-up (Phase B: chunky stage, expand on present).
//
// The stage stays a chunky 4bpp surface in main RAM and jlpPresent expands it
// into GVRAM. That is deliberately the slow-but-correct path: it makes all of
// src/generic work unmodified, so the port renders correctly before any native
// primitive exists. Nothing here claims a JL_HAS_* override yet.
//
// GVRAM lives at $C00000. In 16-colour mode a pixel occupies the low nibble of
// its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so
// one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address
// space per displayed pixel -- the reason this path is a stepping stone rather
// than the destination. See the storage-model note in joey/platform.h.
//
// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path
// renders, and UBER's captured hashes are byte-identical to the Apple IIgs
// golden reference. Input, serial and audio are separately proven -- see
// docs/x68000_port.md for the evidence for each.

#include <x68k/dos.h>
#include <x68k/iocs.h>

#include "port.h"
#include <string.h>

#include "surfaceInternal.h"
#include "x68kPlanar.h"
#include "inputInternal.h"


// ----- Hardware addresses ---------------------------------------------------

#define X68K_GVRAM       ((volatile uint16_t *)0xC00000L)
// GVRAM is addressed as a 512-word-per-line grid regardless of the visible
// width, so the row stride is a constant, not a function of SURFACE_WIDTH.
#define X68K_GVRAM_STRIDE 512u

// _iocs_crtmod screen mode. 12 = 512x512, 16 colours, 31 kHz. The library's
// 320x200 stage is drawn into the top-left corner; there is no 320x200 mode on
// this machine, so some form of letterboxing is unavoidable.
#define X68K_CRTMOD_512_16  12

// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
#define X68K_KEYGROUP_COUNT 15

// MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line.
#define X68K_MFP_GPIP        ((volatile uint8_t *)0xE88001L)
#define X68K_GPIP_VDISP      0x10u
#define X68K_VBL_SPIN_LIMIT  2000000ul
#define X68K_TEE_SPIN_LIMIT  200000ul


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

static int      gPrevCrtMode = -1;
static bool     gModeSet     = false;
static uint16_t gFrameCount  = 0;
// Saved USP from the _dos_super(0) switch, or -1 if we were already in
// supervisor mode and must not switch back.
static int      gPrevSsp     = -1;
// Last sampled V-DISP level, for the frame-counter edge detect.
static uint8_t  gLastVdisp   = 0;
// True once the V-DISP interrupt handler is live; the poll then stands down.
static bool     gVdispInstalled = false;


// Defined below with the timing code; declared here because jlpInit installs it.
static void vdispHandler(void);
static void vdispPoll(void);
extern void *const gX68kVdispHandlerRef;


// ----- Lifecycle ------------------------------------------------------------

bool jlpInit(const jlConfigT *config) {
    (void)config;
    // Human68k starts .X programs in USER mode, so direct I/O reads (the MFP
    // GPIP poll in jlpWaitVBL, and any future register banging) do not see the
    // hardware. Same trap the Atari ST port hit, same fix: go supervisor here
    // and stay there. _dos_super(0) returns the old USP, or a negative value
    // if we were already supervisor -- in which case do not switch back.
    gPrevSsp = _dos_super(0);
    gPrevCrtMode = _iocs_crtmod(-1);        // -1 queries without changing
    _iocs_crtmod(X68K_CRTMOD_512_16);
    _iocs_g_clr_on();                       // clear graphics + enable the plane
    // NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
    // tested, no serial output at all, so it wedges before main() gets going.
    // The handler ABI is not a plain C function: a _VDISPST handler is entered
    // from the interrupt and this one returns with RTS from a context IOCS does
    // not expect. Getting it right needs an asm thunk (save d0-d1/a0-a1, call,
    // restore, correct return), which is worth doing but is not a one-liner.
    // Until then the polled fallback below owns the counter.
    gVdispInstalled = false;
    gModeSet = true;
    return true;
}


void jlpShutdown(void) {
    if (!gModeSet) {
        return;
    }
    if (gPrevCrtMode >= 0) {
        _iocs_crtmod(gPrevCrtMode);
    }
    gModeSet = false;
    if (gPrevSsp >= 0) {
        (void)_dos_super(gPrevSsp);         // back to user mode for Human68k
        gPrevSsp = -1;
    }
}


// ----- Present --------------------------------------------------------------

// Copy the surface's four planes into the TEXT PLANE at $E00000.
//
// This is the payoff of native planar storage. The surface rows are already in
// display format -- 40 bytes per row per plane -- so present is a straight copy
// with a stride change (40 -> 128), not a per-pixel expansion. Traffic per full
// frame is 4 planes x 200 rows x 40 bytes = 32,000 bytes, against the 128,000
// bytes of GVRAM word writes the chunky path moved. Bus traffic is what the
// measured wait states punish, so that 4x cut is the win.
//
// Dirty rows only, and only the marked band within a row.
void jlpPresent(const jlSurfaceT *src) {
    X68kPlanarT *pd;
    uint16_t     y;
    uint16_t     plane;
    uint16_t     firstByte;
    uint16_t     rowBytes;
    const uint8_t *s;
    uint8_t       *d;

    if (src == NULL) {
        return;
    }
    vdispPoll();   // keep the frame counter honest: see the note on vdispPoll
    // Push the palette when it has changed. Without this NOTHING ever uploads
    // colours and the display keeps whatever Human68k left in the text palette.
    // The text plane has ONE 16-entry palette, so SCB per-scanline palette
    // selection cannot be honoured -- palette 0 is used, matching the Atari ST.
    if (gStagePaletteDirty) {
        uint16_t c;
        for (c = 0u; c < 16u; c++) {
            X68K_TEXT_PALETTE[c] = x68kColorFromRgb12(src->palette[0][c]);
        }
        gStagePaletteDirty = false;
    }
    pd = x68kSurfacePlanar(src);
    if (pd == NULL) {
        return;
    }
    for (y = 0; y < SURFACE_HEIGHT; y++) {
        if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
            continue;
        }
        // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane.
        firstByte = (uint16_t)(gStageMinWord[y] >> 1);
        rowBytes  = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
        if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) {
            rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
        }
        for (plane = 0u; plane < X68K_BITPLANES; plane++) {
            s = pd->planes[plane] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
            // Centred: shift down X68K_ORIGIN_Y rows and right X68K_ORIGIN_BYTE
            // bytes. X is byte-aligned (96/8 = 12) so no bit shifting is needed.
            d = (uint8_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP)
                            + x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y))
                            + X68K_ORIGIN_BYTE + firstByte);
            memcpy(d, s, rowBytes);
        }
    }
}


// ----- Input ----------------------------------------------------------------

// _iocs_bitsns hands back a raw per-group key-DOWN bitmap, so there is no ISR
// to install, no vector to take over and no packet state machine -- unlike the
// ST, which has to replace the TOS ikbdsys vector and decode IKBD packets.
void jlpInputInit(void) {
}


void jlpInputShutdown(void) {
}


// jlKeyE -> X68000 scancode. Values taken from XEiJ's keyboard table
// (Keyboard.java), which is the authoritative mapping, not guessed.
// 0 means "this key has no equivalent on an X68000 keyboard".
//
// Notes on the keys that do not map one-to-one:
//   - The X68000 has a SINGLE shift key (0x70), so KEY_LSHIFT and KEY_RSHIFT
//     both read it. A game testing either sees the same physical key.
//   - There is no ALT; OPT.1 (0x72) is the closest equivalent and is what
//     KEY_LALT reads. OPT.2 (0x73) is left unmapped.
static const uint8_t kScanForKey[KEY_COUNT] = {
    [KEY_NONE]      = 0x00,
    [KEY_A]         = 0x1e, [KEY_B] = 0x2e, [KEY_C] = 0x2c, [KEY_D] = 0x20,
    [KEY_E]         = 0x13, [KEY_F] = 0x21, [KEY_G] = 0x22, [KEY_H] = 0x23,
    [KEY_I]         = 0x18, [KEY_J] = 0x24, [KEY_K] = 0x25, [KEY_L] = 0x26,
    [KEY_M]         = 0x30, [KEY_N] = 0x2f, [KEY_O] = 0x19, [KEY_P] = 0x1a,
    [KEY_Q]         = 0x11, [KEY_R] = 0x14, [KEY_S] = 0x1f, [KEY_T] = 0x15,
    [KEY_U]         = 0x17, [KEY_V] = 0x2d, [KEY_W] = 0x12, [KEY_X] = 0x2b,
    [KEY_Y]         = 0x16, [KEY_Z] = 0x2a,
    [KEY_0]         = 0x0b, [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_SPACE]     = 0x35,
    [KEY_ESCAPE]    = 0x01,
    [KEY_RETURN]    = 0x1d,
    [KEY_TAB]       = 0x10,
    [KEY_BACKSPACE] = 0x0f,
    [KEY_UP]        = 0x3c, [KEY_DOWN] = 0x3e,
    [KEY_LEFT]      = 0x3b, [KEY_RIGHT] = 0x3d,
    [KEY_LSHIFT]    = 0x70, [KEY_RSHIFT] = 0x70,   // one physical SHIFT
    [KEY_LCTRL]     = 0x71,
    [KEY_LALT]      = 0x72,                        // OPT.1
    [KEY_F1]        = 0x63, [KEY_F2] = 0x64, [KEY_F3] = 0x65, [KEY_F4] = 0x66,
    [KEY_F5]        = 0x67, [KEY_F6] = 0x68, [KEY_F7] = 0x69, [KEY_F8] = 0x6a,
    [KEY_F9]        = 0x6b, [KEY_F10] = 0x6c,
};


// _iocs_bitsns(group) returns a bitmap of the eight keys in that group, bit n
// set meaning "down": group = scancode >> 3, bit = scancode & 7. Every group
// is fetched once per poll rather than per key, so the whole keyboard costs
// X68K_KEYGROUP_COUNT IOCS calls regardless of how many keys are tested.
//
// No ISR, no vector takeover, no packet decoding -- unlike the ST, which has
// to replace the TOS ikbdsys vector and run an IKBD packet state machine.
void jlpInputPoll(void) {
    uint8_t  groups[X68K_KEYGROUP_COUNT];
    uint16_t group;
    uint16_t key;
    uint8_t  scan;

    for (group = 0; group < X68K_KEYGROUP_COUNT; group++) {
        groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF);
    }
    for (key = 1; key < KEY_COUNT; key++) {
        scan = kScanForKey[key];
        if (scan == 0u) {
            continue;
        }
        group = (uint16_t)(scan >> 3);
        if (group < X68K_KEYGROUP_COUNT &&
            (groups[group] & (uint8_t)(1u << (scan & 7u))) != 0u) {
            gKeyState[key] = 1u;
        }
    }
}


void jlpJoystickReset(jlJoystickE js) {
    (void)js;
    // Digital sticks: nothing to calibrate.
}


// ----- Timing ---------------------------------------------------------------

// There is no plain "wait for vblank" IOCS call -- _iocs_vdispst INSTALLS a
// vertical-display handler rather than blocking -- so poll the MFP's V-DISP
// input directly and edge-detect it. Both spins are bounded so a wedged or
// mis-programmed CRTC degrades to a dropped frame instead of hanging the game.
//
// ASSUMES SUPERVISOR MODE for the $E88001 read. Human68k normally leaves user
// programs in supervisor, unlike TOS on the ST (which needs Super(0L) -- see
// the ST HAL). If that turns out not to hold, this becomes an _iocs_vdispst
// handler bumping gFrameCount instead.
// Vertical-display interrupt handler: the AUTHORITATIVE frame counter.
//
// A polled edge-detector loses ticks, and it loses them exactly when it hurts:
// any frame doing real work between jlFrameCount() calls misses the V-DISP
// transitions that happened meanwhile. Measured: 400 audio refills across ~19
// frames reported ZERO elapsed frames, because nothing polled in between.
// Animation and music tempo would silently run slow under load.
//
// Registered with _iocs_vdispst, which calls this once per vertical display
// period. Plain C (RTS) is the IOCS convention for a _VDISPST handler; IOCS
// owns the interrupt frame and the RTE.
static void vdispHandler(void) {
    gFrameCount++;
}
// Referenced only to keep the handler compiled and honest about its intent
// until the asm thunk exists; see the note in jlpInit.
void *const gX68kVdispHandlerRef = (void *)vdispHandler;


// V-DISP edge detect. This is the PRIMARY frame clock (the interrupt route
// hangs -- see jlpInit), so it is called from every per-frame entry point the
// library owns -- jlpPresent, jlpInputPoll, jlpFrameCount, jlpWaitVBL -- not
// just when the app asks the time. A game that presents or polls input once a
// frame therefore keeps an accurate count even while doing heavy work.
//
// The residual limitation is real and worth knowing: a frame that does NONE of
// those for longer than one V-DISP period still loses ticks. An asm thunk for
// _iocs_vdispst removes it for good.
//
// CRITICAL CONTRACT: jlpFrameCount must be monotonic ON ITS OWN, without
// jlpWaitVBL being called. UBER's timing model (and any game that paces by
// polling rather than blocking) sits in a tight loop reading jlFrameCount and
// never calls jlWaitVBL -- so a counter bumped only inside the wait never
// advances and the caller spins forever. That is exactly what happened here:
// UBER ran for 38,000 frames with a live, moving PC and a frozen screen,
// because it was stuck on its first timed op waiting for a tick that could
// not arrive.
static void vdispPoll(void) {
    uint8_t now;

    if (gVdispInstalled) {
        return;             // the interrupt owns the counter
    }
    now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u);
    if (now != gLastVdisp) {
        gLastVdisp = now;
        if (now != 0u) {
            gFrameCount++;
        }
    }
}


void jlpWaitVBL(void) {
    uint16_t start;
    uint32_t guard;

    // Block until the counter actually moves, so the wait and the counter can
    // never disagree. Bounded, so a wedged CRTC drops a frame rather than
    // hanging the game.
    start = gFrameCount;
    guard = 0ul;
    while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) {
        vdispPoll();
        guard++;
    }
}


uint16_t jlpFrameCount(void) {
    vdispPoll();
    return gFrameCount;
}


#ifdef JOEY_LOG_SERIAL_TEE
// Mirror every log line out RS-232C. Polled and blocking: a diagnostic build
// trades speed for never dropping the line that explains the failure. The
// transmit-ready spin is bounded so an unwired or unopened port degrades to
// slow-but-running instead of wedging the machine being diagnosed.
void jlpLogTee(const char *text) {
    uint32_t guard;

    while (*text != '\0') {
        guard = 0ul;
        while (_iocs_osns232c() == 0 && guard < X68K_TEE_SPIN_LIMIT) {
            guard++;
        }
        if (guard >= X68K_TEE_SPIN_LIMIT) {
            return;                 // port not draining -- give up on this line
        }
        if (*text == '\n') {
            _iocs_out232c((int)'\r');
        }
        _iocs_out232c((int)(unsigned char)*text);
        text++;
    }
}
#endif


uint16_t jlpFrameHz(void) {
    // 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported
    // only -- the generic millisElapsed divides by it.
    return 55u;
}
