/*
===========================================================================
Copyright (C) 2023 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code 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 2 of the License,
or (at your option) any later version.

OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// tiki_parse.cpp : TIKI Script

#include "q_shared.h"
#include "qcommon.h"
#include "tiki.h"

TikiScript *TikiScript::currentScript;

TikiScript::~TikiScript()
{
	Close();
}

TikiScript::TikiScript()
{
	error			= false;
	buffer			= NULL;
	script_p		= NULL;
	end_p			= NULL;
	line			= 0;
	releaseBuffer	= false;
	tokenready		= false;
	memset( token, 0, sizeof( token ) );
	include			= NULL;
	parent			= NULL;
	nummacros		= 0;
	mark_pos		= 0;
	path[ 0 ]		= 0;
}

/*
==============
=
= Close
=
==============
*/
void TikiScript::Close( void )
{
	Uninclude();
	if( this->releaseBuffer )
	{
		if( this->buffer )
			TIKI_Free( this->buffer );
	}

	buffer			= NULL;
	script_p		= NULL;
	end_p			= NULL;
	line			= 0;
	error			= false;
	releaseBuffer	= false;
	tokenready		= false;
	nummacros		= 0;
	memset( token, 0, sizeof( token ) );
	mark_pos		= 0;
}

/*
==============
=
= Filename
=
==============
*/
const char *TikiScript::Filename()
{
	return filename;
}

/*
==============
=
= GetLineNumber
=
==============
*/
int TikiScript::GetLineNumber()
{
	return line;
}

/*
==============
=
= Reset
=
==============
*/
void TikiScript::Reset( void )
{
	Uninclude();

	nummacros		= 0;
	error			= 0;
	line			= 1;
	tokenready		= 0;
	mark_pos		= 0;
	script_p		= buffer;
}

/*
==============
=
= SkipToEOL
=
==============
*/
qboolean TikiScript::SkipToEOL( void )
{
	if( script_p >= end_p )
	{
		return true;
	}

	while( *script_p != TOKENEOL )
	{
		if( script_p >= end_p )
		{
			return true;
		}
		script_p++;
	}
	return false;
}

/*
==============
=
= SafeCheckOverflow
=
==============
*/
qboolean TikiScript::SafeCheckOverflow( void )
{
	return script_p >= end_p;
}

/*
==============
=
= CheckOverflow
=
==============
*/
void TikiScript::CheckOverflow( void )
{
	if( script_p >= end_p )
	{
		this->error = 1;
		TIKI_DPrintf( "End of tiki file reached prematurely reading %s\n", Filename() );
		TIKI_DPrintf( "   This may be caused by having a tiki file command at the end of the file\n" );
		TIKI_DPrintf( "   without an 'end' at the end of the tiki file.\n" );
	}
}

/*
==============
=
= SkipWhiteSpace
=
==============
*/
void TikiScript::SkipWhiteSpace( qboolean crossline )
{
	//
	// skip space
	//
	while( !SafeCheckOverflow() )
	{
		if( *script_p <= TOKENSPACE )
		{
			if( *script_p == TOKENEOL )
			{
				if( !crossline )
				{
					TIKI_DPrintf( "Line %i is incomplete in file %s\n", line, Filename() );
					return;
				}

				line++;
			}
			script_p++;
		}
		else
		{
			if( !AtComment() )
			{
				return;
			}

			if( AtExtendedComment() )
			{
				SkipExtendedComment();
			}
			else
			{
				if( !crossline )
				{
					TIKI_DPrintf( "Line %i is incomplete in file %s\n", line, Filename() );
					return;
				}
				SkipToEOL();
			}
		}
	}
}

/*
==============
=
= AtComment
=
==============
*/
qboolean TikiScript::AtComment( void )
{
	if( script_p >= end_p )
	{
		return false;
	}

	if( *script_p == TOKENCOMMENT )
	{
		return true;
	}

	if( *script_p == TOKENCOMMENT2 )
	{
		return true;
	}

	// Two or more character comment specifiers
	if( ( script_p + 1 ) >= end_p )
	{
		return false;
	}

	if( ( *script_p == '/' ) && ( ( *( script_p + 1 ) == '/' ) || ( *( script_p + 1 ) == '*' ) ) )
	{
		return true;
	}

	return false;
}

