//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Runs the state machine for the host & server
//
// $NoKeywords: $
//=============================================================================//


#include "host_state.h"
#include "eiface.h"
#include "quakedef.h"
#include "server.h"
#include "sv_main.h"
#include "host_cmd.h"
#include "host.h"
#include "screen.h"
#include "icliententity.h"
#include "icliententitylist.h"
#include "client.h"
#include "host_jmp.h"
#include "tier0/vprof.h"
#include "tier0/icommandline.h"
#include "filesystem_engine.h"
#include "zone.h"
#include "iengine.h"
#include "snd_audio_source.h"
#include "sv_steamauth.h"
#ifndef SWDS
#include "vgui_baseui_interface.h"
#endif
#include "sv_plugin.h"
#include "cl_main.h"
#include "sv_steamauth.h"
#include "datacache/imdlcache.h"
#include "sys_dll.h"
#include "testscriptmgr.h"
#if defined( REPLAY_ENABLED )
#include "replay_internal.h"
#include "replayserver.h"
#endif
#include "GameEventManager.h"
#include "tier0/etwprof.h"

#include "ccs.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

extern bool		g_bAbortServerSet;
#ifndef SWDS
extern ConVar	reload_materials;
#endif

typedef enum 
{
	HS_NEW_GAME = 0,
	HS_LOAD_GAME,
	HS_CHANGE_LEVEL_SP,
	HS_CHANGE_LEVEL_MP,
	HS_RUN,
	HS_GAME_SHUTDOWN,
	HS_SHUTDOWN,
	HS_RESTART,
} HOSTSTATES;

// a little class that manages the state machine for the host
class CHostState
{
public:
				CHostState() = default;
	void		Init();
	void		FrameUpdate( float time );
	void		SetNextState( HOSTSTATES nextState );

	void		RunGameInit();

	void		SetState( HOSTSTATES newState, bool clearNext );
	void		GameShutdown();

	void		State_NewGame();
	void		State_LoadGame();
	void		State_ChangeLevelMP();
	void		State_ChangeLevelSP();
	void		State_Run( float time );
	void		State_GameShutdown();
	void		State_Shutdown();
	void		State_Restart();

	bool		IsGameShuttingDown();

	void		RememberLocation();
	void		OnClientConnected(); // Client fully connected
	void		OnClientDisconnected(); // Client disconnected

	HOSTSTATES	m_currentState;
	HOSTSTATES	m_nextState;
	Vector		m_vecLocation;
	QAngle		m_angLocation;
	char		m_levelName[256];
	char		m_landmarkName[256];
	char		m_saveName[256];
	float		m_flShortFrameTime;		// run a few one-tick frames to avoid large timesteps while loading assets

	bool		m_activeGame;
	bool		m_bRememberLocation;
	bool		m_bBackgroundLevel;
	bool		m_bWaitingForConnection;
};

static bool Host_ValidGame( void );
static CHostState	g_HostState;


//-----------------------------------------------------------------------------
// external API for manipulating the host state machine
//-----------------------------------------------------------------------------
void HostState_Init()
{
	g_HostState.Init();
}

void HostState_Frame( float time )
{
	g_HostState.FrameUpdate( time );
}

void HostState_RunGameInit()
{
	g_HostState.RunGameInit();
	g_ServerGlobalVariables.bMapLoadFailed = false;
}

//-----------------------------------------------------------------------------
// start a new game as soon as possible
//-----------------------------------------------------------------------------
void HostState_NewGame( char const *pMapName, bool remember_location, bool background )
{
	Q_strncpy( g_HostState.m_levelName, pMapName, sizeof( g_HostState.m_levelName ) );

	g_HostState.m_landmarkName[0] = 0;
	g_HostState.m_bRememberLocation = remember_location;
	g_HostState.m_bWaitingForConnection = true;
	g_HostState.m_bBackgroundLevel = background;
	if ( remember_location )
	{
		g_HostState.RememberLocation();
	}
	g_HostState.SetNextState( HS_NEW_GAME );
}

