//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Xbox console link
//
//=====================================================================================//

#include "xbox/xbox_console.h"
#include "xbox/xbox_vxconsole.h"
#include "tier0/threadtools.h"
#include "tier0/tslist.h"
#include "tier0/ICommandLine.h"
#include "tier0/memdbgon.h"

// all redirecting funneled here, stop redirecting in this module only
#undef OutputDebugStringA

#pragma comment( lib, "xbdm.lib" )

struct DebugString_t
{
	unsigned int	color;
	char			*pString;
};

#define XBX_DBGCOMMANDPREFIX	"XCMD"
#define XBX_DBGRESPONSEPREFIX	"XACK"
#define XBX_DBGPRINTPREFIX		"XPRT"
#define XBX_DBGCOLORPREFIX		"XCLR"

#define XBX_MAX_RCMDLENGTH		256
#define XBX_MAX_MESSAGE			2048

CThreadFastMutex				g_xbx_dbgChannelMutex;
CThreadFastMutex				g_xbx_dbgCommandHandlerMutex;
static char						g_xbx_dbgRemoteBuf[XBX_MAX_RCMDLENGTH];
static HANDLE					g_xbx_dbgValidEvent;
static HANDLE					g_xbx_dbgCmdCompleteEvent;
bool							g_xbx_bUseVXConsoleOutput = true;
bool							g_xbx_bDoSyncOutput;
static ThreadHandle_t			g_xbx_hDebugThread;
CTSQueue<DebugString_t>			g_xbx_DebugStringQueue;
extern CInterlockedInt			g_xbx_numProfileCounters;
extern unsigned int				g_xbx_profileCounters[];
extern char						g_xbx_profileName[];
int								g_xbx_freeMemory;

_inline bool XBX_NoXBDM()			{ return false; }

static CXboxConsole XboxConsole;

DLL_EXPORT IXboxConsole *GetConsoleInterface()
{
	return &XboxConsole;
}

//-----------------------------------------------------------------------------
//	Low level string output.
//	Input string should be stack based, can get clobbered.
//-----------------------------------------------------------------------------
static void OutputStringToDevice( unsigned int color, char *pString, bool bRemoteValid )
{
	if ( !bRemoteValid )
	{
		// local debug only
		OutputDebugStringA( pString );
		return;
	}

	// remote debug valid
	// non pure colors don't translate well - find closest pure hue
	unsigned int bestColor = color;
	int r = ( bestColor & 0xFF );
	int g = ( bestColor >> 8 ) & 0xFF;
	int b = ( bestColor >> 16 ) & 0xFF;
	if ( ( r && r != 255 ) || ( g && g != 255 ) || ( b && b != 255 ) )
	{
		int r0, g0, b0;
		unsigned int minDist = 0xFFFFFFFF;
		for ( int i=0; i<8; i++ )
		{
			r0 = g0 = b0 = 0;
			if ( i&4 )
				r0 = 255;
			if ( i&2 )
				g0 = 255;
			if ( i&1 )
				b0 = 255;
			unsigned int d = ( r-r0 )*( r-r0 ) + ( g-g0 )*( g-g0 ) + ( b-b0 )*( b-b0 );
			if ( minDist > d )
			{
				minDist = d;
				bestColor = XMAKECOLOR( r0, g0, b0 );
			}
		}
	}

	// create color string
	char colorString[16];
	sprintf( colorString, XBX_DBGCOLORPREFIX "[%8.8x]", bestColor );

	// chunk line out, for each cr
	char strBuffer[XBX_MAX_RCMDLENGTH];
	char *pStart = pString;
	char *pEnd = pStart + strlen( pStart );
	char *pNext;
	while ( pStart < pEnd )
	{
		pNext = strchr( pStart, '\n' );
		if ( !pNext )
			pNext = pEnd;
		else
			*pNext = '\0';

		int length = _snprintf( strBuffer, XBX_MAX_RCMDLENGTH, "%s!%s%s", XBX_DBGPRINTPREFIX, colorString, pStart );
		if ( length == -1 )
		{
			strBuffer[sizeof( strBuffer )-1] = '\0';
		}
		// Send the string
		DmSendNotificationString( strBuffer );

		// advance past cr
		pStart = pNext+1;
	}
}

