/*
s_vox.c - npc sentences
Copyright (C) 2010 Uncle Mike

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.

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.
*/

#include "common.h"
#include "sound.h"
#include "const.h"
#include <ctype.h>

sentence_t	g_Sentences[MAX_SENTENCES];
static uint	g_numSentences;
static char	*rgpparseword[CVOXWORDMAX];	// array of pointers to parsed words
static char	voxperiod[] = "_period";	// vocal pause
static char	voxcomma[] = "_comma";	// vocal pause

static int IsNextWord( const char c )
{
	if( c == '.' || c == ',' || c == ' ' || c == '(' )
		return 1;
	return 0;
}

static int IsSkipSpace( const char c )
{
	if( c == ',' || c == '.' || c == ' ' )
		return 1;
	return 0;
}

static int IsWhiteSpace( const char space )
{
	if( space == ' ' || space == '\t' || space == '\r' || space == '\n' )
		return 1;
	return 0;
}

static int IsCommandChar( const char c )
{
	if( c == 'v' || c == 'p' || c == 's' || c == 'e' || c == 't' )
		return 1;
	return 0;
}

static int IsDelimitChar( const char c )
{
	if( c == '(' || c == ')' )
		return 1;
	return 0;
}

static char *ScanForwardUntil( char *string, const char scan )
{
	while( string[0] )
	{
		if( string[0] == scan )
			return string;
		string++;
	}
	return string;
}

// backwards scan psz for last '/'
// return substring in szpath null terminated
// if '/' not found, return 'vox/'
static char *VOX_GetDirectory( char *szpath, char *psz )
{
	char	c;
	int	cb = 0;
	char	*p = psz + Q_strlen( psz ) - 1;

	// scan backwards until first '/' or start of string
	c = *p;
	while( p > psz && c != '/' )
	{
		c = *( --p );
		cb++;
	}

	if( c != '/' )
	{
		// didn't find '/', return default directory
		Q_strcpy( szpath, "vox/" );
		return psz;
	}

	cb = Q_strlen( psz ) - cb;
	memcpy( szpath, psz, cb );
	szpath[cb] = 0;

	return p + 1;
}

// scan g_Sentences, looking for pszin sentence name
// return pointer to sentence data if found, null if not
// CONSIDER: if we have a large number of sentences, should
// CONSIDER: sort strings in g_Sentences and do binary search.
char *VOX_LookupString( const char *pSentenceName, int *psentencenum )
{
	int	i;

	if( Q_isdigit( pSentenceName ) && (i = Q_atoi( pSentenceName )) < g_numSentences )
	{
		if( psentencenum ) *psentencenum = i;
		return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 );		
	}

	for( i = 0; i < g_numSentences; i++ )
	{
		if( !Q_stricmp( pSentenceName, g_Sentences[i].pName ))
		{
			if( psentencenum ) *psentencenum = i;
			return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 );
		}
	}

	return NULL;
}

// parse a null terminated string of text into component words, with
// pointers to each word stored in rgpparseword
// note: this code actually alters the passed in string!
char **VOX_ParseString( char *psz ) 
{
	int	i, fdone = 0;
	char	c, *p = psz;

	memset( rgpparseword, 0, sizeof( char* ) * CVOXWORDMAX );

	if( !psz ) return NULL;

	i = 0;
	rgpparseword[i++] = psz;

	while( !fdone && i < CVOXWORDMAX )
	{
		// scan up to next word
		c = *p;
		while( c && !IsNextWord( c ))
			c = *(++p);
			
		// if '(' then scan for matching ')'
		if( c == '(' )
		{
			p = ScanForwardUntil( p, ')' );
			c = *(++p);
			if( !c ) fdone = 1;
		}

		if( fdone || !c )
		{
			fdone = 1;
		}
		else
		{	
			// if . or , insert pause into rgpparseword,
			// unless this is the last character
			if(( c == '.' || c == ',' ) && *(p+1) != '\n' && *(p+1) != '\r' && *(p+1) != 0 )
			{
				if( c == '.' ) rgpparseword[i++] = voxperiod;
				else rgpparseword[i++] = voxcomma;

				if( i >= CVOXWORDMAX )
					break;
			}

			// null terminate substring
			*p++ = 0;

			// skip whitespace
			c = *p;
			while( c && IsSkipSpace( c ))
				c = *(++p);

			if( !c ) fdone = 1;
			else rgpparseword[i++] = p;
		}
	}

	return rgpparseword;
}

