#include "../Audio.h"

#include <math.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include <coreinit/cache.h>
#include <coreinit/mutex.h>
#include <coreinit/thread.h>
#include <sndcore2/core.h>
#include <sndcore2/voice.h>
#include <sndcore2/drcvs.h>

#include "SoftwareMixer.h"

#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define CLAMP(x, y, z) MIN(MAX((x), (y)), (z))

static void (*organya_callback)(void);
static unsigned int organya_callback_milliseconds;

static OSMutex sound_list_mutex;
static OSMutex organya_mutex;

static AXVoice *voices[2];

static short *stream_buffers[2];
static long *stream_buffer_long;
static size_t buffer_length;

static unsigned long output_frequency;

static void MixSoundsAndUpdateOrganya(long *stream, size_t frames_total)
{
	OSLockMutex(&organya_mutex);

	if (organya_callback_milliseconds == 0)
	{
		OSLockMutex(&sound_list_mutex);
		Mixer_MixSounds(stream, frames_total);
		OSUnlockMutex(&sound_list_mutex);
	}
	else
	{
		// Synchronise audio generation with Organya.
		// In the original game, Organya ran asynchronously in a separate thread,
		// firing off commands to DirectSound in realtime. To match that, we'd
		// need a very low-latency buffer, otherwise we'd get mistimed instruments.
		// Instead, we can just do this.
		unsigned int frames_done = 0;

		while (frames_done != frames_total)
		{
			static unsigned long organya_countdown;

			if (organya_countdown == 0)
			{
				organya_countdown = (organya_callback_milliseconds * output_frequency) / 1000;	// organya_timer is in milliseconds, so convert it to audio frames
				organya_callback();
			}

			const unsigned int frames_to_do = MIN(organya_countdown, frames_total - frames_done);

			OSLockMutex(&sound_list_mutex);
			Mixer_MixSounds(stream + frames_done * 2, frames_to_do);
			OSUnlockMutex(&sound_list_mutex);

			frames_done += frames_to_do;
			organya_countdown -= frames_to_do;
		}
	}

	OSUnlockMutex(&organya_mutex);
}

static void FrameCallback(void)
{
	// We use a double-buffer: while the Wii U is busy playing one half of the buffer, we update the other.
	// The buffer is 10ms long in total, and this function runs every 3ms.

	// Just assume both voices are in-sync, and only check the first one
	AXVoiceOffsets offsets;
	AXGetVoiceOffsets(voices[0], &offsets);

	unsigned int current_buffer = offsets.currentOffset > buffer_length ? 1 : 0;	// TODO - should probably be '>='

	static unsigned int last_buffer = 1;

	if (current_buffer != last_buffer)
	{
		// Clear the mixer buffer
		memset(stream_buffer_long, 0, buffer_length * sizeof(long) * 2);

		// Fill mixer buffer
		MixSoundsAndUpdateOrganya(stream_buffer_long, buffer_length);

		// Deinterlate samples, convert them to S16, and write them to the double-buffers
		short *left_output_buffer = &stream_buffers[0][buffer_length * last_buffer];
		short *right_output_buffer = &stream_buffers[1][buffer_length * last_buffer];

		long *mixer_buffer_pointer = stream_buffer_long;
		short *left_output_buffer_pointer = left_output_buffer;
		short *right_output_buffer_pointer = right_output_buffer;

		for (unsigned int i = 0; i < buffer_length; ++i)
		{
			const long left_sample = *mixer_buffer_pointer++;
			const long right_sample = *mixer_buffer_pointer++;

			// Clamp samples to sane limits, convert to S16, and store in double-buffers
			if (left_sample > 0x7FFF)
				*left_output_buffer_pointer++ = 0x7FFF;
			else if (left_sample < -0x7FFF)
				*left_output_buffer_pointer++ = -0x7FFF;
			else
				*left_output_buffer_pointer++ = (short)left_sample;

			if (right_sample > 0x7FFF)
				*right_output_buffer_pointer++ = 0x7FFF;
			else if (right_sample < -0x7FFF)
				*right_output_buffer_pointer++ = -0x7FFF;
			else
				*right_output_buffer_pointer++ = (short)right_sample;
		}

		// Make sure the sound hardware can see our data
		DCStoreRange(left_output_buffer, buffer_length * sizeof(short));
		DCStoreRange(right_output_buffer, buffer_length * sizeof(short));

		last_buffer = current_buffer;
	}
}

