/**
 * @file play.c
 * @brief The PLAY note-string parser and the queue that paces it.
 *
 * libakgl's audio commit says where this belongs: *"TEMPO and the PLAY
 * note-string parser ... belong in the interpreter rather than here."* A tone
 * generator synthesises pitches; deciding that `O4CDEFG` means five quarter notes
 * starting at middle C is a language question.
 *
 * On a C128, PLAY holds the program until the string finishes. Section 1.6
 * forbids that, so PLAY parses the whole string into a fixed queue and returns
 * immediately; akbasic_play_service() releases one note at a time against the
 * host's clock and is called from akbasic_runtime_step(). A host keeps its frame
 * rate and the notes still come out in order at their written lengths.
 */

#include <ctype.h>

#include <akerror.h>
#include <akstdlib.h>

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

#include "verbs.h"

/**
 * @brief Semitone offset of each note letter within an octave that starts at C.
 *
 * Indexed by letter minus 'A', so it reads out of order. That is the price of
 * indexing directly instead of searching, and the alternative -- a search over a
 * seven-entry table -- would not be clearer.
 */
static const int NOTE_SEMITONE[7] = {
     9,				/* A */
    11,				/* B */
     0,				/* C */
     2,				/* D */
     4,				/* E */
     5,				/* F */
     7				/* G */
};

/**
 * @brief What each duration letter divides a whole note by.
 *
 * BASIC 7.0 spells them Whole, Half, Quarter, eIghth and Sixteenth -- the eighth
 * is I rather than E because E is already a note.
 */
static const struct
{
    char letter;
    int divisor;
} DURATIONS[] = {
    { 'W',  1 },		/* whole	*/
    { 'H',  2 },		/* half		*/
    { 'Q',  4 },		/* quarter	*/
    { 'I',  8 },		/* eighth	*/
    { 'S', 16 }			/* sixteenth	*/
};

#define DURATION_COUNT ((int)(sizeof(DURATIONS) / sizeof(DURATIONS[0])))

