// AGI v2 VIEW (sprite/animation) decoder.
//
// Parses an in-RAM VIEW resource into a structured loop/cel table
// once at load time, then renders any cel against the priority plane
// for per-pixel actor-vs-picture masking. Re-implements the public
// AGI VIEW byte layout from scratch.
//
// Pixel-doubled blit goes straight to the stage so animation cycles
// don't require an intermediate back-buffer. The 160x168 AGI surface
// maps onto stage columns (2 * agiX, 2 * agiX + 1) at the same destY
// agiPicBlit uses.

#include "agi.h"

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

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


// VIEW header byte offsets (publicly documented v2 layout).
#define AGI_VIEW_HDR_LOOP_COUNT    2u
#define AGI_VIEW_HDR_LOOP_TABLE    5u

// Cel header bits (publicly documented v2 layout).
#define AGI_CEL_TRANSPARENT_MASK   0x0Fu
#define AGI_CEL_MIRROR_FLAG        0x80u
#define AGI_CEL_MIRROR_SRC_SHIFT   4u
#define AGI_CEL_MIRROR_SRC_MASK    0x07u

// Priority bands. Y 0..47 is sky (priority 4); Y 48..167 splits into
// 10 ground bands of 12 pixels each at priorities 5..14. Priority 15
// is "always on top" and reserved for HUD/overlay; an actor's natural
// Y never reaches it.
#define AGI_PRI_SKY_TOP            48
#define AGI_PRI_GROUND_FIRST       5u
#define AGI_PRI_GROUND_BAND_PX     12


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


// ----- File-local types -----

// Per-draw invariants for one cel blit, grouped so they live in memory
// rather than competing for the w65816's tiny register file. Passing a
// pointer to this keeps the hot blit helpers under the backend's
// register-allocation ceiling.
typedef struct {
    const uint8_t  *picPriority;
    int16_t         x;
    int16_t         width;
    uint8_t         actorPri;
    uint8_t         transparent;
    bool            mirrored;
} AgiCelBlitT;


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

static void                 blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY);
static const uint8_t       *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow);
static void                 markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height);
static bool                 parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes);
static const AgiCelInfoT   *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored);


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

// Planar fallback row walker (stage->pixels == NULL). Routes every
// opaque, visible pixel through jlDrawPixel so the port's c2p /
// plane-sync path stays correct. Split out of agiViewDraw so its
// locals don't pile onto the chunky path's register budget.
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) {
    int16_t  width;
    int16_t  cy;
    int16_t  cx;
    int16_t  drawCx;
    int16_t  agiX;
    int16_t  agiY;
    int16_t  stageX;
    int16_t  stageY;
    uint8_t  rleByte;
    uint8_t  color;
    uint8_t  runLen;
    uint8_t  r;
    uint8_t  picPri;

    width = blit->width;
    for (cy = 0; cy < height; cy++) {
        cx = 0;
        for (;;) {
            rleByte = *rle++;
            if (rleByte == 0u) { break; }
            color  = (uint8_t)(rleByte >> 4);
            runLen = (uint8_t)(rleByte & 0x0Fu);
            for (r = 0u; r < runLen; r++) {
                if (cx >= width) { break; }
                if (color != blit->transparent) {
                    drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
                    agiX   = (int16_t)(blit->x + drawCx);
                    agiY   = (int16_t)(topY + cy);
                    if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
                        picPri = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
                        if (picPri <= blit->actorPri) {
                            stageX = (int16_t)(agiX << 1);
                            stageY = (int16_t)(destY + agiY);
                            jlDrawPixel(stage, stageX, stageY, color);
                            jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
                        }
                    }
                }
                cx++;
            }
        }
    }
}


// Chunky fast-path walker for one source row. Consumes the cel's RLE
// stream from `rle`, writing pixel-doubled bytes into `stageRow` where
// the picture priority allows it, and returns the advanced RLE pointer
// so the caller can hand it to the next row. A NULL priRow / stageRow
// means the row is clipped out vertically: the RLE is still walked (so
// the pointer lands correctly for later rows) but nothing is drawn.
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) {
    int16_t  width;
    int16_t  cx;
    int16_t  drawCx;
    int16_t  agiX;
    uint8_t  rleByte;
    uint8_t  color;
    uint8_t  runLen;
    uint8_t  packed;
    uint8_t  r;

    width = blit->width;
    cx    = 0;
    for (;;) {
        rleByte = *rle++;
        if (rleByte == 0u) { break; }
        color  = (uint8_t)(rleByte >> 4);
        runLen = (uint8_t)(rleByte & 0x0Fu);
        // Transparent runs and vertically-clipped rows both just skip
        // ahead by runLen without touching the stage.
        if (color == blit->transparent || priRow == NULL) {
            cx = (int16_t)(cx + runLen);
            if (cx > width) { cx = width; }
            continue;
        }
        packed = (uint8_t)((color << 4) | color);
        for (r = 0u; r < runLen; r++) {
            if (cx >= width) { break; }
            drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
            agiX   = (int16_t)(blit->x + drawCx);
            if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
                if (priRow[agiX] <= blit->actorPri) {
                    stageRow[agiX] = packed;
                }
            }
            cx++;
        }
    }
    return rle;
}


