// Tiles as 8x8 surface regions. The whole API is byte-shoveling
// between jlSurfaceT regions (or between a jlSurfaceT region and a
// stack jlTileT buffer); no separate tileset container, no allocator.
//
// Block coords (bx, by) map to pixel (bx*8, by*8). At 4bpp packed
// each tile row is 4 bytes wide, so byte-aligned row copies are the
// inner loop for everything except the masked / transparent variant,
// which has to read-modify-write each byte to preserve destination
// pixels under transparent (color-0 by convention) source nibbles.

#include <stddef.h>

#include "joey/tile.h"
#include "port.h"
#include "surfaceInternal.h"

// The chunky tile inner loops live in src/generic/genericTile.c (the
// jlpGenericTile* ops); this TU is validation, dispatch, and dirty
// marking only -- it never touches pixel bytes.

// Block -> pixel coordinate for the dirty-rect bookkeeping below.
// Spelled as a shift because ORCA-C does not strength-reduce the *8
// into one -- it emits the ~150-cycle software multiply helper, the
// same trap SURFACE_ROW_OFFSET in surfaceInternal.h documents
// (PERF-AUDIT #48). The shift count 3 assumes TILE_PIXELS_PER_SIDE is
// 8; the guard keeps the two in sync.
#if TILE_PIXELS_PER_SIDE != 8
#error "TILE_BLOCK_TO_PIXEL assumes TILE_PIXELS_PER_SIDE == 8"
#endif
#define TILE_BLOCK_TO_PIXEL(_b) ((uint16_t)((uint16_t)(_b) << 3))

// Whole-tile dirty mark (NATIVE-PERF Phase 2 R1). Every whole-tile op
// marks the compile-time-constant rect h = TILE_PIXELS_PER_SIDE with
// word band exactly [bx*2, bx*2 + 1]: 8 px per tile over 4 px per
// dirty word means SURFACE_WORD_INDEX(bx*8) == bx*2 and
// SURFACE_WORD_INDEX(bx*8 + 7) == bx*2 + 1. Deriving the band from bx
// and widening the 8 rows inline replaces the out-of-line
// surfaceMarkDirtyRows call (~800 cycles measured steady-state on the
// 68000 ports). Callers own the `s == gStage` gate (non-stage
// surfaces carry no dirty state), exactly like surfaceMarkDirtyRect.
// The IIgs keeps its single-JSL asm marker -- already the cheap path
// there.
//
// Compile-time proof of the band identity (breaks the build if either
// the tile side or the 4-px-per-word grain ever changes):
typedef char tileMarkBandProof[(SURFACE_WORD_INDEX(TILE_PIXELS_PER_SIDE) == 2) ? 1 : -1];

// jlTileMapPaste indexes its tileset as base + (idx << 5); pin the
// struct size so that shift can never drift from sizeof(jlTileT).
typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1];

#ifdef JOEYLIB_PLATFORM_IIGS
#define tileMarkDirty(_pixelY, _bx) iigsMarkDirtyRowsInner((uint16_t)(_pixelY), (uint16_t)((_pixelY) + TILE_PIXELS_PER_SIDE), (uint16_t)((uint16_t)(_bx) << 1), (uint16_t)(((uint16_t)(_bx) << 1) + 1u))
// Fill/copy/paste FUSE the mark into their stage-arm asm entries on
// the IIgs (port.h jlpTile* macros -> iigsTile*Mark, NATIVE-PERF
// Phase 2 Step D), so their C-side mark compiles out here.
// copyMasked/pasteMono still mark separately via tileMarkDirty.
#define tileMarkDirtyFused(_pixelY, _bx) ((void)0)
#else
// On non-IIgs ports nothing fuses the mark, so the "fused" spelling
// is the plain helper.
#define tileMarkDirtyFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx))
// Explicit 8-row unroll: gcc-6.5 -O2 keeps the equivalent loop rolled
// (8 iterations of compare + spill-reload, ~670 cycles); the d16(An)
// unroll is ~450. Same widen semantics, row by row, as the loop.
#define TILE_MARK_ROW(_i) do { \
    if (minWord < minPtr[_i]) { \
        minPtr[_i] = minWord; \
    } \
    if (maxWord > maxPtr[_i]) { \
        maxPtr[_i] = maxWord; \
    } \
} while (0)

static void tileMarkDirty(uint16_t pixelY, uint8_t bx) {
    uint8_t *minPtr;
    uint8_t *maxPtr;
    uint8_t  minWord;
    uint8_t  maxWord;

    minWord = (uint8_t)(bx << 1);
    maxWord = (uint8_t)(minWord + 1u);
    minPtr  = &gStageMinWord[pixelY];
    maxPtr  = &gStageMaxWord[pixelY];
    TILE_MARK_ROW(0);
    TILE_MARK_ROW(1);
    TILE_MARK_ROW(2);
    TILE_MARK_ROW(3);
    TILE_MARK_ROW(4);
    TILE_MARK_ROW(5);
    TILE_MARK_ROW(6);
    TILE_MARK_ROW(7);
}
#endif

