//
// libtgvoip is free and unencumbered public domain software.
// For more information, see http://unlicense.org or the UNLICENSE file
// you should have received with this source code distribution.
//


#include <assert.h>
#include "AudioOutputWASAPI.h"
#include "../../logging.h"
#include "../../VoIPController.h"

#define BUFFER_SIZE 960
#define CHECK_RES(res, msg) {if(FAILED(res)){LOGE("%s failed: HRESULT=0x%08X", msg, res); failed=true; return;}}
#define SCHECK_RES(res, msg) {if(FAILED(res)){LOGE("%s failed: HRESULT=0x%08X", msg, res); return;}}

template <class T> void SafeRelease(T **ppT)
{
	if(*ppT)
	{
		(*ppT)->Release();
		*ppT = NULL;
	}
}

#ifdef TGVOIP_WINXP_COMPAT

#endif

using namespace tgvoip::audio;

AudioOutputWASAPI::AudioOutputWASAPI(std::string deviceID){
	isPlaying=false;
	remainingDataLen=0;
	refCount=1;
	HRESULT res;
	res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
	if(FAILED(res) && res!=RPC_E_CHANGED_MODE){
		CHECK_RES(res, "CoInitializeEx");
	}
#ifdef TGVOIP_WINXP_COMPAT
	HANDLE (WINAPI *__CreateEventExA)(LPSECURITY_ATTRIBUTES lpEventAttributes, LPCSTR lpName, DWORD dwFlags, DWORD dwDesiredAccess);
	__CreateEventExA=(HANDLE (WINAPI *)(LPSECURITY_ATTRIBUTES, LPCSTR, DWORD, DWORD))GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateEventExA");
#undef CreateEventEx
#define CreateEventEx __CreateEventExA
#endif
	shutdownEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
	audioSamplesReadyEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
	streamSwitchEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
	ZeroMemory(&format, sizeof(format));
	format.wFormatTag=WAVE_FORMAT_PCM;
	format.nChannels=1;
	format.nSamplesPerSec=48000;
	format.nBlockAlign=2;
	format.nAvgBytesPerSec=format.nSamplesPerSec*format.nBlockAlign;
	format.wBitsPerSample=16;

#ifdef TGVOIP_WINDOWS_DESKTOP
	res=CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&enumerator));
	CHECK_RES(res, "CoCreateInstance(MMDeviceEnumerator)");
	res=enumerator->RegisterEndpointNotificationCallback(this);
	CHECK_RES(res, "enumerator->RegisterEndpointNotificationCallback");
	audioSessionControl=NULL;
	device=NULL;
#endif

	audioClient=NULL;
	renderClient=NULL;
	thread=NULL;

	SetCurrentDevice(deviceID);
}

AudioOutputWASAPI::~AudioOutputWASAPI(){
	if(audioClient){
		audioClient->Stop();
	}

#ifdef TGVOIP_WINDOWS_DESKTOP
	if(audioSessionControl){
		audioSessionControl->UnregisterAudioSessionNotification(this);
	}
#endif

	SetEvent(shutdownEvent);
	if(thread){
		WaitForSingleObjectEx(thread, INFINITE, false);
		CloseHandle(thread);
	}
	SafeRelease(&renderClient);
	SafeRelease(&audioClient);
#ifdef TGVOIP_WINDOWS_DESKTOP
	SafeRelease(&device);
	SafeRelease(&audioSessionControl);
#endif
	CloseHandle(shutdownEvent);
	CloseHandle(audioSamplesReadyEvent);
	CloseHandle(streamSwitchEvent);
#ifdef TGVOIP_WINDOWS_DESKTOP
	if(enumerator)
		enumerator->UnregisterEndpointNotificationCallback(this);
	SafeRelease(&enumerator);
#endif
}

void AudioOutputWASAPI::Start(){
	isPlaying=true;
	if(!thread){
		thread=CreateThread(NULL, 0, AudioOutputWASAPI::StartThread, this, 0, NULL);
	}
}

void AudioOutputWASAPI::Stop(){
	isPlaying=false;
}

bool AudioOutputWASAPI::IsPlaying(){
	return isPlaying;
}