/*
==============
=
= AtExtendedComment
=
==============
*/
qboolean TikiScript::AtExtendedComment( void )
{
	if( script_p >= end_p )
	{
		return false;
	}

	// Two or more character comment specifiers
	if( ( script_p + 1 ) >= end_p )
	{
		return false;
	}

	if( ( *( script_p ) == '/' ) && ( *( script_p + 1 ) == '*' ) )
	{
		return true;
	}

	return false;
}

/*
==============
=
= SkipExtendedComment
=
==============
*/
void TikiScript::SkipExtendedComment( void )
{
	CheckOverflow();

	if( error )
	{
		return;
	}

	while( ( script_p + 1 ) < end_p )
	{
		if( ( *( script_p ) == '*' ) && ( *( script_p + 1 ) == '/' ) )
		{
			script_p = script_p + 2;
			return;
		}

		if( *script_p == '\n' )
			line++;

		script_p++;
	}
}

/*
==============
=
= AtCommand
=
==============
*/
qboolean TikiScript::AtCommand( void )
{
	if( script_p >= end_p )
	{
		return false;
	}

	// Two or more character comment specifiers
	if( ( script_p + 1 ) >= end_p )
	{
		return false;
	}

	if( ( *script_p == '$' ) && ( *( script_p + 1 ) != '$' ) )
	{
		return true;
	}

	return false;
}

/*
==============
=
= Uninclude
=
==============
*/
void TikiScript::Uninclude( void )
{
	if( include )
	{
		currentScript = this;
		include->Close();
		delete include;
		include = NULL;
	}
}

/*
==============
=
= AddMacro
=
==============
*/
void TikiScript::AddMacro( const char *name, const char *expansion )
{
	int i;

	for( i = 0; i < nummacros; i++ )
	{
		if( !stricmp( name, macros[ i ].name ) )
			return;
	}

	if( nummacros >= TIKI_MAXMACROS )
	{
		TIKI_DPrintf( "Too many %cdefine in file %s\n", TOKENSPECIAL, Filename() );
	}
	else
	{
		Q_strncpyz( macros[ nummacros ].name, name, sizeof( macros[ nummacros ].name ) );
		Q_strncpyz( macros[ nummacros ].macro, expansion, sizeof( macros[ nummacros ].macro ) );
		nummacros++;
	}

	// Add the macro to parent
	if( parent )
		parent->AddMacro( name, expansion );
}

/*
==============
=
= ProcessCommand
=
==============
*/
qboolean TikiScript::ProcessCommand( qboolean crossline )
{
	char dummy;
	int i;
	size_t len;
	char command[ 256 ];
	char argument1[ 256 ];
	char argument2[ 256 ];

	sscanf( script_p, "%c%s %s %s\n", &dummy, command, argument1, argument2 );

	if( !stricmp( command, "define" ) )
	{
		AddMacro( argument1, argument2 );
		SkipToEOL();
		SkipWhiteSpace( crossline );
	}
	else if( !stricmp( command, "include" ) )
	{
		SkipToEOL();
		SkipWhiteSpace( crossline );
		include = new TikiScript;
		if( include->LoadFile( argument1, qfalse ) )
		{
			include->SkipNonToken( crossline );
			for( i = 0; i < nummacros; i++ )
			{
				include->AddMacro( macros[ i ].name, macros[ i ].macro );
			}
			include->parent = this;
		}
		else
		{
			TIKI_Error( "^~^~^ Cannot find include file '%s' in %s on line %d\n", argument1, Filename(), GetLineNumber() );
			Uninclude();
		}
	}
	else if( !stricmp( command, "path" ) )
	{
		Q_strncpyz( path, argument1, sizeof( path ) );
		len = strlen( path );

		if( path[ len - 1 ] != '/' &&
			path[ len - 1 ] != '\\' )
		{
			Q_strcat( path, sizeof( path ), "/" );
		}

		SkipToEOL();
		SkipWhiteSpace( crossline );
	}
	else
	{
		return false;
	}

	return true;
}

