#include <memory>
#include "Sound.h"
#include "GBA.h"
#include "Globals.h"
#include "../common/Port.h"
#include "../apu/Gb_Apu.h"
#include "../apu/Multi_Buffer.h"
#include "../common/SoundDriver.h"

extern SoundDriver *systemSoundInit();

static const uint32_t NR52 = 0x84;

static std::unique_ptr<SoundDriver> soundDriver;

static const int SOUND_CLOCK_TICKS_ = 167772; // 1/100 second

static uint16_t soundFinalWave[6400];
static long soundSampleRate = 44100;
bool soundInterpolation = true;
static bool soundPaused = true;
static float soundFiltering = 1.0f;
int SOUND_CLOCK_TICKS = SOUND_CLOCK_TICKS_;
int soundTicks = SOUND_CLOCK_TICKS_;

static float soundVolume = 1.0f;
static int soundEnableFlag = 0x3ff; // emulator channels enabled
static float soundFiltering_ = -1;
static float soundVolume_ = -1;

class Gba_Pcm
{
public:
	void init();
	void apply_control(int idx);
	void update(int dac);
	void end_frame(blip_time_t);

private:
	Blip_Buffer *output;
	blip_time_t last_time;
	int last_amp;
	int shift;
};

class Gba_Pcm_Fifo
{
public:
	int which;
	Gba_Pcm pcm;

	void write_control(int data);
	void write_fifo(int data);
	void timer_overflowed(int which_timer);

private:
	int readIndex;
	int count;
	int writeIndex;
	uint8_t fifo[32];
	int dac;
	int timer;
	bool enabled;
};

static Gba_Pcm_Fifo pcm[2];
static std::unique_ptr<Gb_Apu> gb_apu;
static std::unique_ptr<Stereo_Buffer> stereo_buffer;

static Blip_Synth<blip_high_quality, 1> pcm_synth[3]; // 32 kHz, 16 kHz, 8 kHz

static inline blip_time_t blip_time()
{
	return SOUND_CLOCK_TICKS - soundTicks;
}

inline void Gba_Pcm::init()
{
	this->output = nullptr;
	this->last_time = 0;
	this->last_amp = 0;
	this->shift = 0;
}

void Gba_Pcm::apply_control(int idx)
{
	this->shift = ~ioMem[SGCNT0_H] >> (2 + idx) & 1;

	int ch = 0;
	if ((soundEnableFlag >> idx & 0x100) && (ioMem[NR52] & 0x80))
		ch = ioMem[SGCNT0_H + 1] >> (idx * 4) & 3;

	Blip_Buffer *out = nullptr;
	switch (ch)
	{
		case 1:
			out = stereo_buffer->right();
			break;
		case 2:
			out = stereo_buffer->left();
			break;
		case 3:
			out = stereo_buffer->center();
	}

	if (this->output != out)
	{
		if (this->output)
		{
			auto time = blip_time();

			this->output->set_modified();

			int filter = 0;
			if (soundInterpolation)
			{
				// base filtering on how long since last sample was output
				int period = time - this->last_time;

				int idx = period / 512;
				if (idx >= 3)
					idx = 3;

				static const int filters[] = { 0, 0, 1, 2 };
				filter = filters[idx];
			}

			pcm_synth[filter].offset(time, -this->last_amp, this->output);
		}
		this->last_amp = 0;
		this->output = out;
	}
}

inline void Gba_Pcm::end_frame(blip_time_t time)
{
	this->last_time -= time;
	if (this->last_time < -2048)
		this->last_time = -2048;

	if (this->output)
		this->output->set_modified();
}

void Gba_Pcm::update(int dac)
{
	if (this->output)
	{
		auto time = blip_time();

		dac = static_cast<int8_t>(dac) >> this->shift;
		int delta = dac - this->last_amp;
		if (delta)
		{
			this->last_amp = dac;

			int filter = 0;
			if (soundInterpolation)
			{
				// base filtering on how long since last sample was output
				int period = time - this->last_time;

				int idx = period / 512;
				if (idx >= 3)
					idx = 3;

				static const int filters[] = { 0, 0, 1, 2 };
				filter = filters[idx];
			}

			pcm_synth[filter].offset(time, delta, this->output);
		}
		this->last_time = time;
	}
}

