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

#define TRIM_SCAN_MAX 255
#define TRIM_SAMPLES_BELOW_8 2
#define TRIM_SAMPLES_BELOW_16 512 // 65k * 2 / 256

#define CVOXFILESENTENCEMAX 4096

static int cszrawsentences = 0;
static char *rgpszrawsentence[CVOXFILESENTENCEMAX];
static const char *voxperiod = "_period", *voxcomma = "_comma";

static qboolean S_ShouldTrimSample8( const int8_t *buf, int channels )
{
	if( abs( buf[0] ) > TRIM_SAMPLES_BELOW_8 )
		return false;

	if( channels >= 2 && abs( buf[1] ) > TRIM_SAMPLES_BELOW_8 )
		return false;

	return true;
}

static qboolean S_ShouldTrimSample16( const int16_t *buf, int channels )
{
	if( abs( buf[0] ) > TRIM_SAMPLES_BELOW_16 )
		return false;

	if( channels >= 2 && abs( buf[1] ) > TRIM_SAMPLES_BELOW_16 )
		return false;

	return true;
}

static int S_TrimStart( const wavdata_t *wav, int start )
{
	size_t channels = wav->channels, width = wav->width, i;

	if( wav->type != WF_PCMDATA )
		return start;

	if( width == 1 )
	{
		const int8_t *data = (const int8_t *)&wav->buffer[channels * width * start];

		for( i = 0; i < TRIM_SCAN_MAX && start < wav->samples; i++ )
		{
			if( !S_ShouldTrimSample8( data, wav->channels ))
				break;

			start += channels;
			data += channels;
		}
	}
	else if( width == 2 )
	{
		const int16_t *data = (const int16_t *)&wav->buffer[channels * width * start];

		for( i = 0; i < TRIM_SCAN_MAX && start < wav->samples; i++ )
		{
			if( !S_ShouldTrimSample16( data, wav->channels ))
				break;

			start += channels;
			data += channels;
		}
	}

	return start;
}

static int S_TrimEnd( const wavdata_t *wav, int end )
{
	size_t channels = wav->channels, width = wav->width, i;

	if( wav->type != WF_PCMDATA )
		return end;

	if( width == 1 )
	{
		const int8_t *data = (const int8_t *)&wav->buffer[channels * width * ( end - 1 )];

		for( i = 0; i < TRIM_SCAN_MAX && end > 0; i++ )
		{
			if( !S_ShouldTrimSample8( data, wav->channels ))
				break;

			end -= channels;
			data -= channels;
		}
	}
	else if( width == 2 )
	{
		const int16_t *data = (const int16_t *)&wav->buffer[channels * width * ( end - 1 )];

		for( i = 0; i < TRIM_SCAN_MAX && end > 0; i++ )
		{
			if( !S_ShouldTrimSample16( data, wav->channels ))
				break;

			end -= channels;
			data -= channels;
		}
	}

	return end;
}

static void S_TrimStartEndTimes( channel_t *ch, wavdata_t *wav, int start, int end )
{
	ch->sample = start = S_TrimStart( wav, start );

	// don't overrun the buffer while trimming end
	if( end == 0 )
		end = wav->samples - wav->channels;

	if( end < start )
		end = start;

	ch->forced_end = S_TrimEnd( wav, end );
}

void VOX_LoadWord( channel_t *ch )
{
	SetBits( ch->flags, FL_CHAN_SENTENCE_FINISHED );

	if( ch->word_index < 0 || ch->word_index >= CVOXWORDMAX )
		return;

	const voxword_t *word = &ch->words[ch->word_index];

	if( !word->sfx )
		return;

	wavdata_t *data = S_LoadSound( word->sfx );

	if( !data )
		return;

	ClearBits( ch->flags, FL_CHAN_SENTENCE_FINISHED );
	ch->data = data;

	int start = word->start;
	int end   = word->end;

	if( end <= start )
		end = 0;

	S_TrimStartEndTimes( ch, data, round( start * 0.01f * data->samples ), round( end * 0.01f * data->samples ));
}

void VOX_FreeWord( channel_t *ch )
{
	// TODO: don't set random fields to zero lol, was memset before
	ch->sample = ch->forced_end = 0.0;
	ClearBits( ch->flags, FL_CHAN_FINISHED );
	ch->data = NULL;

	if( ch->word_index < 0 || ch->word_index >= CVOXWORDMAX )
		return;

	voxword_t *word = &ch->words[ch->word_index];

	if( !word->sfx || FBitSet( word->flags, FL_VOXWORD_IN_CACHE ))
		return;

	FS_FreeSound( word->sfx->cache );
	word->sfx->cache = NULL;
	word->sfx = NULL;
}