//-----------------------------------------------------------------------------
//	XBX_IsConsoleConnected
//
//-----------------------------------------------------------------------------
bool CXboxConsole::IsConsoleConnected()
{
	bool bConnected;
 
	if ( g_xbx_dbgValidEvent == NULL )
	{
		// init was never called
		return false;
	}

	AUTO_LOCK_FM( g_xbx_dbgChannelMutex );

	bConnected = ( WaitForSingleObject( g_xbx_dbgValidEvent, 0 ) == WAIT_OBJECT_0 );

	return bConnected;
}

//-----------------------------------------------------------------------------
//	Output string to listening console. Queues output for slave thread.
//	Needs to be lightweight.
//-----------------------------------------------------------------------------
void CXboxConsole::DebugString( unsigned int color, const char* pFormat, ... )
{
	if ( XBX_NoXBDM() )
		return;

	va_list	args;
	char	szStringBuffer[XBX_MAX_MESSAGE];
	int		length;

	// resolve string
	va_start( args, pFormat );
	length = _vsnprintf( szStringBuffer, sizeof( szStringBuffer ), pFormat, args );
	if ( length == -1 )
	{
		szStringBuffer[sizeof( szStringBuffer ) - 1] = '\0';
	}
	va_end( args );

	if ( !g_xbx_bDoSyncOutput )
	{
		// queue string for delayed output
		DebugString_t debugString;
		debugString.color = color;
		debugString.pString = strdup( szStringBuffer );
		g_xbx_DebugStringQueue.PushItem( debugString );
	}
	else
	{
		bool bRemoteValid = g_xbx_bUseVXConsoleOutput && XBX_IsConsoleConnected();
		OutputStringToDevice( color, szStringBuffer, bRemoteValid );
	}
}

//-----------------------------------------------------------------------------
//	Waits for debug queue to drain.
//-----------------------------------------------------------------------------
void CXboxConsole::FlushDebugOutput()
{
	while ( g_xbx_DebugStringQueue.Count() != 0 )
	{
		Sleep( 1 );
	}
}

//-----------------------------------------------------------------------------
//	_xdbg_strlen
//
//	Critical section safe.
//-----------------------------------------------------------------------------
int _xdbg_strlen( const CHAR* str )
{
    const CHAR* strEnd = str;

    while( *strEnd )
        strEnd++;

    return strEnd - str;
}

//-----------------------------------------------------------------------------
//	_xdbg_tolower
//
//	Critical section safe.
//-----------------------------------------------------------------------------
inline CHAR _xdbg_tolower( CHAR ch )
{
    if( ch >= 'A' && ch <= 'Z' )
        return ch - ( 'A' - 'a' );
    else
        return ch;
}

//-----------------------------------------------------------------------------
//	_xdbg_strnicmp
//
//	Critical section safe.
//-----------------------------------------------------------------------------
BOOL _xdbg_strnicmp( const CHAR* str1, const CHAR* str2, int n )
{
    while ( ( _xdbg_tolower( *str1 ) == _xdbg_tolower( *str2 ) ) && *str1 && n > 0 )
    {
        --n;
        ++str1;
        ++str2;
    }
    return ( !n || _xdbg_tolower( *str1 ) == _xdbg_tolower( *str2 ) );
}

//-----------------------------------------------------------------------------
//	_xdbg_strcpy
//
//	Critical section safe.
//-----------------------------------------------------------------------------
VOID _xdbg_strcpy( CHAR* strDest, const CHAR* strSrc )
{
    while ( ( *strDest++ = *strSrc++ ) != 0 );
}

//-----------------------------------------------------------------------------
//	_xdbg_strcpyn
//
//	Critical section safe.
//-----------------------------------------------------------------------------
VOID _xdbg_strcpyn( CHAR* strDest, const CHAR* strSrc, int numChars )
{
	while ( numChars>0 && ( *strDest++ = *strSrc++ ) != 0 )
		numChars--;
}

