// Portable chip-tracker music sequencer. One authored song (baked to
// the JYM1 event stream by tools/songbake) plays on every port through
// the jlAudioVoice 3-voice tone layer plus the jlAudioNoise percussion
// generator: YM2149 on the ST, Paula on the Amiga, OPL2 on DOS, DOC
// oscillators on the IIgs. Per tick the work is a handful of event
// bytes and at most four register-level calls, so sustained playback
// costs register-write time on every port -- no CPU mixing anywhere.
//
// Time base is jlMillisElapsed(), not the frame counter, so one song
// keeps its tempo across 50/60/70 Hz displays. jlAudioFrameTick pumps
// the sequencer once per frame; event timestamps are quantized to the
// frame the deadline falls in (a tick shorter than the frame interval
// still plays every note -- multiple rows fire in one pump). Fine
// tick resolution also stands in for tracker effects: arpeggios and
// slides are authored as fast rows, keeping the runtime this small.
//
// JYM1 stream layout (all multi-byte fields little-endian):
//   0..3 : magic 'J' 'Y' 'M' '1'
//   4..5 : tickMs -- milliseconds per wait tick (1..1000)
//   6..7 : loop target as a byte offset into the event area, or
//          0xFFFF when the song has no authored loop point
//   8..  : events --
//          0x00                       end of song
//          0x10+v freqLo freqHi att   note-on voice v (0..2), or for
//                                     v = 3 the noise voice: freqLo =
//                                     pitch 0..31, freqHi = 0
//          0x20+v                     voice v off (0..3)
//          0x40 msLo msHi             tempo change (new tickMs)
//          0x80|n                     wait n ticks (n = 1..127)

#include <stddef.h>

#include "joey/audio.h"
#include "joey/core.h"
#include "musicInternal.h"

#define MUSIC_HEADER_BYTES  8u
#define MUSIC_TICK_MS_MAX   1000u
#define MUSIC_LOOP_NONE     0xFFFFu
#define MUSIC_ATTEN_OFF     15u

// Logical voice slots: three tones plus the noise voice.
#define MUSIC_VOICE_SLOTS   (JOEY_AUDIO_VOICES + 1)
#define MUSIC_NOISE_SLOT    JOEY_AUDIO_VOICES

#define MUSIC_OP_END        0x00u
#define MUSIC_OP_NOTE       0x10u    // + voice; followed by freqLo freqHi atten
#define MUSIC_OP_NOTE_BYTES 4u
#define MUSIC_OP_OFF        0x20u    // + voice
#define MUSIC_OP_TEMPO      0x40u    // followed by tickMs lo, hi
#define MUSIC_OP_TEMPO_BYTES 3u
#define MUSIC_OP_WAIT       0x80u    // | tick count 1..127
#define MUSIC_WAIT_MASK     0x7Fu

// A pump that has fallen further behind than this (level load, disk
// seek) resynchronizes to now instead of burst-playing the gap.
#define MUSIC_RESYNC_MS     250u

// Backstop against a malformed stream whose loop contains no wait
// event: cap the events one pump will process, resume next frame.
#define MUSIC_MAX_EVENTS_PER_PUMP 100u

static void musicApplyVoice(uint8_t voice);
static void musicHalt(void);

static const uint8_t *gMusicEvents  = NULL;   // event area (header stripped)
static uint32_t       gMusicLength  = 0;      // event area length in bytes
static uint32_t       gMusicPos     = 0;
static uint32_t       gMusicNextMs  = 0;      // deadline for the next event batch
static uint16_t       gMusicTickMs  = 0;
static uint16_t       gMusicLoop    = (uint16_t)MUSIC_LOOP_NONE;
static uint8_t        gMusicBias    = 0;      // jlMusicSetAtten fade bias
static bool           gMusicLooping = false;
static bool           gMusicPlaying = false;
static bool           gMusicPaused  = false;