bool AudioBackend_Init(void)
{
	if (!AXIsInit())
	{
		AXInitParams initparams = {
			.renderer = AX_INIT_RENDERER_48KHZ,
			.pipeline = AX_INIT_PIPELINE_SINGLE,
		};

		AXInitWithParams(&initparams);
	}

	OSInitMutex(&sound_list_mutex);
	OSInitMutex(&organya_mutex);

	output_frequency = AXGetInputSamplesPerSec();

	Mixer_Init(output_frequency);

	buffer_length = output_frequency / 100;	// 10ms buffer

	// Create and initialise two 'voices': each one will stream its own
	// audio - one for the left speaker, and one for the right. 

	// The software-mixer outputs interlaced samples into a buffer of `long`s,
	// so create a buffer for it here.
	stream_buffer_long = (long*)malloc(buffer_length * sizeof(long) * 2);	// `* 2` because it's an interlaced stereo buffer

	if (stream_buffer_long != NULL)
	{
		stream_buffers[0] = (short*)malloc(buffer_length * sizeof(short) * 2);	// `* 2` because it's a double-buffer

		if (stream_buffers[0] != NULL)
		{
			stream_buffers[1] = (short*)malloc(buffer_length * sizeof(short) * 2);	// `* 2` because it's a double-buffer

			if (stream_buffers[1] != NULL)
			{
				voices[0] = AXAcquireVoice(31, NULL, NULL);

				if (voices[0] != NULL)
				{
					voices[1] = AXAcquireVoice(31, NULL, NULL);

					if (voices[1] != NULL)
					{
						for (unsigned int i = 0; i < 2; ++i)
						{
							AXVoiceBegin(voices[i]);

							AXSetVoiceType(voices[i], 0);

							AXVoiceVeData vol = {.volume = 0x8000};
							AXSetVoiceVe(voices[i], &vol);

							AXVoiceDeviceMixData mix_data[6];
							memset(mix_data, 0, sizeof(mix_data));
							mix_data[0].bus[0].volume = i == 0 ? 0x8000 : 0;	// Channel 1 goes on the left speaker
							mix_data[1].bus[0].volume = i == 1 ? 0x8000 : 0;	// Channel 2 goes on the right speaker

							AXSetVoiceDeviceMix(voices[i], AX_DEVICE_TYPE_DRC, 0, mix_data);
							AXSetVoiceDeviceMix(voices[i], AX_DEVICE_TYPE_TV, 0, mix_data);

							AXSetVoiceSrcRatio(voices[i], 1.0f);	// We use the native sample rate
							AXSetVoiceSrcType(voices[i], AX_VOICE_SRC_TYPE_NONE);

							AXVoiceOffsets offs = {
								.dataType = AX_VOICE_FORMAT_LPCM16,
								.loopingEnabled = AX_VOICE_LOOP_ENABLED,
								.loopOffset = 0,
								.endOffset = buffer_length * 2,
								.currentOffset = 0,
								.data = stream_buffers[i]
							};
							AXSetVoiceOffsets(voices[i], &offs);

							AXSetVoiceState(voices[i], AX_VOICE_STATE_PLAYING);

							AXVoiceEnd(voices[i]);
						}

						// Register the frame callback.
						// Apparently, this fires every 3ms - we will use
						// it to update the stream buffers when needed.
						AXRegisterAppFrameCallback(FrameCallback);

						return true;
					}

					AXFreeVoice(voices[0]);
				}

				free(stream_buffers[1]);
			}

			free(stream_buffers[0]);
		}

		free(stream_buffer_long);
	}

	return false;
}

void AudioBackend_Deinit(void)
{
	for (unsigned int i = 0; i < 2; ++i)
	{
		AXFreeVoice(voices[i]);
		free(stream_buffers[i]);
	}

	AXQuit();
}

AudioBackend_Sound* AudioBackend_CreateSound(unsigned int frequency, const unsigned char *samples, size_t length)
{
	OSLockMutex(&sound_list_mutex);

	Mixer_Sound *sound = Mixer_CreateSound(frequency, samples, length);

	OSUnlockMutex(&sound_list_mutex);

	return (AudioBackend_Sound*)sound;
}

void AudioBackend_DestroySound(AudioBackend_Sound *sound)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_DestroySound((Mixer_Sound*)sound);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_PlaySound(AudioBackend_Sound *sound, bool looping)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_PlaySound((Mixer_Sound*)sound, looping);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_StopSound(AudioBackend_Sound *sound)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_StopSound((Mixer_Sound*)sound);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_RewindSound(AudioBackend_Sound *sound)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_RewindSound((Mixer_Sound*)sound);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_SetSoundFrequency(AudioBackend_Sound *sound, unsigned int frequency)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_SetSoundFrequency((Mixer_Sound*)sound, frequency);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_SetSoundVolume(AudioBackend_Sound *sound, long volume)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_SetSoundVolume((Mixer_Sound*)sound, volume);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_SetSoundPan(AudioBackend_Sound *sound, long pan)
{
	if (sound == NULL)
		return;

	OSLockMutex(&sound_list_mutex);

	Mixer_SetSoundPan((Mixer_Sound*)sound, pan);

	OSUnlockMutex(&sound_list_mutex);
}

void AudioBackend_SetOrganyaCallback(void (*callback)(void))
{
	// As far as thread-safety goes - this is guarded by
	// `organya_milliseconds`, which is guarded by `organya_mutex`.
	organya_callback = callback;
}

void AudioBackend_SetOrganyaTimer(unsigned int milliseconds)
{
	OSLockMutex(&organya_mutex);

	organya_callback_milliseconds = milliseconds;

	OSUnlockMutex(&organya_mutex);
}
