/**
 * @file audio.c
 * @brief Implements the audio subsystem.
 */

#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/audio.h>
#include <akgl/error.h>

akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];

/*
 * The device akgl_audio_init() opened, or NULL when the voice table is not
 * connected to one. Everything that mutates a voice locks this stream when it
 * is open, because the device callback reads the same table on SDL's audio
 * thread. When it is NULL there is no other thread to race with.
 */
static SDL_AudioStream *audiostream = NULL;

/** @brief Level every voice is scaled by before the mix is clamped. */
static float32_t mastervolume = 1.0f;

/** @brief Scratch the device callback mixes into before handing it to SDL. */
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];

/*
 * State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
 * no allocation, no locking beyond what already guards the voice table, and it
 * produces the same sequence every run, so a test can assert on noise output.
 */
static uint32_t noisestate = 0x13579bdfu;

/** @brief Whether the voice table has been given its defaults yet. */
static bool voicesready = false;

/**
 * @brief Put every voice back to a flat, audible default.
 *
 * A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
 * sounded a note without first describing an envelope would get nothing and no
 * error saying why. The default is instead the simplest thing that makes noise:
 * a square wave with no attack, no decay and no release, held at full level for
 * as long as the gate is open.
 */
static void reset_voices(void)
{
    int i = 0;

    for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
	akgl_audio_voices[i].active = false;
	akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
	akgl_audio_voices[i].hz = 0.0f;
	akgl_audio_voices[i].phase = 0.0f;
	akgl_audio_voices[i].duration_frames = 0;
	akgl_audio_voices[i].elapsed_frames = 0;
	akgl_audio_voices[i].attack_frames = 0;
	akgl_audio_voices[i].decay_frames = 0;
	akgl_audio_voices[i].release_frames = 0;
	akgl_audio_voices[i].sustain = 1.0f;
	akgl_audio_voices[i].sweep_from_hz = 0.0f;
	akgl_audio_voices[i].sweep_to_hz = 0.0f;
	akgl_audio_voices[i].sweep_step_hz = 0.0f;
    }
    mastervolume = 1.0f;
    voicesready = true;
}

/**
 * @brief Give the voice table its defaults the first time anything touches it.
 *
 * The table is process-wide static storage, so it is reachable before
 * akgl_audio_init() has run. Rather than make every entry point document an
 * ordering requirement, the first one to arrive sets the defaults.
 */
static void ensure_voices(void)
{
    if ( voicesready == false ) {
	reset_voices();
    }
}

/** @brief Lock the voice table against the device callback, if one is running. */
static void lock_voices(void)
{
    if ( audiostream != NULL ) {
	SDL_LockAudioStream(audiostream);
    }
}

/** @brief Release the lock taken by lock_voices(). */
static void unlock_voices(void)
{
    if ( audiostream != NULL ) {
	SDL_UnlockAudioStream(audiostream);
    }
}

/** @brief Convert a duration in milliseconds to a whole number of frames. */
static uint32_t frames_for_ms(uint32_t ms)
{
    return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
}

/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
static float32_t noise_sample(void)
{
    noisestate ^= noisestate << 13;
    noisestate ^= noisestate >> 17;
    noisestate ^= noisestate << 5;
    // The top 24 bits are the well-mixed ones; scale them to -1..1.
    return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
}

/** @brief One sample of @p voice's oscillator at its current phase. */
static float32_t voice_oscillator(akgl_AudioVoice *voice)
{
    float32_t value = 0.0f;

    switch ( voice->waveform ) {
    case AKGL_AUDIO_WAVE_TRIANGLE:
	if ( voice->phase < 0.5f ) {
	    value = (4.0f * voice->phase) - 1.0f;
	} else {
	    value = 3.0f - (4.0f * voice->phase);
	}
	break;
    case AKGL_AUDIO_WAVE_SAWTOOTH:
	value = (2.0f * voice->phase) - 1.0f;
	break;
    case AKGL_AUDIO_WAVE_SQUARE:
	value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
	break;
    case AKGL_AUDIO_WAVE_NOISE:
	value = noise_sample();
	break;
    case AKGL_AUDIO_WAVE_SINE:
	value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
	break;
    }
    return value;
}

/**
 * @brief Envelope level @p frame frames into the gate, before the release.
 *
 * Split out because the release has to start from wherever the gate left off,
 * which for a gate shorter than attack plus decay is partway up or down a
 * ramp rather than at the sustain level.
 */
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
{
    uint32_t elapsed = frame;

    if ( elapsed < voice->attack_frames ) {
	return (float32_t)elapsed / (float32_t)voice->attack_frames;
    }
    elapsed -= voice->attack_frames;
    if ( elapsed < voice->decay_frames ) {
	return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
    }
    return voice->sustain;
}

/** @brief Envelope level for @p voice where it currently stands. */
static float32_t voice_envelope(akgl_AudioVoice *voice)
{
    uint32_t released = 0;
    float32_t gatelevel = 0.0f;

    if ( voice->elapsed_frames < voice->duration_frames ) {
	return voice_gate_level(voice, voice->elapsed_frames);
    }
    released = voice->elapsed_frames - voice->duration_frames;
    if ( released >= voice->release_frames ) {
	return 0.0f;
    }
    gatelevel = voice_gate_level(voice, voice->duration_frames);
    return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
}

