// Amiga audio HAL: PTPlayer (Frank Wille, public domain) drives Paula
// for both module playback and one-shot SFX overlay.
//
// We build PTPlayer in OSCOMPAT mode so it cooperates with audio.device
// rather than taking Paula away from the OS -- matches our cooperative-
// with-Intuition graphics HAL. mt_install allocates all four Paula
// channels via audio.device; mt_init loads a .MOD; mt_Enable=1 starts
// playback; mt_playfx triggers a SFX on a free channel (or the worst
// channel if all are busy) with priority arbitration.
//
// PTPlayer expects modules and SFX samples to live in Chip RAM. The
// asset pipeline does not yet partition into Chip vs. Fast, so we
// AllocMem(MEMF_CHIP) a copy at PlayMod / PlaySfx time and free it at
// StopMod / StopSfx (or shutdown).

#include <string.h>

#include <exec/types.h>
#include <exec/memory.h>
#include <hardware/custom.h>
#include <hardware/dmabits.h>

#include <proto/exec.h>

#include "ptplayer.h"
#include "port.h"
#include "joey/audio.h"

extern struct Custom custom;
extern UBYTE         mt_Enable;
extern UBYTE         mt_E8Trigger;

// ----- Constants -----

// Paula's NTSC PAL period for middle-C-ish playback. PTPlayer's SFX
// API takes a hardware period (cycles per sample) rather than a
// frequency. To convert from a sample's authored Hz: period = clock /
// rate, where clock is ~3546895 (PAL) or ~3579545 (NTSC). We default
// PAL since that's what our screen mode requests, and clamp to legal
// Paula period limits.
#define PAULA_CLOCK_PAL    3546895UL
#define PAULA_PERIOD_MIN   124         // Paula HW limit
#define PAULA_PERIOD_MAX   65535
#define PAULA_CHANNELS     4           // hardware audio channel count

#define SFX_VOLUME_MAX     64

// PSG-style tone voices: Paula loops a 2-byte full-swing square per
// channel (DMA auto-reloads the location/length latches at length
// exhaustion, so a started voice sustains with zero CPU -- the sticky
// contract falls straight out of the hardware). Pitch is the period
// register: toneHz = PAULA_CLOCK_PAL / (period * 2). Loudness is the
// channel's 0..64 volume. The square must live in chip RAM; it is
// AllocMem'd on first use because jlAudioVoice is callable without
// jlAudioInit (jlpAudioShutdown frees it independently of gInstalled).
#define VOICE_SQUARE_BYTES 2
#define VOICE_VOLUME_MAX   64
#define VOICE_ATTEN_OFF    15

// Noise voice: Paula has no noise generator, so channel 3 loops a
// chip-RAM sample of 1-bit LFSR noise (full-swing +127/-128 per LFSR
// bit -- the YM2149 timbre, so the portable pitch semantic carries
// over: spectral edge = sample rate). 1024 bytes puts the worst-case
// loop-repeat rate (~28 Hz at pitch 0) below tonal salience; 512
// would buzz at ~56 Hz. Generated once with a fixed seed so output
// is deterministic.
#define NOISE_BYTES        1024
#define NOISE_CHANNEL      3
#define NOISE_PITCH_MAX    31
#define NOISE_LFSR_SEED    0xACE1u
#define NOISE_LFSR_TAPS    0xB400u   // 16-bit maximal Galois LFSR

// PTPlayer has no built-in "play once" mode -- songs loop forever
// unless the author placed an `E8FF` effect at song end. mt_E8Trigger
// reflects the most-recently-seen E8 value; mt_init resets it to 0.
// When loop=false on PlayMod, we poll for this sentinel in
// jlpAudioFrameTick and clear mt_Enable when seen.
#define PTPLAYER_LOOP_END_MARKER 0xFF

// ----- Module state -----

typedef struct {
    UBYTE *samples;     // Chip-RAM copy of one SFX sample
    UWORD  lengthWords; // sample length in 16-bit words
} SfxSlotT;

static UBYTE     *gModuleChip   = NULL; // Chip-RAM copy of the playing module
static ULONG      gModuleLength = 0;    // bytes to FreeMem on swap / shutdown
static SfxSlotT   gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
static bool       gInstalled    = false;
static bool       gModPlaying   = false;
static bool       gWantLoopOnce = false;   // honor loop=false via E8 marker
static BYTE      *gVoiceChip    = NULL;    // chip-RAM square shared by all voices
static BYTE      *gNoiseChip    = NULL;    // chip-RAM LFSR noise for channel 3

