// Planar 68k sprite codegen for the Amiga (4 separate bitplanes).
//
// Emits a position-independent, cdecl-callable DRAW routine that blits
// one sprite into the Amiga's 4 bitplanes. The sprite's tileData is
// fixed at compile time, so the chunky->planar (c2p) conversion and
// every destination offset are constant-folded here; the emitted
// routine is a flat unrolled chain of byte writes with the plane bytes
// and masks baked in as immediates.
//
// Calling convention (m68k cdecl, invoked through a C function pointer
// by spriteCompiledDraw in spriteCompile.c):
//   void draw(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
//   each p_i = plane_base + y*40 + x/8  (top-left byte of plane i).
// Args sit at 4/8/12/16(sp) on entry. The routine saves a2/a3, loads
// the four plane pointers into a0..a3, then writes plane bytes at
// d16(a_i) with the offset (row*40 + byteCol) baked in -- no per-row
// pointer advance (offsets stay well inside the 8000-byte plane and the
// signed 16-bit displacement range for any on-surface sprite). Only
// d0/a0/a1 are cdecl scratch; a2/a3 are saved/restored via movem.
//
// ALL 8 shifts (x mod 8) are compiled (NATIVE-PERF Phase 1). At shift 0
// a sprite tile column maps to exactly one plane byte; at shift s > 0 a
// column's bits split across two adjacent dest bytes (low 8-s bits of
// the left byte, high s bits of the right byte), and the seam byte
// between two source columns receives disjoint contributions from both.
// The emitter pre-shifts and OR-merges those contributions per dest
// byte at emit time (destMask/destData accumulators), then emits one
// write group per non-empty dest byte -- the same opcode inventory the
// proven shift-0 path used:
//   fully opaque (mask==0xFF): move.b #data,off(a_i)   (direct store)
//   mixed: move.b off(a_i),d0; andi.b #~mask,d0; [ori.b #data,d0;]
//          move.b d0,off(a_i)
// This is algebraically identical to jlpSpriteDrawPlanes' two sequential
// seam RMWs (disjoint masks compose), so pixels are bit-for-bit the
// walker's -- the frozen unaligned/sweep16 golden hashes gate it.

#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitC2p.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"


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

// Tile geometry (TILE_BYTES / TILE_BYTES_PER_ROW / TILE_PIXELS_PER_SIDE)
// comes from joey/tile.h.
#define AMIGA_PLANE_STRIDE    (SURFACE_WIDTH / 8)  // 40 bytes/plane-row
#define AMIGA_PLANES          4u

// 68k opcode words (big-endian). Per-plane move.b variants are indexed
// by plane (a0..a3); andi.b/ori.b only ever target d0.
//   movem.l a2-a3,-(sp) : 48E7 0030  (predec mask is bit-reversed: a3=4,a2=5)
//   movem.l (sp)+,a2-a3 : 4CDF 0C00  (postinc mask: a2=10,a3=11)
//   movea.l d16(sp),aN  : 206F/226F/246F/266F  dddd
//   move.b  d16(aN),d0  : 1028/1029/102A/102B  dddd
//   andi.b  #imm,d0     : 0200 00ii
//   ori.b   #imm,d0     : 0000 00ii
//   move.b  d0,d16(aN)  : 1140/1340/1540/1740  dddd
//   move.b  #imm,d16(aN): 117C/137C/157C/177C  00ii dddd
//   rts                 : 4E75
#define OP_MOVEM_SAVE     0x48E7u
#define OP_MOVEM_SAVE_M   0x0030u
#define OP_MOVEM_REST     0x4CDFu
#define OP_MOVEM_REST_M   0x0C00u
#define OP_ANDI_B_D0      0x0200u
#define OP_ORI_B_D0       0x0000u
#define OP_RTS            0x4E75u

static const uint16_t OP_MOVEA_SP_AN[AMIGA_PLANES]  = { 0x206Fu, 0x226Fu, 0x246Fu, 0x266Fu };
static const uint16_t OP_MOVE_B_AN_D0[AMIGA_PLANES] = { 0x1028u, 0x1029u, 0x102Au, 0x102Bu };
static const uint16_t OP_MOVE_B_D0_AN[AMIGA_PLANES] = { 0x1140u, 0x1340u, 0x1540u, 0x1740u };
static const uint16_t OP_MOVE_B_IMM_AN[AMIGA_PLANES] = { 0x117Cu, 0x137Cu, 0x157Cu, 0x177Cu };

