// AGI v2 PIC (picture / background) decoder.
//
// Paints to two private 160x168 chunky 8bpp buffers (visual + priority).
// The blit step doubles each AGI source column into two stage columns
// so the natural 160x168 internal coordinate system can be used by the
// drawing primitives, and the 320-wide stage receives the canonical
// AGI look.
//
// All algorithms here -- line interpolation, corner drawing, scanline
// flood fill, pen plotting, opcode dispatch -- are re-implementations
// written against the published AGI v2 picture-stream specification.
// No third-party source is referenced.

#include "agi.h"

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

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


// PIC opcodes. Anything < 0xF0 is an argument byte; >= 0xF0 ends the
// current opcode's argument list.
#define OP_FIRST            0xF0u
#define OP_SET_PIC_COLOR    0xF0u
#define OP_DISABLE_PIC      0xF1u
#define OP_SET_PRI_COLOR    0xF2u
#define OP_DISABLE_PRI      0xF3u
#define OP_Y_CORNER         0xF4u
#define OP_X_CORNER         0xF5u
#define OP_ABS_LINE         0xF6u
#define OP_REL_LINE         0xF7u
#define OP_FILL             0xF8u
#define OP_SET_PEN          0xF9u
#define OP_PLOT_PEN         0xFAu
#define OP_END              0xFFu

// Pen control byte bits (set via OP_SET_PEN, consumed by OP_PLOT_PEN).
#define PEN_PATTERN_BIT     0x20u

// Scanline-fill seed stack. Each fill is reentrant-free, so a single
// static stack serves both the visual and priority passes. The bound
// is a safe over-estimate of typical PIC fill complexity; KQ3 PICs
// don't approach it.
#define FILL_STACK_MAX      512


typedef struct {
    uint8_t  visualEnabled;
    uint8_t  priorityEnabled;
    uint8_t  visualColor;
    uint8_t  priorityColor;
    uint8_t  penPattern;
} PicStateT;


// AGI 16-color CGA-derived palette. Entry 6's (R=A, G=5, B=0) keeps the
// classic CGA "brown fix" -- pure (R=A, G=A, B=0) would read as a vivid
// yellow-green on the host, which is wrong for AGI's intended look.
const uint16_t kAgiPalette[16] = {
    0x000u, 0x00Au, 0x0A0u, 0x0AAu,
    0xA00u, 0xA0Au, 0xA50u, 0xAAAu,
    0x555u, 0x55Fu, 0x5F5u, 0x5FFu,
    0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu
};


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

static void     drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst);
static void     drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1);
static void     fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue);
static bool     isOpcode(uint8_t byte);
static void     opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void     opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void     opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void     opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void     plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y);


#ifdef JOEYLIB_PLATFORM_IIGS
#endif

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

static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst) {
    uint16_t  p;
    int16_t   curX;
    int16_t   curY;
    bool      readY;

    p = *pos;
    if (p + 1 >= length) {
        *pos = length;
        return;
    }
    curX = (int16_t)data[p++];
    curY = (int16_t)data[p++];
    plot(pic, state, curX, curY);

    readY = yFirst;
    while (p < length && !isOpcode(data[p])) {
        if (readY) {
            int16_t  newY = (int16_t)data[p++];
            drawLine(pic, state, curX, curY, curX, newY);
            curY = newY;
        } else {
            int16_t  newX = (int16_t)data[p++];
            drawLine(pic, state, curX, curY, newX, curY);
            curX = newX;
        }
        readY = !readY;
    }
    *pos = p;
}


