// sfxMixHost.c - Host-cc differential harness for the shared SFX
// overlay mixer (src/core/audioSfxMix.c). Built by
// scripts/check-sfxmix.sh on the millisHost.c / check-millis.sh
// pattern. The mixer TU has no jlp* platform hooks; its only external
// input is the stream fill callback, mocked below (streamFill) the way
// millisHost.c mocks jlpFrameCount/jlpFrameHz.
//
// Every scenario is fully deterministic: all sample data, background
// (dst) data, and stream chunks come from a fixed-seed LCG -- no time,
// no rand(), no host entropy. Each scenario prints:
//
//   name=<scenario> slots=<activeSlots> len=<bytes> hash=0x<fnv1a>
//   first=<hex of the first bytes, for debugging>
//   raw=<hex of the whole mixed output>
//
// check-sfxmix.sh captures the first run as
// tests/goldens/sfxmix-baseline.txt and later diffs against it:
//   * exact mode (default): hashes must match -- the gate for
//     PERF-AUDIT findings #7 and #33 (bit-identical rewrites).
//   * JL_SFX_TOLERANCE=1 mode: per-sample compare of the raw lines;
//     finding #6 (interp 16x16 multiply) may deviate by at most 1 LSB
//     PER SLOT CONTRIBUTION, so the allowed per-sample delta is
//     tolerance * slots (deviations from simultaneous slots stack
//     before the clamp). That is why slots= is part of the output.
//
// The mixer has no per-slot volume field (volume is applied upstream
// when the sample data is produced), so the "volume 0 / volume max"
// scenarios use amplitude-scaled sample data: amp 0 = silent samples
// (output must equal the untouched background), amp 127 = full-scale
// samples on a full-range background (exercises both clamp branches).

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

#include "audioSfxMixInternal.h"

// Output samples per single mix call.
#define MIX_LEN            1024u
// Debug prefix bytes echoed on the first= line.
#define FIRST_BYTES        16u
// Mixer output rate used by most scenarios (DOS SB-ish rate).
#define OUT_RATE           11025u
// Largest fixed-buffer sample used by the single-mix scenarios.
#define SAMPLE_MAX         4096u
// fixedLongPos64k: enough sequential mix blocks at ~8.19 step to push
// the read index past 65535 input samples (the uint64 posFp headroom
// that the finding #7 rewrite must preserve): 8 * 1024 * 8.19 ~ 67k.
#define LONG_BLOCKS        8u
#define LONG_SAMPLE_LEN    68000u
#define LONG_RATE_HZ       65535u
#define LONG_OUT_RATE      8000u

// Mock stream source: hands out LCG-generated signed samples in
// chunks of at most chunkCap until remaining is exhausted, then
// returns 0 (end-of-stream).
typedef struct {
    uint32_t lcgState;
    uint32_t remaining;
    uint32_t chunkCap;
    int32_t  amp;
} StreamCtxT;

// Prototypes (harness-local, alphabetical).
static void dstFill(uint8_t *dst, uint32_t len, uint32_t seed);
static uint32_t fnv1a(const uint8_t *data, uint32_t len);
static void genSignedSamples(uint32_t *state, uint8_t *dst, uint32_t count, int32_t amp);
static uint32_t lcgNext(uint32_t *state);
static void reportScenario(const char *name, uint8_t slots, const uint8_t *dst, uint32_t len);
static void runFixedScenario(const char *name, uint16_t rateHz, uint32_t outputRate, int32_t amp, uint32_t sampleCount, uint16_t mixLen, uint32_t dstSeed, uint32_t sampleSeed);
static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count);

static uint8_t gDst[MIX_LEN];
static uint8_t gLongOut[LONG_BLOCKS * MIX_LEN];
static uint8_t gLongSample[LONG_SAMPLE_LEN];
static uint8_t gSampleA[SAMPLE_MAX];
static uint8_t gSampleB[SAMPLE_MAX];
static uint8_t gSampleC[SAMPLE_MAX];


// Fill a mix destination with full-range unsigned LCG bytes -- a
// deterministic stand-in for already-rendered MOD output. Full range
// (not centered) so loud SFX hit both clamp branches.
static void dstFill(uint8_t *dst, uint32_t len, uint32_t seed) {
    uint32_t state;
    uint32_t i;

    state = seed;
    for (i = 0; i < len; i++) {
        dst[i] = (uint8_t)(lcgNext(&state) >> 24);
    }
}


// FNV-1a 32-bit.
static uint32_t fnv1a(const uint8_t *data, uint32_t len) {
    uint32_t hash;
    uint32_t i;

    hash = 2166136261u;
    for (i = 0; i < len; i++) {
        hash ^= (uint32_t)data[i];
        hash *= 16777619u;
    }
    return hash;
}


