/***************************************************************************
 * audio.cpp  -  Audio Engine
 *
 * Copyright (C) 2003 - 2008 Florian Richter
 ***************************************************************************/
/*
   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 3 of the License, or
   (at your option) any later version.
   
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "../audio/audio.h"
#include "../audio/sound_manager.h"
#include "../core/game_core.h"
#include "../level/level.h"
#include "../overworld/overworld.h"
#include "../user/preferences.h"
#include "../core/i18n.h"

/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

void Finished_Sound( int channel )
{
	// find the finished sound and free the data
	for( AudioSoundList::iterator itr = pAudio->sounds.begin(), itr_end = pAudio->sounds.end(); itr != itr_end; ++itr )
	{
		cAudio_Sound *obj = (*itr);

		if( obj->m_channel == channel )
		{
			obj->Finished();
		}
	}
}

/* *** *** *** *** *** *** *** *** Audio Sound *** *** *** *** *** *** *** *** *** */

cAudio_Sound :: cAudio_Sound( void )
{
	m_data = NULL;
	m_channel = -1;
	m_resource_id = -1;
}

cAudio_Sound :: ~cAudio_Sound( void )
{
	Free();
}

void cAudio_Sound :: Load( cSound *data )
{
	Free();
	
	m_data = data;
}

void cAudio_Sound :: Free( void )
{
	Stop();

	if( m_data )
	{
		m_data = NULL;
	}
	
	m_channel = -1;
	m_resource_id = -1;
}

void cAudio_Sound :: Finished( void )
{
	m_channel = -1;
}

int cAudio_Sound :: Play( int use_res_id /* = -1 */, int loops /* = 0 */ )
{
	if( !m_data || !m_data->m_chunk )
	{
		return 0;
	}

	if( use_res_id >= 0 )
	{
		for( AudioSoundList::iterator itr = pAudio->sounds.begin(), itr_end = pAudio->sounds.end(); itr != itr_end; ++itr )
		{
			// get object pointer
			cAudio_Sound *obj = (*itr);

			// skip self
			if( !obj || obj->m_channel == m_channel )
			{
				continue;
			}

			// stop Sounds using the given resource id
			if( obj->m_resource_id == use_res_id )
			{
				obj->Stop();
			}
		}
	}

	m_resource_id = use_res_id;
	// play sound
	m_channel = Mix_PlayChannel( -1, m_data->m_chunk, loops );
	// add callback if sound finished playing
	Mix_ChannelFinished( &Finished_Sound );

	return m_channel;
}

void cAudio_Sound :: Stop( void )
{
	// if not loaded or not playing
	if( !m_data || m_channel < 0 )
	{
		return;
	}
	
	Mix_HaltChannel( m_channel );
	m_channel = -1;
}

/* *** *** *** *** *** *** *** *** Audio *** *** *** *** *** *** *** *** *** */

cAudio :: cAudio( void )
{
	initialised = 0;
	sound_enabled = 0;
	music_enabled = 0;

	debug = 0;

	sound_volume = 100;
	music_volume = 80;

	music = NULL;
	music_old = NULL;

	max_sounds = 0;
	sound_count = 0;

	audio_buffer = 4096; // below 2000 can be choppy
	audio_channels = MIX_DEFAULT_CHANNELS; // 1 = Mono, 2 = Stereo

	sounds_played = 0;
	music_played = 0;
}

cAudio :: ~cAudio( void )
{
	Close();
}