/** @brief Append one note to the queue, or report that it is full. */
static akerr_ErrorContext *enqueue(akbasic_AudioState *audio, int voice, double hz, int ms, bool rest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (audio->count < AKBASIC_MAX_PLAY_NOTES), AKBASIC_ERR_BOUNDS,
		     "PLAY queue is full at %d notes", AKBASIC_MAX_PLAY_NOTES);
    audio->queue[audio->count].voice = voice;
    audio->queue[audio->count].hz = hz;
    audio->queue[audio->count].ms = ms;
    audio->queue[audio->count].rest = rest;
    audio->count += 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_play_parse(akbasic_Runtime *obj, const char *notes)
{
    PREPARE_ERROR(errctx);
    akbasic_AudioState *audio = NULL;
    size_t i = 0;
    size_t len = 0;
    int accidental = 0;
    int semitone = 0;
    int wholems = 0;
    int ms = 0;
    int value = 0;
    int d = 0;
    double hz = 0.0;
    char c = '\0';
    bool dotted = false;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && notes != NULL), AKERR_NULLPOINTER,
		     "NULL argument in play_parse");
    audio = &obj->audio_state;
    PASS(errctx, aksl_strlen(notes, &len));

    /*
     * A loop, so no CATCH and no _BREAK macros in here -- they expand to a C
     * break and would leave the loop with an error still pending. PASS and the
     * _RETURN forms only.
     */
    for ( i = 0; i < len; i++ ) {
	c = (char)toupper((unsigned char)notes[i]);

	/* Accidentals and dots are prefixes; they modify the next note letter. */
	if ( c == '#' ) {
	    accidental = 1;
	    continue;
	}
	if ( c == '$' ) {
	    accidental = -1;
	    continue;
	}
	if ( c == '.' ) {
	    dotted = true;
	    continue;
	}
	if ( c == ' ' ) {
	    continue;
	}

	/* A duration letter changes the length of every note after it. */
	for ( d = 0; d < DURATION_COUNT; d++ ) {
	    if ( DURATIONS[d].letter == c ) {
		PASS(errctx, akbasic_audio_whole_note_ms(audio->tempo, &wholems));
		audio->notems = wholems / DURATIONS[d].divisor;
		break;
	    }
	}
	if ( d < DURATION_COUNT ) {
	    continue;
	}

	/* The single-argument settings all take one decimal digit. */
	if ( c == 'V' || c == 'O' || c == 'T' || c == 'U' || c == 'X' ) {
	    FAIL_ZERO_RETURN(errctx,
			     (i + 1 < len && isdigit((unsigned char)notes[i + 1])),
			     AKBASIC_ERR_SYNTAX,
			     "PLAY %c expected a digit", c);
	    value = notes[i + 1] - '0';
	    i += 1;
	    switch ( c ) {
	    case 'V':
		FAIL_ZERO_RETURN(errctx, (value >= 1 && value <= AKBASIC_AUDIO_VOICES),
				 AKBASIC_ERR_BOUNDS,
				 "PLAY V%d out of range (1 to %d)", value, AKBASIC_AUDIO_VOICES);
		audio->voice = value - 1;
		break;
	    case 'O':
		FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 6), AKBASIC_ERR_BOUNDS,
				 "PLAY O%d out of range (0 to 6)", value);
		audio->octave = value;
		break;
	    case 'T':
		FAIL_ZERO_RETURN(errctx, (value >= 0 && value < AKBASIC_ENVELOPES),
				 AKBASIC_ERR_BOUNDS,
				 "PLAY T%d out of range (0 to %d)", value, AKBASIC_ENVELOPES - 1);
		audio->envelope = value;
		break;
	    case 'U':
		FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 9), AKBASIC_ERR_BOUNDS,
				 "PLAY U%d out of range (0 to 9)", value);
		audio->level = (double)value / 9.0;
		break;
	    default:
		/*
		 * X selects the filter, which there is nothing behind -- see
		 * TODO.md section 7. Refused rather than ignored, for the same
		 * reason FILTER is: a program that asked for a filtered sound
		 * and got an unfiltered one has been lied to.
		 */
		FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
			    "PLAY X selects the filter, which this device does not have");
	    }
	    continue;
	}

	ms = audio->notems;
	if ( dotted ) {
	    /* A dot adds half the note's length again. */
	    ms = ms + (ms / 2);
	    dotted = false;
	}

	if ( c == 'R' ) {
	    PASS(errctx, enqueue(audio, audio->voice, 0.0, ms, true));
	    accidental = 0;
	    continue;
	}
	if ( c == 'M' ) {
	    /*
	     * A measure bar. On a C128 it synchronises the voices; with one
	     * sequential queue there is nothing to synchronise, so it is a
	     * no-op rather than an error -- a listing full of them should still
	     * play. TODO.md section 5.
	     */
	    continue;
	}

	FAIL_ZERO_RETURN(errctx, (c >= 'A' && c <= 'G'), AKBASIC_ERR_SYNTAX,
			 "PLAY does not understand '%c'", notes[i]);

	semitone = NOTE_SEMITONE[c - 'A'] + accidental;
	accidental = 0;
	/*
	 * B# wraps to the next octave and C$ to the previous one. Wrapping the
	 * semitone without moving the octave would put B# a full octave below
	 * where it belongs.
	 */
	if ( semitone > 11 ) {
	    semitone -= 12;
	    PASS(errctx, akbasic_audio_note_hz(semitone,
					       (audio->octave < 6) ? audio->octave + 1 : 6,
					       &hz));
	} else if ( semitone < 0 ) {
	    semitone += 12;
	    PASS(errctx, akbasic_audio_note_hz(semitone,
					       (audio->octave > 0) ? audio->octave - 1 : 0,
					       &hz));
	} else {
	    PASS(errctx, akbasic_audio_note_hz(semitone, audio->octave, &hz));
	}
	PASS(errctx, enqueue(audio, audio->voice, hz, ms, false));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_play_service(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AudioState *audio = NULL;
    akbasic_PlayNote *note = NULL;
    akbasic_Envelope *envelope = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in play_service");
    audio = &obj->audio_state;

    if ( audio->head >= audio->count ) {
	/* Nothing queued. Reset so the next PLAY starts from the front. */
	audio->head = 0;
	audio->count = 0;
	audio->sounding = false;
	SUCCEED_RETURN(errctx);
    }
    if ( obj->timems < audio->nextms ) {
	/* The note holding the queue has not run out yet. */
	SUCCEED_RETURN(errctx);
    }
    /*
     * No audio backend means the queue drains silently rather than jamming. A
     * host that took the device away mid-tune should not leave the script stuck
     * behind a note that can never finish.
     */
    note = &audio->queue[audio->head];
    audio->head += 1;
    audio->nextms = obj->timems + note->ms;
    audio->sounding = true;

    if ( obj->audio == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    if ( note->rest ) {
	PASS(errctx, obj->audio->stop(obj->audio, note->voice));
	SUCCEED_RETURN(errctx);
    }

    envelope = &audio->envelopes[audio->envelope];
    PASS(errctx, obj->audio->waveform(obj->audio, note->voice, envelope->waveform));
    PASS(errctx, obj->audio->envelope(obj->audio, note->voice, envelope->attack,
				      envelope->decay, envelope->sustain, envelope->release));
    PASS(errctx, obj->audio->tone(obj->audio, note->voice, note->hz, note->ms));
    SUCCEED_RETURN(errctx);
}
