/**
 * OpenAL cross platform audio library
 * Copyright (C) 1999-2000 by authors.
 * This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 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
 *  Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 *  License along with this library; if not, write to the
 *  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 *  Boston, MA  02111-1307, USA.
 * Or go to http://www.gnu.org/copyleft/lgpl.html
 */

#include <math.h>
#include <stdlib.h>
#include <memory.h>
#include <al\alc.h>
#include <al\alu.h>
#include <OpenAL32\include\alBuffer.h>

static ALubyte *alcErrorStr[]=
{
	"There is no accessible sound device/driver/server.",
	"The Device argument does not name a valid device.",
	"The Context argument does not name a valid context.",
	"No error.",
};

static ALCcontext *Context=NULL;
static ALCuint ContextCount=0;
static ALCenum LastError=ALC_NO_ERROR;

ALCAPI ALCvoid ALCAPIENTRY alcSuspendContext(ALCcontext *context)
{
	EnterCriticalSection(&context->Device->mutex);
}

ALCAPI ALCvoid ALCAPIENTRY alcProcessContext(ALCcontext *context)
{
	LeaveCriticalSection(&context->Device->mutex);
}

ALCAPI ALCenum ALCAPIENTRY alcGetError(ALCdevice *device)
{
	ALCenum errorCode;

	errorCode=LastError;
	LastError=AL_NO_ERROR;
	return errorCode;
}

ALCAPI ALCvoid ALCAPIENTRY alcSetError(ALenum errorCode)
{
	LastError=errorCode;
}

void CALLBACK alcWaveOutProc(HWAVEOUT hDevice,UINT uMsg,DWORD dwInstance,DWORD dwParam1,DWORD dwParam2)
{
	ALCcontext *ALContext;
	LPWAVEHDR WaveHdr;

	if (uMsg==WOM_DONE)
	{
		ALContext=alcGetCurrentContext();
		WaveHdr=((LPWAVEHDR)dwParam1);
		if (ALContext)
		{
			alcSuspendContext(ALContext);
			aluMixData(ALContext,WaveHdr->lpData,WaveHdr->dwBufferLength,ALContext->Format);
			alcProcessContext(ALContext);
		}
		else memset(WaveHdr->lpData,0,WaveHdr->dwBufferLength);
		waveOutWrite(hDevice,WaveHdr,sizeof(WAVEHDR));
	}
}

ALCAPI ALvoid ALCAPIENTRY alcInitContext(ALCcontext *context)
{
	if (context)
	{
		//Lock context
		alcSuspendContext(context);
		//Initialise listener
		context->Listener.Gain=1.0f;
		context->Listener.Position[0]=0.0f;
		context->Listener.Position[1]=0.0f;
		context->Listener.Position[2]=0.0f;
		context->Listener.Velocity[0]=0.0f;
		context->Listener.Velocity[1]=0.0f;
		context->Listener.Velocity[2]=0.0f;
		context->Listener.Forward[0]=0.0f;
		context->Listener.Forward[1]=0.0f;
		context->Listener.Forward[2]=1.0f;
		context->Listener.Up[0]=0.0f; 
		context->Listener.Up[1]=1.0f;
		context->Listener.Up[2]=0.0f;
		context->Listener.Environment=0;
		//Validate context
		context->LastError=AL_NO_ERROR;
		context->InUse=AL_FALSE;
		context->Valid=AL_TRUE;
		//Set output format
		context->Frequency=context->Device->Frequency;
		context->Channels=context->Device->Channels;
		context->Format=context->Device->Format;
		//Set globals
		context->DistanceModel=AL_INVERSE_DISTANCE_CLAMPED;
		context->DopplerFactor=1.0f;
		context->DopplerVelocity=1.0f;
		//Unlock context
		alcProcessContext(context);
	}
}

ALCAPI ALCvoid ALCAPIENTRY alcExitContext(ALCcontext *context)
{
	if (context)
	{
		//Lock context
		alcSuspendContext(context);
		//Invalidate context
		context->LastError=AL_NO_ERROR;
		context->InUse=AL_FALSE;
		context->Valid=AL_TRUE;
		//Unlock context
		alcProcessContext(context);
	}
}

