// Filename:-	snd_music.cpp
//
//  Stuff to parse in special x-fade music format and handle blending etc

//Anything above this #include will be ignored by the compiler
#include "../server/exe_headers.h"


#include "../qcommon/sstring.h"
#include <algorithm>

#ifdef _XBOX
#include "snd_local_console.h"
#include <xtl.h>
#else
#include "snd_local.h"
#include "cl_mp3.h"
#endif

//
#include "snd_music.h"

extern qboolean S_FileExists( const char *psFilename );

#ifdef _XBOX
extern void Z_SetNewDeleteTemporary(bool bTemp);
#endif

#define sKEY_MUSICFILES	"musicfiles"
#define sKEY_ENTRY		"entry"
#define sKEY_EXIT		"exit"
#define sKEY_MARKER		"marker"
#define sKEY_TIME		"time"
#define sKEY_NEXTFILE	"nextfile"
#define sKEY_NEXTMARK	"nextmark"
#define sKEY_LEVELMUSIC	"levelmusic"
#define sKEY_EXPLORE	"explore"
#define sKEY_ACTION		"action"
#define sKEY_BOSS		"boss"
#define sKEY_DEATH		"death"
#define sKEY_USES		"uses"
#define sKEY_USEBOSS	"useboss"

#define sKEY_PLACEHOLDER "placeholder"	// ignore these

#define sFILENAME_DMS	"ext_data/dms.dat"


#define MUSIC_PARSE_ERROR(_string)		Music_Parse_Error(_string)	// only use during parse, not run-time use, and bear in mid that data is zapped after error message, so exit any loops immediately
#define MUSIC_PARSE_WARNING(_string)	Music_Parse_Warning(_string)

typedef struct
{
	sstring_t	sNextFile;
	sstring_t	sNextMark;		// blank if used for an explore piece, name of marker point to enter new file at

} MusicExitPoint_t;

struct MusicExitTime_t	// need to declare this way for operator < below
{
	float		fTime;
	int			iExitPoint;

	// I'm defining this '<' operator so STL's sort algorithm will work
	//
	bool operator < (const MusicExitTime_t& _X) const {return (fTime < _X.fTime);}
};

// it's possible for all 3 of these to be empty if it's boss or death music
//
typedef vector	<MusicExitPoint_t>	MusicExitPoints_t;
typedef vector	<MusicExitTime_t>	MusicExitTimes_t;
typedef map		<sstring_t, float>	MusicEntryTimes_t;	// key eg "marker1"

typedef struct
{
	sstring_t			sFileNameBase;
	MusicEntryTimes_t	MusicEntryTimes;
	MusicExitPoints_t	MusicExitPoints;
	MusicExitTimes_t	MusicExitTimes;

} MusicFile_t;

typedef map <sstring_t, MusicFile_t>	MusicData_t;			// string is "explore", "action", "boss" etc
										MusicData_t* MusicData = NULL;
// there are now 2 of these, because of the new "uses" keyword...
//
sstring_t	gsLevelNameForLoad;		// eg "kejim_base", formed from literal BSP name, but also used as dir name for music paths
sstring_t	gsLevelNameForCompare;	// eg "kejim_base", formed from literal BSP name, but also used as dir name for music paths
sstring_t	gsLevelNameForBossLoad;	// eg "kejim_base', special case for enabling boss music to come from a different dir - sigh....

void Music_Free(void)
{
#ifdef _XBOX
	// Prevents pending state changes from crashing the game after
	// level loads, but before new music data has been parsed.
	extern void S_AvertMusicDisaster(void);
	S_AvertMusicDisaster();
#endif

	if (MusicData)
	{
#ifdef _XBOX
		delete MusicData;
#else
		MusicData->clear();
#endif
	}
	MusicData = NULL;
}

// some sort of error in the music data...
//
static void Music_Parse_Error(LPCSTR psError)
{
	Com_Printf(S_COLOR_RED "Error parsing music data ( in \"%s\" ):\n%s\n",sFILENAME_DMS,psError);
	MusicData->clear();
}

// something to just mention if interested...
//
static void Music_Parse_Warning(LPCSTR psError)
{
	extern cvar_t *s_debugdynamic;
	if (s_debugdynamic && s_debugdynamic->integer)
	{
		Com_Printf(S_COLOR_YELLOW "%s", psError);
	}	
}

// the 2nd param here is pretty kludgy (sigh), and only used for testing for the "boss" type.
// Unfortunately two of the places that calls this doesn't have much other access to the state other than
//	a string, not an enum, so for those cases they only pass in BOSS or EXPLORE, so don't rely on it totally.
//
static LPCSTR Music_BuildFileName(LPCSTR psFileNameBase, MusicState_e eMusicState )
{
	static sstring_t sFileName;

	//HACK!
	if (eMusicState == eBGRNDTRACK_DEATH)
	{
		return "music/death_music.mp3";
	}

	LPCSTR psDirName = (eMusicState == eBGRNDTRACK_BOSS) ? gsLevelNameForBossLoad.c_str() : gsLevelNameForLoad.c_str();	

	sFileName = va("music/%s/%s.mp3",psDirName,psFileNameBase);
	return sFileName.c_str();
}

