// Surface allocation, destruction, persistence, and the library-owned
// stage (the back-buffer surface).

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

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

#define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t))
#define SURFACE_FILE_BYTES    (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES)

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

// (public API declared in joey/surface.h)
// (internal prototypes declared in surfaceInternal.h)

// ----- Module state -----

// Non-static: hot paths read it through surfaceInternal.h's extern to
// skip the jlStageGet() cross-TU call (PERF-AUDIT #10/#17). Written
// only by stageAlloc/stageFree below.
jlSurfaceT *gStage = NULL;

// aligned(2): the ST fused present (present68kStSlam) walks these with
// 4-row long-word loads; the 68000 requires EVEN addresses for word
// and long access (2 is also the mint a.out object format's maximum
// alignment), and char arrays otherwise carry no alignment guarantee.
// Harmless on the other ports (plain GNU-C attribute).
uint8_t gStageMinWord[SURFACE_HEIGHT] __attribute__((aligned(2)));
uint8_t gStageMaxWord[SURFACE_HEIGHT] __attribute__((aligned(2)));

// Present-path telemetry; see surfaceInternal.h. Written by the
// ST/Amiga/DOS jlpPresent implementations, read by the UBER_PROBE
// build. Never reset by the library -- probes log deltas.
uint32_t gPresentCopiedBytes = 0;
uint32_t gPresentScannedRows = 0;

// "Stage SCB / palette has changed since last present-side upload."
// A cheap flag check at present time lets a port skip re-uploading the
// 200-byte SCB and 512-byte palette when neither changed -- ~7 ms /
// frame saved on demos that don't churn palette/SCB (i.e., almost all
// demos).
//
// Initially true so the first present uploads. jlScbSet*, jlPaletteSet,
// and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile)
// re-mark dirty when the stage's data changes; the per-port present
// code reads and clears the flags after uploading.
bool gStageScbDirty     = true;
bool gStagePaletteDirty = true;

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

// (surfaceMarkDirtyRows inlines the per-row widen with walking pointers
// -- see below. The single-row widen is inlined by the surfaceMarkDirtyRect
// macro, and the IIgs multi-row path is the asm marker.)


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

jlSurfaceT *jlStageGet(void) {
    return gStage;
}


void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
    if (dst == NULL || src == NULL || dst == src) {
        return;
    }
    jlpSurfaceCopyChunky(dst, src);          /* memcpy on chunky ports; no-op on planar */
    memcpy(dst->scb,     src->scb,     sizeof(src->scb));
    memcpy(dst->palette, src->palette, sizeof(src->palette));
    jlpSurfaceCopyPlanes(dst, src);          /* 4 plane memcpys on planar ports; no-op on chunky */
    surfaceMarkDirtyAll(dst);
    // Bulk-replacing the stage's SCB/palette must re-mark them dirty so
    // the per-port present re-uploads them; surfaceMarkDirtyAll only
    // widens the pixel bands. Matches the jlScbSet / jlPaletteSet path.
    if (dst == gStage) {
        gStageScbDirty     = true;
        gStagePaletteDirty = true;
    }
}


void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) {
    int32_t left;
    int32_t top;
    int32_t right;
    int32_t bottom;

    if (dst == NULL || src == NULL || dst == src) {
        return;
    }
    if (w == 0u || h == 0u) {
        return;
    }
    // Clip in int32 (jlFillRect precedent), then widen OUTWARD to the
    // 16-px group grid per the header contract. SURFACE_WIDTH is a
    // multiple of 16, so the widened right edge never re-crosses it.
    left   = x;
    top    = y;
    right  = (int32_t)x + w;
    bottom = (int32_t)y + 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 (left >= right || top >= bottom) {
        return;
    }
    left  = left & ~(int32_t)15;
    right = (right + 15) & ~(int32_t)15;
    jlpSurfaceCopyRect(dst, src, (uint16_t)left, (uint16_t)top, (uint16_t)(right - left), (uint16_t)(bottom - top));
    // Pixels-only op: the SCB/palette dirty flags are jlSurfaceCopy's
    // concern, not this one's.
    surfaceMarkDirtyRect(dst, (int16_t)left, (int16_t)top, (int16_t)(right - left), (int16_t)(bottom - top));
}


