// Internal surface definition shared across core and port code.

#ifndef JOEYLIB_SURFACE_INTERNAL_H
#define JOEYLIB_SURFACE_INTERNAL_H

#include <stdint.h>

#include "joey/surface.h"

// Pixels are reached through a pointer rather than an inline array so
// that the per-port HAL can pin the stage's pixel buffer to a specific
// hardware-friendly address (e.g. IIgs $01/2000 with SHR shadow
// inhibited at $C035 so writes stay in fast bank $01 instead of
// auto-mirroring to $E1). Caller-side `s->pixels[i]` syntax is
// unchanged; only allocation/copy paths in surface.c shift to a
// two-buffer model.
//
// portData is per-port opaque storage. On chunky ports (IIgs, DOS) it
// stays NULL -- pixels is the source of truth. On planar ports
// (Amiga, Atari ST) it points to a port-private struct describing the
// 4 bitplanes (Amiga: 4 separate plane buffers + stride; ST: single
// interleaved buffer + stride). Cross-platform code never touches it
// directly -- all primitive access goes through the jlp ops on planar
// ports. See project_planar_68k_plan.md for the full architecture.
struct jlSurfaceT {
    uint8_t  *pixels;
    void     *portData;
    uint8_t   scb[SURFACE_HEIGHT];
    uint16_t  palette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
};

// 16-bit words per scanline. SHR / chunky 4bpp packed = 2 px per byte,
// 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words.
// Dirty tracking grain is 16-bit words because that matches the IIgs
// PEI / PHA slam unit and the Amiga / ST c2p group is 16 px = 4 words.
// (The 4-px-per-word fact is baked into SURFACE_WORD_INDEX's >> 2 below;
// PERF-AUDIT #66 deleted the unused SURFACE_PIXELS_PER_WORD spelling.)
#define SURFACE_WORDS_PER_ROW       (SURFACE_BYTES_PER_ROW / 2)

// x pixel -> dirty word index (4 px per 16-bit word). x is non-negative
// at every call site (clipped to surface bounds), so it is cast to
// uint16_t before the shift to keep the compiler from emitting a signed
// shift helper.
#define SURFACE_WORD_INDEX(_x)      ((uint8_t)((uint16_t)(_x) >> 2))

// Index of the last word in a fully-dirty row.
#define STAGE_DIRTY_FULL_MAX        ((uint8_t)(SURFACE_WORDS_PER_ROW - 1))

// Sentinels for "row is clean": min > max can never happen for a real
// dirty range, so the present loop tests `min > max` to skip a row.
#define STAGE_DIRTY_CLEAN_MIN       0xFFu
#define STAGE_DIRTY_CLEAN_MAX       0x00u

// True when row `y`'s dirty band is the CLEAN sentinel (min > max).
// This is the canonical per-row copy test (the IIgs present asm
// implements the same compare against these sentinels).
#define STAGE_DIRTY_ROW_CLEAN(_y)   (gStageMinWord[_y] > gStageMaxWord[_y])

// True when row `y` has been marked since the last stageDirtyClearAll.
// Single-load form (used by the Amiga present's idle-frame early-out
// and any-dirty scan). Valid ONLY under the clipped-marks invariant:
// every real mark widens minWord to <= STAGE_DIRTY_FULL_MAX (79), so
// min != 0xFF exactly when the row was touched. Use
// STAGE_DIRTY_ROW_CLEAN for the per-row copy decision; use this
// cheaper test where one load per row matters and the invariant holds.
#define STAGE_DIRTY_ROW_TOUCHED(_y) (gStageMinWord[_y] != STAGE_DIRTY_CLEAN_MIN)

// The per-row dirty bands are stored as uint8_t, which is only valid
// because SURFACE_WORDS_PER_ROW - 1 (79) fits in a byte. A future width
// change that pushes the word count past 255 must fail to compile here
// rather than silently truncate the band arrays.
typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : -1];

// Per-row dirty word bands for the stage. gStageMinWord[y] is the
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
// is the rightmost (inclusive). Both default to the CLEAN sentinels
// after stageAlloc and after each jlStagePresent.
extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];

// Present-path telemetry (NATIVE-PERF Phase 6 W0, IIgs added W2).
// The presents accumulate how many bytes they moved toward the
// display and how many band rows they scanned; the UBER_PROBE build
// logs deltas around its probe rows to split O(rows) scan cost from
// O(bytes) copy cost and to detect over-copy (e.g. a bounding-box
// union copying clean content). A few 32-bit adds per present buys
// permanently runnable probes. All FOUR ports count: the ST/Amiga/DOS
// C presents and the IIgs present asm (iigsBlitStageToShr increments
// copied per MVN row / per slam row and scanned by 200 per present).
extern uint32_t gPresentCopiedBytes;
extern uint32_t gPresentScannedRows;

