//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
// 
//

#include "stdafx.h"
#include <stdio.h>
#include <process.h>
#include <string.h>
#include <windows.h>
#include <sys/stat.h>
#include <time.h>
#include "interface.h"
#include "imysqlwrapper.h"
#include "tier1/utlvector.h"
#include "tier1/utlbuffer.h"
#include "tier1/utlsymbol.h"
#include "tier1/utlstring.h"
#include "tier1/utldict.h"
#include "tier2/tier2.h"
#include "filesystem.h"

#include "cbase.h"
#include "gamestats.h"
class CBaseObject;
#include "tf/tf_gamestats.h"
#include "base_gamestats_parse.h"
#include "tier0/icommandline.h"

#include <string>

static TFReportedStats_t										g_reportedStats;

extern void v_escape_string (std::string& s);

//-----------------------------------------------------------------------------
// Weapons.
//-----------------------------------------------------------------------------
static const char *s_aWeaponNames[] =
{
	"NONE",
	"BAT",
	"BOTTLE", 
	"FIREAXE",
	"CLUB",
	"CROWBAR",
	"KNIFE",
	"MEDIKIT",
	"PIPE",
	"SHOVEL",
	"WRENCH",
	"BONESAW",
	"SHOTGUN_PRIMARY",
	"SHOTGUN_SECONDARY",
	"SNIPERRIFLE",
	"MINIGUN",
	"SMG",
	"SYRINGEGUN_MEDIC",
	"TRANQ",
	"ROCKETLAUNCHER",
	"GRENADELAUNCHER",
	"PIPEBOMBLAUNCHER",
	"FLAMETHROWER",
	"GRENADE_NORMAL",
	"GRENADE_CONCUSSION",
	"GRENADE_NAIL",
	"GRENADE_MIRV",
	"GRENADE_MIRV_DEMOMAN",
	"GRENADE_NAPALM",
	"GRENADE_GAS",
	"GRENADE_EMP",
	"GRENADE_CALTROP",
	"GRENADE_PIPEBOMB",
	"GRENADE_SMOKE_BOMB",
	"GRENADE_HEAL",
	"PISTOL",
	"REVOLVER",
	"NAILGUN",
	"PDA",
	"PDA_DEMOMAN",
	"PDA_ENGINEER",
	"PDA_SPY",
	"BUILDER",
	"MEDIGUN",
	"FLAG",
	"GRENADE_MIRVBOMB",
	"FLAMETHROWER_ROCKET",
	"GRENADE_DEMOMAN",
	"SENTRY_BULLET",
	"SENTRY_ROCKET",
	"DISPENSER",
	"INVIS",
};