jlSurfaceT *jlSurfaceCreate(void) {
    jlSurfaceT *s;

    // jlpBigAlloc, NOT the C heap: on IIgs the C heap is a few hundred
    // bytes of bank-0 leftover that is NEITHER big enough for this
    // struct NOR reserved from the Memory Manager (Phase 1 IIgs
    // stabilization; PERF-AUDIT.md #77/#81). Other ports map
    // jlpBigAlloc to malloc, so behavior is unchanged there.
    s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
    if (s == NULL) {
        coreSetError("out of memory allocating surface");
        return NULL;
    }
    memset(s, 0, sizeof(jlSurfaceT));
    // jlpSurfaceAllocPixels returns NULL on planar ports (Amiga); the
    // primary storage is the port-allocated planes via portData below.
    // Classify NULL by the build's storage model: chunky ports need
    // pixels, planar ports need portData. A NULL in the required slot
    // is an out-of-memory failure (header contract: return NULL).
    s->pixels = jlpSurfaceAllocPixels();
    s->portData = jlpSurfaceAllocPortData(s, false);
#ifdef JOEYLIB_NATIVE_CHUNKY
    if (s->pixels == NULL) {
        jlpSurfaceFreePortData(s, false, s->portData);
        jlpBigFree(s);
        coreSetError("out of memory allocating surface pixels");
        return NULL;
    }
#else
    if (s->portData == NULL) {
        jlpSurfaceFreePixels(s->pixels);
        jlpBigFree(s);
        coreSetError("out of memory allocating surface planes");
        return NULL;
    }
#endif
    paletteInitDefault(s);
    return s;
}


void jlSurfaceDestroy(jlSurfaceT *s) {
    if (s == NULL) {
        return;
    }
    if (s == gStage) {
        return;
    }
    jlpSurfaceFreePortData(s, false, s->portData);
    jlpSurfaceFreePixels(s->pixels);
    jlpBigFree(s);
}


// Cross-port hash of the surface's logical pixel content (plus SCB and
// palette), delegated to the port HAL so each port reads its native
// pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while
// producing the same logical-pixel hash. Used by the UBER validation
// harness to pixel-compare ports against an IIgs golden reference.
uint32_t jlSurfaceHash(const jlSurfaceT *s) {
    if (s == NULL) {
        return 0u;
    }
    return jlpSurfaceHash(s);
}


bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
    FILE *fp;
    long  fileSize;

    if (dst == NULL || path == NULL) {
        return false;
    }
    fp = fopen(path, "rb");
    if (fp == NULL) {
        return false;
    }
    if (fseek(fp, 0L, SEEK_END) != 0) {
        fclose(fp);
        return false;
    }
    fileSize = ftell(fp);
    if (fileSize != (long)SURFACE_FILE_BYTES) {
        fclose(fp);
        return false;
    }
    if (fseek(fp, 0L, SEEK_SET) != 0) {
        fclose(fp);
        return false;
    }
    if (!jlpSurfaceLoadFile(dst, fp)) {
        fclose(fp);
        return false;
    }
    if (fread(dst->scb, 1, SURFACE_HEIGHT, fp) != SURFACE_HEIGHT) {
        fclose(fp);
        return false;
    }
    if (fread(dst->palette, 1, SURFACE_PALETTE_BYTES, fp) != SURFACE_PALETTE_BYTES) {
        fclose(fp);
        return false;
    }
    fclose(fp);
    surfaceMarkDirtyAll(dst);
    /* Loading a file into the stage overwrites its SCB/palette; re-mark
     * them dirty so the next present re-uploads them. */
    if (dst == gStage) {
        gStageScbDirty     = true;
        gStagePaletteDirty = true;
    }
    return true;
}


bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
    FILE *fp;

    if (src == NULL || path == NULL) {
        return false;
    }
    fp = fopen(path, "wb");
    if (fp == NULL) {
        return false;
    }
    if (!jlpSurfaceSaveFile(src, fp)) {
        fclose(fp);
        return false;
    }
    if (fwrite(src->scb, 1, SURFACE_HEIGHT, fp) != SURFACE_HEIGHT) {
        fclose(fp);
        return false;
    }
    if (fwrite(src->palette, 1, SURFACE_PALETTE_BYTES, fp) != SURFACE_PALETTE_BYTES) {
        fclose(fp);
        return false;
    }
    fclose(fp);
    return true;
}


void surfaceMarkDirtyAll(const jlSurfaceT *s) {
    if (s != gStage) {
        return;
    }
    // Whole-stage mark is a constant fill of both band arrays. On the
    // IIgs, memset lowers to `jsl memset` -- a far-call byte loop (a C
    // word-fill fared no better: llvm-mos re-derives the far pointer
    // every store), so the fill is one JSL into the asm word-store
    // loop; the other ports' libc memsets are already tight fills.
#if defined(JOEYLIB_PLATFORM_IIGS)
    iigsStageBandsMarkAll();
#else
    memset(gStageMinWord, 0, SURFACE_HEIGHT);
    memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
#endif
}


// Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h):
// widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never
// compile this -- the macro routes them straight to the asm marker
// iigsMarkDirtyRowsInner instead.
#ifndef JOEYLIB_PLATFORM_IIGS
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
    uint8_t *minPtr;
    uint8_t *maxPtr;

    // Straight-line inlined widen (no per-row helper call) with walking
    // pointers, so a multi-row stage op -- sprite draw, fillRect --
    // pays one tight loop instead of yEnd-y function calls. This is
    // the ST/Amiga counterpart to the IIgs asm marker. (Whole-tile
    // marks bypass this entirely since NATIVE-PERF Phase 2 R1 --
    // tile.c's tileMarkDirty unrolls its constant 8 rows inline.)
    //
    // Loop shape is PER-PORT, decided by measurement (Phase 2 Step C,
    // 2026-07-07): the x2 unroll lifted Amiga sprite gate rows +4-6%
    // but cost the ST -5% in BOTH the pointer-walking and counted
    // forms (gcc-mint materializes four live pointers either way),
    // and the simple loop re-measured exactly at the ST baseline.
    // DOS was flat either way, so only the Amiga takes the unroll.
    minPtr = &gStageMinWord[y];
    maxPtr = &gStageMaxWord[y];
#if defined(JOEYLIB_PLATFORM_AMIGA)
    {
        uint16_t rows;
        uint16_t pairs;

        rows  = (uint16_t)(yEnd - y);
        pairs = (uint16_t)(rows >> 1);
        while (pairs != 0u) {
            if (minWord < minPtr[0]) {
                minPtr[0] = minWord;
            }
            if (maxWord > maxPtr[0]) {
                maxPtr[0] = maxWord;
            }
            if (minWord < minPtr[1]) {
                minPtr[1] = minWord;
            }
            if (maxWord > maxPtr[1]) {
                maxPtr[1] = maxWord;
            }
            minPtr += 2;
            maxPtr += 2;
            pairs--;
        }
        if ((rows & 1u) != 0u) {
            if (minWord < *minPtr) {
                *minPtr = minWord;
            }
            if (maxWord > *maxPtr) {
                *maxPtr = maxWord;
            }
        }
    }
#else
    {
        uint8_t *minEnd;

        minEnd = &gStageMinWord[yEnd];
        while (minPtr < minEnd) {
            if (minWord < *minPtr) {
                *minPtr = minWord;
            }
            if (maxWord > *maxPtr) {
                *maxPtr = maxWord;
            }
            minPtr++;
            maxPtr++;
        }
    }
#endif
}
#endif


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

bool stageAlloc(void) {
    if (gStage != NULL) {
        return true;
    }
    // jlpBigAlloc for the same reason as jlSurfaceCreate: the IIgs C
    // heap can neither hold nor protect this struct.
    gStage = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
    if (gStage == NULL) {
        return false;
    }
    memset(gStage, 0, sizeof(jlSurfaceT));
    // jlpStageAllocPixels returns NULL on planar ports (Amiga) where
    // the chunky shadow doesn't exist; the planes from portData are
    // the source of truth. On chunky ports NULL pixels means the
    // back-buffer alloc failed (IIgs pins a fixed address and never
    // returns NULL, so this is a real OOM only on DOS).
    gStage->pixels = jlpStageAllocPixels();
    if (gStage->pixels != NULL) {
        memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
    }
    gStage->portData = jlpSurfaceAllocPortData(gStage, true);
#ifdef JOEYLIB_NATIVE_CHUNKY
    if (gStage->pixels == NULL) {
        jlpSurfaceFreePortData(gStage, true, gStage->portData);
        jlpBigFree(gStage);
        gStage = NULL;
        return false;
    }
#else
    if (gStage->portData == NULL) {
        jlpStageFreePixels(gStage->pixels);
        jlpBigFree(gStage);
        gStage = NULL;
        return false;
    }
#endif
    stageDirtyClearAll();
    paletteInitDefault(gStage);
    // Fresh stage = fresh SCB/palette; force the first present after
    // (re-)init to upload them. The load-time initializers cover the
    // first jlInit, but a jlShutdown/jlInit cycle in one process would
    // otherwise leave stale false flags and skip the upload
    // (NATIVE-PERF Phase 2 R9 -- ports now trust these flags instead
    // of memcmp'ing cached SCB/palette mirrors every present).
    gStageScbDirty     = true;
    gStagePaletteDirty = true;
    return true;
}


void stageDirtyClearAll(void) {
    // Constant fill of both band arrays, same form as its sibling
    // surfaceMarkDirtyAll. This runs after EVERY jlStagePresent, so on
    // the IIgs the two memset far-call byte loops here were ~10 ms of
    // every present's fixed base; the asm word-store loop fixes that.
    // The other ports keep memset (their libc fills are already tight).
#if defined(JOEYLIB_PLATFORM_IIGS)
    iigsStageBandsClearAll();
#else
    memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
    memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);
#endif
}


void stageFree(void) {
    if (gStage == NULL) {
        return;
    }
    jlpSurfaceFreePortData(gStage, true, gStage->portData);
    jlpStageFreePixels(gStage->pixels);
    jlpBigFree(gStage);
    gStage = NULL;
}