void AudioOutputWASAPI::EnumerateDevices(std::vector<tgvoip::AudioOutputDevice>& devs){
#ifdef TGVOIP_WINDOWS_DESKTOP
	HRESULT res;
	res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
	if(FAILED(res) && res!=RPC_E_CHANGED_MODE){
		SCHECK_RES(res, "CoInitializeEx");
	}

	IMMDeviceEnumerator *deviceEnumerator = NULL;
	IMMDeviceCollection *deviceCollection = NULL;

	res=CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator));
	SCHECK_RES(res, "CoCreateInstance(MMDeviceEnumerator)");

	res=deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &deviceCollection);
	SCHECK_RES(res, "EnumAudioEndpoints");

	UINT devCount;
	res=deviceCollection->GetCount(&devCount);
	SCHECK_RES(res, "GetCount");

	for(UINT i=0;i<devCount;i++){
		IMMDevice* device;
		res=deviceCollection->Item(i, &device);
		SCHECK_RES(res, "GetDeviceItem");
		wchar_t* devID;
		res=device->GetId(&devID);
		SCHECK_RES(res, "get device id");

		IPropertyStore* propStore;
		res=device->OpenPropertyStore(STGM_READ, &propStore);
		SafeRelease(&device);
		SCHECK_RES(res, "OpenPropertyStore");

		PROPVARIANT friendlyName;
		PropVariantInit(&friendlyName);
		res=propStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
		SafeRelease(&propStore);

		AudioOutputDevice dev;

		wchar_t actualFriendlyName[128];
		if(friendlyName.vt==VT_LPWSTR){
			wcsncpy(actualFriendlyName, friendlyName.pwszVal, sizeof(actualFriendlyName)/sizeof(wchar_t));
		}else{
			wcscpy(actualFriendlyName, L"Unknown");
		}
		PropVariantClear(&friendlyName);

		char buf[256];
		WideCharToMultiByte(CP_UTF8, 0, devID, -1, buf, sizeof(buf), NULL, NULL);
		dev.id=buf;
		WideCharToMultiByte(CP_UTF8, 0, actualFriendlyName, -1, buf, sizeof(buf), NULL, NULL);
		dev.displayName=buf;
		devs.push_back(dev);

		CoTaskMemFree(devID);
	}

	SafeRelease(&deviceCollection);
	SafeRelease(&deviceEnumerator);
#endif
}

void AudioOutputWASAPI::SetCurrentDevice(std::string deviceID){
	if(thread){
		streamChangeToDevice=deviceID;
		SetEvent(streamSwitchEvent);
	}else{
		ActuallySetCurrentDevice(deviceID);
	}
}

void AudioOutputWASAPI::ActuallySetCurrentDevice(std::string deviceID){
	currentDevice=deviceID;
	HRESULT res;

	if(audioClient){
		res=audioClient->Stop();
		CHECK_RES(res, "audioClient->Stop");
	}

#ifdef TGVOIP_WINDOWS_DESKTOP
	if(audioSessionControl){
		res=audioSessionControl->UnregisterAudioSessionNotification(this);
		CHECK_RES(res, "audioSessionControl->UnregisterAudioSessionNotification");
	}

	SafeRelease(&audioSessionControl);
#endif
	SafeRelease(&renderClient);
	SafeRelease(&audioClient);
#ifdef TGVOIP_WINDOWS_DESKTOP
	SafeRelease(&device);


	if(deviceID=="default"){
		isDefaultDevice=true;
		res=enumerator->GetDefaultAudioEndpoint(eRender, eCommunications, &device);
		CHECK_RES(res, "GetDefaultAudioEndpoint");
	}else{
		IMMDeviceCollection *deviceCollection = NULL;

		isDefaultDevice=false;
		res=enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &deviceCollection);
		CHECK_RES(res, "EnumAudioEndpoints");

		UINT devCount;
		res=deviceCollection->GetCount(&devCount);
		CHECK_RES(res, "GetCount");

		for(UINT i=0;i<devCount;i++){
			IMMDevice* device;
			res=deviceCollection->Item(i, &device);
			CHECK_RES(res, "GetDeviceItem");
			wchar_t* _devID;
			res=device->GetId(&_devID);
			CHECK_RES(res, "get device id");

			char devID[128];
			WideCharToMultiByte(CP_UTF8, 0, _devID, -1, devID, 128, NULL, NULL);

			CoTaskMemFree(_devID);
			if(deviceID==devID){
				this->device=device;
				break;
			}
		}
		if(deviceCollection)
			SafeRelease(&deviceCollection);
		if (!device) {
			LOGW("Requested device not found, using default");
			ActuallySetCurrentDevice("default");
			return;
		}
	}

	if(!device){
		LOGE("Didn't find playback device; failing");
		failed=true;
		return;
	}

	res=device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, (void**)&audioClient);
	CHECK_RES(res, "device->Activate");