float VOX_GetVolumeScale( channel_t *pchan )
{
	if( pchan->currentWord )
	{
		if ( pchan->words[pchan->wordIndex].volume )
		{
			float	volume = pchan->words[pchan->wordIndex].volume * 0.01f;
			if( volume < 1.0f ) return volume;
		}
	}

	return 1.0f;
}

void VOX_SetChanVol( channel_t *ch )
{
	float	scale;

	if( !ch->currentWord )
		return;

	scale = VOX_GetVolumeScale( ch );
	if( scale == 1.0f ) return;

	ch->rightvol = (int)(ch->rightvol * scale);
	ch->leftvol = (int)(ch->leftvol * scale);
}

float VOX_ModifyPitch( channel_t *ch, float pitch )
{
	if( ch->currentWord )
	{
		if( ch->words[ch->wordIndex].pitch > 0 )
		{
			pitch += ( ch->words[ch->wordIndex].pitch - PITCH_NORM ) * 0.01f;
		}
	}

	return pitch;
}

//===============================================================================
//  Get any pitch, volume, start, end params into voxword
//  and null out trailing format characters
//  Format: 
//		someword(v100 p110 s10 e20)
//		
//		v is volume, 0% to n%
//		p is pitch shift up 0% to n%
//		s is start wave offset %
//		e is end wave offset %
//		t is timecompression %
//
//  pass fFirst == 1 if this is the first string in sentence
//  returns 1 if valid string, 0 if parameter block only.
//
//  If a ( xxx ) parameter block does not directly follow a word, 
//  then that 'default' parameter block will be used as the default value
//  for all following words.  Default parameter values are reset
//  by another 'default' parameter block.  Default parameter values
//  for a single word are overridden for that word if it has a parameter block.
// 
//===============================================================================
int VOX_ParseWordParams( char *psz, voxword_t *pvoxword, int fFirst ) 
{
	char		*pszsave = psz;
	char		c, ct, sznum[8];
	static voxword_t	voxwordDefault;
	int		i;
			
	// init to defaults if this is the first word in string.
	if( fFirst )
	{
		voxwordDefault.pitch = -1;
		voxwordDefault.volume = 100;
		voxwordDefault.start = 0;
		voxwordDefault.end = 100;
		voxwordDefault.fKeepCached = 0;
		voxwordDefault.timecompress = 0;
	}

	*pvoxword = voxwordDefault;

	// look at next to last char to see if we have a 
	// valid format:
	c = *( psz + Q_strlen( psz ) - 1 );

	// no formatting, return
	if( c != ')' ) return 1; 

	// scan forward to first '('
	c = *psz;
	while( !IsDelimitChar( c ))
		c = *(++psz);

	// bogus formatting
	if( c == ')' ) return 0;
	
	// null terminate
	*psz = 0;
	ct = *(++psz);

	while( 1 )
	{
		// scan until we hit a character in the commandSet
		while( ct && !IsCommandChar( ct ))
			ct = *(++psz);
		
		if( ct == ')' )
			break;

		memset( sznum, 0, sizeof( sznum ));
		i = 0;

		c = *(++psz);
		
		if( !isdigit( c ))
			break;

		// read number
		while( isdigit( c ) && i < sizeof( sznum ) - 1 )
		{
			sznum[i++] = c;
			c = *(++psz);
		}

		// get value of number
		i = Q_atoi( sznum );

		switch( ct )
		{
		case 'v': pvoxword->volume = i; break;
		case 'p': pvoxword->pitch = i; break;
		case 's': pvoxword->start = i; break;
		case 'e': pvoxword->end = i; break;
		case 't': pvoxword->timecompress = i; break;
		}

		ct = c;
	}

	// if the string has zero length, this was an isolated
	// parameter block.  Set default voxword to these
	// values
	if( Q_strlen( pszsave ) == 0 )
	{
		voxwordDefault = *pvoxword;
		return 0;
	}

	return 1;
}

void VOX_LoadWord( channel_t *pchan )
{
	if( pchan->words[pchan->wordIndex].sfx )
	{
		wavdata_t	*pSource = S_LoadSound( pchan->words[pchan->wordIndex].sfx );

		if( pSource )
		{
			int start = pchan->words[pchan->wordIndex].start;
			int end = pchan->words[pchan->wordIndex].end;

			// apply mixer
			pchan->currentWord = &pchan->pMixer;
			pchan->currentWord->pData = pSource; 
				
			// don't allow overlapped ranges
			if( end <= start ) end = 0;

			if( start || end )
			{
				int	sampleCount = pSource->samples;

				if( start )
				{
					S_SetSampleStart( pchan, pSource, (int)(sampleCount * 0.01f * start));
				}

				if( end )
				{
					S_SetSampleEnd( pchan, pSource, (int)(sampleCount * 0.01f * end));
				}
			}
		}
	}
}

