// Atari ST audio HAL: libxmp-lite Protracker decoder + 68k Timer A IRQ
// running PWM through YM2149 channel-A volume register.
//
// Pipeline:
//   * jlpAudioInit creates an xmp_context. Timer A is NOT started here:
//     the ISR costs ~half the CPU at 12.3 kHz, so the pump is demand-
//     driven -- installed on the first PlayMod/PlaySfx (MFP prescaler
//     200, data 1) and uninstalled once the mix buffer has drained back
//     to pure silence with nothing playing.
//   * jlpAudioPlayMod loads a .MOD via xmp_load_module_from_memory and
//     calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit
//     output that the ISR consumes one byte at a time.
//   * Each Timer A tick the ISR pops one byte off the buffer, writes
//     its high 4 bits to YM register 8 (channel A volume), advances
//     the play pointer, raises a refill flag at half/end. The refill
//     itself happens in jlpAudioFrameTick on the main thread because
//     libxmp-lite is not interrupt-safe.
//
// Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by
// modern standards but period-correct for stock 520ST/1040ST without
// the STe DMA chip. Library users that want the higher-fidelity STe
// path can skip this HAL and build their own.

#include <string.h>

#include <mint/osbind.h>

#include <libxmp-lite/xmp.h>

#include "port.h"
#include "audioSfxMixInternal.h"
#include "joey/audio.h"

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

// MFP Timer A registers and the autovector slot for its IRQ (vector
// $134, Setexc(0x134/4, ...) = Setexc(13, ...)).
#define ST_MFP_TACR     ((volatile uint8_t *)0xFFFFFA19L)
#define ST_MFP_TADR     ((volatile uint8_t *)0xFFFFFA1FL)
#define ST_MFP_IERA     ((volatile uint8_t *)0xFFFFFA07L)
#define ST_MFP_IMRA     ((volatile uint8_t *)0xFFFFFA13L)
#define ST_MFP_ISRA     ((volatile uint8_t *)0xFFFFFA0FL)

// YM2149 (sound chip) supervisor-only ports. Index reg 7 (mixer)
// controls per-channel tone + noise enables; reg 8/9/A are volumes
// for channels A/B/C; regs 0/1, 2/3, 4/5 are tone period for those
// channels; reg 6 is noise period.
#define ST_YM_SELECT    ((volatile uint8_t *)0xFFFF8800L)
#define ST_YM_DATA      ((volatile uint8_t *)0xFFFF8802L)

// YM mixer (reg 7) high bits 6+7 are the I/O port directions TOS
// leaves configured (port A output, port B = centronics printer
// output). Always preserved when we rewrite the mixer.
#define YM_MIXER_IO_DIR 0xC0u
#define YM_REG_MIXER    7u

// Noise generator: reg 6 is the 5-bit noise period (0 = brightest
// hiss .. 31 = lowest rumble -- the portable jlAudioNoise pitch maps
// to it as identity). Noise is mixed onto channel C, whose mixer
// gate is bit 5 (active low, like the tone gates in bits 0-2) and
// whose 4-bit volume reg 0x0A is SHARED with tone voice 2 -- see the
// guards in applyYmVoice/applyYmNoise.
#define YM_REG_NOISE_PERIOD 6u
#define YM_REG_VOL_C        10u
#define YM_MIX_TONE_C       0x04u
#define YM_MIX_NOISE_C      0x20u

#define MFP_TA_BIT      0x20
#define MFP_TACR_STOP   0x00
#define MFP_TACR_DIV200 0x07

#define VEC_MFP_TA      (0x134 / 4)
#define INT_TIMER_A     13              // Jenabint / Jdisint id for Timer A

#define MIX_RATE        12288           // Hz, matches MFP 2.4576 MHz / 200
#define MIX_BUFFER      2048
#define MIX_HALF        (MIX_BUFFER / 2)

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

// The ISR (audio_isr.s) reads/writes these. Names must match the
// .extern declarations there.
volatile uint8_t  gMixBuf[MIX_BUFFER];
volatile uint8_t *gMixPos     = NULL;
volatile uint8_t *gMixMid     = NULL;
volatile uint8_t *gMixEnd     = NULL;
volatile uint8_t  gNeedRefill[2] = { 0, 0 };