//-----------------------------------------------------------------------------
// Classes
//-----------------------------------------------------------------------------
static const char *s_aClassNames[] =
{
	"UNDEFINED",
	"SCOUT",
	"SNIPER",
	"SOLDIER",
	"DEMOMAN",
	"MEDIC",
	"HEAVYWEAPONS",
	"PYRO",
	"SPY",
	"ENGINEER",
	"CIVILIAN",
};

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
static bool LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer )
{
	g_reportedStats.Clear();
	return g_reportedStats.LoadCustomDataFromBuffer( LoadBuffer );
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
static const char *ClassIdToAlias( int iClass )
{
	if ( ( iClass >= ARRAYSIZE( s_aClassNames ) ) || ( iClass < 0 ) )
		return NULL;

	return s_aClassNames[iClass];
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
const char *WeaponIdToAlias( int iWeapon )
{
	if ( ( iWeapon >= ARRAYSIZE( s_aWeaponNames ) ) || ( iWeapon < 0 ) )
		return NULL;

	return s_aWeaponNames[iWeapon];
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void DescribeTF2Stats()
{
#if 0	// not up to date w/latest stats code. 
	int iMap;
	for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) )
	{
		// Get the current map.
		TF_Gamestats_LevelStats_t *pCurrentMap = &g_dictMapStats[iMap];

		Msg( " --- %s ------\n   %d deaths\n   %.2f seconds total playtime\n", pCurrentMap->m_Header.m_szMapName, 
			pCurrentMap->m_aPlayerDeaths.Count(), pCurrentMap->m_Header.m_flTime );
		for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ )
		{
			Msg( "   %s killed %s with %s at (%d,%d,%d), distance %d\n",
				ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ),
				ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ),
				WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ), 
				pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
				pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
				pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ],
				pCurrentMap->m_aPlayerDeaths[ i ].iDistance );
		}
		Msg( "\n---------------------------------\n\n   %d damage records\n", pCurrentMap->m_aPlayerDamage.Count() );

		for( int i = 0; i < pCurrentMap->m_aPlayerDamage.Count(); i++ )
		{
			Msg( "   %.2f : %s at (%d,%d,%d) caused %d damage to %s with %s at (%d,%d,%d)%s%s\n",
				pCurrentMap->m_aPlayerDamage[ i ].fTime,
				ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iAttackClass ),
				pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 0 ],
				pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 1 ],
				pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 2 ],
				pCurrentMap->m_aPlayerDamage[ i ].iDamage,
				ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iTargetClass ),
				WeaponIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iWeapon ), 
				pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 0 ],
				pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 1 ],
				pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 2 ],
				pCurrentMap->m_aPlayerDamage[ i ].iCrit ? ", CRIT!" : "",
				pCurrentMap->m_aPlayerDamage[ i ].iKill ? ", KILL" : "" );
		}

		Msg( "\n" );
	}
#endif
}
extern CUtlDict< int, unsigned short > g_mapOrder;
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void InsertTF2Data( bool bDeathOnly, char const *szStatsFileUserID, time_t fileTime, IMySQL *sql, int iStatsFileVersion )
{
	if ( !sql )
		return;

	std::string userid;
	userid = szStatsFileUserID;
	v_escape_string( userid );

	
	char szDate[128]="Now()";
	if ( fileTime > 0 )
	{
		tm * pTm = localtime( &fileTime );
		Q_snprintf( szDate, ARRAYSIZE( szDate ), "'%04d-%02d-%02d %02d:%02d:%02d'",
			pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec );
	}

	char q[4096];

	
	{
		int iMap;
		for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex(); iMap = g_reportedStats.m_dictMapStats.Next( iMap ) )
		{
			// Get the current map.
			TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap];

#if 1
			int slot = g_mapOrder.Find( pCurrentMap->m_Header.m_szMapName );
			if ( slot == g_mapOrder.InvalidIndex() )
			{
				if ( Q_stricmp( pCurrentMap->m_Header.m_szMapName, "devtest" ) )
					continue;
			}
#endif

			std::string mapname;
			mapname = pCurrentMap->m_Header.m_szMapName;
			v_escape_string( mapname );

			std::string tag;
			tag = ""; // pCurrentMap->m_Tag.m_szTagText;
			v_escape_string( tag );

			int mapversion = 0;

			

			/*
			for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ )
			{
				Msg( "   %s killed %s with %s at (%d,%d,%d), distance %d\n",
					ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ),
					ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ),
					WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ), 
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ],
					pCurrentMap->m_aPlayerDeaths[ i ].iDistance );
			}
			*/
			// Deal with deaths
			for ( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); ++i )
			{
				Q_snprintf( q, sizeof( q ), "REPLACE into %s_deaths (UserID,Tag,LastUpdate,MapName,MapVersion,DeathIndex,X,Y,Z) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,%d,%d,%d,%d);",
					"tf",
					userid.c_str(),
					tag.c_str(),
					mapname.c_str(),
					mapversion,
					i,
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
					pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ] );

				int retcode = sql->Execute( q );
				if ( retcode != 0 )
				{
					printf( "Query %s failed\n", q );
					return;
				}
			}
		}
		if ( bDeathOnly )
			return;
	}

	int iMap;
	for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex(); 
		iMap = g_reportedStats.m_dictMapStats.Next( iMap ) )
	{
		// Get the current map.
		TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap];
		std::string mapname = pCurrentMap->m_Header.m_szMapName;
		v_escape_string( mapname );

		// insert map data into database
		Q_snprintf( q, sizeof( q ), "INSERT into tf_mapdata (ServerID,TimeSubmitted,MapName,RoundsPlayed,TotalTime,BlueWins,RedWins,Stalemates,BlueSuddenDeathWins,RedSuddenDeathWins) "
			"values (\"%s\",%s,\"%s\",%d,%d,%d,%d,%d,%d,%d);",
			szStatsFileUserID,szDate, mapname.c_str(), pCurrentMap->m_Header.m_iRoundsPlayed, pCurrentMap->m_Header.m_iTotalTime, pCurrentMap->m_Header.m_iBlueWins,
			pCurrentMap->m_Header.m_iRedWins, pCurrentMap->m_Header.m_iStalemates, pCurrentMap->m_Header.m_iBlueSuddenDeathWins, pCurrentMap->m_Header.m_iRedSuddenDeathWins );

