// Drawing primitives on chunky 4bpp packed surfaces.
//
// Byte layout: two pixels per byte, high nibble is the LEFT pixel.
// Clipping is handled at the C API boundary so ASM inner loops (future)
// can run with branch-free, pre-validated parameters.

#include <stddef.h>
#include <string.h>

#include "joey/draw.h"
#include "joey/debug.h"
#include "port.h"
#include "surfaceInternal.h"


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

// Flood-fill seed stack: each entry is (x, y) = 4 bytes, so 512 slots
// = 2 KB. For 320x200 surfaces with reasonable region sizes this is
// well above the worst-case scanline-fill seed depth (typically <50).
// On overflow the fill silently truncates rather than crashing.
#define FLOOD_STACK_SIZE 512

// Flood-fill fallback pixel read. On chunky ports (no
// JL_HAS_SAMPLE_PIXEL override) the nibble read inlines via
// SURFACE_READ_NIBBLE instead of paying the jlpGenericSamplePixel
// function hop per walked pixel (PERF-AUDIT #14/#24); planar ports keep
// their platform jlpSamplePixel (plane decode). Every call site passes
// side-effect-free arguments -- SURFACE_READ_NIBBLE evaluates its x
// argument twice.
#if !defined(JL_HAS_SAMPLE_PIXEL)
    #define floodReadPixel(_s, _x, _y) SURFACE_READ_NIBBLE((_s), (_x), (_y))
#else
    #define floodReadPixel(_s, _x, _y) jlpSamplePixel((_s), (_x), (_y))
#endif

// ----- Prototypes -----

static void    fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void    floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
static void    plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);

// ----- Internal helpers (alphabetical) -----

// (fillRectClipped moved to src/generic/genericDraw.c as jlpGenericFillRect,
// the chunky default for the migrated jlpFillRect op.)

// Fill an axis-aligned span that the CALLER has already proven to be
// fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <=
// SURFACE_HEIGHT, w > 0, h > 0). Identical tail to jlFillRect but with
// the 32-bit clip elided -- on an on-surface rect that clip is a
// no-op, so callers that already hold the on-surface guarantee (rect
// outlines, circle scanlines, axis-aligned lines) avoid re-deriving
// it per span. Pixel output and dirty-mark word bands are bit-for-bit
// identical to routing the same span through jlFillRect.
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
    // Compile-time-selected op: machine fill (IIgs asm stage / Amiga+ST planar)
    // or the generic chunky fill.
    jlpFillRect(s, x, y, w, h, colorIndex);
#if !defined(JL_FILL_RECT_MARK_FUSED)
    // On IIgs the stage arm of jlpFillRect fused this mark into the asm
    // entry (port.h); non-stage surfaces never carry dirty state, so
    // nothing is lost by compiling it out there.
    surfaceMarkDirtyRect(s, x, y, w, h);
#endif
}


