// x86 sprite codegen (DOS port). Emits 32-bit cdecl-callable PIC
// draw / save / restore routines that read or write 4bpp packed
// surface bytes via [esi+disp8] chains. The C side calls them
// through a function pointer cast.
//
// Calling convention:
//   draw(uint8_t *dst)                              -- arg in [esp+8] after prologue saves esi
//   save/restore(const uint8_t *src, uint8_t *dst)  -- args in [esp+12]/[esp+16] after esi+edi save
//
// Per-byte emit, with opaque-run coalescing for the draw path:
//   - all-transparent (both nibbles 0): skip, no instruction
//   - mixed: mov al,[esi+col]; and al,mask; or al,val; mov [esi+col],al
//                                                 (3 + 2 + 2 + 3 = 10 bytes)
//   - run of N consecutive fully-opaque bytes: emit largest chunks
//     while N >= 4: mov dword [esi+col], imm32   (7 bytes,  1 store)
//     if   N >= 2: mov word  [esi+col], imm16   (6 bytes,  1 store)
//     if   N == 1: mov byte  [esi+col], imm8    (4 bytes,  1 store)
//     A run of 4 opaque bytes is therefore one 7-byte store instead of
//     four 4-byte stores (16 bytes / 4 stores). Unaligned access is
//     fine on 386+.
// Per row:
//   add esi, SURFACE_BYTES_PER_ROW                (6 bytes encoded)
// Prologue:
//   push esi; mov esi, [esp+8]                    (1 + 4 = 5 bytes)
// Epilogue:
//   pop esi; ret                                  (1 + 1 = 2 bytes)

#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitChunky.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"


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

// Tile geometry comes from joey/tile.h; TRANSPARENT_NIBBLE from
// spriteInternal.h.

// Stack-arg displacements for the emitted cdecl routines, after the
// prologue saves esi (+esi/edi for save/restore):
//   draw(dst)              -> dst at [esp+8]  (1 push)
//   save/restore(src, dst) -> src at [esp+12], dst at [esp+16] (2 pushes)
#define DRAW_ARG_DST_DISP        0x08u
#define COPY_ARG_SRC_DISP        0x0Cu
#define COPY_ARG_DST_DISP        0x10u

// Largest disp8 [esi+disp8] can encode as a non-negative offset. col
// values above this need the disp32 ModRM form (sign-extended disp8
// would address a negative offset).
#define DISP8_MAX                127u

// Worst-case emitted bytes for one draw dest byte. The mixed RMW path
// is mov al,[esi+d]; and al; or al; mov [esi+d],al = 3+2+2+3 = 10 with
// disp8; the disp32 form adds 3 bytes per [esi+disp32] operand (two of
// them), so up to 16 for a wide mixed byte.
#define DRAW_MAX_BYTES_PER_DESTBYTE  16u
// Bytes for the per-row `add esi, imm32` advance.
#define ROW_STRIDE_ADVANCE_BYTES     6u
// Bytes for the `pop esi[/edi]; ret` epilogue (worst case save/restore).
#define EPILOGUE_BYTES               3u


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

// shiftedByteAt / spriteSourceByte -- the transparency contract shared
// with the IIgs emitter -- come from spriteEmitChunky.h.
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col);
static uint32_t esiOperandLen(uint16_t col);


// ----- Emit helpers (alphabetical) -----

// Shared body for save/restore. Walks heightPx rows of copyBytes
// using rep movsd for the dword-aligned bulk and rep movsb for the
// byte tail. After each row except the last, advances either esi
// or edi by (SURFACE_BYTES_PER_ROW - copyBytes) so the strided side
// (screen) lines up with the next scanline; the contiguous side
// (backup) advances naturally because rep movs* leaves the index
// register one past the last byte copied.
//
// strideOnSrc=true   -> source has the screen stride (SAVE)
// strideOnSrc=false  -> destination has the screen stride (RESTORE)
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
    uint16_t row;
    uint16_t dwords;
    uint16_t tail;
    int32_t  advance;

    dwords  = (uint16_t)(copyBytes >> 2);
    tail    = (uint16_t)(copyBytes & 0x3u);
    advance = (int32_t)SURFACE_BYTES_PER_ROW - (int32_t)copyBytes;

    for (row = 0; row < heightPx; row++) {
        // Worst case per row: rep movsd (7) + rep movsb (7) + add (6).
        if (cursor + 7u + 7u + 6u > cap) {
            return SPRITE_EMIT_OVERFLOW;
        }
        if (dwords > 0) {
            // mov ecx, dwords (B9 imm32); rep movsd (F3 A5)
            out[cursor++] = 0xB9;
            out[cursor++] = (uint8_t)(dwords & 0xFFu);
            out[cursor++] = (uint8_t)((dwords >> 8) & 0xFFu);
            out[cursor++] = 0;
            out[cursor++] = 0;
            out[cursor++] = 0xF3;
            out[cursor++] = 0xA5;
        }
        if (tail > 0) {
            // mov ecx, tail (B9 imm32); rep movsb (F3 A4)
            out[cursor++] = 0xB9;
            out[cursor++] = (uint8_t)(tail & 0xFFu);
            out[cursor++] = 0;
            out[cursor++] = 0;
            out[cursor++] = 0;
            out[cursor++] = 0xF3;
            out[cursor++] = 0xA4;
        }
        if (row + 1u < heightPx) {
            // SAVE: add esi, advance (81 C6 imm32)
            // RESTORE: add edi, advance (81 C7 imm32)
            out[cursor++] = 0x81;
            out[cursor++] = (uint8_t)(strideOnSrc ? 0xC6u : 0xC7u);
            out[cursor++] = (uint8_t)(advance & 0xFFu);
            out[cursor++] = (uint8_t)((advance >> 8) & 0xFFu);
            out[cursor++] = (uint8_t)((advance >> 16) & 0xFFu);
            out[cursor++] = (uint8_t)((advance >> 24) & 0xFFu);
        }
    }
    return cursor;
}


