

//#if (_WIN32_WINNT >= 0x0602 /*_WIN32_WINNT_WIN8*/)

#include <xaudio2.h>

#ifndef XAUDIO2_DLL
#error wrong <xaudio2.h> included!
#endif

#include "XAudio2API.h"
#include "util/helpers/helpers.h"

#include <WbemCli.h>
#include <OleAuto.h>
#include <system_error>

// guid from mmdeviceapi.h
static const GUID DEVINTERFACE_AUDIO_RENDER_GUID = { 0xe6327cad, 0xdcec, 0x4949, 0xae, 0x8a, 0x99, 0x1e, 0x97, 0x6a, 0x79, 0xd2 };

#pragma comment(lib, "wbemuuid.lib")

static_assert(IAudioAPI::kBlockCount < XAUDIO2_MAX_QUEUED_BUFFERS, "too many xaudio2 buffers");

HMODULE XAudio2API::s_xaudio_dll = nullptr;
std::vector<XAudio2API::DeviceDescriptionPtr> XAudio2API::s_devices;

XAudio2API::XAudio2API(std::wstring device_id, uint32 samplerate, uint32 channels, uint32 samples_per_block, uint32 bits_per_sample)
	: IAudioAPI(samplerate, channels, samples_per_block, bits_per_sample), m_device_id(std::move(device_id))
{
	const auto _XAudio2Create = (decltype(&XAudio2Create))GetProcAddress(s_xaudio_dll, "XAudio2Create");
	if (!_XAudio2Create)
		throw std::runtime_error("can't find XAudio2Create import");

	HRESULT hres;
	IXAudio2* xaudio;
	if (FAILED((hres = _XAudio2Create(&xaudio, 0, XAUDIO2_DEFAULT_PROCESSOR))))
		throw std::runtime_error(fmt::format("can't create xaudio device (hres: {:#x})", hres));

	m_xaudio = decltype(m_xaudio)(xaudio);

	IXAudio2MasteringVoice* mastering_voice;
	if (FAILED((hres = m_xaudio->CreateMasteringVoice(&mastering_voice, channels, samplerate, 0, m_device_id.empty() ? nullptr : m_device_id.c_str()))))
		throw std::runtime_error(fmt::format("can't create xaudio mastering voice (hres: {:#x})", hres));

	m_mastering_voice = decltype(m_mastering_voice)(mastering_voice);

	m_wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
	m_wfx.Format.nChannels = channels;
	m_wfx.Format.nSamplesPerSec = samplerate;
	m_wfx.Format.wBitsPerSample = bits_per_sample;
	m_wfx.Format.nBlockAlign = (m_wfx.Format.nChannels * m_wfx.Format.wBitsPerSample) / 8; // must equal (nChannels × wBitsPerSample) / 8
	m_wfx.Format.nAvgBytesPerSec = m_wfx.Format.nSamplesPerSec * m_wfx.Format.nBlockAlign; // must equal nSamplesPerSec × nBlockAlign.
	m_wfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);

	m_wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
	m_wfx.Samples.wValidBitsPerSample = bits_per_sample;
	switch(channels)
	{
	case 8:
		m_wfx.dwChannelMask |= (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_FRONT_RIGHT_OF_CENTER);
		break;
	case 6:
		m_wfx.dwChannelMask |= (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT);
		break;
	case 4:
		m_wfx.dwChannelMask |= (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT);
		break;
	case 2:
		m_wfx.dwChannelMask |= (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
		break;
	default:
		m_wfx.dwChannelMask = 0;
		break;
	}
	
	IXAudio2SourceVoice* source_voice;
	if (FAILED((hres = m_xaudio->CreateSourceVoice(&source_voice, &m_wfx.Format, 0, 1.0f))))
		throw std::runtime_error(fmt::format("can't create xaudio source voice (hres: {:#x})", hres));

	m_source_voice = decltype(m_source_voice)(source_voice);

	m_sound_buffer_size = kBlockCount * (samples_per_block * channels * (bits_per_sample / 8));

	for (uint32 i = 0; i < kBlockCount; ++i)
		m_audio_buffer[i] = std::make_unique<uint8[]>(m_bytesPerBlock);

	m_xaudio->StartEngine();
}

void XAudio2API::XAudioDeleter::operator()(IXAudio2* ptr) const
{
	if (ptr)
		ptr->Release();
}

void XAudio2API::VoiceDeleter::operator()(IXAudio2Voice* ptr) const
{
	if (ptr)
		ptr->DestroyVoice();
}

XAudio2API::~XAudio2API()
{
	if(m_xaudio)
		m_xaudio->StopEngine();

	XAudio2API::Stop();

	m_source_voice.reset();
	m_mastering_voice.reset();
	m_xaudio.reset();
}

void XAudio2API::SetVolume(sint32 volume)
{
	IAudioAPI::SetVolume(volume);
	m_mastering_voice->SetVolume((float)volume / 100.0f);
}

bool XAudio2API::Play()
{
	if (m_playing)
		return true;

	m_playing = SUCCEEDED(m_source_voice->Start());
	return m_playing;
}

bool XAudio2API::Stop()
{
	if (!m_playing)
		return true;

	m_playing = FAILED(m_source_voice->Stop());
	m_source_voice->FlushSourceBuffers();

	return m_playing;
}

bool XAudio2API::InitializeStatic()
{
	if (s_xaudio_dll)
		return true;

	// win 10
	s_xaudio_dll = LoadLibraryEx(XAUDIO2_DLL, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);

	try
	{
		if (!s_xaudio_dll)
			throw std::exception();

		const auto _XAudio2Create = (decltype(&XAudio2Create))GetProcAddress(s_xaudio_dll, "XAudio2Create");
		if (!_XAudio2Create)
			throw std::exception();

		RefreshDevices();
		return true;
	}
	catch (const std::exception&)
	{
		if (s_xaudio_dll)
			FreeLibrary(s_xaudio_dll);

		return false;
	}
}