// Smith's scanline flood fill. Implements both the unbounded and the
// boundary-stopped variants in one pass: the matching predicate is
// (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill,
// matchColor is the original seed color) or (pixel != matchColor)
// when matchEqual is false (jlFloodFillBounded, matchColor is the
// boundary that stops the fill).
//
// Algorithm:
//   1. Push seed (x, y) on stack.
//   2. Pop a seed; skip if its pixel no longer matches (already
//      filled by an earlier span overlap).
//   3. Scan left and right from the seed to find the longest run of
//      matching pixels containing it -- this is the current span.
//   4. Fill the span with newColor.
//   5. Walk the row above and the row below, scanning the columns
//      that overlap the just-filled span; for each contiguous run of
//      matching pixels, push the rightmost x of that run as a new
//      seed (so popping that seed next will scan the same run).
//   6. Repeat until the stack drains.
//
// Stack overflow truncates the fill rather than crashing; for vector
// art (Sierra-style picture playback) the input is well-behaved and
// 512 entries is plenty.
static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual) {
    static int16_t stackX[FLOOD_STACK_SIZE];
    static int16_t stackY[FLOOD_STACK_SIZE];
    static uint8_t floodMarkBuf[SURFACE_WIDTH];
    int16_t  sp;
    int16_t  x;
    int16_t  y;
    int16_t  leftX;
    int16_t  rightX;
    uint8_t  pix;
    uint8_t  newNibble;

    newNibble = (uint8_t)(newColor & 0x0F);
    matchColor = (uint8_t)(matchColor & 0x0F);

    sp = 0;
    stackX[sp] = startX;
    stackY[sp] = startY;
    sp++;

    while (sp > 0) {
        sp--;
        x = stackX[sp];
        y = stackY[sp];

        if (y < 0 || y >= SURFACE_HEIGHT || x < 0 || x >= SURFACE_WIDTH) {
            continue;
        }

#if defined(JL_HAS_FLOOD_WALK_AND_SCANS)
        // Highest-tier asm fast path (IIgs): seed-test + walk-left + walk-right
        // + 1-row fill + scan-above + scan-below + push, all in one
        // cross-segment call. The asm caches row addr / match decoder
        // across every sub-operation. C just pops and dispatches; this
        // path completes the entire per-seed work and computes the row
        // address itself, so we don't pay y*160 in C. On this platform it
        // always handles the pop, so the C fallback below is never reached.
        if (s->pixels != NULL) {
            bool seedMatched;
            if (jlpFloodWalkAndScans(s->pixels, x, y,
                                         matchColor, newNibble, matchEqual,
                                         stackX, stackY,
                                         &sp, FLOOD_STACK_SIZE,
                                         &seedMatched, &leftX, &rightX)) {
                // The asm filled the span internally but does not mark
                // dirty -- that is the C wrapper's job. Mark the just-
                // filled run so jlStagePresent blits it.
                if (seedMatched) {
                    surfaceMarkDirtyRect(s, leftX, y, (int16_t)(rightX - leftX + 1), 1);
                }
                continue;
            }
        }
#endif

        // Walk-out: find the matching run containing the seed. Planar ports
        // (Amiga) override with a plane-aware walk; everyone else uses the
        // portable floodReadPixel walk (inline nibble read on chunky,
        // platform jlpSamplePixel on planar).
        {
#if defined(JL_HAS_FLOOD_WALK_PLANES)
            bool seedMatched;
            if (jlpFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual,
                                          &seedMatched, &leftX, &rightX)) {
                if (!seedMatched) {
                    continue;
                }
            } else
#endif
            {
                // matchEqual is loop-invariant across the whole fill, so
                // fork the walk ONCE per seed pop instead of re-branching
                // on it per walked pixel (PERF-AUDIT #35). The two
                // variants differ only in the stop test -- (pix !=
                // matchColor) vs (pix == matchColor || pix == newNibble)
                // -- and the span fill / scan / push code below stays
                // shared, so the duplication is confined to these small
                // walk loops.
                if (matchEqual) {
                    pix = floodReadPixel(s, x, y);
                    if (pix != matchColor) {
                        continue;
                    }

                    // Walk left to find the start of the matching run.
                    leftX = x;
                    while (leftX > 0) {
                        pix = floodReadPixel(s, (int16_t)(leftX - 1), y);
                        if (pix != matchColor) {
                            break;
                        }
                        leftX--;
                    }

                    // Walk right to find the end.
                    rightX = x;
                    while (rightX < SURFACE_WIDTH - 1) {
                        pix = floodReadPixel(s, (int16_t)(rightX + 1), y);
                        if (pix != matchColor) {
                            break;
                        }
                        rightX++;
                    }
                } else {
                    pix = floodReadPixel(s, x, y);
                    if (pix == matchColor || pix == newNibble) {
                        continue;
                    }

                    // Walk left to find the start of the fillable run.
                    leftX = x;
                    while (leftX > 0) {
                        pix = floodReadPixel(s, (int16_t)(leftX - 1), y);
                        if (pix == matchColor || pix == newNibble) {
                            break;
                        }
                        leftX--;
                    }

                    // Walk right to find the end.
                    rightX = x;
                    while (rightX < SURFACE_WIDTH - 1) {
                        pix = floodReadPixel(s, (int16_t)(rightX + 1), y);
                        if (pix == matchColor || pix == newNibble) {
                            break;
                        }
                        rightX++;
                    }
                }
            }
        }

        // Fill the span. walk-out already guaranteed leftX/rightX are in
        // [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same
        // for y, so the span is provably on-surface -- route it through
        // the shared on-surface tail (chunky fill + planar dual-write +
        // dirty mark) rather than re-clipping through jlFillRect. The
        // planar dual-write here is load-bearing: without it
        // PLANAR_PRESENT builds (and, post-Phase-9, every build) display
        // flood-filled regions as the unfilled background.
        {
            int16_t spanW = (int16_t)(rightX - leftX + 1);
            fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
        }

        // Scan rows above and below for run boundaries: fill markBuf[] with
        // 1/0 per pixel, then walk it for run-edge transitions and push a
        // seed at each run's right edge. Planar ports (Amiga) override the
        // markBuf fill with a plane-aware scan; everyone else fills it via
        // the portable floodReadPixel walk.
        {
            int16_t  i;
            int16_t  spanLen;
            int16_t  scanY;
            int16_t  side;
            bool     curHit;
            bool     prevHit;

            spanLen = (int16_t)(rightX - leftX + 1);
            for (side = 0; side < 2; side++) {
                if (side == 0) {
                    if (y <= 0) {
                        continue;
                    }
                    scanY  = (int16_t)(y - 1);
                } else {
                    if (y >= SURFACE_HEIGHT - 1) {
                        continue;
                    }
                    scanY  = (int16_t)(y + 1);
                }
                if (
#if defined(JL_HAS_FLOOD_SCAN_ROW_PLANES)
                    !jlpFloodScanRowPlanes(s, leftX, rightX, scanY,
                                           matchColor, newNibble, matchEqual,
                                           floodMarkBuf)
#else
                    true
#endif
                   ) {
                    // C fallback: fill markBuf the slow way. Same
                    // loop-invariant matchEqual fork as the walk-out
                    // above (PERF-AUDIT #35) -- one branch per row scan
                    // instead of one per scanned pixel.
                    if (matchEqual) {
                        for (i = 0; i < spanLen; i++) {
                            pix = floodReadPixel(s, (int16_t)(leftX + i), scanY);
                            floodMarkBuf[i] = (uint8_t)((pix == matchColor) ? 1 : 0);
                        }
                    } else {
                        for (i = 0; i < spanLen; i++) {
                            pix = floodReadPixel(s, (int16_t)(leftX + i), scanY);
                            floodMarkBuf[i] = (uint8_t)((pix != matchColor && pix != newNibble) ? 1 : 0);
                        }
                    }
                }
                // Walk markBuf for run-edge transitions.
                prevHit = false;
                for (i = 0; i < spanLen; i++) {
                    curHit = floodMarkBuf[i] != 0;
                    if (!curHit && prevHit) {
                        if (sp < FLOOD_STACK_SIZE) {
                            stackX[sp] = (int16_t)(leftX + i - 1);
                            stackY[sp] = scanY;
                            sp++;
                        }
                    }
                    prevHit = curHit;
                }
                if (prevHit) {
                    if (sp < FLOOD_STACK_SIZE) {
                        stackX[sp] = rightX;
                        stackY[sp] = scanY;
                        sp++;
                    }
                }
            }
        }
    }
}