extern void mfpTimerAIsr(void);

static xmp_context gXmpCtx        = NULL;
static bool        gReady         = false;
static bool        gXmpStarted    = false;
static bool        gXmpLoaded     = false;
static int         gXmpLoopCount  = 0;

// Demand-driven Timer A: the 12.3 kHz ISR eats ~half the CPU, so it
// only runs while there is (or may be) audible output. gHalfSilent[i]
// records that half i was last refilled with pure silence; once both
// halves are silent and nothing is playing, frameTick stops the pump.
static bool        gPumpRunning   = false;
static bool        gHalfSilent[2] = { false, false };

static void (*gOldTimerAVec)(void) = NULL;

// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
// Mixed in over libxmp's MOD output during jlpAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];

// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
// drive the mixer through this one shadow so they share one source of
// truth: 0x3F = all tones and noise off (PWM-DAC default). The YM
// can't be read back, so this shadow IS the authoritative state.
static volatile uint8_t gYmToneMask = 0x3Fu;

// ----- Internal helpers -----

// Supexec workers defined further down (with the PSG voice cluster)
// but needed by the early lifecycle functions.
static long ymPsgAllOff(void);

// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
// TOS I/O-port-direction high bits. Supervisor context required.
static void ymWriteMixer(void) {
    *ST_YM_SELECT = YM_REG_MIXER;
    *ST_YM_DATA   = (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask);
}


static long installTimerA(void) {
    uint16_t i;

    // Park the buffer in known-silent state (unsigned 8-bit middle
    // = 128) before opening the IRQ gate.
    for (i = 0; i < MIX_BUFFER; i++) {
        gMixBuf[i] = 0x80;
    }
    gMixPos = (volatile uint8_t *)gMixBuf;
    gMixMid = (volatile uint8_t *)(gMixBuf + MIX_HALF);
    gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
    gNeedRefill[0] = 0;
    gNeedRefill[1] = 0;

    // YM2149 setup for PWM-via-volume on channel A:
    //   reg 7 (mixer): set bits 0 (tone A off) and 3 (noise A off);
    //                  preserve bits 6+7 (I/O port directions, used
    //                  by TOS for floppy / keyboard / printer).
    //   reg 8 (channel A volume): start at 0 to avoid a pop at start.
    //
    // Without the mixer setup, whatever state TOS left noise A in
    // gets gated by our 12 kHz volume writes -- if noise A was on,
    // a constant volume = constant hiss. Standard PWM-DAC trick is
    // to disable both tone and noise so the volume reg is a pure
    // 4-bit amplitude DAC.
    //
    // We can't reliably read back YM regs on the ST (the data port
    // returns last-write, not register contents), so we OR in the
    // disable bits over an assumed-safe TOS-default mask. Bit 6 set
    // (port A output) matches stock TOS; bit 7 set (port B output)
    // matches the centronics-printer direction TOS configures.
    gYmToneMask = 0x3Fu;   // all tones + noise disabled (PWM-DAC default)
    ymWriteMixer();        // -> 0xFF: tones+noise off, I/O ports A+B output
    *ST_YM_SELECT = 8;
    *ST_YM_DATA   = 0;     // channel A volume = 0 to avoid a pop at start
    *ST_YM_SELECT = 9;
    *ST_YM_DATA   = 0;     // channel B volume = 0
    *ST_YM_SELECT = 10;
    *ST_YM_DATA   = 0;     // channel C volume = 0

    // MFP Timer A: stop, install our vector, set prescaler 200 + data
    // 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
    *ST_MFP_TACR = MFP_TACR_STOP;
    gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)mfpTimerAIsr);
    *ST_MFP_TADR = 1;
    *ST_MFP_TACR = MFP_TACR_DIV200;
    Jenabint(INT_TIMER_A);
    return 0;
}


static long uninstallTimerA(void) {
    Jdisint(INT_TIMER_A);
    *ST_MFP_TACR = MFP_TACR_STOP;
    if (gOldTimerAVec != NULL) {
        (void)Setexc(VEC_MFP_TA, (long)gOldTimerAVec);
        gOldTimerAVec = NULL;
    }
    /* Silence channel A volume so handoff back to TOS is clean (no
     * residual DC level on the speaker). */
    *ST_YM_SELECT = 8;
    *ST_YM_DATA   = 0;
    return 0;
}


