/*****************************************************************************\
  Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

#include <memory>
#include "apu.h"
#include "resampler.h"

#include "bapu/snes/snes.hpp"
#include "bapu/dsp/sdsp.hpp"
#include "bapu/smp/smp.hpp"

static constexpr int APU_DEFAULT_INPUT_RATE = 31950; // ~59.94Hz
static constexpr int APU_SAMPLE_BLOCK = 48;
static constexpr int APU_NUMERATOR_NTSC = 15664;
static constexpr int APU_DENOMINATOR_NTSC = 328125;
static constexpr int APU_NUMERATOR_PAL = 34176;
static constexpr int APU_DENOMINATOR_PAL = 709379;
// Max number of samples we'll ever generate before call to port API and
// moving the samples to the resampler.
// This is 535 sample frames, which corresponds to 1 video frame + some leeway
// for use with SoundSync, multiplied by 2, for left and right samples.
static constexpr int MINIMUM_BUFFER_SIZE = 550 * 2;

namespace SNES
{
	CPU cpu;
}

namespace spc
{
	static bool sound_in_sync = true;
	static bool sound_enabled = false;

	static std::unique_ptr<Resampler> resampler;

	static int32_t reference_time;
	static uint32_t remainder;

	static constexpr int timing_hack_numerator = 256;
	static int timing_hack_denominator = 256;
	/* Set these to NTSC for now. Will change to PAL in S9xAPUTimingSetSpeedup
	   if necessary on game load. */
	static uint32_t ratio_numerator = APU_NUMERATOR_NTSC;
	static uint32_t ratio_denominator = APU_DENOMINATOR_NTSC;
}

void S9xClearSamples()
{
	spc::resampler->clear();
}

bool S9xMixSamples(uint8_t *dest, int sample_count)
{
	int16_t *out = reinterpret_cast<int16_t *>(dest);

	if (Settings.Mute)
	{
		std::fill_n(&out[0], sample_count, 0);
		S9xClearSamples();
	}
	else
	{
		if (spc::resampler->avail() >= sample_count)
			spc::resampler->read(reinterpret_cast<short *>(out), sample_count);
		else
		{
			std::fill_n(&out[0], sample_count, 0);
			return false;
		}
	}

	spc::sound_in_sync = spc::resampler->space_empty() >= 535 * 2 || !Settings.SoundSync || Settings.TurboMode || Settings.Mute;

	return true;
}

int S9xGetSampleCount()
{
	return spc::resampler->avail();
}

void S9xLandSamples()
{
	spc::sound_in_sync = spc::resampler->space_empty() >= 535 * 2 || !Settings.SoundSync || Settings.TurboMode || Settings.Mute;
}

bool S9xSyncSound()
{
	if (!Settings.SoundSync || spc::sound_in_sync)
		return true;

	S9xLandSamples();

	return spc::sound_in_sync;
}

static void UpdatePlaybackRate()
{
	if (!Settings.SoundInputRate)
		Settings.SoundInputRate = APU_DEFAULT_INPUT_RATE;

	double time_ratio = static_cast<double>(Settings.SoundInputRate) * spc::timing_hack_numerator / (Settings.SoundPlaybackRate * spc::timing_hack_denominator);
	spc::resampler->time_ratio(time_ratio);
}

bool S9xInitSound(int buffer_ms)
{
	// The resampler and spc unit use samples (16-bit short) as arguments.
	int buffer_size_samples = MINIMUM_BUFFER_SIZE;
	int requested_buffer_size_samples = Settings.SoundPlaybackRate * buffer_ms * 2 / 1000;

	if (requested_buffer_size_samples > buffer_size_samples)
		buffer_size_samples = requested_buffer_size_samples;

	spc::resampler.reset(new Resampler(buffer_size_samples));
	if (!spc::resampler)
		return false;

	SNES::dsp.spc_dsp.set_output(spc::resampler.get());

	UpdatePlaybackRate();

	spc::sound_enabled = S9xOpenSoundDevice();

	return spc::sound_enabled;
}

void S9xSetSoundControl(uint8_t voice_switch)
{
	SNES::dsp.spc_dsp.set_stereo_switch((voice_switch << 8) | voice_switch);
}

void S9xSetSoundMute(bool mute)
{
	Settings.Mute = mute;
	if (!spc::sound_enabled)
		Settings.Mute = true;
}

bool S9xInitAPU()
{
	spc::resampler.reset();

	return true;
}

void S9xDeinitAPU()
{
	spc::resampler.reset();
}

static inline int S9xAPUGetClock(int32_t cpucycles)
{
	return (spc::ratio_numerator * (cpucycles - spc::reference_time) + spc::remainder) / spc::ratio_denominator;
}

static inline int S9xAPUGetClockRemainder(int32_t cpucycles)
{
	return (spc::ratio_numerator * (cpucycles - spc::reference_time) + spc::remainder) % spc::ratio_denominator;
}

void S9xAPUExecute()
{
	SNES::smp.clock -= S9xAPUGetClock(CPU.Cycles);
	SNES::smp.enter();

	spc::remainder = S9xAPUGetClockRemainder(CPU.Cycles);

	S9xAPUSetReferenceTime(CPU.Cycles);
}

uint8_t S9xAPUReadPort(int port)
{
	S9xAPUExecute();
	return static_cast<uint8_t>(SNES::smp.port_read(port & 3));
}

void S9xAPUWritePort(int port, uint8_t byte)
{
	S9xAPUExecute();
	SNES::cpu.port_write(port & 3, byte);
}

void S9xAPUSetReferenceTime(int32_t cpucycles)
{
	spc::reference_time = cpucycles;
}

void S9xAPUEndScanline()
{
	S9xAPUExecute();
	SNES::dsp.synchronize();

	if (spc::resampler->space_filled() >= APU_SAMPLE_BLOCK || !spc::sound_in_sync)
		S9xLandSamples();
}

void S9xAPUTimingSetSpeedup(int ticks)
{
	spc::timing_hack_denominator = 256 - ticks;

	spc::ratio_numerator = Settings.PAL ? APU_NUMERATOR_PAL : APU_NUMERATOR_NTSC;
	spc::ratio_denominator = (Settings.PAL ? APU_DENOMINATOR_PAL : APU_DENOMINATOR_NTSC) * spc::timing_hack_denominator / spc::timing_hack_numerator;

	UpdatePlaybackRate();
}

void S9xResetAPU()
{
	spc::reference_time = 0;
	spc::remainder = 0;

	SNES::cpu.reset();
	SNES::smp.power();
	SNES::dsp.power();

	S9xClearSamples();
}