// Plot a single clipped pixel WITHOUT marking the dirty rect. Used by
// the Bresenham line/circle fallbacks, which mark one precomputed
// bounding box after their loop (the public jlDrawPixel clips inline
// and calls jlpDrawPixel directly on every platform).
//
// CONTRACT (PERF-AUDIT #61): s is non-NULL -- both callers
// (jlDrawCircle, jlDrawLine) validate it once at API entry, so there
// is no per-pixel NULL re-check here. The per-pixel BOUNDS check is
// load-bearing and stays: it IS the off-surface clipping for these
// fallbacks, matching jlDrawPixel's clip.
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
    if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
        return;
    }
    // Compile-time-selected op: machine plot (asm/planar) or the generic chunky
    // nibble RMW (src/generic/genericDraw.c). No runtime dispatch.
    jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
}


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

void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
    int16_t x;
    int16_t y;
    int16_t err;
    int32_t minX;
    int32_t minY;
    int32_t maxX;
    int32_t maxY;

    if (s == NULL) {
        return;
    }
    if (r == 0) {
        jlDrawPixel(s, cx, cy, colorIndex);
        return;
    }

    // Fast path: when the bounding circle is fully on-surface we can
    // hand off to the port asm (no per-pixel bounds check needed in
    // the inner loop) and mark the bounding box dirty once. A circle
    // with r >= SURFACE_HEIGHT can never fit on-surface, and casting
    // such an r to int16_t can wrap negative (r > 32767), so reject it
    // here before the cast feeds the fast path / dirty-rect extents.
#if defined(JL_HAS_DRAW_CIRCLE)
    if (r < SURFACE_HEIGHT) {
        int16_t ir = (int16_t)r;
        // On-surface fast path: machine asm / planar. No runtime platform check
        // -- the whole block is compiled out on targets without JL_HAS_DRAW_CIRCLE.
        if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
            cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
            jlpDrawCircle(s, cx, cy, r, colorIndex);
            surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
                                 (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
            return;
        }
    }
#endif

    // Known limitation (PERF-AUDIT #12): for r >= 32768 the (int16_t)r
    // cast below would wrap negative, `while (x >= y)` would be false
    // immediately, and the outline draws NOTHING. That is silent but
    // harmless: no pixel is plotted, so nothing must be marked either
    // -- return explicitly (the old accumulated box stayed empty and
    // collapsed to the same no-op). A huge-r outline CAN legitimately
    // cross the surface (e.g. cx=-32768, r=33000 passes through
    // x ~= 232), but drawing it correctly needs 32-bit Bresenham state
    // throughout; that fix is deliberately applied only to
    // jlFillCircle, where coverage (not the perimeter) is what
    // matters. Accepted as a documented no-op here.
    if (r > INT16_MAX) {
        return;
    }

    // Bresenham midpoint: maintain (x, y) on the perimeter, eight-
    // octant symmetry plots all 8 reflections each iteration. Plots
    // through plotPixelNoMark so off-surface pixels clip individually
    // (same per-pixel clip as jlDrawPixel) without paying the
    // cross-segment dirty mark per pixel; one bounding box is marked
    // after the loop instead.
    //
    // The dirty box is statically known BEFORE the loop (PERF-AUDIT
    // #36/#37): the first iteration runs with x == r, y == 0 and its
    // eight reflection folds set exactly [cx - r, cx + r] x
    // [cy - r, cy + r]; no later iteration can widen it (both offsets
    // never exceed r), and the old accumulation folded every
    // reflection whether or not its pixel landed on-surface. So the
    // precomputed box, clamped after the loop exactly as before, marks
    // the identical region the deleted per-iteration folds produced --
    // including the collapse to a no-op mark for a fully off-surface
    // circle. Computed in int32: cx +/- r overflows int16 for centers
    // near the int16 limits, and an int16 wrap here could under-mark
    // (stale rows on present), which clamping int32 values can never
    // do.
    minX = (int32_t)cx - (int32_t)r;
    minY = (int32_t)cy - (int32_t)r;
    maxX = (int32_t)cx + (int32_t)r;
    maxY = (int32_t)cy + (int32_t)r;
    x = (int16_t)r;
    y = 0;
    err = (int16_t)(1 - x);
    while (x >= y) {
        plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
        plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
        y++;
        /* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits
         * a multiply helper -- two ADDs are unconditionally cheaper. */
        if (err <= 0) {
            err = (int16_t)(err + y + y + 1);
        } else {
            x--;
            err = (int16_t)(err + y + y - x - x + 1);
        }
    }

    // Clamp the precomputed box to the surface and mark it once. For a
    // circle entirely off one side of the surface the clamp leaves
    // min > max on that axis and nothing is marked (same collapse the
    // old accumulated box produced). The clamped values fit int16_t,
    // so the narrowing casts into the mark are safe.
    if (minX < 0)                  { minX = 0; }
    if (minY < 0)                  { minY = 0; }
    if (maxX > SURFACE_WIDTH - 1)  { maxX = SURFACE_WIDTH - 1; }
    if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
    if (minX <= maxX && minY <= maxY) {
        surfaceMarkDirtyRect(s, (int16_t)minX, (int16_t)minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
    }
}