//-----------------------------------------------------------------------------
// load a new game as soon as possible
//-----------------------------------------------------------------------------
void HostState_LoadGame( char const *pSaveFileName, bool remember_location )
{
#ifndef SWDS
	// Make sure the freaking save file exists....
	if ( !saverestore->SaveFileExists( pSaveFileName ) )
	{
			Warning("Save file %s can't be found!\n", pSaveFileName );
			SCR_EndLoadingPlaque();
			return;
	}

	Q_strncpy( g_HostState.m_saveName, pSaveFileName, sizeof( g_HostState.m_saveName )  );

	// Tell the game .dll we are loading another game
	serverGameDLL->PreSaveGameLoaded( pSaveFileName, sv.IsActive() );

	g_HostState.m_bRememberLocation = remember_location;
	g_HostState.m_bBackgroundLevel = false;
	g_HostState.m_bWaitingForConnection = true;
	if ( remember_location )
	{
		g_HostState.RememberLocation();
	}

	g_HostState.SetNextState( HS_LOAD_GAME );
#endif
}

// change level (single player style - smooth transition)
void HostState_ChangeLevelSP( char const *pNewLevel, char const *pLandmarkName )
{
	Q_strncpy( g_HostState.m_levelName, pNewLevel, sizeof( g_HostState.m_levelName ) );
	Q_strncpy( g_HostState.m_landmarkName, pLandmarkName, sizeof( g_HostState.m_landmarkName ) );
	g_HostState.SetNextState( HS_CHANGE_LEVEL_SP );
}

// change level (multiplayer style - respawn all connected clients)
void HostState_ChangeLevelMP( char const *pNewLevel, char const *pLandmarkName )
{
	Steam3Server().NotifyOfLevelChange();

	Q_strncpy( g_HostState.m_levelName, pNewLevel, sizeof( g_HostState.m_levelName ) );
	Q_strncpy( g_HostState.m_landmarkName, pLandmarkName, sizeof( g_HostState.m_landmarkName ) );
	g_HostState.SetNextState( HS_CHANGE_LEVEL_MP );
}

// shutdown the game as soon as possible
void HostState_GameShutdown()
{
	Steam3Server().NotifyOfLevelChange();

	// This will get called during shutdown, ignore it.
	if ( g_HostState.m_currentState != HS_SHUTDOWN &&
		 g_HostState.m_currentState != HS_RESTART &&
		 g_HostState.m_currentState != HS_GAME_SHUTDOWN
		 )
	{
		g_HostState.SetNextState( HS_GAME_SHUTDOWN );
	}
}

// shutdown the engine/program as soon as possible
void HostState_Shutdown()
{
#if defined( REPLAY_ENABLED )
	// If we're recording the game, finalize the replay so players can download it.
	if ( sv.IsDedicated() && g_pReplay && g_pReplay->IsRecording() )
	{
		// Stop recording and publish all blocks/session info data synchronously.
		g_pReplay->SV_EndRecordingSession( true );
	}
#endif

	g_HostState.SetNextState( HS_SHUTDOWN );
}

//-----------------------------------------------------------------------------
// Purpose: Restart the engine
//-----------------------------------------------------------------------------
void HostState_Restart()
{
	g_HostState.SetNextState( HS_RESTART );
}

bool HostState_IsGameShuttingDown()
{
	return g_HostState.IsGameShuttingDown();
}

bool HostState_IsShuttingDown()
{
	return ( g_HostState.m_currentState == HS_SHUTDOWN ||
		g_HostState.m_currentState == HS_RESTART ||
			g_HostState.m_currentState == HS_GAME_SHUTDOWN );
}


void HostState_OnClientConnected()
{
	g_HostState.OnClientConnected();
}

void HostState_OnClientDisconnected()
{
	g_HostState.OnClientDisconnected();
}

void HostState_SetSpawnPoint(Vector &position, QAngle &angle)
{
	g_HostState.m_angLocation = angle;
	g_HostState.m_vecLocation = position;
	g_HostState.m_bRememberLocation = true;
}

//-----------------------------------------------------------------------------
static void WatchDogHandler()
{
	Host_Error( "WatchdogHandler called - server exiting.\n" );
}

//-----------------------------------------------------------------------------
// Class implementation
//-----------------------------------------------------------------------------

void CHostState::Init()
{
	SetState( HS_RUN, true );
	m_currentState = HS_RUN;
	m_nextState = HS_RUN;
	m_activeGame = false;
	m_levelName[0] = 0;
	m_saveName[0] = 0;
	m_landmarkName[0] = 0;
	m_bRememberLocation = 0;
	m_bBackgroundLevel = false;
	m_vecLocation.Init();
	m_angLocation.Init();
	m_bWaitingForConnection = false;
	m_flShortFrameTime = 1.0;

	CCS_Init();

	Plat_SetWatchdogHandlerFunction( WatchDogHandler );
}

void CHostState::SetState( HOSTSTATES newState, bool clearNext )
{
	m_currentState = newState;
	if ( clearNext )
	{
		m_nextState = newState;
	}
}

