// DOS HAL: VGA mode 13h, 6-bit DAC, nibble-expand present.
//
// JoeyLib surfaces are 4bpp chunky with a 200-entry SCB table selecting
// one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of
// operations map cleanly:
//   * All 16 palettes x 16 colors = 256 entries fill the VGA DAC once.
//   * Pixel byte = (scb[y] << 4) | (source nibble).
//   * No per-scanline palette programming needed at display time.
//
// Access to VGA memory uses DJGPP's nearptr mechanism for speed.

#include <setjmp.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>      // uclock / UCLOCKS_PER_SEC

#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/exceptn.h>
#include <sys/nearptr.h>

#include "port.h"
#include "surfaceInternal.h"

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

#define VGA_LINEAR_ADDR   0xA0000u
#define VGA_STRIDE        320u
#define DAC_INDEX_PORT    0x3C8
#define DAC_DATA_PORT     0x3C9
#define VGA_INPUT_STAT_1  0x3DA
#define VGA_VRETRACE_BIT  0x08

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

static void crashHandler(int sig);
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
static void installCrashLog(void);
static void rebuildExpandLut(uint8_t palBase);
static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src);

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

static uint8_t *gVgaMem       = NULL;
static bool     gNearEnabled  = false;
static FILE    *gCrashLog     = NULL;

// (The old $3DA edge-poll frame-counter state lived here; removed with
// the poll-rate-dependent counter itself -- see jlpFrameCount.)

// VGA DAC programming is ~768 outportb calls; skipped when the core
// gStagePaletteDirty flag (surfaceInternal.h) says the stage palette
// is unchanged -- see uploadPaletteIfNeeded.

// Expand LUT for jlpPresent. Maps a 4bpp-packed source byte to the
// 2-byte little-endian mode-13h output pair for the current scanline's
// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
// pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
// write therefore lands the left pixel at the lower address, matching
// the old two-byte sequence exactly. Rebuilt only when the SCB-derived
// palBase changes from the previous row (most stages run long runs of
// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
static uint16_t gExpandLut[256];
static uint8_t  gExpandLutBase   = 0;
static bool     gExpandLutValid  = false;

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

static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
    uint8_t        palBase;
    const uint8_t *srcBytes;
    uint32_t      *out;
    uint16_t       quads;
    uint16_t       i;

    palBase = (uint8_t)(src->scb[y] << 4);
    if (!gExpandLutValid || palBase != gExpandLutBase) {
        rebuildExpandLut(palBase);
    }
    // Dirty bands are chunky-word aligned (jlpPresent, the only
    // caller, computes pixelX = minWord*4 and pixelW = words*4), so x
    // and w are multiples of 4: &dst[x] is 32-bit aligned and the
    // dword fusion below is tail-free by construction. Two source
    // bytes (= 4 nibbles) fuse into ONE aligned 32-bit store straight
    // into VGA RAM (perf-hunt-2 rank 6) -- little-endian puts the
    // first LUT word in the low half, so the written bytes are
    // identical to the old per-pair 16-bit stores. DOSBox's
    // instruction-counting core charges the old loop ~3.5
    // instructions per VGA byte; halving the loop trips halves that.
    srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
    out      = (uint32_t *)&dst[x];
    quads    = (uint16_t)(w >> 2);

    for (i = 0; i < quads; i++) {
        *out++ = (uint32_t)gExpandLut[srcBytes[0]] | ((uint32_t)gExpandLut[srcBytes[1]] << 16);
        srcBytes += 2;
    }
}


static void rebuildExpandLut(uint8_t palBase) {
    uint16_t byte;
    uint8_t  leftPixel;
    uint8_t  rightPixel;

    for (byte = 0; byte < 256u; byte++) {
        // Packed high nibble = LEFT pixel (lower x); packed low nibble
        // = RIGHT pixel. The little-endian word puts the LEFT pixel in
        // the low byte so a 16-bit store lands it at the lower (even)
        // VGA address -- byte-for-byte identical to the old pair store.
        leftPixel        = (uint8_t)(palBase | (byte >> 4));
        rightPixel       = (uint8_t)(palBase | (byte & 0x0Fu));
        gExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
    }
    gExpandLutBase  = palBase;
    gExpandLutValid = true;
}