// Mark the cel's stage bounding box dirty once after a chunky blit.
// Computes the pixel-doubled bbox from the source rect, clips it to the
// stage so the surface marker doesn't reject the whole rect for an
// off-screen edge, and records it if any of it is on-screen.
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) {
    int16_t  minStageX;
    int16_t  maxStageX;
    int16_t  minStageY;
    int16_t  maxStageY;

    minStageX = (int16_t)(x << 1);
    maxStageX = (int16_t)((x + width) << 1);
    minStageY = (int16_t)(destY + topY);
    maxStageY = (int16_t)(destY + topY + height);
    if (minStageX < 0) { minStageX = 0; }
    if (minStageY < 0) { minStageY = 0; }
    if (maxStageX > (int16_t)SURFACE_WIDTH)  { maxStageX = (int16_t)SURFACE_WIDTH; }
    if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
    if (maxStageX > minStageX && maxStageY > minStageY) {
        surfaceMarkDirtyRect(stage, minStageX, minStageY,
                             (int16_t)(maxStageX - minStageX),
                             (int16_t)(maxStageY - minStageY));
    }
}


static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) {
    uint8_t         celCount;
    uint8_t         i;
    uint16_t        celOffset;
    const uint8_t  *celStart;
    uint8_t         flags;

    if (maxBytes < 1u) {
        return false;
    }
    celCount = loopStart[0];
    if ((uint16_t)celCount > AGI_MAX_CELS_PER_LOOP) {
        return false;
    }
    if (maxBytes < (uint16_t)(1u + (uint16_t)celCount * 2u)) {
        return false;
    }

    loop->celCount = celCount;
    for (i = 0u; i < celCount; i++) {
        celOffset = (uint16_t)(loopStart[1u + (uint16_t)i * 2u] |
                               ((uint16_t)loopStart[2u + (uint16_t)i * 2u] << 8));
        if ((uint16_t)(celOffset + 3u) > maxBytes) {
            return false;
        }
        celStart = loopStart + celOffset;
        flags    = celStart[2];
        loop->cels[i].width            = celStart[0];
        loop->cels[i].height           = celStart[1];
        loop->cels[i].transparentColor = (uint8_t)(flags & AGI_CEL_TRANSPARENT_MASK);
        loop->cels[i].mirrored         = (uint8_t)((flags & AGI_CEL_MIRROR_FLAG) ? 1u : 0u);
        loop->cels[i].mirrorSourceLoop = (uint8_t)((flags >> AGI_CEL_MIRROR_SRC_SHIFT) & AGI_CEL_MIRROR_SRC_MASK);
        loop->cels[i].rleData          = celStart + 3;
    }
    return true;
}


// If the cel is a mirror, swap to the source loop's same-index cel and
// flag the caller to flip the RLE walk horizontally. Mirror chains
// don't recurse in valid AGI data; we don't follow more than one hop.
//
// Sierra's encoder marks every cel in BOTH halves of a mirror pair
// with the mirror flag, with the source-loop field pointing to the
// lower-numbered loop. The cel is only an actual mirror when the
// source-loop field points somewhere ELSE; when source == this loop,
// this IS the source data and must be drawn unflipped (otherwise both
// loops in a pair render mirrored and e.g. Graham faces left whether
// the player presses left or right).
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored) {
    const AgiCelInfoT  *cel;
    uint8_t             sourceLoop;

    if (loopIdx >= view->loopCount) {
        return NULL;
    }
    if (celIdx >= view->loops[loopIdx].celCount) {
        return NULL;
    }
    cel = &view->loops[loopIdx].cels[celIdx];
    *outMirrored = false;
    if (cel->mirrored) {
        sourceLoop = cel->mirrorSourceLoop;
        if (sourceLoop != loopIdx && sourceLoop < view->loopCount && celIdx < view->loops[sourceLoop].celCount) {
            cel = &view->loops[sourceLoop].cels[celIdx];
            *outMirrored = true;
        }
    }
    return cel;
}


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