bool cAudio :: Init( void )
{
	// Get current device parameters
	int dev_frequency = 0;
	Uint16 dev_format = 0;
	int dev_channels = 0;
	Mix_QuerySpec( &dev_frequency, &dev_format, &dev_channels );

	bool sound = pPreferences->audio_sound;
	bool music = pPreferences->audio_music;

	// if no change
	if( music_enabled == music && sound_enabled == sound && dev_frequency == pPreferences->audio_hz )
	{
		return 1;
	}

	Close();

	// if no audio
	if( !music && !sound )
	{
		return 1;
	}

	// if audio system is not initialised
	if( !initialised )
	{
		if( debug )
		{
			printf( "Initializing Audio System - Buffer %i, Frequency %i, Speaker Channels %i\n", audio_buffer, pPreferences->audio_hz, audio_channels );
		}

		/*	Initializing preferred Audio System specs with Mixer Standard format (Stereo)
		*
		*	frequency	: Output sampling frequency in samples per second (Hz).
		*	format		: Output sample format.
		*	channels	: Number of sound channels in output. 2 for stereo and 1 for mono.
		*	chunk size	: Bytes used per output sample.
		*/

		if( Mix_OpenAudio( pPreferences->audio_hz, MIX_DEFAULT_FORMAT, audio_channels, audio_buffer ) < 0 ) 
		{
			printf( "Warning : Could not init 16-bit Audio\n- Reason : %s\n", SDL_GetError() );
			return 0;
		}

		initialised = 1;
	}


	if( debug )
	{
		printf( "Audio Sound Channels available : %d\n", Mix_AllocateChannels( -1 ) );
	}

	// music initialization
	if( music && !music_enabled )
	{
		music_enabled = 1;

		// set music volume
		Set_Music_Volume( music_volume );
	}
	// music de-initialization
	else if( !music && music_enabled )
	{
		Halt_Music();

		music_enabled = 0;
	}

	// sound initialization
	if( sound && !sound_enabled )
	{
		sound_enabled = 1;

		// create sound array
		Set_Max_Sounds();
		// set sound volume
		Set_Sound_Volume( sound_volume );
	}
	// sound de-initialization
	else if( !sound && sound_enabled )
	{
		Stop_Sounds();

		sound_enabled = 0;
	}

	return 1;
}

void cAudio :: Close( void )
{
	if( initialised )
	{
		if( debug )
		{
			printf( "Closing Audio System\n" );
		}

		if( sound_enabled )
		{
			Stop_Sounds();

			// clear sounds
			for( AudioSoundList::iterator itr = sounds.begin(), itr_end = sounds.end(); itr != itr_end; ++itr )
			{
				delete *itr;
			}

			sounds.clear();

			Mix_AllocateChannels( 0 );
			max_sounds = 0;
			sound_enabled = 0;
		}

		if( music_enabled )
		{
			Halt_Music();

			if( music )
			{
				Mix_FreeMusic( music );
				music = NULL;
			}

			if( music_old )
			{
				Mix_FreeMusic( music_old );
				music_old = NULL;
			}

			music_enabled = 0;
		}

		Mix_CloseAudio();

		initialised = 0;
	}
}

void cAudio :: Set_Max_Sounds( unsigned int limit /* = 10 */ )
{
	if( !initialised || !sound_enabled )
	{
		return;
	}

	// if limit is too small set it to the minimum
	if( limit < 5 )
	{
		limit = 5;
	}

	max_sounds = limit;

	// remove exceeding sounds
	while( sounds.size() > max_sounds )
	{
		delete *sounds.end();
		sounds.erase( sounds.end() );
	}

	// change channels managed by the mixer
	Mix_AllocateChannels( max_sounds );

	if( debug )
	{
		printf( "Audio Sound Channels changed : %d\n", Mix_AllocateChannels( -1 ) );
	}
}

cSound *cAudio :: Get_Sound_File( string filename )
{
	if( !initialised || !sound_enabled )
	{
		return NULL;
	}

	// not available
	if( !File_Exists( filename ) )
	{
		// add sound directory
		if( filename.find( DATA_DIR "/" GAME_SOUNDS_DIR "/" ) == string::npos )
		{
			filename.insert( 0, DATA_DIR "/" GAME_SOUNDS_DIR "/" );
		}
	}

	cSound *sound = pSound_Manager->Get_Pointer( filename );

	// if not already cached
	if( !sound )
	{
		sound = new cSound();

		// loaded sound
		if( sound->Load( filename ) )
		{
			pSound_Manager->Add( sound );

			if( debug )
			{
				printf( "Loaded sound file : %s\n", filename.c_str() );
			}
		}
		// failed loading
		else
		{
			printf( "Could not load sound file : %s \nReason : %s\n", filename.c_str(), SDL_GetError() );
			
			delete sound;
			return NULL;
		}
	}

	return sound;
}

