
#include "wf_wasapi.h"
#include "wf_info.h"

#include <initguid.h>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys_devpkey.h>
#include <audioclient.h>

#include <freerdp/log.h>
#define TAG SERVER_TAG("windows")

//#define REFTIMES_PER_SEC  10000000
//#define REFTIMES_PER_MILLISEC  10000

#define REFTIMES_PER_SEC 100000
#define REFTIMES_PER_MILLISEC 100

//#define REFTIMES_PER_SEC  50000
//#define REFTIMES_PER_MILLISEC  50

#ifndef __MINGW32__
DEFINE_GUID(CLSID_MMDeviceEnumerator, 0xBCDE0395, 0xE52F, 0x467C, 0x8E, 0x3D, 0xC4, 0x57, 0x92,
            0x91, 0x69, 0x2E);
DEFINE_GUID(IID_IMMDeviceEnumerator, 0xA95664D2, 0x9614, 0x4F35, 0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36,
            0x17, 0xE6);
DEFINE_GUID(IID_IAudioClient, 0x1cb9ad4c, 0xdbfa, 0x4c32, 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03,
            0xb2);
DEFINE_GUID(IID_IAudioCaptureClient, 0xc8adbd64, 0xe71e, 0x48a0, 0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c,
            0xd3, 0x17);
#endif

LPWSTR devStr = nullptr;
wfPeerContext* latestPeer = nullptr;

int wf_rdpsnd_set_latest_peer(wfPeerContext* peer)
{
	latestPeer = peer;
	return 0;
}

int wf_wasapi_activate(RdpsndServerContext* context)
{
	wchar_t* pattern = L"Stereo Mix";
	HANDLE hThread;

	wf_wasapi_get_device_string(pattern, &devStr);

	if (devStr == nullptr)
	{
		WLog_ERR(TAG, "Failed to match for output device! Disabling rdpsnd.");
		return 1;
	}

	WLog_DBG(TAG, "RDPSND (WASAPI) Activated");
	if (!(hThread = CreateThread(nullptr, 0, wf_rdpsnd_wasapi_thread, latestPeer, 0, nullptr)))
	{
		WLog_ERR(TAG, "CreateThread failed");
		return 1;
	}
	(void)CloseHandle(hThread);

	return 0;
}

int wf_wasapi_get_device_string(LPWSTR pattern, LPWSTR* deviceStr)
{
	HRESULT hr;
	IMMDeviceEnumerator* pEnumerator = nullptr;
	IMMDeviceCollection* pCollection = nullptr;
	IMMDevice* pEndpoint = nullptr;
	IPropertyStore* pProps = nullptr;
	LPWSTR pwszID = nullptr;
	unsigned int count;

	CoInitialize(nullptr);
	hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, &IID_IMMDeviceEnumerator,
	                      (void**)&pEnumerator);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to cocreate device enumerator");
		exit(1);
	}

	hr = pEnumerator->lpVtbl->EnumAudioEndpoints(pEnumerator, eCapture, DEVICE_STATE_ACTIVE,
	                                             &pCollection);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to create endpoint collection");
		exit(1);
	}

	pCollection->lpVtbl->GetCount(pCollection, &count);
	WLog_INFO(TAG, "Num endpoints: %u", count);

	if (count == 0)
	{
		WLog_ERR(TAG, "No endpoints!");
		exit(1);
	}

	for (unsigned int i = 0; i < count; ++i)
	{
		PROPVARIANT nameVar;
		PropVariantInit(&nameVar);

		hr = pCollection->lpVtbl->Item(pCollection, i, &pEndpoint);
		if (FAILED(hr))
		{
			WLog_ERR(TAG, "Failed to get endpoint %u", i);
			exit(1);
		}

		hr = pEndpoint->lpVtbl->GetId(pEndpoint, &pwszID);
		if (FAILED(hr))
		{
			WLog_ERR(TAG, "Failed to get endpoint ID");
			exit(1);
		}

		hr = pEndpoint->lpVtbl->OpenPropertyStore(pEndpoint, STGM_READ, &pProps);
		if (FAILED(hr))
		{
			WLog_ERR(TAG, "Failed to open property store");
			exit(1);
		}

		hr = pProps->lpVtbl->GetValue(pProps, &PKEY_Device_FriendlyName, &nameVar);
		if (FAILED(hr))
		{
			WLog_ERR(TAG, "Failed to get device friendly name");
			exit(1);
		}

		// do this a more reliable way
		if (wcscmp(pattern, nameVar.pwszVal) < 0)
		{
			unsigned int devStrLen;
			WLog_INFO(TAG, "Using sound output endpoint: [%s] (%s)", nameVar.pwszVal, pwszID);
			// WLog_INFO(TAG, "matched %d characters", wcscmp(pattern, nameVar.pwszVal);
			devStrLen = wcslen(pwszID);
			*deviceStr = (LPWSTR)calloc(devStrLen + 1, 2);
			if (!deviceStr)
				return -1;
			wcscpy_s(*deviceStr, devStrLen + 1, pwszID);
		}
		CoTaskMemFree(pwszID);
		pwszID = nullptr;
		PropVariantClear(&nameVar);

		pProps->lpVtbl->Release(pProps);
		pProps = nullptr;

		pEndpoint->lpVtbl->Release(pEndpoint);
		pEndpoint = nullptr;
	}

	pCollection->lpVtbl->Release(pCollection);
	pCollection = nullptr;

	pEnumerator->lpVtbl->Release(pEnumerator);
	pEnumerator = nullptr;
	CoUninitialize();

	return 0;
}