// this MUST return NULL for non-base states unless doing debug-query
const char *Music_BaseStateToString( MusicState_e eMusicState, qboolean bDebugPrintQuery /* = qfalse */ )
{
	switch (eMusicState)
	{
		case eBGRNDTRACK_EXPLORE:	return "explore";
		case eBGRNDTRACK_ACTION:	return "action";
		case eBGRNDTRACK_BOSS:		return "boss";
		case eBGRNDTRACK_SILENCE:	return "silence";	// not used in this module, but snd_dma uses it now it's de-static'd
		case eBGRNDTRACK_DEATH:		return "death";

		// info only, not map<> lookup keys (unlike above)...
		//
		case eBGRNDTRACK_ACTIONTRANS0:		if (bDebugPrintQuery) return "action_tr0";
		case eBGRNDTRACK_ACTIONTRANS1:		if (bDebugPrintQuery) return "action_tr1";
		case eBGRNDTRACK_ACTIONTRANS2:		if (bDebugPrintQuery) return "action_tr2";
		case eBGRNDTRACK_ACTIONTRANS3:		if (bDebugPrintQuery) return "action_tr3";
		case eBGRNDTRACK_EXPLORETRANS0:		if (bDebugPrintQuery) return "explore_tr0";
		case eBGRNDTRACK_EXPLORETRANS1:		if (bDebugPrintQuery) return "explore_tr1";
		case eBGRNDTRACK_EXPLORETRANS2:		if (bDebugPrintQuery) return "explore_tr2";
		case eBGRNDTRACK_EXPLORETRANS3:		if (bDebugPrintQuery) return "explore_tr3";
		case eBGRNDTRACK_FADE:				if (bDebugPrintQuery) return "fade";
	}

	return NULL;
}

static qboolean Music_ParseMusic(CGenericParser2 &Parser, MusicData_t *MusicData, CGPGroup *pgMusicFiles, LPCSTR psMusicName, LPCSTR psMusicNameKey, MusicState_e eMusicState)
{
	qboolean bReturn = qfalse;

#ifdef _XBOX
	Z_SetNewDeleteTemporary(true);
#endif
	MusicFile_t MusicFile;
#ifdef _XBOX
	Z_SetNewDeleteTemporary(false);
#endif

	CGPGroup *pgMusicFile = pgMusicFiles->FindSubGroup(psMusicName);
	if (pgMusicFile)
	{
		// read subgroups...  
		//
		qboolean bEntryFound = qfalse;
		qboolean bExitFound  = qfalse;
		//
		// (read entry points first, so I can check exit points aren't too close in time)
		//
		CGPGroup *pEntryGroup = pgMusicFile->FindSubGroup(sKEY_ENTRY);
		if (pEntryGroup)
		{
			// read entry points...
			//
			for (CGPValue *pValue = pEntryGroup->GetPairs(); pValue; pValue = pValue->GetNext())
			{
				LPCSTR psKey	= pValue->GetName();
				LPCSTR psValue	= pValue->GetTopValue();

				//if (!strncmp(psKey,sKEY_MARKER,strlen(sKEY_MARKER)))	// for now, assume anything is a marker
				{
					MusicFile.MusicEntryTimes[psKey] = atof(psValue);
					bEntryFound = qtrue;						// harmless to keep setting
				}
			}
		}

		for (CGPGroup *pGroup = pgMusicFile->GetSubGroups(); pGroup; pGroup = pGroup->GetNext())
		{
			LPCSTR psGroupName = pGroup->GetName();

			if (!strcmp(psGroupName,sKEY_ENTRY))
			{
				// skip entry points, I've already read them in above
				//
			}
			else
			if (!strcmp(psGroupName,sKEY_EXIT))
			{
				int iThisExitPointIndex = MusicFile.MusicExitPoints.size();	// must eval this first, so unaffected by push_back etc
				//
				// read this set of exit points...
				//
				MusicExitPoint_t MusicExitPoint;
				for (CGPValue *pValue = pGroup->GetPairs(); pValue; pValue = pValue->GetNext())
				{
					LPCSTR psKey	= pValue->GetName();
					LPCSTR psValue	= pValue->GetTopValue();

					if (!strcmp(psKey,sKEY_NEXTFILE))
					{
						MusicExitPoint.sNextFile = psValue;
						bExitFound  = qtrue;	// harmless to keep setting
					}
					else
					if (!strcmp(psKey,sKEY_NEXTMARK))
					{
						MusicExitPoint.sNextMark = psValue;
					}
					else
					if (!strncmp(psKey,sKEY_TIME,strlen(sKEY_TIME)))
					{
						MusicExitTime_t MusicExitTime;
										MusicExitTime.fTime		= atof(psValue);
										MusicExitTime.iExitPoint= iThisExitPointIndex;

						// new check, don't keep this this exit point if it's within 1.5 seconds either way of an entry point...
						//
						qboolean bTooCloseToEntryPoint = qfalse;
						for (MusicEntryTimes_t::iterator itEntryTimes = MusicFile.MusicEntryTimes.begin(); itEntryTimes != MusicFile.MusicEntryTimes.end(); ++itEntryTimes)
						{
							float fThisEntryTime = (*itEntryTimes).second;

							if (Q_fabs(fThisEntryTime - MusicExitTime.fTime) < 1.5f)
							{
//								bTooCloseToEntryPoint = qtrue;	// not sure about this, ignore for now
								break;
							}
						}
						if (!bTooCloseToEntryPoint)
						{
#ifdef _XBOX
							Z_SetNewDeleteTemporary(true);
#endif
							MusicFile.MusicExitTimes.push_back(MusicExitTime);
#ifdef _XBOX
							Z_SetNewDeleteTemporary(false);
#endif
						}
					}
				}

#ifdef _XBOX
				Z_SetNewDeleteTemporary(true);
#endif
				MusicFile.MusicExitPoints.push_back(MusicExitPoint);
#ifdef _XBOX
				Z_SetNewDeleteTemporary(false);
#endif
				int iNumExitPoints = MusicFile.MusicExitPoints.size();

				// error checking...
				//
				switch (eMusicState)
				{
					case eBGRNDTRACK_EXPLORE:	
						if (iNumExitPoints > iMAX_EXPLORE_TRANSITIONS)
						{
							MUSIC_PARSE_ERROR( va("\"%s\" has > %d %s transitions defined!\n",psMusicName,iMAX_EXPLORE_TRANSITIONS,psMusicNameKey) );
							return qfalse;
						}
						break;

					case eBGRNDTRACK_ACTION:
						if (iNumExitPoints > iMAX_ACTION_TRANSITIONS)
						{
							MUSIC_PARSE_ERROR( va("\"%s\" has > %d %s transitions defined!\n",psMusicName,iMAX_ACTION_TRANSITIONS,psMusicNameKey) );
							return qfalse;
						}
						break;

					case eBGRNDTRACK_BOSS:
					case eBGRNDTRACK_DEATH:

						MUSIC_PARSE_ERROR( va("\"%s\" has %s transitions defined, this is not allowed!\n",psMusicName,psMusicNameKey) );
						break;					
				}
			}
		}

		// for now, assume everything was ok unless some obvious things are missing...
		//
		bReturn = qtrue;

		if (eMusicState != eBGRNDTRACK_BOSS && eMusicState != eBGRNDTRACK_DEATH)	// boss & death pieces can omit entry/exit stuff
		{
			if (!bEntryFound)
			{
				MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\" in group \"%s\"\n",sKEY_ENTRY,psMusicName));
				bReturn = qfalse;
			}
			if (!bExitFound)
			{
				MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\" in group \"%s\"\n",sKEY_EXIT,psMusicName));
				bReturn = qfalse;
			}
		}
	}
	else
	{
		MUSIC_PARSE_ERROR(va("Unable to find musicfiles entry \"%s\"\n",psMusicName));
	}

	if (bReturn)
	{
		MusicFile.sFileNameBase  = psMusicName; 
		(*MusicData)[ psMusicNameKey ] = MusicFile;
	}

	return bReturn;
}