// pitch 0..31 -> Paula period, geometric 124..1773 (~9%/step, about
// 1.5 semitones). Effective sample rate = 3546895/period: pitch 0 =
// ~28.6 kHz bright hiss, pitch 31 = ~2.0 kHz sample-and-hold rumble.
// Every entry is inside Paula's legal 124..65535 range by
// construction. Derivation: round(124 * (1773/124)^(i/31)).
static const UWORD kNoisePeriod[NOISE_PITCH_MAX + 1] = {
     124,  135,  147,  160,  175,  190,  208,  226,
     246,  268,  292,  319,  347,  378,  412,  449,
     489,  533,  581,  633,  690,  752,  819,  892,
     972, 1060, 1154, 1258, 1371, 1493, 1627, 1773
};

// ----- Internal helpers -----

// Keep PTPlayer's automatic SFX channel selection (sfx_cha = -1) off
// the noise channel. mt_end clears the per-channel music-only flags
// (its resetch does so on every channel), and mt_end runs in the
// PlayMod/StopMod/Shutdown paths -- so the mask must be re-applied
// after every mt_end, not set once at install.
static void maskNoiseChannel(void) {
    if (gInstalled) {
        mt_musicmask((void *)&custom, (UBYTE)(1u << NOISE_CHANNEL));
    }
}


static UWORD periodForRate(uint16_t rateHz) {
    unsigned long period;

    if (rateHz == 0) {
        return 428;  // ~middle C, sane default
    }
    period = PAULA_CLOCK_PAL / (unsigned long)rateHz;
    if (period < PAULA_PERIOD_MIN) {
        period = PAULA_PERIOD_MIN;
    }
    if (period > PAULA_PERIOD_MAX) {
        period = PAULA_PERIOD_MAX;
    }
    return (UWORD)period;
}


// ----- HAL API (alphabetical) -----

bool jlpAudioInit(void) {
    int i;

    if (gInstalled) {
        return true;
    }
    if (mt_install() == 0) {
        return false;
    }
    mt_Enable     = 0;
    gInstalled    = true;
    maskNoiseChannel();
    gModPlaying   = false;
    gWantLoopOnce = false;
    gModuleChip   = NULL;
    gModuleLength = 0;
    for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
        gSfxSlots[i].samples     = NULL;
        gSfxSlots[i].lengthWords = 0;
    }
    return true;
}


void jlpAudioShutdown(void) {
    int i;

    // Voice-layer teardown first, independent of gInstalled: the
    // tone square and noise sample are allocated on first use, which
    // does not require jlpAudioInit. DMA must stop before the frees
    // or Paula keeps looping freed chip RAM.
    custom.dmacon = DMAF_AUD0 | DMAF_AUD1 | DMAF_AUD2 | DMAF_AUD3;
    for (i = 0; i < PAULA_CHANNELS; i++) {
        custom.aud[i].ac_vol = 0;
    }
    if (gVoiceChip != NULL) {
        FreeMem(gVoiceChip, VOICE_SQUARE_BYTES);
        gVoiceChip = NULL;
    }
    if (gNoiseChip != NULL) {
        FreeMem(gNoiseChip, NOISE_BYTES);
        gNoiseChip = NULL;
    }

    if (!gInstalled) {
        return;
    }
    mt_Enable = 0;
    mt_end((void *)&custom);
    mt_remove();
    gInstalled  = false;
    gModPlaying = false;

    if (gModuleChip != NULL) {
        FreeMem(gModuleChip, gModuleLength);
        gModuleChip   = NULL;
        gModuleLength = 0;
    }
    for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
        if (gSfxSlots[i].samples != NULL) {
            FreeMem(gSfxSlots[i].samples, (ULONG)gSfxSlots[i].lengthWords * 2UL);
            gSfxSlots[i].samples     = NULL;
            gSfxSlots[i].lengthWords = 0;
        }
    }
}


bool jlpAudioIsPlayingMod(void) {
    return gModPlaying && mt_Enable != 0;
}


