/*
   Copyright (C) 1997-2001 Id Software, Inc.

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 */

#include "snd_local.h"

struct mempool_s *soundpool;

cvar_t *s_volume;
cvar_t *s_musicvolume;
cvar_t *s_attenuation_model;
cvar_t *s_attenuation_maxdistance;
cvar_t *s_attenuation_refdistance;
cvar_t *s_openAL_device;

static cvar_t *s_doppler;

static qboolean snd_shutdown_bug = qfalse;
static ALCdevice *alDevice;
static ALCcontext *alContext;
static char *alDevices[256], alCurDevice;

/*
 * Commands
 */

#ifdef ENABLE_PLAY
static void S_Play( void )
{
	int i;
	char name[MAX_QPATH];

	i = 1;
	while( i < trap_Cmd_Argc() )
	{
		Q_strncpyz( name, trap_Cmd_Argv( i ), sizeof( name ) );

		S_StartLocalSound( name );
		i++;
	}
}
#endif // ENABLE_PLAY

static void S_Music( void )
{
	if( trap_Cmd_Argc() == 2 )
	{
		S_StartBackgroundTrack( trap_Cmd_Argv( 1 ), trap_Cmd_Argv( 1 ) );
	}
	else if( trap_Cmd_Argc() == 3 )
	{
		S_StartBackgroundTrack( trap_Cmd_Argv( 1 ), trap_Cmd_Argv( 2 ) );
	}
	else
	{
		Com_Printf( "music <intro> <loop>\n" );
		return;
	}
}

static void S_StopMusic( void )
{
	S_StopBackgroundTrack();
}

static void S_ListDevices( void )
{
	int i;
	char *device, *defaultDevice;

	defaultDevice = ( char * )qalcGetString( NULL, ALC_DEFAULT_DEVICE_SPECIFIER );
	device = ( char * )qalcGetString( NULL, ALC_DEVICE_SPECIFIER );
	if( alDevices[0] )
	{
		Com_Printf( "Available OpenAL devices:\n" );
		for( i = 0; alDevices[i]; i++ )
		{
			Com_Printf( "%s%i. %s\n", ( i+1 == alCurDevice ? "-> " : "" ), i+1, alDevices[i] );
		}
	}

	if( defaultDevice && *defaultDevice )
	{
		Com_Printf( "--------------\n" );
		Com_Printf( "Default device:\n" );
		Com_Printf( "%s\n", defaultDevice );
	}
}

/*
 * Sound system wide functions (snd_loc.h)
 */

ALuint S_SoundFormat( int width, int channels )
{
	if( width == 1 )
	{
		if( channels == 1 )
			return AL_FORMAT_MONO8;
		else if( channels == 2 )
			return AL_FORMAT_STEREO8;
	}
	else if( width == 2 )
	{
		if( channels == 1 )
			return AL_FORMAT_MONO16;
		else if( channels == 2 )
			return AL_FORMAT_STEREO16;
	}

	Com_Printf( "Unknown sound format: %i channels, %i bits.\n", channels, width * 8 );
	return AL_FORMAT_MONO16;
}

// OpenAL error messages
const char *S_ErrorMessage( ALenum error )
{
	switch( error )
	{
	case AL_NO_ERROR:
		return "No error";
	case AL_INVALID_NAME:
		return "Invalid name";
	case AL_INVALID_ENUM:
		return "Invalid enumerator";
	case AL_INVALID_VALUE:
		return "Invalid value";
	case AL_INVALID_OPERATION:
		return "Invalid operation";
	case AL_OUT_OF_MEMORY:
		return "Out of memory";
	default:
		return "Unknown error";
	}
}

/*
 * Global functions (sound.h)
 */

int S_API( void )
{
	return SOUND_API_VERSION;
}

void S_Error( const char *format, ... )
{
	va_list	argptr;
	char msg[1024];

	va_start( argptr, format );
	Q_vsnprintfz( msg, sizeof( msg ), format, argptr );
	va_end( argptr );

	trap_Error( msg );
}

#ifndef SOUND_HARD_LINKED
// this is only here so the functions in q_shared.c and q_math.c can link
void Sys_Error( const char *format, ... )
{
	va_list	argptr;
	char msg[1024];

	va_start( argptr, format );
	Q_vsnprintfz( msg, sizeof( msg ), format, argptr );
	va_end( argptr );

	trap_Error( msg );
}