// Sticky per-voice state, re-applied through the HAL on change (and
// on every pump on the IIgs -- see musicPump). Slot 3 is the noise
// voice: gMusicFreq holds its 0..31 pitch and gMusicOn distinguishes
// "silent" from "pitch 0", which is a valid bright hiss.
static bool           gMusicOn[MUSIC_VOICE_SLOTS];
static uint16_t       gMusicFreq[MUSIC_VOICE_SLOTS];
static uint8_t        gMusicAtten[MUSIC_VOICE_SLOTS];


bool jlMusicIsPlaying(void) {
    return gMusicPlaying;
}


void jlMusicPause(void) {
    uint8_t voice;

    if (!gMusicPlaying || gMusicPaused) {
        return;
    }
    gMusicPaused = true;
    for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
        if (voice == MUSIC_NOISE_SLOT) {
            jlAudioNoise(0u, (uint8_t)MUSIC_ATTEN_OFF);
        } else {
            jlAudioVoice(voice, 0u, (uint8_t)MUSIC_ATTEN_OFF);
        }
    }
}


bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop) {
    uint16_t tickMs;
    uint16_t loopOfs;

    if (gMusicPlaying) {
        musicHalt();
    }
    if (data == NULL || length <= MUSIC_HEADER_BYTES) {
        return false;
    }
    if (data[0] != 'J' || data[1] != 'Y' || data[2] != 'M' || data[3] != '1') {
        return false;
    }
    tickMs  = (uint16_t)(data[4] | ((uint16_t)data[5] << 8));
    loopOfs = (uint16_t)(data[6] | ((uint16_t)data[7] << 8));
    if (tickMs == 0u || tickMs > MUSIC_TICK_MS_MAX) {
        return false;
    }
    if (loopOfs != (uint16_t)MUSIC_LOOP_NONE && (uint32_t)loopOfs >= length - MUSIC_HEADER_BYTES) {
        return false;
    }

    // The stream is read in place; the caller's buffer must stay
    // valid until jlMusicStop (or song end with loop=false). The
    // fade bias (jlMusicSetAtten) deliberately carries over.
    gMusicEvents  = data + MUSIC_HEADER_BYTES;
    gMusicLength  = length - MUSIC_HEADER_BYTES;
    gMusicTickMs  = tickMs;
    gMusicLoop    = loopOfs;
    gMusicLooping = loop;
    gMusicPos     = 0;
    gMusicNextMs  = jlMillisElapsed();
    gMusicPaused  = false;
    gMusicPlaying = true;
    return true;
}


void jlMusicResume(void) {
    uint8_t voice;

    if (!gMusicPlaying || !gMusicPaused) {
        return;
    }
    gMusicPaused = false;
    gMusicNextMs = jlMillisElapsed();
    for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
        if (gMusicOn[voice]) {
            musicApplyVoice(voice);
        }
    }
}


void jlMusicSetAtten(uint8_t atten) {
    uint8_t voice;

    if (atten > MUSIC_ATTEN_OFF) {
        atten = (uint8_t)MUSIC_ATTEN_OFF;
    }
    gMusicBias = atten;
    if (!gMusicPlaying || gMusicPaused) {
        return;
    }
    for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
        if (gMusicOn[voice]) {
            musicApplyVoice(voice);
        }
    }
}


void jlMusicStop(void) {
    if (!gMusicPlaying) {
        return;
    }
    musicHalt();
}


static void musicApplyVoice(uint8_t voice) {
    uint16_t atten;

    atten = (uint16_t)gMusicAtten[voice] + gMusicBias;
    if (atten > MUSIC_ATTEN_OFF) {
        atten = MUSIC_ATTEN_OFF;
    }
    if (voice == MUSIC_NOISE_SLOT) {
        if (!gMusicOn[voice]) {
            jlAudioNoise(0u, (uint8_t)MUSIC_ATTEN_OFF);
        } else {
            jlAudioNoise((uint8_t)gMusicFreq[voice], (uint8_t)atten);
        }
    } else {
        if (!gMusicOn[voice]) {
            jlAudioVoice(voice, 0u, (uint8_t)MUSIC_ATTEN_OFF);
        } else {
            jlAudioVoice(voice, gMusicFreq[voice], (uint8_t)atten);
        }
    }
}


