/**
 * @file runtime_audio.c
 * @brief The group I verb implementations: SOUND, ENVELOPE, VOL, PLAY, TEMPO
 *        and the refusal that is FILTER.
 *
 * Same rule as src/runtime_graphics.c -- no SDL and no libakgl header here.
 * Everything goes through the akbasic_AudioBackend record.
 *
 * The reference implements none of these, so the semantics are Commodore BASIC
 * 7.0's. Two of its behaviours are deliberately not reproduced and both are
 * recorded in TODO.md section 5: PLAY does not block, and SOUND's frequency-sweep
 * arguments are refused rather than faked.
 */

#include <string.h>

#include <akerror.h>

#include <akbasic/audio.h>
#include <akbasic/args.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>

#include "verbs.h"

/* Most verbs answer "did something happen"; this is that answer. */
#define SUCCEED_TRUE(__obj, __dest)			\
    do {						\
	*(__dest) = &(__obj)->staticTrueValue;		\
    } while ( 0 )

/** @brief Refuse politely when the host lent us no audio device. */
static akerr_ErrorContext *require_audio(akbasic_Runtime *obj, const char *verb)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
		     "NULL argument in require_audio");
    FAIL_ZERO_RETURN(errctx, (obj->audio != NULL), AKBASIC_ERR_DEVICE,
		     "%s needs an audio device and this runtime has none", verb);
    SUCCEED_RETURN(errctx);
}


/**
 * @brief Issue SOUND's swept note, translating its arguments out of SID space.
 *
 * BASIC 7.0 spells the sweep as `dir`, `min` and `step`, all in the SID's
 * 16-bit register units rather than in hertz, and `dir` picks the shape:
 *
 *     0  no sweep -- handled by the caller, which issues a held note instead
 *     1  up, from the SOUND frequency toward min
 *     2  down, from the SOUND frequency toward min
 *     3  oscillate between the two
 *
 * **Direction 3 sweeps once rather than oscillating**, and that is a deviation
 * worth knowing about: akgl_audio_sweep runs one pass from a start to an end,
 * and a genuine oscillation needs the mixer to turn around at the endpoints.
 * Sweeping once in the direction the endpoints imply is the closest honest
 * approximation; TODO.md section 5 records it.
 *
 * Directions 1 and 2 both name `min` as the far end, so the *value* of min is
 * what decides which way the pitch actually travels -- a `min` above the
 * starting frequency rises whatever `dir` says. That is the SID's behaviour and
 * akgl_audio_sweep works the same way, comparing its two endpoints, so the two
 * agree without this having to second-guess either.
 */
static akerr_ErrorContext AKERR_NOIGNORE *sweep_note(akbasic_Runtime *obj, int voice, double hz, double *args, int ms)
{
    PREPARE_ERROR(errctx);
    double tohz = 0.0;
    double stephz = 0.0;
    double stepbase = 0.0;

    PASS(errctx, akbasic_audio_register_to_hz((int)args[4], &tohz));

    /*
     * `step` is a register *delta*, not a register value, so it cannot go
     * through register_to_hz on its own -- that maps a position, and a delta has
     * no position. Convert it as the distance between register 0 and register
     * `step`, which is the same linear scale the table applies to everything
     * else.
     */
    PASS(errctx, akbasic_audio_register_to_hz(0, &stepbase));
    PASS(errctx, akbasic_audio_register_to_hz((int)args[5], &stephz));
    stephz -= stepbase;

    /*
     * A zero step would never arrive and libakgl refuses it outright. One hertz
     * is the smallest move that still gets there, and a program that asked for
     * no movement asked for a held note -- which is what it gets.
     */
    if ( stephz <= 0.0 ) {
	stephz = 1.0;
    }
    PASS(errctx, obj->audio->sweep(obj->audio, voice, hz, tohz, stephz, ms));
    SUCCEED_RETURN(errctx);
}

/* --------------------------------------------------------------- SOUND --- */