void VOX_SetChanVol( channel_t *ch )
{
	voxword_t *word;

	if( !ch->words || FBitSet( ch->flags, FL_CHAN_SENTENCE_FINISHED ))
		return;

	word = &ch->words[ch->word_index];

	if( word->volume == 100 )
		return;

	ch->leftvol = ch->leftvol * word->volume * 0.01f;
	ch->rightvol = ch->rightvol * word->volume * 0.01f;
}

float VOX_ModifyPitch( channel_t *ch, float pitch )
{
	voxword_t *word;

	if( !ch->words || FBitSet( ch->flags, FL_CHAN_SENTENCE_FINISHED ))
		return pitch;

	word = &ch->words[ch->word_index];

	if( word->pitch == PITCH_NORM )
		return pitch;

	pitch += ( word->pitch - PITCH_NORM ) * 0.01f;

	return pitch;
}

static const char *VOX_GetDirectory( char *szpath, const char *psz, int nsize )
{
	const char *p;
	int len;

	// HACKHACK: some modders send strings like "/fvox/_period four"
	// which should get parsed as "_period four" said by fvox
	// it might be incorrect but ignore first slash here for now
	if( psz[0] == '/' )
		psz++;

	// search / backwards
	p = Q_strrchr( psz, '/' );

	if( !p )
	{
		Q_strncpy( szpath, "vox/", nsize );
		return psz;
	}

	len = p - psz + 1;

	if( len > nsize )
	{
		Con_Printf( "%s: invalid directory in: %s\n", __func__, psz );
		return NULL;
	}

	memcpy( szpath, psz, len );
	szpath[len] = 0;

	return p + 1;
}

static const char *VOX_LookupString( const char *pszin )
{
	int i = -1, len;
	const char *c;

	// check if we are an immediate sentence
	if( *pszin == '#' )
	{
		// immediate sentence, probably coming from "speak" command
		return pszin + 1;
	}

	// check if we received an index
	if( Q_isdigit( pszin ))
	{
		i = Q_atoi( pszin );

		if( i >= cszrawsentences )
			i = -1;
	}

	// last hope: find it in sentences array
	if( i == -1 )
	{
		for( i = 0; i < cszrawsentences; i++ )
		{
			if( !Q_stricmp( pszin, rgpszrawsentence[i] ))
				break;
		}
	}

	// not found, exit
	if( i == cszrawsentences )
		return NULL;

	len = Q_strlen( rgpszrawsentence[i] );

	c = &rgpszrawsentence[i][len + 1];
	for( ; *c == ' ' || *c == '\t'; c++ );

	return c;
}

static int VOX_ParseString( char *psz, char *rgpparseword[CVOXWORDMAX] )
{
	int i = 0;

	if( !psz )
		return i;

	rgpparseword[i++] = psz;

	while( i < CVOXWORDMAX )
	{
		// skip to next word
		for( ; *psz &&
			*psz != ' ' &&
			*psz != '.' &&
			*psz != ',' &&
			*psz != '('; psz++ );

		// skip anything in between ( and )
		if( *psz == '(' )
		{
			for( ; *psz && *psz != ')'; psz++ );
			psz++;
		}

		if( !*psz )
			return i;

		// . and , are special but if not end of string
		if(( *psz == '.' || *psz == ',' ) &&
			psz[1] != '\n' && psz[1] != '\r' && psz[1] != '\0' )
		{
			if( *psz == '.' )
				rgpparseword[i++] = (char *)voxperiod;
			else rgpparseword[i++] = (char *)voxcomma;

			if( i >= CVOXWORDMAX )
				return i;
		}

		*psz++ = 0;

		for( ; *psz && ( *psz == '.' || *psz == ' ' || *psz == ',' );
		     psz++ );

		if( !*psz )
			return i;

		rgpparseword[i++] = psz;
	}

	return i;
}

static void VOX_MakeDefaultWordParams( voxword_t *voxword )
{
	*voxword = (voxword_t) {
		.volume = 100,
		.pitch = 100,
		.end = 100,
	};
}