bool cAudio :: Play_Sound( string filename, int res_id /* = -1 */, int volume /* = -1 */, int loops /* = 0 */ )
{
	if( !initialised || !sound_enabled )
	{
		return 0;
	}

	// not available
	if( !File_Exists( filename ) )
	{
		// add sound directory
		if( filename.find( DATA_DIR "/" GAME_SOUNDS_DIR "/" ) == string::npos )
		{
			filename.insert( 0, DATA_DIR "/" GAME_SOUNDS_DIR "/" );
		}

		// not found
		if( !File_Exists( filename ) )
		{
			printf( "Could not find sound file : %s\n", filename.c_str() );
			return 0;
		}
	}

	cSound *sound_data = Get_Sound_File( filename );

	// failed loading
	if( !sound_data )
	{
		printf( "Warning : Could not load sound file : %s\n", filename.c_str() );
		return 0;
	}

	// create channel
	cAudio_Sound *sound = Create_Sound_Channel();

	if( !sound )
	{
		// no free channel available
		return 0;
	}

	// load data
	sound->Load( sound_data );
	// play
	sound->Play( res_id, loops );

	// failed to play
	if( sound->m_channel < 0 )
	{
		if( debug )
		{
			printf( "Could not play sound file : %s\n", filename.c_str() );
		}

		return 0;
	}
	// playing successfully
	else
	{
		sounds_played++;

		// volume is out of range
		if( volume > MIX_MAX_VOLUME )
		{
			printf( "PlaySound Volume is out of range : %d\n", volume );
			volume = sound_volume;
		}
		// no volume is given
		else if( volume < 0 )
		{
			volume = sound_volume;
		}

		// set volume
		Mix_Volume( sound->m_channel, volume );
	}

	return 1;
}

bool cAudio :: Play_Music( string filename, int loops /* = 0 */, bool force /* = 1 */, unsigned int fadein_ms /* = 0 */ )
{
	if( !music_enabled || !initialised )
	{
		return 0;
	}

	if( filename.find( DATA_DIR "/" GAME_MUSIC_DIR "/" ) == string::npos )
	{
		filename.insert( 0, DATA_DIR "/" GAME_MUSIC_DIR "/" );
	}

	// no valid file
	if( !File_Exists( filename ) )
	{
		printf( "Couldn't find music file : %s\n", filename.c_str() );
		return 0;
	}

	// if music is stopped resume it
	Resume_Music();

	// if no music is playing or force to play the given music
	if( !Is_Music_Playing() || force ) 
	{
		// stop and free current music
		if( music )
		{
			Halt_Music();
			Mix_FreeMusic( music );
		}
		// free old music
		if( music_old )
		{
			Mix_FreeMusic( music_old );
			music_old = NULL;
		}

		// load the given music
		music = Mix_LoadMUS( filename.c_str() );

		// loaded
		if( music )
		{
			// count success
			music_played++;

			// no fade in
			if( !fadein_ms )
			{
				Mix_PlayMusic( music, loops );
			}
			// fade in
			else
			{
				Mix_FadeInMusic( music, loops, fadein_ms );
			}
		}
		// not loaded
		else 
		{
			if( debug )
			{
				printf( "Couldn't load music file : %s\n", filename.c_str() );
			}

			// failed to play
			return 0;
		}
	}
	// music is playing and is not forced
	else
	{
		// if music is loaded
		if( music )
		{
			// if old music is loaded free the wanted next playing music data
			if( music_old )
			{
				Mix_FreeMusic( music );
				music = NULL;
			}
			// if no old music move current to old music
			else
			{
				music_old = music;
				music = NULL;
			}
		}

		// load the wanted next playing music
		music = Mix_LoadMUS( filename.c_str() );
	}
	
	return 1;
}

