// sprite.c - 16x16 fixed-shape 4bpp packed sprite engine for SHR 320
// mode.  See runtime/include/iigs/sprite.h for the API contract and
// the $C035-shadow-gotcha discussion.
//
// Standalone init path (Phase 0.6 decision): no startdesk(), no QD,
// no Window Mgr.  We poke NEWVIDEO ($C029), SCBs ($E1:9D00..), and
// palette 0 ($E1:9E00..) ourselves.  This keeps the sprite probe
// runnable under bare-metal runInMame.sh --check-u8.
//
// Pixel arithmetic notes:
//   - SHR 320 mode is 200 lines x 160 bytes per line = 32000 bytes,
//     based at $E1:2000.  Scan line N starts at $E1:2000 + N*160.
//   - 16x16 sprite = 16 lines x 8 bytes per line = 128 bytes.
//   - 4bpp packed: each byte holds two pixels, HIGH nibble = LEFT.
//   - Transparency: a source nibble == 0 leaves the destination
//     nibble untouched.  Other nibbles overwrite.

#include "iigs/sprite.h"


// ----- SHR memory map constants --------------------------------------
// $C029  NEWVIDEO       bit 7 = 1 to enable SHR
// $E1:2000..$E1:9CFF    SHR pixel data
// $E1:9D00..$E1:9DC7    SCBs (200 bytes, one per scan line)
// $E1:9E00..$E1:9FFF    16 palettes x 32 bytes
#define IIGS_NEWVIDEO     0x00C029UL
#define IIGS_SHR_PIXELS   0xE12000UL
#define IIGS_SHR_SCB      0xE19D00UL
#define IIGS_SHR_PALETTE  0xE19E00UL
#define IIGS_SHR_BYTES_PER_LINE  160U
#define IIGS_SHR_LINE_COUNT      200U
#define IIGS_SPRITE_HEIGHT       16U
#define IIGS_SPRITE_BYTES        128U   // 8 bytes per line x 16 lines


// ----- private state -------------------------------------------------
// Built-in 16-sprite save buffer.  Placed at bank 0 $A000 by the
// linker (BSS default for sprite-probe builds is --bss-base 0xA000;
// $A000..$AFFF for the buffer is OUTSIDE the $C035 shadow window).
//
// In linker layouts where BSS is bumped down (e.g. tiny demos), the
// reviewer's gotcha kicks in: bank-0 $2000..$9FFF mirrors to
// $E1:2000..$9FFF.  Callers in such layouts MUST call
// iigsSpriteAttachBuffer() with a caller-supplied buffer above $A000.

static uint8_t       gBuiltinSaveBuf[IIGS_SPRITE_MAX_DEFAULT * IIGS_SPRITE_BYTES];
static uint8_t *     gSaveBuf      = gBuiltinSaveBuf;
static uint16_t      gSaveCap      = IIGS_SPRITE_MAX_DEFAULT;
static IigsSpriteT   gSpriteList[IIGS_SPRITE_MAX_DEFAULT];
static uint16_t      gSpriteCount  = 0;


// Default 16-color palette: a simple R/G/B/W ramp.  Entry 0 = black
// (so transparency in the source maps to "no plot" rather than a
// visible black pixel; the BACKGROUND shows through, which is the
// correct semantic).  Entries 1..15 walk through a grayscale-ish
// palette for the sprite-probe test image.
static const uint16_t gDefaultPalette[16] = {
    0x0000,  // 0  black (transparent in source semantics)
    0x0F00,  // 1  red
    0x00F0,  // 2  green
    0x000F,  // 3  blue
    0x0FF0,  // 4  yellow
    0x0F0F,  // 5  magenta
    0x00FF,  // 6  cyan
    0x0FFF,  // 7  white
    0x0888,  // 8  light gray
    0x0444,  // 9  dark gray
    0x0F88,  // 10 pink
    0x08F8,  // 11 light green
    0x088F,  // 12 light blue
    0x0FF8,  // 13 light yellow
    0x0F8F,  // 14 light magenta
    0x08FF,  // 15 light cyan
};