//-----------------------------------------------------------------------------
//	_xdbg_gettoken
//
//	Critical section safe.
//-----------------------------------------------------------------------------
void _xdbg_gettoken( CHAR** tokenStream, CHAR* token, int tokenSize )
{
	int		c;
	int		len;
	CHAR*	data;

	len = 0;

	// skip prefix whitespace
	data = *tokenStream;
	while ( ( c = *data ) <= ' ' ) 
	{
		if ( !c )
			goto cleanUp;
		data++;
	}

	// parse a token
	do
	{
		if ( len < tokenSize )
			token[len++] = c;

		data++;
		c = *data;
	} while ( c > ' ' );

	if ( len >= tokenSize ) 
		len = 0;

cleanUp:
	token[len]   = '\0';
	*tokenStream = data;
}

//-----------------------------------------------------------------------------
//	_xdbg_tokenize
//
//	Critical section safe.
//-----------------------------------------------------------------------------
int _xdbg_tokenize( CHAR* tokenStream, CHAR** tokens, int maxTokens )
{
	char	token[64];

	// tokenize stream into seperate tokens
	int	numTokens = 0;
	while ( 1 )
	{
		tokens[numTokens++] = tokenStream;
		if ( numTokens >= maxTokens )
			break;

		_xdbg_gettoken( &tokenStream, token, sizeof( token ) );
		if ( !tokenStream[0] || !token[0] )
			break;

		*tokenStream = '\0';
		tokenStream++;
	}
	return ( numTokens );
}

//-----------------------------------------------------------------------------
//	_xdbg_findtoken
//
//	Critical section safe. Returns -1 if not found
//-----------------------------------------------------------------------------
int _xdbg_findtoken( CHAR** tokens, int numTokens, CHAR* token )
{
	int	i;
	int	len;

	len = _xdbg_strlen( token );
	for ( i=0; i<numTokens; i++ )
	{
		if ( _xdbg_strnicmp( tokens[i], token, len ) )
			return i;
	}
	
	// not found
	return -1;
}