// Emit the `[esi+col]` ModRM + displacement that follows an opcode in
// the draw stores. reg field is 000 (al / opcode-extension 0), so the
// ModRM is 0x46 for the disp8 form (col <= 127) and 0x86 for the disp32
// form. disp8 is sign-extended by the CPU, so any col in 128..255 would
// address a NEGATIVE offset and corrupt pixels; wide sprites
// (widthTiles > 31) hit this, so emit the disp32 form there. Returns
// the new cursor.
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col) {
    if (col <= DISP8_MAX) {
        out[cursor++] = 0x46;                            // ModRM mod=01 [esi+disp8]
        out[cursor++] = (uint8_t)(col & 0xFFu);
    } else {
        out[cursor++] = 0x86;                            // ModRM mod=10 [esi+disp32]
        out[cursor++] = (uint8_t)(col & 0xFFu);
        out[cursor++] = (uint8_t)((col >> 8) & 0xFFu);
        out[cursor++] = 0x00;
        out[cursor++] = 0x00;
    }
    return cursor;
}


// Bytes emitEsiOperand will write for `col` (ModRM + disp).
static uint32_t esiOperandLen(uint16_t col) {
    return (col <= DISP8_MAX) ? 2u : 5u;
}


// Emit a draw routine for one shift variant. Returns bytes written, or
// SPRITE_EMIT_OVERFLOW if the routine would exceed `cap`.
// Routine signature: void f(uint8_t *dst).
uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t  cursor;
    uint16_t  row;
    uint16_t  col;
    uint16_t  runEnd;
    uint16_t  runLen;
    uint16_t  heightPx;
    uint16_t  spriteBytesPerRow;
    uint16_t  destBytesPerRow;
    uint8_t   value;
    uint8_t   opaqueMask;
    uint8_t   v1;
    uint8_t   v2;
    uint8_t   v3;
    uint8_t   m;

    if (shift > 1u) {
        return 0u;
    }

    cursor             = 0;
    heightPx           = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    spriteBytesPerRow  = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
    destBytesPerRow    = (uint16_t)(spriteBytesPerRow + shift);  /* shift is 0 or 1 */

    // Prologue: push esi; mov esi, [esp+8]
    if (cursor + 5u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x56;                     // push esi
    out[cursor++] = 0x8B; out[cursor++] = 0x74;
    out[cursor++] = 0x24; out[cursor++] = DRAW_ARG_DST_DISP;

    // Body: per row, scan dest bytes coalescing fully-opaque runs.
    for (row = 0; row < heightPx; row++) {
        if (row > 0) {
            // add esi, SURFACE_BYTES_PER_ROW (32-bit imm)
            if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
                return SPRITE_EMIT_OVERFLOW;
            }
            out[cursor++] = 0x81; out[cursor++] = 0xC6;
            out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu);
            out[cursor++] = (uint8_t)((SURFACE_BYTES_PER_ROW >> 8) & 0xFFu);
            out[cursor++] = 0x00;
            out[cursor++] = 0x00;
        }
        col = 0;
        while (col < destBytesPerRow) {
            shiftedByteAt(sp, row, col, shift, spriteBytesPerRow, &value, &opaqueMask);
            if (opaqueMask == 0x00) {
                col++;
                continue;
            }
            // Reserve the worst case for one dest byte (wide mixed RMW
            // = 16 bytes) plus the epilogue.
            if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
                return SPRITE_EMIT_OVERFLOW;
            }
            if (opaqueMask != 0xFFu) {
                // Mixed: read-modify-write.
                //   mov al, [esi+col]            (8A /r)
                //   and al, ~opaqueMask          (24 mm)
                //   or  al, value                (0C vv)
                //   mov [esi+col], al            (88 /r)
                out[cursor++] = 0x8A;
                cursor = emitEsiOperand(out, cursor, col);
                out[cursor++] = 0x24;
                out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu);
                out[cursor++] = 0x0C;
                out[cursor++] = value;
                out[cursor++] = 0x88;
                cursor = emitEsiOperand(out, cursor, col);
                col++;
                continue;
            }
            // Fully opaque at col -- find the end of the run.
            runEnd = (uint16_t)(col + 1);
            while (runEnd < destBytesPerRow) {
                shiftedByteAt(sp, row, runEnd, shift, spriteBytesPerRow, &v1, &m);
                if (m != 0xFFu) {
                    break;
                }
                runEnd++;
            }
            runLen = (uint16_t)(runEnd - col);

            // Emit dword stores while >= 4 bytes remain, then a word
            // store if >= 2, then a single byte. shiftedByteAt is cheap
            // enough that re-reading per chunk beats threading a
            // fixed-size buffer through.
            while (runLen >= 4) {
                // mov dword [esi+col], imm32   (C7 /0 ... ii ii ii ii)
                if (cursor + 1u + esiOperandLen(col) + 4u + EPILOGUE_BYTES > cap) {
                    return SPRITE_EMIT_OVERFLOW;
                }
                shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
                shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m);
                shiftedByteAt(sp, row, (uint16_t)(col + 3), shift, spriteBytesPerRow, &v3, &m);
                out[cursor++] = 0xC7;
                cursor = emitEsiOperand(out, cursor, col);
                out[cursor++] = value;
                out[cursor++] = v1;
                out[cursor++] = v2;
                out[cursor++] = v3;
                col    = (uint16_t)(col + 4);
                runLen = (uint16_t)(runLen - 4);
                if (runLen > 0) {
                    shiftedByteAt(sp, row, col, shift, spriteBytesPerRow, &value, &opaqueMask);
                }
            }
            if (runLen >= 2) {
                // mov word [esi+col], imm16    (66 C7 /0 ... ii ii)
                if (cursor + 2u + esiOperandLen(col) + 2u + EPILOGUE_BYTES > cap) {
                    return SPRITE_EMIT_OVERFLOW;
                }
                shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
                out[cursor++] = 0x66;
                out[cursor++] = 0xC7;
                cursor = emitEsiOperand(out, cursor, col);
                out[cursor++] = value;
                out[cursor++] = v1;
                col    = (uint16_t)(col + 2);
                runLen = (uint16_t)(runLen - 2);
                if (runLen > 0) {
                    shiftedByteAt(sp, row, col, shift, spriteBytesPerRow, &value, &opaqueMask);
                }
            }
            if (runLen == 1) {
                // mov byte [esi+col], imm8     (C6 /0 ... ii)
                if (cursor + 1u + esiOperandLen(col) + 1u + EPILOGUE_BYTES > cap) {
                    return SPRITE_EMIT_OVERFLOW;
                }
                out[cursor++] = 0xC6;
                cursor = emitEsiOperand(out, cursor, col);
                out[cursor++] = value;
                col++;
            }
        }
    }

    // Epilogue: pop esi; ret
    if (cursor + 2u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x5E;
    out[cursor++] = 0xC3;
    return cursor;
}