// I only need this because GP2 can't cope with trailing whitespace (for !@#$%^'s sake!!!!)...
//
// (output buffer will always be just '\n' seperated, regardless of possible "\r\n" pairs)
//
// (remember to Z_Free() the returned char * when done with it!!!)
//
static char *StripTrailingWhiteSpaceOnEveryLine(char *pText)
{
#ifdef _XBOX
	Z_SetNewDeleteTemporary(true);
#endif
	string strNewText;

	while (*pText)
	{
		char sOneLine[1024];	// BTO: was 16k

		// find end of line...
		//				
		char *pThisLineEnd = pText;
		while (*pThisLineEnd && *pThisLineEnd != '\r' && ((pThisLineEnd-pText) < sizeof(sOneLine)-1))
		{
			pThisLineEnd++;
		}	

		int iCharsToCopy = pThisLineEnd - pText;
		strncpy(sOneLine, pText, iCharsToCopy);
				sOneLine[iCharsToCopy]='\0';
		pText += iCharsToCopy;
		while (*pText == '\n' || *pText == '\r') pText++;

		// trim trailing...
		//
		qboolean bTrimmed = qfalse;
		do
		{
			bTrimmed = qfalse;
			int iStrLen = strlen(sOneLine);

			if (iStrLen)
			{
				if (sOneLine[iStrLen-1] == '\t' || sOneLine[iStrLen-1] == ' ')
				{
					sOneLine[iStrLen-1] = '\0';
					bTrimmed = qtrue;
				}
			}
		}
		while (bTrimmed);
		
		strNewText += sOneLine;
		strNewText += "\n";
	}

	char  *pNewText = (char *) Z_Malloc( strlen(strNewText.c_str())+1,
								TAG_TEMP_WORKSPACE,
								qfalse);
	strcpy(pNewText, strNewText.c_str());
#ifdef _XBOX
	Z_SetNewDeleteTemporary(false);
#endif
	return pNewText;
}


// called from SV_SpawnServer, but before map load and music start etc.
//
// This just initialises the Lucas music structs so the background music player can interrogate them...
//
sstring_t gsLevelNameFromServer;
void Music_SetLevelName(const char *psLevelName)
{	
	gsLevelNameFromServer = psLevelName;	
}

