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

#include <vgui/ILocalize.h>
#include <vgui/IScheme.h>
#include <vgui/ISurface.h>
#include <vgui/IVGui.h>
#include <vgui/IInput.h>
#include <vgui/isystem.h>

#include <vgui_controls/MessageBox.h>
#include <vgui_controls/Controls.h>
#include <vgui_controls/Panel.h>

#include "SDKLauncherDialog.h"
#include "appframework/tier3app.h"
#include "tier0/icommandline.h"
#include "filesystem_tools.h"
#include "sdklauncher_main.h"
#include "configs.h"
#include "min_footprint_files.h"
#include "CreateModWizard.h"
#include "inputsystem/iinputsystem.h"

#include <io.h>
#include <stdio.h>

// Since windows redefines MessageBox.
typedef vgui::MessageBox vguiMessageBox;

#include <winsock2.h>
#include "steam/steam_api.h"

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

HANDLE g_dwChangeHandle = NULL;

#define DEFAULTGAMEDIR_KEYNAME	"DefaultGameDir"

// Dummy window
static WNDCLASS staticWndclass = { NULL };
static ATOM staticWndclassAtom = 0;
static HWND staticHwnd = 0;
CSteamAPIContext g_SteamAPIContext;
CSteamAPIContext *steamapicontext = &g_SteamAPIContext;

// This is the base engine + mod-specific game dir (e.g. "c:\tf2\mytfmod\")
char	gamedir[1024];	
extern	char g_engineDir[50];
CSDKLauncherDialog *g_pMainFrame = 0;


bool g_bAutoHL2Mod = false;
bool g_bModWizard_CmdLineFields = false;
char g_ModWizard_CmdLine_ModDir[MAX_PATH];
char g_ModWizard_CmdLine_ModName[256];
bool g_bAppQuit = false;


//-----------------------------------------------------------------------------
// Purpose: Message handler for dummy app
//-----------------------------------------------------------------------------
static LRESULT CALLBACK messageProc( HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam )
{
	// See if we've gotten a VPROJECT change
	if ( msg == WM_SETTINGCHANGE )
	{
		if ( g_pMainFrame != NULL  )
		{
			char szCurrentGame[MAX_PATH];

			// Get VCONFIG from the registry
			GetVConfigRegistrySetting( GAMEDIR_TOKEN, szCurrentGame, sizeof( szCurrentGame ) );

			g_pMainFrame->SetCurrentGame( szCurrentGame );
		}
	} 
 
	return ::DefWindowProc(hwnd,msg,wparam,lparam);
}

const char* GetLastWindowsErrorString()
{
	static char err[2048];
	
	LPVOID lpMsgBuf;
	FormatMessage( 
		FORMAT_MESSAGE_ALLOCATE_BUFFER | 
		FORMAT_MESSAGE_FROM_SYSTEM | 
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		GetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
		(LPTSTR) &lpMsgBuf,
		0,
		NULL 
	);

	strncpy( err, (char*)lpMsgBuf, sizeof( err ) );
	LocalFree( lpMsgBuf );

	err[ sizeof( err ) - 1 ] = 0;

	return err;
}

//-----------------------------------------------------------------------------
// Purpose: Creates a dummy window that handles windows messages
//-----------------------------------------------------------------------------
void CreateMessageWindow( void )
{
	// Make and register a very simple window class
	memset(&staticWndclass, 0, sizeof(staticWndclass));
	staticWndclass.style = 0;
	staticWndclass.lpfnWndProc = messageProc;
	staticWndclass.hInstance = GetModuleHandle(NULL);
	staticWndclass.lpszClassName = "SDKLauncher_Window";
	staticWndclassAtom = ::RegisterClass( &staticWndclass );

	// Create an empty window just for message handling
	staticHwnd = CreateWindowEx(0, "SDKLauncher_Window", "Hidden Window", 0, 0, 0, 1, 1, NULL, NULL, GetModuleHandle(NULL), NULL);
}
 
//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void ShutdownMessageWindow( void )
{
	// Kill our windows instance
	::DestroyWindow( staticHwnd );
	::UnregisterClass("VConfig_Window", ::GetModuleHandle(NULL));
}

