// Space Taxi -- audio dispatch.
//
// Simple 1-voice SFX engine on top of jlAudioVoice. Each SFX call
// programs voice slot 2 (conventionally SFX) with a tone + attenuation
// and arms a frame-tick countdown. When the countdown reaches 0 the
// voice is silenced. New SFX preempt any in-progress one.
//
// Continuous thrust SFX is edge-triggered: on -> arm a looping tone,
// off -> silence. The thrust voice (slot 1) is independent of the
// transient SFX voice so a pickup chirp on top of thrust doesn't
// cut the thrust note.
//
// MUSIC MODEL (important): Space Taxi gameplay has NO background
// music. The C64 only loads songs via $CB02 at non-gameplay sites:
//   - title-screen setup ($459C / $46BC) -> song 8
//   - title-init via $4741 ($477C)       -> song 25
//   - score-screen draw ($4C29)          -> song 7
//   - score-screen variant ($4EB6)       -> song 6
// During gameplay the IRQ music engine ticks ($CC6C) but all 3
// voices are silent because no track pointers are loaded.
// stAudioPlayMusic / stAudioStopMusic stay as stubs for the eventual
// title-screen and score-screen jingle dispatch; do not call them
// from gameplay state transitions.

#include "spacetaxi.h"


#define ST_SFX_VOICE          2u
#define ST_THRUST_VOICE       1u

#define ST_SFX_PICKUP_HZ      720u
#define ST_SFX_PICKUP_TICKS   20u
#define ST_SFX_DROPOFF_HZ     1100u
#define ST_SFX_DROPOFF_TICKS  25u
#define ST_SFX_LAND_HZ        90u
#define ST_SFX_LAND_TICKS     12u
#define ST_SFX_GEAR_HZ        140u   // approximation -- $6410 table untraced
#define ST_SFX_GEAR_TICKS     5u

// C64-matched SFX behavior (see MECHANICS.md "Sound (SID)"):
//
// Thrust: voice 1 freq sweep. $6A63 writes $A0 = 160 to $721B at
// thrust-engage; the phase handler decrements $721B once per tick,
// LSRs it, and writes the result to both bytes of $D400/$D401 --
// giving a SID freq word that drops over the burst. PAL C64 maps
// freq_word=$5050 -> ~1207 Hz at the start; the sweep ends near
// silence. Reproduce with per-frame freq updates on ST_THRUST_VOICE.
#define ST_THRUST_SWEEP_INIT  160u
// Atten scale on the JoeyLib audio HAL is SN76489-style: 0 = loud,
// 15 = silent. Anything >= 15 keys-off the voice (treated as silent
// by halAudioVoice on DOS). Keep all SFX in 0..14 to actually be
// audible. 6 = moderately loud; 10 = quieter.
#define ST_THRUST_ATTEN        8u
// Audible-Hz scale factor for the sweep counter -- chosen so initial
// (counter=160) lands ~1200 Hz to match the C64 PAL freq mapping.
#define ST_THRUST_HZ_PER_TICK 8u

// Crash audio is TWO simultaneous events on the C64:
//   1. impact "bang": voice 3 noise burst ($D412 = $81 then $80 a
//      short time later). Approximated here with rapid pseudo-random
//      freq jumps on ST_SFX_VOICE.
//   2. descending "scream": $721B initialised to $A0 at $6A61, then
//      the phase-1 handler ($6A72) LSRs it each tick into $D400/$D401
//      -- voice 1 freq drops from ~1.2 kHz toward silence over the
//      death anim. Reproduced here on ST_THRUST_VOICE (same voice the
//      C64 uses; the cab can't be thrusting during a crash anyway).
#define ST_SFX_CRASH_TICKS         30u
#define ST_CRASH_SCREAM_INIT      120u   // matches stEngine ST_CRASH_ANIM_FRAMES

#define ST_SFX_DEFAULT_ATTEN   6u


static uint16_t gSfxTicksLeft;
static bool     gThrustOn;
static uint16_t gThrustSweep;       // counts down to zero; per-frame freq
static uint16_t gCrashTicks;        // noise burst countdown (impact "bang")
static uint16_t gCrashScreamSweep;  // voice-1 descending sweep during crash anim
static uint16_t gNoiseRng;          // xorshift state for crash noise


static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks);
static void silenceSfx(void);


void stAudioInit(void) {
    (void)jlAudioInit();
    gThrustOn         = false;
    gSfxTicksLeft     = 0u;
    gThrustSweep      = 0u;
    gCrashTicks       = 0u;
    gCrashScreamSweep = 0u;
    gNoiseRng         = 0xACE1u; // arbitrary nonzero xorshift seed
    jlAudioVoice(ST_SFX_VOICE, 0u, 0u);
    jlAudioVoice(ST_THRUST_VOICE, 0u, 0u);
}


void stAudioShutdown(void) {
    silenceSfx();
    if (gThrustOn) {
        jlAudioVoice(ST_THRUST_VOICE, 0u, 0u);
        gThrustOn = false;
    }
    jlAudioStopMod();
    jlAudioShutdown();
}


