#include "../vpc/vpc.h"
#include "crccheck_shared.h"
#include "tier1/checksum_crc.h"
#include "tier1/strtools.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#ifdef _WIN32
#include <process.h>
#else
#include <stdlib.h>
#define stricmp strcasecmp
#endif

#pragma warning( disable : 4996 )
#pragma warning( disable : 4127 )

#define MAX_INCLUDE_STACK_DEPTH 10


static bool IsValidPathChar( char token ) 
{
	// does it look like a file?  If this ends up too tight, can probably just check that it's not '[' or '{'
	// cause conditional blocks are what we really want to avoid.
	return isalpha(token) || isdigit(token) || (token == '.') || (token == '\\') || (token == '/');
}

extern const char *g_szArrPlatforms[];
static void BuildReplacements( const char *token, char *szReplacements ) 
{
	// Now go pickup the any files that exist, but were non-matches
	*szReplacements = '\0';
	for ( int i = 0; g_szArrPlatforms[i] != NULL; i++ )
	{
		char szPath[MAX_PATH];
		char szPathExpanded[MAX_PATH];

		V_strncpy( szPath, token, sizeof(szPath) );
		Sys_ReplaceString( szPath, "$os", g_szArrPlatforms[i], szPathExpanded, sizeof(szPathExpanded) );
		V_FixSlashes( szPathExpanded );
		V_RemoveDotSlashes( szPathExpanded );
		V_FixDoubleSlashes( szPathExpanded );

		// this fopen is probably using a relative path, but that's ok, as everything in
		// the crc code is opening relative paths and assuming the cwd is set ok.
		FILE *f = fopen( szPathExpanded, "rb" );
		if ( f )
		{
			fclose(f);
			// strcat - blech
			strcat( szReplacements, g_szArrPlatforms[i] ); // really just need to stick the existing platforms seen in
			strcat( szReplacements, ";" );
		}

	}
}

static const char * GetToken( const char *ln, char *token ) 
{
	*token = '\0';

	while ( *ln && isspace(*ln) )
		ln++;

	if (!ln[0])
		return NULL;

	if ( ln[0] == '"' )
	{ // does vpc allow \" inside the filename string - shouldn't matter, but we're going to assume no.
		ln++;
		while (*ln)
		{
			if ( ln[0] == '"' )
				break;
			*token++ = *ln++;
		}
		*token = '\0';
	}
	else if ( IsValidPathChar( *ln ) )
	{
		while (*ln)
		{
			if ( isspace(*ln) )
				break;
			*token++ = *ln++;
		}
		*token = '\0';
	}
	else
	{
		token[0] = ln[0];
		token[1] = '\0';
	}

	return ln;
}

static void PerformFileSubstitions( char * line, int linelen ) 
{
	static bool bFindFilePending = false;
	const char *ln = line;

	if ( !bFindFilePending )
	{
		ln = V_stristr( ln, "$file " );
		if ( ln )
			bFindFilePending = true;
	}

	if ( bFindFilePending )
	{
		char token[1024];
		ln = GetToken( ln, token );
		if ( !ln )
			return; // no more tokens on line, we should try the next line

		bFindFilePending = false;


		if ( V_stristr(token, "$os") )
		{
			if ( !IsValidPathChar( *token ) )
				fprintf( stderr, "Warning: can't expand %s for crc calculation.  Changes to this file set won't trigger automatic rebuild\n", token );
			char szReplacements[2048];
			char buffer[4096];
			BuildReplacements( token, szReplacements );
			Sys_ReplaceString( line, "$os", szReplacements, buffer, sizeof(buffer) );
			V_strncpy( line, buffer, linelen );
		}
	}

	static bool bFindFilePatternPending = false;
	ln = line;

	if ( !bFindFilePatternPending )
	{
		ln = V_stristr( ln, "$filepattern" );
		while ( ln )
		{
			ln += 13;
			if ( isspace( ln[-1] ) )
			{
				bFindFilePatternPending = true;
				break;
			}
		}
	}

	if ( bFindFilePatternPending )
	{
		char token[1024];
		ln = GetToken( ln, token );
		if ( !ln )
			return; // no more tokens on line, we should try the next line

		bFindFilePatternPending = false;

		char szReplacements[2048]; szReplacements[0] = '\0';
		char buffer[4096];
		CUtlVector< CUtlString > vecResults;
		Sys_ExpandFilePattern( token, vecResults );
		if ( vecResults.Count() )
		{
			for ( int i= 0; i < vecResults.Count(); i++ )
			{
				V_strncat( szReplacements, CFmtStr( "%s;", vecResults[i].String() ).Access(), V_ARRAYSIZE( szReplacements ) );
			}

			CRC32_t nCRC = CRC32_ProcessSingleBuffer( szReplacements, V_strlen( szReplacements ) );

			Sys_ReplaceString( line, token, CFmtStr( "%s:%u", token, nCRC ).Access(), buffer, sizeof(buffer) );
			V_strncpy( line, buffer, linelen );
		}
		else
		{
			if ( !IsValidPathChar( *token ) )
				fprintf( stderr, "Warning: %s couldn't be expanded during crc calculation.  Changes to this file set won't trigger automatic project rebuild\n", token );
		}
	}

}