static qboolean Music_ParseLeveldata(const char *psLevelName)
{
	qboolean bReturn = qfalse;

	if (MusicData == NULL)
	{
#ifdef _XBOX
		MusicData = new MusicData_t;
#else
		// sorry vv, false leaks make it hard to find true leaks
		static MusicData_t singleton;
		MusicData = &singleton;
#endif
	}
	
	// already got this data?
	//
	if (MusicData->size() && !Q_stricmp(psLevelName,gsLevelNameForCompare.c_str()))
	{
		return qtrue;
	}
	
	MusicData->clear();	

	char sLevelName[MAX_QPATH];
	Q_strncpyz(sLevelName,psLevelName,sizeof(sLevelName));
	
	gsLevelNameForLoad		= sLevelName;	// harmless to init here even if we fail to parse dms.dat file	
	gsLevelNameForCompare	= sLevelName;	// harmless to init here even if we fail to parse dms.dat file	
	gsLevelNameForBossLoad	= sLevelName;	// harmless to init here even if we fail to parse dms.dat file	

	char *pText = NULL;
	/*int iTotalBytesLoaded = */FS_ReadFile(sFILENAME_DMS, (void **)&pText );			
	if (pText) 
	{
		char *psStrippedText = StripTrailingWhiteSpaceOnEveryLine(pText);		
		CGenericParser2 Parser;
		char *psDataPtr = psStrippedText;	// because ptr gets advanced, so we supply a clone that GP can alter
		if (Parser.Parse(&psDataPtr, true))
		{
			CGPGroup *pFileGroup = Parser.GetBaseParseGroup();
			if (pFileGroup)
			{
				CGPGroup *pgMusicFiles = pFileGroup->FindSubGroup(sKEY_MUSICFILES);
				if (pgMusicFiles)
				{
					CGPGroup *pgLevelMusic = pFileGroup->FindSubGroup(sKEY_LEVELMUSIC);

					if (pgLevelMusic)
					{
						CGPGroup *pgThisLevelMusic = NULL;
						//
						// check for new USE keyword...
						//
						int iSanityLimit = 0;
						sstring_t sSearchName(sLevelName);

						while (sSearchName.c_str()[0] && iSanityLimit < 10)
						{
							gsLevelNameForLoad		= sSearchName;
							gsLevelNameForBossLoad	= sSearchName;
							pgThisLevelMusic = pgLevelMusic->FindSubGroup(sSearchName.c_str());

							if (pgThisLevelMusic)
							{
								CGPValue *pValue = pgThisLevelMusic->FindPair(sKEY_USES);
								if (pValue)
								{
									// re-search using the USE param...
									//									
									sSearchName = pValue->GetTopValue();
									iSanityLimit++;
//									Com_DPrintf("Using \"%s\"\n",sSearchName.c_str());
								}
								else
								{
									// no new USE keyword found...
									//
									sSearchName = "";
								}
							}
							else
							{
								// level entry not found...
								//
								break;
							}
						}

						// now go ahead and use the final music set we've decided on...
						//
						if (pgThisLevelMusic && iSanityLimit < 10)
						{
							// these are optional fields, so see which ones we find...
							//
							LPCSTR psName_Explore = NULL;
							LPCSTR psName_Action  = NULL;
							LPCSTR psName_Boss	  = NULL;
							LPCSTR psName_Death	  = NULL;
							//
							LPCSTR psName_UseBoss = NULL;

							for (CGPValue *pValue = pgThisLevelMusic->GetPairs(); pValue; pValue = pValue->GetNext())
							{
								LPCSTR psKey	= pValue->GetName();
								LPCSTR psValue	= pValue->GetTopValue();								

								if (Q_stricmp(psValue,sKEY_PLACEHOLDER))	// ignore "placeholder" items
								{
									if (!Q_stricmp(psKey,sKEY_EXPLORE))
									{
										psName_Explore = psValue;
									}
									else
									if (!Q_stricmp(psKey,sKEY_ACTION))
									{
										psName_Action  = psValue;
									}
									else
									if (!Q_stricmp(psKey,sKEY_USEBOSS))
									{
										psName_UseBoss = psValue;
									}
									else
									if (!Q_stricmp(psKey,sKEY_BOSS))
									{
										psName_Boss = psValue;
									}
									else
									if (!Q_stricmp(psKey,sKEY_DEATH))
									{
										psName_Death = psValue;
									}
								}
							}
									
							bReturn = qtrue;	// defualt to ON now, so I can turn it off if "useboss" fails

							if (psName_UseBoss)
							{
								CGPGroup *pgLevelMusicOfBoss = pgLevelMusic->FindSubGroup(psName_UseBoss);
								if (pgLevelMusicOfBoss)
								{
									CGPValue *pValueBoss = pgLevelMusicOfBoss->FindPair(sKEY_BOSS);
									if (pValueBoss)
									{	
										psName_Boss = pValueBoss->GetTopValue();
										gsLevelNameForBossLoad = psName_UseBoss;
									}
									else
									{
										MUSIC_PARSE_ERROR(va("'useboss' \"%s\" has no \"boss\" entry!\n",psName_UseBoss));
										bReturn = qfalse;
									}
								}
								else
								{
									MUSIC_PARSE_ERROR(va("Unable to find 'useboss' entry \"%s\"\n",psName_UseBoss));									
									bReturn = qfalse;
								}
							}


							// done this way in case I want to conditionally pass any bools depending on music type...
							//
							if (bReturn && psName_Explore)
							{
								bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Explore,	sKEY_EXPLORE, eBGRNDTRACK_EXPLORE);
							}
							if (bReturn && psName_Action)
							{
								bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Action,	sKEY_ACTION,  eBGRNDTRACK_ACTION);
							}
							if (bReturn && psName_Boss)
							{
								bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Boss,	sKEY_BOSS,    eBGRNDTRACK_BOSS);
							}
							if (bReturn /*&& psName_Death*/)	// LAST MINUTE HACK!!, always force in some death music!!!!
							{
								//bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Death,	sKEY_DEATH,   eBGRNDTRACK_DEATH);

								MusicFile_t m;
											m.sFileNameBase = "death_music";
								(*MusicData)[ sKEY_DEATH ] = m;
							}
						}
						else
						{
							MUSIC_PARSE_WARNING(va("Unable to find entry for \"%s\" in \"%s\"\n",sLevelName,sFILENAME_DMS));
						}
					}
					else
					{
						MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\"\n",sKEY_LEVELMUSIC));
					}
				}
				else
				{
					MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\"\n",sKEY_MUSICFILES));
				}
			}
			else
			{
				MUSIC_PARSE_ERROR( "Error calling GP2.GetBaseParseGroup()\n" );
			}
		}
		else
		{
			MUSIC_PARSE_ERROR( "Error using GP to parse file\n" );
		}

		Z_Free(psStrippedText);
		FS_FreeFile( pText );
	}
	else
	{
		MUSIC_PARSE_ERROR( "Unable to even read main file\n" );	// file name specified in error message
	}

	if (bReturn)
	{
		// sort exit points, and do some error checking...
		//
		for (MusicData_t::iterator itMusicData = MusicData->begin(); itMusicData != MusicData->end(); ++itMusicData)
		{
			LPCSTR psMusicStateType	= (*itMusicData).first.c_str();
			MusicFile_t &MusicFile	= (*itMusicData).second;

			// kludge up an enum, only interested in boss or not at the moment, so...
			//
			MusicState_e eMusicState = !stricmp(psMusicStateType,"boss") ? eBGRNDTRACK_BOSS : !stricmp(psMusicStateType,"death") ? eBGRNDTRACK_DEATH : eBGRNDTRACK_EXPLORE;

			if (!MusicFile.MusicExitTimes.empty())
			{
				sort(MusicFile.MusicExitTimes.begin(),MusicFile.MusicExitTimes.end());
			}

			// check music exists...
			//
			LPCSTR psMusicFileName = Music_BuildFileName( MusicFile.sFileNameBase.c_str(), eMusicState );
			if (!S_FileExists( psMusicFileName ))
			{
				MUSIC_PARSE_ERROR(va("Music file \"%s\" not found!\n",psMusicFileName));
				return qfalse;		// have to return, because music data destroyed now
			}

			// check all transition music pieces exist, and that entry points into new pieces after transitions also exist...
			//
			for (int iExitPoint=0; iExitPoint < MusicFile.MusicExitPoints.size(); iExitPoint++)
			{
				MusicExitPoint_t &MusicExitPoint = MusicFile.MusicExitPoints[ iExitPoint ];

				LPCSTR psTransitionFileName = Music_BuildFileName( MusicExitPoint.sNextFile.c_str(), eMusicState );
				if (!S_FileExists( psTransitionFileName ))
				{
					MUSIC_PARSE_ERROR(va("Transition file \"%s\" (entry \"%s\" ) not found!\n",psTransitionFileName, MusicExitPoint.sNextFile.c_str()));
					return qfalse;		// have to return, because music data destroyed now
				}

				LPCSTR psNextMark = MusicExitPoint.sNextMark.c_str();
				if (strlen(psNextMark))	// always NZ ptr
				{
					// then this must be "action" music under current rules...
					//					
					assert( !strcmp(psMusicStateType, Music_BaseStateToString(eBGRNDTRACK_ACTION) ? Music_BaseStateToString(eBGRNDTRACK_ACTION):"") );
					//
					// does this marker exist in the explore piece?
					//					
					MusicData_t::iterator itExploreMusicData = MusicData->find( Music_BaseStateToString(eBGRNDTRACK_EXPLORE) );
					if (itExploreMusicData != MusicData->end())
					{
						MusicFile_t &MusicFile_Explore = (*itExploreMusicData).second;

						if (!MusicFile_Explore.MusicEntryTimes.count(psNextMark))
						{
							MUSIC_PARSE_ERROR( va("Unable to find entry point \"%s\" in description for \"%s\"\n",psNextMark,MusicFile_Explore.sFileNameBase.c_str()) );
							return qfalse;		// have to return, because music data destroyed now
						}
					}
					else
					{
						MUSIC_PARSE_ERROR( va("Unable to find %s piece to match \"%s\"\n", Music_BaseStateToString(eBGRNDTRACK_EXPLORE), MusicFile.sFileNameBase.c_str() ) );
						return qfalse;		// have to return, because music data destroyed now
					}
				}
			}
		}
	}
	
