// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.

#include <limits.h>
#include <stddef.h>

#include "audioSfxMixInternal.h"

// interp()/mix arithmetic (delta * fracHalf up to ~8.4M, and the
// >> 15 on a possibly-negative value) requires a >= 32-bit int with
// arithmetic right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy
// this; the file is excluded from the IIgs build (16-bit int) by
// make/iigs.mk. Fail loudly if anyone ever shares this mixer with a
// 16-bit-int target instead of producing silent garbage SFX.
#if INT_MAX < 0x7FFFFFFF
#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"
#endif

// Prototypes (file-local, alphabetical).
static int interp(int8_t s0, int8_t s1, uint16_t frac);
static bool streamRefill(AudioSfxSlotT *slot);


void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) {
    AudioSfxSlotT *slot;
    const uint8_t *smp;
    const int8_t  *streamBuf;
    uint32_t       idx;
    uint32_t       stepIdx;
    uint32_t       length;
    uint16_t       frac;
    uint16_t       stepFrac;
    uint16_t       streamLen;
    uint16_t       i;
    uint8_t        s;
    int            sample;
    int            mixed;
    int8_t         s0;
    int8_t         s1;

    for (s = 0; s < numSlots; s++) {
        slot = &slots[s];
        if (!slot->active) {
            continue;
        }
        // Split the 16.16 step once per slot: the inner loops advance
        // a uint32_t sample index + uint16_t fraction with a carry
        // compare instead of 64-bit fixed-point math per sample.
        stepIdx  = slot->stepFp >> 16;
        stepFrac = (uint16_t)(slot->stepFp & 0xFFFFu);
        idx      = slot->posIdx;
        frac     = slot->posFrac;
        // The streaming and fixed-buffer cases are two separate inner
        // loops so the loop-invariant isStream test runs once per slot
        // and the slot fields stay cached in locals across the
        // volatile dst stores (which would otherwise force per-sample
        // re-reads); the short mix body is intentionally duplicated
        // between them.
        if (slot->fill != NULL) {
            // Streaming: refill the prefetch buffer when drained.
            streamBuf = slot->streamBuf;
            streamLen = slot->streamLen;
            for (i = 0; i < len; i++) {
                if (idx >= streamLen) {
                    if (!streamRefill(slot)) {
                        slot->active = false;
                        break;
                    }
                    // streamRefill rewound the slot position; re-read
                    // the fields it updated.
                    idx       = slot->posIdx;
                    frac      = slot->posFrac;
                    streamLen = slot->streamLen;
                }
                s0     = streamBuf[idx];
                s1     = (idx + 1u < streamLen) ? streamBuf[idx + 1u] : s0;
                sample = interp(s0, s1, frac);
                mixed  = (int)dst[i] - 128 + sample;
                if (mixed > 127) {
                    mixed = 127;
                } else if (mixed < -128) {
                    mixed = -128;
                }
                dst[i] = (uint8_t)(mixed + 128);
                frac   = (uint16_t)(frac + stepFrac);
                if (frac < stepFrac) {
                    idx++;
                }
                idx += stepIdx;
            }
        } else {
            // Fixed buffer.
            smp    = slot->sample;
            length = slot->length;
            for (i = 0; i < len; i++) {
                if (idx >= length) {
                    slot->active = false;
                    break;
                }
                s0     = (int8_t)smp[idx];
                s1     = (idx + 1u < length) ? (int8_t)smp[idx + 1u] : s0;
                sample = interp(s0, s1, frac);
                mixed  = (int)dst[i] - 128 + sample;
                if (mixed > 127) {
                    mixed = 127;
                } else if (mixed < -128) {
                    mixed = -128;
                }
                dst[i] = (uint8_t)(mixed + 128);
                frac   = (uint16_t)(frac + stepFrac);
                if (frac < stepFrac) {
                    idx++;
                }
                idx += stepIdx;
            }
        }
        slot->posIdx  = idx;
        slot->posFrac = frac;
    }
}