// Stack displacement of arg p_i AFTER the prologue pushes a2/a3 (8 bytes):
// entry 4/8/12/16(sp) -> 12/16/20/24(sp).
#define ARG_DISP(i)       (uint16_t)(12u + (uint16_t)(i) * 4u)

#define PROLOGUE_BYTES    20u  // movem(4) + 4 * movea.l(4)
#define EPILOGUE_BYTES    6u   // movem(4) + rts(2)


// ----- Emit helpers -----

// c2pColumn (identical pixel walk to jlpSpriteDrawPlanes' c2p loop)
// and putWord are shared with the ST emitter via spriteEmitC2p.h.


// ----- Emit API -----

// Per-row accumulators. File statics, NOT stack locals: the compile
// path runs deep (jlSpriteCompile -> planBuild -> measure -> emitter,
// with vsnprintf-depth logging possible underneath) and a ~200-byte
// stack frame here overflowed the Amiga shell's default 4 KB task
// stack nondeterministically. Emit-time only and single-threaded, so
// statics are safe.
static uint8_t gDestMask[SURFACE_WIDTH / 8 + 1];
static uint8_t gDestData[SURFACE_WIDTH / 8 + 1][AMIGA_PLANES];


uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t cursor;
    uint16_t wTiles;
    uint16_t srcH;
    uint16_t lastDest;
    uint8_t  i;
    int16_t  row;
    int16_t  byteCol;
    int16_t  d;

    if (shift >= 8u) {
        return 0u;   // Amiga shift space is x & 7; defensive.
    }
    if (shift != 0u && sp->widthPx >= SURFACE_WIDTH) {
        // A full-width sprite can never draw unaligned on-surface (the
        // dispatcher gate requires x + w <= 320 with x > 0), so a
        // shifted variant would be dead bytes in the arena.
        return 0u;
    }
    wTiles = sp->widthTiles;
    srcH   = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    // Shift 0 writes wTiles dest bytes per row; shifted rows spill one.
    lastDest = (shift == 0u) ? (uint16_t)(wTiles - 1u) : wTiles;

    cursor = 0u;

    // Prologue: save a2/a3, load p0..p3 into a0..a3.
    if (cursor + PROLOGUE_BYTES > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEM_SAVE);
    cursor = putWord(out, cursor, OP_MOVEM_SAVE_M);
    for (i = 0u; i < AMIGA_PLANES; i++) {
        cursor = putWord(out, cursor, OP_MOVEA_SP_AN[i]);
        cursor = putWord(out, cursor, ARG_DISP(i));
    }

    for (row = 0; row < (int16_t)srcH; row++) {
        uint16_t       tileY       = (uint16_t)((uint16_t)row >> 3);
        uint16_t       inTileY     = (uint16_t)((uint16_t)row & 7u);
        const uint8_t *tileRowBase = sp->tileData
                                   + (uint32_t)tileY * wTiles * TILE_BYTES
                                   + (uint32_t)inTileY * TILE_BYTES_PER_ROW;
        uint16_t       rowOff      = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE);

        // Phase 1: accumulate this row's per-dest-byte mask/data. A
        // source column at shift s contributes its low 8-s bits to dest
        // byte c (>> s) and its high s bits to dest byte c+1 (<< 8-s);
        // the seam byte's two contributions occupy disjoint bit ranges,
        // so OR-accumulation reproduces the walker's two sequential
        // RMWs exactly.
        for (d = 0; d <= (int16_t)lastDest; d++) {
            gDestMask[d]    = 0u;
            gDestData[d][0] = 0u;
            gDestData[d][1] = 0u;
            gDestData[d][2] = 0u;
            gDestData[d][3] = 0u;
        }
        for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
            const uint8_t *trp = tileRowBase + (uint32_t)byteCol * TILE_BYTES;
            uint8_t        plane[4];
            uint8_t        opaque;

            c2pColumn(trp, plane, &opaque);
            if (opaque == 0u) {
                continue;
            }
            if (shift == 0u) {
                gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | opaque);
                for (i = 0u; i < AMIGA_PLANES; i++) {
                    gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | plane[i]);
                }
            } else {
                gDestMask[byteCol]      = (uint8_t)(gDestMask[byteCol]      | (uint8_t)(opaque >> shift));
                gDestMask[byteCol + 1]  = (uint8_t)(gDestMask[byteCol + 1]  | (uint8_t)(opaque << (8u - shift)));
                for (i = 0u; i < AMIGA_PLANES; i++) {
                    gDestData[byteCol][i]     = (uint8_t)(gDestData[byteCol][i]     | (uint8_t)(plane[i] >> shift));
                    gDestData[byteCol + 1][i] = (uint8_t)(gDestData[byteCol + 1][i] | (uint8_t)(plane[i] << (8u - shift)));
                }
            }
        }

        // Phase 2: emit one write group per non-empty dest byte.
        for (d = 0; d <= (int16_t)lastDest; d++) {
            uint8_t  m = gDestMask[d];
            uint16_t off;

            if (m == 0u) {
                continue;
            }
            off = (uint16_t)(rowOff + (uint16_t)d);

            for (i = 0u; i < AMIGA_PLANES; i++) {
                if (m == 0xFFu) {
                    // Fully covered dest byte: store the data directly.
                    // move.b #data,off(a_i)  (6 bytes)
                    if (cursor + 6u > cap) {
                        return SPRITE_EMIT_OVERFLOW;
                    }
                    cursor = putWord(out, cursor, OP_MOVE_B_IMM_AN[i]);
                    cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]);
                    cursor = putWord(out, cursor, off);
                } else {
                    // Exact reservation (see ST emitter): a worst-case reserve
                    // would falsely overflow the emit pass on trailing shorter
                    // writes. +ori.b only when this plane sets bits.
                    uint32_t planeBytes = (gDestData[d][i] != 0u) ? 16u : 12u;

                    if (cursor + planeBytes > cap) {
                        return SPRITE_EMIT_OVERFLOW;
                    }
                    // move.b off(a_i),d0
                    cursor = putWord(out, cursor, OP_MOVE_B_AN_D0[i]);
                    cursor = putWord(out, cursor, off);
                    // andi.b #~m,d0
                    cursor = putWord(out, cursor, OP_ANDI_B_D0);
                    cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~m));
                    // ori.b #data,d0  (skip if no bits)
                    if (gDestData[d][i] != 0u) {
                        cursor = putWord(out, cursor, OP_ORI_B_D0);
                        cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]);
                    }
                    // move.b d0,off(a_i)
                    cursor = putWord(out, cursor, OP_MOVE_B_D0_AN[i]);
                    cursor = putWord(out, cursor, off);
                }
            }
        }
    }

    // Epilogue: restore a2/a3, return.
    if (cursor + EPILOGUE_BYTES > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEM_REST);
    cursor = putWord(out, cursor, OP_MOVEM_REST_M);
    cursor = putWord(out, cursor, OP_RTS);
    return cursor;
}