#ifdef _DEBUG
/*
	// dump the whole thing out to prove it was read in ok...
	//
	if (bReturn)
	{
		for (MusicData_t::iterator itMusicData = MusicData->begin(); itMusicData != MusicData->end(); ++itMusicData)
		{
			LPCSTR psMusicState		= (*itMusicData).first.c_str();
			MusicFile_t &MusicFile	= (*itMusicData).second;

			OutputDebugString(va("Music State:  \"%s\",  File: \"%s\"\n",psMusicState, MusicFile.sFileNameBase.c_str()));

			// entry times...
			//
			for (MusicEntryTimes_t::iterator itEntryTimes = MusicFile.MusicEntryTimes.begin(); itEntryTimes != MusicFile.MusicEntryTimes.end(); ++itEntryTimes)
			{
				LPCSTR	psMarkerName	= (*itEntryTimes).first.c_str();
				float	fEntryTime		= (*itEntryTimes).second;

				OutputDebugString(va("Entry time for \"%s\": %f\n", psMarkerName, fEntryTime));
			}

			// exit points...
			//
			for (int i=0; i<MusicFile.MusicExitPoints.size(); i++)
			{
				MusicExitPoint_t &MusicExitPoint = MusicFile.MusicExitPoints[i];

				OutputDebugString(va("Exit point %d:	sNextFile: \"%s\", sNextMark: \"%s\"\n",i,MusicExitPoint.sNextFile.c_str(),MusicExitPoint.sNextMark.c_str()));
			}

			// exit times...
			//
			for (i=0; i<MusicFile.MusicExitTimes.size(); i++)
			{
				MusicExitTime_t &MusicExitTime = MusicFile.MusicExitTimes[i];
				
				OutputDebugString(va("Exit time %d:		fTime: %f, iExitPoint: %d\n",i,MusicExitTime.fTime,MusicExitTime.iExitPoint));
			}
		}
	}
*/
#endif

	return bReturn;
}


