//     ____                _       __               
//    / __ )____  _____   | |     / /___ ___________
//   / __  / __ \/ ___/   | | /| / / __ `/ ___/ ___/
//  / /_/ / /_/ (__  )    | |/ |/ / /_/ / /  (__  ) 
// /_____/\____/____/     |__/|__/\__,_/_/  /____/  
//                                              
//       A futuristic real-time strategy game.
//          This file is part of Bos Wars.
//
/**@name sound.cpp - The sound. */
//
//      (c) Copyright 1998-2008 by Lutz Sammer, Fabrice Rossi,
//                                 and Jimmy Salmon
//
//      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; only version 2 of the License.
//
//      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.
//

//@{

/*----------------------------------------------------------------------------
--  Includes
----------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "stratagus.h"

#include "SDL.h"

#include "sound.h"
#include "video.h"
#include "sound.h"
#include "unitsound.h"
#include "unittype.h"
#include "player.h"
#include "unit.h"
#include "sound_server.h"
#include "missile.h"
#include "map.h"
#include "ui.h"
#include "widgets.h"

/*----------------------------------------------------------------------------
--  Variables
----------------------------------------------------------------------------*/

/**
**  Various sounds used in game.
**
**  FIXME: @todo Must remove static config.
*/
GameSound GameSounds
#ifndef laterUSE_CCL
={
	SoundConfig("placement error"),
	SoundConfig("placement success"),
	SoundConfig("click"),
	SoundConfig("transport docking"),
	SoundConfig("building construction"),
	SoundConfig("rescue (human) UNUSED"),
}
#endif
	;

/**
**  Selection handling
*/
struct SelectionHandling {
	Origin Source;         /// origin of the sound
	CSound *Sound;         /// last sound played by this unit
	unsigned char HowMany; /// number of sound played in this group
};

SelectionHandling SelectionHandler;

static int ViewPointOffset;      /// Distance to Volume Mapping
int DistanceSilent;              /// silent distance

/*----------------------------------------------------------------------------
--  Functions
----------------------------------------------------------------------------*/

/**
**  "Randomly" choose a sample from a sound group.
*/
static CSample *SimpleChooseSample(const CSound *sound)
{
	if (sound->Number == ONE_SOUND) {
		return sound->Sound.OneSound;
	} else {
		return sound->Sound.OneGroup[FrameCounter % sound->Number];
	}
}

/**
**  Choose the sample to play
*/
static CSample *ChooseSample(CSound *sound, bool selection, Origin &source)
{
	CSample *result = NULL;

	if (!sound || !SoundEnabled()) {
		return NULL;
	}

	if (sound->Number == TWO_GROUPS) {
		// handle a special sound (selection)
		if (SelectionHandler.Source.Base == source.Base &&
				SelectionHandler.Source.Id == source.Id) {
			if (SelectionHandler.Sound == sound->Sound.TwoGroups.First) {
				result = SimpleChooseSample(SelectionHandler.Sound);
				SelectionHandler.HowMany++;
				if (SelectionHandler.HowMany >= 3) {
					SelectionHandler.HowMany = 0;
					SelectionHandler.Sound = sound->Sound.TwoGroups.Second;
				}
			} else {
				// check whether the second group is really a group
				if (SelectionHandler.Sound->Number > 1) {
					result = SelectionHandler.Sound->Sound.OneGroup[SelectionHandler.HowMany];
					SelectionHandler.HowMany++;
					if (SelectionHandler.HowMany >= SelectionHandler.Sound->Number) {
						SelectionHandler.HowMany = 0;
						SelectionHandler.Sound = sound->Sound.TwoGroups.First;
					}
				} else {
					result = SelectionHandler.Sound->Sound.OneSound;
					SelectionHandler.HowMany = 0;
					SelectionHandler.Sound = sound->Sound.TwoGroups.First;
				}
			}
		} else {
			SelectionHandler.Source = source;
			SelectionHandler.Sound = sound->Sound.TwoGroups.First;
			result = SimpleChooseSample(SelectionHandler.Sound);
			SelectionHandler.HowMany = 1;
		}
	} else {
		// normal sound/sound group handling
		result = SimpleChooseSample(sound);
		if (selection) {
			SelectionHandler.Source = source;
		}
	}

	return result;
}

/**
**  Maps a UnitVoiceGroup to a CSound*.
**
**  @param unit    Sound initiator
**  @param voice   Type of sound wanted
**
**  @return        Sound identifier
*/
static CSound *ChooseUnitVoiceSound(const CUnit *unit, UnitVoiceGroup voice)
{
	switch (voice) {
		case VoiceAcknowledging:
			return unit->Type->Sound.Acknowledgement.Sound;
		case VoiceReady:
			return unit->Type->Sound.Ready.Sound;
		case VoiceSelected:
			return unit->Type->Sound.Selected.Sound;
		case VoiceHelpMe:
			return unit->Type->Sound.Help.Sound;
		case VoiceDying:
			return unit->Type->Sound.Dead.Sound;
		case VoiceBuilding:
			return GameSounds.BuildingConstruction.Sound;
		case VoiceDocking:
			return GameSounds.Docking.Sound;
	}

	return NO_SOUND;
}