#else
	std::wstring devID;

	if (deviceID=="default"){
		Platform::String^ defaultDevID=Windows::Media::Devices::MediaDevice::GetDefaultAudioRenderId(Windows::Media::Devices::AudioDeviceRole::Communications);
		if(defaultDevID==nullptr){
			LOGE("Didn't find playback device; failing");
			failed=true;
			return;
		}else{
			isDefaultDevice=true;
			devID=defaultDevID->Data();
		}
	}else{
		int wchars_num=MultiByteToWideChar(CP_UTF8, 0, deviceID.c_str(), -1, NULL, 0);
		wchar_t* wstr=new wchar_t[wchars_num];
		MultiByteToWideChar(CP_UTF8, 0, deviceID.c_str(), -1, wstr, wchars_num);
		devID=wstr;
	}

	HRESULT res1, res2;
	IAudioClient2* audioClient2=WindowsSandboxUtils::ActivateAudioDevice(devID.c_str(), &res1, &res2);
	CHECK_RES(res1, "activate1");
	CHECK_RES(res2, "activate2");

	AudioClientProperties properties={};
	properties.cbSize=sizeof AudioClientProperties;
	properties.eCategory=AudioCategory_Communications;
	res = audioClient2->SetClientProperties(&properties);
	CHECK_RES(res, "audioClient2->SetClientProperties");

	audioClient = audioClient2;
#endif

	// {2C693079-3F59-49FD-964F-61C005EAA5D3}
	const GUID guid = { 0x2c693079, 0x3f59, 0x49fd, { 0x96, 0x4f, 0x61, 0xc0, 0x5, 0xea, 0xa5, 0xd3 } };
	res = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, 60 * 10000, 0, &format, &guid);
	CHECK_RES(res, "audioClient->Initialize");

	uint32_t bufSize;
	res = audioClient->GetBufferSize(&bufSize);
	CHECK_RES(res, "audioClient->GetBufferSize");

	LOGV("buffer size: %u", bufSize);
	estimatedDelay=0;
	REFERENCE_TIME latency, devicePeriod;
	if(SUCCEEDED(audioClient->GetStreamLatency(&latency))){
		if(SUCCEEDED(audioClient->GetDevicePeriod(&devicePeriod, NULL))){
			estimatedDelay=(int32_t)(latency/10000+devicePeriod/10000);
		}
	}

	res = audioClient->SetEventHandle(audioSamplesReadyEvent);
	CHECK_RES(res, "audioClient->SetEventHandle");

	res = audioClient->GetService(IID_PPV_ARGS(&renderClient));
	CHECK_RES(res, "audioClient->GetService");

	BYTE* data;
	res = renderClient->GetBuffer(bufSize, &data);
	CHECK_RES(res, "renderClient->GetBuffer");

	res = renderClient->ReleaseBuffer(bufSize, AUDCLNT_BUFFERFLAGS_SILENT);
	CHECK_RES(res, "renderClient->ReleaseBuffer");

#ifdef TGVOIP_WINDOWS_DESKTOP
	res=audioClient->GetService(IID_PPV_ARGS(&audioSessionControl));
	CHECK_RES(res, "audioClient->GetService(IAudioSessionControl)");

	res=audioSessionControl->RegisterAudioSessionNotification(this);
	CHECK_RES(res, "audioSessionControl->RegisterAudioSessionNotification");
#endif

	audioClient->Start();

	LOGV("set current output device done");
}

DWORD WINAPI AudioOutputWASAPI::StartThread(void* arg) {
	((AudioOutputWASAPI*)arg)->RunThread();
	return 0;
}