// Prototype (drawTextUnion is the only other static in this TU).
static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds);


// jlDrawText's per-glyph dirty-union tracking, hoisted out of the
// glyph loop into a noinline helper: keeping five live union locals
// inside the loop overflows the llvm-mos w65816 register allocator
// ("ran out of registers" hard error). bounds[] = {minCx, maxCx,
// minCy, maxCy, drewAny}. noinline is portable gcc/clang syntax.
static void __attribute__((noinline)) drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds) {
    if (bounds[4] == 0u) {
        bounds[4] = 1u;
        bounds[0] = cx;
        bounds[1] = cx;
        bounds[2] = cy;
    } else {
        if (cx < bounds[0]) {
            bounds[0] = cx;
        }
        if (cx > bounds[1]) {
            bounds[1] = cx;
        }
    }
    // cy never decreases, so the last drawn glyph's row is always the
    // union's bottom edge.
    bounds[3] = cy;
}


// ----- Public API (alphabetical) -----

void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str) {
    uint16_t entry;
    uint8_t  cx;
    uint8_t  cy;
    uint8_t  ch;
    uint8_t  srcBx;
    uint8_t  srcBy;
    uint8_t  bounds[5];

    if (dst == NULL || fontSurface == NULL || asciiMap == NULL || str == NULL) {
        return;
    }
    cx = bx;
    cy = by;
    // Entry sanitation (PERF-AUDIT #22 verifier correction): the main
    // loop below hands cx/cy straight to jlpTileCopyMasked, which does
    // no bounds checking, so an out-of-range start must be disposed of
    // here -- the wrap logic only guarantees in-range cx/cy after the
    // first wrap. The historical contract is preserved bit-for-bit
    // (the chk:drawText-offgrid golden covers it): characters landing
    // on an out-of-range position are consumed without drawing while
    // the cursor advances and wraps exactly as for drawn characters.
    // For the documented bx in [0,39] / by in [0,24] inputs this whole
    // loop costs two compares.
    while (cx >= TILE_BLOCKS_PER_ROW || cy >= TILE_BLOCKS_PER_COL) {
        if (*str == '\0') {
            return;
        }
        str++;
        cx++;
        if (cx >= TILE_BLOCKS_PER_ROW) {
            cx = 0;
            cy++;
            if (cy >= TILE_BLOCKS_PER_COL) {
                return;
            }
        }
    }
    bounds[4] = 0u;
    while (*str != '\0') {
        ch    = (uint8_t)*str++;
        entry = asciiMap[ch];
        if (entry != TILE_NO_GLYPH) {
            srcBx = (uint8_t)(entry & 0x00FFu);
            srcBy = (uint8_t)((entry >> 8) & 0x00FFu);
            // Only the asciiMap-supplied source block still needs
            // validation per glyph: cx/cy are in range by the entry
            // sanitation + wrap invariant and the NULL checks ran once
            // above, so the public jlTileCopyMasked wrapper (which
            // would re-check all of it and issue a dirty mark per
            // glyph) is skipped (PERF-AUDIT #22).
            if (srcBx < TILE_BLOCKS_PER_ROW && srcBy < TILE_BLOCKS_PER_COL) {
                jlpTileCopyMasked(dst, cx, cy, fontSurface, srcBx, srcBy, 0u);
                drawTextUnion(cx, cy, bounds);
            }
        }
        cx++;
        if (cx >= TILE_BLOCKS_PER_ROW) {
            cx = 0;
            cy++;
            if (cy >= TILE_BLOCKS_PER_COL) {
                break;
            }
        }
    }
    // One union dirty mark for the whole string instead of an 8-row
    // mark per glyph (PERF-AUDIT #22). Over-approximates when the
    // string wraps rows, which is safe: dirty state only widens what
    // present uploads.
    if (bounds[4] != 0u) {
        surfaceMarkDirtyRect(dst, (int16_t)TILE_BLOCK_TO_PIXEL(bounds[0]), (int16_t)TILE_BLOCK_TO_PIXEL(bounds[2]), TILE_BLOCK_TO_PIXEL(bounds[1] - bounds[0] + 1u), TILE_BLOCK_TO_PIXEL(bounds[3] - bounds[2] + 1u));
    }
}


void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
    if (dst == NULL || src == NULL) {
        return;
    }
    if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
        srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
        return;
    }
    // Single op: machine asm/planar override or the generic chunky default.
    jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
    if (dst == gStage) {
        tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
    }
}