static qboolean VOX_ParseWordParams( char *psz, voxword_t *pvoxword, voxword_t *default_voxword )
{
	int len, i;
	char sznum[8], *pszsave = psz;

	*pvoxword = *default_voxword;

	len = Q_strlen( psz );

	if( len == 0 )
		return false;

	// no special params
	if( psz[len-1] != ')' )
		return true;

	for( ; *psz != '(' && *psz != ')'; psz++ );

	// invalid syntax
	if( *psz == ')' )
		return false;

	// split filename and params
	*psz++ = '\0';

	for( ;; )
	{
		char command;

		// find command
		for( ; *psz &&
			*psz != 'v' &&
			*psz != 'p' &&
			*psz != 's' &&
			*psz != 'e' &&
			*psz != 't'; psz++ )
		{
			if( *psz == ')' )
				break;
		}

		command = *psz++;

		if( !isdigit((byte)*psz ))
			break;

		memset( sznum, 0, sizeof( sznum ));
		for( i = 0; i < sizeof( sznum ) - 1 && isdigit((byte)*psz ); i++, psz++ )
			sznum[i] = *psz;

		i = Q_atoi( sznum );
		switch( command )
		{
		case 'e':
			pvoxword->end = bound( 0, i, 100 );
			break;
		case 'p':
			pvoxword->pitch = bound( 0, i, UINT16_MAX );
			break;
		case 's':
			pvoxword->start = bound( 0, i, 100 );
			break;
		case 't':
			pvoxword->timecompress = bound( 0, i, 100 );
			break;
		case 'v':
			pvoxword->volume = bound( 0, i, UINT16_MAX );
			break;
		}
	}

	// no actual word but new defaults
	if( Q_strlen( pszsave ) == 0 )
	{
		*default_voxword = *pvoxword;
		return false;
	}

	return true;
}

void VOX_LoadSound( channel_t *ch, const char *pszin )
{
	char buffer[512] = { 0 }, szpath[32] = { 0 };
	char *rgpparseword[CVOXWORDMAX] = { 0 };
	const char *psz;
	int i, j;
	int num_words;
	voxword_t default_voxword;
	voxword_t words_buf[CVOXWORDMAX + 1]; // local scratch: parsed words + null terminator

	if( !pszin )
		return;

	// free any existing words from a previous sentence on this channel
	if( ch->words )
	{
		VOX_FreeWord( ch );
		Mem_Free2( &ch->words );
	}

	psz = VOX_LookupString( pszin );

	if( !psz )
	{
		// sometimes modders remove sentences but entities continue to use them, so it's a warning, not an error
		Con_Printf( S_WARN "%s: no sentence named %s\n", __func__, pszin );
		return;
	}

	psz = VOX_GetDirectory( szpath, psz, sizeof( szpath ));

	if( !psz )
	{
		Con_Printf( S_ERROR "%s: failed getting directory for %s\n", __func__, pszin );
		return;
	}

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

	Q_strncpy( buffer, psz, sizeof( buffer ));
	num_words = VOX_ParseString( buffer, rgpparseword );

	VOX_MakeDefaultWordParams( &default_voxword );
	memset( words_buf, 0, sizeof( words_buf ));

	for( i = 0, j = 0; i < num_words; i++ )
	{
		char pathbuffer[MAX_SYSPATH];

		if( !VOX_ParseWordParams( rgpparseword[i], &words_buf[j], &default_voxword ))
			continue;

		if( Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s", szpath, rgpparseword[i] ) < 0 )
		{
			Con_Printf( S_ERROR "%s: path to word in sentence %s is too long\n", __func__, pszin );
			return;
		}

		qboolean in_cache = false;
		words_buf[j].sfx = S_FindName( pathbuffer, &in_cache );
		if( in_cache )
			SetBits( words_buf[j].flags, FL_VOXWORD_IN_CACHE );

		j++;
	}

	// words_buf[j].sfx is already NULL from the memset — null terminator
	ch->words = Mem_Malloc( sndpool, ( j + 1 ) * sizeof( voxword_t ));
	memcpy( ch->words, words_buf, ( j + 1 ) * sizeof( voxword_t ));

	ch->sfx = ch->words[0].sfx;
	ch->word_index = 0;
	VOX_LoadWord( ch );
}

static void VOX_ReadSentenceFile_( byte *buf, fs_offset_t size )
{
	char *p, *last;

	p = (char *)buf;
	last = p + size;

	while( p < last )
	{
		char *name = NULL, *value = NULL;

		if( cszrawsentences >= CVOXFILESENTENCEMAX )
			break;

		for( ; p < last && ( *p == '\n' || *p == '\r' || *p == '\t' || *p == ' ' );
		     p++ );

		if( *p != '/' )
		{
			name = p;

			for( ; p < last && *p != ' ' && *p != '\t' ; p++ );

			if( p < last )
				*p++ = 0;

			value = p;
		}

		for( ; p < last && *p != '\n' && *p != '\r'; p++ );

		if( p < last )
			*p++ = 0;

		if( name )
		{
			int index = cszrawsentences;
			int size = strlen( name ) + strlen( value ) + 2;

			rgpszrawsentence[index] = Mem_Malloc( sndpool, size );
			memcpy( rgpszrawsentence[index], name, size );
			rgpszrawsentence[index][size - 1] = 0;
			cszrawsentences++;
		}
	}
}

static void VOX_ReadSentenceFile( const char *path )
{
	byte *buf;
	fs_offset_t size;

	VOX_Shutdown();

	buf = FS_LoadFile( path, &size, false );
	if( !buf ) return;

	VOX_ReadSentenceFile_( buf, size );

	Mem_Free( buf );
}

void VOX_Init( void )
{
	VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" );
}

void VOX_Shutdown( void )
{
	int i;

	for( i = 0; i < cszrawsentences; i++ )
		Mem_Free( rgpszrawsentence[i] );

	cszrawsentences = 0;
}

#if XASH_ENGINE_TESTS
#include "tests.h"

static void Test_VOX_GetDirectory( void )
{
	const char *data[] =
	{
		"", "", "vox/",
		"bark bark", "bark bark", "vox/",
		"barney/meow", "meow", "barney/",
		"/fvox/_period", "_period", "fvox/",
	};
	int i;

	for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 3 )
	{
		string szpath;
		const char *p = VOX_GetDirectory( szpath, data[i+0], sizeof( szpath ));

		TASSERT_STR( p, data[i+1] );
		TASSERT_STR( szpath, data[i+2] );
	}
}

