/* FluidSynth - A Software Synthesizer
 *
 * Copyright (C) 2003  Peter Hanappe and others.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA
 */

#define FLUIDINC
#include "config.h"
#if !C_FLUIDSYNTH && defined(WIN32) && !defined(C_HX_DOS)
#include "fluid_synth.h"
#include "fluid_adriver.h"
#include "fluid_settings.h"

#if DSOUND_SUPPORT

#define INITGUID

#include <mmsystem.h>
#include <dsound.h>

#define NOBITMAP
#include <mmreg.h>

static DWORD WINAPI fluid_dsound_audio_run(LPVOID lpParameter);

static char* fluid_win32_error(HRESULT hr);

typedef struct
{
	fluid_audio_driver_t driver;
	LPDIRECTSOUND direct_sound;
	LPDIRECTSOUNDBUFFER prim_buffer;
	LPDIRECTSOUNDBUFFER sec_buffer;
	HANDLE thread;
	DWORD threadID;
	fluid_synth_t* synth;
	fluid_audio_callback_t write;
	HANDLE quit_ev;
	int   bytes_per_second;
	DWORD buffer_byte_size;
	DWORD queue_byte_size;
	DWORD frame_size;
} fluid_dsound_audio_driver_t;

typedef struct
{
	LPGUID devGUID;
	char* devname;
} fluid_dsound_devsel_t;

int delete_fluid_dsound_audio_driver(fluid_audio_driver_t* d);

BOOL CALLBACK
fluid_dsound_enum_callback(LPGUID guid, LPCTSTR description, LPCTSTR module, LPVOID context)
{
	fluid_settings_t* settings = (fluid_settings_t*)context;
	fluid_settings_add_option(settings, "audio.dsound.device", (const char*)description);

	return TRUE;
}

BOOL CALLBACK
fluid_dsound_enum_callback2(LPGUID guid, LPCTSTR description, LPCTSTR module, LPVOID context)
{
	fluid_dsound_devsel_t* devsel = (fluid_dsound_devsel_t*)context;
	FLUID_LOG(FLUID_DBG, "Testing audio device: %s", description);

	if (FLUID_STRCASECMP(devsel->devname, description) == 0)
	{
		devsel->devGUID = FLUID_NEW(GUID);

		if (devsel->devGUID)
		{
			memcpy(devsel->devGUID, guid, sizeof(GUID));
			FLUID_LOG(FLUID_DBG, "Selected audio device GUID: %p", devsel->devGUID);
		}
	}

	return TRUE;
}

void fluid_dsound_audio_driver_settings(fluid_settings_t* settings)
{
	fluid_settings_register_str(settings, "audio.dsound.device", "default", 0,NULL,NULL);
	fluid_settings_add_option(settings, "audio.dsound.device", "default");
	DirectSoundEnumerate((LPDSENUMCALLBACK)fluid_dsound_enum_callback, settings);
}


/*
 * new_fluid_dsound_audio_driver
 */