void AudioOutputWASAPI::RunThread() {
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);

	HANDLE waitArray[]={shutdownEvent, streamSwitchEvent, audioSamplesReadyEvent};
	HRESULT res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
	CHECK_RES(res, "CoInitializeEx in render thread");

	uint32_t bufferSize;
	res=audioClient->GetBufferSize(&bufferSize);
	CHECK_RES(res, "audioClient->GetBufferSize");
	uint64_t framesWritten=0;

	bool running=true;
	//double prevCallback=VoIPController::GetCurrentTime();

	while(running){
		DWORD waitResult=WaitForMultipleObjectsEx(3, waitArray, false, INFINITE, false);
		if(waitResult==WAIT_OBJECT_0){ // shutdownEvent
			LOGV("render thread shutting down");
			running=false;
		}else if(waitResult==WAIT_OBJECT_0+1){ // streamSwitchEvent
			LOGV("stream switch");
			ActuallySetCurrentDevice(streamChangeToDevice);
			ResetEvent(streamSwitchEvent);
			LOGV("stream switch done");
		}else if(waitResult==WAIT_OBJECT_0+2){ // audioSamplesReadyEvent
			if(!audioClient)
				continue;

			BYTE* data;
			uint32_t padding;
			uint32_t framesAvailable;
			res=audioClient->GetCurrentPadding(&padding);
			CHECK_RES(res, "audioClient->GetCurrentPadding");
			framesAvailable=bufferSize-padding;
			res=renderClient->GetBuffer(framesAvailable, &data);
			CHECK_RES(res, "renderClient->GetBuffer");

			//double t=VoIPController::GetCurrentTime();
			//LOGV("framesAvail: %u, time: %f, isPlaying: %d", framesAvailable, t-prevCallback, isPlaying);
			//prevCallback=t;

			size_t bytesAvailable=framesAvailable*2;
			while(bytesAvailable>remainingDataLen){
				if(isPlaying){
					InvokeCallback(remainingData+remainingDataLen, 960*2);
				}else{
					memset(remainingData+remainingDataLen, 0, 960*2);
				}
				remainingDataLen+=960*2;
			}
			memcpy(data, remainingData, bytesAvailable);
			if(remainingDataLen>bytesAvailable){
				memmove(remainingData, remainingData+bytesAvailable, remainingDataLen-bytesAvailable);
			}
			remainingDataLen-=bytesAvailable;

			res=renderClient->ReleaseBuffer(framesAvailable, 0);
			CHECK_RES(res, "renderClient->ReleaseBuffer");
			framesWritten+=framesAvailable;
		}
	}
}

#ifdef TGVOIP_WINDOWS_DESKTOP
HRESULT AudioOutputWASAPI::OnSessionDisconnected(AudioSessionDisconnectReason reason) {
	if(!isDefaultDevice){
		streamChangeToDevice="default";
		SetEvent(streamSwitchEvent);
	}
	return S_OK;
}

HRESULT AudioOutputWASAPI::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR newDevID) {
	if(flow==eRender && role==eCommunications && isDefaultDevice){
		streamChangeToDevice="default";
		SetEvent(streamSwitchEvent);
	}
	return S_OK;
}

ULONG AudioOutputWASAPI::AddRef(){
	return InterlockedIncrement(&refCount);
}

ULONG AudioOutputWASAPI::Release(){
	return InterlockedDecrement(&refCount);
}

HRESULT AudioOutputWASAPI::QueryInterface(REFIID iid, void** obj){
	if(!obj){
		return E_POINTER;
	}
	*obj=NULL;

	if(iid==IID_IUnknown){
		*obj=static_cast<IUnknown*>(static_cast<IAudioSessionEvents*>(this));
		AddRef();
	}else if(iid==__uuidof(IMMNotificationClient)){
		*obj=static_cast<IMMNotificationClient*>(this);
		AddRef();
	}else if(iid==__uuidof(IAudioSessionEvents)){
		*obj=static_cast<IAudioSessionEvents*>(this);
		AddRef();
	}else{
		return E_NOINTERFACE;
	}

	return S_OK;
}
#endif