void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
    if (dst == NULL || src == NULL) {
        return;
    }
    if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
        srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
        return;
    }
    jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
    if (dst == gStage) {
        tileMarkDirty(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
    }
}


void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
    if (s == NULL) {
        return;
    }
    if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
        return;
    }
    // Single compile-time-selected op: machine asm/planar override or the
    // generic chunky default.
    jlpTileFill(s, bx, by, colorIndex);
    if (s == gStage) {
        tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx);
    }
}


void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) {
    uint8_t        tx;
    uint8_t        ty;
    uint8_t        idx;
    const uint8_t *row;

    if (dst == NULL || tiles == NULL || map == NULL) {
        return;
    }
    if (wTiles == 0u || hTiles == 0u) {
        return;
    }
    if ((uint16_t)bx + wTiles > TILE_BLOCKS_PER_ROW || (uint16_t)by + hTiles > TILE_BLOCKS_PER_COL) {
        return;
    }
    // One validation above, one dirty union below: the per-cell loop
    // pays only the jlp dispatch (inline on the 68k ports since Phase
    // 2 R4a; the fused stage entry on IIgs). Tileset indexing is a
    // 16-bit shift folded into the far base ONCE per cell -- the
    // natural tiles[idx] spelling made the 65816 do a 32-bit indexed
    // multiply per cell, which alone erased the batch win there.
    row = map;
#ifdef JOEYLIB_PLATFORM_IIGS
    // The stage test is hoisted OUT of the loop on the IIgs: the
    // jlpTilePaste macro's per-cell (dst == gStage) ternary
    // materializes a boolean (~65 cycles/cell) that a 10x5 map pays
    // 50 times. The fused stage entry marks each pasted tile itself,
    // so the stage arm needs no union; non-stage surfaces carry no
    // dirty state.
    if (dst == gStage) {
        // ONE JSL for the whole map (NATIVE-PERF Phase 4): the asm
        // walker amortizes the per-tile JSL + arg staging + derive +
        // 8-row mark across the rectangle and issues one band union.
        iigsTileMapPasteMark(bx, by, wTiles, hTiles, (const uint8_t *)tiles, map);
        return;
    }
    for (ty = 0; ty < hTiles; ty++) {
        for (tx = 0; tx < wTiles; tx++) {
            idx = row[tx];
            if (idx != TILE_MAP_SKIP) {
                iigsTilePasteInner(&dst->pixels[SURFACE_ROW_OFFSET((uint16_t)(by + ty) << 3) + ((uint16_t)(bx + tx) << 2)], (const uint8_t *)tiles + ((uint16_t)idx << 5));
            }
        }
        row += wTiles;
    }
#elif defined(jlpTileMapPaste)
    // Whole-map engine (NATIVE-PERF Phase 4 ST / Phase 6 W3 seam):
    // port.h aliases jlpTileMapPaste to the port's engine (ST
    // stTileMapPaste, DOS dosTileMapPaste), which hoists the setup --
    // portData/pixels check, row-base derive, group or dword walk --
    // out of the per-cell loop the generic arm below pays. One arm,
    // one mark stanza, however many ports bring engines.
    (void)tx;
    (void)ty;
    (void)idx;
    (void)row;
    jlpTileMapPaste(dst, bx, by, wTiles, hTiles, tiles, map);
    // One rectangle-union mark for the whole map. Over-marks
    // TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
    // pixels unchanged).
    if (dst == gStage) {
        surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
    }
#else
    for (ty = 0; ty < hTiles; ty++) {
        for (tx = 0; tx < wTiles; tx++) {
            idx = row[tx];
            if (idx != TILE_MAP_SKIP) {
                jlpTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), (const uint8_t *)tiles + ((uint16_t)idx << 5));
            }
        }
        row += wTiles;
    }
    // One rectangle-union mark for the whole map. Over-marks
    // TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
    // pixels unchanged).
    if (dst == gStage) {
        surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
    }
#endif
}


void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
    if (dst == NULL || in == NULL) {
        return;
    }
    if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
        return;
    }
    // Single op: machine asm/planar override or the generic chunky default.
    jlpTilePaste(dst, bx, by, &in->pixels[0]);
    if (dst == gStage) {
        tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx);
    }
}


void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
    if (dst == NULL || in == NULL) {
        return;
    }
    if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
        return;
    }
    fgColor &= 0x0Fu;
    bgColor &= 0x0Fu;
    // Single compile-time op: planar ports colorize in their own tile format
    // (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
    // Replaces the old runtime pixels==NULL planar/chunky branch.
    jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
    if (dst == gStage) {
        tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx);
    }
}


void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out) {
    if (src == NULL || out == NULL) {
        return;
    }
    if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
        return;
    }
    // Read-only single op: machine asm/planar override or the generic chunky
    // default. No dirty mark.
    jlpTileSnap(src, bx, by, &out->pixels[0]);
}