void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) {
    if (rateHz == 0 || outputRate == 0) {
        return;
    }
    slot->sample    = sample;
    slot->length    = length;
    slot->posIdx    = 0u;
    slot->posFrac   = 0u;
    slot->stepFp    = ((uint32_t)rateHz << 16) / outputRate;
    slot->fill      = NULL;
    slot->fillCtx   = NULL;
    slot->streamLen = 0u;
    slot->streamEof = false;
    slot->active    = true;
}


void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate) {
    if (rateHz == 0 || outputRate == 0 || fill == NULL) {
        return;
    }
    slot->sample    = NULL;
    slot->length    = 0u;
    slot->posIdx    = 0u;
    slot->posFrac   = 0u;
    slot->stepFp    = ((uint32_t)rateHz << 16) / outputRate;
    slot->fill      = fill;
    slot->fillCtx   = ctx;
    slot->streamLen = 0u;
    slot->streamEof = false;
    slot->active    = true;
    // Prime the first chunk so the mixer doesn't refill mid-mix on
    // its very first sample.
    (void)streamRefill(slot);
}


// Linear interpolation between two adjacent signed 8-bit samples.
// `frac` is the 16-bit fractional part of the read position; 0 =
// exactly at s0, 0xFFFF = nearly s1. Result is a signed int in
// [-128, +127].
//
// Formulated as a 16x16->32 signed multiply with explicit int16_t
// locals so m68k gcc matches its mulhisi3 pattern and emits a single
// muls.w instead of a __mulsi3 library call (~250-350 cycles per
// sample on the 68000). Dropping frac's low bit means the result can
// differ from the exact delta * frac >> 16 by at most 1 LSB of 8-bit
// audio -- an accepted, inaudible deviation. The >> 15 of a possibly-
// negative int32_t relies on arithmetic shift, which holds on every
// target this file builds for (DOS/ST).
static int interp(int8_t s0, int8_t s1, uint16_t frac) {
    int16_t delta;
    int16_t fracHalf;

    delta    = (int16_t)(s1 - s0);
    fracHalf = (int16_t)(uint16_t)(frac >> 1);
    return (int)s0 + (int)(((int32_t)delta * fracHalf) >> 15);
}


// Refill a streaming slot's prefetch buffer. Preserves the very last
// sample of the previous chunk at streamBuf[0] so linear interp can
// step across the chunk boundary without a discontinuity click. The
// new chunk fills streamBuf[1..]. On a real refill (a previous chunk
// existed) the position resets to 0 so the first read interpolates the
// carried old-last sample at streamBuf[0] into the new chunk's first
// sample at streamBuf[1], bridging the boundary. On the very first
// prime there is no real previous sample, so the position resets to
// "after byte 0" and the first read starts at the new chunk's first
// sample (index 1).
//
// Returns true if more samples are available, false if the stream
// has ended (caller should mark the slot inactive).
static bool streamRefill(AudioSfxSlotT *slot) {
    int8_t   lastSample;
    uint32_t got;
    bool     haveCarry;

    if (slot->streamEof) {
        return false;
    }
    haveCarry  = (slot->streamLen > 0u);
    lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
    // Clamp the callback's reported count to the request size so a
    // non-conforming fill() that returns more than asked cannot make
    // the mixer read past streamBuf. (Memory safety still requires
    // fill() to honor count -- an over-returning callback has already
    // over-written streamBuf+1; this caps only the subsequent reads.)
    got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
    if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
        got = AUDIO_SFX_STREAM_BUF - 1u;
    }
    if (got == 0u) {
        slot->streamEof = true;
        slot->streamLen = 0u;
        return false;
    }
    slot->streamBuf[0] = lastSample;
    slot->streamLen    = (uint16_t)(got + 1u);
    if (haveCarry) {
        // Bridge the chunk boundary: start at index 0 so the first
        // read interpolates streamBuf[0] (old last sample) -> [1]
        // (new first sample), replaying one sample with no audible
        // discontinuity click.
        slot->posIdx  = 0u;
        slot->posFrac = 0u;
    } else {
        // First prime: streamBuf[0] is a synthetic zero, not a real
        // sample. Skip it so it isn't played as a leading sample.
        slot->posIdx  = 1u;
        slot->posFrac = 0u;
    }
    return true;
}