static void uploadPalette(const jlSurfaceT *src) {
    uint16_t p;
    uint16_t c;
    uint16_t rgb;
    uint8_t  r;
    uint8_t  g;
    uint8_t  b;

    // The whole index+data burst relies on the DAC's auto-increment
    // index latch staying in step across the 768 data writes. Mask
    // interrupts so no installed ISR can desync the latch by touching
    // $3C8/$3C9 mid-burst. Invariant: no installed ISR may write the
    // VGA DAC ports while a palette upload is in progress.
    disable();
    outportb(DAC_INDEX_PORT, 0);
    for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
        for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
            rgb = src->palette[p][c];
            r   = (uint8_t)((rgb >> 8) & 0x0F);
            g   = (uint8_t)((rgb >> 4) & 0x0F);
            b   = (uint8_t)(rgb & 0x0F);
            // VGA DAC is 6-bit per channel; expand JoeyLib's 4-bit
            // channel to 6 bits (0..63).
            outportb(DAC_DATA_PORT, (uint8_t)((r << 2) | (r >> 2)));
            outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2)));
            outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2)));
        }
    }
    enable();
}


// Restore VGA text mode and dump CPU state to gCrashLog so the host
// can recover the report after DOSBox exits. Runs from a signal
// context so we cannot call much of libc safely; fprintf into an
// already-open FILE* and a stale __dpmi_int are the riskiest things
// we touch, both of which DJGPP documents as signal-safe enough for
// crash reporting.
static void crashHandler(int sig) {
    __dpmi_regs       regs;
    struct __jmp_buf *st;

    if (gCrashLog != NULL) {
        // __djgpp_exception_state is a jmp_buf (array of one struct).
        // Decaying it as &[0] gives a pointer to the register fields.
        st = (__djgpp_exception_state_ptr != NULL)
                ? &(*__djgpp_exception_state_ptr)[0]
                : NULL;
        fprintf(gCrashLog, "JoeyLib DOS crash: signal %d\n", sig);
        if (st != NULL) {
            fprintf(gCrashLog,
                "  eip=%08lx eflags=%08lx cs=%04x\n",
                st->__eip, st->__eflags, st->__cs);
            fprintf(gCrashLog,
                "  eax=%08lx ebx=%08lx ecx=%08lx edx=%08lx\n",
                st->__eax, st->__ebx, st->__ecx, st->__edx);
            fprintf(gCrashLog,
                "  esi=%08lx edi=%08lx ebp=%08lx esp=%08lx\n",
                st->__esi, st->__edi, st->__ebp, st->__esp);
            fprintf(gCrashLog,
                "  ds=%04x es=%04x fs=%04x gs=%04x ss=%04x\n",
                st->__ds, st->__es, st->__fs, st->__gs, st->__ss);
        }
        fflush(gCrashLog);
        fclose(gCrashLog);
        gCrashLog = NULL;
    }

    if (gNearEnabled) {
        __djgpp_nearptr_disable();
        gNearEnabled = false;
    }
    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x0003;
    __dpmi_int(0x10, &regs);

    signal(sig, SIG_DFL);
    raise(sig);
}


// Open CRASH.LOG in cwd (DOSBox's mounted dir on the host) and install
// signal handlers that route fault reports there. This runs before the
// VGA mode change so its file handle is established while the screen
// is still text-mode, but the writes themselves only happen on a
// fault, by which time mode might be 13h -- which is fine because the
// log goes to disk, not the screen.
static void installCrashLog(void) {
    if (gCrashLog != NULL) {
        return;
    }
    gCrashLog = fopen("CRASH.LOG", "w");
    if (gCrashLog == NULL) {
        return;
    }
    setbuf(gCrashLog, NULL);
    signal(SIGSEGV, crashHandler);
    signal(SIGFPE,  crashHandler);
    signal(SIGILL,  crashHandler);
    signal(SIGABRT, crashHandler);
}


// `src` is always gStage (jlpPresent's contract), so the core palette
// dirty flag is the change signal, replacing a 512-byte cached-mirror
// memcmp per present (NATIVE-PERF Phase 2 R9). The SCB flag is not
// consumed here: mode 13h has one DAC; the per-line SCB choice is
// applied in the pixel expansion, which reads src->scb directly.
static void uploadPaletteIfNeeded(const jlSurfaceT *src) {
    if (!gStagePaletteDirty) {
        return;
    }
    uploadPalette(src);
    gStagePaletteDirty = false;
}


// ----- HAL API (alphabetical) -----