void Gba_Pcm_Fifo::timer_overflowed(int which_timer)
{
	if (which_timer == this->timer && this->enabled)
	{
		/* Mother 3 fix, refined to not break Metroid Fusion */
		if (this->count == 16 || !this->count)
		{
			// Need to fill FIFO
			int saved_count = this->count;
			CPUCheckDMA(3, this->which ? 4 : 2);
			if (!saved_count && this->count == 16)
				CPUCheckDMA(3, this->which ? 4 : 2);
			if (!this->count)
			{
				// Not filled by DMA, so fill with 16 bytes of silence
				int reg = this->which ? FIFOB_L : FIFOA_L;
				for (int n = 8; n--; )
				{
					soundEvent(reg, static_cast<uint16_t>(0));
					soundEvent(reg + 2, static_cast<uint16_t>(0));
				}
			}
		}

		// Read next sample from FIFO
		--this->count;
		this->dac = this->fifo[this->readIndex];
		this->readIndex = (this->readIndex + 1) & 31;
		this->pcm.update(this->dac);
	}
}

void Gba_Pcm_Fifo::write_control(int data)
{
	this->enabled = !!(data & 0x0300);
	this->timer = !!(data & 0x0400);

	if (data & 0x0800)
	{
		// Reset
		this->writeIndex = 0;
		this->readIndex = 0;
		this->count = 0;
		this->dac = 0;
		memset(this->fifo, 0, sizeof(this->fifo));
	}

	this->pcm.apply_control(this->which);
	this->pcm.update(this->dac);
}

inline void Gba_Pcm_Fifo::write_fifo(int data)
{
	this->fifo[this->writeIndex] = data & 0xFF;
	this->fifo[this->writeIndex + 1] = data >> 8;
	this->count += 2;
	this->writeIndex = (this->writeIndex + 2) & 31;
}

static void apply_control()
{
	pcm[0].pcm.apply_control(0);
	pcm[1].pcm.apply_control(1);
}

static int gba_to_gb_sound(int addr)
{
	static const int table[] =
	{
		0xFF10, 0, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0, 0,
		0xFF16, 0xFF17, 0, 0, 0xFF18, 0xFF19, 0, 0,
		0xFF1A, 0, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0, 0,
		0xFF20, 0xFF21, 0, 0, 0xFF22, 0xFF23, 0, 0,
		0xFF24, 0xFF25, 0, 0, 0xFF26, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37,
		0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFF3C, 0xFF3D, 0xFF3E, 0xFF3F,
	};
	if (addr >= 0x60 && addr < 0xA0)
		return table[addr - 0x60];
	return 0;
}

void soundEvent(uint32_t address, uint8_t data)
{
	int gb_addr = gba_to_gb_sound(address);
	if (gb_addr)
	{
		ioMem[address] = data;
		gb_apu->write_register(blip_time(), gb_addr, data);

		if (address == NR52)
			apply_control();
	}

	ioMem[NR52] = (ioMem[NR52] & 0x80) | (gb_apu->read_status() & 0x7f);

	// TODO: what about byte writes to SGCNT0_H etc.?
}

static void apply_volume(bool apu_only = false)
{
	if (!apu_only)
		soundVolume_ = soundVolume;

	if (gb_apu)
	{
		static const float apu_vols[] = { 0.25, 0.5, 1, 0.25 };
		gb_apu->volume(soundVolume_ * apu_vols[ioMem[SGCNT0_H] & 3]);
	}

	if (!apu_only)
		for (int i = 0; i < 3; ++i)
			pcm_synth[i].volume(0.66 / 256 * soundVolume_);
}

static void write_SGCNT0_H(int data)
{
	WRITE16LE(&ioMem[SGCNT0_H], data & 0x770F);
	pcm[0].write_control(data);
	pcm[1].write_control(data >> 4);
	apply_volume(true);
}

void soundEvent(uint32_t address, uint16_t data)
{
	switch (address)
	{
		case SGCNT0_H:
			write_SGCNT0_H(data);
			break;

		case FIFOA_L:
		case FIFOA_H:
			pcm[0].write_fifo(data);
			WRITE16LE(&ioMem[address], data);
			break;

		case FIFOB_L:
		case FIFOB_H:
			pcm[1].write_fifo(data);
			WRITE16LE(&ioMem[address], data);
			break;

		case 0x88:
			data &= 0xC3FF;
			WRITE16LE(&ioMem[address], data);
			break;

		default:
			soundEvent(address & ~1, static_cast<uint8_t>(data)); // even
			soundEvent(address | 1, static_cast<uint8_t>(data >> 8)); // odd
	}
}

void soundTimerOverflow(int timer)
{
	pcm[0].timer_overflowed(timer);
	pcm[1].timer_overflowed(timer);
}

static void end_frame(blip_time_t time)
{
	pcm[0].pcm.end_frame(time);
	pcm[1].pcm.end_frame(time);

	gb_apu->end_frame(time);
	stereo_buffer->end_frame(time);
}