// The library-owned stage surface, defined in surface.c (written only
// by stageAlloc/stageFree). Direct extern -- matching the band arrays
// above and codegenArenaInternal.h's direct-extern globals -- so hot
// paths (the port.h IIgs macros, the dirty-mark macro below, the
// palette/scb stage checks) compare surface identity with a plain load
// instead of a cross-TU jlStageGet() call. jlStageGet() stays as the
// public accessor. Treat as read-only outside surface.c.
extern jlSurfaceT *gStage;

// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b.
// Strength-reduced to shifts so the compiler doesn't emit a software
// multiply (~150 cyc per call); 32 KB hashed twice -> ~5 minutes per
// UBER run. The
// shift form is 16-bit-equivalent (mod 2^16) so hash values are
// identical to the original `* 31u` / `* 251u` form.
//   lo *= 31  ==  (lo << 5) - lo
//   hi *= 251 ==  (hi << 8) - (hi << 2) - hi
#define SURFACE_HASH_MIX_BYTE(lo_, hi_, b_) do { \
    (lo_) = (uint16_t)(((((lo_) << 5) - (lo_)) + (b_))); \
    (hi_) = (uint16_t)((((hi_) << 8) - ((hi_) << 2) - (hi_)) + (b_)); \
} while (0)

// Stage SCB / palette dirty flags. jlScbSet* and jlPaletteSet set them
// true when the stage's data is modified; the per-port present code
// checks the flags and clears after upload. Replaces a per-frame
// 712-byte memcmp pair the IIgs port used to run unconditionally.
extern bool gStageScbDirty;
extern bool gStagePaletteDirty;

// Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1
// (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes
// straight to the asm marker so a multi-row mark stays a single JSL;
// elsewhere it is the C loop in surface.c.
#ifdef JOEYLIB_PLATFORM_IIGS
void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) iigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max))
// Constant-fill both band arrays without libc memset (llvm-mos lowers
// memset to a far-call byte loop -- ~10 ms per present; joeyDraw.s).
// ClearAll = CLEAN sentinels (post-present); MarkAll = every row fully
// dirty. iigsByteFill is the same fix for arbitrary far-pointer fills
// (SCB range): value replicated in both bytes of `fill`, count >= 1.
void iigsStageBandsClearAll(void);
void iigsStageBandsMarkAll(void);
void iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count);
#else
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
#endif

// Drawing primitives call this with their already-clipped destination
// rect. If `s` is the stage, the affected rows' [minWord, maxWord]
// bands are widened to cover the rect. If `s` is any other surface,
// the call is a no-op -- non-stage surfaces never get presented, so
// they don't carry dirty state.
//
// CONTRACT: the rect is already clipped to surface bounds and w and h
// are POSITIVE. There is deliberately no w/h <= 0 guard here -- every
// caller proves positive dimensions before marking (jlDrawRect's
// h == 2 interior edges were the last violator, fixed per PERF-AUDIT
// #65's verifier correction), so empty ranges cannot reach this macro.
//
// Macro, not a function (ORCA/C-era rule: no `inline` in headers the
// IIgs build shares): the stage test and the h == 1 single-row widen
// run inline at the call site; only multi-row marks pay a call, into
// surfaceMarkDirtyRows above. Every argument is evaluated exactly
// once, but keep call-site arguments side-effect free anyway.
//
// Planar ports rely on the chunky shadow + c2p path through Phase 8.
// Planar-native primitives (Phases 3+) dual-write: they update both
// the chunky pixels and the bitplanes in the same call, so c2p at
// present time always derives correct planes from up-to-date chunky.
// Phase 9 deletes the chunky shadow + c2p; only at that point will
// per-row planar-vs-chunky tracking even be a possible question, and
// the plan is to avoid it entirely there too (planes become the only
// source of truth).
#define surfaceMarkDirtyRect(_s, _x, _y, _w, _h) do { \
    if ((_s) == gStage) { \
        int16_t markX_   = (_x); \
        int16_t markY_   = (_y); \
        int16_t markH_   = (_h); \
        uint8_t markMin_ = SURFACE_WORD_INDEX(markX_); \
        uint8_t markMax_ = SURFACE_WORD_INDEX(markX_ + (_w) - 1); \
        if (markH_ == 1) { \
            if (markMin_ < gStageMinWord[markY_]) { \
                gStageMinWord[markY_] = markMin_; \
            } \
            if (markMax_ > gStageMaxWord[markY_]) { \
                gStageMaxWord[markY_] = markMax_; \
            } \
        } else { \
            surfaceMarkDirtyRows((uint16_t)markY_, (uint16_t)(markY_ + markH_), markMin_, markMax_); \
        } \
    } \
} while (0)

// Shorthand for "every row, full width" -- used by jlSurfaceClear and
// the bulk-replace paths (jlSurfaceCopy, jlSurfaceLoadFile). No-op if `s`
// is not the stage.
void surfaceMarkDirtyAll(const jlSurfaceT *s);