/**
**  Compute a suitable volume for something taking place at a given
**  distance from the current view point.
**
**  @param d      distance
**  @param range  range
**
**  @return       volume for given distance (0..??)
*/
static unsigned char VolumeForDistance(unsigned short d, unsigned char range)
{
	int d_tmp;
	int range_tmp;

	// FIXME: THIS IS SLOW!!!!!!!
	if (d <= ViewPointOffset || range == INFINITE_SOUND_RANGE) {
		return MaxVolume;
	} else {
		if (range) {
			d -= ViewPointOffset;
			d_tmp = d * MAX_SOUND_RANGE;
			range_tmp = DistanceSilent * range;
			if (d_tmp > range_tmp) {
				return 0;
			} else {
				return (unsigned char)((range_tmp - d_tmp) *
					MAX_SOUND_RANGE / range_tmp);
			}
		} else {
			return 0;
		}
	}
}

/**
**  Calculate the volume associated with a request, either by clipping the
**  range parameter of this request, or by mapping this range to a volume.
*/
static unsigned char CalculateVolume(bool isVolume, int power,
	unsigned char range)
{
	if (isVolume) {
		if (power > MaxVolume) {
			return MaxVolume;
		} else {
			return (unsigned char)power;
		}
	} else {
		// map distance to volume
		return VolumeForDistance(power, range);
	}
}

/**
**  Calculate the stereo value for a unit
*/
static char CalculateStereo(const CUnit *unit)
{
	int stereo;

	stereo = ((unit->X * TileSizeX + unit->Type->TileWidth * TileSizeX / 2 +
		unit->IX - UI.SelectedViewport->MapX * TileSizeX) * 256 /
		((UI.SelectedViewport->MapWidth - 1) * TileSizeX)) - 128;
	if (stereo < -128) {
		stereo = -128;
	} else if (stereo > 127) {
		stereo = 127;
	}

	return stereo;
}

/**
**  Ask to the sound server to play a sound attached to a unit. The
**  sound server may discard the sound if needed (e.g., when the same
**  unit is already speaking).
**
**  @param unit   Sound initiator, unit speaking
**  @param voice  Type of sound wanted (Ready,Die,Yes,...)
*/
void PlayUnitSound(const CUnit *unit, UnitVoiceGroup voice)
{
	CSound *sound = ChooseUnitVoiceSound(unit, voice);
	if (!sound) {
		return;
	}

	bool selection = (voice == VoiceSelected || voice == VoiceBuilding);
	Origin source = {unit, unit->Slot};

	int channel = PlaySample(ChooseSample(sound, selection, source));
	if (channel == -1) {
		return;
	}
	SetChannelVolume(channel, CalculateVolume(false, ViewPointDistanceToUnit(unit), sound->Range));
	SetChannelStereo(channel, CalculateStereo(unit));
}

/**
**  Ask to the sound server to play a sound attached to a unit. The
**  sound server may discard the sound if needed (e.g., when the same
**  unit is already speaking).
**
**  @param unit   Sound initiator, unit speaking
**  @param sound  Sound to be generated
*/
void PlayUnitSound(const CUnit *unit, CSound *sound)
{
	Origin source = {unit, unit->Slot};
	int channel = PlaySample(ChooseSample(sound, false, source));
	if (channel == -1) {
		return;
	}
	SetChannelVolume(channel, CalculateVolume(false, ViewPointDistanceToUnit(unit), sound->Range));
	SetChannelStereo(channel, CalculateStereo(unit));
}

/**
**  Ask the sound server to play a sound for a missile.
**
**  @param missile  Sound initiator, missile exploding
**  @param sound    Sound to be generated
*/
void PlayMissileSound(const Missile *missile, CSound *sound)
{
	int stereo;

	stereo = ((missile->X + missile->Type->G->Width / 2 -
		UI.SelectedViewport->MapX * TileSizeX) * 256 /
		((UI.SelectedViewport->MapWidth - 1) * TileSizeX)) - 128;
	if (stereo < -128) {
		stereo = -128;
	} else if (stereo > 127) {
		stereo = 127;
	}

	Origin source = {NULL, 0};

	int channel = PlaySample(ChooseSample(sound, false, source));
	if (channel == -1) {
		return;
	}
	SetChannelVolume(channel, CalculateVolume(false, ViewPointDistanceToMissile(missile), sound->Range));
	SetChannelStereo(channel, stereo);
}

/**
**  Play a game sound
**
**  @param sound   Sound to play
**  @param volume  Volume level to play the sound
*/
void PlayGameSound(CSound *sound, unsigned char volume)
{
	Origin source = {NULL, 0};

	int channel = PlaySample(ChooseSample(sound, false, source));
	if (channel == -1) {
		return;
	}
	SetChannelVolume(channel, CalculateVolume(true, volume, sound->Range));
}

static std::map<int, LuaActionListener *> ChannelMap;

