/**
 * @file audio_akgl.c
 * @brief Wires the audio backend record to libakgl's tone generator.
 *
 * Another thin adaptor. The one thing in here worth reading is the waveform
 * mapping: akbasic_Waveform and akgl_AudioWaveform currently agree on every
 * value, and the mapping is still written out rather than cast across, because
 * two enumerations agreeing by accident is not a contract and a future SOUND
 * waveform number would break the coincidence silently.
 */

#include <string.h>

#include <akerror.h>

#include <akgl/audio.h>
#include <akgl/error.h>

#include <akbasic/akgl.h>
#include <akbasic/error.h>

/**
 * @brief Map a BASIC waveform onto libakgl's.
 *
 * @param waveform The BASIC-side selection.
 * @param dest Output destination populated by the function.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When the waveform is not one of the four SOUND defines.
 */
static akerr_ErrorContext *to_akgl_waveform(akbasic_Waveform waveform, akgl_AudioWaveform *dest)
{
    PREPARE_ERROR(errctx);

    switch ( waveform ) {
    case AKBASIC_WAVE_TRIANGLE:
	*dest = AKGL_AUDIO_WAVE_TRIANGLE;
	break;
    case AKBASIC_WAVE_SAWTOOTH:
	*dest = AKGL_AUDIO_WAVE_SAWTOOTH;
	break;
    case AKBASIC_WAVE_SQUARE:
	*dest = AKGL_AUDIO_WAVE_SQUARE;
	break;
    case AKBASIC_WAVE_NOISE:
	*dest = AKGL_AUDIO_WAVE_NOISE;
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "No such waveform %d", (int)waveform);
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
{
    PREPARE_ERROR(errctx);

    (void)self;
    FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
    PASS(errctx, akgl_audio_tone(voice, (float32_t)hz, (uint32_t)ms));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A note whose pitch moves, for SOUND's dir/min/step arguments.
 *
 * New in libakgl 0.3.0, which is why the record's entry may be NULL and why
 * SOUND checks before using it. The sweep is advanced on the mixer's own frame
 * counter -- doing it from akbasic_runtime_step() instead would tie audible
 * pitch to how often a host happens to call us, which is the reason this was
 * filed upstream rather than faked here.
 */
static akerr_ErrorContext *snd_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_hz, int ms)
{
    PREPARE_ERROR(errctx);

    (void)self;
    FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
    PASS(errctx, akgl_audio_sweep(voice, (float32_t)from_hz, (float32_t)to_hz,
				  (float32_t)step_hz, (uint32_t)ms));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_stop(akbasic_AudioBackend *self, int voice)
{
    PREPARE_ERROR(errctx);

    (void)self;
    PASS(errctx, akgl_audio_stop(voice));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
{
    PREPARE_ERROR(errctx);
    akgl_AudioWaveform mapped = AKGL_AUDIO_WAVE_SQUARE;

    (void)self;
    PASS(errctx, to_akgl_waveform(waveform, &mapped));
    PASS(errctx, akgl_audio_waveform(voice, mapped));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
{
    PREPARE_ERROR(errctx);

    (void)self;
    FAIL_ZERO_RETURN(errctx, (attack >= 0 && decay >= 0 && release >= 0), AKERR_VALUE,
		     "Negative envelope stage: %d/%d/%d", attack, decay, release);
    PASS(errctx, akgl_audio_envelope(voice, (uint32_t)attack, (uint32_t)decay,
				     (float32_t)sustain, (uint32_t)release));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_volume(akbasic_AudioBackend *self, double level)
{
    PREPARE_ERROR(errctx);

    (void)self;
    PASS(errctx, akgl_audio_volume((float32_t)level));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *snd_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
{
    PREPARE_ERROR(errctx);

    (void)self;
    PASS(errctx, akgl_audio_voice_active(voice, active));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_audio_init_akgl(akbasic_AudioBackend *obj)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
		     "NULL backend in audio_init_akgl");
    PASS(errctx, akgl_error_init());

    /*
     * Opens an SDL audio device. A host that owns its own audio pipeline can
     * skip this entirely and pull samples with akgl_audio_mix() instead -- the
     * voice table works either way, which is the design upstream chose and the
     * reason its own suite can be deterministic.
     */
    PASS(errctx, akgl_audio_init());

    obj->self		= NULL;	/* the voice table is libakgl's, not ours */
    obj->tone		= snd_tone;
    obj->sweep		= snd_sweep;
    obj->stop		= snd_stop;
    obj->waveform	= snd_waveform;
    obj->envelope	= snd_envelope;
    obj->volume		= snd_volume;
    obj->voice_active	= snd_voice_active;
    SUCCEED_RETURN(errctx);
}