void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
    int16_t dx;
    int16_t dy;
    int16_t sx;
    int16_t sy;
    int16_t err;
    int16_t e2;
    int16_t tmp;
    int16_t minX;
    int16_t minY;
    int16_t maxX;
    int16_t maxY;
    int32_t span;

    if (s == NULL) {
        return;
    }

    // Horizontal and vertical fast paths use jlFillRect; the general
    // case Bresenham plots per-pixel through plotPixelNoMark so per-pixel
    // off-surface clipping just works.
    if (y0 == y1) {
        if (x0 > x1) {
            tmp = x0;
            x0  = x1;
            x1  = tmp;
        }
        // Compute the span in 32-bit so far-apart off-surface endpoints
        // (e.g. x0=-20000, x1=20000) do not overflow the 16-bit int the
        // subtraction would otherwise promote to on a 16-bit-int target.
        // Clip the START into range BEFORE clamping the length: clamping
        // first anchored a surface-width span at a far-off-surface x0,
        // so a line crossing the whole visible row clipped to nothing
        // (PERF-AUDIT #72). The final clamp keeps the downstream
        // uint16_t narrowing safe.
        span = (int32_t)x1 - (int32_t)x0 + 1;
        if (x0 < 0) {
            span = span + x0;
            x0   = 0;
        }
        if (span <= 0) {
            return;
        }
        if (span > SURFACE_WIDTH) {
            span = SURFACE_WIDTH;
        }
        // On-surface (the common case) skips jlFillRect's 32-bit clip:
        // span is already <= SURFACE_WIDTH and x0 <= x1, so the row is
        // fully on-surface exactly when its left edge and y are. Off-
        // surface still routes through jlFillRect for correct clipping.
        if (x0 >= 0 && y0 >= 0 && y0 < SURFACE_HEIGHT &&
            (int32_t)x0 + span <= SURFACE_WIDTH) {
            fillRectOnSurface(s, x0, y0, (int16_t)span, 1, colorIndex);
        } else {
            jlFillRect(s, x0, y0, (uint16_t)span, 1, colorIndex);
        }
        return;
    }
    if (x0 == x1) {
        if (y0 > y1) {
            tmp = y0;
            y0  = y1;
            y1  = tmp;
        }
        // Same start-clip-then-clamp ordering as the horizontal path
        // (PERF-AUDIT #72), mirrored for y0/SURFACE_HEIGHT.
        span = (int32_t)y1 - (int32_t)y0 + 1;
        if (y0 < 0) {
            span = span + y0;
            y0   = 0;
        }
        if (span <= 0) {
            return;
        }
        if (span > SURFACE_HEIGHT) {
            span = SURFACE_HEIGHT;
        }
        if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 &&
            (int32_t)y0 + span <= SURFACE_HEIGHT) {
            fillRectOnSurface(s, x0, y0, 1, (int16_t)span, colorIndex);
        } else {
            jlFillRect(s, x0, y0, 1, (uint16_t)span, colorIndex);
        }
        return;
    }

    // Diagonal: if both endpoints are on-surface, the inner Bresenham
    // can run without per-pixel bound checks. Hand off to the port
    // fast path; bounding-box dirty marking happens here in C either
    // way.
#if defined(JL_HAS_DRAW_LINE)
    if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH &&
        y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT) {
        int16_t bbx = (x0 < x1) ? x0 : x1;
        int16_t bby = (y0 < y1) ? y0 : y1;
        int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1);
        int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1);
        // On-surface fast path: machine asm / planar, compiled out where unsupported.
        jlpDrawLine(s, x0, y0, x1, y1, colorIndex);
        surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh);
        return;
    }
#endif

    // Diagonal fallback: plot through plotPixelNoMark so each pixel still
    // clips off-surface individually (matching jlDrawPixel), but skip the
    // per-pixel cross-segment dirty mark; one bounding box is marked once
    // after the loop.
    //
    // The dirty box is statically known BEFORE the loop (PERF-AUDIT
    // #9/#37): Bresenham walks the monotone path from (x0, y0) to
    // (x1, y1) and terminates exactly at (x1, y1), so the visited-point
    // bounding box is the endpoint bounding box -- the same four
    // ternaries the JL_HAS_DRAW_LINE fast path computes. The old
    // per-pixel accumulation folded every VISITED point (clipped or
    // not), so after the post-loop clamp this precomputed box marks the
    // identical region it did. For endpoints running off-surface the
    // clamped endpoint box is a SUPERSET of the box of the actually
    // PLOTTED (clipped) pixels, which is safe: over-marking only makes
    // present copy more; under-marking would leave stale rows. Computed
    // from the ORIGINAL endpoints here, before the loop mutates x0/y0.
    minX = (x0 < x1) ? x0 : x1;
    minY = (y0 < y1) ? y0 : y1;
    maxX = (x0 > x1) ? x0 : x1;
    maxY = (y0 > y1) ? y0 : y1;
    dx  = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
    dy  = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
    sx  = (int16_t)((x0 < x1) ? 1 : -1);
    sy  = (int16_t)((y0 < y1) ? 1 : -1);
    err = (int16_t)(dx + dy);
    while (1) {
        plotPixelNoMark(s, x0, y0, colorIndex);
        if (x0 == x1 && y0 == y1) {
            break;
        }
        e2 = (int16_t)(2 * err);
        if (e2 >= dy) {
            err = (int16_t)(err + dy);
            x0  = (int16_t)(x0 + sx);
        }
        if (e2 <= dx) {
            err = (int16_t)(err + dx);
            y0  = (int16_t)(y0 + sy);
        }
    }

    // Clamp the endpoint box to the surface and mark it once. For a line
    // entirely off one side of the surface the clamp leaves min > max on
    // that axis and nothing is marked -- exactly how the old accumulated
    // box (which held the same raw endpoint extents) collapsed.
    if (minX < 0)                  { minX = 0; }
    if (minY < 0)                  { minY = 0; }
    if (maxX > SURFACE_WIDTH - 1)  { maxX = SURFACE_WIDTH - 1; }
    if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
    if (minX <= maxX && minY <= maxY) {
        surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
    }
}