//		Msg( "%s\n", q );

		int retcode = sql->Execute( q );
		if ( retcode != 0 )
		{
			Msg( "Query %s failed\n", q );
			return;
		}

		// insert the class data
		for ( int i = TF_CLASS_UNDEFINED + 1; i < TF_CLASS_CIVILIAN; i++ )
		{
			TF_Gamestats_ClassStats_t &classStats = pCurrentMap->m_aClassStats[i];
			if  ( 0 == classStats.iSpawns )	// skip any classes that have had no spawns
				continue;

			Q_snprintf( q, sizeof( q ), "INSERT into tf_classdata (ServerID,MapName,TimeSubmitted,Class,Spawns,TotalTime,Score,Kills,Deaths,Assists,Captures) "
				"values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%d,%d);",
				szStatsFileUserID, mapname.c_str(), szDate, i, classStats.iSpawns, classStats.iTotalTime, classStats.iScore, classStats.iKills, classStats.iDeaths,
				classStats.iAssists, classStats.iCaptures );

//			Msg( "%s\n", q );
			int retcode = sql->Execute( q );
			if ( retcode != 0 )
			{
				Msg( "Query %s failed\n", q );
				return;
			}
		}

		// insert the weapon data
		for ( int i = TF_WEAPON_NONE+1; i < TF_WEAPON_COUNT; i++ )
		{
			TF_Gamestats_WeaponStats_t &weaponStats = pCurrentMap->m_aWeaponStats[i];

			// skip any weapons that have no shots fired
			if  ( 0 == weaponStats.iShotsFired )	
				continue;

			Q_snprintf( q, sizeof( q ), "INSERT into tf_weapondata (ServerID, MapName, TimeSubmitted,WeaponID,ShotsFired,ShotsFiredCrit,ShotsHit,DamageTotal,"
				"HitsWithKnownDistance,DistanceTotal) "
				"values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%llu)",
				szStatsFileUserID, mapname.c_str(), szDate, i, weaponStats.iShotsFired, weaponStats.iCritShotsFired, weaponStats.iHits, weaponStats.iTotalDamage,
				weaponStats.iHitsWithKnownDistance, weaponStats.iTotalDistance );

//			Msg( "%s\n", q );
			int retcode = sql->Execute( q );
			if ( retcode != 0 )
			{
				Msg( "Query %s failed\n", q );
				return;
			}
		}
	}
}