/*
==============
=
= Completed
=
==============
*/
qboolean TikiScript::Completed( void )
{
	return script_p >= end_p;
}

/*
==============
=
= FindMacro
=
==============
*/
const char *TikiScript::FindMacro( const char *macro )
{
	int i;

	for( i = 0; i < nummacros; i++ )
	{
		if( !stricmp( macro, macros[ i ].name ) )
			return macros[ i ].macro;
	}

	return NULL;
}

/*
==============
=
= SkipNonToken
=
==============
*/
void TikiScript::SkipNonToken( qboolean crossline )
{
	//
	// skip space and comments
	//
	SkipWhiteSpace( crossline );
	while( !error )
	{
		if( !AtCommand() )
			break;
		if( !ProcessCommand( crossline ) )
			break;
		if( include )
			break;
	}
}

/*
==============
=
= TokenAvailable
=
==============
*/
qboolean TikiScript::TokenAvailable( qboolean crossline )
{
	if( include )
	{
		qboolean ret = include->TokenAvailable( crossline );

		if( ret || !crossline || !include->Completed() )
			return ret;

		Uninclude();
	}
	if( tokenready )
	{
		return true;
	}

	while( !SafeCheckOverflow() )
	{
		if( *script_p <= TOKENSPACE )
		{
			if( *script_p == TOKENEOL )
			{
				if( !crossline )
				{
					return false;
				}
				line++;
			}
			script_p++;
		}
		else
		{
			if( !AtComment() )
				return true;

			if( AtExtendedComment() )
			{
				SkipExtendedComment();
			}
			else
			{
				if( !crossline )
					return false;
				SkipToEOL();
			}
		}
	}

	return false;
}

/*
==============
=
= CommentAvailable
=
==============
*/
qboolean TikiScript::CommentAvailable( qboolean crossline )
{
	const char *searchptr;

	searchptr = script_p;

	if( searchptr >= end_p )
	{
		return false;
	}

	while( *searchptr <= TOKENSPACE )
	{
		if( ( *searchptr == TOKENEOL ) && ( !crossline ) )
		{
			return false;
		}
		searchptr++;
		if( searchptr >= end_p )
		{
			return false;
		}
	}

	return true;
}

/*
==============
=
= UnGetToken
=
==============
*/
void TikiScript::UnGetToken( void )
{
	if( include )
	{
		include->UnGetToken();
	}
	else
	{
		tokenready = true;
	}
}

/*
==============
=
= AtString
=
==============
*/
qboolean TikiScript::AtString( qboolean crossline )
{
	TikiScript *i;
	//
	// skip space
	//

	i = this;
	while( 1 )
	{
		i->SkipNonToken( crossline );

		if( !i->include )
			break;

		i = i->include;
	}

	return ( !i->error ) && ( *i->script_p == '"' );
}