void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
    if (s == NULL) {
        return;
    }
    // Clip BEFORE plotting and marking (the unsigned casts fold both
    // bounds tests per axis). Marking with the raw coords after
    // plotPixelNoMark silently clipped violated surfaceMarkDirtyRect's
    // already-clipped contract and scribbled outside the stage
    // dirty-band arrays for off-surface draws (PERF-AUDIT #1). Calling
    // jlpDrawPixel directly also drops plotPixelNoMark's redundant
    // bounds re-check on this path; the helper itself stays for the
    // Bresenham fallbacks.
    //
    // This is ONE body for every port: jlpDrawPixel is the compile-time
    // selected plot (IIgs asm / planar / generic chunky RMW), and the
    // surfaceMarkDirtyRect macro runs the stage compare (direct gStage
    // load, no jlStageGet() hop -- PERF-AUDIT #10/#17) and the
    // single-row widen inline with no call layers (PERF-AUDIT #20),
    // which is exactly the code the old hand-written IIgs branch spelled
    // out. NB (IIgs): an all-C inline RMW plot here (gRowOffsetLut, no
    // multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec)
    // -- the far-pointer code for s->pixels[...] costs more than the
    // cross-segment JSL into the hand-tuned asm, so the asm plot stays.
    if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
        return;
    }
#ifdef JOEYLIB_PLATFORM_IIGS
    // Fused plot+mark in ONE JSL for the stage (NATIVE-PERF Phase 2
    // R10); non-stage surfaces carry no dirty state, so the plain plot
    // suffices there. The batching plotters (plotPixelNoMark) keep the
    // unfused inner.
    if (s == gStage) {
        iigsDrawPixelMark((uint16_t)x, (uint16_t)y, (uint16_t)(colorIndex & 0x0Fu));
        return;
    }
    jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
#else
    jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
    surfaceMarkDirtyRect(s, x, y, 1, 1);
#endif
}