static void Test_VOX_LookupString( void )
{
	int i;
	const char *p, *data[] =
	{
		"0", "123",
		"3", "SPAAACE",
		"-2", NULL,
		"404", NULL,
		"not found", NULL,
		"exactmatch", "123",
		"caseinsensitive", "456",
		"SentenceWithTabs", "789",
		"SentenceWithSpaces", "SPAAACE",
	};

	VOX_Shutdown();

	rgpszrawsentence[cszrawsentences++] = (char*)"exactmatch\000123";
	rgpszrawsentence[cszrawsentences++] = (char*)"CaseInsensitive\000456";
	rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithTabs\0\t\t\t789";
	rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithSpaces\0  SPAAACE";
	rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithTabsAndSpaces\0\t \t\t MEOW";

	for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 2 )
	{
		p = VOX_LookupString( data[i] );

		TASSERT_STR( p, data[i+1] );
	}

	cszrawsentences = 0;
}

static void Test_VOX_ParseString( void )
{
	char *rgpparseword[CVOXWORDMAX];
	const char *data[] =
	{
		"(p100) my ass is, heavy!(p80 t20) clik.",
		"(p100)", "my", "ass", "is", "_comma", "heavy!(p80 t20)", "clik", NULL,
		"freeman...",
		"freeman", "_period", NULL,
	};
	int i = 0;

	while( i < sizeof( data ) / sizeof( data[0] ))
	{
		char buffer[4096];
		int wordcount, j = 0;
		Q_strncpy( buffer, data[i], sizeof( buffer ));
		wordcount = VOX_ParseString( buffer, rgpparseword );

		i++;

		while( data[i] )
		{
			TASSERT_STR( data[i], rgpparseword[j] );
			i++;
			j++;
		}

		TASSERT( j == wordcount );

		i++;
	}
}

static void Test_VOX_ParseWordParams( void )
{
	string buffer;
	qboolean ret;
	voxword_t default_word;
	voxword_t word;

	VOX_MakeDefaultWordParams( &default_word );

	Q_strncpy( buffer, "heavy!(p80)", sizeof( buffer ));
	ret = VOX_ParseWordParams( buffer, &word, &default_word );
	TASSERT_STR( buffer, "heavy!" );
	TASSERT( word.pitch == 80 );
	TASSERT( ret );

	Q_strncpy( buffer, "(p105)", sizeof( buffer ));
	ret = VOX_ParseWordParams( buffer, &word, &default_word );
	TASSERT_STR( buffer, "" );
	TASSERT( word.pitch == 105 );
	TASSERT( !ret );

	Q_strncpy( buffer, "quiet(v50)", sizeof( buffer ));
	ret = VOX_ParseWordParams( buffer, &word, &default_word );
	TASSERT_STR( buffer, "quiet" );
	TASSERT( word.pitch == 105 ); // defaulted
	TASSERT( word.volume == 50 );
	TASSERT( ret );
}

void Test_RunVOX( void )
{
	TRUN( Test_VOX_GetDirectory() );
	TRUN( Test_VOX_LookupString() );
	TRUN( Test_VOX_ParseString() );
	TRUN( Test_VOX_ParseWordParams() );
}

#endif /* XASH_ENGINE_TESTS */