void Com_Printf( const char *format, ... )
{
	va_list	argptr;
	char msg[1024];

	va_start( argptr, format );
	Q_vsnprintfz( msg, sizeof( msg ), format, argptr );
	va_end( argptr );

	trap_Print( msg );
}
#endif

#ifdef _WIN32
#define ALDRIVER "OpenAL32.dll"
#elif defined ( __MACOSX__ )
#define ALDRIVER "/System/Library/Frameworks/OpenAL.framework/OpenAL"
#else
#define ALDRIVER "libopenal.so.0"
#endif
qboolean S_Init( void *hwnd, qboolean verbose )
{
	int defaultDeviceNum, numDevices;
	char *device, *defaultDevice;

	soundpool = S_MemAllocPool( "OpenAL sound module" );

#ifdef OPENAL_RUNTIME
	if( !QAL_Init( ALDRIVER ) )
	{
		Com_Printf( "Failed to load OpenAL library: %s\n", ALDRIVER );
		goto fail_no_device;
	}
#endif

	s_openAL_device = trap_Cvar_Get( "s_openAL_device", "0", CVAR_ARCHIVE );

	// get default device identifier
	defaultDevice = ( char * )qalcGetString( NULL, ALC_DEFAULT_DEVICE_SPECIFIER );
	defaultDeviceNum = 1;

	numDevices = 0;
	device = ( char * )qalcGetString( NULL, ALC_DEVICE_SPECIFIER );
	if( device && *device )
	{
		for(; *device; device += strlen( device ) + 1 )
		{
			if( numDevices == sizeof( alDevices ) / sizeof( *alDevices )-1 )
				break;

			alDevices[numDevices] = S_Malloc( strlen( device ) + 1 );
			strcpy( alDevices[numDevices], device );
			if( defaultDevice && !strcmp( defaultDevice, device ) )
				defaultDeviceNum = numDevices + 1;
			numDevices++;
		}
	}
	alDevices[numDevices] = 0;

	if( !numDevices )
		alCurDevice = 0;
	else if( s_openAL_device->integer == 0 )
		alCurDevice = defaultDeviceNum;
	else
		alCurDevice = bound( 1, s_openAL_device->integer, numDevices );
	alDevice = qalcOpenDevice( alCurDevice ? (const ALchar *)alDevices[alCurDevice-1] : NULL );
	if( !alDevice )
	{
		Com_Printf( "Failed to open device\n" );
		goto fail_no_device;
	}

	// Create context
	alContext = qalcCreateContext( alDevice, NULL );
	if( !alContext )
	{
		Com_Printf( "Failed to create context\n" );
		goto fail;
	}
	qalcMakeContextCurrent( alContext );

	if( verbose )
	{
		Com_Printf( "OpenAL initialised\n" );
		Com_Printf( "  Device:     %s\n", qalcGetString( alDevice, ALC_DEVICE_SPECIFIER ) );
		Com_Printf( "  Vendor:     %s\n", qalGetString( AL_VENDOR ) );
		Com_Printf( "  Version:    %s\n", qalGetString( AL_VERSION ) );
		Com_Printf( "  Renderer:   %s\n", qalGetString( AL_RENDERER ) );
		Com_Printf( "  Extensions: %s\n", qalGetString( AL_EXTENSIONS ) );
	}

	// Check for Linux shutdown race condition
	if( !Q_stricmp( qalGetString( AL_VENDOR ), "J. Valenzuela" ) )
		snd_shutdown_bug = qtrue;

	s_volume = trap_Cvar_Get( "s_volume", "0.8", CVAR_ARCHIVE );
	s_musicvolume = trap_Cvar_Get( "s_musicvolume", "0.8", CVAR_ARCHIVE );
	s_doppler = trap_Cvar_Get( "s_doppler", "0", CVAR_DEVELOPER );
	s_attenuation_model = trap_Cvar_Get( "s_attenuation_model", S_DEFAULT_ATTENUATION_MODEL, CVAR_DEVELOPER|CVAR_LATCH_SOUND );
	s_attenuation_maxdistance = trap_Cvar_Get( "s_attenuation_maxdistance", S_DEFAULT_ATTENUATION_MAXDISTANCE, CVAR_DEVELOPER|CVAR_LATCH_SOUND );
	s_attenuation_refdistance = trap_Cvar_Get( "s_attenuation_refdistance", "160", CVAR_DEVELOPER|CVAR_LATCH_SOUND );

	qalDopplerFactor( s_doppler->value );
	qalDopplerVelocity( 2200 );

	switch( s_attenuation_model->integer )
	{
	case 0:
		qalDistanceModel( AL_LINEAR_DISTANCE );
		break;
	case 1:
	default:
		qalDistanceModel( AL_LINEAR_DISTANCE_CLAMPED );
		break;
	case 2:
		qalDistanceModel( AL_INVERSE_DISTANCE );
		break;
	case 3:
		qalDistanceModel( AL_INVERSE_DISTANCE_CLAMPED );
		break;
	case 4:
		qalDistanceModel( AL_EXPONENT_DISTANCE );
		break;
	case 5:
		qalDistanceModel( AL_EXPONENT_DISTANCE_CLAMPED );
		break;
	}

	s_doppler->modified = qfalse;

	if( !S_InitDecoders( verbose ) )
	{
		Com_Printf( "Failed to init decoders\n" );
		goto fail;
	}
	if( !S_InitBuffers() )
	{
		Com_Printf( "Failed to init buffers\n" );
		goto fail;
	}
	if( !S_InitSources() )
	{
		Com_Printf( "Failed to init sources\n" );
		goto fail;
	}

#ifdef ENABLE_PLAY
	trap_Cmd_AddCommand( "play", S_Play );
#endif
	trap_Cmd_AddCommand( "music", S_Music );
	trap_Cmd_AddCommand( "stopmusic", S_StopMusic );
	trap_Cmd_AddCommand( "soundlist", S_SoundList );
	trap_Cmd_AddCommand( "sounddevices", S_ListDevices );

	return qtrue;

fail:
	if( !snd_shutdown_bug )
		qalcMakeContextCurrent( NULL );

	qalcDestroyContext( alContext );
	qalcCloseDevice( alDevice );
fail_no_device:
	S_MemFreePool( &soundpool );
	return qfalse;
}