static void silenceMixBuffer(void) {
    uint16_t i;
    for (i = 0; i < MIX_BUFFER; i++) {
        gMixBuf[i] = 0x80;
    }
}


static bool anySfxActive(void) {
    uint8_t i;
    for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
        if (gSfxSlots[i].active) {
            return true;
        }
    }
    return false;
}


static void pumpStart(void) {
    if (gPumpRunning) {
        return;
    }
    Supexec(installTimerA);
    gPumpRunning   = true;
    gHalfSilent[0] = false;
    gHalfSilent[1] = false;
}


static void pumpStop(void) {
    if (!gPumpRunning) {
        return;
    }
    Supexec(uninstallTimerA);
    gPumpRunning = false;
}


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

bool jlpAudioInit(void) {
    if (gReady) {
        return true;
    }
    gXmpCtx = xmp_create_context();
    if (gXmpCtx == NULL) {
        return false;
    }
    gReady = true;
    return true;
}


void jlpAudioShutdown(void) {
    // PSG teardown first, independent of gReady: the tone/noise voice
    // layer is usable without jlpAudioInit, and the YM latches -- a
    // live voice keeps sounding after exit unless gated off here.
    Supexec(ymPsgAllOff);

    if (!gReady) {
        return;
    }
    pumpStop();
    silenceMixBuffer();

    if (gXmpCtx != NULL) {
        if (gXmpStarted) {
            xmp_end_player(gXmpCtx);
            gXmpStarted = false;
        }
        if (gXmpLoaded) {
            xmp_release_module(gXmpCtx);
            gXmpLoaded = false;
        }
        xmp_free_context(gXmpCtx);
        gXmpCtx = NULL;
    }
    gReady = false;
}


bool jlpAudioIsPlayingMod(void) {
    return gReady && gXmpStarted;
}


void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
    if (!gReady || gXmpCtx == NULL) {
        return;
    }
    if (gXmpStarted) {
        xmp_end_player(gXmpCtx);
        gXmpStarted = false;
    }
    if (gXmpLoaded) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
    }
    if (xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length) != 0) {
        return;
    }
    gXmpLoaded = true;
    if (xmp_start_player(gXmpCtx, MIX_RATE,
            XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
        return;
    }
    gXmpLoopCount = loop ? -1 : 0;
    gXmpStarted   = true;
    pumpStart();
}


void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
    if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
    pumpStart();
}


void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
    if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
    pumpStart();
}


// YM2149 has three PSG voices; a proper tone-mode driver would
// program voice A here. Not wired yet -- AGI-on-ST will be silent
// until this is filled in.
void jlpAudioTone(uint16_t freqHz) {
    (void)freqHz;
}


// YM2149 PSG: three tone channels (A/B/C) addressed via reg 0..5
// (12-bit tone period each) and reg 8/9/A (4-bit volume each).
// Reg 7 (mixer) gates tone + noise per channel; bits 0..2 = tone
// disable A/B/C (0 = enabled), 3..5 = noise disable, 6..7 = I/O
// port direction (preserve TOS defaults).
//
// Tone period units: 2 MHz / 16 / period = output Hz. So period =
// 125000 / freqHz. 12-bit range covers 30 Hz - 62 kHz.
//
// Volume mapping: AGI atten 0..15 (0=loud) -> YM vol 15..0 (15=loud).
// YM also accepts bit 4 = envelope-controlled volume; we never set
// it (no envelope on AGI tones).
//
// YM regs are supervisor-only on the ST, so this is a Supexec block.
// We stash the request in static globals (Supexec'd functions have
// no parameters of their own).
static volatile uint8_t  gYmVoice;
static volatile uint16_t gYmFreqHz;
static volatile uint8_t  gYmAtten;
static volatile uint8_t  gYmNoisePitch;
static volatile uint8_t  gYmNoiseAtten;
// gYmToneMask is the shared YM mixer shadow, defined in module state.