/**
 * @brief Pitch @p voice sounds at where its sweep currently stands.
 *
 * Recomputed from the frame counter each time rather than added to as it goes,
 * so a sweep lands on exactly the frequencies its step size describes however
 * the caller happens to have chopped up its calls to akgl_audio_mix().
 *
 * A voice that is not sweeping keeps the frequency it was given.
 */
static float32_t voice_sweep_hz(akgl_AudioVoice *voice)
{
    uint32_t ticks = 0;
    float32_t moved = 0.0f;
    float32_t hz = 0.0f;

    if ( voice->sweep_step_hz <= 0.0f ) {
	return voice->hz;
    }
    ticks = voice->elapsed_frames / AKGL_AUDIO_SWEEP_TICK_FRAMES;
    moved = voice->sweep_step_hz * (float32_t)ticks;
    if ( voice->sweep_to_hz < voice->sweep_from_hz ) {
	hz = voice->sweep_from_hz - moved;
	if ( hz < voice->sweep_to_hz ) {
	    hz = voice->sweep_to_hz;
	}
    } else {
	hz = voice->sweep_from_hz + moved;
	if ( hz > voice->sweep_to_hz ) {
	    hz = voice->sweep_to_hz;
	}
    }
    return hz;
}

/**
 * @brief Start a note on @p voice, sweeping or held.
 *
 * The whole of what akgl_audio_tone() and akgl_audio_sweep() do once their
 * arguments have been checked. A held note is a sweep with a step of 0, which
 * is also what makes a voice reused for a plain tone stop sweeping.
 */
static void start_note(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
    ensure_voices();
    lock_voices();
    akgl_audio_voices[voice].hz = from_hz;
    akgl_audio_voices[voice].phase = 0.0f;
    akgl_audio_voices[voice].sweep_from_hz = from_hz;
    akgl_audio_voices[voice].sweep_to_hz = to_hz;
    akgl_audio_voices[voice].sweep_step_hz = step_hz;
    akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
    akgl_audio_voices[voice].elapsed_frames = 0;
    akgl_audio_voices[voice].active = true;
    unlock_voices();
}