static void musicHalt(void) {
    uint8_t voice;

    for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
        gMusicOn[voice]    = false;
        gMusicFreq[voice]  = 0;
        gMusicAtten[voice] = (uint8_t)MUSIC_ATTEN_OFF;
        musicApplyVoice(voice);
    }
    gMusicPlaying = false;
    gMusicPaused  = false;
    gMusicEvents  = NULL;
    gMusicLength  = 0;
    gMusicPos     = 0;
}


void musicPump(void) {
    uint32_t now;
    uint16_t eventsLeft;
    uint16_t tempo;
    uint8_t  op;
    uint8_t  voice;

    if (!gMusicPlaying || gMusicPaused) {
        return;
    }
    now = jlMillisElapsed();
    if ((int32_t)(now - gMusicNextMs) < 0) {
#if defined(JOEYLIB_PLATFORM_IIGS)
        // The IIgs backends are ~80 ms one-shots retriggered per call
        // (DOC oscillators have no gate to hold); re-assert the active
        // voices every frame so held notes sustain. The other ports'
        // tone/noise hardware holds a programmed voice on its own.
        for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
            if (gMusicOn[voice]) {
                musicApplyVoice(voice);
            }
        }
#endif
        return;
    }
    if (now - gMusicNextMs > MUSIC_RESYNC_MS) {
        gMusicNextMs = now;
    }

    eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP;
    while (gMusicPlaying && eventsLeft > 0u && (int32_t)(now - gMusicNextMs) >= 0) {
        eventsLeft--;
        if (gMusicPos >= gMusicLength) {
            musicHalt();
            return;
        }
        op = gMusicEvents[gMusicPos];
        if ((op & MUSIC_OP_WAIT) != 0u) {
            gMusicPos++;
            gMusicNextMs += (uint32_t)(op & MUSIC_WAIT_MASK) * gMusicTickMs;
        } else if (op >= MUSIC_OP_NOTE && op < MUSIC_OP_NOTE + MUSIC_VOICE_SLOTS) {
            if (gMusicPos + MUSIC_OP_NOTE_BYTES > gMusicLength) {
                musicHalt();
                return;
            }
            voice               = (uint8_t)(op - MUSIC_OP_NOTE);
            gMusicFreq[voice]   = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8));
            gMusicAtten[voice]  = gMusicEvents[gMusicPos + 3];
            gMusicOn[voice]     = (voice == MUSIC_NOISE_SLOT) || gMusicFreq[voice] != 0u;
            gMusicPos          += MUSIC_OP_NOTE_BYTES;
            musicApplyVoice(voice);
        } else if (op >= MUSIC_OP_OFF && op < MUSIC_OP_OFF + MUSIC_VOICE_SLOTS) {
            voice           = (uint8_t)(op - MUSIC_OP_OFF);
            gMusicOn[voice] = false;
            gMusicPos++;
            musicApplyVoice(voice);
        } else if (op == MUSIC_OP_TEMPO) {
            if (gMusicPos + MUSIC_OP_TEMPO_BYTES > gMusicLength) {
                musicHalt();
                return;
            }
            tempo = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8));
            if (tempo == 0u || tempo > MUSIC_TICK_MS_MAX) {
                musicHalt();
                return;
            }
            gMusicTickMs  = tempo;
            gMusicPos    += MUSIC_OP_TEMPO_BYTES;
        } else if (op == MUSIC_OP_END) {
            if (gMusicLooping) {
                gMusicPos = (gMusicLoop == (uint16_t)MUSIC_LOOP_NONE) ? 0u : (uint32_t)gMusicLoop;
            } else {
                musicHalt();
                return;
            }
        } else {
            musicHalt();                // malformed opcode: hard stop
            return;
        }
    }
}