uint8_t agiActorPriorityForY(int16_t y) {
    int16_t  capped;
    int16_t  band;

    if (y < AGI_PRI_SKY_TOP) {
        return 4u;
    }
    capped = (y > (int16_t)(AGI_PIC_HEIGHT - 1)) ? (int16_t)(AGI_PIC_HEIGHT - 1) : y;
    band   = (int16_t)(AGI_PRI_GROUND_FIRST + (capped - AGI_PRI_SKY_TOP) / AGI_PRI_GROUND_BAND_PX);
    if (band > 14) {
        band = 14;
    }
    return (uint8_t)band;
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif

void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
                 int16_t x, int16_t y, uint8_t actorPri,
                 const uint8_t *picPriority,
                 jlSurfaceT *stage, int16_t destY) {
    const AgiCelInfoT  *cel;
    AgiCelBlitT         blit;
    const uint8_t      *rle;
    bool                mirrored;
    int16_t             height;
    int16_t             topY;
    uint8_t            *stagePixels;

    cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
    if (cel == NULL) {
        return;
    }
    height           = (int16_t)cel->height;
    blit.picPriority = picPriority;
    blit.x           = x;
    blit.width       = (int16_t)cel->width;
    blit.actorPri    = actorPri;
    blit.transparent = cel->transparentColor;
    blit.mirrored    = mirrored;
    rle              = cel->rleData;
    topY             = (int16_t)(y - height + 1);
    stagePixels      = (stage != NULL) ? stage->pixels : NULL;

    // Per-pixel chunky fast path: pixel-doubled output means each
    // source pixel maps to exactly one stage byte (both nibbles =
    // same color). We can skip jlDrawPixel and write the byte
    // direct, marking the cel bbox dirty once at the end. ~80x
    // faster on DOSBox @ 386SX-equivalent CPU cycles.
    if (stagePixels != NULL) {
        int16_t         cy;
        int16_t         agiY;
        const uint8_t  *priRow;
        uint8_t        *stageRow;

        for (cy = 0; cy < height; cy++) {
            agiY = (int16_t)(topY + cy);
            // Hoist per-row work out of the per-pixel inner loop.
            // For rows entirely outside the pic clip we still have to
            // walk the RLE so the byte pointer ends in the right place
            // for subsequent rows.
            if (agiY < 0 || agiY >= (int16_t)AGI_PIC_HEIGHT) {
                priRow   = NULL;
                stageRow = NULL;
            } else {
                priRow   = picPriority + (int32_t)agiY * (int32_t)AGI_PIC_WIDTH;
                stageRow = stagePixels +
                           (int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW;
            }
            rle = blitRowChunky(&blit, rle, priRow, stageRow);
        }
        markCelDirty(stage, x, blit.width, destY, topY, height);
        return;
    }

    // Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
    // jlDrawPixel so the port's c2p / plane-sync path stays correct.
    blitPlanar(&blit, rle, height, topY, stage, destY);
}


void agiViewFree(AgiViewT *view) {
    if (view->raw != NULL) {
        free(view->raw);
        view->raw = NULL;
    }
    view->rawLength = 0u;
    view->loopCount = 0u;
}


bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length) {
    uint8_t   loopCount;
    uint8_t   i;
    uint16_t  loopOffset;

    memset(view, 0, sizeof(*view));
    if (length < AGI_VIEW_HDR_LOOP_TABLE) {
        free(rawBytes);
        return false;
    }

    loopCount = rawBytes[AGI_VIEW_HDR_LOOP_COUNT];
    if ((uint16_t)loopCount > AGI_MAX_LOOPS_PER_VIEW) {
        free(rawBytes);
        return false;
    }
    if ((uint16_t)(AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)loopCount * 2u) > length) {
        free(rawBytes);
        return false;
    }

    view->raw       = rawBytes;
    view->rawLength = length;
    view->loopCount = loopCount;

    for (i = 0u; i < loopCount; i++) {
        loopOffset = (uint16_t)(rawBytes[AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)i * 2u] |
                                ((uint16_t)rawBytes[AGI_VIEW_HDR_LOOP_TABLE + 1u + (uint16_t)i * 2u] << 8));
        if (loopOffset >= length) {
            agiViewFree(view);
            return false;
        }
        if (!parseLoop(&view->loops[i], rawBytes + loopOffset, (uint16_t)(length - loopOffset))) {
            agiViewFree(view);
            return false;
        }
    }
    return true;
}