// Documented AGI line algorithm. Major axis steps in unit increments;
// the minor axis is linearly interpolated with round-half-away-from-
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
// 65816 build avoids long-arithmetic helpers entirely.
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
    int16_t  dx;
    int16_t  dy;
    int16_t  absDx;
    int16_t  absDy;
    int16_t  steps;
    int16_t  i;
    int16_t  half;
    int16_t  px;
    int16_t  py;
    int16_t  sx;
    int16_t  sy;

    dx = (int16_t)(x1 - x0);
    dy = (int16_t)(y1 - y0);
    if (dx == 0 && dy == 0) {
        plot(pic, state, x0, y0);
        return;
    }

    absDx = (int16_t)((dx >= 0) ? dx : -dx);
    absDy = (int16_t)((dy >= 0) ? dy : -dy);
    sx    = (int16_t)((dx >= 0) ? 1 : -1);
    sy    = (int16_t)((dy >= 0) ? 1 : -1);

    if (absDx >= absDy) {
        steps = absDx;
        half  = (int16_t)(steps / 2);
        for (i = 0; i <= steps; i++) {
            px = (int16_t)(x0 + sx * i);
            py = (int16_t)(y0 + (dy * i + sy * half) / steps);
            plot(pic, state, px, py);
        }
    } else {
        steps = absDy;
        half  = (int16_t)(steps / 2);
        for (i = 0; i <= steps; i++) {
            py = (int16_t)(y0 + sy * i);
            px = (int16_t)(x0 + (dx * i + sx * half) / steps);
            plot(pic, state, px, py);
        }
    }
}