void XAudio2API::Destroy()
{
	if (s_xaudio_dll)
		FreeLibrary(s_xaudio_dll);
}

const std::vector<XAudio2API::DeviceDescriptionPtr>& XAudio2API::RefreshDevices()
{
	s_devices.clear();

	try
	{
		struct IWbemLocator *wbem_locator = nullptr;

		HRESULT hres = CoCreateInstance(__uuidof(WbemLocator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IWbemLocator), (LPVOID*)&wbem_locator);
		if (FAILED(hres) || !wbem_locator)
			throw std::system_error(hres, std::system_category());

		std::shared_ptr<OLECHAR> path(SysAllocString(LR"(\\.\root\cimv2)"), SysFreeString);
		std::shared_ptr<OLECHAR> language(SysAllocString(L"WQL"), SysFreeString);
		std::shared_ptr<OLECHAR> query(SysAllocString(LR"(SELECT Name,DeviceID FROM Win32_PNPEntity WHERE PNPClass = "AudioEndpoint")"), SysFreeString);
		std::shared_ptr<OLECHAR> name_row(SysAllocString(L"Name"), SysFreeString);
		std::shared_ptr<OLECHAR> device_id_row(SysAllocString(L"DeviceID"), SysFreeString);

		IWbemServices *wbem_services = nullptr;
		hres = wbem_locator->ConnectServer(path.get(), nullptr, nullptr, nullptr, 0, nullptr, nullptr, &wbem_services);
		wbem_locator->Release(); // Free memory resources.

		if (FAILED(hres) || !wbem_services)
			throw std::system_error(hres, std::system_category());

		hres = CoSetProxyBlanket(wbem_services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE);
		if (FAILED(hres))
			throw std::system_error(hres, std::system_category());

		IEnumWbemClassObject* wbem_enum = nullptr;
		hres = wbem_services->ExecQuery(language.get(), query.get(), WBEM_FLAG_RETURN_WBEM_COMPLETE | WBEM_FLAG_FORWARD_ONLY, nullptr, &wbem_enum);
		if (FAILED(hres) || !wbem_enum)
			throw std::system_error(hres, std::system_category());

		ULONG returned;
		IWbemClassObject* object[20];
		// WBEM_S_TIMEDOUT
		while (SUCCEEDED(hres = wbem_enum->Next(100, 20, object, &returned)) && returned > 0)
		{
			for (ULONG i = 0; i < returned; ++i)
			{
				std::wstring name, device_id;

				VARIANT var;
				if (SUCCEEDED(object[i]->Get(name_row.get(), 0L, &var, NULL, NULL)) && var.vt == VT_BSTR && var.bstrVal)
				{
					name = var.bstrVal;
					if (SUCCEEDED(object[i]->Get(device_id_row.get(), 0L, &var, NULL, NULL)) && var.vt == VT_BSTR && var.bstrVal)
					{
						std::wstring id = var.bstrVal;

						if(id.find(L"{0.0.0.00000000}") == std::wstring::npos)
						{
							object[i]->Release();
							continue;
						}

						std::replace(id.begin(), id.end(), L'\\', L'#'); // xaudio devices have "#" instead of backslashes
						
						std::wstringstream tmp;
						tmp << L"\\\\?\\" << id << L"#{" << WStringFromGUID(DEVINTERFACE_AUDIO_RENDER_GUID) << L"}";
						device_id = tmp.str();
					}
				}
			
				auto device = std::make_shared<XAudio2DeviceDescription>(name,device_id);
				s_devices.emplace_back(device);

				object[i]->Release();
			}
		}

		// Only add default device if audio devices exist
		if (s_devices.size() > 0) {
			auto default_device = std::make_shared<XAudio2DeviceDescription>(L"Primary Sound Driver", L"");
			s_devices.insert(s_devices.begin(), default_device);
		}
		
		wbem_enum->Release();

		// Clean up
		wbem_services->Release();
	}
	catch (const std::system_error& ex)
	{
		cemuLog_log(LogType::Force, "XAudio2API::RefreshDevices: error while refreshing device list ({} - code: 0x{:08x})", ex.what(), ex.code().value());
	}

	CoUninitialize();
	return s_devices;
}

bool XAudio2API::FeedBlock(sint16* data)
{
	// check if we queued too many blocks
	if (m_blocks_queued >= kBlockCount)
	{
		m_blocks_queued = GetQueuedBuffers();

		if (m_blocks_queued >= kBlockCount)
		{
			cemuLog_logDebug(LogType::Force, "dropped xaudio2 block since too many buffers are queued");
			return false;
		}
	}

	memcpy(m_audio_buffer[m_offset].get(), data, m_bytesPerBlock);

	XAUDIO2_BUFFER buffer{};
	buffer.AudioBytes = m_bytesPerBlock;
	buffer.pAudioData = m_audio_buffer[m_offset].get();
	m_source_voice->SubmitSourceBuffer(&buffer);

	m_offset = (m_offset + 1) % kBlockCount;
	m_blocks_queued++;
	return true;
}

uint32 XAudio2API::GetQueuedBuffers() const
{
	XAUDIO2_VOICE_STATE state{};
	m_source_voice->GetState(&state);
	return state.BuffersQueued;
}

bool XAudio2API::NeedAdditionalBlocks() const
{
	return GetQueuedBuffers() < GetAudioDelay();
}