SpewRetval_t SDKLauncherSpewOutputFunc( SpewType_t spewType, char const *pMsg )
{
#ifdef _WIN32
	OutputDebugString( pMsg );
#endif

	if (spewType == SPEW_ERROR)
	{
		// In Windows vgui mode, make a message box or they won't ever see the error.
#ifdef _WIN32
		MessageBox( NULL, pMsg, "Error", MB_OK | MB_TASKMODAL );
		TerminateProcess( GetCurrentProcess(), 1 );
#elif _LINUX
		_exit(1);
#else
#error "Implement me"
#endif
		
		return SPEW_ABORT;
	}
	if (spewType == SPEW_ASSERT)
	{
		if ( CommandLine()->FindParm( "-noassert" ) == 0 )
			return SPEW_DEBUGGER;
		else
			return SPEW_CONTINUE;
	}
	return SPEW_CONTINUE;
}


const char* GetSDKLauncherBinDirectory()
{
	static char path[MAX_PATH] = {0};
	if ( path[0] == 0 )
	{
		GetModuleFileName( (HMODULE)GetAppInstance(), path, sizeof( path ) );
		Q_StripLastDir( path, sizeof( path ) );	// Get rid of the filename.
		Q_StripTrailingSlash( path );
	}
	return path;
}


const char* GetSDKToolsBinDirectory( )
{
	static char path[MAX_PATH] = {0};
	if ( path[0] == 0 )
	{
		GetModuleFileName( (HMODULE)GetAppInstance(), path, sizeof( path ) );
		Q_StripLastDir( path, sizeof( path ) );	// Get rid of the filename.
		V_strncat( path, g_engineDir, sizeof( path ) );
		V_strncat( path, "\\bin", sizeof( path ) );
	}
	return path;
}


const char* GetSDKLauncherBaseDirectory()
{
	static char basedir[512] = {0};
	if ( basedir[0] == 0 )
	{
		Q_strncpy( basedir, GetSDKLauncherBinDirectory(), sizeof( basedir ) );
		Q_StripLastDir( basedir, sizeof( basedir ) );	// Get rid of the bin directory.
		Q_StripTrailingSlash( basedir );
	}
	return basedir;
}


void SubstituteBaseDir( const char *pIn, char *pOut, int outLen )
{
	Q_StrSubst( pIn, "%basedir%", GetSDKLauncherBaseDirectory(), pOut, outLen );
}


CUtlVector<char> g_FileData;
CUtlVector<char> g_ReplacementData[2];

CUtlVector<char>* GetFileStringWithReplacements( 
	const char *pInputFilename, 
	const char **ppReplacements, int nReplacements,
	int &dataWriteLen )
{
	Assert( nReplacements % 2 == 0 );
 
	// Read in the file data.
	FileHandle_t hFile = g_pFullFileSystem->Open( pInputFilename, "rb" );
	if ( !hFile )
	{
		return false;
	}
	g_FileData.SetSize( g_pFullFileSystem->Size( hFile ) );
	g_pFullFileSystem->Read( g_FileData.Base(), g_FileData.Count(), hFile );
	g_pFullFileSystem->Close( hFile );
	
	CUtlVector<char> *pCurData = &g_FileData;
	dataWriteLen = g_FileData.Count();
	if ( nReplacements )
	{
		// Null-terminate it.
		g_FileData.AddToTail( 0 );

		// Apply all the string substitutions.
		int iCurCount = g_FileData.Count() * 2;
		g_ReplacementData[0].EnsureCount( iCurCount );
		g_ReplacementData[1].EnsureCount( iCurCount );
		for ( int i=0; i < nReplacements/2; i++ )
		{
			for ( int iTestCount=0; iTestCount < 64; iTestCount++ )
			{
				if ( Q_StrSubst( pCurData->Base(), ppReplacements[i*2], ppReplacements[i*2+1], g_ReplacementData[i&1].Base(), g_ReplacementData[i&1].Count() ) )
					break;

				// Ok, we would overflow the string.. add more space to do the string substitution into.
				iCurCount += 2048;
				g_ReplacementData[0].EnsureCount( iCurCount );
				g_ReplacementData[1].EnsureCount( iCurCount );
			}
			pCurData = &g_ReplacementData[i&1];
			dataWriteLen = strlen( pCurData->Base() );
		}
	}
	
	return pCurData;
}


