//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: searches through all bsp files in the current directory parsing out entity details
//
//=============================================================================//

#include <stdio.h>
#include <string.h>
#include <io.h>
#include <malloc.h>

#define max(x,y) ( ((x) > (y)) ? (x) : (y) )

void SetSearchWord( const char *searchWord );
char *FindSearchWord( char *buffer, char *bufend );
char *ParseToken( char *data, char *newToken );

void ClearTable( void );
void ClearUsageCountTable( void );
void AddToTable( const char *name );
void PrintOutTable( void );

void ParseFGD( char *buffer, char *bufend, const char *searchKey );

const char *g_UsageString = "usage:  entcount [-fgd <fgdfile>] [-nofgd] [-permap] [-onlyent <entname>] [-files <searchmask>]\n";


int main( int argc, char *argv[] )
{
	if ( argc < 2 )
	{
		printf( g_UsageString );
		return 0;
	}

	bool printPerMap = false;
	const char *filterEnt = NULL;
	const char *fgdFile = NULL;
	const char *fileMask = "*.bsp";

	// parse the arguments
	for ( int count = 1; count < argc; count++ )
	{
		if ( !stricmp( argv[count], "-permap" ) )
		{
			printPerMap = true;
		}
		else if ( !stricmp( argv[count], "-onlyent" ) )
		{
			count++;
			if ( count < argc )
			{
				filterEnt = argv[count];
			}
		}
		else if ( !stricmp( argv[count], "-fgd" ) )
		{
			count++;
			if ( count < argc )
			{
				fgdFile = argv[count];
			}
		}
		else if ( !stricmp( argv[count], "-files" ) )
		{
			count++;
			if ( count < argc )
			{
				fileMask = argv[count];
			}
		}
		else if ( !stricmp( argv[count], "-nofgd" ) )
		{
		}
		else
		{
			printf( "error: unknown parameter \"%s\"\n", argv[count] );
			printf( g_UsageString );
			return 1;
		}
	}

	// clear the entity accumulator table
	ClearTable();

	// open and parse the FGD, unless the -nofgd flag is specified
	if ( fgdFile && !filterEnt && !printPerMap )
	{
		FILE *f = fopen( fgdFile, "rb" );
		if ( !f )
		{
			printf( "error: could not open file %s\n", fgdFile );
			return 2;
		}

		int filelen;
		fseek( f, 0, SEEK_END );
		filelen = ftell( f );
		fseek( f, 0, SEEK_SET );

		// allocate and load into memory
		char *buffer = (char*)malloc( filelen );
		char *bufend = buffer + filelen;
		fread( buffer, filelen, 1, f );
		fclose( f );

		// search for all @pointclass, then @solidclass
		ParseFGD( buffer, bufend, "PointClass" );
		ParseFGD( buffer, bufend, "SolidClass" );

		// reset the usage counts to 0
		ClearUsageCountTable();	

		free( buffer );
	}

	// parse through all the bsp files
	_finddata_t fileinfo;
	int FHandle = _findfirst( fileMask, &fileinfo );
	
	if ( FHandle == -1 )
	{
		printf( "error: no files found in current directory\n" );
		return 1;
	}

	SetSearchWord( "\"classname\"" );

	do {
		// open the file
		FILE *f = fopen( fileinfo.name, "rb" );
		if ( !f )
		{
			printf( "error: couldn't open file %s\n", fileinfo.name );
			return 2;
		}

		// calculate file length
		int filelen;
		fseek( f, 0, SEEK_END );
		filelen = ftell( f );
		fseek( f, 0, SEEK_SET );

		// allocate and load into memory
		char *buffer = (char*)malloc( filelen );
		char *bufpos = buffer + strlen( "\"classname\"" ) - 1;
		char *bufend = buffer + filelen;
		fread( buffer, filelen, 1, f );
		fclose( f );

		bool entFound = false;

		while ( 1 )
		{
			bufpos = FindSearchWord( bufpos, bufend );
			if ( !bufpos )
				break;

			// find the next word
			static char Token[256];
			ParseToken( bufpos, Token );

			// add the word to the list, filtering if necessary
			if ( !filterEnt || !stricmp(filterEnt, Token) )
			{
				AddToTable( Token );
				entFound = true;
			}

			bufpos += strlen( Token );
		}

		free( buffer );

		// print the bsp name, if an ent is found, or we are not filtering for ents
		if ( entFound || !filterEnt )
			printf( "%s\n", fileinfo.name );

		if ( printPerMap )
		{
			PrintOutTable();
			ClearUsageCountTable();
		}

	} while ( _findnext(FHandle, &fileinfo) == 0 );

	PrintOutTable();

	return 0;
}

void ParseFGD( char *buffer, char *bufend, const char *searchKey )
{
	char *bufpos = buffer + strlen( searchKey ) - 1;
	SetSearchWord( searchKey );

	while ( 1 )
	{
		bufpos = FindSearchWord( bufpos, bufend );
		if ( !bufpos )
			break;

		// search for the corresponding '='
		while ( *bufpos != '=' )
			bufpos++;
		bufpos++;
		
		// find the classname
		static char Token[256];
		ParseToken( bufpos, Token );

		AddToTable( Token );

		bufpos += strlen( Token );
	}
}