// Scanline flood fill on one plane. Replaces every cell of value
// `bgValue` 4-connected to (x, y) with `newValue`. The seed stack is
// bounded; runs that don't fit are silently dropped, which mirrors
// the original interpreter's behavior under pathological PIC inputs.
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue) {
    static int16_t  stackX[FILL_STACK_MAX];
    static int16_t  stackY[FILL_STACK_MAX];
    uint16_t        sp;
    int16_t         cx;
    int16_t         cy;
    int16_t         left;
    int16_t         right;
    int16_t         scanX;
    bool            aboveOpen;
    bool            belowOpen;

    if (x < 0 || x >= AGI_PIC_WIDTH || y < 0 || y >= AGI_PIC_HEIGHT) {
        return;
    }
    if (plane[y * AGI_PIC_WIDTH + x] != bgValue) {
        return;
    }
    if (bgValue == newValue) {
        return;
    }

    sp = 0;
    stackX[sp] = x;
    stackY[sp] = y;
    sp++;

    while (sp > 0u) {
        sp--;
        cx = stackX[sp];
        cy = stackY[sp];

        if (plane[cy * AGI_PIC_WIDTH + cx] != bgValue) {
            continue;
        }

        // Expand the current row to the bg run's full extent.
        left = cx;
        while (left > 0 && plane[cy * AGI_PIC_WIDTH + (left - 1)] == bgValue) {
            left--;
        }
        right = cx;
        while (right < (int16_t)(AGI_PIC_WIDTH - 1) && plane[cy * AGI_PIC_WIDTH + (right + 1)] == bgValue) {
            right++;
        }
        for (scanX = left; scanX <= right; scanX++) {
            plane[cy * AGI_PIC_WIDTH + scanX] = newValue;
        }

        // Push one seed per bg run on the row above.
        if (cy > 0) {
            aboveOpen = false;
            for (scanX = left; scanX <= right; scanX++) {
                if (plane[(cy - 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
                    if (!aboveOpen) {
                        if (sp < FILL_STACK_MAX) {
                            stackX[sp] = scanX;
                            stackY[sp] = (int16_t)(cy - 1);
                            sp++;
                        }
                        aboveOpen = true;
                    }
                } else {
                    aboveOpen = false;
                }
            }
        }
        // And one seed per bg run on the row below.
        if (cy < (int16_t)(AGI_PIC_HEIGHT - 1)) {
            belowOpen = false;
            for (scanX = left; scanX <= right; scanX++) {
                if (plane[(cy + 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
                    if (!belowOpen) {
                        if (sp < FILL_STACK_MAX) {
                            stackX[sp] = scanX;
                            stackY[sp] = (int16_t)(cy + 1);
                            sp++;
                        }
                        belowOpen = true;
                    }
                } else {
                    belowOpen = false;
                }
            }
        }
    }
}


static bool isOpcode(uint8_t byte) {
    return byte >= OP_FIRST;
}


static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
    uint16_t  p;
    int16_t   prevX;
    int16_t   prevY;
    int16_t   curX;
    int16_t   curY;
    bool      have;

    p    = *pos;
    have = false;
    prevX = 0;
    prevY = 0;
    while (p + 1 < length && !isOpcode(data[p])) {
        curX = (int16_t)data[p++];
        curY = (int16_t)data[p++];
        if (!have) {
            plot(pic, state, curX, curY);
            have = true;
        } else {
            drawLine(pic, state, prevX, prevY, curX, curY);
        }
        prevX = curX;
        prevY = curY;
    }
    *pos = p;
}


static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
    uint16_t  p;
    int16_t   x;
    int16_t   y;

    p = *pos;
    while (p + 1 < length && !isOpcode(data[p])) {
        x = (int16_t)data[p++];
        y = (int16_t)data[p++];
        if (state->visualEnabled) {
            fillPlane(pic->visual, x, y, AGI_VISUAL_BG, state->visualColor);
        }
        if (state->priorityEnabled) {
            fillPlane(pic->priority, x, y, AGI_PRIORITY_BG, state->priorityColor);
        }
    }
    *pos = p;
}


// Plot-with-pen. With pen patterning off, each iteration consumes an
// X,Y pair and plots a single pixel. With patterning on, each
// iteration consumes a texture byte first (we discard it -- proper
// patterned brushes are deferred to a later pass). Size and shape
// are likewise stubbed to single-pixel; KQ3 PICs use pens sparingly
// and dropping the brush footprint just thins occasional grass / sky
// dithering.
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
    uint16_t  p;
    int16_t   x;
    int16_t   y;

    p = *pos;
    while (p < length && !isOpcode(data[p])) {
        if (state->penPattern) {
            // Skip the texture-selector byte.
            p++;
            if (p >= length || isOpcode(data[p])) {
                break;
            }
        }
        if (p + 1 >= length) {
            break;
        }
        x = (int16_t)data[p++];
        y = (int16_t)data[p++];
        plot(pic, state, x, y);
    }
    *pos = p;
}


static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
    uint16_t  p;
    int16_t   curX;
    int16_t   curY;
    int16_t   newX;
    int16_t   newY;
    uint8_t   disp;
    int16_t   dx;
    int16_t   dy;

    p = *pos;
    if (p + 1 >= length) {
        *pos = p;
        return;
    }
    curX = (int16_t)data[p++];
    curY = (int16_t)data[p++];
    plot(pic, state, curX, curY);

    while (p < length && !isOpcode(data[p])) {
        disp = data[p++];
        // High nibble = X disp (bit 7 sign, bits 4-6 magnitude 0..7).
        // Low nibble  = Y disp (bit 3 sign, bits 0-2 magnitude 0..7).
        dx = (int16_t)((disp >> 4) & 0x07u);
        if (disp & 0x80u) {
            dx = (int16_t)-dx;
        }
        dy = (int16_t)(disp & 0x07u);
        if (disp & 0x08u) {
            dy = (int16_t)-dy;
        }
        newX = (int16_t)(curX + dx);
        newY = (int16_t)(curY + dy);
        drawLine(pic, state, curX, curY, newX, newY);
        curX = newX;
        curY = newY;
    }
    *pos = p;
}


static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) {
    uint16_t  idx;

    if (x < 0 || x >= (int16_t)AGI_PIC_WIDTH) {
        return;
    }
    if (y < 0 || y >= (int16_t)AGI_PIC_HEIGHT) {
        return;
    }
    idx = (uint16_t)(y * (int16_t)AGI_PIC_WIDTH + x);
    if (state->visualEnabled) {
        pic->visual[idx] = state->visualColor;
    }
    if (state->priorityEnabled) {
        pic->priority[idx] = state->priorityColor;
    }
}


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