bool CopyWithReplacements( 
	const char *pInputFilename, 
	const char **ppReplacements, int nReplacements,
	const char *pOutputFilenameFormat, ... )
{
	int dataWriteLen;
	CUtlVector<char> *pCurData = GetFileStringWithReplacements( pInputFilename, ppReplacements, nReplacements, dataWriteLen );
	if ( !pCurData )
	{
		char msg[512];
		Q_snprintf( msg, sizeof( msg ), "Can't open %s for reading.", pInputFilename );
		::MessageBox( NULL, msg, "Error", MB_OK );
		return false;
	} 

	// Get the output filename.
	char outFilename[MAX_PATH];
	va_list marker;
	va_start( marker, pOutputFilenameFormat );
	Q_vsnprintf( outFilename, sizeof( outFilename ), pOutputFilenameFormat, marker );
	va_end( marker );

	// Write it out. I'd like to use IFileSystem, but Steam lowercases all filenames, which screws case-sensitive linux
	// (since the linux makefiles are tuned to the casing in Perforce).
	FILE *hFile = fopen( outFilename, "wb" );
	if ( !hFile )
	{
		char msg[512];
		Q_snprintf( msg, sizeof( msg ), "Can't open %s for writing.", outFilename );
		::MessageBox( NULL, msg, "Error", MB_OK );
		return false;
	}

	fwrite( pCurData->Base(), 1, dataWriteLen, hFile );
	fclose( hFile );
	return true;
}

int InitializeVGui()
{
	vgui::ivgui()->SetSleep(false);

	// find our configuration directory
	char szConfigDir[512];
	const char *steamPath = getenv("SteamInstallPath");
	if (steamPath)
	{
		// put the config dir directly under steam
		Q_snprintf(szConfigDir, sizeof(szConfigDir), "%s/config", steamPath);
	}
	else
	{
		// we're not running steam, so just put the config dir under the platform
		Q_strncpy( szConfigDir, "platform/config", sizeof(szConfigDir));
	}
	g_pFullFileSystem->CreateDirHierarchy("config", "PLATFORM");
	g_pFullFileSystem->AddSearchPath(szConfigDir, "CONFIG", PATH_ADD_TO_HEAD);

	// initialize the user configuration file
	vgui::system()->SetUserConfigFile("DedicatedServerDialogConfig.vdf", "CONFIG");

	// Init the surface
	vgui::Panel *pPanel = new vgui::Panel(NULL, "TopPanel");
	pPanel->SetVisible(true);

	vgui::surface()->SetEmbeddedPanel(pPanel->GetVPanel());

	// load the scheme
	vgui::scheme()->LoadSchemeFromFile("Resource/sdklauncher_scheme.res", NULL);
	
	// localization
	g_pVGuiLocalize->AddFile( "resource/platform_english.txt" );
	g_pVGuiLocalize->AddFile( "vgui/resource/vgui_english.txt" );
	g_pVGuiLocalize->AddFile( "sdklauncher_english.txt" );

	// Start vgui
	vgui::ivgui()->Start();

	// add our main window
	g_pMainFrame = new CSDKLauncherDialog(pPanel, "SDKLauncherDialog");

	// show main window
	g_pMainFrame->MoveToCenterOfScreen();
	g_pMainFrame->Activate();

	return 0;
}


void ShutdownVGui()
{
	delete g_pMainFrame;
}


KeyValues* LoadGameDirsFile()
{
	char filename[MAX_PATH];
	Q_snprintf( filename, sizeof( filename ), "%ssdklauncher_gamedirs.txt", gamedir );

	KeyValues *dataFile = new KeyValues("gamedirs");
	dataFile->UsesEscapeSequences( true );
	dataFile->LoadFromFile( g_pFullFileSystem, filename, NULL );
	return dataFile;
}


bool SaveGameDirsFile( KeyValues *pFile )
{
	char filename[MAX_PATH];
	Q_snprintf( filename, sizeof( filename ), "%ssdklauncher_gamedirs.txt", gamedir );
	return pFile->SaveToFile( g_pFullFileSystem, filename );
}



class CModalPreserveMessageBox : public vguiMessageBox
{
public:
	CModalPreserveMessageBox(const char *title, const char *text, vgui::Panel *parent)
		: vguiMessageBox( title, text, parent )
	{
		m_PrevAppFocusPanel = vgui::input()->GetAppModalSurface();
	}

	~CModalPreserveMessageBox()
	{
		vgui::input()->SetAppModalSurface( m_PrevAppFocusPanel );
	}


public:
	vgui::VPANEL m_PrevAppFocusPanel;
};
		


void VGUIMessageBox( vgui::Panel *pParent, const char *pTitle, const char *pMsg, ... )
{
	char msg[4096];
	va_list marker;
	va_start( marker, pMsg );
	Q_vsnprintf( msg, sizeof( msg ), pMsg, marker );
	va_end( marker );

	vguiMessageBox *dlg = new CModalPreserveMessageBox( pTitle, msg, pParent );
	dlg->DoModal();
	dlg->RequestFocus();
}