// Called every host frame from spacetaxi.c's main loop (alongside
// jlAudioFrameTick which advances the mixer). Drives:
//   - thrust sweep: per-frame freq decrement on ST_THRUST_VOICE
//     mirroring the C64's $721B sweep
//   - crash noise: per-frame xorshift -> freq jump on ST_SFX_VOICE
//     approximating voice-3 noise
//   - transient SFX countdown (pickup/dropoff/land tones)
void stAudioFrameTick(void) {
    // Thrust voice has two modes: normal thrust (gThrustOn) sweeps
    // freq down while held, OR crash scream sweep takes over when
    // active (mutually exclusive -- the cab can't crash and thrust
    // simultaneously). Crash scream wins if both are armed.
    if (gCrashScreamSweep > 0u) {
        gCrashScreamSweep--;
        if (gCrashScreamSweep == 0u) {
            jlAudioVoice(ST_THRUST_VOICE, 0u, 0u);
        } else {
            jlAudioVoice(ST_THRUST_VOICE,
                         (uint16_t)(gCrashScreamSweep * ST_THRUST_HZ_PER_TICK),
                         ST_THRUST_ATTEN);
        }
    } else if (gThrustOn && gThrustSweep > 0u) {
        gThrustSweep--;
        if (gThrustSweep == 0u) {
            jlAudioVoice(ST_THRUST_VOICE, 0u, 0u);
        } else {
            jlAudioVoice(ST_THRUST_VOICE,
                         (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK),
                         ST_THRUST_ATTEN);
        }
    }
    // Noise-burst "bang" on the SFX voice, separate from the scream.
    if (gCrashTicks > 0u) {
        uint16_t  pitchHz;
        // xorshift LFSR -- 16-bit, period 65535.
        gNoiseRng ^= (uint16_t)(gNoiseRng << 7);
        gNoiseRng ^= (uint16_t)(gNoiseRng >> 9);
        gNoiseRng ^= (uint16_t)(gNoiseRng << 8);
        // Map random word to a noisy-feeling pitch range 100..700 Hz.
        pitchHz = (uint16_t)(100u + (gNoiseRng % 600u));
        jlAudioVoice(ST_SFX_VOICE, pitchHz, ST_SFX_DEFAULT_ATTEN);
        gCrashTicks--;
        if (gCrashTicks == 0u) {
            jlAudioVoice(ST_SFX_VOICE, 0u, 0u);
        }
    } else if (gSfxTicksLeft > 0u) {
        gSfxTicksLeft--;
        if (gSfxTicksLeft == 0u) {
            silenceSfx();
        }
    }
}


void stAudioPlayMusic(uint8_t musicId) {
    // TODO: per-target music dispatch. Silent for now so the
    // engine bring-up isn't blocked on writing per-platform music
    // assets.
    (void)musicId;
}


void stAudioStopMusic(void) {
    jlAudioStopMod();
}


// Engine thrust: starts a new freq-sweep burst on each rising edge.
// Mirrors the C64's $6A63 reset of $721B to $A0 -> phase handler
// decrements and writes to $D400/$D401 each tick.
void stAudioSfxThrust(bool on) {
    if (on == gThrustOn) {
        return;
    }
    gThrustOn = on;
    if (on) {
        gThrustSweep = ST_THRUST_SWEEP_INIT;
        jlAudioVoice(ST_THRUST_VOICE,
                     (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK),
                     ST_THRUST_ATTEN);
    } else {
        gThrustSweep = 0u;
        jlAudioVoice(ST_THRUST_VOICE, 0u, 0u);
    }
}


void stAudioSfxGear(void) {
    // $63FC-$640C: the gear toggle fires SFX ptr $6410 through $42E9.
    // The exact $6410 waveform is untraced; a short low click stands
    // in until that SFX table is decoded -- marked approximation, not
    // ROM-derived.
    armSfx(ST_SFX_GEAR_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_GEAR_TICKS);
}


void stAudioSfxLand(void) {
    armSfx(ST_SFX_LAND_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_LAND_TICKS);
}


void stAudioSfxPickup(void) {
    armSfx(ST_SFX_PICKUP_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_PICKUP_TICKS);
}


void stAudioSfxDropoff(void) {
    armSfx(ST_SFX_DROPOFF_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_DROPOFF_TICKS);
}


// Crash: kicks off TWO simultaneous events on the C64 (see header
// comment near ST_SFX_CRASH_TICKS):
//   1. impact "bang" -- noise burst on ST_SFX_VOICE
//   2. descending "scream" -- voice-1 freq sweep on ST_THRUST_VOICE
// Force gThrustOn false so the scream takes over the thrust voice
// cleanly even if the player was thrust-holding into the wall.
void stAudioSfxCrash(void) {
    gThrustOn         = false;
    gCrashTicks       = ST_SFX_CRASH_TICKS;
    gCrashScreamSweep = ST_CRASH_SCREAM_INIT;
    gSfxTicksLeft     = 0u;
}


// ----- internal -----

static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks) {
    jlAudioVoice(ST_SFX_VOICE, freq, atten);
    gSfxTicksLeft = ticks;
}


static void silenceSfx(void) {
    jlAudioVoice(ST_SFX_VOICE, 0u, 0u);
    gSfxTicksLeft = 0u;
}