void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
    if (s == NULL) {
        return;
    }
    if (w == 0 || h == 0) {
        return;
    }
    // Degenerate dimensions: a 1xN or Nx1 rect IS a line, and a 1x1
    // rect is a single pixel. jlFillRect handles both correctly so we
    // don't need to fork the inner logic.
    if (h == 1 || w == 1) {
        jlFillRect(s, x, y, w, h, colorIndex);
        return;
    }
    // Fast path: when the OUTER rect is fully on-surface every one of
    // the four edge spans is also fully on-surface, so each can skip
    // jlFillRect's 32-bit clip and route straight through the shared
    // on-surface tail. The shared 16-bit predicate (PERF-AUDIT #62)
    // is semantically identical to the 32-bit x + w <= SURFACE_WIDTH
    // test it replaces for every rect that reaches it: w/h >= 2 here
    // (w == 0 / h == 0 returned above, w == 1 / h == 1 routed to
    // jlFillRect, satisfying the macro's w/h >= 1 contract); any
    // w > SURFACE_WIDTH -- w == 65535 included -- fails the macro's
    // guarded first compare exactly as x + w <= SURFACE_WIDTH would
    // for x >= 0; and for w <= SURFACE_WIDTH the guarded subtraction
    // cannot wrap, so x <= SURFACE_WIDTH - w IS x + w <= SURFACE_WIDTH
    // (boundary proof at the macro definition: w == 320 passes only
    // x == 0; x == 319 / w == 1 passes; x == -1 fails).
    if (SURFACE_RECT_ON_SURFACE(x, y, w, h)) {
        // Raw edge fills + ONE dirty mark for the whole outline
        // (NATIVE-PERF Phase 2 R11, the fill-circle span pattern).
        // Band state is provably identical to marking each edge:
        // bands are [min, max] intervals per row, and the left+right
        // edge words widen an interior row to exactly the same
        // [wordof(x), wordof(x+w-1)] the full-width mark produces.
        // Top edge.
        jlpFillRectNoMark(s, x, y, (int16_t)w, 1, colorIndex);
        // Bottom edge.
        jlpFillRectNoMark(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex);
        // Interior vertical edges only exist when h > 2 (an h == 2 rect
        // is just the two horizontal edges). Skipping them keeps h - 2
        // == 0 rects out of jlpFillRect, whose contract requires
        // positive dimensions (PERF-AUDIT #65 verifier correction).
        if (h > 2) {
            // Left edge (interior only -- top and bottom corners already drawn).
            jlpFillRectNoMark(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
            // Right edge (interior only).
            jlpFillRectNoMark(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
        }
        surfaceMarkDirtyRect(s, x, y, (int16_t)w, (int16_t)h);
        return;
    }
    // General (possibly clipped) case: let jlFillRect clip each edge.
    // Edge coordinates are computed in 32-bit: w and h are uint16_t, so
    // y + h - 1 (or x + w - 1) can reach 65733, and narrowing that
    // through an int16_t cast wrapped it back on-surface, drawing
    // phantom edges (PERF-AUDIT #11). An edge whose true coordinate is
    // off-surface is skipped instead -- exactly what jlFillRect's clip
    // would do if it received the unwrapped value.
    {
        int32_t bottom;
        int32_t right;
        int32_t innerY;

        bottom = (int32_t)y + (int32_t)h - 1;
        right  = (int32_t)x + (int32_t)w - 1;
        innerY = (int32_t)y + 1;
        // Top edge.
        jlFillRect(s, x, y, w, 1, colorIndex);
        // Bottom edge.
        if (bottom >= 0 && bottom < SURFACE_HEIGHT) {
            jlFillRect(s, x, (int16_t)bottom, w, 1, colorIndex);
        }
        // Interior vertical edges: skip when h == 2 (same contract note
        // as the fast path above) and when their top row is already
        // below the surface (innerY can be 32768 for y == INT16_MAX --
        // the int16_t cast would wrap it on-surface).
        if (h > 2 && innerY < SURFACE_HEIGHT) {
            // Left edge (interior only -- top and bottom corners already drawn).
            jlFillRect(s, x, (int16_t)innerY, 1, (uint16_t)(h - 2), colorIndex);
            // Right edge (interior only).
            if (right >= 0 && right < SURFACE_WIDTH) {
                jlFillRect(s, (int16_t)right, (int16_t)innerY, 1, (uint16_t)(h - 2), colorIndex);
            }
        }
    }
}


void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
    int16_t  ir;
    int32_t  x;
    int32_t  yLo;
    int32_t  yHi;
    int32_t  lim;
    uint16_t y;
    uint32_t xx;
    uint32_t yy;
    uint32_t r2;
    bool     onSurface;

    if (s == NULL) {
        return;
    }
    if (r == 0) {
        jlDrawPixel(s, cx, cy, colorIndex);
        return;
    }

    // Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully
    // on-surface, and casting such an r to int16_t can wrap negative
    // (r > 32767), spuriously passing the bounds test and feeding the
    // asm / dirty-rect garbage. Reject it here before the cast. ir's
    // init is only for the compiler's flow analysis -- it is read only
    // under onSurface, which guarantees the real assignment below ran.
    onSurface = false;
    ir        = 0;
    if (r < SURFACE_HEIGHT) {
        ir = (int16_t)r;
        if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
            cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
            // The whole bounding circle is on-surface, so every scanline
            // span below is too. Remember this so the C fallback can
            // route spans through the raw jlpFillRect (one batched dirty
            // mark after the loop) instead of re-clipping and re-marking
            // each one through jlFillRect.
            onSurface = true;
#if defined(JL_HAS_FILL_CIRCLE)
            // Stage/portData-gated fast path (returns bool). Compiled out where
            // unsupported; the span fill below is the universal fallback.
            if (jlpFillCircle(s, cx, cy, r, colorIndex)) {
                surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
                                     (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
                return;
            }
#endif
        }
    }

    r2 = (uint32_t)r * (uint32_t)r;

    // Full-coverage test (PERF-AUDIT #12): if the farthest surface
    // corner from (cx, cy) lies inside the disk, the circle covers the
    // whole surface -- fill it in one call. This is also the only path
    // that can produce the correct full fill for r >= 32768, which the
    // old (int16_t)r cast wrapped negative (drawing nothing). Skipped
    // when onSurface: a fully on-surface circle can never reach a
    // corner, so the test could not pass. All math is 32-bit: the
    // farthest corner delta is at most 33087 per axis, so the squared
    // sum (~2.2e9) and r*r (~4.3e9) both fit uint32_t.
    if (!onSurface) {
        int32_t farDx;
        int32_t farDy;

        farDx = (int32_t)cx;
        if (farDx < 0) {
            farDx = -farDx;
        }
        lim = (int32_t)(SURFACE_WIDTH - 1) - (int32_t)cx;
        if (lim < 0) {
            lim = -lim;
        }
        if (lim > farDx) {
            farDx = lim;
        }
        farDy = (int32_t)cy;
        if (farDy < 0) {
            farDy = -farDy;
        }
        lim = (int32_t)(SURFACE_HEIGHT - 1) - (int32_t)cy;
        if (lim < 0) {
            lim = -lim;
        }
        if (lim > farDy) {
            farDy = lim;
        }
        if ((uint32_t)farDx * (uint32_t)farDx + (uint32_t)farDy * (uint32_t)farDy <= r2) {
            jlFillRect(s, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, colorIndex);
            return;
        }
    }

    // Clamp the span loop to the y offsets whose rows can intersect the
    // surface (PERF-AUDIT #12): row cy + y is on-surface for y in
    // [-cy, SURFACE_HEIGHT-1 - cy] and row cy - y for y in
    // [cy - (SURFACE_HEIGHT-1), cy], so the union hull is
    // yLo = max(0, -cy, cy - (SURFACE_HEIGHT-1)) and
    // yHi = min(r, max(cy, SURFACE_HEIGHT-1 - cy)). Computed once in
    // int32; for a fully on-surface circle this yields exactly [0, r].
    // A huge clipped r now costs at most SURFACE_HEIGHT iterations
    // instead of r+1.
    yLo = -(int32_t)cy;
    lim = (int32_t)cy - (int32_t)(SURFACE_HEIGHT - 1);
    if (lim > yLo) {
        yLo = lim;
    }
    if (yLo < 0) {
        yLo = 0;
    }
    yHi = (int32_t)cy;
    lim = (int32_t)(SURFACE_HEIGHT - 1) - (int32_t)cy;
    if (lim > yHi) {
        yHi = lim;
    }
    if (yHi > (int32_t)r) {
        yHi = (int32_t)r;
    }
    if (yLo > yHi) {
        return;
    }

    // For each y offset from yLo to yHi, find the largest x such that
    // x*x + y*y <= r*r and emit a horizontal span. Maintain xx=x*x,
    // yy=y*y incrementally so the hot loop never does a 32-bit multiply
    // -- critical on 65816 / 68000 / 286 where mul is slow or absent.
    // (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are
    // uint32_t so r*r does not wrap for r >= 256 (a uint16_t product
    // overflows at r == 256 -> r2 == 0). yy is seeded with yLo*yLo (the
    // incremental chain breaks on a jump), and the first iteration's
    // while walks x down from r to the yLo row's span in one go -- O(r)
    // once per call, accepted for this cold clipped path. The walk
    // condition is xx > r2 - yy rather than xx + yy > r2 because xx +
    // yy can exceed uint32_t range for r >= 32768 (yy <= r2 always, so
    // the subtraction never underflows). The loop variable is uint16_t:
    // yHi <= max(cy, SURFACE_HEIGHT-1 - cy) <= 32967, so y never wraps.
    /* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit
     * multiply helpers for the `2 * ...` constants. spanWidth is hoisted
     * because both span fills in each branch need it. */
    xx = r2;
    yy = (uint32_t)yLo * (uint32_t)yLo;
    x  = (int32_t)r;
    for (y = (uint16_t)yLo; y <= (uint16_t)yHi; y++) {
        uint32_t xxLimit;

        xxLimit = r2 - yy;
        while (xx > xxLimit) {
            xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
            x--;
        }
        if (onSurface) {
            // 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and
            // 0 <= cx - x ... cx + x < SURFACE_WIDTH for every span, so
            // jlFillRect's clip would be a no-op -- skip it. x <= r <
            // SURFACE_HEIGHT here, so the int16_t narrowing is safe.
            //
            // Spans go through the RAW no-mark fill op, not
            // fillRectOnSurface, so the per-span dirty mark is batched
            // into ONE bounding-square mark after the loop (PERF-AUDIT
            // #38); the NoMark twin also keeps the IIgs fused-mark
            // entry from re-marking every span (port.h). The op is the
            // planar-aware fill on planar ports (the JL_HAS_FILL_RECT
            // function override writes the bitplanes), so calling it
            // directly preserves the planar write -- pixel output is
            // bit-identical to the fillRectOnSurface routing.
            uint16_t spanWidth;

            spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
            jlpFillRectNoMark(s, (int16_t)(cx - (int16_t)x), (int16_t)(cy + (int16_t)y), (int16_t)spanWidth, 1, colorIndex);
            if (y > 0) {
                jlpFillRectNoMark(s, (int16_t)(cx - (int16_t)x), (int16_t)(cy - (int16_t)y), (int16_t)spanWidth, 1, colorIndex);
            }
        } else {
            // Clipped case: clip the span in 32-bit HERE instead of
            // narrowing cx - x / 2x + 1 into jlFillRect's int16/uint16
            // parameters -- for huge r those narrowings wrap (e.g.
            // cx=-32000, r=32767 puts cx - x at -64767, which wrapped
            // back on-surface) and either drop visible spans or draw
            // phantom ones. The row test replaces jlFillRect's y clip
            // the same way. For spans that never wrapped this is
            // bit-identical to the old jlFillRect routing (same clip,
            // same fillRectOnSurface tail).
            int32_t spanLeft;
            int32_t spanRight;
            int32_t row;

            spanLeft  = (int32_t)cx - x;
            spanRight = (int32_t)cx + x;
            if (spanLeft < 0) {
                spanLeft = 0;
            }
            if (spanRight > (int32_t)(SURFACE_WIDTH - 1)) {
                spanRight = (int32_t)(SURFACE_WIDTH - 1);
            }
            if (spanLeft <= spanRight) {
                int16_t spanWidth;

                spanWidth = (int16_t)(spanRight - spanLeft + 1);
                row = (int32_t)cy + (int32_t)y;
                if (row >= 0 && row < SURFACE_HEIGHT) {
                    fillRectOnSurface(s, (int16_t)spanLeft, (int16_t)row, spanWidth, 1, colorIndex);
                }
                if (y > 0) {
                    row = (int32_t)cy - (int32_t)y;
                    if (row >= 0 && row < SURFACE_HEIGHT) {
                        fillRectOnSurface(s, (int16_t)spanLeft, (int16_t)row, spanWidth, 1, colorIndex);
                    }
                }
            }
        }
        yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
    }

    // One bounding-square dirty mark for the whole on-surface disk
    // (PERF-AUDIT #38) instead of 2r+1 per-span marks. onSurface
    // guarantees the loop above ran the full y = 0..r range (yLo/yHi
    // resolve to exactly [0, r] for a fully on-surface circle), so the
    // square covers every filled span. Rows away from the center are
    // over-marked out to the full circle width -- exactly the mark the
    // JL_HAS_FILL_CIRCLE fast path issues -- and over-marking is safe
    // (present copies more; under-marking would leave stale rows). The
    // clipped (!onSurface) spans above keep their per-span marks via
    // fillRectOnSurface.
    if (onSurface) {
        surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
    }
}