// ----- forward decls (alphabetized) ----------------------------------
static void          blitSprite(const IigsSpriteT *s);
static void          restoreBackground(const IigsSpriteT *s, const uint8_t *save);
static void          saveBackground(const IigsSpriteT *s, uint8_t *save);
static uint32_t      shrLineAddr(uint16_t y);


// Compute the 24-bit address of the start of SHR scan line y.
// y MUST be < 200.
static uint32_t shrLineAddr(uint16_t y) {
    return IIGS_SHR_PIXELS + (uint32_t)y * (uint32_t)IIGS_SHR_BYTES_PER_LINE;
}


void iigsSpriteInit(void) {
    // 1. Turn on SHR via NEWVIDEO bit 7.  NEWVIDEO is a bank-0 soft
    //    switch ($C029); bit 7 = SHR enable, bit 6 = linearize,
    //    bit 5 = B&W, bit 0 = bank-0 mirror.  We want bit 7 only.
    *(volatile uint8_t *)IIGS_NEWVIDEO = 0xC1;

    // 2. SCBs: 200 entries at $E1:9D00.  Value 0x00 = 320 mode,
    //    palette 0, no fill, no interrupt.  Wipe the unused 56 bytes
    //    after row 199 to a known value too (matches Apple's spec).
    {
        volatile uint8_t *scb = (volatile uint8_t *)IIGS_SHR_SCB;
        for (uint16_t i = 0; i < 256U; i++) {
            scb[i] = 0x00;
        }
    }

    // 3. Palette 0 to the default ramp.
    iigsSpriteSetPalette((const uint16_t *)0);

    // 4. Clear the framebuffer to color 0 (black background).  32000
    //    bytes at $E1:2000..$9CFF.  Use 16-bit stores via the C
    //    compiler's natural codegen.
    {
        volatile uint16_t *p = (volatile uint16_t *)IIGS_SHR_PIXELS;
        uint16_t           n = (IIGS_SHR_BYTES_PER_LINE * IIGS_SHR_LINE_COUNT) / 2U;  // 16000 words
        for (uint16_t i = 0; i < n; i++) {
            p[i] = 0;
        }
    }

    // 5. Reset sprite list.
    gSpriteCount = 0;
}


void iigsSpriteSetPalette(const uint16_t *palette16) {
    const uint16_t *src = (palette16 != (const uint16_t *)0) ? palette16 : gDefaultPalette;
    volatile uint16_t *dst = (volatile uint16_t *)IIGS_SHR_PALETTE;
    for (uint16_t i = 0; i < 16U; i++) {
        dst[i] = src[i];
    }
}


uint16_t iigsSpriteAttachBuffer(void *buf, size_t size) {
    if (buf == (void *)0 || size == 0) {
        gSaveBuf = gBuiltinSaveBuf;
        gSaveCap = IIGS_SPRITE_MAX_DEFAULT;
        return IIGS_SPRITE_MAX_DEFAULT;
    }
    gSaveBuf = (uint8_t *)buf;
    uint16_t maxSprites = (uint16_t)(size / (size_t)IIGS_SPRITE_BYTES);
    if (maxSprites > IIGS_SPRITE_MAX_DEFAULT) {
        // The list array is fixed-size; cap at IIGS_SPRITE_MAX_DEFAULT
        // (callers wanting more sprites should also enlarge the list,
        // which is a follow-up).
        maxSprites = IIGS_SPRITE_MAX_DEFAULT;
    }
    gSaveCap = maxSprites;
    return maxSprites;
}


void iigsSpriteBegin(void) {
    gSpriteCount = 0;
}


uint16_t iigsSpriteAdd(const IigsSpriteT *s) {
    if (gSpriteCount >= gSaveCap) {
        return 0xFFFFU;
    }
    uint16_t idx = gSpriteCount;
    gSpriteList[idx] = *s;
    // Force even x: drop bit 0 so byte arithmetic is exact.
    gSpriteList[idx].x = (uint16_t)(s->x & 0xFFFEU);
    gSpriteCount = (uint16_t)(idx + 1);
    return idx;
}