bool agiPicAlloc(AgiPicT *pic) {
    pic->visual   = (uint8_t *)jlAlloc(AGI_PIC_PIXELS);
    pic->priority = (uint8_t *)jlAlloc(AGI_PIC_PIXELS);
    if (pic->visual == NULL || pic->priority == NULL) {
        agiPicFree(pic);
        return false;
    }
    return true;
}


void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY) {
    int16_t   sy;
    int16_t   sx;
    int16_t   dx;
    uint8_t   color;

    // Fast path: chunky shadow exists -> write packed bytes direct,
    // mark the whole region dirty once at the end. ~30x faster than
    // calling jlDrawPixel 53,760 times (each does bounds + 1-pixel
    // dirty mark). Required for state-2 / state-3 title transitions
    // to render under a second on period-correct DOSBox CPU cycles.
    if (stage != NULL && stage->pixels != NULL) {
        const uint8_t  *src = pic->visual;
        uint8_t        *dst;
        int16_t         rowOff;

        for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
            rowOff = (int16_t)((int16_t)(destY + sy) * (int16_t)SURFACE_BYTES_PER_ROW);
            dst    = &stage->pixels[rowOff];
            for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
                color  = (uint8_t)(src[sy * AGI_PIC_WIDTH + sx] & 0x0Fu);
                // Pixel-doubled: each source pixel = 1 dst byte
                // (both nibbles = same color).
                dst[sx] = (uint8_t)((color << 4) | color);
            }
        }
        surfaceMarkDirtyRect(stage, 0, destY,
                             (int16_t)SURFACE_WIDTH, (int16_t)AGI_PIC_HEIGHT);
        return;
    }

    // Planar fallback (s->pixels NULL: Phase-9 Amiga). Goes through
    // jlDrawPixel so the port's c2p / plane-sync path stays correct.
    for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
        for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
            color = pic->visual[sy * AGI_PIC_WIDTH + sx];
            dx = (int16_t)(sx << 1);
            jlDrawPixel(stage, dx,     (int16_t)(destY + sy), color);
            jlDrawPixel(stage, (int16_t)(dx + 1), (int16_t)(destY + sy), color);
        }
    }
}


void agiPicClear(AgiPicT *pic) {
    if (pic->visual != NULL) {
        memset(pic->visual, AGI_VISUAL_BG, AGI_PIC_PIXELS);
    }
    if (pic->priority != NULL) {
        memset(pic->priority, AGI_PRIORITY_BG, AGI_PIC_PIXELS);
    }
}


bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length) {
    PicStateT  state;
    uint16_t   pos;
    uint8_t    op;

    state.visualEnabled   = 0u;
    state.priorityEnabled = 0u;
    state.visualColor     = 0u;
    state.priorityColor   = 0u;
    state.penPattern      = 0u;

    pos = 0u;
    while (pos < length) {
        op = data[pos++];
        switch (op) {
            case OP_SET_PIC_COLOR:
                if (pos >= length) {
                    return false;
                }
                state.visualColor   = data[pos++];
                state.visualEnabled = 1u;
                break;
            case OP_DISABLE_PIC:
                state.visualEnabled = 0u;
                break;
            case OP_SET_PRI_COLOR:
                if (pos >= length) {
                    return false;
                }
                state.priorityColor   = data[pos++];
                state.priorityEnabled = 1u;
                break;
            case OP_DISABLE_PRI:
                state.priorityEnabled = 0u;
                break;
            case OP_Y_CORNER:
                drawCorner(pic, &state, data, length, &pos, true);
                break;
            case OP_X_CORNER:
                drawCorner(pic, &state, data, length, &pos, false);
                break;
            case OP_ABS_LINE:
                opAbsLine(pic, &state, data, length, &pos);
                break;
            case OP_REL_LINE:
                opRelLine(pic, &state, data, length, &pos);
                break;
            case OP_FILL:
                opFill(pic, &state, data, length, &pos);
                break;
            case OP_SET_PEN:
                if (pos >= length) {
                    return false;
                }
                state.penPattern = (uint8_t)((data[pos++] & PEN_PATTERN_BIT) ? 1u : 0u);
                break;
            case OP_PLOT_PEN:
                opPlotPen(pic, &state, data, length, &pos);
                break;
            case OP_END:
                return true;
            default:
                // Unknown opcode -- stop cleanly rather than misparsing.
                return false;
        }
    }
    return true;
}