/**
**  Callback for PlaySoundFile
*/
static void PlaySoundFileCallback(int channel)
{
	LuaActionListener *listener = ChannelMap[channel];
	if (listener != NULL) {
		listener->action("");
		ChannelMap[channel] = NULL;
	}
	delete GetChannelSample(channel);
}

/**
**  Play a sound file
**
**  @param name      Filename of a sound to play
**  @param listener  Optional lua callback
**
**  @return          Channel number the sound is playing on, -1 for error
*/
int PlayFile(const std::string &name, LuaActionListener *listener)
{
	int channel = -1;
	CSample *sample;

	sample = LoadSample(name);
	if (sample) {
		channel = PlaySample(sample);
		if (channel != -1) {
			SetChannelVolume(channel, MaxVolume);
			SetChannelFinishedCallback(channel, PlaySoundFileCallback);
			ChannelMap[channel] = listener;
		}
	}

	return channel;
}

/**
**  Ask the sound server to change the range of a sound.
**
**  @param sound  the id of the sound to modify.
**  @param range  the new range for this sound.
*/
void SetSoundRange(CSound *sound, unsigned char range)
{
	if (sound != NO_SOUND) {
		sound->Range = range;
	}
}

/**
**  Ask the sound server to register a sound (and currently to load it)
**  and to return an unique identifier for it. The unique identifier is
**  memory pointer of the server.
**
**  @param files   An array of wav files.
**  @param number  Number of files belonging together.
**
**  @return        the sound unique identifier
*/
CSound *RegisterSound(const char *files[], unsigned number)
{
	unsigned i;
	CSound *id;

	id = new CSound;
	if (number > 1) { // load a sound group
		id->Sound.OneGroup = new CSample *[number];
		id->Number = number;
		for (i = 0; i < number; ++i) {
			id->Sound.OneGroup[i] = LoadSample(files[i]);
			if (!id->Sound.OneGroup[i]) {
				delete id;
				return NO_SOUND;
			}
		}
	} else { // load a unique sound
		id->Sound.OneSound = LoadSample(files[0]);
		if (!id->Sound.OneSound) {
			delete id;
			return NO_SOUND;
		}
		id->Number = ONE_SOUND;
	}
	id->Range = MAX_SOUND_RANGE;
	return id;
}

/**
**  Ask the sound server to put together two sounds to form a special sound.
**
**  @param first   first part of the group
**  @param second  second part of the group
**
**  @return        the special sound unique identifier
*/
CSound *RegisterTwoGroups(CSound *first, CSound *second)
{
	CSound *id;

	if (first == NO_SOUND || second == NO_SOUND) {
		return NO_SOUND;
	}
	id = new CSound;
	id->Number = TWO_GROUPS;
	id->Sound.TwoGroups.First = first;
	id->Sound.TwoGroups.Second = second;
	id->Range = MAX_SOUND_RANGE;

	return id;
}

/**
**  Lookup the sound id's for the game sounds.
*/
void InitSoundClient(void)
{
	if (!SoundEnabled()) { // No sound enabled
		return;
	}
	// let's map game sounds, look if already setup in ccl.

	if (!GameSounds.PlacementError.Sound) {
		GameSounds.PlacementError.Sound =
			SoundForName(GameSounds.PlacementError.Name);
	}
	if (!GameSounds.PlacementSuccess.Sound) {
		GameSounds.PlacementSuccess.Sound =
			SoundForName(GameSounds.PlacementSuccess.Name);
	}
	if (!GameSounds.Click.Sound) {
		GameSounds.Click.Sound = SoundForName(GameSounds.Click.Name);
	}
	if (!GameSounds.Docking.Sound) {
		GameSounds.Docking.Sound =
			SoundForName(GameSounds.Docking.Name);
	}
	if (!GameSounds.BuildingConstruction.Sound) {
		GameSounds.BuildingConstruction.Sound =
			SoundForName(GameSounds.BuildingConstruction.Name);
	}
	if (!GameSounds.Rescue.Sound && !GameSounds.Rescue.Name.empty()) {
		GameSounds.Rescue.Sound =
			SoundForName(GameSounds.Rescue.Name);
	}
	if (!GameSounds.ChatMessage.Sound && !GameSounds.ChatMessage.Name.empty()) {
		GameSounds.ChatMessage.Sound =
			SoundForName(GameSounds.ChatMessage.Name);
	}

	int MapWidth = (UI.MapArea.EndX - UI.MapArea.X + TileSizeX) / TileSizeX;
	int MapHeight = (UI.MapArea.EndY - UI.MapArea.Y + TileSizeY) / TileSizeY;
	DistanceSilent = 3 * std::max(MapWidth, MapHeight);
	ViewPointOffset = std::max(MapWidth / 2, MapHeight / 2);
}


CSound::~CSound()
{
	if (this->Number == ONE_SOUND) {
		delete Sound.OneSound;
	} else if (this->Number == TWO_GROUPS) {
	} else {
		for (int i = 0; i < this->Number; ++i) {
			delete this->Sound.OneGroup[i];
		}
		delete[] this->Sound.OneGroup;
	}
}


//@}