// returns ptr to music file, or NULL for error/missing...
//
static MusicFile_t *Music_GetBaseMusicFile( LPCSTR psMusicState )	// where psMusicState is (eg) "explore", "action" or "boss"
{
	MusicData_t::iterator it = MusicData->find( psMusicState );
	if (it != MusicData->end())
	{
		MusicFile_t *pMusicFile = &(*it).second;
		return pMusicFile;
	}

	return NULL;
}

static MusicFile_t *Music_GetBaseMusicFile( MusicState_e eMusicState )
{
	LPCSTR	psMusicStateString = Music_BaseStateToString( eMusicState );
	if ( psMusicStateString )
	{
		return Music_GetBaseMusicFile( psMusicStateString );
	}

	return NULL;
}


// where label is (eg) "kejim_base"...
//
qboolean Music_DynamicDataAvailable(const char *psDynamicMusicLabel)
{		
	char sLevelName[MAX_QPATH];
	Q_strncpyz(sLevelName,COM_SkipPath( const_cast<char*>( (psDynamicMusicLabel&&psDynamicMusicLabel[0])?psDynamicMusicLabel:gsLevelNameFromServer.c_str() ) ),sizeof(sLevelName));
	strlwr(sLevelName);

	if (strlen(sLevelName))	// avoid error messages when there's no music waiting to be played and we try and restart it...
	{
		if (Music_ParseLeveldata(sLevelName))
		{
			return !!(	Music_GetBaseMusicFile( eBGRNDTRACK_EXPLORE ) &&
						Music_GetBaseMusicFile(	eBGRNDTRACK_ACTION	)
						);
		}
	}

	return qfalse;
}

LPCSTR Music_GetFileNameForState( MusicState_e eMusicState)
{
	MusicFile_t *pMusicFile = NULL;
	switch (eMusicState)
	{
		case eBGRNDTRACK_EXPLORE:
		case eBGRNDTRACK_ACTION:
		case eBGRNDTRACK_BOSS:
		case eBGRNDTRACK_DEATH:

			pMusicFile = Music_GetBaseMusicFile( eMusicState );
			if (pMusicFile)
			{
				return Music_BuildFileName( pMusicFile->sFileNameBase.c_str(), eMusicState );
			}
			break;

		case eBGRNDTRACK_ACTIONTRANS0:
		case eBGRNDTRACK_ACTIONTRANS1:
		case eBGRNDTRACK_ACTIONTRANS2:
		case eBGRNDTRACK_ACTIONTRANS3:

			pMusicFile = Music_GetBaseMusicFile( eBGRNDTRACK_ACTION );
			if (pMusicFile)
			{
				int iTransNum = eMusicState - eBGRNDTRACK_ACTIONTRANS0;
				if (iTransNum < pMusicFile->MusicExitPoints.size())
				{
					return Music_BuildFileName( pMusicFile->MusicExitPoints[iTransNum].sNextFile.c_str(), eMusicState );
				}
			}
			break;

		case eBGRNDTRACK_EXPLORETRANS0:
		case eBGRNDTRACK_EXPLORETRANS1:
		case eBGRNDTRACK_EXPLORETRANS2:
		case eBGRNDTRACK_EXPLORETRANS3:

			pMusicFile = Music_GetBaseMusicFile( eBGRNDTRACK_EXPLORE );
			if (pMusicFile)
			{
				int iTransNum = eMusicState - eBGRNDTRACK_EXPLORETRANS0;
				if (iTransNum < pMusicFile->MusicExitPoints.size())
				{
					return Music_BuildFileName( pMusicFile->MusicExitPoints[iTransNum].sNextFile.c_str(), eMusicState );
				}
			}
			break;

		default:
			#ifndef FINAL_BUILD
			assert(0);	// duh....what state are they asking for?
			Com_Printf( S_COLOR_RED "Music_GetFileNameForState( %d ) unhandled case!\n",eMusicState );
			#endif
			break;
	}

	return NULL;
}