// Fill dst with signed 8-bit samples (stored as their uint8_t bit
// pattern, matching AudioSfxSlotT.sample) scaled to amplitude `amp`
// (0 = silence, 127 = full scale). Truncation toward zero is
// C99-defined, so the scaling is portable and deterministic.
static void genSignedSamples(uint32_t *state, uint8_t *dst, uint32_t count, int32_t amp) {
    uint32_t i;
    int32_t  raw;
    int32_t  scaled;

    for (i = 0; i < count; i++) {
        raw    = (int32_t)(lcgNext(state) >> 24) - 128;
        scaled = (raw * amp) / 127;
        dst[i] = (uint8_t)(int8_t)scaled;
    }
}


// Numerical Recipes LCG; only the high byte is consumed (the low bits
// of an LCG are weak).
static uint32_t lcgNext(uint32_t *state) {
    *state = (*state * 1664525u) + 1013904223u;
    return *state;
}


// Emit the three per-scenario output lines (see file header).
static void reportScenario(const char *name, uint8_t slots, const uint8_t *dst, uint32_t len) {
    uint32_t i;
    uint32_t show;

    printf("name=%s slots=%u len=%lu hash=0x%08lX\n", name, (unsigned)slots, (unsigned long)len, (unsigned long)fnv1a(dst, len));
    show = (len < FIRST_BYTES) ? len : FIRST_BYTES;
    printf("first=");
    for (i = 0; i < show; i++) {
        if (i > 0) {
            putchar(' ');
        }
        printf("%02X", dst[i]);
    }
    printf("\n");
    printf("raw=");
    for (i = 0; i < len; i++) {
        printf("%02X", dst[i]);
    }
    printf("\n");
}


// One fixed-buffer slot mixed once into a fresh background. mixLen
// may exceed MIX_LEN up to sizeof(gLongOut).
static void runFixedScenario(const char *name, uint16_t rateHz, uint32_t outputRate, int32_t amp, uint32_t sampleCount, uint16_t mixLen, uint32_t dstSeed, uint32_t sampleSeed) {
    AudioSfxSlotT slot;
    uint32_t      state;

    state = sampleSeed;
    genSignedSamples(&state, gSampleA, sampleCount, amp);
    dstFill(gLongOut, mixLen, dstSeed);
    memset(&slot, 0, sizeof(slot));
    audioSfxSlotArm(&slot, gSampleA, sampleCount, rateHz, outputRate);
    audioSfxOverlayMix(gLongOut, mixLen, &slot, 1u);
    reportScenario(name, 1u, gLongOut, mixLen);
}


// Mock jlAudioStreamFillT (see StreamCtxT).
static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) {
    StreamCtxT *sc;
    uint32_t    give;

    sc   = (StreamCtxT *)ctx;
    give = count;
    if (give > sc->chunkCap) {
        give = sc->chunkCap;
    }
    if (give > sc->remaining) {
        give = sc->remaining;
    }
    genSignedSamples(&sc->lcgState, (uint8_t *)dst, give, sc->amp);
    sc->remaining -= give;
    return give;
}