fluid_audio_driver_t*
new_fluid_dsound_audio_driver(fluid_settings_t* settings, fluid_synth_t* synth)
{
	HRESULT hr;
	DSBUFFERDESC desc;
	fluid_dsound_audio_driver_t* dev = NULL;
	DSCAPS caps;
	char* buf1;
	DWORD bytes1;
	double sample_rate;
	int periods, period_size;
	fluid_dsound_devsel_t devsel;
	WAVEFORMATEX format;

	/* create and clear the driver data */
	dev = FLUID_NEW(fluid_dsound_audio_driver_t);

	if (dev == NULL)
	{
		FLUID_LOG(FLUID_ERR, "Out of memory");
		return NULL;
	}

	FLUID_MEMSET(dev, 0, sizeof(fluid_dsound_audio_driver_t));
	dev->synth = synth;

	fluid_settings_getnum(settings, "synth.sample-rate", &sample_rate);
	fluid_settings_getint(settings, "audio.periods", &periods);
	fluid_settings_getint(settings, "audio.period-size", &period_size);

	/* Clear the buffer format */
	ZeroMemory(&format, sizeof(WAVEFORMATEX));

	/* check the format */
	if (fluid_settings_str_equal(settings, "audio.sample-format", "float"))
	{
		FLUID_LOG(FLUID_DBG, "Selected 32 bit sample format");

		dev->frame_size = 2 * sizeof(float);
		dev->write = fluid_synth_write_float;

		format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
	}
	else if (fluid_settings_str_equal(settings, "audio.sample-format", "16bits"))
	{
		FLUID_LOG(FLUID_DBG, "Selected 16 bit sample format");

		dev->frame_size = 2 * sizeof(short);
		dev->write = fluid_synth_write_s16;

		format.wFormatTag = WAVE_FORMAT_PCM;
	}
	else
	{
		FLUID_LOG(FLUID_ERR, "Unhandled sample format");
		goto error_recovery;
	}

	dev->buffer_byte_size = period_size * dev->frame_size;
	dev->queue_byte_size = periods * dev->buffer_byte_size;
	dev->bytes_per_second = (int)(sample_rate * dev->frame_size);

	/* Finish to initialize the buffer format */
	format.nChannels = 2;
	format.wBitsPerSample = (WORD)(dev->frame_size * 4);
	format.nSamplesPerSec = (DWORD)sample_rate;
	format.nBlockAlign = (WORD)dev->frame_size;
	format.nAvgBytesPerSec = dev->bytes_per_second;

	devsel.devGUID = NULL;

	/* get the selected device name. if none is specified, use NULL for the default device. */
	if (fluid_settings_dupstr(settings, "audio.dsound.device", &devsel.devname) == FLUID_OK /* ++ alloc device name */
		&& devsel.devname && strlen(devsel.devname) > 0)
	{
		/* look for the GUID of the selected device */
		DirectSoundEnumerate((LPDSENUMCALLBACK)fluid_dsound_enum_callback2, (void*)& devsel);
	}

	if (devsel.devname)
	{
		FLUID_FREE(devsel.devname);    /* -- free device name */
	}

	/* open DirectSound */
	hr = DirectSoundCreate(devsel.devGUID, &dev->direct_sound, NULL);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_ERR, "Failed to create the DirectSound object");
		goto error_recovery;
	}

	hr = IDirectSound_SetCooperativeLevel(dev->direct_sound, GetDesktopWindow(), DSSCL_PRIORITY);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_ERR, "Failed to set the cooperative level");
		goto error_recovery;
	}

	caps.dwSize = sizeof(caps);
	hr = IDirectSound_GetCaps(dev->direct_sound, &caps);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_ERR, "Failed to query the device capacities");
		goto error_recovery;
	}

	/* create primary buffer */

	ZeroMemory(&desc, sizeof(DSBUFFERDESC));
	desc.dwSize = sizeof(DSBUFFERDESC);
	desc.dwFlags = DSBCAPS_PRIMARYBUFFER;

	if (caps.dwFreeHwMixingStreamingBuffers > 0)
	{
		desc.dwFlags |= DSBCAPS_LOCHARDWARE;
	}

	hr = IDirectSound_CreateSoundBuffer(dev->direct_sound, &desc, &dev->prim_buffer, NULL);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_ERR, "Failed to allocate the primary buffer");
		goto error_recovery;
	}

	/* set the primary sound buffer to this format. if it fails, just
	   print a warning. */
	hr = IDirectSoundBuffer_SetFormat(dev->prim_buffer, &format);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_WARN, "Can't set format of primary sound buffer: %s", fluid_win32_error(hr));
	}

	/* initialize the buffer description */

	ZeroMemory(&desc, sizeof(DSBUFFERDESC));
	desc.dwSize = sizeof(DSBUFFERDESC);
	desc.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2;
	desc.lpwfxFormat = &format;
	desc.dwBufferBytes = dev->queue_byte_size;

	if (caps.dwFreeHwMixingStreamingBuffers > 0)
	{
		desc.dwFlags |= DSBCAPS_LOCHARDWARE;
	}

	/* create the secondary sound buffer */

	hr = IDirectSound_CreateSoundBuffer(dev->direct_sound, &desc, &dev->sec_buffer, NULL);

	if (hr != DS_OK)
	{
		FLUID_LOG(FLUID_ERR, "dsound: Can't create sound buffer: %s", fluid_win32_error(hr));
		goto error_recovery;
	}


	/* Lock */
	hr = IDirectSoundBuffer_Lock(dev->sec_buffer, 0, 0, (void*)& buf1, &bytes1, 0, 0, DSBLOCK_ENTIREBUFFER);

	if ((hr != DS_OK) || (buf1 == NULL))
	{
		FLUID_LOG(FLUID_PANIC, "Failed to lock the audio buffer. Exiting.");
		goto error_recovery;
	}

	/* fill the buffer with silence */
	memset(buf1, 0, bytes1);

	/* Unlock */
	IDirectSoundBuffer_Unlock(dev->sec_buffer, buf1, bytes1, 0, 0);

	/* Create object to signal thread exit */
	dev->quit_ev = CreateEvent(NULL, FALSE, FALSE, NULL);

	if (dev->quit_ev == NULL)
	{
		goto error_recovery;
	}

	/* start the audio thread */
	dev->thread = CreateThread(NULL, 0, fluid_dsound_audio_run, (LPVOID)dev, 0, &dev->threadID);

	if (dev->thread == NULL)
	{
		goto error_recovery;
	}

	return (fluid_audio_driver_t*)dev;

error_recovery:
	delete_fluid_dsound_audio_driver((fluid_audio_driver_t*)dev);
	return NULL;
}