void VOX_FreeWord( channel_t *pchan )
{
	pchan->currentWord = NULL; // sentence is finished
	memset( &pchan->pMixer, 0, sizeof( pchan->pMixer ));

	// release unused sounds
	if( pchan->words[pchan->wordIndex].sfx )
	{
		// If this wave wasn't precached by the game code
		if( !pchan->words[pchan->wordIndex].fKeepCached )
		{
			FS_FreeSound( pchan->words[pchan->wordIndex].sfx->cache );
			pchan->words[pchan->wordIndex].sfx->cache = NULL;
			pchan->words[pchan->wordIndex].sfx = NULL;
		}
	}
}

void VOX_LoadFirstWord( channel_t *pchan, voxword_t *pwords )
{
	int	i = 0;

	// copy each pointer in the sfx temp array into the
	// sentence array, and set the channel to point to the
	// sentence array
	while( pwords[i].sfx != NULL )
	{
		pchan->words[i] = pwords[i];
		i++;
	}		
	pchan->words[i].sfx = NULL;

	pchan->wordIndex = 0;
	VOX_LoadWord( pchan );
}

// return number of samples mixed
int VOX_MixDataToDevice( channel_t *pchan, int sampleCount, int outputRate, int outputOffset )
{
	// save this to compute total output
	int	startingOffset = outputOffset;

	if( !pchan->currentWord )
		return 0;

	while( sampleCount > 0 && pchan->currentWord )
	{
		int	timeCompress = pchan->words[pchan->wordIndex].timecompress;
		int	outputCount = S_MixDataToDevice( pchan, sampleCount, outputRate, outputOffset, timeCompress );

		outputOffset += outputCount;
		sampleCount -= outputCount;

		// if we finished load a next word
		if( pchan->currentWord->finished )
		{
			VOX_FreeWord( pchan );
			pchan->wordIndex++;
			VOX_LoadWord( pchan );

			if( pchan->currentWord )
			{
				pchan->sfx = pchan->words[pchan->wordIndex].sfx;
			}
		}
	}
	return outputOffset - startingOffset;
}

// link all sounds in sentence, start playing first word.
void VOX_LoadSound( channel_t *pchan, const char *pszin )
{
	char	buffer[512];
	int	i, cword;
	char	pathbuffer[64];
	char	szpath[32];
	voxword_t	rgvoxword[CVOXWORDMAX];
	char	*psz;

	if( !pszin || !*pszin )
		return;

	memset( rgvoxword, 0, sizeof( voxword_t ) * CVOXWORDMAX );
	memset( buffer, 0, sizeof( buffer ));

	// lookup actual string in g_Sentences, 
	// set pointer to string data
	psz = VOX_LookupString( pszin, NULL );

	if( !psz )
	{
		Con_DPrintf( S_ERROR "VOX_LoadSound: no such sentence %s\n", pszin );
		return;
	}

	// get directory from string, advance psz
	psz = VOX_GetDirectory( szpath, psz );

	if( Q_strlen( psz ) > sizeof( buffer ) - 1 )
	{
		Con_Printf( S_ERROR "VOX_LoadSound: sentence is too long %s\n", psz );
		return;
	}

	// copy into buffer
	Q_strcpy( buffer, psz );
	psz = buffer;

	// parse sentence (also inserts null terminators between words)
	VOX_ParseString( psz );

	// for each word in the sentence, construct the filename,
	// lookup the sfx and save each pointer in a temp array	

	i = 0;
	cword = 0;
	while( rgpparseword[i] )
	{
		// Get any pitch, volume, start, end params into voxword
		if( VOX_ParseWordParams( rgpparseword[i], &rgvoxword[cword], i == 0 ))
		{
			// this is a valid word (as opposed to a parameter block)
			Q_strcpy( pathbuffer, szpath );
			Q_strncat( pathbuffer, rgpparseword[i], sizeof( pathbuffer ));
			Q_strncat( pathbuffer, ".wav", sizeof( pathbuffer ));

			// find name, if already in cache, mark voxword
			// so we don't discard when word is done playing
			rgvoxword[cword].sfx = S_FindName( pathbuffer, &( rgvoxword[cword].fKeepCached ));
			cword++;
		}
		i++;
	}

	VOX_LoadFirstWord( pchan, rgvoxword );

	pchan->isSentence = true;
	pchan->sfx = rgvoxword[0].sfx;
}