void CHostState::SetNextState( HOSTSTATES next )
{
	Assert( m_currentState == HS_RUN );
	m_nextState = next;
}

void CHostState::RunGameInit()
{
	Assert( !m_activeGame );

	if ( serverGameDLL )
	{
		serverGameDLL->GameInit();
	}
	m_activeGame = true;
}

void CHostState::GameShutdown()
{
	if ( m_activeGame )
	{
		serverGameDLL->GameShutdown();
		m_activeGame = false;
	}
}


// These State_ functions execute that state's code right away
// The external API queues up state changes to happen when the state machine is processed.
void CHostState::State_NewGame()
{
	CETWScope timer( "CHostState::State_NewGame" );

	if ( Host_ValidGame() )
	{
		// Demand load game .dll if running with -nogamedll flag, etc.
		if ( !serverGameClients )
		{
			SV_InitGameDLL();
		}

		if ( !serverGameClients )
		{
			Warning( "Can't start game, no valid server.dll loaded\n" );
		}
		else
		{
			if ( Host_NewGame( m_levelName, false, m_bBackgroundLevel ) )
			{
				// succesfully started the new game
				SetState( HS_RUN, true );
				return;
			}
		}
	}

	SCR_EndLoadingPlaque();

	// new game failed
	GameShutdown();
	// run the server at the console
	SetState( HS_RUN, true );
}

void CHostState::State_LoadGame()
{
#ifndef SWDS
	HostState_RunGameInit();
	
	if ( saverestore->LoadGame( m_saveName ) )
	{
		// succesfully started the new game
        GetTestScriptMgr()->CheckPoint( "load_game" );
		SetState( HS_RUN, true );
		return;
	}
#endif

	SCR_EndLoadingPlaque();

	// load game failed
	GameShutdown();
	// run the server at the console
	SetState( HS_RUN, true );
	
	if ( IsX360() )
	{
		// On the 360 we need to return to the background map
		g_ServerGlobalVariables.bMapLoadFailed = true;
		Cbuf_Clear();
		Cbuf_AddText( "startupmenu force" );
		Cbuf_Execute();
	}
}


void CHostState::State_ChangeLevelMP()
{
	if ( Host_ValidGame() )
	{
		Steam3Server().NotifyOfLevelChange();

#ifndef SWDS
		// start progress bar immediately for multiplayer level transitions
		EngineVGui()->EnabledProgressBarForNextLoad();
#endif
		if ( Host_Changelevel( false, m_levelName, m_landmarkName ) )
		{
			SetState( HS_RUN, true );
			return;
		}
	}
	// fail
	ConMsg( "Unable to change level!\n" );
	SetState( HS_RUN, true );

	IGameEvent *event = g_GameEventManager.CreateEvent( "server_changelevel_failed" );
	if ( event )
	{
		event->SetString( "levelname", m_levelName );
		g_GameEventManager.FireEvent( event );
	}
}


void CHostState::State_ChangeLevelSP()
{
	if ( Host_ValidGame() )
	{
		Host_Changelevel( true, m_levelName, m_landmarkName );
		SetState( HS_RUN, true );
		return;
	}
	// fail
	ConMsg( "Unable to change level!\n" );
	SetState( HS_RUN, true );
}

static bool IsClientActive()
{
	if ( !sv.IsActive() )
		return cl.IsActive();

	for ( int i = 0; i < sv.GetClientCount(); i++ )
	{
		CGameClient *pClient = sv.Client( i );
		if ( pClient->IsActive() )
			return true;
	}
	return false;
}

static bool IsClientConnected()
{
	if ( !sv.IsActive() )
		return cl.IsConnected();

	for ( int i = 0; i < sv.GetClientCount(); i++ )
	{
		CGameClient *pClient = sv.Client( i );
		if ( pClient->IsConnected() )
			return true;
	}
	return false;
}

