// Copyright (C) 2020 Daniel Mall
#include "CAudio_Sokol.h"
#include "sokol_audio.h"
#include "CAudioMixMultiSource.h"
#include <cassert>
#include <map>

// most platforms have a dedicated audio thread which the audio plays on
// the emscripten wasm webaudio platform has only the main thread

// todo - multithreaded access to the audio mixer state
// todo - confirm that frame drawing and audio filling are on the same thread when using the sokol audio push model
// todo - apply volume value, test fading of music
// consider - when changing the background music should the previous music be faded out?

//! audio state
class CAudioMixerState_Sokol
{
public:
	void Deallocate();

public:
	//! is audio functioning
	bool m_bAudioEnabled = false;

	//! is audio active
	bool m_bAudioActive = false;

	//! play music option
	bool m_bIsMusicOn = true;

	//! play sounds option
	bool m_bIsSoundsOn = true;

	//! cache of audio loaded from files
	std::map<std::string, CAudio_Sokol*> m_mapAudioCache;

	//! number of sounds mixing at the current time
	uint32_t m_nSoundCount = 0;

	//! root node of the currently playing items
	CAudio_Sokol* m_root = nullptr;

	//! audio mixing algorithm
	CAudioMixMultiSource m_mix;

public:
	static void DeallocateAudioList(CAudio_Sokol* pAudio);
};

//! audio device
static CAudioMixerState_Sokol* gs_pAudioState = nullptr;

//! add audio to the play queue
static void AddAudio(CAudio_Sokol* pRoot, CAudio_Sokol* pNewAudio)
{
	if (pRoot == nullptr)
	{
		return;
	}

	while (pRoot->m_pNext != nullptr)
	{
		pRoot = pRoot->m_pNext;
	}

	pRoot->m_pNext = pNewAudio;
}

//! add audio music to the play queue
static void AddMusic(CAudio_Sokol* pRoot, CAudio_Sokol* pNewAudio)
{
	CAudio_Sokol* pRootNext = pRoot->m_pNext;
	while (pRootNext != NULL)
	{
		// phase out any current music
		if (pRootNext->m_bLoop)
		{
			pRootNext->m_nLength = 0;
			pRootNext->m_nVolume = 0;
			pRootNext->m_bLoop = false;
			pRootNext->m_bSoundCount = false;
		}

		pRootNext = pRootNext->m_pNext;
	}

	AddAudio(pRoot, pNewAudio);
}

//! add an audio to the play queue
static void PlayAudio(CAudio_Sokol* pAudio, bool bLoop, int nVolume)
{
	if (pAudio == nullptr)
	{
		return;
	}

	if (!gs_pAudioState->m_bAudioEnabled)
	{
		return;
	}

	// If sound, check if under max number of sounds allowed, else don't play
	if (!bLoop)
	{
		if (gs_pAudioState->m_nSoundCount >= AUDIOMIXER_MAXSOUNDS)
		{
			return;
		}
		else
		{
			gs_pAudioState->m_nSoundCount++;
		}
	}

	CAudio_Sokol* pNewAudio = new CAudio_Sokol;
	if (pNewAudio == nullptr)
	{
		return;
	}

	{
		// copy original audio source to a playing audio source
		pNewAudio->m_nLength = pAudio->m_nLength;
		pNewAudio->m_nLengthTrue = pAudio->m_nLengthTrue;
		pNewAudio->m_pBufferTrue = pAudio->m_pBufferTrue;
		pNewAudio->m_pBuffer = pAudio->m_pBuffer;
		pNewAudio->m_bLoop = pAudio->m_bLoop;
		pNewAudio->m_nVolume = pAudio->m_nVolume;
		pNewAudio->m_pNext = pAudio->m_pNext;
		pNewAudio->m_bFree = false;
	}

	pNewAudio->m_nVolume = nVolume;
	pNewAudio->m_bLoop = bLoop;

	if (bLoop)
	{
		AddMusic(gs_pAudioState->m_root, pNewAudio);
	}
	else
	{
		AddAudio(gs_pAudioState->m_root, pNewAudio);
	}
}

//! play music on loop
void CAudioMixer_Sokol::PlayMusic(CAudio* pAudio, int nVolume)
{
	PlayAudio((CAudio_Sokol*)pAudio, true, nVolume);
}

//! play sound one time
void CAudioMixer_Sokol::PlaySound(CAudio* pAudio, int nVolume)
{
	if (!gs_pAudioState->m_bIsSoundsOn)
	{
		return;
	}

	PlayAudio((CAudio_Sokol*)pAudio, false, nVolume);
}

//! reset the playback position to beginning
void CAudio_Sokol::SetToBegin()
{
	m_pBuffer = m_pBufferTrue;
	m_nLength = m_nLengthTrue;
}