// Compiled save/restore (NATIVE-PERF Phase 1): plane-run copiers over
// the 16-px-group window. shift is the width CLASS (0 = ceil(w/16)
// groups, 1 = one more; see SPRITE_SAVE_CLASS), so only classes 0/1
// emit. cdecl void copy(p0, p1, p2, p3, buf): the dispatcher passes
// each plane base already advanced to the window's first byte
// (y*40 + bx/8, even because bx % 16 == 0 and planes are AllocMem'd),
// and the routine copies run = nGroups*2 bytes per plane per row in
// [p0 run][p1 run][p2 run][p3 run] order -- byte-identical to the
// interpreted aligned walker's plane-run format (format law). Row
// displacements are baked (max 199*40 + 4 < d16 range); the buffer
// side walks contiguously via (a1)+.
//   movem.l a2-a4,-(sp)   : 48E7 0038  (predec mask bit-reversed)
//   movem.l (sp)+,a2-a4   : 4CDF 1C00
//   movea.l d16(sp),aN    : a0=206F a2=246F a3=266F a4=286F a1=226F
//   SAVE  move.l d16(aP),(a1)+ : 22E8/22EA/22EB/22EC  dddd
//   SAVE  move.w d16(aP),(a1)+ : 32E8/32EA/32EB/32EC  dddd
//   REST  move.l (a1)+,d16(aP) : 2159/2559/2759/2959  dddd
//   REST  move.w (a1)+,d16(aP) : 3159/3559/3759/3959  dddd
#define OP_MOVEM_SAVE_A24   0x0038u
#define OP_MOVEM_REST_A24   0x1C00u