// Bake a single VIEW cel into the visual + priority planes (the
// implementation of add.to.pic). Pixels outside the picture window
// or set to the cel's transparent color are skipped. priColor in
// 4..15 overwrites the priority pixels where the visual is opaque;
// 0 preserves the existing priority. RLE format matches agiViewDraw
// (per-row 0-byte EOR marker after a variable run).
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
                   uint8_t loop, uint8_t cel,
                   int16_t baseX, int16_t baseY,
                   uint8_t priColor, uint8_t margin) {
    const AgiCelInfoT  *info;
    const AgiCelInfoT  *resolved;
    const uint8_t      *rle;
    uint8_t             actualLoop;
    int16_t             width;
    int16_t             height;
    uint8_t             trans;
    bool                mirrored;
    int16_t             topY;
    int16_t             cy;
    int16_t             cx;
    int16_t             drawCx;
    int16_t             agiX;
    int16_t             agiY;
    uint8_t             rleByte;
    uint8_t             color;
    uint8_t             runLen;
    uint8_t             r;

    (void)margin;
    if (view == NULL || pic == NULL || pic->visual == NULL) {
        return;
    }
    if (loop >= view->loopCount) {
        return;
    }
    if (cel >= view->loops[loop].celCount) {
        return;
    }
    info       = &view->loops[loop].cels[cel];
    actualLoop = loop;
    mirrored   = false;
    if (info->mirrored != 0u) {
        if (info->mirrorSourceLoop != loop && info->mirrorSourceLoop < view->loopCount
            && cel < view->loops[info->mirrorSourceLoop].celCount) {
            actualLoop = info->mirrorSourceLoop;
            mirrored   = true;
        }
    }
    resolved = &view->loops[actualLoop].cels[cel];

    width  = (int16_t)resolved->width;
    height = (int16_t)resolved->height;
    trans  = resolved->transparentColor;
    rle    = resolved->rleData;
    topY   = (int16_t)(baseY - height + 1);

    for (cy = 0; cy < height; cy++) {
        cx = 0;
        for (;;) {
            rleByte = *rle++;
            if (rleByte == 0u) {
                // End-of-row marker.
                break;
            }
            color  = (uint8_t)(rleByte >> 4);
            runLen = (uint8_t)(rleByte & 0x0Fu);
            for (r = 0u; r < runLen; r++) {
                if (cx >= width) {
                    break;
                }
                if (color != trans) {
                    drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
                    agiX   = (int16_t)(baseX + drawCx);
                    agiY   = (int16_t)(topY + cy);
                    if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
                        int16_t  idx = (int16_t)(agiY * (int16_t)AGI_PIC_WIDTH + agiX);
                        bool     paint = true;

                        // AGI v2 priority masking: when priColor is a
                        // real priority value (>= AGI_PRIORITY_BG),
                        // skip the pixel if the existing priority is
                        // strictly higher -- the new art is behind
                        // existing higher-priority art. This is how
                        // KQ3's title "III" ends up behind the KQ
                        // logo (III priColor=4 = background, KQ logo
                        // in PIC 45 sits at a higher band).
                        if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
                            if (pic->priority[idx] > priColor) {
                                paint = false;
                            }
                        }
                        if (paint) {
                            pic->visual[idx] = color;
                            if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
                                pic->priority[idx] = priColor;
                            }
                        }
                    }
                }
                cx++;
            }
        }
    }
}


void agiPicFree(AgiPicT *pic) {
    if (pic->visual != NULL) {
        jlFree(pic->visual);
        pic->visual = NULL;
    }
    if (pic->priority != NULL) {
        jlFree(pic->priority);
        pic->priority = NULL;
    }
}