void CHostState::State_Run( float frameTime )
{
	static bool s_bFirstRunFrame = true;

	if ( m_flShortFrameTime > 0 )
	{
		if ( IsClientActive() )
		{
			m_flShortFrameTime = (m_flShortFrameTime > frameTime) ? (m_flShortFrameTime-frameTime) : 0;
		}
		// Only clamp time if client is in process of connecting or is already connected.
		if ( IsClientConnected() )
		{
			frameTime = min( frameTime, host_state.interval_per_tick );
		}
	}

	// Nice big timeout to play it safe while still ensuring that we don't get stuck in
	// infinite loops.
	int nTimerWaitSeconds = 60;
	if ( s_bFirstRunFrame ) 
	{
		// The first frame can take a while especially during fork startup.
		s_bFirstRunFrame = false;
		nTimerWaitSeconds *= 2;
	}
	if ( sv.IsDedicated() )
	{
		Plat_BeginWatchdogTimer( nTimerWaitSeconds );
	}

	Host_RunFrame( frameTime );

	if ( sv.IsDedicated() )
	{
		Plat_EndWatchdogTimer();
	}

	switch( m_nextState )
	{
	case HS_RUN:
		break;

	case HS_LOAD_GAME:
	case HS_NEW_GAME:
#if !defined( SWDS )
		SCR_BeginLoadingPlaque();
#endif
		// FALL THROUGH INTENTIONALLY TO SHUTDOWN

	case HS_SHUTDOWN:
	case HS_RESTART:
		// NOTE: The game must be shutdown before a new game can start, 
		// before a game can load, and before the system can be shutdown.
		// This is done here instead of pathfinding through a state transition graph.
		// That would be equivalent as the only way to get from HS_RUN to HS_LOAD_GAME is through HS_GAME_SHUTDOWN.
	case HS_GAME_SHUTDOWN:
		SetState( HS_GAME_SHUTDOWN, false );
		break;

	case HS_CHANGE_LEVEL_MP:
	case HS_CHANGE_LEVEL_SP:
		SetState( m_nextState, true );
		break;

	default:
		SetState( HS_RUN, true );
		break;
	}
}

void CHostState::State_GameShutdown()
{
	if ( serverGameDLL )
	{
		Steam3Server().NotifyOfLevelChange();
		g_pServerPluginHandler->LevelShutdown();
#if !defined(SWDS)
		audiosourcecache->LevelShutdown();
#endif
	}

	GameShutdown();
#ifndef SWDS
	saverestore->ClearSaveDir();
#endif
	Host_ShutdownServer();

	switch( m_nextState )
	{
	case HS_LOAD_GAME:
	case HS_NEW_GAME:
	case HS_SHUTDOWN:
	case HS_RESTART:
		SetState( m_nextState, true );
		break;
	default:
		SetState( HS_RUN, true );
		break;
	}
}