qboolean Music_StateIsTransition( MusicState_e eMusicState )
{
	return (eMusicState >= eBGRNDTRACK_FIRSTTRANSITION &&
			eMusicState <= eBGRNDTRACK_LASTTRANSITION
			);
}


qboolean Music_StateCanBeInterrupted( MusicState_e eMusicState, MusicState_e eProposedMusicState )
{
	// death music can interrupt anything...
	//
	if (eProposedMusicState == eBGRNDTRACK_DEATH)
		return qtrue;
	//
	// ... and can't be interrupted once started...(though it will internally-switch to silence at the end, rather than loop)
	//
	if (eMusicState == eBGRNDTRACK_DEATH)
	{
		return qfalse;
	}

	// boss music can interrupt anything (other than death, but that's already handled above)...
	//
	if (eProposedMusicState == eBGRNDTRACK_BOSS)
		return qtrue;
	//
	// ... and can't be interrupted once started...
	//
	if (eMusicState == eBGRNDTRACK_BOSS)
	{
		// ...except by silence (or death, but again, that's already handled above)
		//
		if (eProposedMusicState == eBGRNDTRACK_SILENCE)	 
			return qtrue;	

		return qfalse;
	}

	// action music can interrupt anything (after boss & death filters above)...
	//
	if (eProposedMusicState == eBGRNDTRACK_ACTION)
		return qtrue;

	// nothing can interrupt a transition (after above filters)...
	//
	if (Music_StateIsTransition( eMusicState ))
		return qfalse;

	// current state is therefore interruptable...
	//
	return qtrue;
}



// returns qtrue if music is allowed to transition out of current state, based on current play position...
// (doesn't bother returning final state after transition (eg action->transition->explore) becuase it's fairly obvious)
//
// supply:
//
// playing point in float seconds
// enum of track being queried
//
// get:
//
// enum of transition track to switch to
// float time of entry point of new track *after* transition
//
qboolean Music_AllowedToTransition( float			fPlayingTimeElapsed,
									MusicState_e	eMusicState,
									//
									MusicState_e	*peTransition /* = NULL */,
									float			*pfNewTrackEntryTime /* = NULL */
									)
{
	const float fTimeEpsilon = 0.3f;	// arb., how close we have to be to an exit point to take it.
										//		if set too high then music change is sloppy
										//		if set too low[/precise] then we might miss an exit if client fps is poor


	MusicFile_t *pMusicFile = Music_GetBaseMusicFile( eMusicState );	
	if (pMusicFile && !pMusicFile->MusicExitTimes.empty())
	{
		MusicExitTime_t	T;
						T.fTime = fPlayingTimeElapsed;

		// since a MusicExitTimes_t item is a sorted array, we can use the equal_range algorithm...
		//
		pair <MusicExitTimes_t::iterator, MusicExitTimes_t::iterator> itp = equal_range( pMusicFile->MusicExitTimes.begin(), pMusicFile->MusicExitTimes.end(), T);
		if (itp.first != pMusicFile->MusicExitTimes.begin())
			itp.first--;	// encompass the one before, in case we've just missed an exit point by < fTimeEpsilon
		if (itp.second!= pMusicFile->MusicExitTimes.end())
			itp.second++;	// increase range to one beyond, so we can do normal STL being/end looping below
		for (MusicExitTimes_t::iterator it = itp.first; it != itp.second; ++it)
		{
			MusicExitTimes_t::iterator pExitTime = it;
			
			if ( Q_fabs(pExitTime->fTime - fPlayingTimeElapsed) <= fTimeEpsilon )
			{
				// got an exit point!, work out feedback params...
				//
				int iExitPoint = pExitTime->iExitPoint;
				//
				// the two params to give back...
				//
				MusicState_e	eFeedBackTransition			= eBGRNDTRACK_EXPLORETRANS0;	// any old default
				float			fFeedBackNewTrackEntryTime	= 0.0f;
				//
				// check legality in case of crap data...
				//
				if (iExitPoint < pMusicFile->MusicExitPoints.size())
				{
					MusicExitPoint_t &ExitPoint = pMusicFile->MusicExitPoints[ iExitPoint ];

					switch (eMusicState)
					{
						case eBGRNDTRACK_EXPLORE:
						{
							assert(iExitPoint < iMAX_EXPLORE_TRANSITIONS);	// already been checked, but sanity
							assert(!ExitPoint.sNextMark.c_str()[0]);		// simple error checking, but harmless if tripped. explore transitions go to silence, hence no entry time for [silence] state after transition

							eFeedBackTransition = (MusicState_e) (eBGRNDTRACK_EXPLORETRANS0 + iExitPoint);
						}
						break;

						case eBGRNDTRACK_ACTION:
						{
							assert(iExitPoint < iMAX_ACTION_TRANSITIONS);	// already been checked, but sanity

							// if there's an entry marker point defined...
							//
							if (ExitPoint.sNextMark.c_str()[0])
							{
								MusicData_t::iterator itExploreMusicData = MusicData->find( Music_BaseStateToString(eBGRNDTRACK_EXPLORE) );
								//
								// find "explore" music...
								//
								if (itExploreMusicData != MusicData->end())
								{
									MusicFile_t &MusicFile_Explore = (*itExploreMusicData).second;
									//
									// find the entry marker within the music and read the time there...
									//
									MusicEntryTimes_t::iterator itEntryTime = MusicFile_Explore.MusicEntryTimes.find( ExitPoint.sNextMark.c_str() );
									if (itEntryTime != MusicFile_Explore.MusicEntryTimes.end())
									{
										fFeedBackNewTrackEntryTime = (*itEntryTime).second;										
										eFeedBackTransition = (MusicState_e) (eBGRNDTRACK_ACTIONTRANS0 + iExitPoint);
									}
									else
									{
										#ifndef FINAL_BUILD
										assert(0);	// sanity, should have been caught elsewhere, but harmless to do this
										Com_Printf( S_COLOR_RED "Music_AllowedToTransition() unable to find entry marker \"%s\" in \"%s\"",ExitPoint.sNextMark.c_str(), MusicFile_Explore.sFileNameBase.c_str());
										#endif
										return qfalse;
									}
								}
								else
								{
									#ifndef FINAL_BUILD
									assert(0);	// sanity, should have been caught elsewhere, but harmless to do this
									Com_Printf( S_COLOR_RED "Music_AllowedToTransition() unable to find %s version of \"%s\"\n",Music_BaseStateToString(eBGRNDTRACK_EXPLORE), pMusicFile->sFileNameBase.c_str());
									#endif
									return qfalse;
								}
							}
							else
							{
								eFeedBackTransition			= eBGRNDTRACK_ACTIONTRANS0;
								fFeedBackNewTrackEntryTime	= 0.0f;		// already set to this, but FYI
							}
						}
						break;

						default:
						{
							#ifndef FINAL_BUILD
							assert(0);
							Com_Printf( S_COLOR_RED "Music_AllowedToTransition(): No code to transition from music type %d\n",eMusicState);
							#endif
							return qfalse;
						}
						break;
					}
				}
				else
				{
					#ifndef FINAL_BUILD
					assert(0);
					Com_Printf( S_COLOR_RED "Music_AllowedToTransition(): Illegal exit point %d, max = %d (music: \"%s\")\n",iExitPoint, pMusicFile->MusicExitPoints.size()-1, pMusicFile->sFileNameBase.c_str() );
					#endif
					return qfalse;
				}


				// feed back answers...
				//
				if ( peTransition)
				{
					*peTransition = eFeedBackTransition;
				}

				if ( pfNewTrackEntryTime )
				{
					*pfNewTrackEntryTime = fFeedBackNewTrackEntryTime;
				}

				return qtrue;
			}
		}
	}

	return qfalse;
}