// RESTORE: copy backup -> screen. Destination has the screen stride.
uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t cursor;
    uint16_t heightPx;
    uint16_t copyBytes;

    if (shift > 1u) {
        return 0u;
    }

    cursor    = 0;
    heightPx  = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift);  /* shift is 0 or 1 */

    // Prologue: push esi; push edi; mov esi,[esp+12]; mov edi,[esp+16]
    if (cursor + 10u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x56;
    out[cursor++] = 0x57;
    out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
    out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;

    cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, false);
    if (cursor == SPRITE_EMIT_OVERFLOW) {
        return SPRITE_EMIT_OVERFLOW;
    }

    // Epilogue: pop edi; pop esi; ret
    if (cursor + 3u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x5F;
    out[cursor++] = 0x5E;
    out[cursor++] = 0xC3;
    return cursor;
}


// SAVE: copy screen -> backup. Source has the screen stride.
uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t cursor;
    uint16_t heightPx;
    uint16_t copyBytes;

    if (shift > 1u) {
        return 0u;
    }

    cursor    = 0;
    heightPx  = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift);  /* shift is 0 or 1 */

    if (cursor + 10u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x56;
    out[cursor++] = 0x57;
    out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
    out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;

    cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, true);
    if (cursor == SPRITE_EMIT_OVERFLOW) {
        return SPRITE_EMIT_OVERFLOW;
    }

    if (cursor + 3u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    out[cursor++] = 0x5F;
    out[cursor++] = 0x5E;
    out[cursor++] = 0xC3;
    return cursor;
}