//! move the playback position forward
void CAudio_Sokol::AdvanceFrames(int nAdvanceFrames)
{
	m_pBuffer += (nAdvanceFrames * WAV16_CHANNELS);
	m_nLength -= (nAdvanceFrames * WAV16_FRAMESIZE);
}

CAudio_Sokol::~CAudio_Sokol()
{
	if (m_bFree && (m_pBufferTrue != nullptr))
	{
		// todo - deallocate
		// m_pBufferTrue
		assert(false);
		m_pBufferTrue = nullptr;
	}
}

//! turn on audio mixer music
void CAudioMixer_Sokol::TurnOnMusic()
{
	gs_pAudioState->m_bIsMusicOn = true;
}

//! turn off audio mixer music
void CAudioMixer_Sokol::TurnOffMusic()
{
	gs_pAudioState->m_bIsMusicOn = false;
}

//! turn on audio mixer sounds
void CAudioMixer_Sokol::TurnOnSounds()
{
	gs_pAudioState->m_bIsSoundsOn = true;
}

//! turn off audio mixer sounds
void CAudioMixer_Sokol::TurnOffSounds()
{
	gs_pAudioState->m_bIsSoundsOn = false;
}

void CAudioMixer_Sokol::TurnOffAudioMixer()
{
	if (!gs_pAudioState->m_bAudioEnabled)
	{
		return;
	}

	gs_pAudioState->m_bAudioActive = false;
}

void CAudioMixer_Sokol::TurnOnAudioMixer()
{
	if (!gs_pAudioState->m_bAudioEnabled)
	{
		return;
	}

	gs_pAudioState->m_bAudioActive = true;
}

//! add audio music to the play queue
void CAudioMixer_Sokol::ClearAudioQueue()
{
	CAudio_Sokol* pRootNext = gs_pAudioState->m_root;
	while (pRootNext != NULL)
	{
		// phase out any current music or sounds
		pRootNext->m_nLength = 0;
		pRootNext->m_nVolume = 0;
		pRootNext->m_bLoop = false;
		pRootNext->m_bSoundCount = false;
		pRootNext = pRootNext->m_pNext;
	}

	gs_pAudioState->m_nSoundCount = 0;
}

//! start audio so call once at the start of program
void CAudioMixer_Sokol::AudioDeviceOpen()
{
	gs_pAudioState = new CAudioMixerState_Sokol;
	gs_pAudioState->m_root = new CAudio_Sokol;
	gs_pAudioState->m_nSoundCount = 0;
	gs_pAudioState->m_bAudioEnabled = true;

	saudio_desc audioDesc = { };
	audioDesc.num_channels = 2;
	saudio_setup(&audioDesc);

	const int nAudioSampleRate = saudio_sample_rate();
	const int nAudioChannels = saudio_channels();
	assert(nAudioChannels == 2);
	assert(nAudioSampleRate == 44100);
}

//! end audio so call once at the end of program
void CAudioMixer_Sokol::AudioDeviceClose()
{
	if (gs_pAudioState->m_bAudioEnabled)
	{
		TurnOffAudioMixer();
		gs_pAudioState->Deallocate();
		saudio_shutdown();
	}

	delete gs_pAudioState;
}

static CAudio_Sokol* CreateAudioFromSource(const CAudioWAV& source, bool bLoop, int nVolume)
{
	if (!gs_pAudioState->m_bAudioEnabled)
	{
		return nullptr;
	}

	std::map<std::string, CAudio_Sokol*>::iterator iFind = gs_pAudioState->m_mapAudioCache.find(source.m_szId);
	if (iFind != gs_pAudioState->m_mapAudioCache.end())
	{
		return iFind->second;
	}

	CAudio_Sokol* pNewAudio = new CAudio_Sokol;
	pNewAudio->m_pNext = nullptr;
	pNewAudio->m_bLoop = bLoop;
	pNewAudio->m_nVolume = nVolume;

	if (source.m_pBuffer != nullptr)
	{
		// WAV audio samples
		pNewAudio->m_pBufferTrue = (int16_t*)source.m_pBufferWav;
		pNewAudio->m_nLengthTrue = source.m_nSizeWav;
	}
	else
	{
		// todo - read from file
		// pRW = SDL_RWFromFile(source.m_szFilename.c_str(), "rb");
		assert(false);
		pNewAudio->m_bFree = true;
	}

	pNewAudio->SetToBegin();
	gs_pAudioState->m_mapAudioCache[source.m_szId] = pNewAudio;
	return pNewAudio;
}

//! load audio from memory
CAudio* CAudioMixer_Sokol::CreateAudioFromMemory(std::string szId, const unsigned char* pBuffer, int nBufferSize, bool bLoop, int nVolume)
{
	CAudioWAV source;
	source.m_szId = szId;
	source.m_pBuffer = (void*)pBuffer;
	source.m_nBufferSize = nBufferSize;
	const bool bValidWAV = source.LoadWAV();
	assert(bValidWAV);
	return CreateAudioFromSource(source, bLoop, nVolume);
}