/////////// entity table stuff //////////////
const int MAX_ENTS = 2000;
int NumEnts = 0;
char *EntNames[ MAX_ENTS ];
int EntUsage[ MAX_ENTS ];

void ClearTable( void )
{
	memset( EntNames, 0, sizeof(EntNames) );
	memset( EntUsage, 0, sizeof(EntUsage) );
}

void ClearUsageCountTable( void )
{
	memset( EntUsage, 0, sizeof(EntUsage) );
}

void AddToTable( const char *name )
{
	// search for it in the table
	for ( int i = 0; i < NumEnts;  i++ )
	{
		if ( EntNames[i] && !strcmp(EntNames[i], name) )
		{
			// it's already in the table
			// increment the usage count
			EntUsage[i] += 1;
			return;
		}
	}

	// append to the table
	EntNames[NumEnts] = (char*)malloc( strlen(name) + 1 );
	strcpy( EntNames[NumEnts], name );
	EntUsage[NumEnts] = 1;

	NumEnts++;
}

void PrintOutTable( void )
{
	while ( 1 )
	{
		// find the highest item
		int highestUsage = -1;
		int highestEnt = 0;

		for ( int i = 0; i < NumEnts; i++ )
		{
			if ( EntNames[i] && highestUsage < EntUsage[i] )
			{
				highestUsage = EntUsage[i];
				highestEnt = i;
			}
		}

		// check for no more ents
		if ( highestUsage == -1 )
			return;
		
		// display usage stats of item
		printf( " %5d  %s\n", highestUsage, EntNames[highestEnt] );

		// remove item from list
		free( EntNames[highestEnt] );
		EntNames[highestEnt] = NULL;
	}
}



////////// string search stuff ////////////

static unsigned char JumpTable[256];
static int SearchWordLen = 0;
static const char *SearchWord;

void SetSearchWord( const char *Word )
{
	SearchWord = Word;
	SearchWordLen = strlen( SearchWord );

	// build the jump table
	
	// initialize all values to jump the length of the string
	memset( JumpTable, SearchWordLen, sizeof(JumpTable) );

	// set the amount the searcher can jump forward, depending on the character
	for ( int i = 0; i < SearchWordLen; i++ )
	{
		JumpTable[ (unsigned char)SearchWord[i] ] = max( SearchWordLen - i - 1, 1 );
	}
}

char *FindSearchWord( char *buffer, char *bufend )
{

/*
	for ( int i = SearchWordLen-1; i >= 0; i-- )
	{
		if ( *buffer != SearchWord[i] )
		{
			buffer += ( JumpTable[ (unsigned char)(*(buffer + i - SearchWordLen + 1)) ] - 1 );

			// no more buffer to search
			if ( buffer >= bufend )
				return NULL;

			// reset search counter
			i = SearchWordLen;
		}
		else
		{
			// it's a match, move backwards to search
			buffer--;
		}
	}
*/

	while ( 1 )
	{
		if ( strnicmp(buffer - SearchWordLen, SearchWord, SearchWordLen) )
		{
			// strings not equal, jump ahead
			buffer += JumpTable[ (unsigned char)*buffer ];

			if ( buffer >= bufend )
				return NULL;
		}
		else
		{
			break;
		}
	}

	// we have a match!

	// return a pointer just past the found key
	return buffer;
}



/*
==============
ParseToken

Parse a token out of a string
outputs the parsed token into newToken
==============
*/
char *ParseToken( char *data, char *newToken )
{
	int             c;
	int             len;
	
	len = 0;
	newToken[0] = 0;
	
	if (!data)
		return NULL;
		
// skip whitespace
skipwhite:
	while ( (c = *data) <= ' ')
	{
		if (c == 0)
			return NULL;                    // end of file;
		data++;
	}
	
// skip // comments
	if (c=='/' && data[1] == '/')
	{
		while (*data && *data != '\n')
			data++;
		goto skipwhite;
	}
	

// handle quoted strings specially
	if (c == '\"')
	{
		data++;
		while ( len < 256 )
		{
			c = *data++;
			if (c=='\"' || !c)
			{
				newToken[len] = 0;
				return data;
			}
			newToken[len] = c;
			len++;
		}

		if ( len >= 256 )
		{
			len--;
			newToken[len] = 0;
		}
	}

// parse single characters
	if ( c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' )
	{
		newToken[len] = c;
		len++;
		newToken[len] = 0;
		return data+1;
	}

// parse a regular word
	do
	{
		newToken[len] = c;
		data++;
		len++;
		c = *data;
		if ( c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' )
			break;

		if ( len >= 256 )
		{
			len--;
			newToken[len] = 0;
			break;
		}

	} while (c>32);
	
	newToken[len] = 0;
	return data;
}