void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
    UBYTE *chip;

    if (!gInstalled) {
        return;
    }
    mt_Enable     = 0;
    mt_end((void *)&custom);
    gModPlaying   = false;
    gWantLoopOnce = false;
    if (gModuleChip != NULL) {
        FreeMem(gModuleChip, gModuleLength);
        gModuleChip   = NULL;
        gModuleLength = 0;
    }

    chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
    if (chip == NULL) {
        return;
    }
    memcpy(chip, data, length);
    gModuleChip   = chip;
    gModuleLength = (ULONG)length;

    // PTPlayer takes the module pointer in a0, sample-data pointer in
    // a1 (NULL = samples follow the module header), song position in
    // d0. mt_init resets mt_E8Trigger to 0 so subsequent jlpAudioFrame
    // Tick polling sees a clean state.
    mt_init((void *)&custom, chip, NULL, 0);
    maskNoiseChannel();                 // mt_end above cleared the mask
    mt_Enable     = 1;
    gWantLoopOnce = !loop;
    gModPlaying   = true;
}


void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
    SfxStructure  sfx;
    SfxSlotT     *s;
    UBYTE        *chip;
    UWORD         words;

    if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    if (length < 2 || length > 0x1FFFEUL) {
        return;  // Paula sample length is 16 bits of words = 128KB max
    }

    s = &gSfxSlots[slot];
    if (s->samples != NULL) {
        FreeMem(s->samples, (ULONG)s->lengthWords * 2UL);
        s->samples     = NULL;
        s->lengthWords = 0;
    }

    chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
    if (chip == NULL) {
        return;
    }
    memcpy(chip, sample, length);
    words = (UWORD)(length >> 1);

    s->samples     = chip;
    s->lengthWords = words;

    sfx.sfx_ptr = chip;
    sfx.sfx_len = words;
    sfx.sfx_per = periodForRate(rateHz);
    sfx.sfx_vol = SFX_VOLUME_MAX;
    sfx.sfx_cha = -1;                 // best free channel
    sfx.sfx_pri = (BYTE)(slot + 1);   // priority 1..N from slot index
    mt_playfx((void *)&custom, &sfx);
}


// Streaming SFX is not wired on Amiga: PTPlayer's mt_playfx wants a
// complete sample at fire time. Implementing streaming here would
// need a per-slot Paula buffer with mid-playback refill from the
// audio IRQ. No-op until then; callers that depend on streaming
// will hear silence on Amiga.
void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
    (void)slot;
    (void)fill;
    (void)ctx;
    (void)rateHz;
}


// Noise voice on Paula channel 3: a looped chip-RAM LFSR sample.
// DMA auto-reload sustains it with zero CPU, exactly like the tone
// squares; while a MOD plays, PTPlayer's interrupt owns the channel
// and noise writes lose -- the same exclusivity as the tone voices.
void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
    UWORD vol;

    if (atten >= VOICE_ATTEN_OFF) {
        custom.dmacon                       = DMAF_AUD3;
        custom.aud[NOISE_CHANNEL].ac_vol    = 0;
        return;
    }

    if (gNoiseChip == NULL) {
        uint16_t lfsr;
        int      i;

        gNoiseChip = (BYTE *)AllocMem(NOISE_BYTES, MEMF_CHIP);
        if (gNoiseChip == NULL) {
            return;
        }
        lfsr = NOISE_LFSR_SEED;
        for (i = 0; i < NOISE_BYTES; i++) {
            gNoiseChip[i] = (BYTE)((lfsr & 1u) ? 127 : -128);
            lfsr          = (uint16_t)((lfsr >> 1) ^ ((lfsr & 1u) ? NOISE_LFSR_TAPS : 0u));
        }
    }

    if (pitch > NOISE_PITCH_MAX) {
        pitch = NOISE_PITCH_MAX;
    }
    vol = (UWORD)(((UWORD)(VOICE_ATTEN_OFF - atten) * VOICE_VOLUME_MAX) / VOICE_ATTEN_OFF);

    // ptr/len writes latch at the next loop reload; per/vol take
    // effect immediately -- a live retune is click-free and the
    // redundant DMA set is harmless.
    custom.aud[NOISE_CHANNEL].ac_ptr = (UWORD *)gNoiseChip;
    custom.aud[NOISE_CHANNEL].ac_len = NOISE_BYTES / 2;   // Paula length is in words
    custom.aud[NOISE_CHANNEL].ac_per = kNoisePeriod[pitch];
    custom.aud[NOISE_CHANNEL].ac_vol = vol;
    custom.dmacon                    = (UWORD)(DMAF_SETCLR | DMAF_MASTER | DMAF_AUD3);
}