cAudio_Sound *cAudio :: Get_Playing_Sound( string filename )
{
	if( !sound_enabled || !initialised )
	{
		return NULL;
	}

	// add sound directory
	if( filename.find( DATA_DIR "/" GAME_SOUNDS_DIR "/" ) == string::npos )
	{
		filename.insert( 0, DATA_DIR "/" GAME_SOUNDS_DIR "/" );
	}

	// get all sounds
	for( AudioSoundList::iterator itr = sounds.begin(), itr_end = sounds.end(); itr != itr_end; ++itr )
	{
		// get object pointer
		cAudio_Sound *obj = (*itr);

		// if not playing
		if( obj->m_channel < 0 )
		{
			continue;
		}

		// found it
		if( obj->m_data->m_filename.compare( filename ) == 0 )
		{
			// return first found
			return obj;
		}
	}

	// not found
	return NULL;
}

cAudio_Sound *cAudio :: Create_Sound_Channel( void )
{
	// get all sounds
	for( AudioSoundList::iterator itr = sounds.begin(), itr_end = sounds.end(); itr != itr_end; ++itr )
	{
		// get object pointer
		cAudio_Sound *obj = (*itr);

		// if not playing
		if( obj->m_channel < 0 )
		{
			// found a free channel
			obj->Free();
			return obj;
		}
	}

	// if not maximum sounds
	if( sounds.size() < max_sounds )
	{
		cAudio_Sound *sound = new cAudio_Sound();
		sounds.push_back( sound );
		return sound;
	}

	// none found
	return NULL;
}

void cAudio :: Toggle_Music( void )
{
	pPreferences->audio_music = !pPreferences->audio_music;
	Init();

	// play music
	if( music_enabled )
	{
		// valid level music available
		if( pActive_Level->valid_music )
		{
			Play_Music( pActive_Level->musicfile, -1, 1, 2000 );
		}
		// in overworld
		else if( Game_Mode == MODE_OVERWORLD )
		{
			Play_Music( pActive_Overworld->musicfile, -1, 1, 2000 );
		}
		// in menu
		else
		{
			Play_Music( "game/menu.ogg", -1, 1, 2000 );
		}

		// Warning if no music pack is installed and music got enabled
		if( !File_Exists( DATA_DIR "/" GAME_MUSIC_DIR "/game/menu.ogg" ) && !File_Exists( DATA_DIR "/" GAME_MUSIC_DIR "/land/land_1.ogg" ) )
		{
			Draw_Static_Text( _("Warning : No music addon detected"), &orange );
		}
	}
}

void cAudio :: Toggle_Sounds( void )
{
	pPreferences->audio_sound = !pPreferences->audio_sound;
	Init();

	// play a test sound
	if( sound_enabled )
	{
		Play_Sound( "audio_on.ogg" );
	}
}

void cAudio :: Pause_Music( void )
{
	if( !music_enabled || !initialised )
	{
		return;
	}

	// Check if music is currently playing
	if( Mix_PlayingMusic() )
	{
		Mix_PauseMusic();
	}
}

void cAudio :: Resume_Sound( int channel /* = -1 */ )
{
	if( !sound_enabled || !initialised )
	{
		return;
	}

	// resume playback on all previously active channels
	Mix_Resume( channel );
}

void cAudio :: Resume_Music( void )
{
	if( !music_enabled || !initialised )
	{
		return;
	}

	// Check if music is currently paused
	if( Mix_PausedMusic() )
	{
		Mix_ResumeMusic();
	}
}

void cAudio :: Fadeout_Sounds( unsigned int ms /* = 200 */, int channel /* = -1 */, bool overwrite_fading /* = 0 */ )
{
	if( !sound_enabled || !initialised )
	{
		return;
	}

	// Check the Channels
	if( Mix_Playing( channel ) )
	{
		// Do not fade out the sound again
		if( !overwrite_fading && Is_Sound_Fading( -1 ) == MIX_FADING_OUT )
		{
			return;
		}

		Mix_FadeOutChannel( channel, ms );
	}
}