//-----------------------------------------------------------------------------
//	_DebugCommandHandler
//
//-----------------------------------------------------------------------------
HRESULT __stdcall _DebugCommandHandler( const CHAR* strCommand, CHAR* strResponse, DWORD dwResponseLen, PDM_CMDCONT pdmcc )
{
	CHAR			buff[256];
	CHAR*			args[8];
	int				numArgs;

	AUTO_LOCK_FM( g_xbx_dbgCommandHandlerMutex );

    // skip over the command prefix and the exclamation mark
    strCommand += _xdbg_strlen( XBX_DBGCOMMANDPREFIX ) + 1;

	if ( strCommand[0] == '\0' )
	{
		// just a ping
		goto cleanUp;
	}

	// get command and optional arguments
	_xdbg_strcpyn( buff, strCommand, sizeof( buff ) );
	numArgs = _xdbg_tokenize( buff, args, sizeof( args )/sizeof( CHAR* ) );

    if ( _xdbg_strnicmp( args[0], "__connect__", 11 ) )
    {
		if ( numArgs > 1 && atoi( args[1] ) == VXCONSOLE_PROTOCOL_VERSION )
		{
			// initial connect - respond that we're connected
			_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "Connected To Application.", dwResponseLen );
			SetEvent( g_xbx_dbgValidEvent );

			// notify convar system to send its commands
			// allows vxconsole to re-connect during game
			_xdbg_strcpy( g_xbx_dbgRemoteBuf, "getcvars" );
			XBX_QueueEvent( XEV_REMOTECMD, ( int )g_xbx_dbgRemoteBuf, 0, 0 );
		}
		else
		{
			_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "Rejecting Connection: Wrong Protocol Version.", dwResponseLen );
		}
		goto cleanUp;
    }

	if ( _xdbg_strnicmp( args[0], "__disconnect__", 14 ) )
    {
		// respond that we're disconnected
        _xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "Disconnected.", dwResponseLen );
		ResetEvent( g_xbx_dbgValidEvent );
		goto cleanUp;
    }

    if ( _xdbg_strnicmp( args[0], "__complete__", 12 ) )
    {
		// remote server has finished command - respond to acknowledge
		_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "OK", dwResponseLen );

		// set the complete event - allows expected synchronous calling mechanism
		SetEvent( g_xbx_dbgCmdCompleteEvent );
        goto cleanUp;
    }

    if ( _xdbg_strnicmp( args[0], "__memory__", 10 ) )
    {		
		// get a current stat of available memory
		MEMORYSTATUS stat;
		GlobalMemoryStatus( &stat );
		g_xbx_freeMemory = stat.dwAvailPhys;

		if ( _xdbg_findtoken( args, numArgs, "quiet" ) > 0 )
		{
			_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "OK", dwResponseLen );
		}
		else
		{
			// 32 MB is reserved and fixed by OS, so not reporting
			_snprintf( strResponse, dwResponseLen, XBX_DBGRESPONSEPREFIX "Available: %.2f MB, Used: %.2f MB, Free: %.2f MB", 
				stat.dwTotalPhys/( 1024.0f*1024.0f ) - 32.0f,
				( stat.dwTotalPhys - stat.dwAvailPhys )/( 1024.0f*1024.0f ) - 32.0f, 
				stat.dwAvailPhys/( 1024.0f*1024.0f ) );
		}
		goto cleanUp;
	}

	if ( g_xbx_dbgRemoteBuf[0] )
	{
		// previous command still pending
		_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "Cannot execute: Previous command still pending", dwResponseLen );
	}
	else
	{
		// add the command to the event queue to be processed by main app
		_xdbg_strcpy( g_xbx_dbgRemoteBuf, strCommand );
		XBX_QueueEvent( XEV_REMOTECMD, ( int )g_xbx_dbgRemoteBuf, 0, 0 );
		_xdbg_strcpyn( strResponse, XBX_DBGRESPONSEPREFIX "OK", dwResponseLen );
	}

cleanUp:
    return XBDM_NOERR;
}

//-----------------------------------------------------------------------------
//	XBX_SendRemoteCommand
//
//-----------------------------------------------------------------------------
void CXboxConsole::SendRemoteCommand( const char *pCommand, bool async )
{
	char cmdString[XBX_MAX_RCMDLENGTH];

	if ( XBX_NoXBDM() || !IsConsoleConnected() )
		return;

	AUTO_LOCK_FM( g_xbx_dbgChannelMutex );

	_snprintf( cmdString, sizeof( cmdString ), "%s!%s", XBX_DBGCOMMANDPREFIX, pCommand );
	HRESULT hr = DmSendNotificationString( cmdString );
	if ( FAILED( hr ) )
	{
		XBX_Error( "XBX_SendRemoteCommand: failed on %s", cmdString );
	}

	// wait for command completion
	if ( !async )
	{
		DWORD timeout;
		if ( !strnicmp( pCommand, "Assert()", 8 ) )
		{
			// the assert is waiting for user to make selection
			timeout = INFINITE;
		}
		else
		{
			// no vxconsole operation should take this long
			timeout = 15000;
		}

		if ( WaitForSingleObject( g_xbx_dbgCmdCompleteEvent, timeout ) == WAIT_TIMEOUT )
		{
			// we have no choice but to dump core
			DmCrashDump( false );
		}
	}
}