//-----------------------------------------------------------------------------
//	Sys_Error
//
//-----------------------------------------------------------------------------
void Sys_Error( const char* format, ... )
{
	va_list argptr;
		
	va_start( argptr,format );
	vfprintf( stderr, format, argptr );
	va_end( argptr );

	exit( 1 );
}


void SafeSnprintf( char *pOut, int nOutLen, const char *pFormat, ... )
{
	va_list marker;
	va_start( marker, pFormat );
	V_vsnprintf( pOut, nOutLen, pFormat, marker );
	va_end( marker );

	pOut[nOutLen-1] = 0;
}


// for linked lists of strings
struct StringNode_t 
{
	StringNode_t *m_pNext;
	char m_Text[1];											// the string data
};


static StringNode_t *MakeStrNode( char const *pStr )
{
	size_t nLen = strlen( pStr );
	StringNode_t *nRet = ( StringNode_t * ) new unsigned char[sizeof( StringNode_t ) + nLen ];
	strcpy( nRet->m_Text, pStr );
	return nRet;
}

//-----------------------------------------------------------------------------
//	Sys_LoadTextFileWithIncludes
//-----------------------------------------------------------------------------
int Sys_LoadTextFileWithIncludes( const char* filename, char** bufferptr, bool bInsertFileMacroExpansion )
{
	FILE *pFileStack[MAX_INCLUDE_STACK_DEPTH];
	int nSP = MAX_INCLUDE_STACK_DEPTH;
	
	StringNode_t *pFileLines = NULL;		// tail ptr for fast adds
	
	size_t nTotalFileBytes = 0;
	FILE *handle = fopen( filename, "r" );
	if ( !handle )
		return -1;

	pFileStack[--nSP] = handle;								// push
	while ( nSP < MAX_INCLUDE_STACK_DEPTH )
	{
		// read lines
		for (;;)
		{
			char lineBuffer[4096];
			char *ln = fgets( lineBuffer, sizeof( lineBuffer ), pFileStack[nSP] );
			if ( !ln ) 
				break;										// out of text
			
			ln += strspn( ln, "\t " );						// skip white space

			// Need to insert actual files to make sure crc changes if disk-matched files match
			if ( bInsertFileMacroExpansion )
				PerformFileSubstitions( ln, sizeof(lineBuffer) - (ln-lineBuffer) );

			if ( memcmp( ln, "#include", 8 ) == 0 )
			{
				// omg, an include
				ln += 8;
				ln += strspn( ln, " \t\"<" );				// skip whitespace, ", and <
				
				size_t nPathNameLength = strcspn( ln, " \t\">\n" );
				if ( !nPathNameLength )
				{
					Sys_Error( "bad include %s via %s\n", lineBuffer, filename );
				}
				ln[nPathNameLength] = 0;					// kill everything after end of filename
				
				FILE *inchandle = fopen( ln, "r" );
				if ( !inchandle )
				{
					Sys_Error( "can't open #include of %s\n", ln );
				}
				if ( !nSP )
				{
					Sys_Error( "include nesting too deep via %s", filename );
				}
				pFileStack[--nSP] = inchandle;
			}
			else
			{
				size_t nLen = strlen( ln );
				nTotalFileBytes += nLen;
				StringNode_t *pNewLine = MakeStrNode( ln );

				pNewLine->m_pNext = pFileLines;
				pFileLines = pNewLine;
			}
		}
		fclose( pFileStack[nSP] );
		nSP++;												// pop stack
	}
	
	
	// Reverse the pFileLines list so it goes the right way.
	StringNode_t *pPrev = NULL;
	StringNode_t *pCur;
	for( pCur = pFileLines; pCur; )
	{
		StringNode_t *pNext = pCur->m_pNext;
		pCur->m_pNext = pPrev;
		pPrev = pCur;
		pCur = pNext;
	}
	pFileLines = pPrev;


	// Now dump all the lines out into a single buffer.
	char *buffer = new char[nTotalFileBytes + 1];			// and null
	*bufferptr = buffer;									// tell caller

	// copy all strings and null terminate
	int nLine = 0;
	StringNode_t *pNext;
	for( pCur=pFileLines; pCur; pCur=pNext )
	{
		pNext = pCur->m_pNext;
		size_t nLen = strlen( pCur->m_Text );
		memcpy( buffer, pCur->m_Text, nLen );
		buffer += nLen;
		nLine++;
		
		// Cleanup the line..
		//delete [] (unsigned char*)pCur;
	}
	*( buffer++ ) = 0;										// null

	return (int)nTotalFileBytes;
}