/*
==============
=
= GetToken
=
==============
*/
const char *TikiScript::GetToken( qboolean crossline )
{
	TikiScript *i;
	const char *macro_start;
	const char *macro_end;
	char *token_p;
	int len;
	const char *subst;
	char macro[ 256 ];
	char temptoken[ 256 ];

	i = this;
	while( 1 )
	{
		if( i->include )
		{
			const char *ret;

			ret = i->include->GetToken( crossline );

			if( *ret || !crossline || !i->include->Completed() )
			{
				return ret;
			}
			i->Uninclude();
		}

		if( i->tokenready )
		{
			i->tokenready = false;
			currentScript = i;
			return i->token;
		}

		i->SkipNonToken( crossline );

		if( i->error )
		{
			return "";
		}

		if( !i->include )
		{
			break;
		}

		i = i->include;
	}

	token_p = ( char * )i->script_p;

	if( *token_p == '"' )
	{
		currentScript = i;
		return i->GetString( crossline );
	}

	macro_start = NULL;
	macro_end = NULL;
	token_p = i->token;
	while( ( *i->script_p > TOKENSPACE ) && !i->AtComment() )
	{
		if( ( *i->script_p == '\\' ) && ( i->script_p < ( end_p - 1 ) ) )
		{
			i->script_p++;
			switch( *i->script_p )
			{
			case 'n':	*token_p++ = '\n';			break;
			case 'r':	*token_p++ = '\n';			break;
			case '\'':	*token_p++ = '\'';			break;
			case '\"':	*token_p++ = '\"';			break;
			case '\\':	*token_p++ = '\\';			break;
			default:	*token_p++ = *i->script_p;	break;
			}
			i->script_p++;
		}
		else
		{
			if( *i->script_p == '$' )
			{
				if( macro_start )
					macro_end = token_p;
				else
					macro_start = token_p;
			}
			*token_p++ = *i->script_p++;
		}

		if( token_p == &i->token[ TIKI_MAXTOKEN ] )
		{
			TIKI_DPrintf( "Token too large on line %i in file %s\n", i->line, i->Filename() );
			break;
		}

		if( i->script_p == i->end_p )
		{
			break;
		}
	}

	*token_p = 0;

	if( macro_start )
	{
		if( !macro_end ) macro_end = token_p;

		len = ( macro_end - 1 ) - ( macro_start + 1 );
		if( len == -1 )
		{
			if( macro_start - i->token != 0 )
				memcpy( temptoken, i->token, macro_start - i->token );

			temptoken[ macro_start - i->token ] = '$';
			temptoken[ macro_start - i->token + 1 ] = 0;
			if( *( macro_end + 1 ) )
			{
				strcat( temptoken, macro_end + 1 );
			}
			Q_strncpyz( i->token, temptoken, sizeof( i->token ) );
		}
		else
		{
			memcpy( macro, ( macro_start + 1 ), len + 1 );
			macro[ len + 1 ] = 0;
			subst = i->FindMacro( macro );
			if( subst )
			{
				if( macro_start - i->token != 0 )
					memcpy( temptoken, i->token, macro_start - i->token );

				temptoken[ macro_start - i->token ] = 0;
				strcat( temptoken, subst );
				if( *( macro_end + 1 ) )
				{
					strcat( temptoken, macro_end + 1 );
				}
				Q_strncpyz( i->token, temptoken, sizeof( i->token ) );
			}
		}
	}

	i->currentScript = i;
	return i->token;
}

/*
==============
=
= GetLine
=
==============
*/
const char *TikiScript::GetLine( qboolean crossline )
{
	TikiScript *i;
	const char *start;
	int size;

	i = this;
	while( 1 )
	{
		if( i->include )
		{
			const char *ret;

			ret = i->include->GetToken( crossline );

			if( *ret || !crossline || !i->include->Completed() )
			{
				return ret;
			}
			i->Uninclude();
		}

		if( i->tokenready )
		{
			i->tokenready = false;
			return i->token;
		}

		if( !i->include )
			break;

		i = i->include;
	}

	if( i->error )
	{
		return "";
	}

	//
	// copy token
	//
	start = i->script_p;
	SkipToEOL();
	size = i->script_p - start;
	if( size < ( TIKI_MAXTOKEN - 1 ) )
	{
		memcpy( i->token, start, size );
		i->token[ size ] = '\0';
	}
	else
	{
		TIKI_DPrintf( "Token too large on line %i in file %s\n", i->line, i->filename );
	}

	return i->token;
}

/*
==============
=
= GetAndIgnoreLine
=
==============
*/
const char *TikiScript::GetAndIgnoreLine( qboolean crossline )
{
	const char *start;
	int size;

	if( include )
	{
		const char *ret;

		ret = include->GetAndIgnoreLine( crossline );

		if( *ret || !crossline || !include->Completed() )
			return ret;

		Uninclude();
	}

	if( tokenready )
	{
		tokenready = false;
		return token;
	}

	//
	// skip space
	//
	SkipWhiteSpace( crossline );

	if( error )
	{
		return "";
	}

	//
	// copy token
	//
	start = script_p;
	SkipToEOL();
	size = script_p - start;
	if( size < ( TIKI_MAXTOKEN - 1 ) )
	{
		memcpy( token, start, size );
		token[ size ] = '\0';
	}
	else
	{
		TIKI_DPrintf( "Token too large on line %i in file %s\n", line, filename );
	}

	return token;
}