//-----------------------------------------------------------------------------
//	Handle delayed VXConsole transactions
//
//-----------------------------------------------------------------------------
static unsigned _DebugThreadFunc( void *pParam )
{
	while ( 1 )
	{
		Sleep( 10 );

		if ( !g_xbx_DebugStringQueue.Count() && !g_xbx_numProfileCounters && !g_xbx_freeMemory )
		{
			continue;
		}

		if ( g_xbx_numProfileCounters )
		{
			// build and send asynchronously
			char dbgCommand[XBX_MAX_RCMDLENGTH];
			_snprintf( dbgCommand, sizeof( dbgCommand ), "SetProfileData() %s 0x%8.8x", g_xbx_profileName, g_xbx_profileCounters );
			XBX_SendRemoteCommand( dbgCommand, true );

			// mark as sent
			g_xbx_numProfileCounters = 0;
		}

		if ( g_xbx_freeMemory )
		{
			// build and send asynchronously
			char dbgCommand[XBX_MAX_RCMDLENGTH];
			_snprintf( dbgCommand, sizeof( dbgCommand ), "FreeMemory() 0x%8.8x", g_xbx_freeMemory );
			XBX_SendRemoteCommand( dbgCommand, true );

			// mark as sent
			g_xbx_freeMemory = 0;
		}

		bool bRemoteValid = g_xbx_bUseVXConsoleOutput && XBX_IsConsoleConnected();
		while ( 1 )
		{
			DebugString_t debugString;
			if ( !g_xbx_DebugStringQueue.PopItem( &debugString ) )
			{
				break;
			}

			OutputStringToDevice( debugString.color, debugString.pString, bRemoteValid );
			free( debugString.pString );
		}
	}

	return 0;
}

//-----------------------------------------------------------------------------
//	XBX_InitConsoleMonitor
//
//-----------------------------------------------------------------------------
void CXboxConsole::InitConsoleMonitor( bool bWaitForConnect )
{
	if ( XBX_NoXBDM() )
		return;

	// create our events
	g_xbx_dbgValidEvent = CreateEvent( XBOX_DONTCARE, TRUE, FALSE, NULL );
	g_xbx_dbgCmdCompleteEvent = CreateEvent( XBOX_DONTCARE, FALSE, FALSE, NULL );

	// register our command handler with the debug monitor
	HRESULT hr = DmRegisterCommandProcessor( XBX_DBGCOMMANDPREFIX, _DebugCommandHandler );
	if ( FAILED( hr ) )
	{
		XBX_Error( "XBX_InitConsoleMonitor: failed to register command processor" );
	}

	// user can have output bypass slave thread
	g_xbx_bDoSyncOutput = CommandLine()->FindParm( "-syncoutput" ) != 0;

	// create a slave thread to do delayed VXConsole transactions
	ThreadId_t threadID;
	g_xbx_hDebugThread = CreateSimpleThread( _DebugThreadFunc, NULL, &threadID, 16*1024 ); 
	ThreadSetDebugName( threadID, "DebugThread" );
	ThreadSetAffinity( g_xbx_hDebugThread, XBOX_PROCESSOR_5 );

	if ( bWaitForConnect )
	{
		XBX_DebugString( XBX_CLR_DEFAULT, "Waiting For VXConsole Connection...\n" );
		WaitForSingleObject( g_xbx_dbgValidEvent, INFINITE );
	}
}

//-----------------------------------------------------------------------------
//	Sends a disconnect signal to possibly attached VXConsole.
//-----------------------------------------------------------------------------
void CXboxConsole::DisconnectConsoleMonitor()
{
	if ( XBX_NoXBDM() )
		return;

	// caller is trying to safely stop vxconsole traffic, disconnect must be synchronous
	XBX_SendRemoteCommand( "Disconnect()", false );
}

bool CXboxConsole::GetXboxName( char *pName, unsigned *pLength )
{
	return ( DmGetXboxName( pName, (DWORD *)pLength ) == XBDM_NOERR );
}

void CXboxConsole::CrashDump( bool b )
{
	DmCrashDump(b);
}

