#ifndef NO_XAUDIO2

// Application
#include "wxvbam.h"
#include <stdio.h>

// Interface
#include "../common/SoundDriver.h"

// XAudio2
#include <XAudio2.h>

// MMDevice API
#include <mmdeviceapi.h>
#include <vector>
#include <string>

// Internals
#include "../System.h" // for systemMessage()
#include "../gba/Globals.h"


class XAudio2_Output;

static void xaudio2_device_changed( XAudio2_Output * );

class XAudio2_Device_Notifier : public IMMNotificationClient
{
	volatile LONG registered;
	IMMDeviceEnumerator *pEnumerator;

	std::wstring last_device;

	CRITICAL_SECTION lock;
	std::vector<XAudio2_Output*> instances;

public:
	XAudio2_Device_Notifier() : registered( 0 )
	{
		InitializeCriticalSection( &lock );
	}
	~XAudio2_Device_Notifier()
	{
		DeleteCriticalSection( &lock );
	}

	ULONG STDMETHODCALLTYPE AddRef()
	{
		return 1;
	}

	ULONG STDMETHODCALLTYPE Release()
	{
		return 1;
	}

	HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, VOID **ppvInterface )
	{
		if (IID_IUnknown == riid)
		{
			*ppvInterface = (IUnknown*)this;
		}
		else if (__uuidof(IMMNotificationClient) == riid)
		{
			*ppvInterface = (IMMNotificationClient*)this;
		}
		else
		{
			*ppvInterface = NULL;
			return E_NOINTERFACE;
		}
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged( EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId )
	{
		if ( flow == eRender && last_device.compare( pwstrDeviceId ) != 0 )
		{
			last_device = pwstrDeviceId;

			EnterCriticalSection( &lock );
			for ( auto it = instances.begin(); it < instances.end(); ++it )
			{
				xaudio2_device_changed( *it );
			}
			LeaveCriticalSection( &lock );
		}

		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE OnDeviceAdded( LPCWSTR pwstrDeviceId ) { return S_OK; }
	HRESULT STDMETHODCALLTYPE OnDeviceRemoved( LPCWSTR pwstrDeviceId ) { return S_OK; }
	HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( LPCWSTR pwstrDeviceId, DWORD dwNewState ) { return S_OK; }
	HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( LPCWSTR pwstrDeviceId, const PROPERTYKEY key ) { return S_OK; }

	void do_register(sound_out_i_xaudio2 * p_instance)
	{
		if ( InterlockedIncrement( &registered ) == 1 )
		{
			pEnumerator = NULL;
			HRESULT hr = CoCreateInstance( __uuidof( MMDeviceEnumerator ), NULL, CLSCTX_INPROC_SERVER, __uuidof( IMMDeviceEnumerator ), ( void** ) &pEnumerator );
			if ( SUCCEEDED( hr ) )
			{
				pEnumerator->RegisterEndpointNotificationCallback( this );
			}
		}

		EnterCriticalSection( &lock );
		instances.push_back( p_instance );
		LeaveCriticalSection( &lock );
	}

	void do_unregister( sound_out_i_xaudio2 * p_instance )
	{
		if ( InterlockedDecrement( &registered ) == 0 )
		{
			if (pEnumerator)
			{
				pEnumerator->UnregisterEndpointNotificationCallback( this );
				pEnumerator->Release();
				pEnumerator = NULL;
			}
		}

		EnterCriticalSection( &lock );
		for ( auto it = instances.begin(); it < instances.end(); ++it )
		{
			if ( *it == p_instance )
			{
				instances.erase( it );
				break;
			}
		}
		LeaveCriticalSection( &lock );
	}
} g_notifier;

// Synchronization Event
class XAudio2_BufferNotify : public IXAudio2VoiceCallback
{
public:
	HANDLE hBufferEndEvent;

	XAudio2_BufferNotify() {
		hBufferEndEvent = NULL;
		hBufferEndEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
		assert( hBufferEndEvent != NULL );
	}

	~XAudio2_BufferNotify() {
		CloseHandle( hBufferEndEvent );
		hBufferEndEvent = NULL;
	}

    STDMETHOD_( void, OnBufferEnd ) ( void *pBufferContext ) {
		assert( hBufferEndEvent != NULL );
		SetEvent( hBufferEndEvent );
	}


	// dummies:
	STDMETHOD_( void, OnVoiceProcessingPassStart ) ( UINT32 BytesRequired ) {}
	STDMETHOD_( void, OnVoiceProcessingPassEnd ) () {}
	STDMETHOD_( void, OnStreamEnd ) () {}
	STDMETHOD_( void, OnBufferStart ) ( void *pBufferContext ) {}
	STDMETHOD_( void, OnLoopEnd ) ( void *pBufferContext ) {}
	STDMETHOD_( void, OnVoiceError ) ( void *pBufferContext, HRESULT Error ) {};
};


// Class Declaration
class XAudio2_Output
	: public SoundDriver
{
public:
	XAudio2_Output();
	~XAudio2_Output();

	// Initialization
	bool init(long sampleRate);

	// Sound Data Feed
	void write(u16 * finalWave, int length);

	// Play Control
	void pause();
	void resume();
	void reset();
	void close();
	void device_change();

	// Configuration Changes
	void setThrottle( unsigned short throttle );

private:
	bool   failed;
	bool   initialized;
	bool   playing;
	UINT32 freq;
	UINT32 bufferCount;
	BYTE  *buffers;
	int    currentBuffer;
	int    soundBufferLen;

	volatile bool device_changed;

	IXAudio2               *xaud;
	IXAudio2MasteringVoice *mVoice; // listener
	IXAudio2SourceVoice    *sVoice; // sound source
	XAUDIO2_BUFFER          buf;
	XAUDIO2_VOICE_STATE     vState;
	XAudio2_BufferNotify    notify; // buffer end notification
};


// Class Implementation
XAudio2_Output::XAudio2_Output()
{
	failed = false;
	initialized = false;
	playing = false;
	freq = 0;
	bufferCount = gopts.audio_buffers;
	buffers = NULL;
	currentBuffer = 0;
	device_changed = false;

	xaud = NULL;
	mVoice = NULL;
	sVoice = NULL;
	ZeroMemory( &buf, sizeof( buf ) );
	ZeroMemory( &vState, sizeof( vState ) );

	g_notifier.do_register( this );
}


XAudio2_Output::~XAudio2_Output()
{
	g_notifier.do_unregister( this );
	close();
}

void XAudio2_Output::close()
{
	initialized = false;

	if( sVoice ) {
		if( playing ) {
			HRESULT hr = sVoice->Stop( 0 );
			assert( hr == S_OK );
		}
		sVoice->DestroyVoice();
		sVoice = NULL;
	}

	if( buffers ) {
		free( buffers );
		buffers = NULL;
	}

	if( mVoice ) {
		mVoice->DestroyVoice();
		mVoice = NULL;
	}

	if( xaud ) {
		xaud->Release();
		xaud = NULL;
	}
}

void XAudio2_Output::device_change()
{
	device_changed = true;
}


static int XA2GetDev(IXAudio2 *);


bool XAudio2_Output::init(long sampleRate)
{
	if( failed || initialized ) return false;

	HRESULT hr;

	// Initialize XAudio2
	UINT32 flags = 0;
#ifdef _DEBUG
	flags = XAUDIO2_DEBUG_ENGINE;
#endif

	hr = XAudio2Create( &xaud, flags );
	if( hr != S_OK ) {
		wxLogError( _("The XAudio2 interface failed to initialize!") );
		failed = true;
		return false;
	}


	freq = sampleRate;

	// calculate the number of samples per frame first
	// then multiply it with the size of a sample frame (16 bit * stereo)
	soundBufferLen = ( freq / 60 ) * 4;

	// create own buffers to store sound data because it must not be
	// manipulated while the voice plays from it
	buffers = (BYTE *)malloc( ( bufferCount + 1 ) * soundBufferLen );
	// + 1 because we need one temporary buffer when all others are in use

	WAVEFORMATEX wfx;
	ZeroMemory( &wfx, sizeof( wfx ) );
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nChannels = 2;
	wfx.nSamplesPerSec = freq;
	wfx.wBitsPerSample = 16;
	wfx.nBlockAlign = wfx.nChannels * ( wfx.wBitsPerSample / 8 );
	wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;


	// create sound receiver
	hr = xaud->CreateMasteringVoice(
		&mVoice,
		XAUDIO2_DEFAULT_CHANNELS,
		XAUDIO2_DEFAULT_SAMPLERATE,
		0,
		XA2GetDev(xaud),
		NULL );
	if( hr != S_OK ) {
		wxLogError( _("XAudio2: Creating mastering voice failed!") );
		failed = true;
		return false;
	}


	// create sound emitter
	hr = xaud->CreateSourceVoice( &sVoice, &wfx, 0, 4.0f, &notify );
	if( hr != S_OK ) {
		wxLogError( _("XAudio2: Creating source voice failed!") );
		failed = true;
		return false;
	}


	if( gopts.upmix ) {
		// set up stereo upmixing
		XAUDIO2_DEVICE_DETAILS dd;
		ZeroMemory( &dd, sizeof( dd ) );
		hr = xaud->GetDeviceDetails( 0, &dd );
		assert( hr == S_OK );
		float *matrix = NULL;
		matrix = (float*)malloc( sizeof( float ) * 2 * dd.OutputFormat.Format.nChannels );
        if( matrix == NULL ) return false;
		bool matrixAvailable = true;
		switch( dd.OutputFormat.Format.nChannels ) {
			case 4: // 4.0
	//Speaker \ Left Source           Right Source
	/*Front L*/	matrix[0] = 1.0000f;  matrix[1] = 0.0000f;
	/*Front R*/	matrix[2] = 0.0000f;  matrix[3] = 1.0000f;
	/*Back  L*/	matrix[4] = 1.0000f;  matrix[5] = 0.0000f;
	/*Back  R*/	matrix[6] = 0.0000f;  matrix[7] = 1.0000f;
				break;
			case 5: // 5.0
	//Speaker \ Left Source           Right Source
	/*Front L*/	matrix[0] = 1.0000f;  matrix[1] = 0.0000f;
	/*Front R*/	matrix[2] = 0.0000f;  matrix[3] = 1.0000f;
	/*Front C*/	matrix[4] = 0.7071f;  matrix[5] = 0.7071f;
	/*Side  L*/	matrix[6] = 1.0000f;  matrix[7] = 0.0000f;
	/*Side  R*/	matrix[8] = 0.0000f;  matrix[9] = 1.0000f;
				break;
			case 6: // 5.1
	//Speaker \ Left Source           Right Source
	/*Front L*/	matrix[0] = 1.0000f;  matrix[1] = 0.0000f;
	/*Front R*/	matrix[2] = 0.0000f;  matrix[3] = 1.0000f;
	/*Front C*/	matrix[4] = 0.7071f;  matrix[5] = 0.7071f;
	/*LFE    */	matrix[6] = 0.0000f;  matrix[7] = 0.0000f;
	/*Side  L*/	matrix[8] = 1.0000f;  matrix[9] = 0.0000f;
	/*Side  R*/	matrix[10] = 0.0000f;  matrix[11] = 1.0000f;
				break;
			case 7: // 6.1
	//Speaker \ Left Source           Right Source
	/*Front L*/	matrix[0] = 1.0000f;  matrix[1] = 0.0000f;
	/*Front R*/	matrix[2] = 0.0000f;  matrix[3] = 1.0000f;
	/*Front C*/	matrix[4] = 0.7071f;  matrix[5] = 0.7071f;
	/*LFE    */	matrix[6] = 0.0000f;  matrix[7] = 0.0000f;
	/*Side  L*/	matrix[8] = 1.0000f;  matrix[9] = 0.0000f;
	/*Side  R*/	matrix[10] = 0.0000f;  matrix[11] = 1.0000f;
	/*Back  C*/	matrix[12] = 0.7071f;  matrix[13] = 0.7071f;
				break;
			case 8: // 7.1
	//Speaker \ Left Source           Right Source
	/*Front L*/	matrix[0] = 1.0000f;  matrix[1] = 0.0000f;
	/*Front R*/	matrix[2] = 0.0000f;  matrix[3] = 1.0000f;
	/*Front C*/	matrix[4] = 0.7071f;  matrix[5] = 0.7071f;
	/*LFE    */	matrix[6] = 0.0000f;  matrix[7] = 0.0000f;
	/*Back  L*/	matrix[8] = 1.0000f;  matrix[9] = 0.0000f;
	/*Back  R*/	matrix[10] = 0.0000f;  matrix[11] = 1.0000f;
	/*Side  L*/	matrix[12] = 1.0000f;  matrix[13] = 0.0000f;
	/*Side  R*/	matrix[14] = 0.0000f;  matrix[15] = 1.0000f;
				break;
			default:
				matrixAvailable = false;
				break;
		}
		if( matrixAvailable ) {
			hr = sVoice->SetOutputMatrix( NULL, 2, dd.OutputFormat.Format.nChannels, matrix );
			assert( hr == S_OK );
		}
		free( matrix );
		matrix = NULL;
	}


	hr = sVoice->Start( 0 );
	assert( hr == S_OK );
	playing = true;

	currentBuffer = 0;
	device_changed = false;

	initialized = true;
	return true;
}


void XAudio2_Output::write(u16 * finalWave, int length)
{
	if( !initialized || failed ) return;

	while( true ) {
		if ( device_changed ) {
			close();
			if (!init(freq)) return;
		}

		sVoice->GetState( &vState );

		assert( vState.BuffersQueued <= bufferCount );

		if( vState.BuffersQueued < bufferCount ) {
			if( vState.BuffersQueued == 0 ) {
				// buffers ran dry
				if( systemVerbose & VERBOSE_SOUNDOUTPUT ) {
					static unsigned int i = 0;
					log( "XAudio2: Buffers were not refilled fast enough (i=%i)\n", i++ );
				}
			}
			// there is at least one free buffer
			break;
		} else {
			// the maximum number of buffers is currently queued
			if( synchronize && !speedup && !gopts.throttle ) {
				// wait for one buffer to finish playing
				if (WaitForSingleObject( notify.hBufferEndEvent, 10000 ) == WAIT_TIMEOUT) {
					device_changed = true;
				}
			} else {
				// drop current audio frame
				return;
			}
		}
	}

	// copy & protect the audio data in own memory area while playing it
	CopyMemory( &buffers[ currentBuffer * soundBufferLen ], finalWave, soundBufferLen );

	buf.AudioBytes = soundBufferLen;
	buf.pAudioData = &buffers[ currentBuffer * soundBufferLen ];

	currentBuffer++;
	currentBuffer %= ( bufferCount + 1 ); // + 1 because we need one temporary buffer

	HRESULT hr = sVoice->SubmitSourceBuffer( &buf ); // send buffer to queue
	assert( hr == S_OK );
}


void XAudio2_Output::pause()
{
	if( !initialized || failed ) return;

	if( playing ) {
		HRESULT hr = sVoice->Stop( 0 );
		assert( hr == S_OK );
		playing = false;
	}
}


void XAudio2_Output::resume()
{
	if( !initialized || failed ) return;

	if( !playing ) {
		HRESULT hr = sVoice->Start( 0 );
		assert( hr == S_OK );
		playing = true;
	}
}


void XAudio2_Output::reset()
{
	if( !initialized || failed ) return;

	if( playing ) {
		HRESULT hr = sVoice->Stop( 0 );
		assert( hr == S_OK );
	}

	sVoice->FlushSourceBuffers();
	sVoice->Start( 0 );
	playing = true;
}


void XAudio2_Output::setThrottle( unsigned short throttle )
{
	if( !initialized || failed ) return;

	if( throttle == 0 ) throttle = 100;
	HRESULT hr = sVoice->SetFrequencyRatio( (float)throttle / 100.0f );
	assert( hr == S_OK );
}

void xaudio2_device_changed( XAudio2_Output * instance )
{
	instance->device_change();
}

SoundDriver *newXAudio2_Output()
{
	return new XAudio2_Output();
}

int GetXA2Devices(IXAudio2 *xa, wxArrayString *names, wxArrayString *ids,
		  const wxString *match)
{
	HRESULT hr;
	UINT32 dev_count = 0;
	hr = xa->GetDeviceCount( &dev_count );
	if( hr != S_OK ) {
		wxLogError(_("XAudio2: Enumerating devices failed!"));
		return true;
	} else {
		XAUDIO2_DEVICE_DETAILS dd;
		for( UINT32 i = 0; i < dev_count; i++ ) {
			hr = xa->GetDeviceDetails( i, &dd );
			if( hr != S_OK ) {
				continue;
			} else {
				if(ids) {
					ids->push_back(dd.DeviceID);
					names->push_back( dd.DisplayName );
				} else if(*match == dd.DeviceID)
					return i;
			}
		}
	}
	return -1;
}

bool GetXA2Devices(wxArrayString &names, wxArrayString &ids)
{
	HRESULT hr;
	IXAudio2 *xa = NULL;
	UINT32 flags = 0;
#ifdef _DEBUG
	flags = XAUDIO2_DEBUG_ENGINE;
#endif

	hr = XAudio2Create( &xa, flags );
	if( hr != S_OK ) {
		wxLogError( _("The XAudio2 interface failed to initialize!") );
		return false;
	}
	GetXA2Devices(xa, &names, &ids, NULL);
	xa->Release();
	return true;
}

static int XA2GetDev(IXAudio2 *xa)
{
	if(gopts.audio_dev.empty())
		return 0;
	else {
		int ret = GetXA2Devices(xa, NULL, NULL, &gopts.audio_dev);
		return ret < 0 ? 0 : ret;
	}
}
#endif // #ifndef NO_XAUDIO2