/*
==============
=
= GetRaw
=
==============
*/
const char *TikiScript::GetRaw( void )
{
	const char *start;
	int size;

	if( include )
	{
		const char *ret;

		ret = include->GetRaw();

		if( *ret || !include->Completed() )
			return ret;

		Uninclude();
	}

	//
	// skip white space
	//
	SkipWhiteSpace( true );

	if( error )
	{
		return "";
	}

	//
	// copy token
	//
	start = script_p;
	SkipToEOL();
	size = script_p - start;
	if( size < ( TIKI_MAXTOKEN - 1 ) )
	{
		memset( token, 0, sizeof( token ) );
		memcpy( token, start, size );
	}
	else
	{
		TIKI_DPrintf( "Token too large on line %i in file %s\n", line, filename );
	}

	return token;
}

/*
==============
=
= GetString
=
==============
*/
const char *TikiScript::GetString( qboolean crossline )
{
	TikiScript *i;
	int startline;
	char *token_p;

	i = this;
	while( 1 )
	{
		if( i->include )
		{
			const char *ret;

			ret = i->include->GetString( crossline );

			if( *ret || !crossline || !i->include->Completed() )
			{
				return ret;
			}
			i->Uninclude();
		}

		// is a token already waiting?
		if( i->tokenready )
		{
			i->tokenready = false;
			return i->token;
		}

		i->SkipNonToken( crossline );

		if( !i->include )
			break;

		i = i->include;
	}

	if( i->error )
	{
		return "";
	}

	if( *i->script_p != '"' )
	{
		TIKI_DPrintf( "Expecting string on line %i in file %s\n", i->line, i->filename );
		return i->token;
	}

	i->script_p++;

	startline = i->line;
	token_p = i->token;
	while( *i->script_p != '"' )
	{
		if( *i->script_p == TOKENEOL )
		{
			TIKI_DPrintf( "Line %i is incomplete while reading string in file %s\n", i->line, i->filename );
			return i->token;
		}

		if( ( *i->script_p == '\\' ) && ( i->script_p < ( end_p - 1 ) ) )
		{
			i->script_p++;
			switch( *i->script_p )
			{
			case 'n':	*token_p++ = '\n';			break;
			case 'r':	*token_p++ = '\n';			break;
			case '\'':	*token_p++ = '\'';			break;
			case '\"':	*token_p++ = '\"';			break;
			case '\\':	*token_p++ = '\\';			break;
			default:	*token_p++ = *i->script_p;	break;
			}
			i->script_p++;
		}
		else
		{
			*token_p++ = *i->script_p++;
		}

		if( i->script_p >= i->end_p )
		{
			TIKI_DPrintf( "End of token file reached prematurely while reading string on\n"
				"line %d in file %s\n", startline, i->filename );
		}

		if( token_p == &i->token[ TIKI_MAXTOKEN ] )
		{
			TIKI_DPrintf( "String too large on line %i in file %s\n", i->line, i->filename );
		}
	}

	*token_p = 0;

	// skip last quote
	i->script_p++;

	return i->token;
}

/*
==============
=
= GetSpecific
=
==============
*/
qboolean TikiScript::GetSpecific( const char *string )
{
	if( include )
	{
		qboolean ret = include->GetSpecific( string );
		if( ret || !include->Completed() )
		{
			return ret;
		}
	}

	do
	{
		if( !TokenAvailable( true ) )
		{
			return false;
		}
		GetToken( true );
	} while( strcmp( token, string ) );

	return true;
}

/*
==============
=
= GetInteger
=
==============
*/
int TikiScript::GetInteger( qboolean crossline )
{
	return atoi( GetToken( crossline ) );
}

/*
==============
=
= GetDouble
=
==============
*/
double TikiScript::GetDouble( qboolean crossline )
{
	return atof( GetToken( crossline ) );
}

/*
==============
=
= GetFloat
=
==============
*/
float TikiScript::GetFloat( qboolean crossline )
{
	return ( float )GetDouble( crossline );
}