// typically used to get a (predefined) random entry point for the action music, but will work on any defined type with entry points, 
//	defaults safely to 0.0f if no info available...
//
float Music_GetRandomEntryTime( MusicState_e eMusicState )
{
	MusicData_t::iterator itMusicData = MusicData->find( Music_BaseStateToString( eMusicState ) );
	if (itMusicData != MusicData->end())
	{
		MusicFile_t &MusicFile = (*itMusicData).second;

		if (MusicFile.MusicEntryTimes.size())	// make sure at least one defined, else default to start
		{
			// Quake's random number generator isn't very good, so instead of this:
			//
			// int iRandomEntryNum = Q_irand(0, (MusicFile.MusicEntryTimes.size()-1) );
			//
			// ... I'll do this (ensuring we don't get the same result on two consecutive calls, but without while-loop)...
			//
			static int	iPrevRandomNumber = -1;
			static int	iCallCount = 0;
						iCallCount++;
			int iRandomEntryNum = (rand()+iCallCount) % (MusicFile.MusicEntryTimes.size());	// legal range
			if (iRandomEntryNum == iPrevRandomNumber && MusicFile.MusicEntryTimes.size()>1)
			{
				iRandomEntryNum += 1;
				iRandomEntryNum %= (MusicFile.MusicEntryTimes.size());
			}
			iPrevRandomNumber = iRandomEntryNum;

//			OutputDebugString(va("Music_GetRandomEntryTime(): Entry %d\n",iRandomEntryNum));

			for (MusicEntryTimes_t::iterator itEntryTime = MusicFile.MusicEntryTimes.begin(); itEntryTime != MusicFile.MusicEntryTimes.end(); ++itEntryTime)
			{
				if (!iRandomEntryNum--)
				{
					return (*itEntryTime).second;										
				}
			}
		}
	}

	return 0.0f;
}

// info only, used in "soundinfo" command...
//
const char *Music_GetLevelSetName(void)
{
	if (Q_stricmp(gsLevelNameForCompare.c_str(), gsLevelNameForLoad.c_str()))
	{
		// music remap via USES command...
		//
		return va("%s -> %s",gsLevelNameForCompare.c_str(), gsLevelNameForLoad.c_str());
	}

	return gsLevelNameForLoad.c_str();
}

///////////////// eof /////////////////////

