#ifdef _WIN32
#include "WasapiAudioPlayer.h"
#include "spdlog/spdlog.h"

// These constants are currently missing from the MinGW headers.
#ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
# define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM  0x80000000
#endif
#ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY
# define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY  0x08000000
#endif

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);

namespace Ship {
	void WasapiAudioPlayer::ThrowIfFailed(HRESULT res) {
		if (FAILED(res)) {
			throw res;
		}
	}

	bool WasapiAudioPlayer::SetupStream(void) {
		try {
			ThrowIfFailed(DeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device));
			ThrowIfFailed(device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, IID_PPV_ARGS_Helper(&client)));

			WAVEFORMATEX desired;
			desired.wFormatTag = WAVE_FORMAT_PCM;
			desired.nChannels = 2;
			desired.nSamplesPerSec  = this->GetSampleRate();
			desired.nAvgBytesPerSec = desired.nSamplesPerSec * 2 * 2;
			desired.nBlockAlign = 4;
			desired.wBitsPerSample = 16;
			desired.cbSize = 0;

			ThrowIfFailed(client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, 2000000, 0, &desired, nullptr));

			ThrowIfFailed(client->GetBufferSize(&bufferFrameCount));
			ThrowIfFailed(client->GetService(IID_PPV_ARGS(&rclient)));

			started = false;
			initialized = true;
		}
		catch (HRESULT res) {
			return false;
		}

		return true;
	}

	bool WasapiAudioPlayer::Init(void) {
		try {
			ThrowIfFailed(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&DeviceEnumerator)));
		}
		catch (HRESULT res) {
			return false;
		}

		ThrowIfFailed(DeviceEnumerator->RegisterEndpointNotificationCallback(this));

		return true;
	}

	int WasapiAudioPlayer::Buffered(void) {
		if (!initialized) {
			if (!SetupStream()) {
				return 0;
			}
		}
		try {
			UINT32 padding;
			ThrowIfFailed(client->GetCurrentPadding(&padding));
			return padding;
		}
		catch (HRESULT res) {
			return 0;
		}
	}

	int WasapiAudioPlayer::GetDesiredBuffered(void) {
		return 2480;
	}

	void WasapiAudioPlayer::Play(const uint8_t* Buffer, uint32_t BufferLen) {
		if (!initialized) {
			if (!SetupStream()) {
				return;
			}
		}
		try {
			UINT32 frames = BufferLen / 4;

			UINT32 padding;
			ThrowIfFailed(client->GetCurrentPadding(&padding));

			UINT32 available = bufferFrameCount - padding;
			if (available < frames) {
				frames = available;
			}
			if (available == 0) {
				return;
			}

			BYTE* data;
			ThrowIfFailed(rclient->GetBuffer(frames, &data));
			memcpy(data, Buffer, frames * 4);
			ThrowIfFailed(rclient->ReleaseBuffer(frames, 0));

			if (!started && padding + frames > 1500) {
				started = true;
				ThrowIfFailed(client->Start());
			}
		}
		catch (HRESULT res) {
		}
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) {
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::OnDeviceAdded(LPCWSTR pwstrDeviceId) {
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::OnDeviceRemoved(LPCWSTR pwstrDeviceId) {
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) {
		if (flow == eRender && role == eConsole) {
			// This callback runs on a separate thread,
			// but it's not important how fast this write takes effect.
			initialized = false;
		}
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) {
		return S_OK;
	}

	ULONG STDMETHODCALLTYPE WasapiAudioPlayer::AddRef() {
		return InterlockedIncrement(&refcount);
	}

	ULONG STDMETHODCALLTYPE WasapiAudioPlayer::Release() {
		ULONG rc = InterlockedDecrement(&refcount);
		if (rc == 0) {
			delete this;
		}
		return rc;
	}

	HRESULT STDMETHODCALLTYPE WasapiAudioPlayer::QueryInterface(REFIID riid, VOID** ppvInterface) {
		if (riid == __uuidof(IUnknown)) {
			AddRef();
			*ppvInterface = (IUnknown*)this;
		}
		else if (riid == __uuidof(IMMNotificationClient)) {
			AddRef();
			*ppvInterface = (IMMNotificationClient*)this;
		}
		else {
			*ppvInterface = nullptr;
			return E_NOINTERFACE;
		}
		return S_OK;
	}
}
#endif