//-----------------------------------------------------------------------------
// Walk to a specific module and dump size info
//-----------------------------------------------------------------------------
int CXboxConsole::DumpModuleSize( const char *pName )
{
	HRESULT error;
	PDM_WALK_MODULES pWalkMod = NULL;
	DMN_MODLOAD modLoad;
	int size = 0;

	// iterate and find match
	do 
	{
		error = DmWalkLoadedModules( &pWalkMod, &modLoad );
		if ( XBDM_NOERR == error && !stricmp( modLoad.Name, pName ) )
		{
			Msg( "0x%8.8x, %5.2f MB, %s\n", modLoad.BaseAddress, modLoad.Size/( 1024.0f*1024.0f ), modLoad.Name );
			size = modLoad.Size;
			error = XBDM_ENDOFLIST;
		}
	} 
	while ( XBDM_NOERR == error );
	DmCloseLoadedModules( pWalkMod );

	if ( error != XBDM_ENDOFLIST )
	{
		Warning( "DmWalkLoadedModules() failed.\n" );
	}

	return size;
}

//-----------------------------------------------------------------------------
// 360 spew sizes of dll modules
//-----------------------------------------------------------------------------
char const* HACK_stristr( char const* pStr, char const* pSearch ) // hack because moved code from above vstdlib
{
	AssertValidStringPtr(pStr);
	AssertValidStringPtr(pSearch);

	if (!pStr || !pSearch) 
		return 0;

	char const* pLetter = pStr;

	// Check the entire string
	while (*pLetter != 0)
	{
		// Skip over non-matches
		if (tolower((unsigned char)*pLetter) == tolower((unsigned char)*pSearch))
		{
			// Check for match
			char const* pMatch = pLetter + 1;
			char const* pTest = pSearch + 1;
			while (*pTest != 0)
			{
				// We've run off the end; don't bother.
				if (*pMatch == 0)
					return 0;

				if (tolower((unsigned char)*pMatch) != tolower((unsigned char)*pTest))
					break;

				++pMatch;
				++pTest;
			}

			// Found a match!
			if (*pTest == 0)
				return pLetter;
		}

		++pLetter;
	}

	return 0;
}

void CXboxConsole::DumpDllInfo( const char *pBasePath )
{
	// Directories containing dlls
	static char *dllDirs[] = 
	{
		"bin",
		"hl2\\bin",
		"tf\\bin",
		"portal\\bin",
		"episodic\\bin"
	};

	char binPath[MAX_PATH];
	char dllPath[MAX_PATH];
	char searchPath[MAX_PATH];

	HMODULE hModule;
	WIN32_FIND_DATA wfd;
	HANDLE hFind;

	Msg( "Dumping Module Sizes...\n" );

	for ( int i = 0; i < ARRAYSIZE( dllDirs ); ++i )
	{
		int totalSize = 0;

		_snprintf( binPath, sizeof( binPath ), "%s\\%s", pBasePath, dllDirs[i] );
		_snprintf( searchPath, sizeof( binPath ), "%s\\*.dll", binPath );

		// show the directory we're searching
		Msg( "\nDirectory: %s\n\n", binPath );

		// Start the find and check for failure.
		hFind = FindFirstFile( searchPath, &wfd );
		if ( INVALID_HANDLE_VALUE == hFind )
		{
			Warning( "No Files Found.\n" );
		}
		else
		{
			// Load and unload each dll individually.
			do
			{
				if ( !HACK_stristr( wfd.cFileName, "_360.dll" ) )
				{
					// exclude explicit pc dlls
					// FindFirstFile does not support a spec mask of *_360.dll on the Xbox HDD
					continue;
				}

				_snprintf( dllPath, sizeof( dllPath ), "%s\\%s", binPath, wfd.cFileName );
				hModule = LoadLibrary( dllPath );
				if ( hModule )
				{
					totalSize += DumpModuleSize( wfd.cFileName );
					FreeLibrary( hModule );
				}
				else
				{
					Warning( "Failed to load: %s\n", dllPath );
				}
			} 
			while( FindNextFile( hFind, &wfd ) );

			FindClose( hFind );

			Msg( "Total Size: %.2f MB\n", totalSize/( 1024.0f*1024.0f ) ); 
		}
	}
}

void CXboxConsole::OutputDebugString( const char *p )
{
	::OutputDebugStringA( p );
}

bool CXboxConsole::IsDebuggerPresent()
{
	return ( DmIsDebuggerPresent() != 0 );
}