// Just like fgets() but it removes trailing newlines.
char* ChompLineFromFile( char *pOut, int nOutBytes, FILE *fp )
{
	char *pReturn = fgets( pOut, nOutBytes, fp );
	if ( pReturn )
	{
		int len = (int)strlen( pReturn );
		if ( len > 0 && pReturn[len-1] == '\n' )
		{
			pReturn[len-1] = 0;
			if ( len > 1 && pReturn[len-2] == '\r' )
				pReturn[len-2] = 0;
		}
	}

	return pReturn;
}


bool CheckSupplementalString( const char *pSupplementalString, const char *pReferenceSupplementalString )
{
	// The supplemental string is only checked while VPC is determining if a project file is stale or not. 
	// It's not used by the pre-build event's CRC check.
	// The supplemental string contains various options that tell how the project was built. It's generated in VPC_GenerateCRCOptionString.
	//
	// If there's no reference supplemental string (which is the case if we're running vpccrccheck.exe), then we ignore it and continue.
	if ( !pReferenceSupplementalString )
		return true;
	
	return ( pSupplementalString && pReferenceSupplementalString && stricmp( pSupplementalString, pReferenceSupplementalString ) == 0 );
}

bool CheckVPCExeCRC( char *pVPCCRCCheckString, const char *szFilename, char *pErrorString, int nErrorStringLength )
{
	if ( pVPCCRCCheckString == NULL )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "Unexpected end-of-file in %s", szFilename );
		return false;
	}

	char *pSpace = strchr( pVPCCRCCheckString, ' ' );
	if ( !pSpace )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "Invalid line ('%s') in %s", pVPCCRCCheckString, szFilename );
		return false;
	}

	// Null-terminate it so we have the CRC by itself and the filename follows the space.
	*pSpace = 0;
	const char *pVPCFilename = pSpace + 1;

	// Parse the CRC out.
	unsigned int nReferenceCRC;
	sscanf( pVPCCRCCheckString, "%x", &nReferenceCRC );

	char *pBuffer;
	int cbVPCExe = Sys_LoadFile( pVPCFilename, (void**)&pBuffer );
	if ( !pBuffer )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "Unable to load %s for comparison.", pVPCFilename );
		return false;
	}

	if ( cbVPCExe < 0 )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "Could not load file '%s' to check CRC", pVPCFilename );
		return false;
	}

	// Calculate the CRC from the contents of the file.
	CRC32_t nCRCFromFileContents = CRC32_ProcessSingleBuffer( pBuffer, cbVPCExe );
	delete [] pBuffer;

	// Compare them.
	if ( nCRCFromFileContents != nReferenceCRC )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "VPC executable has changed since the project was generated." );
		return false;
	}
	return true;
}