// Tell the launcher we're done.
void CHostState::State_Shutdown()
{
	CCS_Shutdown();

#if !defined(SWDS)
	CL_EndMovie();
#endif

	eng->SetNextState( IEngine::DLL_CLOSE );
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CHostState::State_Restart( void )
{
	// Just like a regular shutdown
	State_Shutdown();

	// But signal launcher/front end to restart engine
	eng->SetNextState( IEngine::DLL_RESTART );
}


//-----------------------------------------------------------------------------
// this is the state machine's main processing loop
//-----------------------------------------------------------------------------
void CHostState::FrameUpdate( float time )
{
	CCS_Tick( time );

#if _DEBUG
	int loopCount = 0;
#endif

	if ( setjmp (host_abortserver) )
	{
		Init();
		return;
	}

	g_bAbortServerSet = true;

	while ( true )
	{
		int oldState = m_currentState;

		// execute the current state (and transition to the next state if not in HS_RUN)
		switch( m_currentState )
		{
		case HS_NEW_GAME:
			g_pMDLCache->BeginMapLoad();
			State_NewGame();
			break;
		case HS_LOAD_GAME:
			g_pMDLCache->BeginMapLoad();
			State_LoadGame();
			break;
		case HS_CHANGE_LEVEL_MP:
			g_pMDLCache->BeginMapLoad();
			m_flShortFrameTime = 0.5f;
			State_ChangeLevelMP();
			break;
		case HS_CHANGE_LEVEL_SP:
			g_pMDLCache->BeginMapLoad();
			m_flShortFrameTime = 1.5f; // 1.5s of slower frames
			State_ChangeLevelSP();
			break;
		case HS_RUN:
			State_Run( time );
			break;
		case HS_GAME_SHUTDOWN:
			State_GameShutdown();
			break;
		case HS_SHUTDOWN:
			State_Shutdown();
			break;
		case HS_RESTART:
			g_pMDLCache->BeginMapLoad();
			State_Restart();
			break;
		}

		// only do a single pass at HS_RUN per frame.  All other states loop until they reach HS_RUN 
		if ( oldState == HS_RUN )
			break;

		// shutting down
		if ( oldState == HS_SHUTDOWN ||
			 oldState == HS_RESTART )
			break;

		// Only HS_RUN is allowed to persist across loops!!!
		// Also, detect circular state graphs (more than 8 cycling changes is probably a loop)
		// NOTE: In the current graph, there are at most 2.
#if _DEBUG
		if ( (m_currentState == oldState) || (++loopCount > 8) )
		{
			Host_Error( "state crash!\n" );
		}
#endif
	}
}


bool CHostState::IsGameShuttingDown( void )
{
	return ( ( m_currentState == HS_GAME_SHUTDOWN ) || ( m_nextState == HS_GAME_SHUTDOWN ) );
}

void CHostState::RememberLocation()
{
#ifndef SWDS
	Assert( m_bRememberLocation );

	m_vecLocation = MainViewOrigin();
	VectorAngles( MainViewForward(), m_angLocation );

	IClientEntity *localPlayer = entitylist ? entitylist->GetClientEntity( cl.m_nPlayerSlot + 1 ) : NULL;
	if ( localPlayer )
	{
		m_vecLocation = localPlayer->GetAbsOrigin();
	}

	m_vecLocation.z -= 64.0f; // subtract out a bit of Z to position the player lower
#endif
}

void CHostState::OnClientConnected()
{
	if ( !m_bWaitingForConnection )
		return;
	m_bWaitingForConnection = false;

	if ( m_bRememberLocation )
	{
		m_bRememberLocation = false;
		Cbuf_AddText( va( "setpos_exact %f %f %f\n", m_vecLocation.x, m_vecLocation.y, m_vecLocation.z ) ); 
		Cbuf_AddText( va( "setang_exact %f %f %f\n", m_angLocation.x, m_angLocation.y, m_angLocation.z ) );
	}

#if !defined( SWDS )
	if ( reload_materials.GetBool() )
	{
		// building cubemaps requires the materials to reload after map
		reload_materials.SetValue( 0 );
		Cbuf_AddText( "mat_reloadallmaterials\n" );
	}

	// Spew global texture memory usage if asked to
	if( CommandLine()->CheckParm( "-dumpvidmemstats" ) )
	{
		FileHandle_t fp;
		fp = g_pFileSystem->Open( "vidmemstats.txt", "a" );

		g_pFileSystem->FPrintf( fp, "%s:\n", g_HostState.m_levelName );
	
#ifdef VPROF_ENABLED
		CVProfile *pProf = &g_VProfCurrentProfile;

		int prefixLen = strlen( "TexGroup_Global_" );
		float total = 0.0f;
		for ( int i=0; i < pProf->GetNumCounters(); i++ )
		{
			if ( pProf->GetCounterGroup( i ) == COUNTER_GROUP_TEXTURE_GLOBAL )
			{
				// The counters are in bytes and the panel is all in kilobytes.
				float value = pProf->GetCounterValue( i ) * ( 1.0f /  ( 1024.0f * 1024.0f ) );
				total += value;
				const char *pName = pProf->GetCounterName( i );
				if( Q_strnicmp( pName, "TexGroup_Global_", prefixLen ) == 0 )
				{
					pName += prefixLen;
				}
				g_pFileSystem->FPrintf( fp, "%s: %0.3fMB\n", pName, value );
			}
		}
		g_pFileSystem->FPrintf( fp, "vidmem total: %0.3fMB\n", total );
#endif

#if 0
		g_pFileSystem->FPrintf( fp, "hunk total: %0.3fMB\n", Cache_TotalUsed() * ( 1.0f / ( 1024.0f * 1024.0f ) ) );
		g_pFileSystem->FPrintf( fp, "hunk sound: %0.3fMB\n", Cache_TotalUsed_Sound() * ( 1.0f / ( 1024.0f * 1024.0f ) ) );
		g_pFileSystem->FPrintf( fp, "hunk models: %0.3fMB\n", Cache_TotalUsed_Models() * ( 1.0f / ( 1024.0f * 1024.0f ) ) );
#endif
		g_pFileSystem->FPrintf( fp, "---------------------------------\n" );
		g_pFileSystem->Close( fp );
		Cbuf_AddText( "quit\n" );
	}
#endif
}

void CHostState::OnClientDisconnected() 
{
}

// Determine if this is a valid game
static bool Host_ValidGame( void )
{
	// No multi-client single player games
	if ( sv.IsMultiplayer() )
	{
		if ( deathmatch.GetInt() )
			return true;
	}
	else
	{
		/*
		// Single player must have CD validation
		if ( IsValidCD() )
		{
			return true;
		}
		*/
		return true;
	}

	ConDMsg("Unable to launch game\n");
	return false;
}