DWORD WINAPI wf_rdpsnd_wasapi_thread(LPVOID lpParam)
{
	IMMDeviceEnumerator* pEnumerator = nullptr;
	IMMDevice* pDevice = nullptr;
	IAudioClient* pAudioClient = nullptr;
	IAudioCaptureClient* pCaptureClient = nullptr;
	WAVEFORMATEX* pwfx = nullptr;
	HRESULT hr;
	REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC;
	REFERENCE_TIME hnsActualDuration;
	UINT32 bufferFrameCount;
	UINT32 numFramesAvailable;
	UINT32 packetLength = 0;
	UINT32 dCount = 0;
	BYTE* pData;

	wfPeerContext* context;
	wfInfo* wfi;

	wfi = wf_info_get_instance();
	context = (wfPeerContext*)lpParam;

	CoInitialize(nullptr);
	hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, &IID_IMMDeviceEnumerator,
	                      (void**)&pEnumerator);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to cocreate device enumerator");
		exit(1);
	}

	hr = pEnumerator->lpVtbl->GetDevice(pEnumerator, devStr, &pDevice);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to cocreate get device");
		exit(1);
	}

	hr = pDevice->lpVtbl->Activate(pDevice, &IID_IAudioClient, CLSCTX_ALL, nullptr,
	                               (void**)&pAudioClient);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to activate audio client");
		exit(1);
	}

	hr = pAudioClient->lpVtbl->GetMixFormat(pAudioClient, &pwfx);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to get mix format");
		exit(1);
	}

	pwfx->wFormatTag = wfi->agreed_format->wFormatTag;
	pwfx->nChannels = wfi->agreed_format->nChannels;
	pwfx->nSamplesPerSec = wfi->agreed_format->nSamplesPerSec;
	pwfx->nAvgBytesPerSec = wfi->agreed_format->nAvgBytesPerSec;
	pwfx->nBlockAlign = wfi->agreed_format->nBlockAlign;
	pwfx->wBitsPerSample = wfi->agreed_format->wBitsPerSample;
	pwfx->cbSize = wfi->agreed_format->cbSize;

	hr = pAudioClient->lpVtbl->Initialize(pAudioClient, AUDCLNT_SHAREMODE_SHARED, 0,
	                                      hnsRequestedDuration, 0, pwfx, nullptr);

	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to initialize the audio client");
		exit(1);
	}

	hr = pAudioClient->lpVtbl->GetBufferSize(pAudioClient, &bufferFrameCount);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to get buffer size");
		exit(1);
	}

	hr = pAudioClient->lpVtbl->GetService(pAudioClient, &IID_IAudioCaptureClient,
	                                      (void**)&pCaptureClient);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to get the capture client");
		exit(1);
	}

	hnsActualDuration = (UINT32)REFTIMES_PER_SEC * bufferFrameCount / pwfx->nSamplesPerSec;

	hr = pAudioClient->lpVtbl->Start(pAudioClient);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to start capture");
		exit(1);
	}

	dCount = 0;

	while (wfi->snd_stop == FALSE)
	{
		DWORD flags;

		Sleep(hnsActualDuration / REFTIMES_PER_MILLISEC / 2);

		hr = pCaptureClient->lpVtbl->GetNextPacketSize(pCaptureClient, &packetLength);
		if (FAILED(hr))
		{
			WLog_ERR(TAG, "Failed to get packet length");
			exit(1);
		}

		while (packetLength != 0)
		{
			hr = pCaptureClient->lpVtbl->GetBuffer(pCaptureClient, &pData, &numFramesAvailable,
			                                       &flags, nullptr, nullptr);
			if (FAILED(hr))
			{
				WLog_ERR(TAG, "Failed to get buffer");
				exit(1);
			}

			// Here we are writing the audio data
			// not sure if this flag is ever set by the system; msdn is not clear about it
			if (!(flags & AUDCLNT_BUFFERFLAGS_SILENT))
				context->rdpsnd->SendSamples(context->rdpsnd, pData, packetLength,
				                             (UINT16)(GetTickCount() & 0xffff));

			hr = pCaptureClient->lpVtbl->ReleaseBuffer(pCaptureClient, numFramesAvailable);
			if (FAILED(hr))
			{
				WLog_ERR(TAG, "Failed to release buffer");
				exit(1);
			}

			hr = pCaptureClient->lpVtbl->GetNextPacketSize(pCaptureClient, &packetLength);
			if (FAILED(hr))
			{
				WLog_ERR(TAG, "Failed to get packet length");
				exit(1);
			}
		}
	}

	pAudioClient->lpVtbl->Stop(pAudioClient);
	if (FAILED(hr))
	{
		WLog_ERR(TAG, "Failed to stop audio client");
		exit(1);
	}

	CoTaskMemFree(pwfx);

	if (pEnumerator != nullptr)
		pEnumerator->lpVtbl->Release(pEnumerator);

	if (pDevice != nullptr)
		pDevice->lpVtbl->Release(pDevice);

	if (pAudioClient != nullptr)
		pAudioClient->lpVtbl->Release(pAudioClient);

	if (pCaptureClient != nullptr)
		pCaptureClient->lpVtbl->Release(pCaptureClient);

	CoUninitialize();

	return 0;
}