bool jlpInit(const jlConfigT *config) {
    __dpmi_regs regs;

    (void)config;

    installCrashLog();

    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x0013;
    __dpmi_int(0x10, &regs);

    // BIOS int 10h fn 10h/01h: set the VGA overscan ("border") color.
    // Force it to palette index 0 (black) so the bezel area outside
    // mode 13h's 320x200 doesn't show whatever the prior text mode
    // left in the attribute controller.
    memset(&regs, 0, sizeof(regs));
    regs.h.ah = 0x10;
    regs.h.al = 0x01;
    regs.h.bh = 0x00;
    __dpmi_int(0x10, &regs);

    if (!__djgpp_nearptr_enable()) {
        return false;
    }
    gNearEnabled = true;
    gVgaMem      = (uint8_t *)(__djgpp_conventional_base + VGA_LINEAR_ADDR);
    return true;
}


void jlpPresent(const jlSurfaceT *src) {
    int16_t  y;
    uint8_t  minWord;
    uint8_t  maxWord;
    int16_t  pixelX;
    uint16_t pixelW;

    if (src == NULL || gVgaMem == NULL) {
        return;
    }
    uploadPaletteIfNeeded(src);

    // Walk per-row dirty bands: each chunky word holds 4 mode-13h
    // bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
    // gives the byte range expandAndWriteLine needs.
    // Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed
    // ST/Amiga idle-scan trick): a clean row's minWord is the 0xFF
    // CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX
    // (79), so 4 clean rows read exactly 0xFFFFFFFF and the whole
    // group skips. A failing group keeps the per-row CLEAN test, so a
    // clean row INSIDE a dirty group is skipped exactly as before --
    // written bytes and counters are byte-identical to the row loop.
    {
        typedef uint32_t __attribute__((may_alias)) BandGroupT;
        const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
        uint8_t           g;

        for (g = 0u; g < (uint8_t)(SURFACE_HEIGHT / 4u); g++) {
            int16_t yEnd;

            if (minGroups[g] == 0xFFFFFFFFul) {
                continue;
            }
            yEnd = (int16_t)((uint16_t)g * 4u + 4u);
            for (y = (int16_t)((uint16_t)g * 4u); y < yEnd; y++) {
                // Canonical clean-row test (finding #47): min > max is the
                // CLEAN sentinel; see STAGE_DIRTY_ROW_CLEAN.
                if (STAGE_DIRTY_ROW_CLEAN(y)) {
                    continue;
                }
                minWord = gStageMinWord[y];
                maxWord = gStageMaxWord[y];
                // Defense-in-depth: an upstream clipping bug that wrote
                // maxWord >= SURFACE_WORDS_PER_ROW would push the expand
                // copy past the 320-byte VGA row into the next scanline.
                // Clamp to the in-contract maximum word index.
                if (maxWord >= SURFACE_WORDS_PER_ROW) {
                    maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
                }
                pixelX = (int16_t)((uint16_t)minWord << 2);
                pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
                expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
                gPresentCopiedBytes += pixelW;
            }
        }
    }
    gPresentScannedRows += SURFACE_HEIGHT;
}


// P12 (perf-hunt-2 rank 4): display-side parity probe for the
// UBER_PROBE build -- DOS's analogue of the ST jlpProbeDisplayMismatch
// / IIgs shrParity gates. Re-derives every VGA byte from the stage
// INDEPENDENTLY of the gExpandLut fast path (naive nibble split + scb
// palette base) and counts mismatches against the live VGA
// framebuffer. Zero after a present = the present contract holds; the
// golden hashes gate the stage, not the screen, so this is DOS's only
// display-side gate. Not probe-guarded (tiny, mirrors the other
// ports' always-compiled probe helpers).
uint32_t dosProbeVgaMismatch(const jlSurfaceT *src) {
    uint32_t bad = 0;
    uint16_t x;
    uint16_t y;

    if (src == NULL || gVgaMem == NULL) {
        return 0xFFFFFFFFul;
    }
    for (y = 0; y < SURFACE_HEIGHT; y++) {
        const uint8_t *rowPx   = &src->pixels[(uint32_t)y * SURFACE_BYTES_PER_ROW];
        const uint8_t *vga     = &gVgaMem[(uint32_t)y * VGA_STRIDE];
        uint8_t        palBase = (uint8_t)(src->scb[y] << 4);

        for (x = 0; x < SURFACE_WIDTH; x++) {
            uint8_t packed = rowPx[x >> 1];
            uint8_t nib    = ((x & 1u) != 0u) ? (uint8_t)(packed & 0x0Fu) : (uint8_t)(packed >> 4);

            if ((uint8_t)(palBase | nib) != vga[x]) {
                bad++;
            }
        }
    }
    return bad;
}