static long applyYmVoice(void) {
    uint16_t period;
    uint8_t  vol;
    uint8_t  toneReg;
    uint8_t  volReg;

    toneReg = (uint8_t)(gYmVoice * 2u);   // reg 0/2/4
    volReg  = (uint8_t)(8u + gYmVoice);   // reg 8/9/A

    if (gYmFreqHz == 0u || gYmAtten >= 15u) {
        // Silence: disable tone on this channel (mixer bit 0/1/2 set),
        // zero volume. Don't touch other channels' bits. Channel C's
        // volume reg is shared with the noise generator -- leave the
        // level alone while noise is live or a voice-2 note-off would
        // kill active percussion.
        gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
        ymWriteMixer();
        if (gYmVoice != 2u || (gYmToneMask & YM_MIX_NOISE_C) != 0u) {
            *ST_YM_SELECT = volReg;
            *ST_YM_DATA   = 0;
        }
        return 0;
    }

    period = (uint16_t)(125000u / (uint32_t)gYmFreqHz);
    if (period == 0u) {
        period = 1u;
    }
    if (period > 0x0FFFu) {
        period = 0x0FFFu;
    }

    *ST_YM_SELECT = toneReg;
    *ST_YM_DATA   = (uint8_t)(period & 0xFFu);
    *ST_YM_SELECT = (uint8_t)(toneReg + 1u);
    *ST_YM_DATA   = (uint8_t)((period >> 8) & 0x0Fu);

    vol = (uint8_t)(15u - gYmAtten);
    *ST_YM_SELECT = volReg;
    *ST_YM_DATA   = (uint8_t)(vol & 0x0Fu);

    // Enable tone on this channel (clear its bit in mixer).
    gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
    ymWriteMixer();
    return 0;
}


// Supexec worker for jlpAudioNoise; request stashed in gYmNoisePitch/
// gYmNoiseAtten like the applyYmVoice cluster. Channel C's volume reg
// is one 4-bit level shared by tone voice 2 and noise: the last
// on-call wins the level, off-calls preserve it while the other
// consumer is live and zero it only when both gates are closed
// (a nonzero level with both gates closed is a DC offset).
static long applyYmNoise(void) {
    if (gYmNoiseAtten >= 15u) {
        gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_NOISE_C);
        ymWriteMixer();
        if ((gYmToneMask & YM_MIX_TONE_C) != 0u) {
            *ST_YM_SELECT = YM_REG_VOL_C;
            *ST_YM_DATA   = 0;
        }
        return 0;
    }
    // Program period + volume before opening the mixer gate so the
    // gate never opens onto stale settings.
    *ST_YM_SELECT = YM_REG_NOISE_PERIOD;
    *ST_YM_DATA   = (uint8_t)(gYmNoisePitch & 0x1Fu);
    *ST_YM_SELECT = YM_REG_VOL_C;
    *ST_YM_DATA   = (uint8_t)((uint8_t)(15u - gYmNoiseAtten) & 0x0Fu);
    gYmToneMask = (uint8_t)(gYmToneMask & (uint8_t)~YM_MIX_NOISE_C);
    ymWriteMixer();
    return 0;
}


// Supexec worker: gate every tone and the noise off and zero all
// three volumes. Used by jlpAudioShutdown so a latched PSG voice
// (the chip free-runs with zero CPU) cannot keep sounding after the
// program exits.
static long ymPsgAllOff(void) {
    gYmToneMask = 0x3Fu;
    ymWriteMixer();
    *ST_YM_SELECT = 8;
    *ST_YM_DATA   = 0;
    *ST_YM_SELECT = 9;
    *ST_YM_DATA   = 0;
    *ST_YM_SELECT = YM_REG_VOL_C;
    *ST_YM_DATA   = 0;
    return 0;
}


// PIT-style tick ISR not wired on ST yet: Timer A is already busy
// driving 12 kHz PWM for MOD output (see installTimerA). A future
// AGI-on-ST sound path could lift the YM2149 tone driver onto
// Timer B or C and register it here.
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
    (void)fn;
    (void)hz;
    return false;
}