ALCAPI ALCcontext*ALCAPIENTRY alcCreateContext(ALCdevice *device,ALCint *attrList)
{
	ALCcontext *ALContext;

	if (!Context)
    {
		Context=malloc(sizeof(ALCcontext));
		if (Context)
		{
			memset(Context,0,sizeof(ALCcontext));
			Context->Device=device;
			Context->Valid=AL_TRUE;
			alcInitContext(Context);
			ContextCount++;
		}
		ALContext=Context;
	}
	else
	{
		ALContext=Context;
		while (ALContext->next)
			ALContext=ALContext->next;
		if (ALContext)
		{
			ALContext->next=malloc(sizeof(ALCcontext));
			if (ALContext->next)
			{
				memset(ALContext->next,0,sizeof(ALCcontext));
				ALContext->next->previous=ALContext;
				ALContext->next->Device=device;
				ALContext->next->Valid=AL_TRUE;
				alcInitContext(ALContext);
				ContextCount++;
			}
			ALContext=ALContext->next;
		}
	}
	return ALContext;
}

ALCAPI ALCvoid ALCAPIENTRY alcDestroyContext(ALCcontext *context)
{
	ALCcontext *ALContext;

	if (context)
	{
		ALContext=((ALCcontext *)context);
		alcExitContext(ALContext);
		if (ALContext->previous)
			ALContext->previous->next=ALContext->next;
		else
			Context=ALContext->next;
		if (ALContext->next)
			ALContext->next->previous=ALContext->previous;
		memset(ALContext,0,sizeof(ALCcontext));
		ContextCount--;
		free(ALContext);
	}
}

ALCAPI ALCcontext * ALCAPIENTRY alcGetCurrentContext(ALCvoid)
{
	ALCcontext *ALContext;

	ALContext=Context;
	while ((ALContext)&&(!ALContext->InUse))
		ALContext=ALContext->next;
	return ALContext;
}

ALCAPI ALCdevice* ALCAPIENTRY alcGetContextsDevice(ALCcontext *context)
{
	ALCdevice *ALDevice=NULL;
	ALCcontext *ALContext;

	ALContext=context;
	if (ALContext)
	{
		alcSuspendContext(ALContext);
		ALDevice=ALContext->Device;
		alcProcessContext(ALContext);
	}
	return ALDevice;
}

ALCAPI ALCboolean ALCAPIENTRY alcMakeContextCurrent(ALCcontext *context)
{
	ALCcontext *ALContext;
	
	if (ALContext=alcGetCurrentContext())
	{
		alcSuspendContext(ALContext);
		ALContext->InUse=AL_FALSE;
		alcProcessContext(ALContext);
	}
	if (ALContext=context)
	{
		alcSuspendContext(ALContext);
		ALContext->InUse=AL_TRUE;
		alcProcessContext(ALContext);
	}
	return AL_TRUE;
}