void flush_samples(Multi_Buffer *buffer)
{
	// We want to write the data frame by frame to support legacy audio drivers
	// that don't use the length parameter of the write method.
	// TODO: Update the Win32 audio drivers (DS, OAL, XA2), and flush all the
	// samples at once to help reducing the audio delay on all platforms.
	int soundBufferLen = (soundSampleRate / 60) * 4;

	// soundBufferLen should have a whole number of sample pairs
	assert(!(soundBufferLen % (2 * sizeof(*soundFinalWave))));

	// number of samples in output buffer
	int out_buf_size = soundBufferLen / sizeof(*soundFinalWave);

	// Keep filling and writing soundFinalWave until it can't be fully filled
	while (buffer->samples_avail() >= out_buf_size)
	{
		buffer->read_samples(reinterpret_cast<blip_sample_t *>(soundFinalWave), out_buf_size);
		if (soundPaused)
			soundResume();

		soundDriver->write(soundFinalWave, soundBufferLen);
	}
}

static void apply_filtering()
{
	soundFiltering_ = soundFiltering;

	int base_freq = static_cast<int>(32768 - soundFiltering_ * 16384);
	int nyquist = stereo_buffer->sample_rate() / 2;

	for (int i = 0; i < 3; ++i)
	{
		int cutoff = base_freq >> i;
		if (cutoff > nyquist)
			cutoff = nyquist;
		pcm_synth[i].treble_eq(blip_eq_t(0, 0, stereo_buffer->sample_rate(), cutoff));
	}
}

void psoundTickfn()
{
 	if (gb_apu && stereo_buffer)
	{
		// Run sound hardware to present
		end_frame(SOUND_CLOCK_TICKS);

		flush_samples(stereo_buffer.get());

		if (soundFiltering_ != soundFiltering)
			apply_filtering();

		if (soundVolume_ != soundVolume)
			apply_volume();
	}

	ioMem[NR52] = (ioMem[NR52] & 0x80) | (gb_apu->read_status() & 0x7f);
}

static void apply_muting()
{
	if (!stereo_buffer)
		return;

	// PCM
	apply_control();

	if (gb_apu)
		// APU
		for (int i = 0; i < 4; ++i)
			if (soundEnableFlag >> i & 1)
				gb_apu->set_output(stereo_buffer->center(), stereo_buffer->left(), stereo_buffer->right(), i);
			else
				gb_apu->set_output(nullptr, nullptr, nullptr, i);
}

static void reset_apu()
{
	gb_apu->reduce_clicks(true);
	gb_apu->reset(gb_apu->mode_agb, true);

	if (stereo_buffer)
		stereo_buffer->clear();

	soundTicks = SOUND_CLOCK_TICKS;
}

static void remake_stereo_buffer()
{
	// Clears pointers kept to old stereo_buffer
	pcm[0].pcm.init();
	pcm[1].pcm.init();

	// APU
	if (!gb_apu)
	{
		gb_apu.reset(new Gb_Apu); // TODO: handle out of memory
		reset_apu();
	}

	// Stereo_Buffer
	stereo_buffer.reset(new Stereo_Buffer); // TODO: handle out of memory
	stereo_buffer->set_sample_rate(soundSampleRate); // TODO: handle out of memory
	stereo_buffer->clock_rate(gb_apu->clock_rate);

	// PCM
	pcm[0].which = 0;
	pcm[1].which = 1;
	apply_filtering();

	// Volume Level
	apply_muting();
	apply_volume();
}

void soundShutdown()
{
	soundDriver.reset();

	// APU
	gb_apu.reset();

	// Stereo_Buffer
	stereo_buffer.reset();
}

void soundPause()
{
	soundPaused = true;
	if (soundDriver)
		soundDriver->pause();
}

void soundResume()
{
	soundPaused = false;
	if (soundDriver)
		soundDriver->resume();
}

void soundSetEnable(int channels)
{
	soundEnableFlag = channels;
	apply_muting();
}

void soundReset()
{
	soundDriver->reset();

	remake_stereo_buffer();
	reset_apu();

	soundPaused = true;
	SOUND_CLOCK_TICKS = soundTicks = SOUND_CLOCK_TICKS_;

	soundEvent(NR52, static_cast<uint8_t>(0x80));
}

bool soundInit()
{
	soundDriver.reset(systemSoundInit());
	if (!soundDriver)
		return false;

	if (!soundDriver->init(soundSampleRate))
		return false;

	soundPaused = true;
	return true;
}

void soundSetSampleRate(long sampleRate)
{
	if (soundSampleRate != sampleRate)
	{
		soundSampleRate = sampleRate;

		remake_stereo_buffer();
	}
}