bool VPC_CheckProjectDependencyCRCs( const char *pProjectFilename, const char *pReferenceSupplementalString, char *pErrorString, int nErrorStringLength )
{
	// Build the xxxxx.vcproj.vpc_crc filename
	char szFilename[512];
	SafeSnprintf( szFilename, sizeof( szFilename ), "%s.%s", pProjectFilename, VPCCRCCHECK_FILE_EXTENSION );
	
	// Open it up.
	FILE *fp = fopen( szFilename, "rt" );
	if ( !fp )
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "Unable to load %s to check CRC strings", szFilename );
		return false;
	}

	bool bReturnValue = false;
	char lineBuffer[2048];

	// Check the version of the CRC file.
	const char *pVersionString = ChompLineFromFile( lineBuffer, sizeof( lineBuffer ), fp );
	if ( pVersionString && stricmp( pVersionString, VPCCRCCHECK_FILE_VERSION_STRING ) == 0 )
	{
		char *pVPCExeCRCString = ChompLineFromFile( lineBuffer, sizeof( lineBuffer ), fp );
		if ( CheckVPCExeCRC( pVPCExeCRCString, szFilename, pErrorString, nErrorStringLength ) )
		{
			// Check the supplemental CRC string.
			const char *pSupplementalString = ChompLineFromFile( lineBuffer, sizeof( lineBuffer ), fp );
			if ( CheckSupplementalString( pSupplementalString, pReferenceSupplementalString ) )
			{
				// Now read each line. Each line has a CRC and a filename on it.
				while ( 1 )
				{
					char *pLine = ChompLineFromFile( lineBuffer, sizeof( lineBuffer ), fp );
					if ( !pLine )
					{
						// We got all the way through the file without a CRC error, so all's well.
						bReturnValue = true;
						break;
					}

					char *pSpace = strchr( pLine, ' ' );
					if ( !pSpace )
					{
						SafeSnprintf( pErrorString, nErrorStringLength, "Invalid line ('%s') in %s", pLine, szFilename );
						break;
					}

					// Null-terminate it so we have the CRC by itself and the filename follows the space.
					*pSpace = 0;
					const char *pVPCFilename = pSpace + 1;

					// Parse the CRC out.
					unsigned int nReferenceCRC;
					sscanf( pLine, "%x", &nReferenceCRC );


					// Calculate the CRC from the contents of the file.
					char *pBuffer;
					int nTotalFileBytes = Sys_LoadTextFileWithIncludes( pVPCFilename, &pBuffer, true );
					if ( nTotalFileBytes == -1 )
					{
						SafeSnprintf( pErrorString, nErrorStringLength, "Unable to load %s for CRC comparison.", pVPCFilename );
						break;
					}

					CRC32_t nCRCFromTextContents = CRC32_ProcessSingleBuffer( pBuffer, nTotalFileBytes );
					delete [] pBuffer;

					// Compare them.
					if ( nCRCFromTextContents != nReferenceCRC )
					{
						SafeSnprintf( pErrorString, nErrorStringLength, "This VCPROJ is out of sync with its VPC scripts.\n  %s mismatches (0x%x vs 0x%x).\n  Please use VPC to re-generate!\n  \n", pVPCFilename, nReferenceCRC, nCRCFromTextContents );
						break;
					}
				}
			}
			else
			{
				SafeSnprintf( pErrorString, nErrorStringLength, "Supplemental string mismatch." );
			}
		}
	}
	else
	{
		SafeSnprintf( pErrorString, nErrorStringLength, "CRC file %s has an invalid version string ('%s')", szFilename, pVersionString ? pVersionString : "[null]" );
	}

	fclose( fp );
	return bReturnValue;
}


int VPC_OldeStyleCRCChecks( int argc, char **argv )
{
	for ( int i=1; (i+2) < argc; )
	{
		const char *pTestArg = argv[i];
		if ( stricmp( pTestArg, "-crc" ) != 0 )
		{
			++i;
			continue;
		}

		const char *pVPCFilename = argv[i+1];
		
		// Get the CRC value on the command line.
		const char *pTestCRC = argv[i+2];
		unsigned int nCRCFromCommandLine;
		sscanf( pTestCRC, "%x", &nCRCFromCommandLine );

		// Calculate the CRC from the contents of the file.
		char *pBuffer;
		int nTotalFileBytes = Sys_LoadTextFileWithIncludes( pVPCFilename, &pBuffer, true );
		if ( nTotalFileBytes == -1 )
		{
			Sys_Error( "Unable to load %s for CRC comparison.", pVPCFilename );
		}

		CRC32_t nCRCFromTextContents = CRC32_ProcessSingleBuffer( pBuffer, nTotalFileBytes );
		delete [] pBuffer;
		
		// Compare them.
		if ( nCRCFromTextContents != nCRCFromCommandLine )
		{
			Sys_Error( "  \n  This VCPROJ is out of sync with its VPC scripts.\n  %s mismatches (0x%x vs 0x%x).\n  Please use VPC to re-generate!\n  \n", pVPCFilename, nCRCFromCommandLine, nCRCFromTextContents );
		}

		i += 2;
	}

	return 0;
}


int VPC_CommandLineCRCChecks( int argc, char **argv )
{
	if ( argc < 2 )
	{
		fprintf( stderr, "Invalid arguments to " VPCCRCCHECK_EXE_FILENAME ". Format: " VPCCRCCHECK_EXE_FILENAME " [project filename]\n" );
		return 1;
	}

	const char *pFirstCRC = argv[1];

	// If the first argument starts with -crc but is not -crc2, then this is an old CRC check command line with all the CRCs and filenames
	// directly on the command line. The new format puts all that in a separate file.
	if ( pFirstCRC[0] == '-' && pFirstCRC[1] == 'c' && pFirstCRC[2] == 'r' && pFirstCRC[3] == 'c' && pFirstCRC[4] != '2' )
	{
		return VPC_OldeStyleCRCChecks( argc, argv );
	}

	if ( stricmp( pFirstCRC, "-crc2" ) != 0 )
	{
		fprintf( stderr, "Missing -crc2 parameter on vpc CRC check command line." );
		return 1;
	}

	const char *pProjectFilename = argv[2];

	char errorString[1024];
	bool bCRCsValid = VPC_CheckProjectDependencyCRCs( pProjectFilename, NULL, errorString, sizeof( errorString ) );

	if ( bCRCsValid )
	{
		return 0;
	}
	else
	{
		fprintf( stderr, "%s", errorString );
		return 1;
	}
}