static const uint16_t OP_COPY_ARG_AN[5]     = { 0x206Fu, 0x246Fu, 0x266Fu, 0x286Fu, 0x226Fu };
static const uint16_t OP_SAVE_L_AN[AMIGA_PLANES] = { 0x22E8u, 0x22EAu, 0x22EBu, 0x22ECu };
static const uint16_t OP_SAVE_W_AN[AMIGA_PLANES] = { 0x32E8u, 0x32EAu, 0x32EBu, 0x32ECu };
static const uint16_t OP_REST_L_AN[AMIGA_PLANES] = { 0x2159u, 0x2559u, 0x2759u, 0x2959u };
static const uint16_t OP_REST_W_AN[AMIGA_PLANES] = { 0x3159u, 0x3559u, 0x3759u, 0x3959u };

// Arg displacements after the 12-byte movem push (+4 return PC):
// p0/p1/p2/p3/buf at entry 4/8/12/16/20 -> 16/20/24/28/32.
#define COPY_ARG_DISP(i)    (uint16_t)(16u + (uint16_t)(i) * 4u)

static uint32_t emitPlaneRunCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool isRestore) {
    uint32_t cursor;
    uint16_t nGroups;
    uint16_t run;
    uint16_t srcH;
    int16_t  row;
    uint8_t  p;
    uint16_t i;

    if (shift > 1u) {
        return 0u;
    }
    nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift);
    if ((uint16_t)(nGroups << 4) > SURFACE_WIDTH) {
        // Unreachable class for this sprite width; both save and
        // restore bail identically (pair provenance).
        return 0u;
    }
    run  = (uint16_t)(nGroups << 1);   // bytes per plane per row
    srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);

    cursor = 0u;
    if (cursor + 24u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEM_SAVE);
    cursor = putWord(out, cursor, OP_MOVEM_SAVE_A24);
    for (i = 0u; i < 5u; i++) {
        cursor = putWord(out, cursor, OP_COPY_ARG_AN[i]);
        cursor = putWord(out, cursor, COPY_ARG_DISP(i));
    }
    for (row = 0; row < (int16_t)srcH; row++) {
        uint16_t rowDisp = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE);

        // Worst case per row: 4 planes x (longs + one word) ops at
        // 4 bytes each.
        if (cursor + (uint32_t)(4u * ((run >> 2) + 1u) * 4u) + 6u > cap) {
            return SPRITE_EMIT_OVERFLOW;
        }
        for (p = 0u; p < AMIGA_PLANES; p++) {
            uint16_t disp  = rowDisp;
            uint16_t longs = (uint16_t)(run >> 2);

            for (i = 0u; i < longs; i++) {
                cursor = putWord(out, cursor, isRestore ? OP_REST_L_AN[p] : OP_SAVE_L_AN[p]);
                cursor = putWord(out, cursor, disp);
                disp   = (uint16_t)(disp + 4u);
            }
            if ((run & 2u) != 0u) {
                cursor = putWord(out, cursor, isRestore ? OP_REST_W_AN[p] : OP_SAVE_W_AN[p]);
                cursor = putWord(out, cursor, disp);
            }
        }
    }
    if (cursor + 6u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEM_REST);
    cursor = putWord(out, cursor, OP_MOVEM_REST_A24);
    cursor = putWord(out, cursor, OP_RTS);
    return cursor;
}


uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    return emitPlaneRunCopy(out, cap, sp, shift, false);
}


uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    return emitPlaneRunCopy(out, cap, sp, shift, true);
}