ALCAPI ALCvoid ALCAPIENTRY alcUpdateContext(ALCcontext *context)
{
	ALuint BufferID,Freq,Bits,Channels,innerAngle,outerAngle;
    ALuint Data,DataSize,Loop,State,Size,i,status;
	ALfloat Pitch,Gain,outerGain,minDist,maxDist;
	ALfloat Pos[3],Vel[3],Dir[3];
	DSBUFFERDESC DSBDescription;
	WAVEFORMATEX OutputType;
	ALCcontext *ALContext;
	ALsource *ALSource;
	ALvoid *Dest;

	ALContext=context;
	ALSource=ALContext->Source;
	alcSuspendContext(ALContext);
	//Platform specific context updating
	if (ALContext->Device->DShandle)
	{
		for (i=0;i<ALContext->SourceCount;i++)
		{
			if (alIsSource((ALuint)ALSource))
			{
				alGetSourcei((ALuint)ALSource,AL_BUFFER,&BufferID);
				if (alIsBuffer(BufferID))
				{
					alGetBufferi(BufferID,AL_FREQUENCY,&Freq);
					alGetBufferi(BufferID,AL_BITS,&Bits);
					alGetBufferi(BufferID,AL_CHANNELS,&Channels);
					alGetBufferi(BufferID,AL_DATA,&Data);	
					alGetBufferi(BufferID,AL_SIZE,&DataSize);	

					alGetSourcefv((ALuint)ALSource,AL_POSITION,Pos);
					alGetSourcefv((ALuint)ALSource,AL_VELOCITY,Vel);
					alGetSourcefv((ALuint)ALSource,AL_DIRECTION,Dir);
					
					alGetSourcef((ALuint)ALSource,AL_PITCH,&Pitch);
					alGetSourcef((ALuint)ALSource,AL_GAIN,&Gain);
					alGetSourcef((ALuint)ALSource,AL_REFERENCE_DISTANCE,&minDist);
					alGetSourcef((ALuint)ALSource,AL_MAX_DISTANCE,&maxDist);
					alGetSourcei((ALuint)ALSource,AL_CONE_INNER_ANGLE,&innerAngle);
					alGetSourcei((ALuint)ALSource,AL_CONE_OUTER_ANGLE,&outerAngle);
					alGetSourcef((ALuint)ALSource,AL_CONE_OUTER_GAIN,&outerGain);
					alGetSourcei((ALuint)ALSource,AL_LOOPING,&Loop);
					alGetSourcei((ALuint)ALSource,AL_SOURCE_STATE,&State);

					switch (State)
					{
						case AL_INITIAL:
							if (ALSource->uservalue1)
								IDirectSoundBuffer_SetCurrentPosition((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,0);
							break;
						case AL_PLAYING:
							if (ALSource->play)
							{
								if (ALSource->uservalue1)
								{
									IDirectSoundBuffer_Stop((LPDIRECTSOUNDBUFFER)ALSource->uservalue1);
									IDirectSoundBuffer_Release((LPDIRECTSOUNDBUFFER)ALSource->uservalue1);
									ALSource->uservalue1=NULL;
								}
								memset(&DSBDescription,0,sizeof(DSBUFFERDESC));
								DSBDescription.dwSize=sizeof(DSBUFFERDESC);
								DSBDescription.dwFlags=DSBCAPS_CTRLVOLUME|DSBCAPS_CTRLFREQUENCY|DSBCAPS_CTRL3D|DSBCAPS_GLOBALFOCUS;
								DSBDescription.dwBufferBytes=DataSize;
								DSBDescription.lpwfxFormat=&OutputType;
								memset(&OutputType,0,sizeof(WAVEFORMATEX));
								OutputType.wFormatTag=WAVE_FORMAT_PCM;
								OutputType.nChannels=Channels;
								OutputType.wBitsPerSample=Bits;
								OutputType.nBlockAlign=OutputType.nChannels*OutputType.wBitsPerSample/8;
								OutputType.nSamplesPerSec=Freq;
								OutputType.nAvgBytesPerSec=OutputType.nSamplesPerSec*OutputType.nBlockAlign;
								OutputType.cbSize=0;
								if (IDirectSound_CreateSoundBuffer(ALContext->Device->DShandle,&DSBDescription,&(LPDIRECTSOUNDBUFFER)ALSource->uservalue1,NULL)==DS_OK)
								{
									IDirectSoundBuffer_Lock((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,0,0,&Dest,&Size,NULL,NULL,DSBLOCK_ENTIREBUFFER);
									memcpy(Dest,(ALubyte *)Data,Size);
									IDirectSoundBuffer_Unlock((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,Dest,Size,NULL,0);
									IDirectSoundBuffer_SetCurrentPosition((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,0);
									ALSource->current_buffer=BufferID;
								}
							}
							if (ALSource->uservalue1)
							{
								IDirectSoundBuffer_SetVolume((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,(long)(-2000.0*log10(Gain)));
								IDirectSoundBuffer_SetFrequency((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,(long)(Freq*Pitch));
								if (IDirectSoundBuffer_QueryInterface((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,&IID_IDirectSound3DBuffer,(LPUNKNOWN *)&ALSource->uservalue2)==DS_OK)
								{
									IDirectSound3DBuffer_SetMode((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,DS3DMODE_HEADRELATIVE,DS3D_DEFERRED);
									IDirectSound3DBuffer_SetPosition((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,Pos[0],Pos[1],Pos[2],DS3D_DEFERRED);
									IDirectSound3DBuffer_SetVelocity((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,Vel[0],Vel[1],Vel[2],DS3D_DEFERRED);
									IDirectSound3DBuffer_SetMinDistance((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,minDist,DS3D_DEFERRED);
									IDirectSound3DBuffer_SetMaxDistance((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,maxDist,DS3D_DEFERRED);
									IDirectSound3DBuffer_SetConeAngles((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,innerAngle,outerAngle,DS3D_DEFERRED);
									IDirectSound3DBuffer_SetConeOutsideVolume((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,(long)(-2000.0*log10(outerGain)),DS3D_DEFERRED);
									IDirectSound3DBuffer_SetConeOrientation((LPDIRECTSOUND3DBUFFER)ALSource->uservalue2,Dir[0],Dir[1],Dir[2],DS3D_DEFERRED);
								}
								if (ALSource->play)
								{
									IDirectSoundBuffer_Play((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,0,0,(Loop?DSBPLAY_LOOPING:0));
									ALSource->play=AL_FALSE;
								} 
								IDirectSoundBuffer_GetStatus((LPDIRECTSOUNDBUFFER)ALSource->uservalue1,&status);
								ALSource->state=((status&DSBSTATUS_PLAYING)?AL_PLAYING:AL_STOPPED);
							}
							break;
						case AL_PAUSED:
							if (ALSource->uservalue1)
								IDirectSoundBuffer_Stop((LPDIRECTSOUNDBUFFER)ALSource->uservalue1);
							break;
						case AL_STOPPED:
							if (ALSource->uservalue1)
								IDirectSoundBuffer_Stop((LPDIRECTSOUNDBUFFER)ALSource->uservalue1);
							break;
					}
				}
				ALSource=ALSource->next;
			}
		}
		if (ALContext->Device->DS3dlistener)
		{
			IDirectSound3DListener_SetPosition(ALContext->Device->DS3dlistener,ALContext->Listener.Position[0],ALContext->Listener.Position[1],ALContext->Listener.Position[2],DS3D_DEFERRED);
			IDirectSound3DListener_SetVelocity(ALContext->Device->DS3dlistener,ALContext->Listener.Velocity[0],ALContext->Listener.Velocity[1],ALContext->Listener.Velocity[2],DS3D_DEFERRED);
			IDirectSound3DListener_SetOrientation(ALContext->Device->DS3dlistener,ALContext->Listener.Forward[0],ALContext->Listener.Forward[1],ALContext->Listener.Forward[2],ALContext->Listener.Up[0],ALContext->Listener.Up[1],ALContext->Listener.Up[2],DS3D_DEFERRED);
			IDirectSound3DListener_SetDopplerFactor(ALContext->Device->DS3dlistener,ALContext->DopplerFactor,DS3D_DEFERRED);
			IDirectSound3DListener_SetRolloffFactor (ALContext->Device->DS3dlistener,ALContext->DistanceModel?1.0f:0.0f,DS3D_DEFERRED);
			IDirectSound3DListener_CommitDeferredSettings(ALContext->Device->DS3dlistener);
		}		
	}
	alcProcessContext(ALContext);
}

ALCAPI ALCdevice* ALCAPIENTRY alcOpenDevice(ALCubyte *deviceName)
{
	GUID DSPROPSETID_VoiceManager_Def = {0x62a69bae, 0xdf9d, 0x11d1, {0x99, 0xa6, 0x0, 0xc0, 0x4f, 0xc9, 0x9d, 0x46}};
	LPDIRECTSOUNDBUFFER DStempbuffer;
	DSBUFFERDESC DSBDescription;
	WAVEFORMATEX OutputType;
	ALCdevice *device=NULL;
	ALint vmode=1;
	ALint i;

	device=malloc(sizeof(ALCdevice));
	if (device)
	{
		//Initialise device structure
		memset(device,0,sizeof(ALCdevice));
		//Validate device
		device->LastError=AL_NO_ERROR;
		device->InUse=AL_TRUE;
		device->Valid=AL_TRUE;
		//Set output format
		device->Frequency=22050;
		device->Channels=2;
		device->Format=AL_FORMAT_STEREO16;
		//Platform specific
		InitializeCriticalSection(&device->mutex);
		memset(&OutputType,0,sizeof(WAVEFORMATEX));
		OutputType.wFormatTag=WAVE_FORMAT_PCM;
		OutputType.nChannels=device->Channels;
		OutputType.wBitsPerSample=(((device->Format==AL_FORMAT_MONO16)||(device->Format==AL_FORMAT_STEREO16))?16:8);
		OutputType.nBlockAlign=OutputType.nChannels*OutputType.wBitsPerSample/8;
		OutputType.nSamplesPerSec=device->Frequency;
		OutputType.nAvgBytesPerSec=OutputType.nSamplesPerSec*OutputType.nBlockAlign;
		OutputType.cbSize=0;
		//Initialise requested device
		if (strcmp(deviceName,"DirectSound")==0)
		{
			//Init COM
			CoInitialize(NULL);
			//DirectSound Init code
			if (CoCreateInstance(&CLSID_DirectSound,NULL,CLSCTX_INPROC_SERVER,&IID_IDirectSound,&device->DShandle)==S_OK)
			{
				if (IDirectSound_Initialize(device->DShandle,NULL)==DS_OK)
				{
					if (IDirectSound_SetCooperativeLevel(device->DShandle,GetForegroundWindow(),DSSCL_PRIORITY)==DS_OK)
					{
						memset(&DSBDescription,0,sizeof(DSBUFFERDESC));
						DSBDescription.dwSize=sizeof(DSBUFFERDESC);
						DSBDescription.dwFlags=DSBCAPS_PRIMARYBUFFER|DSBCAPS_CTRL3D;
						if (IDirectSound_CreateSoundBuffer(device->DShandle,&DSBDescription,&device->DSpbuffer,NULL)==DS_OK)
						{
							if (IDirectSoundBuffer_SetFormat(device->DSpbuffer,&OutputType)==DS_OK)
							{
								if (IDirectSoundBuffer_QueryInterface(device->DSpbuffer,&IID_IDirectSound3DListener,&device->DS3dlistener)==DS_OK)
								{
									memset(&DSBDescription,0,sizeof(DSBUFFERDESC));
									DSBDescription.dwSize=sizeof(DSBUFFERDESC);
									DSBDescription.dwFlags=DSBCAPS_CTRLVOLUME|DSBCAPS_CTRLFREQUENCY|DSBCAPS_CTRL3D;
									DSBDescription.dwBufferBytes=64;
									DSBDescription.lpwfxFormat=&OutputType;
									if (IDirectSound_CreateSoundBuffer(device->DShandle,&DSBDescription,&DStempbuffer,NULL)==DS_OK)
									{
										if (IDirectSoundBuffer_QueryInterface(DStempbuffer,&IID_IKsPropertySet,&device->DSpropertyset)==DS_OK)
											IKsPropertySet_Set(device->DSpropertyset,&DSPROPSETID_VoiceManager_Def,0,NULL,0,&vmode,sizeof(vmode));
										IDirectSoundBuffer_Release(DStempbuffer);
									}
								}
							}
						}
					}
				}
			}
		}
		else
		{
			//Default to WaveOut code
			if (waveOutOpen(&device->handle,WAVE_MAPPER,&OutputType,0,0,WAVE_FORMAT_DIRECT_QUERY)==MMSYSERR_NOERROR)
			{
				if (waveOutOpen(&device->handle,WAVE_MAPPER,&OutputType,(DWORD)&alcWaveOutProc,(DWORD)0,CALLBACK_FUNCTION)==MMSYSERR_NOERROR)
				{
					// Setup Windows Multimedia driver buffers and start playing
					for (i=0;i<3;i++)
					{
						memset(&device->buffer[i],0,sizeof(WAVEHDR));
						device->buffer[i].lpData=malloc(OutputType.nAvgBytesPerSec/10);
						device->buffer[i].dwBufferLength=OutputType.nAvgBytesPerSec/10;
						device->buffer[i].dwFlags=0;
						device->buffer[i].dwLoops=0;
						waveOutPrepareHeader(device->handle,&device->buffer[i],sizeof(WAVEHDR));
						if (waveOutWrite(device->handle,&device->buffer[i],sizeof(WAVEHDR))!=MMSYSERR_NOERROR)
						{
							waveOutUnprepareHeader(device->handle,&device->buffer[i],sizeof(WAVEHDR));
							free(device->buffer[i].lpData);
						}
					}
				}
			}
		}
	}
	return device;
}

ALCAPI ALCvoid ALCAPIENTRY alcCloseDevice(ALCdevice *device)
{
	ALint i;

	if (device)
	{
		EnterCriticalSection(&device->mutex);
		//Platform specific exit
		if (device->DShandle)
		{
			if (device->DSpropertyset)
				IKsPropertySet_Release(device->DSpropertyset);
			if (device->DS3dlistener)
				IDirectSound3DListener_Release(device->DS3dlistener);
			if (device->DSpbuffer)
				IDirectSoundBuffer_Release(device->DSpbuffer);
			if (device->DShandle)
				IDirectSound_Release(device->DShandle);
			//Deinit COM
			CoUninitialize();		
		}
		else
		{
			waveOutReset(device->handle);
			for (i=0;i<3;i++)
			{
				waveOutUnprepareHeader(device->handle,&device->buffer[i],sizeof(WAVEHDR));
				free(device->buffer[i].lpData);
			}
			waveOutClose(device->handle);
		}
		//Release device structure
		DeleteCriticalSection(&device->mutex);
		memset(device,0,sizeof(ALCdevice));
		free(device);
	}
	else alcSetError(ALC_INVALID_DEVICE);
}