bool g_bDeathOnly = false;

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int TF_ParseCustomGameStatsData( ParseContext_t *ctx )
{
	FILE *fp = fopen( ctx->file, "rb" );
	if ( !fp )
		return CUSTOMDATA_FAILED;

	CUtlBuffer statsBuffer;

	struct _stat sb;
	_stat( ctx->file, &sb );
	statsBuffer.Clear();
	statsBuffer.EnsureCapacity( sb.st_size );
	fread( statsBuffer.Base(), sb.st_size, 1, fp );
	statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size );
	fclose( fp );

	if ( memcmp( statsBuffer.Base(), "\"gamestats\"", 11 ) == 0 )
	{
		Msg( "Got new-style file format that we don't support, skipping\n" );
		return CUSTOMDATA_SUCCESS;
	}

	if ( memcmp( statsBuffer.Base(), "PERFDATA:", 9 ) == 0 )
	{
		ProcessPerfData( ctx->mysql, sb.st_mtime, "tf_perfdata", ( ( char * ) statsBuffer.Base() ) + 9 );
		return CUSTOMDATA_SUCCESS;
	}
	char shortname[ 128 ];
	Q_FileBase( ctx->file, shortname, sizeof( shortname ) );

	char szCurrentStatsFileUserID[17];
	int iCurrentStatsFileVersion;

	iCurrentStatsFileVersion = statsBuffer.GetShort();
	statsBuffer.Get( szCurrentStatsFileUserID, 16 );
	szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0;

	unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt();
	if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED )
	{
		// we don't care about the standard data, so why is it here?
		return CUSTOMDATA_FAILED;
	}

	// check for custom data
	bool bHasCustomData = (statsBuffer.TellPut() != statsBuffer.TellGet());
	if( !bHasCustomData )
	{
		// where's our data?
		return CUSTOMDATA_FAILED;
	}

	if ( !LoadCustomDataFromBuffer( statsBuffer ) )
		return CUSTOMDATA_FAILED;

	if( ctx->describeonly )
	{
		DescribeTF2Stats();
	}
	else
	{
		if ( CommandLine()->CheckParm( "-deathsonly" ) )
		{
			g_bDeathOnly = true;
		}

		InsertTF2Data( g_bDeathOnly, szCurrentStatsFileUserID, sb.st_mtime, ctx->mysql, iCurrentStatsFileVersion );
	}

	return CUSTOMDATA_SUCCESS;
}

//-----------------------------------------------------------------------------
// Purpose: Called after all new files have been parsed & imported
//-----------------------------------------------------------------------------
void TF_PostImport( IMySQL *sql )
{
#if 0	// now handled by PHP script
	if ( g_bDeathOnly )
		return;
	// All the new data files have been imported to SQL.  Now, do a rollup of the raw data into the rollup tables.

	// delete existing rollup for class data
	int retcode = sql->Execute( "delete from tf_classdata_rollup" );
	if ( 0 != retcode )
	{
		Msg( "Failed to delete from tf_classdata_rollup\n" );
		return;
	}
	// create new rollup for class data
	retcode = sql->Execute( "insert into tf_classdata_rollup (class,spawns,totaltime,score,kills,deaths,assists,captures) "
		"select class,sum(spawns),sum(totaltime),sum(score),sum(kills),sum(deaths),sum(assists),sum(captures) from tf_classdata group by class;" );
	if ( 0 != retcode )
	{
		Msg( "Failed to create class data rollup\n" );
		return;
	}

	// delete existing rollup for map data
	retcode = sql->Execute( "delete from tf_mapdata_rollup" );
	if ( 0 != retcode )
	{
		Msg( "Failed to delete from tf_mapdata_rollup\n" );
		return;
	}
	// create new rollup for map data
	retcode = sql->Execute( "insert into tf_mapdata_rollup (mapname,roundsplayed,totaltime,bluewins,redwins,stalemates) "
		"select mapname,sum(roundsplayed),sum(totaltime),sum(bluewins),sum(redwins),sum(stalemates) from tf_mapdata group by mapname;" );
	if ( 0 != retcode )
	{
		Msg( "Failed to create map data rollup\n" );
		return;
	}
#endif
}