//-----------------------------------------------------------------------------
// Purpose: Take a NULL terminated sentence, and parse any commands contained in
//			{}.  The string is rewritten in place with those commands removed.
//
// Input  : *pSentenceData - sentence data to be modified in place
//			sentenceIndex - global sentence table index for any data that is 
//							parsed out
//-----------------------------------------------------------------------------
void VOX_ParseLineCommands( char *pSentenceData, int sentenceIndex )
{
	char	tempBuffer[512];
	char	*pNext, *pStart;
	int	length, tempBufferPos = 0;

	if( !pSentenceData )
		return;

	pStart = pSentenceData;

	while( *pSentenceData )
	{
		pNext = ScanForwardUntil( pSentenceData, '{' );

		// find length of "good" portion of the string (not a {} command)
		length = pNext - pSentenceData;
		if( tempBufferPos + length > sizeof( tempBuffer ))
		{
			Con_Printf( S_ERROR "Sentence too long (max length %d characters)\n", sizeof(tempBuffer) - 1 );
			return;
		}

		// Copy good string to temp buffer
		memcpy( tempBuffer + tempBufferPos, pSentenceData, length );
		
		// move the copy position
		tempBufferPos += length;

		pSentenceData = pNext;
		
		// skip ahead of the opening brace
		if( *pSentenceData ) pSentenceData++;
		
		// skip whitespace
		while( *pSentenceData && *pSentenceData <= 32 )
			pSentenceData++;

		// simple comparison of string commands:
		switch( Q_tolower( *pSentenceData ))
		{
		case 'l':
			// all commands starting with the letter 'l' here
			if( !Q_strnicmp( pSentenceData, "len", 3 ))
			{
				g_Sentences[sentenceIndex].length = Q_atof( pSentenceData + 3 );
			}
			break;
		case 0:
		default:
			break;
		}

		pSentenceData = ScanForwardUntil( pSentenceData, '}' );
		
		// skip the closing brace
		if( *pSentenceData ) pSentenceData++;

		// skip trailing whitespace
		while( *pSentenceData && *pSentenceData <= 32 )
			pSentenceData++;
	}

	if( tempBufferPos < sizeof( tempBuffer ))
	{
		// terminate cleaned up copy
		tempBuffer[tempBufferPos] = 0;
		
		// copy it over the original data
		Q_strcpy( pStart, tempBuffer );
	}
}

// Load sentence file into memory, insert null terminators to
// delimit sentence name/sentence pairs.  Keep pointer to each
// sentence name so we can search later.
void VOX_ReadSentenceFile( const char *psentenceFileName )
{
	char	c, *pch, *pFileData;
	char	*pchlast, *pSentenceData;
	fs_offset_t	fileSize;

	// load file
	pFileData = (char *)FS_LoadFile( psentenceFileName, &fileSize, false );
	if( !pFileData ) return; // this game just doesn't used vox sound system

	pch = pFileData;
	pchlast = pch + fileSize;

	while( pch < pchlast )
	{
		if( g_numSentences >= MAX_SENTENCES )
		{
			Con_Printf( S_ERROR "VOX_Init: too many sentences specified, max is %d\n", MAX_SENTENCES );
			break;
		}

		// only process this pass on sentences
		pSentenceData = NULL;

		// skip newline, cr, tab, space

		c = *pch;
		while( pch < pchlast && IsWhiteSpace( c ))
			c = *(++pch);

		// skip entire line if first char is /
		if( *pch != '/' )
		{
			sentence_t *pSentence = &g_Sentences[g_numSentences++];

			pSentence->pName = pch;
			pSentence->length = 0;

			// scan forward to first space, insert null terminator
			// after sentence name

			c = *pch;
			while( pch < pchlast && c != ' ' )
				c = *(++pch);

			if( pch < pchlast )
				*pch++ = 0;

			// a sentence may have some line commands, make an extra pass
			pSentenceData = pch;
		}

		// scan forward to end of sentence or eof
		while( pch < pchlast && pch[0] != '\n' && pch[0] != '\r' )
			pch++;
	
		// insert null terminator
		if( pch < pchlast ) *pch++ = 0;

		// If we have some sentence data, parse out any line commands
		if( pSentenceData && pSentenceData < pchlast )
		{
			int	index = g_numSentences - 1;

			// the current sentence has an index of count-1
			VOX_ParseLineCommands( pSentenceData, index );
		}
	}
}

void VOX_Init( void )
{
	memset( g_Sentences, 0, sizeof( g_Sentences ));
	g_numSentences = 0;

	VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" );
}


void VOX_Shutdown( void )
{
	g_numSentences = 0;
}