uint16_t iigsSpriteCount(void) {
    return gSpriteCount;
}


// Copy 8 bytes per line x 16 lines from the SHR framebuffer (under
// sprite *s) into the per-slot save area (128 bytes).
static void saveBackground(const IigsSpriteT *s, uint8_t *save) {
    uint16_t byteX = (uint16_t)(s->x >> 1);          // byte offset within line
    uint16_t y     = s->y;
    for (uint16_t row = 0; row < IIGS_SPRITE_HEIGHT; row++) {
        uint32_t addr = shrLineAddr(y + row) + (uint32_t)byteX;
        const volatile uint8_t *src = (const volatile uint8_t *)addr;
        for (uint16_t col = 0; col < 8U; col++) {     // 8 bytes per sprite row
            save[(uint16_t)(row * 8U + col)] = src[col];
        }
    }
}


// Blit the sprite over the framebuffer with transparent-zero-nibble
// semantics.  Each source byte holds two pixels (high nibble = LEFT).
// A nibble == 0 leaves the corresponding destination nibble untouched.
static void blitSprite(const IigsSpriteT *s) {
    uint16_t byteX = (uint16_t)(s->x >> 1);
    uint16_t y     = s->y;
    const uint8_t *src = s->pixels;
    for (uint16_t row = 0; row < IIGS_SPRITE_HEIGHT; row++) {
        uint32_t addr = shrLineAddr(y + row) + (uint32_t)byteX;
        volatile uint8_t *dst = (volatile uint8_t *)addr;
        for (uint16_t col = 0; col < 8U; col++) {
            uint8_t  sb    = src[(uint16_t)(row * 8U + col)];
            uint8_t  hi    = (uint8_t)(sb & 0xF0U);
            uint8_t  lo    = (uint8_t)(sb & 0x0FU);
            uint8_t  cur   = dst[col];
            // Transparent nibble (== 0 in source) keeps current dest
            // nibble; opaque nibble overwrites.
            uint8_t  newHi = (hi != 0U) ? hi : (uint8_t)(cur & 0xF0U);
            uint8_t  newLo = (lo != 0U) ? lo : (uint8_t)(cur & 0x0FU);
            dst[col] = (uint8_t)(newHi | newLo);
        }
    }
}


// Inverse of saveBackground: copy 128 bytes from the save area back
// onto the framebuffer at the sprite's recorded position.
static void restoreBackground(const IigsSpriteT *s, const uint8_t *save) {
    uint16_t byteX = (uint16_t)(s->x >> 1);
    uint16_t y     = s->y;
    for (uint16_t row = 0; row < IIGS_SPRITE_HEIGHT; row++) {
        uint32_t addr = shrLineAddr(y + row) + (uint32_t)byteX;
        volatile uint8_t *dst = (volatile uint8_t *)addr;
        for (uint16_t col = 0; col < 8U; col++) {
            dst[col] = save[(uint16_t)(row * 8U + col)];
        }
    }
}


void iigsSpriteRenderAll(void) {
    for (uint16_t i = 0; i < gSpriteCount; i++) {
        IigsSpriteT *s   = &gSpriteList[i];
        uint8_t     *sav = &gSaveBuf[(uint16_t)(i * IIGS_SPRITE_BYTES)];
        saveBackground(s, sav);
        blitSprite(s);
    }
}


void iigsSpriteEraseAll(void) {
    // Walk in reverse so overlapping sprites de-occlude correctly:
    // the LAST sprite painted is the TOP sprite; restoring its save
    // area first uncovers what was underneath it (which may include
    // earlier sprites that we then restore in turn).
    uint16_t i = gSpriteCount;
    while (i > 0) {
        i--;
        IigsSpriteT *s   = &gSpriteList[i];
        const uint8_t *sav = &gSaveBuf[(uint16_t)(i * IIGS_SPRITE_BYTES)];
        restoreBackground(s, sav);
    }
}