void cAudio :: Fadeout_Sounds( unsigned int ms, string filename, bool overwrite_fading /* = 0 */ )
{
	if( !sound_enabled || !initialised )
	{
		return;
	}

	// add sound directory
	if( filename.find( DATA_DIR "/" GAME_SOUNDS_DIR "/" ) == string::npos )
	{
		filename.insert( 0, DATA_DIR "/" GAME_SOUNDS_DIR "/" );
	}

	// get all sounds
	for( AudioSoundList::iterator itr = sounds.begin(), itr_end = sounds.end(); itr != itr_end; ++itr )
	{
		// get object pointer
		cAudio_Sound *obj = (*itr);

		// filename does not match
		if( obj->m_data->m_filename.compare( filename ) != 0 )
		{
			continue;
		}

		// Do not fade out the sound again
		if( !overwrite_fading && Is_Sound_Fading( obj->m_channel ) == MIX_FADING_OUT )
		{
			continue;
		}

		Mix_FadeOutChannel( obj->m_channel, ms );
	}
}

void cAudio :: Fadeout_Music( unsigned int ms /* = 500 */, bool overwrite_fading /* = 0 */ )
{
	if( !music_enabled || !initialised )
	{
		return;
	}

	// if music is currently playing
	if( Mix_PlayingMusic() )
	{
		Mix_Fading status = Is_Music_Fading();

		// don't fade the music out again
		if( !overwrite_fading && status == MIX_FADING_OUT )
		{
			return;
		} 
		// Can't stop fade in with SDL_Mixer and fade out is ignored when fading in
		else if( status == MIX_FADING_IN )
		{
			Halt_Music();
		}

		Mix_FadeOutMusic( ms );
	}
}

void cAudio :: Set_Music_Position( float position )
{
	if( !music_enabled || !initialised || Is_Music_Fading() == MIX_FADING_OUT )
	{
		return;
	}

	Mix_SetMusicPosition( position );
}

Mix_Fading cAudio :: Is_Music_Fading( void )
{
	if( !music_enabled || !initialised )
	{
		return MIX_NO_FADING;
	}

	return Mix_FadingMusic();
}

Mix_Fading cAudio :: Is_Sound_Fading( int sound_channel )
{
	if( !sound_enabled || !initialised )
	{
		return MIX_NO_FADING;
	}

	 return Mix_FadingChannel( sound_channel );
}

bool cAudio :: Is_Music_Paused( void )
{
	if( !music_enabled || !initialised )
	{
		return 0;
	}
	
	if( Mix_PausedMusic() )
	{
		return 1;
	}
	
	return 0;
}

bool cAudio :: Is_Music_Playing( void )
{
	if( !music_enabled || !initialised )
	{
		return 0;
	}
	
	if( Mix_PlayingMusic() )
	{
		return 1;
	}
	
	return 0;
}

void cAudio :: Halt_Sounds( int channel /* = -1 */ )
{
	if( !sound_enabled || !initialised )
	{
		return;
	}

	// Check all Channels
	if( Mix_Playing( channel ) )
	{
		Mix_HaltChannel( channel );
	}
}

void cAudio :: Halt_Music( void )
{
	if( !initialised )
	{
		return;
	}

	// Checks if music is playing
	if( Mix_PlayingMusic() )
	{
		Mix_HaltMusic();
	}
}

void cAudio :: Stop_Sounds( void )
{
	if( !initialised )
	{
		return;
	}

	// Stop all channels
	if( Mix_Playing( -1 ) )
	{
		Mix_HaltChannel( -1 );
	}
}

void cAudio :: Set_Sound_Volume( Uint8 volume, int channel /* = -1 */ )
{
	if( volume > MIX_MAX_VOLUME || !initialised )
	{
		return;
	}

	Mix_Volume( channel, volume );
}

void cAudio :: Set_Music_Volume( Uint8 volume )
{
	if( volume > MIX_MAX_VOLUME || !initialised )
	{
		return;
	}

	Mix_VolumeMusic( volume );
}

void cAudio :: Update( void )
{
	if( !initialised )
	{
		return;
	}

	// if music is enabled
	if( music_enabled )
	{
		// if no music is playing
		if( !Mix_PlayingMusic() && music ) 
		{
			Mix_PlayMusic( music, 0 );
			music_played++;

			// delete old music if available
			if( music_old )
			{
				Mix_FreeMusic( music_old );
				music_old = NULL;
			}
		}
	}
}

/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

cAudio *pAudio = NULL;