akerr_ErrorContext *akbasic_cmd_sound(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[8];
    int count = 0;
    int voice = 0;
    int ms = 0;
    double hz = 0.0;
    akbasic_Envelope *envelope = NULL;

    (void)lval; (void)rval;
    PASS(errctx, require_audio(obj, "SOUND"));
    PASS(errctx, akbasic_args_numbers(obj, expr, "SOUND", args, 8, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX,
		     "SOUND expected a voice, a frequency and a duration");

    voice = (int)args[0];
    FAIL_ZERO_RETURN(errctx, (voice >= 1 && voice <= AKBASIC_AUDIO_VOICES),
		     AKBASIC_ERR_BOUNDS, "SOUND voice %d out of range (1 to %d)",
		     voice, AKBASIC_AUDIO_VOICES);
    voice -= 1;			/* BASIC counts voices from 1; the backend from 0 */

    PASS(errctx, akbasic_audio_register_to_hz((int)args[1], &hz));

    /*
     * BASIC counts SOUND's duration in jiffies -- sixtieths of a second, the
     * PAL/NTSC vertical blank the KERNAL counts on. The backend takes
     * milliseconds.
     */
    FAIL_ZERO_RETURN(errctx, (args[2] >= 0.0), AKBASIC_ERR_VALUE,
		     "SOUND duration must not be negative");
    ms = (int)((args[2] * 1000.0) / 60.0);

    /*
     * Arguments 4 through 6 are a frequency sweep: a direction, a floor and a
     * step. `dir` is 0 to hold, 1 to sweep up, 2 to sweep down and 3 to
     * oscillate; `min` is the far end of the sweep as a register value, and
     * `step` is how far the pitch moves per tick.
     *
     * This was refused outright until libakgl 0.3.0 grew akgl_audio_sweep --
     * faking it by re-issuing tones from akbasic_runtime_step() would have tied
     * audible pitch to how often the host happens to call us, a tune that
     * changes key with the frame rate. It is still refused when the host's
     * backend has no sweep, which is what an older one looks like.
     */
    if ( count >= 4 && args[3] != 0.0 ) {
	FAIL_ZERO_RETURN(errctx, (args[3] >= 0.0 && args[3] <= 3.0), AKBASIC_ERR_BOUNDS,
			 "SOUND direction %d out of range (0 to 3)", (int)args[3]);
	FAIL_ZERO_RETURN(errctx, (obj->audio->sweep != NULL), AKBASIC_ERR_DEVICE,
			 "SOUND's frequency sweep needs an audio device that can sweep");
	FAIL_ZERO_RETURN(errctx, (count >= 6), AKBASIC_ERR_SYNTAX,
			 "SOUND's frequency sweep expected a direction, a minimum and a step");
	PASS(errctx, sweep_note(obj, voice, hz, args, ms));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    if ( count >= 7 ) {
	/* The seventh argument selects a waveform, 0 through 3. */
	FAIL_ZERO_RETURN(errctx, (args[6] >= 0.0 && args[6] <= 3.0), AKBASIC_ERR_BOUNDS,
			 "SOUND waveform %d out of range (0 to 3)", (int)args[6]);
	PASS(errctx, obj->audio->waveform(obj->audio, voice, (akbasic_Waveform)(int)args[6]));
    } else {
	envelope = &obj->audio_state.envelopes[obj->audio_state.envelope];
	PASS(errctx, obj->audio->waveform(obj->audio, voice, envelope->waveform));
    }

    PASS(errctx, obj->audio->tone(obj->audio, voice, hz, ms));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------ ENVELOPE --- */

akerr_ErrorContext *akbasic_cmd_envelope(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[7];
    int count = 0;
    int preset = 0;
    int ms = 0;
    akbasic_Envelope *envelope = NULL;

    (void)lval; (void)rval;
    /*
     * ENVELOPE defines a preset; it does not sound anything, so it needs no
     * device and a program can set its instruments up before a host lends it one.
     */
    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in ENVELOPE");
    PASS(errctx, akbasic_args_numbers(obj, expr, "ENVELOPE", args, 7, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
		     "ENVELOPE expected a preset number");

    preset = (int)args[0];
    FAIL_ZERO_RETURN(errctx, (preset >= 0 && preset < AKBASIC_ENVELOPES), AKBASIC_ERR_BOUNDS,
		     "ENVELOPE %d out of range (0 to %d)", preset, AKBASIC_ENVELOPES - 1);
    envelope = &obj->audio_state.envelopes[preset];

    /*
     * Attack, decay and release arrive as the SID's 0-15 rate numbers and become
     * milliseconds. Sustain is the odd one out: it is a *level*, not a time, so
     * 0-15 maps onto 0.0-1.0 rather than through the rate table.
     */
    if ( count >= 2 ) {
	PASS(errctx, akbasic_audio_rate_to_ms((int)args[1], true, &ms));
	envelope->attack = ms;
    }
    if ( count >= 3 ) {
	PASS(errctx, akbasic_audio_rate_to_ms((int)args[2], false, &ms));
	envelope->decay = ms;
    }
    if ( count >= 4 ) {
	FAIL_ZERO_RETURN(errctx, (args[3] >= 0.0 && args[3] <= 15.0), AKBASIC_ERR_BOUNDS,
			 "ENVELOPE sustain %d out of range (0 to 15)", (int)args[3]);
	envelope->sustain = args[3] / 15.0;
    }
    if ( count >= 5 ) {
	PASS(errctx, akbasic_audio_rate_to_ms((int)args[4], false, &ms));
	envelope->release = ms;
    }
    if ( count >= 6 ) {
	FAIL_ZERO_RETURN(errctx, (args[5] >= 0.0 && args[5] <= 3.0), AKBASIC_ERR_BOUNDS,
			 "ENVELOPE waveform %d out of range (0 to 3)", (int)args[5]);
	envelope->waveform = (akbasic_Waveform)(int)args[5];
    }
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------------- VOL --- */

akerr_ErrorContext *akbasic_cmd_vol(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[1];
    int count = 0;
    int level = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in VOL");
    PASS(errctx, akbasic_args_numbers(obj, expr, "VOL", args, 1, &count));
    FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "VOL expected a level");

    level = (int)args[0];
    FAIL_ZERO_RETURN(errctx, (level >= 0 && level <= 15), AKBASIC_ERR_BOUNDS,
		     "VOL %d out of range (0 to 15)", level);
    obj->audio_state.level = (double)level / 15.0;

    /*
     * Recorded whatever happens, but only pushed when there is somewhere to push
     * it. VOL before the host attaches a device should still take effect when
     * one arrives.
     */
    if ( obj->audio != NULL ) {
	PASS(errctx, obj->audio->volume(obj->audio, obj->audio_state.level));
    }
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* --------------------------------------------------------------- TEMPO --- */

akerr_ErrorContext *akbasic_cmd_tempo(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[1];
    int count = 0;
    int tempo = 0;
    int wholems = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in TEMPO");
    PASS(errctx, akbasic_args_numbers(obj, expr, "TEMPO", args, 1, &count));
    FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "TEMPO expected a value");

    tempo = (int)args[0];
    PASS(errctx, akbasic_audio_whole_note_ms(tempo, &wholems));
    obj->audio_state.tempo = tempo;

    /*
     * The current note length rescales with the tempo rather than resetting to a
     * quarter note: a program that set eighth notes and then changed tempo means
     * faster eighth notes, not quarter notes again.
     */
    PASS(errctx, akbasic_audio_whole_note_ms(AKBASIC_TEMPO_DEFAULT, &count));
    obj->audio_state.notems = (obj->audio_state.notems * wholems) / count;
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- PLAY --- */

akerr_ErrorContext *akbasic_cmd_play(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;
    akbasic_Value *value = NULL;

    (void)lval; (void)rval;
    PASS(errctx, require_audio(obj, "PLAY"));
    FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");

    arg = akbasic_leaf_first_argument(expr);
    if ( arg == NULL ) {
	arg = expr->right;
    }
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "PLAY expected a string");
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
		     "PLAY expected a string");

    /*
     * Parses and queues, then returns. A C128 would hold the program here until
     * the last note finished; section 1.6 forbids that, so the notes come out of
     * akbasic_runtime_step() against the host's clock instead. TODO.md section 5.
     */
    PASS(errctx, akbasic_play_parse(obj, value->stringval));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------- FILTER --- */

akerr_ErrorContext *akbasic_cmd_filter(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);

    (void)expr; (void)lval; (void)rval; (void)dest;
    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in FILTER");

    /*
     * FILTER sets the SID's cutoff, its three band switches and its resonance.
     * akgl_audio_* synthesises raw waveforms and mixes them; there is no filter
     * stage to configure, and SDL3 supplies no primitive to build one from.
     *
     * Refused rather than accepted-and-ignored. A program that asks for a
     * low-pass and gets an unfiltered square wave has been lied to, and the whole
     * reason it is in the table at all is so the diagnostic names the real
     * problem instead of "undefined verb". TODO.md section 7.
     */
    FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
		"FILTER needs a filter stage this device does not have");
}