void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
    int16_t sx;
    int16_t sy;
    int16_t sw;
    int16_t sh;
    int32_t left;
    int32_t top;
    int32_t right;
    int32_t bottom;

    if (s == NULL) {
        return;
    }

    // 16-bit on-surface fast path (PERF-AUDIT #13): the overwhelmingly
    // common case (HUD fills, tile-sized fills) is fully on-surface and
    // needs no 32-bit math -- every int32 op below is a multi-
    // instruction carry-chained sequence on the 16-bit CPUs. The shared
    // predicate (PERF-AUDIT #62) expands to exactly the guarded compare
    // chain this test spelled out inline -- w/h upper bounds first so
    // the SURFACE_WIDTH - w subtraction cannot wrap for w up to 65535,
    // then the x/y compares (boundary proof at the macro definition:
    // w == 65535 fails the guard; w == 320 passes only x == 0;
    // x == 319 / w == 1 passes; x == -1 fails). w > 0 && h > 0 keeps
    // empty rects out of fillRectOnSurface, whose contract requires
    // positive dimensions (and satisfies the macro's w/h >= 1
    // contract); they fall through to the slow path's empty-clip
    // return instead.
    if (w > 0 && h > 0 && SURFACE_RECT_ON_SURFACE(x, y, w, h)) {
        fillRectOnSurface(s, x, y, (int16_t)w, (int16_t)h, colorIndex);
        return;
    }

    // Slow path: clip in 32-bit so any int16_t x/y combined with any
    // uint16_t w/h is handled without overflow (a uint16_t w > 32767
    // would wrap negative if narrowed to int16_t before clipping, and a
    // negative x with a large w must still fill out to the right
    // surface edge). The surface is only 320x200, so the clipped result
    // always fits int16_t.
    left   = (int32_t)x;
    top    = (int32_t)y;
    right  = left + (int32_t)w;
    bottom = top  + (int32_t)h;
    if (left   < 0)              { left   = 0; }
    if (top    < 0)              { top    = 0; }
    if (right  > SURFACE_WIDTH)  { right  = SURFACE_WIDTH; }
    if (bottom > SURFACE_HEIGHT) { bottom = SURFACE_HEIGHT; }
    if (right <= left || bottom <= top) {
        return;
    }
    sx = (int16_t)left;
    sy = (int16_t)top;
    sw = (int16_t)(right - left);
    sh = (int16_t)(bottom - top);
    // The clipped rect is now provably on-surface; share the fill +
    // planar dual-write + dirty-mark tail with the other on-surface
    // span emitters so there is one source of truth for it.
    fillRectOnSurface(s, sx, sy, sw, sh, colorIndex);
}