// VGA mode 13h vertical refresh on a real CRT runs at ~70 Hz. We
// detect the start of vertical retrace by polling input status
// register 1 ($3DA) bit 3: 1 = currently in vretrace. To get a
// rising-edge wait (so back-to-back calls each yield one frame),
// first wait while we're already in retrace, then wait until we
// re-enter retrace.
void jlpWaitVBL(void) {
    while (inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT) {
        /* spin while still in current retrace */;
    }
    while (!(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT)) {
        /* spin until next retrace begins */;
    }
}


// Frame count derived from the millisecond clock (PIT ISR fires or
// uclock -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The
// old implementation edge-polled $3DA bit 3 and only advanced when a
// call happened to land DURING the ~1ms retrace pulse, so any caller
// doing more than ~1ms of work between polls silently lost frames
// (PERF-AUDIT.md finding #73 -- UBER's batched timing loop stalled on
// it outright). This form is poll-rate-independent and monotonic;
// jlpWaitVBL above still synchronizes to the real retrace.
uint16_t jlpFrameCount(void) {
    return (uint16_t)(((uint64_t)jlpMillisElapsed() * 70u) / 1000u);
}


uint16_t jlpFrameHz(void) {
    /* VGA mode 13h vertical refresh on a real CRT runs at ~70 Hz
     * (70.086 to be exact). Reporting 70 keeps ops/sec scaling
     * accurate within ~0.1%. */
    return 70u;
}


// Wall-clock ms has two possible sources:
//
//   1. When the audio HAL has reprogrammed PIT counter 0 for the
//      60 Hz sound dispatcher, uclock()'s sub-tick read is wrong
//      (uclock assumes 0..65535 cycles; ours is 0..gPitDivisor).
//      We bypass uclock and derive ms from the ISR fire counter,
//      which advances exactly once per 1/gAudioIsrHz second wall.
//
//   2. Otherwise (audio ISR not installed), uclock works correctly
//      because PIT 0 is at the BIOS default and DJGPP's sub-tick
//      math holds.
//
// gAudioIsr* are owned by src/dos/audio.c.
extern volatile uint32_t gAudioIsrFires;
extern uint16_t          gAudioIsrHz;
extern volatile bool     gAudioIsrActive;

static uclock_t gMillisBase    = 0;
static bool     gMillisBaseSet = false;

uint32_t jlpMillisElapsed(void) {
    uclock_t delta;

    if (gAudioIsrActive && gAudioIsrHz > 0u) {
        // Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
        // wall-clock-correct and monotonic. uint64 promotion in
        // the multiply keeps 60 Hz * 4M ticks (~18 hours) from
        // overflowing.
        return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
                          / (uint32_t)gAudioIsrHz);
    }
    // Use an explicit one-shot flag rather than gMillisBase == 0, so a
    // legitimate uclock() value of 0 on the first call cannot re-arm
    // the base every call.
    if (!gMillisBaseSet) {
        gMillisBase    = uclock();
        gMillisBaseSet = true;
        return 0u;
    }
    // Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of
    // dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not
    // 1193.18), which would run the clock ~0.015% fast. The uint64
    // intermediate keeps delta*1000 from overflowing.
    delta = uclock() - gMillisBase;
    return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
}


void jlpShutdown(void) {
    __dpmi_regs regs;

    if (gNearEnabled) {
        __djgpp_nearptr_disable();
        gNearEnabled = false;
    }
    gVgaMem = NULL;

    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x0003;
    __dpmi_int(0x10, &regs);

    if (gCrashLog != NULL) {
        fclose(gCrashLog);
        gCrashLog = NULL;
    }
}


// DOS / VGA mode 13h is chunky-native (8bpp linear). portData is
// unused; the chunky `pixels` buffer feeds the present-time
// nearest-neighbor copy to VGA RAM.


// surfaceCopyPlanes (and the sprite *Planes hooks): elided to ((void)0) by
// port.h's shared chunky block (PERF-AUDIT #42) -- DOS has no bitplanes to
// mirror into, so the former real calls into the no-op generics are gone.

/* Phase 9 reader hooks: chunky ports use the original s->pixels-based
 * paths. */