/** @brief Refuse a voice index that is not in the table. */
static akerr_ErrorContext *check_voice(int voice)
{
    PREPARE_ERROR(errctx);
    FAIL_NONZERO_RETURN(
	errctx,
	((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
	AKERR_OUTOFBOUNDS,
	"Voice %d is not in the range 0 to %d",
	voice,
	AKGL_AUDIO_MAX_VOICES - 1);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Fill SDL's request from the voice table.
 *
 * SDL holds the stream lock for the duration of this callback, which is the
 * same lock lock_voices() takes, so the voice table cannot change underneath a
 * mix in progress.
 */
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
    akerr_ErrorContext *errctx = NULL;
    int frames = 0;

    while ( additional_amount > 0 ) {
	frames = additional_amount / (int)sizeof(float32_t);
	if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
	    frames = AKGL_AUDIO_MIX_FRAMES;
	}
	if ( frames <= 0 ) {
	    return;
	}
	errctx = akgl_audio_mix(mixbuffer, frames);
	if ( errctx != NULL ) {
	    // There is nobody to return an error to on the audio thread, and
	    // refusing to write leaves SDL underrunning. Report and go quiet.
	    //
	    // Raising an error here at all is only safe from libakerror 2.0.0
	    // on. Before that the pool was unlocked, so this callback and the
	    // main thread could scan AKERR_ARRAY_ERROR at the same time and be
	    // handed the same slot -- two threads writing one context, and one
	    // of them releasing it out from under the other. 2.0.0 makes
	    // finding a free slot and claiming it one operation under a lock.
	    // libakgl requires it; see the guard in include/akgl/error.h.
	    LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
	    errctx->handled = true;
	    errctx = akerr_release_error(errctx);
	    return;
	}
	SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
	additional_amount -= frames * (int)sizeof(float32_t);
    }
}

akerr_ErrorContext *akgl_audio_init(void)
{
    SDL_AudioSpec spec;

    PREPARE_ERROR(errctx);
    if ( audiostream != NULL ) {
	SUCCEED_RETURN(errctx);
    }

    ensure_voices();

    spec.format = SDL_AUDIO_F32;
    spec.channels = 1;
    spec.freq = AKGL_AUDIO_SAMPLE_RATE;

    audiostream = SDL_OpenAudioDeviceStream(
	SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
	&spec,
	&audio_stream_callback,
	NULL);
    FAIL_ZERO_RETURN(
	errctx,
	audiostream,
	AKGL_ERR_SDL,
	"Couldn't open an audio device: %s",
	SDL_GetError());

    // Devices open paused so a caller can set a stream up before it is heard.
    // Nothing here needs that, and a caller who expected akgl_audio_tone() to
    // make a sound would otherwise get silence with no error to explain it.
    FAIL_ZERO_RETURN(
	errctx,
	SDL_ResumeAudioStreamDevice(audiostream),
	AKGL_ERR_SDL,
	"Couldn't start the audio device: %s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_shutdown(void)
{
    SDL_AudioStream *closing = audiostream;

    PREPARE_ERROR(errctx);
    // Clear the pointer before destroying the stream: lock_voices() checks it,
    // and SDL_DestroyAudioStream can run the callback one last time.
    audiostream = NULL;
    if ( closing != NULL ) {
	SDL_DestroyAudioStream(closing);
    }
    reset_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));
    FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
    FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");

    // A step of 0 is what says "one pitch, held", so this also clears a sweep
    // left on the voice by an earlier akgl_audio_sweep().
    start_note(voice, hz, hz, 0.0f, ms);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));
    FAIL_NONZERO_RETURN(errctx, (from_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Start frequency %f is not positive", from_hz);
    FAIL_NONZERO_RETURN(errctx, (to_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Target frequency %f is not positive", to_hz);
    FAIL_NONZERO_RETURN(
	errctx,
	(step_hz <= 0.0f),
	AKERR_OUTOFBOUNDS,
	"Step %f is not positive; the direction of a sweep comes from the two frequencies",
	step_hz);
    FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");

    start_note(voice, from_hz, to_hz, step_hz, ms);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_stop(int voice)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));

    ensure_voices();
    lock_voices();
    akgl_audio_voices[voice].active = false;
    akgl_audio_voices[voice].elapsed_frames = 0;
    akgl_audio_voices[voice].phase = 0.0f;
    unlock_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));
    FAIL_NONZERO_RETURN(
	errctx,
	((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
	AKERR_OUTOFBOUNDS,
	"Waveform %d is not one of the %d shapes",
	(int)waveform,
	(int)AKGL_AUDIO_WAVE_SINE + 1);

    ensure_voices();
    lock_voices();
    akgl_audio_voices[voice].waveform = waveform;
    unlock_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));
    FAIL_NONZERO_RETURN(
	errctx,
	((sustain < 0.0f) || (sustain > 1.0f)),
	AKERR_OUTOFBOUNDS,
	"Sustain level %f is not between 0.0 and 1.0",
	sustain);

    ensure_voices();
    lock_voices();
    akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
    akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
    akgl_audio_voices[voice].release_frames = frames_for_ms(release);
    akgl_audio_voices[voice].sustain = sustain;
    unlock_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_volume(float32_t level)
{
    PREPARE_ERROR(errctx);
    FAIL_NONZERO_RETURN(
	errctx,
	((level < 0.0f) || (level > 1.0f)),
	AKERR_OUTOFBOUNDS,
	"Volume level %f is not between 0.0 and 1.0",
	level);

    ensure_voices();
    lock_voices();
    mastervolume = level;
    unlock_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_voice_active(int voice, bool *active)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, check_voice(voice));
    FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");

    ensure_voices();
    lock_voices();
    *active = akgl_audio_voices[voice].active;
    unlock_voices();
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_audio_mix(float32_t *dest, int frames)
{
    akgl_AudioVoice *voice = NULL;
    float32_t sum = 0.0f;
    bool sweeping = false;
    int i = 0;
    int v = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
    FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);

    for ( i = 0; i < frames; i++ ) {
	sum = 0.0f;
	for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
	    voice = &akgl_audio_voices[v];
	    if ( voice->active == false ) {
		continue;
	    }
	    if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
		// Gate and release are both spent. The voice goes quiet on its
		// own so a caller does not have to keep a clock to stop it.
		voice->active = false;
		voice->elapsed_frames = 0;
		voice->phase = 0.0f;
		continue;
	    }
	    sweeping = (voice->sweep_step_hz > 0.0f);
	    if ( sweeping ) {
		voice->hz = voice_sweep_hz(voice);
	    } else {
		// Derived from the frame counter rather than accumulated one
		// increment at a time. A float increment of hz/rate is not exact,
		// and adding it 44100 times a second walks the pitch off over the
		// length of a held note.
		voice->phase = (float32_t)SDL_fmod(
		    ((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
		    1.0);
	    }
	    sum += voice_oscillator(voice) * voice_envelope(voice);
	    if ( sweeping ) {
		// The derived form above assumes one frequency for the whole note.
		// Under a sweep it would jump the waveform at every step -- an
		// audible click -- so a swept voice accumulates instead and takes
		// the drift the derived form exists to avoid.
		voice->phase = (float32_t)SDL_fmod(
		    (double)voice->phase + ((double)voice->hz / (double)AKGL_AUDIO_SAMPLE_RATE),
		    1.0);
	    }
	    voice->elapsed_frames += 1;
	}
	sum = sum * mastervolume;
	// Three voices at full level can sum past full scale. Clamping rather
	// than scaling by the voice count keeps a single voice at the level it
	// was asked for instead of a third of it.
	if ( sum > 1.0f ) {
	    sum = 1.0f;
	} else if ( sum < -1.0f ) {
	    sum = -1.0f;
	}
	dest[i] = sum;
    }
    SUCCEED_RETURN(errctx);
}