void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor) {
    uint8_t  seedColor;

    if (s == NULL) {
        return;
    }
    if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
        return;
    }
    /* jlpSamplePixel reads from whichever storage the port uses --
     * works on both chunky (s->pixels) and planar (s->portData) ports. */
    seedColor = jlpSamplePixel(s, x, y);
    if ((seedColor & 0x0F) == (newColor & 0x0F)) {
        return;
    }
    floodFillInternal(s, x, y, newColor, seedColor, true);
}


void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor) {
    uint8_t  pix;

    if (s == NULL) {
        return;
    }
    if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
        return;
    }
    pix = jlpSamplePixel(s, x, y);
    // Starting on a boundary pixel or already-filled pixel: nothing
    // to do.
    if ((pix & 0x0F) == (boundaryColor & 0x0F)) {
        return;
    }
    if ((pix & 0x0F) == (newColor & 0x0F)) {
        return;
    }
    floodFillInternal(s, x, y, newColor, boundaryColor, false);
}


uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
    if (s == NULL) {
        return 0;
    }
    if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
        return 0;
    }
#if !defined(JL_HAS_SAMPLE_PIXEL)
    // Chunky ports (no JL_HAS_SAMPLE_PIXEL override): inline the nibble
    // read. The out-of-line jlpGenericSamplePixel hop re-marshalled
    // s/x/y just to run this same macro, and its body paid the y * 160
    // software multiply on IIgs (PERF-AUDIT #14/#24). x and y are plain
    // locals here, so the macro's double x evaluation is side-effect
    // free.
    return SURFACE_READ_NIBBLE(s, x, y);
#else
    // Planar ports: the platform jlpSamplePixel decodes the portData
    // planes (4 plane bits assembled into the nibble).
    return jlpSamplePixel(s, x, y);
#endif
}




void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) {
    uint8_t nibble;
    uint8_t doubled;

    if (s == NULL) {
        return;
    }
    nibble  = colorIndex & 0x0F;
    doubled = (uint8_t)((nibble << 4) | nibble);
    // Compile-time-selected op: a machine override (asm / planar) or the
    // portable-C generic default -- no runtime dispatch.
    jlpSurfaceClear(s, doubled);
    surfaceMarkDirtyAll(s);
}