void jlpAudioTone(uint16_t freqHz) {
    jlpAudioVoice(0u, freqHz, 0u);          // single full-volume tone on voice 0
}


// 3-voice PSG-style tone on Paula channels 0-2, programmed directly
// (no audio.device AllocAudio -- consistent with the ST's direct YM
// pokes; a JoeyLib game owns the machine while it runs). Caveats:
// while a MOD is playing, PTPlayer rewrites all four channels' regs
// from its CIA interrupt and voice writes lose -- same voices-vs-MOD
// exclusivity as the ST's YM. mt_playfx SFX can also land on a busy
// voice channel (it picks any free-looking channel); prefer the
// tracker/voice layer OR the MOD/SFX layer per scene, not both.
void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
    ULONG period;
    UWORD vol;

    if (voice >= JOEY_AUDIO_VOICES) {
        return;
    }
    if (freqHz == 0u || atten >= VOICE_ATTEN_OFF) {
        // Silence: stop the channel's DMA and zero its volume. The
        // dmacon write without DMAF_SETCLR clears the named bit only.
        custom.dmacon             = (UWORD)(DMAF_AUD0 << voice);
        custom.aud[voice].ac_vol  = 0;
        return;
    }

    if (gVoiceChip == NULL) {
        gVoiceChip = (BYTE *)AllocMem(VOICE_SQUARE_BYTES, MEMF_CHIP);
        if (gVoiceChip == NULL) {
            return;
        }
        gVoiceChip[0] = 127;
        gVoiceChip[1] = -128;
    }

    period = PAULA_CLOCK_PAL / ((ULONG)freqHz * 2UL);
    if (period < PAULA_PERIOD_MIN) {
        period = PAULA_PERIOD_MIN;
    }
    if (period > PAULA_PERIOD_MAX) {
        period = PAULA_PERIOD_MAX;
    }

    // AGI atten 0..14 (0 = loud) -> Paula volume 64..4, linear.
    vol = (UWORD)(((UWORD)(VOICE_ATTEN_OFF - atten) * VOICE_VOLUME_MAX) / VOICE_ATTEN_OFF);

    custom.aud[voice].ac_ptr = (UWORD *)gVoiceChip;
    custom.aud[voice].ac_len = VOICE_SQUARE_BYTES / 2;   // Paula length is in words
    custom.aud[voice].ac_per = (UWORD)period;
    custom.aud[voice].ac_vol = vol;
    custom.dmacon            = (UWORD)(DMAF_SETCLR | DMAF_MASTER | (DMAF_AUD0 << voice));
}


bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
    // CIA-B Timer B at hz could drive this; not wired until the
    // Paula voice path lands.
    (void)fn;
    (void)hz;
    return false;
}


void jlpAudioCriticalEnter(void) {
}


void jlpAudioCriticalExit(void) {
}


void jlpAudioStopMod(void) {
    if (!gInstalled) {
        return;
    }
    mt_Enable     = 0;
    mt_end((void *)&custom);
    maskNoiseChannel();                 // mt_end cleared the mask
    gModPlaying   = false;
    gWantLoopOnce = false;
}


void jlpAudioStopSfx(uint8_t slot) {
    UBYTE i;

    if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    // PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
    // picks a channel dynamically and does not return which one. We
    // can't reliably map slot -> channel after the fact, so stop all
    // PAULA_CHANNELS channels' SFX state. mt_stopfx is idempotent on
    // idle channels, so stopping a quiet one is harmless.
    for (i = 0; i < PAULA_CHANNELS; i++) {
        mt_stopfx((void *)&custom, i);
    }
}


void jlpAudioFrameTick(void) {
    // PTPlayer drives itself off CIA-B; the only host-loop work is the
    // play-once watchdog. When the song author placed an `E8FF` at
    // song end, mt_E8Trigger latches to 0xFF -- if the caller passed
    // loop=false to PlayMod, that's our cue to stop the song.
    if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) {
        mt_Enable     = 0;
        gModPlaying   = false;
        gWantLoopOnce = false;
    }
}