void S_Shutdown( qboolean verbose )
{
	int i;

	S_StopStream();
	S_StopBackgroundTrack();

#ifdef ENABLE_PLAY
	trap_Cmd_RemoveCommand( "play" );
#endif
	trap_Cmd_RemoveCommand( "music" );
	trap_Cmd_RemoveCommand( "stopmusic" );
	trap_Cmd_RemoveCommand( "soundlist" );
	trap_Cmd_RemoveCommand( "sounddevices" );

	S_ShutdownSources();
	S_ShutdownBuffers();

	if( !snd_shutdown_bug )
		qalcMakeContextCurrent( NULL );

	qalcDestroyContext( alContext );
	qalcCloseDevice( alDevice );

	S_ShutdownDecoders( verbose );

	QAL_Shutdown();

	for( i = 0; alDevices[i]; i++ )
	{
		S_Free( alDevices[i] );
		alDevices[i] = NULL;
	}
	S_MemFreePool( &soundpool );
}

void S_Update( const vec3_t origin, const vec3_t velocity, const vec3_t forward, const vec3_t right, const vec3_t up )
{
	int i;
	float orientation[6];

	for( i = 0; i < 3; i++ )
	{
		orientation[i] = forward[i];
		orientation[i+3] = up[i];
	}

	qalListenerfv( AL_POSITION, origin );
	qalListenerfv( AL_VELOCITY, velocity );
	qalListenerfv( AL_ORIENTATION, orientation );

	S_UpdateSources();
	S_UpdateStream();
	S_UpdateMusic();

	s_volume->modified = qfalse; // Checked by src and stream
	s_musicvolume->modified = qfalse; // Checked by stream and music

	if( s_doppler->modified )
	{
		if( s_doppler->integer )
			qalDopplerFactor( s_doppler->value );
		else
			qalDopplerFactor( 0.0f );
		s_doppler->modified = qfalse;
	}
}

void S_StopAllSounds( void )
{
	S_StopAllSources();
	S_StopBackgroundTrack();
}

void S_Activate( qboolean activate )
{
	// TODO: Actually stop playing sounds while not active?
	if( activate )
		qalListenerf( AL_GAIN, 1 );
	else
		qalListenerf( AL_GAIN, 0 );
}
