/*
 * Copyright (c) 2021-2022 Rupert Carmichael
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <stddef.h>
#include <stdint.h>

#include <speex/speex_resampler.h>

#include "cega_mixer.h"
#include "ym2413.h"
#include "ymfm_shim.h"

#define SAMPLERATE_PSG 224010 // Approximate PSG sample rate (Hz)
//#define SAMPLERATE_YM2612 53267
#define SAMPLERATE_YM2612 53340

#define SIZE_PSGBUF 4480 << 1 // 4461 max in PAL mode, but give some overhead

static int16_t *abuf = NULL; // Buffer to output resampled data into
static size_t samplerate = 48000; // Default sample rate is 48000Hz
static unsigned framerate = 60; // Default to 60 for NTSC
static unsigned rsq = 3; // Default resampler quality is 3

// Chips and related options
static unsigned psg_stereo = 0;
static unsigned psg_enable = 1;
static unsigned ym_enable = 0;
static sn76496_t *psg;

// Buffers
static int16_t psgbuf[SIZE_PSGBUF]; // Buffer for raw PSG output samples

// Sample counters
static size_t ymsamps = 0;

// Sample counters
static uint8_t psgcycs = 0;
static uint8_t ymcycs = 0;

// Speex
static SpeexResamplerState *resamp_psg = NULL;
static SpeexResamplerState *resamp_2413 = NULL;
static SpeexResamplerState *resamp_2612 = NULL;
static int err;

// Callback to notify the fronted that N samples are ready
static void (*cega_mixer_cb)(size_t);

void cega_mixer_get_cycs(uint8_t *p, uint8_t *y) {
    *p = psgcycs;
    if (y)
        *y = ymcycs;
}

void cega_mixer_set_cycs(uint8_t p, uint8_t y) {
    psgcycs = p;
    ymcycs = y;
}

void cega_mixer_set_psg(sn76496_t *p) {
    psg = p;
    psg->buf = psgbuf;
}

// Set the output sample rate
void cega_mixer_set_rate(size_t rate) {
    switch (rate) {
        case 44100: case 48000: case 96000: case 192000:
            samplerate = rate;
            break;
        default:
            break;
    }
}

// Set the region
void cega_mixer_set_region(unsigned region) {
    framerate = region > 2 ? 50 : 60; // 50 for PAL, 60 for NSTC/PAL-M
}

// Set the resampler quality
void cega_mixer_set_rsqual(unsigned qual) {
    if (qual <= 10)
        rsq = qual;
}

// Set the pointer to the output audio buffer
void cega_mixer_set_buffer(int16_t *ptr) {
    abuf = ptr;
}

// Set the callback that notifies the frontend that N audio samples are ready
void cega_mixer_set_callback(void (*cb)(size_t)) {
    cega_mixer_cb = cb;
}

// Deinitialize the resampler
void cega_mixer_deinit(void) {
    if (resamp_psg) {
        speex_resampler_destroy(resamp_psg);
        resamp_psg = NULL;
    }
    if (resamp_2413) {
        speex_resampler_destroy(resamp_2413);
        resamp_2413 = NULL;
    }
    if (resamp_2612) {
        speex_resampler_destroy(resamp_2612);
        resamp_2612 = NULL;
    }
}

// Bring up the Speex resampler
void cega_mixer_init(unsigned st) {
    psg_stereo = st;
    resamp_psg =
        speex_resampler_init(st ? 2 : 1, SAMPLERATE_PSG, samplerate, rsq, &err);
    resamp_2413 =
        speex_resampler_init(1, SAMPLERATE_YM2413, samplerate, rsq, &err);
    resamp_2612 =
        speex_resampler_init(2, SAMPLERATE_YM2612, samplerate, rsq, &err);
}

void cega_mixer_chips_enable(unsigned p, unsigned y) {
    psg_enable = p;
    ym_enable = y;

    // Reset buffer positions when chips are disabled
    if (!psg_enable)
        psg->bufpos = 0;
    if (!ym_enable)
        ym2413_disable();
}

void cega_mixer_chips_exec(void) {
    if (psg_enable && (++psgcycs == 16)) {
        sn76496_exec(psg);
        psgcycs = 0;
    }

    if (ym_enable && (++ymcycs == 72)) {
        ymsamps += ym2413_exec();
        ymcycs = 0;
    }
}

// Resample raw YM2413 audio and push samples to the frontend
static void cega_mixer_resamp_2413(size_t in_ym) {
    int16_t *ymbuf = ym2413_get_buffer();
    psg->bufpos = 0;

    spx_uint32_t in_len = in_ym; // Samples or Sample Pairs
    spx_uint32_t outsamps = samplerate / framerate;

    err = speex_resampler_process_int(resamp_2413,
        0, (spx_int16_t*)ymbuf, &in_len, (spx_int16_t*)abuf, &outsamps);
    cega_mixer_cb(outsamps);
}

// Resample raw PSG audio and push samples to the frontend
static void cega_mixer_resamp_psg(size_t in_psg) {
    psg->bufpos = 0;

    spx_uint32_t in_len = in_psg; // Samples or Sample Pairs
    spx_uint32_t outsamps = samplerate / framerate;

    if (psg_stereo) {
        err = speex_resampler_process_interleaved_int(resamp_psg,
            (spx_int16_t*)psgbuf, &in_len, (spx_int16_t*)abuf, &outsamps);
        cega_mixer_cb(outsamps << 1);
    }
    else {
        err = speex_resampler_process_int(resamp_psg,
            0, (spx_int16_t*)psgbuf, &in_len, (spx_int16_t*)abuf, &outsamps);
        cega_mixer_cb(outsamps);
    }
}

// Resample raw PSG and YM2413 audio and push samples to the frontend
static void cega_mixer_resamp_psg_2413(size_t in_psg, size_t in_ym) {
    int16_t *ymbuf = ym2413_get_buffer();
    psg->bufpos = 0;

    spx_uint32_t in_len = in_ym;
    spx_uint32_t outsamps = samplerate / framerate;

    int16_t ymbuf_rs[outsamps];
    err = speex_resampler_process_int(resamp_2413,
        0, (spx_int16_t*)ymbuf, &in_len, (spx_int16_t*)ymbuf_rs, &outsamps);

    in_len = in_psg;
    err = speex_resampler_process_int(resamp_psg,
        0, (spx_int16_t*)psgbuf, &in_len, (spx_int16_t*)abuf, &outsamps);

    for (size_t i = 0; i < outsamps; ++i)
        abuf[i] += ymbuf_rs[i];

    cega_mixer_cb(outsamps);
}

void cega_mixer_resamp(void) {
    if (psg_enable && ym_enable)
        cega_mixer_resamp_psg_2413(psg->bufpos, ymsamps);
    else if (psg_enable)
        cega_mixer_resamp_psg(psg->bufpos);
    else if (ym_enable)
        cega_mixer_resamp_2413(ymsamps);

    // Set sample counters to 0
    ymsamps = 0;
}

// Resample raw PSG and YM2612 audio and push samples to the frontend
void cega_mixer_resamp_md(size_t in_psg, size_t in_ym) {
    int16_t *ybuf = ymfm_shim_get_buffer();
    psg->bufpos = 0;

    spx_uint32_t in_len = in_psg;
    spx_uint32_t outsamps = (samplerate / framerate);

    int16_t psgbuf_rs[outsamps];
    err = speex_resampler_process_int(resamp_psg,
        0, (spx_int16_t*)psgbuf, &in_len, (spx_int16_t*)psgbuf_rs, &outsamps);

    in_len = in_ym;
    err = speex_resampler_process_interleaved_int(resamp_2612,
        (spx_int16_t*)ybuf, &in_len, (spx_int16_t*)abuf, &outsamps);

    for (size_t i = 0; i < outsamps << 1; ++i)
        abuf[i] += (psgbuf_rs[i >> 1] / 3);

    cega_mixer_cb(outsamps << 1);
}