int main(void) {
    AudioSfxSlotT slot;
    AudioSfxSlotT slots[3];
    StreamCtxT    ctx;
    uint32_t      state;
    uint32_t      block;

    printf("sfxMixHost: audioSfxOverlayMix scenarios (LCG-seeded, deterministic)\n");

    // --- Fixed-buffer slots, one slot per mix. ---
    // Rate conversion up: 8363 Hz source into an 11025 Hz mix
    // (step ~0.7585, every fractional interp path exercised).
    runFixedScenario("fixedUp8363to11025", 8363u, OUT_RATE, 100, 2048u, (uint16_t)MIX_LEN, 0xD57B0001u, 0x5EED0001u);
    // Rate conversion down: 22050 -> 11025 (step 2.0, input skipping).
    runFixedScenario("fixedDown22050to11025", 22050u, OUT_RATE, 100, 2200u, (uint16_t)MIX_LEN, 0xD57B0002u, 0x5EED0002u);
    // Unity rate: step exactly 1.0, frac stays 0 (interp identity).
    runFixedScenario("fixedUnity11025", 11025u, OUT_RATE, 80, 1100u, (uint16_t)MIX_LEN, 0xD57B0003u, 0x5EED0003u);
    // Odd stepFp ((5000 << 16) / 11025 = 29721): frac takes odd
    // values, the case where the finding #6 interp reformulation
    // deviates. Single slot, so tolerance mode checks the strict
    // 1-LSB-per-sample bound here (the even-step scenarios above are
    // insensitive to the frac low bit and must stay bit-identical).
    // The deviation only fires when the dropped d * 1 crosses a 65536
    // boundary (~|delta| / 65536 odds per odd-frac sample), so this
    // one mixes 8x longer at full amplitude to make hits certain.
    runFixedScenario("fixedOddStep5000", 5000u, OUT_RATE, 127, 4096u, (uint16_t)(LONG_BLOCKS * MIX_LEN), 0xD57B000Bu, 0x5EED000Cu);
    // "Volume" 0: silent samples; output must equal the background.
    runFixedScenario("fixedAmpZero", 8363u, OUT_RATE, 0, 2048u, (uint16_t)MIX_LEN, 0xD57B0004u, 0x5EED0004u);
    // "Volume" max: full-scale samples on a full-range background --
    // both clamp branches (+127 / -128) fire.
    runFixedScenario("fixedAmpMax", 8363u, OUT_RATE, 127, 2048u, (uint16_t)MIX_LEN, 0xD57B0005u, 0x5EED0005u);
    // Short sample: slot runs out and deactivates mid-mix (~output
    // 395 of 1024); also hits the s1 == s0 tail at idx + 1 == length.
    runFixedScenario("fixedShortEnd", 8363u, OUT_RATE, 127, 300u, (uint16_t)MIX_LEN, 0xD57B0006u, 0x5EED0006u);

    // --- Multiple simultaneous fixed slots. ---
    // Three rates (down / unity / up), three amplitudes; slot B is
    // short and deactivates mid-mix while A and C keep going.
    state = 0x5EED0007u;
    genSignedSamples(&state, gSampleA, 2048u, 60);
    state = 0x5EED0008u;
    genSignedSamples(&state, gSampleB, 900u, 90);
    state = 0x5EED0009u;
    genSignedSamples(&state, gSampleC, 2048u, 127);
    memset(slots, 0, sizeof(slots));
    audioSfxSlotArm(&slots[0], gSampleA, 2048u, 5000u, OUT_RATE);
    audioSfxSlotArm(&slots[1], gSampleB, 900u, 11025u, OUT_RATE);
    audioSfxSlotArm(&slots[2], gSampleC, 2048u, 18000u, OUT_RATE);
    dstFill(gDst, MIX_LEN, 0xD57B0007u);
    audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 3u);
    reportScenario("multiSlot3", 3u, gDst, MIX_LEN);

    // --- Streaming slots. ---
    // Rate-up stream whose 200-sample chunks force several
    // streamRefill calls inside a single mix (prime + ~3 mid-mix).
    memset(&slot, 0, sizeof(slot));
    ctx.lcgState  = 0x5712EA41u;
    ctx.remaining = 4000u;
    ctx.chunkCap  = 200u;
    ctx.amp       = 100;
    audioSfxSlotArmStream(&slot, streamFill, &ctx, 8363u, OUT_RATE);
    dstFill(gDst, MIX_LEN, 0xD57B0008u);
    audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u);
    reportScenario("streamRefillMid", 1u, gDst, MIX_LEN);

    // Rate-down stream (step 2.0) that ends mid-mix: 500 source
    // samples cover only ~250 outputs, then fill returns 0 and the
    // slot deactivates with the background left untouched after it.
    memset(&slot, 0, sizeof(slot));
    ctx.lcgState  = 0x5712EA42u;
    ctx.remaining = 500u;
    ctx.chunkCap  = 255u;
    ctx.amp       = 110;
    audioSfxSlotArmStream(&slot, streamFill, &ctx, 22050u, OUT_RATE);
    dstFill(gDst, MIX_LEN, 0xD57B0009u);
    audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u);
    reportScenario("streamEndMid", 1u, gDst, MIX_LEN);

    // Fixed + streaming slot in the same mix call (the isStream
    // branch takes both arms across one dst).
    state = 0x5EED000Au;
    genSignedSamples(&state, gSampleA, 2048u, 70);
    memset(slots, 0, sizeof(slots));
    audioSfxSlotArm(&slots[0], gSampleA, 2048u, 6000u, OUT_RATE);
    ctx.lcgState  = 0x5712EA43u;
    ctx.remaining = 3000u;
    ctx.chunkCap  = 180u;
    ctx.amp       = 95;
    audioSfxSlotArmStream(&slots[1], streamFill, &ctx, 16000u, OUT_RATE);
    dstFill(gDst, MIX_LEN, 0xD57B000Au);
    audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 2u);
    reportScenario("mixedFixedStream", 2u, gDst, MIX_LEN);

    // --- Long-position fixed slot (finding #7 headroom). ---
    // Sequential mix blocks walk the read index past 65535 input
    // samples; a posFp rewrite that truncates the index (or drops the
    // frac carry) diverges here. Output is the concatenation of all
    // blocks so every block is part of the hash and raw compare.
    state = 0x5EED000Bu;
    genSignedSamples(&state, gLongSample, LONG_SAMPLE_LEN, 90);
    memset(&slot, 0, sizeof(slot));
    audioSfxSlotArm(&slot, gLongSample, LONG_SAMPLE_LEN, LONG_RATE_HZ, LONG_OUT_RATE);
    for (block = 0; block < LONG_BLOCKS; block++) {
        dstFill(gLongOut + (block * MIX_LEN), MIX_LEN, 0xD57B0100u + block);
        audioSfxOverlayMix(gLongOut + (block * MIX_LEN), (uint16_t)MIX_LEN, &slot, 1u);
    }
    reportScenario("fixedLongPos64k", 1u, gLongOut, LONG_BLOCKS * MIX_LEN);

    return 0;
}