int delete_fluid_dsound_audio_driver(fluid_audio_driver_t* d)
{
	fluid_dsound_audio_driver_t* dev = (fluid_dsound_audio_driver_t*)d;
	fluid_return_if_fail(dev != NULL);

	/* wait till the audio thread exits */
	if (dev->thread != NULL)
	{
		/* tell the audio thread to stop its loop */
		SetEvent(dev->quit_ev);

		if (WaitForSingleObject(dev->thread, 2000) != WAIT_OBJECT_0)
		{
			/* on error kill the thread mercilessly */
			FLUID_LOG(FLUID_DBG, "Couldn't join the audio thread. killing it.");
			TerminateThread(dev->thread, 0);
		}

		/* Release the thread object */
		CloseHandle(dev->thread);
	}

	/* Release the event object */
	if (dev->quit_ev != NULL)
	{
		CloseHandle(dev->quit_ev);
	}

	/* release all the allocated resources */

	if (dev->sec_buffer != NULL)
	{
		IDirectSoundBuffer_Stop(dev->sec_buffer);
		IDirectSoundBuffer_Release(dev->sec_buffer);
	}

	if (dev->prim_buffer != NULL)
	{
		IDirectSoundBuffer_Release(dev->prim_buffer);
	}

	if (dev->direct_sound != NULL)
	{
		IDirectSound_Release(dev->direct_sound);
	}

	FLUID_FREE(dev);
	return 0;
}

static DWORD WINAPI fluid_dsound_audio_run(LPVOID lpParameter)
{
	fluid_dsound_audio_driver_t* dev = (fluid_dsound_audio_driver_t*)lpParameter;
	short* buf1, * buf2;
	DWORD bytes1, bytes2;
	DWORD cur_position, frames, play_position, write_position, bytes;
	HRESULT res;
	int     ms;

	cur_position = 0;

	/* boost the priority of the audio thread */
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);

	IDirectSoundBuffer_Play(dev->sec_buffer, 0, 0, DSBPLAY_LOOPING);

	for (;;)
	{
		IDirectSoundBuffer_GetCurrentPosition(dev->sec_buffer, &play_position, &write_position);

		if (cur_position <= play_position)
		{
			bytes = play_position - cur_position;
		}
		else if ((play_position < cur_position) && (write_position <= cur_position))
		{
			bytes = dev->queue_byte_size + play_position - cur_position;
		}
		else
		{
			bytes = 0;
		}

		if (bytes >= dev->buffer_byte_size)
		{

			/* Lock */
			res = IDirectSoundBuffer_Lock(dev->sec_buffer, cur_position, bytes, (void*)& buf1, &bytes1, (void*)& buf2, &bytes2, 0);

			if ((res != DS_OK) || (buf1 == NULL))
			{
				FLUID_LOG(FLUID_PANIC, "Failed to lock the audio buffer. System lockup might follow. Exiting.");
				ExitProcess(0);
			}

			/* fill the first part of the buffer */
			if (bytes1 > 0)
			{
				frames = bytes1 / dev->frame_size;
				dev->write(dev->synth, frames, buf1, 0, 2, buf1, 1, 2);
				cur_position += frames * dev->frame_size;
			}

			/* fill the second part of the buffer */
			if ((buf2 != NULL) && (bytes2 > 0))
			{
				frames = bytes2 / dev->frame_size;
				dev->write(dev->synth, frames, buf2, 0, 2, buf2, 1, 2);
				cur_position += frames * dev->frame_size;
			}

			/* Unlock */
			IDirectSoundBuffer_Unlock(dev->sec_buffer, buf1, bytes1, buf2, bytes2);

			if (cur_position >= dev->queue_byte_size)
			{
				cur_position -= dev->queue_byte_size;
			}

			/* 1 ms of wait */
			ms = 1;
		}
		else
		{
			/* Calculate how many milliseconds to sleep (minus 1 for safety) */
			ms = (dev->buffer_byte_size - bytes) * 1000 / dev->bytes_per_second - 1;

			if (ms < 1)
			{
				ms = 1;
			}
		}

		/* Wait quit event or timeout */
		if (WaitForSingleObject(dev->quit_ev, ms) == WAIT_OBJECT_0)
		{
			break;
		}
	}

	return 0;
}


static char* fluid_win32_error(HRESULT hr)
{
	char* s = "Don't know why";

	switch (hr)
	{
	case E_NOINTERFACE:
		s = "No such interface";
		break;

	case DSERR_GENERIC:
		s = "Generic error";
		break;

	case DSERR_ALLOCATED:
		s = "Required resources already allocated";
		break;

	case DSERR_BADFORMAT:
		s = "The format is not supported";
		break;

	case DSERR_INVALIDPARAM:
		s = "Invalid parameter";
		break;

	case DSERR_NOAGGREGATION:
		s = "No aggregation";
		break;

	case DSERR_OUTOFMEMORY:
		s = "Out of memory";
		break;

	case DSERR_UNINITIALIZED:
		s = "Uninitialized";
		break;

	case DSERR_UNSUPPORTED:
		s = "Function not supported";
		break;
	}

	return s;
}

#endif /* DSOUND_SUPPORT */
#endif