//! load audio from a file
CAudio* CAudioMixer_Sokol::CreateAudioFromFile(std::string szFilename, bool bLoop, int nVolume)
{
	CAudioWAV source;
	source.m_szId = szFilename;
	source.m_szFilename = szFilename;
	return CreateAudioFromSource(source, bLoop, nVolume);
}

void CAudioMixer_Sokol::PushAudioToDeviceQueue()
{
	const int nAudioFramesToFill = saudio_expect();
	if (nAudioFramesToFill <= 0)
	{
		return;
	}

	// wav file with 2 channels and 16-bit values
	constexpr int nChannels = 2;
	constexpr int nWavSingleFrameSize = (sizeof(int16_t) * nChannels);

	if (gs_pAudioState->m_mix.m_final == nullptr)
	{
		gs_pAudioState->m_mix.AllocateWav16(nAudioFramesToFill);
	}
	assert(nAudioFramesToFill <= gs_pAudioState->m_mix.m_nAllocatedSizeInFrames);
	gs_pAudioState->m_mix.Reset();

	CAudio_Sokol* pAudio = gs_pAudioState->m_root;
	CAudio_Sokol* pPreviousAudio = pAudio;
	bool bMusicFade = false;

	// first one is a place holder
	pAudio = pAudio->m_pNext;

	while (pAudio != nullptr)
	{
		if (pAudio->m_nLength > 0)
		{
			if (pAudio->m_bLoop)
			{
				if (gs_pAudioState->m_bIsMusicOn)
				{
					// play music loop so play the audio with a wrap around (seamless loop)
					int nFramesLeft = (pAudio->m_nLength / nWavSingleFrameSize);
					int nWrapFramesLeft = nAudioFramesToFill;

					while (nFramesLeft <= nAudioFramesToFill)
					{
						// remaining audio does not fill the entire stream asked for so start with the present remainder
						gs_pAudioState->m_mix.AddSamplesWav16(pAudio->m_pBuffer, nFramesLeft);
						nWrapFramesLeft -= nFramesLeft;
						pAudio->SetToBegin();
						nFramesLeft = (pAudio->m_nLength / nWavSingleFrameSize);
					}

					// finish filling the stream asked for
					gs_pAudioState->m_mix.AddSamplesWav16(pAudio->m_pBuffer, nWrapFramesLeft);
					pAudio->AdvanceFrames(nWrapFramesLeft);
				}
			}
			else
			{
				// play as much of the sound length as we have (play a sound use case)
				const int nFramesLeft = (pAudio->m_nLength / nWavSingleFrameSize);
				const int nPlayFrames = (nAudioFramesToFill > nFramesLeft) ? nFramesLeft : nAudioFramesToFill;
				gs_pAudioState->m_mix.AddSamplesWav16(pAudio->m_pBuffer, nPlayFrames);
				pAudio->AdvanceFrames(nPlayFrames);
			}

			pPreviousAudio = pAudio;
			pAudio = pAudio->m_pNext;
			gs_pAudioState->m_mix.NextSource();
		}
		else if (pAudio->m_bLoop)
		{
			// reset music loop audio when empty
			pAudio->m_pBuffer = pAudio->m_pBufferTrue;
			pAudio->m_nLength = pAudio->m_nLengthTrue;
		}
		else
		{
			// reached the end of this sound playing
			pPreviousAudio->m_pNext = pAudio->m_pNext;
			if (pAudio->m_bSoundCount)
			{
				gs_pAudioState->m_nSoundCount--;
			}

			pAudio->m_pNext = nullptr;
			CAudioMixerState_Sokol::DeallocateAudioList(pAudio);
			pAudio = pPreviousAudio->m_pNext;
		}
	}

	if (gs_pAudioState->m_mix.m_nFinalFrames > 0)
	{
		// push the mixed audio sample to the sokol audio system
		gs_pAudioState->m_mix.MixSourcesWav16();
		saudio_push(gs_pAudioState->m_mix.m_final, gs_pAudioState->m_mix.m_nFinalFrames);
	}
}

//! deallocate audio list
void CAudioMixerState_Sokol::DeallocateAudioList(CAudio_Sokol* pAudio)
{
	while (pAudio != nullptr)
	{
		CAudio* pDeallocateAudio = pAudio;
		pAudio = pAudio->m_pNext;
		delete pDeallocateAudio;
	}
}

void CAudioMixerState_Sokol::Deallocate()
{
	// deallocate cache of audio
	for (auto it : m_mapAudioCache)
	{
		CAudio_Sokol* pAudio = it.second;
		DeallocateAudioList(pAudio);
	}
	m_mapAudioCache.clear();

	// deallocate playing copies
	DeallocateAudioList(m_root);

	m_mix.Deallocate();
}