// Reset every row to CLEAN. Called by jlStagePresent after the slam.
void stageDirtyClearAll(void);

// y -> byte offset of row y in a SURFACE_BYTES_PER_ROW-strided buffer.
// On IIgs this expands to a single indexed long-mode read against
// gRowOffsetLut (built once at jlpInit).
//
// The explicit (y << 1) byte-pointer arithmetic dodges the multiply
// helper that gets emitted for `uint16_t arr[N]` indexing
// (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the
// compiler emits a single ASL + indexed long-mode read.
//
// Other ports get the straight multiply -- gcc / OpenWatcom optimize
// the constant 160 to a shift+add chain.
#ifdef JOEYLIB_PLATFORM_IIGS
extern const uint16_t gRowOffsetLut[SURFACE_HEIGHT];
#define SURFACE_ROW_OFFSET(_y) \
    (*((const uint16_t *)((const uint8_t *)gRowOffsetLut + ((uint16_t)(_y) << 1))))
#else
#define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
#endif

// Chunky 4bpp nibble read: the color index of pixel (x, y) in
// s->pixels. Callers must have already bounds-checked x/y
// (non-negative, on-surface). Macro (not a function -- same ORCA/C-era
// rule as above) so jlSamplePixel, jlpGenericSamplePixel, and the
// flood-fill C fallback walks all inline the read; SURFACE_ROW_OFFSET
// dodges the y * 160 software-multiply helper on IIgs. Only the taken
// parity arm's byte load executes, so each call reads one byte. `_x`
// is evaluated twice -- pass side-effect-free arguments.
#define SURFACE_READ_NIBBLE(_s, _x, _y) \
    (((_x) & 1) \
        ? (uint8_t)((_s)->pixels[SURFACE_ROW_OFFSET(_y) + ((uint16_t)(_x) >> 1)] & 0x0Fu) \
        : (uint8_t)((_s)->pixels[SURFACE_ROW_OFFSET(_y) + ((uint16_t)(_x) >> 1)] >> 4))

// True when the rect (x, y) .. (x + w - 1, y + h - 1) lies fully
// on-surface: the shared fast-path gate for jlFillRect, jlDrawRect,
// and sprite.c's isFullyOnSurface (#44) -- one source of
// truth for the on-surface clip invariant (PERF-AUDIT #62). All 16-bit
// on purpose: the int32 form of this test is a multi-instruction
// carry-chained sequence per compare on the 65816/8086. Guard order is
// load-bearing: w <= SURFACE_WIDTH and h <= SURFACE_HEIGHT are tested
// BEFORE the SURFACE_WIDTH - w / SURFACE_HEIGHT - h subtractions so
// those stay in int16_t range (an unguarded 320 - 65535 would wrap
// positive and falsely pass).
//
// Intended argument types: int16_t x/y, uint16_t w/h. CONTRACT: w >= 1
// and h >= 1 -- for w == 0 the macro would accept x == SURFACE_WIDTH,
// so callers exclude empty rects first (jlFillRect tests w > 0 &&
// h > 0; jlDrawRect returns early on w == 0 || h == 0). Boundary
// proof: w == 65535 -> the first compare fails and && short-circuits
// before the subtraction could wrap; w == 320 -> passes only for
// x == 0 (x <= 320 - 320); x == 319 with w == 1 -> passes (319 <= 319,
// the rightmost column); x == -1 -> x >= 0 fails. For w in
// [1, SURFACE_WIDTH] and x >= 0 the subtraction cannot wrap, so
// x <= SURFACE_WIDTH - w is exactly x + w <= SURFACE_WIDTH -- the
// 32-bit form this replaces. Arguments are evaluated up to twice --
// pass side-effect-free expressions.
#define SURFACE_RECT_ON_SURFACE(_x, _y, _w, _h) \
    ((_w) <= SURFACE_WIDTH && (_h) <= SURFACE_HEIGHT && \
     (_x) >= 0 && (_y) >= 0 && \
     (_x) <= (int16_t)(SURFACE_WIDTH - (int16_t)(_w)) && \
     (_y) <= (int16_t)(SURFACE_HEIGHT - (int16_t)(_h)))

// Record a human-readable error string for jlLastError(). Defined in
// init.c; usable by other core TUs (e.g. surface.c on allocation /
// limit failure). The string must have static lifetime.
void coreSetError(const char *message);

// Allocate and free the library-owned stage (the back-buffer surface
// that jlStagePresent flips to the display). Called from init.c during
// jlInit / jlShutdown. The stage's pixel storage is supplied by
// the port HAL via jlpStageAllocPixels.
bool stageAlloc(void);
void stageFree(void);

// Fill all 16 of `s`'s palettes with the standard 16-color EGA
// palette. Called by stageAlloc and jlSurfaceCreate so a program that
// draws without first calling jlPaletteSet still gets recognizable
// colors instead of an all-black palette.
void paletteInitDefault(jlSurfaceT *s);

#endif
