// cursor.c - iigs/cursor.h implementation.  Push/pop stack of
// CursorRecord COPIES so transient cursor swaps (busy, I-beam, etc.)
// can be installed and restored without the caller juggling pointers
// into Memory-Manager-relocatable storage.
//
// Phase 2.5 (2026-06-01) scope: thin wrappers + Wait/IBeam ROM shapes.
// Embedded cursor blobs are NOT in scope; callers wanting a custom
// cursor should construct their own ORCA-shape Cursor record and pass
// it to SetCursor() directly.
//
// The save stack stores 128-byte COPIES (not pointers) - the largest
// standard IIgs cursor is 16x16 (ROM arrow): 4-byte header + 32 bytes
// data + 32 bytes mask + 4 bytes hotspot = 72 bytes.  128 is generous.
// Copying the whole record is mandatory: toolset-owned cursors live in
// MM-relocatable handles and the live pointer can move out from under
// us between push and pop if the heap compacts.

#include "iigs/cursor.h"
#include "iigs/toolbox.h"


// Size of one save slot.  Covers the full 16x16 ROM-style cursor with
// headroom for slightly larger custom records (24x16 etc).  Pushes of
// cursors larger than this truncate the copy and return success - the
// pop will then restore a partial record which still has valid header
// + data + mask but a (possibly garbage) hotspot.  Document this
// limitation in the header if a larger cursor ever ships.
#define CURSOR_COPY_BYTES 128

// CursorRecord prefix layout (per ORCA quickdraw.h:112-118):
//   Word cursorHeight  - size in BYTES (not pixels)
//   Word cursorWidth   - enclosing rectangle width in WORDS
//   Word cursorData[]  - cursorHeight/2 words of bitmap
//   Word cursorMask[]  - cursorHeight/2 words of mask
//   Point cursorHotSpot - 4 bytes (h, v)
// So total = 4 (header) + 2*cursorHeight (data+mask) + 4 (hotspot).
// We compute the live record size from the header so partial copies
// don't drag in trailing slop from neighboring Memory Mgr blocks.
#define CURSOR_HEADER_BYTES   4
#define CURSOR_HOTSPOT_BYTES  4


static unsigned char gCursorStack[IIGS_CURSOR_STACK_DEPTH][CURSOR_COPY_BYTES];
static unsigned short gCursorStackBytes[IIGS_CURSOR_STACK_DEPTH];
static unsigned short gCursorStackDepth = 0;

// Application-registered "default" cursor.  Pop returns to this when
// the save stack underflows; that way a mismatched push/pop pair still
// lands the user in a known cursor instead of leaking ROM state.
static const IigsCursorT *gRegisteredCursor = (const IigsCursorT *)0;


// Compute the byte length of a live CursorRecord from its header.
// Returns 0 if the pointer is NULL.  Clamps to CURSOR_COPY_BYTES so
// the memcpy below never overruns the save slot.
static unsigned short cursorRecordBytes(const void *p) {
    if (!p) {
        return 0;
    }
    const unsigned short *w = (const unsigned short *)p;
    // cursorHeight is in bytes; data + mask occupy 2*cursorHeight.
    unsigned short height = w[0];
    unsigned short total = (unsigned short)(CURSOR_HEADER_BYTES + 2U * height + CURSOR_HOTSPOT_BYTES);
    if (total > CURSOR_COPY_BYTES) {
        total = CURSOR_COPY_BYTES;
    }
    return total;
}


// Save the currently-active cursor into the next stack slot.  Returns
// 0 on success, nonzero on stack overflow or NULL live cursor (which
// means InitCursor() never ran - the InitCursor invariant from the
// header).
static uint16_t pushCurrent(void) {
    if (gCursorStackDepth >= IIGS_CURSOR_STACK_DEPTH) {
        return 1;
    }
    void *live = GetCursorAdr();
    if (!live) {
        // Cursor Mgr never initialized.  Hard-error per the
        // InitCursor invariant - SetCursor on a NULL save buffer
        // would walk through 0 in ROM.
        return 2;
    }
    unsigned short n = cursorRecordBytes(live);
    unsigned char *dst = gCursorStack[gCursorStackDepth];
    const unsigned char *src = (const unsigned char *)live;
    for (unsigned short i = 0; i < n; i++) {
        dst[i] = src[i];
    }
    gCursorStackBytes[gCursorStackDepth] = n;
    gCursorStackDepth++;
    return 0;
}


uint16_t iigsCursorPushArrow(void) {
    uint16_t rc = pushCurrent();
    if (rc != 0) {
        return rc;
    }
    // InitCursor reinstalls the ROM arrow shape without reallocating
    // the Cursor Mgr save buffer (idempotent after first call from
    // startdesk()).  Same effect as SetCursor(romArrow) without us
    // having to fish the arrow's address out of toolset internals.
    InitCursor();
    return 0;
}


uint16_t iigsCursorPushBusy(void) {
    uint16_t rc = pushCurrent();
    if (rc != 0) {
        return rc;
    }
    // WaitCursor (QDAuxiliary 0x0A12) installs the ROM wristwatch
    // cursor.  Internally calls SetCursor on the ROM busy shape.
    WaitCursor();
    return 0;
}


uint16_t iigsCursorPop(void) {
    if (gCursorStackDepth == 0) {
        // Underflow: try the registered fallback so a stray Pop
        // doesn't leave us with whatever transient cursor happens to
        // be live.  If the application never called Register either,
        // hard-error so the caller notices the mismatch.
        if (gRegisteredCursor) {
            SetCursor((void *)gRegisteredCursor);
            return 0;
        }
        return 1;
    }
    gCursorStackDepth--;
    // Install our saved COPY directly.  SetCursor reads the record by
    // pointer and copies bytes into the Cursor Mgr's working area; it
    // does NOT retain our pointer past the call, so it's safe to hand
    // it a pointer into our gCursorStack[].
    SetCursor(gCursorStack[gCursorStackDepth]);
    return 0;
}


uint16_t iigsCursorRegister(const IigsCursorT *cursor) {
    gRegisteredCursor = cursor;
    if (cursor) {
        if (!GetCursorAdr()) {
            // InitCursor invariant: refuse to install before the
            // Cursor Mgr has been brought up.  Keep the pointer
            // registered for a later (post-InitCursor) Pop fallback.
            return 2;
        }
        SetCursor((void *)cursor);
    }
    return 0;
}