void jlpAudioCriticalEnter(void) {
    // No tick-callback ISR is registered (jlpAudioTickRegister returns
    // false). The Timer A MOD ISR synchronizes with the producer via
    // the gNeedRefill flags, not this critical section, so masking
    // here would only starve that ~12 kHz audio ISR for no benefit.
}


void jlpAudioCriticalExit(void) {
}


void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
    // Same Supexec-with-stashed-args shape as jlpAudioVoice; noise
    // inherits the MOD-collision caveat documented there (the PWM
    // pump's installTimerA force-silences the whole PSG on start).
    gYmNoisePitch = pitch;
    gYmNoiseAtten = atten;
    Supexec(applyYmNoise);
}


void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
    if (voice >= JOEY_AUDIO_VOICES) {
        return;
    }
    // The PWM-via-volume engine in jlpAudioInit hammers YM reg 8
    // (channel A volume) at 12 kHz for MOD output. If that IRQ is
    // running, it will fight the per-voice writes here. AGI does not
    // call jlAudioInit, so Timer A is not installed and the YM is
    // ours -- but a future caller mixing modes would hear collisions
    // on channel A.
    gYmVoice  = voice;
    gYmFreqHz = freqHz;
    gYmAtten  = atten;
    Supexec(applyYmVoice);
}


void jlpAudioStopMod(void) {
    if (!gReady || gXmpCtx == NULL) {
        return;
    }
    if (gXmpStarted) {
        xmp_end_player(gXmpCtx);
        gXmpStarted = false;
    }
    if (gXmpLoaded) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
    }
    silenceMixBuffer();
    // StopMod discards the mixed tail by design (buffer just went
    // silent), so the pump can stop immediately -- unless an SFX is
    // still live, in which case frameTick's drain path owns the stop.
    if (!anySfxActive()) {
        pumpStop();
    }
}


void jlpAudioStopSfx(uint8_t slot) {
    if (slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    gSfxSlots[slot].active = false;
}


// Drains ISR-raised refill flags by re-running libxmp's mixer into
// the consumed half then overlaying any active SFX. Called from the
// game loop, never from IRQ.
//
// Soft-real-time constraint: the producer (this function) must refill
// a consumed half within MIX_HALF / MIX_RATE = 1024 / 12288 ~= 83 ms
// of the ISR raising its flag, or the ISR drains into the half still
// being rewritten (audible tearing). A full MOD-frame mix plus
// audioSfxOverlayMix can approach that budget under load, so callers
// should run the game loop comfortably faster than ~12 Hz.
void jlpAudioFrameTick(void) {
    bool silentFill;

    if (!gReady || !gPumpRunning) {
        return;
    }
    // Underrun guard: both flags set means the producer fell a full
    // buffer behind, so the ISR is now draining a half this tick is
    // about to rewrite. Silence first so the in-flight torn read plays
    // as silence rather than garbage, then refill both halves below.
    if (gNeedRefill[0] && gNeedRefill[1]) {
        silenceMixBuffer();
    }
    // silentFill is sampled BEFORE the overlay: a slot that exhausts
    // during this overlay still wrote its tail into the half, so the
    // half only counts as silent when nothing was live going in.
    if (gNeedRefill[0]) {
        silentFill = !gXmpStarted && !anySfxActive();
        if (gXmpStarted) {
            xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);
        } else {
            memset((void *)gMixBuf, 0x80, MIX_HALF);
        }
        audioSfxOverlayMix(gMixBuf, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
        gHalfSilent[0] = silentFill;
        gNeedRefill[0] = 0;
    }
    if (gNeedRefill[1]) {
        silentFill = !gXmpStarted && !anySfxActive();
        if (gXmpStarted) {
            xmp_play_buffer(gXmpCtx, (void *)(gMixBuf + MIX_HALF), MIX_HALF, gXmpLoopCount);
        } else {
            memset((void *)(gMixBuf + MIX_HALF), 0x80, MIX_HALF);
        }
        audioSfxOverlayMix(gMixBuf + MIX_HALF, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
        gHalfSilent[1] = silentFill;
        gNeedRefill[1] = 0;
    }
    // Drain-to-silence stop: both halves hold pure silence and nothing
    // is playing, so the ISR would stream silence forever -- park it.
    if (!gXmpStarted && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
        pumpStop();
    }
}
