//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//===========================================================================//
#include "quakedef.h"
#include "bspfile.h"
#include "host.h"
#include "sys.h"
#include "filesystem_engine.h"
#include "utldict.h"
#include "demo.h"
#ifndef SWDS
#include "vgui_baseui_interface.h"
#endif

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

// Imported from other .cpp files
void Host_Map_f( const CCommand &args );
void Host_Map_Background_f( const CCommand &args );
void Host_Map_Commentary_f( const CCommand &args );
void Host_Changelevel_f( const CCommand &args );
void Host_Changelevel2_f( const CCommand &args );

//-----------------------------------------------------------------------------
// Purpose: For each map, stores when the map last changed on disk and whether
//  it is a valid map
//-----------------------------------------------------------------------------
class CMapListItem
{
public:
	enum
	{
		INVALID = 0,
		PENDING,
		VALID,
	};

					CMapListItem( void );
	
	void			SetValid( int valid );
	int				GetValid( void ) const;

	void			SetFileTimestamp( long ts );
	long			GetFileTimestamp( void ) const;

	bool			IsSameTime( long ts ) const;

	static long		GetFSTimeStamp( char const *name );
	static int		CheckFSHeaderVersion( char const *name );

private:
	int				m_nValid;
	long			m_lFileTimestamp;
};

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CMapListItem::CMapListItem( void )
{
	m_nValid = PENDING;
	m_lFileTimestamp = 0L;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : valid - 
//-----------------------------------------------------------------------------
void CMapListItem::SetValid( int valid )
{
	m_nValid = valid;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
int CMapListItem::GetValid( void ) const
{
	return m_nValid;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : ts - 
//-----------------------------------------------------------------------------
void CMapListItem::SetFileTimestamp( long ts )
{
	m_lFileTimestamp = ts;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : long
//-----------------------------------------------------------------------------
long CMapListItem::GetFileTimestamp( void ) const
{
	return m_lFileTimestamp;
}

//-----------------------------------------------------------------------------
// Purpose: Check whether this map file has changed related to the passed in timestamp
// Input  : ts - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CMapListItem::IsSameTime( long ts ) const
{
	return ( m_lFileTimestamp == ts ) ? true : false;
}

//-----------------------------------------------------------------------------
// Purpose: Get the timestamp for the file from the file system
// Input  : *name - 
// Output : long
//-----------------------------------------------------------------------------
long CMapListItem::GetFSTimeStamp( char const *name )
{
	long ts = g_pFileSystem->GetFileTime( name );
	return ts;
}

//-----------------------------------------------------------------------------
// Purpose: Check whether the specified map header version is up-to-date
// Input  : *name - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
int CMapListItem::CheckFSHeaderVersion( char const *name )
{
	dheader_t header;
	memset( &header, 0, sizeof( header ) );

	FileHandle_t fp = g_pFileSystem->Open ( name, "rb" );
	if ( fp )
	{
		g_pFileSystem->Read( &header, sizeof( header ), fp );
		g_pFileSystem->Close( fp );
	}

	return ( header.version >= MINBSPVERSION && header.version <= BSPVERSION ) ? VALID : INVALID;
}

// How often to check the filesystem for updated map info
#define MIN_REFRESH_INTERVAL 60.0f

//-----------------------------------------------------------------------------
// Purpose: Stores the current list of maps for the engine
//-----------------------------------------------------------------------------
class CMapListManager
{
public:
	CMapListManager( void );
	~CMapListManager( void );

	// See if it's time to revisit the items in the list
	void			RefreshList( void );

	// Get item count, etc
	int				GetMapCount( void ) const;
	int				IsMapValid( int index ) const;
	char const 		*GetMapName( int index ) const;

	void			Think( void );

private:
	// Clear list
	void			ClearList( void );
	// Rebuild list from scratch
	void			BuildList( void );

private:
	// Dictionary of items
	CUtlDict< CMapListItem, int > m_Items;

	// Time of last update
	float			m_flLastRefreshTime;

	bool			m_bDirty;
};

// Singleton manager object
static CMapListManager g_MapListMgr;

void Host_UpdateMapList( void )
{
	g_MapListMgr.Think();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CMapListManager::CMapListManager( void )
{
	m_flLastRefreshTime = -1.0f;
	m_bDirty = false;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CMapListManager::~CMapListManager( void )
{
	ClearList();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMapListManager::Think( void )
{
	return;

	if ( !m_bDirty )
		return;

#ifndef SWDS
	// Only update pending files if console is visible to avoid slamming FS while in a map
	if ( !EngineVGui()->IsConsoleVisible() )
		return;
#endif

	int i;

	m_bDirty = false;
		
	for ( i = m_Items.Count() - 1; i >= 0 ; i-- )
	{
		CMapListItem *item = &m_Items[ i ];
		if ( item->GetValid() != CMapListItem::PENDING )
		{
			continue;
		}

		char const *filename = m_Items.GetElementName( i );

		item->SetValid( CMapListItem::CheckFSHeaderVersion( filename ) );

		// Keep fixing things up next frame
		m_bDirty = true;
		break;
	}
}

//-----------------------------------------------------------------------------
// Purpose: FIXME:  Refresh doesn't notice maps that have been deleted... oh well
//-----------------------------------------------------------------------------
void CMapListManager::RefreshList( void )
{
	if ( m_flLastRefreshTime == -1.0f )
	{
		BuildList();
		return;
	}

	if ( realtime < m_flLastRefreshTime + MIN_REFRESH_INTERVAL )
		return;

	ConDMsg( "Refreshing map list...\n" );

	// Search the directory structure.
	char mapwild[MAX_QPATH];
	Q_strncpy(mapwild,"maps/*.bsp", sizeof( mapwild ) );
	char const *findfn = Sys_FindFirst( mapwild, NULL, 0 );
	while ( findfn )
	{
		if ( IsPC() && V_stristr( findfn, ".360.bsp" ) )
		{
			// ignore 360 bsp
			findfn = Sys_FindNext( NULL, 0 );
			continue;
		}
		else if ( IsX360() && !V_stristr( findfn, ".360.bsp" ) )
		{
			// ignore pc bsp
			findfn = Sys_FindNext( NULL, 0 );
			continue;
		}

		// Make full fileame (maps/foo.bsp) and map name (foo)
		char szFileName[ MAX_QPATH ] = { 0 };
		V_snprintf( szFileName, sizeof( szFileName ), "maps/%s", findfn );

		char szMapName[256] = { 0 };
		V_strncpy( szMapName, findfn, sizeof( szMapName ) );
		char *pExt = V_stristr( szMapName, ".bsp" );
		if ( pExt )
		{
			*pExt = '\0';
		}

		int idx = m_Items.Find( szMapName );
		if ( idx == m_Items.InvalidIndex() )
		{
			CMapListItem item;
			item.SetFileTimestamp( item.GetFSTimeStamp( szFileName ) );
			item.SetValid( CMapListItem::PENDING );
			// Insert into dictionary
			m_Items.Insert( szMapName, item );

			m_bDirty = true;
		}
		else
		{
			CMapListItem *item = &m_Items[ idx ];
			Assert( item );

			// Make sure data is up to date
			long timestamp = g_pFileSystem->GetFileTime( szFileName );
			if ( !item->IsSameTime( timestamp ) )
			{
				item->SetFileTimestamp( timestamp );
				item->SetValid( CMapListItem::PENDING );

				m_bDirty = true;
			}
		}

		findfn = Sys_FindNext( NULL, 0 );
	}

	Sys_FindClose();

	m_flLastRefreshTime = realtime;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : int
//-----------------------------------------------------------------------------
int CMapListManager::GetMapCount( void ) const
{
	return m_Items.Count();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : index - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
int CMapListManager::IsMapValid( int index ) const
{
	if ( !m_Items.IsValidIndex( index ) )
		return false;

	CMapListItem const *item = &m_Items[ index ];
	Assert( item );
	return item->GetValid();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : index - 
// Output : char const
//-----------------------------------------------------------------------------
char const *CMapListManager::GetMapName( int index ) const
{
	if ( !m_Items.IsValidIndex( index ) )
		return "Invalid!!!";

	return m_Items.GetElementName( index );
}

//-----------------------------------------------------------------------------
// Purpose: Wipe the list
//-----------------------------------------------------------------------------
void CMapListManager::ClearList( void )
{
	m_Items.Purge();
	m_bDirty = false;
}

//-----------------------------------------------------------------------------
// Purpose: Rebuild the entire list
//-----------------------------------------------------------------------------
void CMapListManager::BuildList( void )
{
	ClearList();

	// Search the directory structure.
	char mapwild[MAX_QPATH];
	Q_strncpy(mapwild,"maps/*.bsp", sizeof( mapwild ) );
	char const *findfn = Sys_FindFirst( mapwild, NULL, 0 );
	while ( findfn )
	{
		if ( IsPC() && V_stristr( findfn, ".360.bsp" ) )
		{
			// ignore 360 bsp
			findfn = Sys_FindNext( NULL, 0 );
			continue;
		}
		else if ( IsX360() && !V_stristr( findfn, ".360.bsp" ) )
		{
			// ignore pc bsp
			findfn = Sys_FindNext( NULL, 0 );
			continue;
		}

		// Make full fileame (maps/foo.bsp) and map name (foo)
		char szFileName[ MAX_QPATH ] = { 0 };
		V_snprintf( szFileName, sizeof( szFileName ), "maps/%s", findfn );

		char szMapName[256] = { 0 };
		V_strncpy( szMapName, findfn, sizeof( szMapName ) );
		char *pExt = V_stristr( szMapName, ".bsp" );
		if ( pExt )
		{
			*pExt = '\0';
		}

		CMapListItem item;
		item.SetFileTimestamp( item.GetFSTimeStamp( szFileName ) );
		item.SetValid( CMapListItem::PENDING );

		// Insert into dictionary
		int idx = m_Items.Find( szMapName );
		if ( idx == m_Items.InvalidIndex() )
		{
			m_Items.Insert( szMapName, item );
		}

		findfn = Sys_FindNext( NULL, 0 );
	}

	Sys_FindClose();

	// Remember time we build the list
	m_flLastRefreshTime = realtime;

	m_bDirty = true;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *pakorfilesys - 
//			*mapname - 
// Output : static void
//-----------------------------------------------------------------------------
static bool MapList_CheckPrintMap( const char *pakorfilesys, const char *mapname, int valid,
							   bool showoutdated, bool verbose )
{
	bool validorpending = ( valid != CMapListItem::INVALID ) ? true : false;

	if ( !verbose )
	{
		return validorpending;
	}

	char prefix[ 32 ];
	prefix[ 0 ] = 0;

	switch ( valid )
	{
	default:
	case CMapListItem::VALID:
		break;
	case CMapListItem::PENDING:
		Q_strncpy( prefix, "PENDING:  ", sizeof( prefix ) );
		break;
	case CMapListItem::INVALID:
		Q_strncpy( prefix, "OUTDATED:  ", sizeof( prefix ) );
		break;
	}

	if ( validorpending ^ showoutdated )
	{
		ConMsg( "%s %s %s\n", prefix, pakorfilesys, mapname );
	}

	return validorpending;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *pszSubString - 
//			listobsolete - 
//			maxitemlength - 
// Output : static int
//-----------------------------------------------------------------------------
static int MapList_CountMaps( const char *pszSubString, bool listobsolete, int& maxitemlength )
{
	g_MapListMgr.RefreshList();

	maxitemlength = 0;

	int substringlength = 0;
	if ( pszSubString && pszSubString[0] )
	{
		substringlength = strlen(pszSubString);
	}

	//
	// search through the path, one element at a time
	//
	int count = 0;
	int showOutdated;
	for( showOutdated = listobsolete ? 1 : 0; showOutdated >= 0; showOutdated-- )
	{
		for ( int i = 0; i < g_MapListMgr.GetMapCount(); i++ )
		{
			char const *mapname = g_MapListMgr.GetMapName( i );
			int valid = g_MapListMgr.IsMapValid( i );

			if ( !substringlength || V_stristr( mapname, pszSubString ) )
			{
				if ( MapList_CheckPrintMap( "(fs)", mapname, valid, showOutdated ? true : false, false ) )
				{
					maxitemlength = max( maxitemlength, (int)( strlen( mapname ) + 1 ) );
					count++;
				}
			}
		}
	}

	return count;
}

//-----------------------------------------------------------------------------
// Purpose: 
//  Lists all maps matching the substring
//  If the substring is empty, or "*", then lists all maps
// Input  : *pszSubString - 
//-----------------------------------------------------------------------------
int MapList_ListMaps( const char *pszSubString, bool listobsolete, bool verbose, int maxcount, int maxitemlength, char maplist[][ 64 ] )
{
	g_MapListMgr.RefreshList();
	int substringlength = 0;
	if (pszSubString && pszSubString[0])
	{
		substringlength = strlen(pszSubString);
	}

	//
	// search through the path, one element at a time
	//

	if ( verbose )
	{
		ConMsg( "-------------\n");
	}

	int count = 0;
	int showOutdated;
	for( showOutdated = listobsolete ? 1 : 0; showOutdated >= 0; showOutdated-- )
	{
		if ( count >= maxcount )
			break;

		//search the directory structure.
		for ( int i = 0; i < g_MapListMgr.GetMapCount(); i++ )
		{
			if ( count >= maxcount )
				break;

			char const *mapname = g_MapListMgr.GetMapName( i );
			int valid = g_MapListMgr.IsMapValid( i );

			if ( !substringlength || V_stristr( mapname, pszSubString ) )
			{
				if ( MapList_CheckPrintMap( "(fs)", mapname, valid, showOutdated ? true : false, verbose ) )
				{
					if ( maxitemlength != 0 )
					{
						Q_strncpy( maplist[ count ], mapname, maxitemlength );
					}
					count++;
				}
			}
		}
	}

	return count;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *partial - 
//			context - 
//			longest - 
//			maxcommands - 
//			**commands - 
// Output : int
//-----------------------------------------------------------------------------
int _Host_Map_f_CompletionFunc( char const *cmdname, char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] )
{
	char *substring = (char *)partial;
	if ( Q_strstr( partial, cmdname ) )
	{
		substring = (char *)partial + strlen( cmdname );
	}

	int longest = 0;
	int count = min( MapList_CountMaps( substring, false, longest ), COMMAND_COMPLETION_MAXITEMS );
	if ( count > 0 )
	{
		MapList_ListMaps( substring, false, false, COMMAND_COMPLETION_MAXITEMS, longest, commands );

		// Now prepend maps * in front of all of the options
		int i;
		for ( i = 0; i < count ; i++ )
		{
			char old[ COMMAND_COMPLETION_ITEM_LENGTH ];
			Q_strncpy( old, commands[ i ], sizeof( old ) );
			Q_snprintf( commands[ i ], sizeof( commands[ i ] ), "%s%s", cmdname, old );
		}
	}

	return count;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *partial - 
//			context - 
//			longest - 
//			maxcommands - 
//			**commands - 
// Output : int
//-----------------------------------------------------------------------------
static int Host_Map_f_CompletionFunc( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] )
{
	char const *cmdname = "map ";
	return _Host_Map_f_CompletionFunc( cmdname, partial, commands );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *partial - 
//			context - 
//			longest - 
//			maxcommands - 
//			**commands - 
// Output : int
//-----------------------------------------------------------------------------
static int Host_Map_Commentary_f_CompletionFunc( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] )
{
	char const *cmdname = "map_commentary ";
	return _Host_Map_f_CompletionFunc( cmdname, partial, commands );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *partial - 
//			context - 
//			longest - 
//			maxcommands - 
//			**commands - 
// Output : int
//-----------------------------------------------------------------------------
static int Host_Changelevel_f_CompletionFunc( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] )
{
	char const *cmdname = "changelevel ";
	return _Host_Map_f_CompletionFunc( cmdname, partial, commands );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *partial - 
//			context - 
//			longest - 
//			maxcommands - 
//			**commands - 
// Output : int
//-----------------------------------------------------------------------------
static int Host_Changelevel2_f_CompletionFunc( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] )
{
	char const *cmdname = "changelevel2 ";
	return _Host_Map_f_CompletionFunc( cmdname, partial, commands );
}


//-----------------------------------------------------------------------------
// Purpose: do a dir of the maps dir
//-----------------------------------------------------------------------------
static void Host_Maps_f( const CCommand &args )
{
	const char *pszSubString = NULL;

	if ( args.ArgC() != 2 && args.ArgC() != 3 )
	{
		ConMsg( "Usage:  maps <substring>\nmaps * for full listing\n" );
		return;
	}

	if ( args.ArgC() == 2 )
	{
		pszSubString = args[1];
		if (!pszSubString || !pszSubString[0])
			return;
	}

	if ( pszSubString && ( pszSubString[0] == '*' ))
		pszSubString = NULL;

	int longest = 0;
	int count = MapList_CountMaps( pszSubString, true, longest );
	if ( count > 0 )
	{
		MapList_ListMaps( pszSubString, true, true, count, 0, NULL );
	}
}

#ifndef BENCHMARK
static ConCommand maps("maps", Host_Maps_f, "Displays list of maps." );
static ConCommand map("map", Host_Map_f, "Start playing on specified map.", FCVAR_DONTRECORD, Host_Map_f_CompletionFunc );
static ConCommand map_background("map_background", Host_Map_Background_f, "Runs a map as the background to the main menu.", FCVAR_DONTRECORD, Host_Map_f_CompletionFunc );
static ConCommand map_commentary("map_commentary", Host_Map_Commentary_f, "Start playing, with commentary, on a specified map.", FCVAR_DONTRECORD, Host_Map_Commentary_f_CompletionFunc );
static ConCommand changelevel("changelevel", Host_Changelevel_f, "Change server to the specified map", FCVAR_DONTRECORD, Host_Changelevel_f_CompletionFunc );
static ConCommand changelevel2("changelevel2", Host_Changelevel2_f, "Transition to the specified map in single player", FCVAR_DONTRECORD, Host_Changelevel2_f_CompletionFunc );
#endif