/*
==============
=
= GetVector
=
==============
*/
void TikiScript::GetVector( qboolean crossline, float *vec )
{
	vec[ 0 ] = GetFloat( crossline );
	vec[ 1 ] = GetFloat( crossline );
	vec[ 2 ] = GetFloat( crossline );
}

/*
==============
=
= LinesInFile
=
==============
*/
int TikiScript::LinesInFile( void )
{
	qboolean		temp_tokenready;
	const char		*temp_script_p;
	int				temp_line;
	char			temp_token[ TIKI_MAXTOKEN ];
	int				numentries;

	temp_tokenready		= tokenready;
	temp_script_p		= script_p;
	temp_line			= line;
	Q_strncpyz( temp_token, token, sizeof( temp_token ) );

	numentries = 0;

	Reset();
	while( TokenAvailable( true ) )
	{
		GetLine( true );
		numentries++;
	}

	tokenready	= temp_tokenready;
	script_p	= temp_script_p;
	line		= temp_line;
	Q_strncpyz( token, temp_token, sizeof( token ) );

	return numentries;
}

/*
==============
=
= Parse
=
==============
*/
void TikiScript::Parse( char *data, int length, const char *name )
{
	Close();

	buffer = data;
	Reset();
	this->length = length;
	end_p = script_p + length;
	strncpy( filename, name, TIKI_MAXTOKEN );
}

/*
==============
=
= LoadFile
=
==============
*/
qboolean TikiScript::LoadFile( const char *name, qboolean quiet )
{
	int			length;
	char		*buf;

	Close();

	length = TIKI_ReadFileEx( name, ( void ** )&buf, quiet );

	if( length < 0 )
	{
		if( !quiet )
			TIKI_DPrintf( "Tiki:LoadFile Couldn't load %s\n", name );
		return false;
	}

	// create our own space
	buffer = ( char * )TIKI_Alloc( length + 1 );
	// copy the file over to our space
	memcpy( buffer, buf, length );
	buffer[ length ] = 0;
	// free the file
	TIKI_FreeFile( buf );

	Parse( buffer, length, name );
	releaseBuffer = true;

	if( error )
	{
		return false;
	}

	return true;
}

/*
==============
=
= Token
=
==============
*/
const char *TikiScript::Token( void )
{
	if( include )
	{
		return include->token;
	}
	else
	{
		return token;
	}
}

/*
==============
=
= MarkPos
=
==============
*/
void TikiScript::MarkPos( void )
{
	mark[ mark_pos ].mark_script_p = script_p;
	mark[ mark_pos ].mark_tokenready = tokenready;
	Q_strncpyz( mark[ mark_pos ].mark_token, token, sizeof(mark[ mark_pos ].mark_token));
	mark_pos++;
}

/*
==============
=
= ReturnToMark
=
==============
*/
void TikiScript::ReturnToMark( void )
{
	if( mark_pos <= 0 )
	{
		return;
	}

	mark_pos--;
	script_p	= mark[ mark_pos ].mark_script_p;
	tokenready	= mark[ mark_pos ].mark_tokenready;
	memcpy( token, mark[ mark_pos ].mark_token, sizeof( token ) );
}

/*
==============
=
= ReplaceLineWithWhitespace
=
==============
*/
void TikiScript::ReplaceLineWithWhitespace( bool deleteFromStartOfLine )
{
	char *change_p = ( char * )script_p;

	if( deleteFromStartOfLine && *( change_p - 1 ) != '\n' )
	{
		while( *( change_p - 1 ) != TOKENEOL && change_p > buffer )
		{
			change_p--;
		}
	}

	if( *change_p != TOKENEOL )
	{
		if( change_p >= end_p )
		{
			script_p = change_p;
			return;
		}

		do
		{
			*change_p++ = TOKENSPACE;
		} while( *change_p != TOKENEOL && change_p < end_p );
	}

	if( change_p < end_p )
	{
		script_p = change_p + 1;
		line++;
	}
	else
	{
		script_p = change_p;
	}
}

/*
==============
=
= GetParentToken
=
==============
*/
const char *TikiScript::GetParentToken( void )
{
	if( parent )
		return parent->token;
	else
		return "";
}