//-----------------------------------------------------------------------------
// Purpose: Startup our file watch
//-----------------------------------------------------------------------------
void UpdateConfigsStatus_Init( void )
{
	// Watch our config file for changes
	if ( g_dwChangeHandle == NULL)
	{
		char szConfigDir[MAX_PATH];
		Q_strncpy( szConfigDir, GetSDKLauncherBinDirectory(), sizeof( szConfigDir ) );
		Q_strncat ( szConfigDir, "\\", MAX_PATH );
		Q_strncat ( szConfigDir, g_engineDir, MAX_PATH );
		Q_strncat ( szConfigDir, "\\bin", MAX_PATH );

		g_dwChangeHandle = FindFirstChangeNotification( 
			szConfigDir,													// directory to watch 
			false,															// watch the subtree 
			(FILE_NOTIFY_CHANGE_DIR_NAME|FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_LAST_WRITE|FILE_NOTIFY_CHANGE_SIZE|FILE_NOTIFY_CHANGE_ATTRIBUTES));	// watch file and dir name changes 
 
		if ( g_dwChangeHandle == INVALID_HANDLE_VALUE )
		{
			// FIXME: Unable to watch the file
		}
	}
}	 

//-----------------------------------------------------------------------------
// Purpose: Update our status
//-----------------------------------------------------------------------------
void UpdateConfigsStatus( void )
{
 	// Wait for notification.
 	DWORD dwWaitStatus = WaitForSingleObject( g_dwChangeHandle, 0 );

	if ( dwWaitStatus == WAIT_OBJECT_0 )
	{
		// Something in the watched folder changed!
		if ( g_pMainFrame != NULL )
		{
			g_pMainFrame->RefreshConfigs();
		}
		
		// Start the next update
		if ( FindNextChangeNotification( g_dwChangeHandle ) == FALSE )
		{
			// This means that something unknown happened to our search handle!
			Assert( 0 );
			return;
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: Stop watching the file
//-----------------------------------------------------------------------------
void UpdateConfigsStatus_Shutdown( void )
{
	FindCloseChangeNotification( g_dwChangeHandle );
}

void QuickLaunchCommandLine( char *pCommandLine )
{
	STARTUPINFO si;
	memset( &si, 0, sizeof( si ) );
	si.cb = sizeof( si );

	PROCESS_INFORMATION pi;
	memset( &pi, 0, sizeof( pi ) );

	DWORD dwFlags = 0;
	
	if ( !CreateProcess( 
		0,
		pCommandLine, 
		NULL,							// security
		NULL,
		TRUE,
		dwFlags,						// flags
		NULL,							// environment
		GetSDKLauncherBaseDirectory(),	// current directory
		&si,
		&pi ) )
	{
		::MessageBoxA( NULL, GetLastWindowsErrorString(), "Error", MB_OK | MB_ICONINFORMATION | MB_APPLMODAL );
	}
}

bool RunQuickLaunch()
{
	char cmdLine[512];

	if ( CommandLine()->FindParm( "-runhammer" ) )
	{
		Q_snprintf( cmdLine, sizeof( cmdLine ), "\"%s\\%s\\bin\\hammer.exe\"", GetSDKLauncherBinDirectory(), g_engineDir );
		QuickLaunchCommandLine( cmdLine );
		return true;
	}
	else if ( CommandLine()->FindParm( "-runmodelviewer" ) )
	{
		Q_snprintf( cmdLine, sizeof( cmdLine ), "\"%s\\%s\\bin\\hlmv.exe\"", GetSDKLauncherBinDirectory(), g_engineDir );
		QuickLaunchCommandLine( cmdLine );
		return true;
	}
	else if ( CommandLine()->FindParm( "-runfaceposer" ) )
	{
		Q_snprintf( cmdLine, sizeof( cmdLine ), "\"%s\\%s\\bin\\hlfaceposer.exe\"", GetSDKLauncherBinDirectory(), g_engineDir );
		QuickLaunchCommandLine( cmdLine );
		return true;
	}
	
	return false;
}


void CheckCreateModParameters()
{
	if ( CommandLine()->FindParm( "-AutoHL2Mod" ) )
		g_bAutoHL2Mod = true;

	int iParm = CommandLine()->FindParm( "-CreateMod" );
	if ( iParm == 0 )
		return;

	if ( (iParm + 2) < CommandLine()->ParmCount() )
	{
		// Set it up so the mod wizard can skip the mod dir/mod name panel.
		g_bModWizard_CmdLineFields = true;
		Q_strncpy( g_ModWizard_CmdLine_ModDir, CommandLine()->GetParm( iParm + 1 ), sizeof( g_ModWizard_CmdLine_ModDir ) );
		Q_strncpy( g_ModWizard_CmdLine_ModName, CommandLine()->GetParm( iParm + 2 ), sizeof( g_ModWizard_CmdLine_ModName ) );

		RunCreateModWizard( true );
	}
}


//-----------------------------------------------------------------------------
// The application object
//-----------------------------------------------------------------------------
class CSDKLauncherApp : public CVguiSteamApp
{
	typedef CVguiSteamApp BaseClass;

public:
	// Methods of IApplication
	virtual bool Create();
	virtual bool PreInit();
	virtual int Main();
	virtual void PostShutdown();
	virtual void Destroy() {}
};

DEFINE_WINDOWED_STEAM_APPLICATION_OBJECT( CSDKLauncherApp );


//-----------------------------------------------------------------------------
// The application object
//-----------------------------------------------------------------------------
bool CSDKLauncherApp::Create()
{
	SpewOutputFunc( SDKLauncherSpewOutputFunc );

	AppSystemInfo_t appSystems[] = 
	{
		{ "inputsystem.dll",		INPUTSYSTEM_INTERFACE_VERSION },
		{ "vgui2.dll",				VGUI_IVGUI_INTERFACE_VERSION },
		{ "", "" }	// Required to terminate the list
	};

	return AddSystems( appSystems );
}


//-----------------------------------------------------------------------------
// Purpose: Entry point
//-----------------------------------------------------------------------------
bool CSDKLauncherApp::PreInit()
{
	if ( !BaseClass::PreInit() )
		return false;

	// Make sure we're using the proper environment variable
	ConvertObsoleteVConfigRegistrySetting( GAMEDIR_TOKEN );

	if ( !CommandLine()->ParmValue( "-game" ) )
	{
		Error( "SDKLauncher requires -game on the command line." );
		return false;
	}

	// winsock aware
	WSAData wsaData;
	WSAStartup( MAKEWORD(2,0), &wsaData );

	// Create a window to capture messages
	CreateMessageWindow();

	FileSystem_SetErrorMode( FS_ERRORMODE_AUTO );

	if ( !BaseClass::SetupSearchPaths( NULL, false, true ) )
	{
		::MessageBox( NULL, "Error", "Unable to initialize file system\n", MB_OK );
		return false;
	}

	// Set gamedir.
	Q_MakeAbsolutePath( gamedir, sizeof( gamedir ), GetGameInfoPath() );
	Q_AppendSlash( gamedir, sizeof( gamedir ) );

	// the "base dir" so we can scan mod name
	g_pFullFileSystem->AddSearchPath(GetSDKLauncherBaseDirectory(), SDKLAUNCHER_MAIN_PATH_ID);	
	// the main platform dir
	g_pFullFileSystem->AddSearchPath("platform","PLATFORM", PATH_ADD_TO_HEAD);

	return true;
}

void CSDKLauncherApp::PostShutdown()
{
	// Stop our message window
	ShutdownMessageWindow();
	::WSACleanup();

	BaseClass::PostShutdown();
}


//-----------------------------------------------------------------------------
// Purpose: Entry point
//-----------------------------------------------------------------------------
int CSDKLauncherApp::Main()
{
	SetVConfigRegistrySetting( "sourcesdk", GetSDKLauncherBaseDirectory() );

	// If they just want to run Hammer or hlmv, just do that and exit.
	if ( RunQuickLaunch() )
		return 1;
	
	// Run app frame loop
	int ret = InitializeVGui();
	if ( ret != 0 )
		return ret;

	DumpMinFootprintFiles( false );

	SteamAPI_InitSafe();
	SteamAPI_SetTryCatchCallbacks( false ); // We don't use exceptions, so tell steam not to use try/catch in callback handlers
	g_SteamAPIContext.Init();
	
	// Start looking for file updates
//	UpdateConfigsStatus_Init();

	// Check if they want to run the Create Mod wizard right off the bat.
	CheckCreateModParameters();

	while ( vgui::ivgui()->IsRunning() && !g_bAppQuit )
	{
		Sleep( 10 );
//		UpdateConfigsStatus();
		vgui::ivgui()->RunFrame();
	}
	
	ShutdownVGui();

//	UpdateConfigsStatus_Shutdown();

	return 1;
}