//========= Copyright Valve Corporation, All rights reserved. ============//
//
//	EXCLUDE_PATHS.CPP
//
//	DVD Exclude paths.
//=====================================================================================//
#include "vxconsole.h"

#define UM_CHECKSTATECHANGE		(WM_USER + 100)
#define UM_FIRSTTIMEPOPULATE	(WM_USER + 101)

#define MAX_TREE_DEPTH		32	
#define ROOT_NAME			"Xbox:\\"
#define EXCLUDEPATHS_FILE	"xbox_exclude_paths.txt"

// known shipping 360 gamedirs
struct GameName_t
{
	const char *pName;
	bool		bIsModPath;
};

struct GamePath_t
{
	CUtlString	pathName;
	bool		bIsModPath;
};

GameName_t g_GameNames[] = 
{
	{ "bin",		false },
	{ "platform",	false },
	{ "tf",			true },
	{ "portal",		true },
	{ "hl2",		true },
	{ "episodic",	true },
	{ "ep2",		true }
};

CUtlVector< CUtlString >	g_ExcludePaths;
BOOL						g_bLinkGameDirs;

inline bool PathLessThan( GamePath_t const &lhs, GamePath_t const &rhs )
{
	if ( lhs.bIsModPath != rhs.bIsModPath )
	{
		// sort mod paths to the bottom
		return ( lhs.bIsModPath == false );
	}

	return ( stricmp( lhs.pathName.String(), rhs.pathName.String() ) < 0 ); 
}
CUtlRBTree< GamePath_t > g_PathTable( 0, 0, PathLessThan );

//-----------------------------------------------------------------------------
// Add string to TreeView at specified level.  Assumes an inorder insert.
// A node (at specified depth) must be inserted before it's children.
//-----------------------------------------------------------------------------
static HTREEITEM AddItemToTree( HWND hWndTree, LPSTR lpszItem, int nLevel, bool bIsModPath )
{ 
	TVITEM tvi = { 0 }; 
	TVINSERTSTRUCT tvins = { 0 }; 
	static HTREEITEM s_hPrevItems[MAX_TREE_DEPTH];

	if ( nLevel < 0 || nLevel >= MAX_TREE_DEPTH )
	{
		Assert( 0 );
		return NULL;
	}

	HTREEITEM hParent;
	HTREEITEM hPrev; 
	if ( !nLevel )
	{
		// one root item only, reset
		for ( int i = 0; i < ARRAYSIZE( s_hPrevItems ); i++ )
		{
			s_hPrevItems[i] = NULL;
		}
		hParent = TVI_ROOT;
		hPrev = (HTREEITEM)TVI_FIRST;
	}
	else
	{
		hParent = s_hPrevItems[nLevel-1];
		hPrev = s_hPrevItems[nLevel];
		if ( !hParent )
		{
			// parent should have been setup
			Assert( 0 );
			return NULL;
		}
		if ( !hPrev )
		{
			hPrev = TVI_FIRST;
		}
	}

	tvi.mask = TVIF_TEXT | TVIF_PARAM; 
	tvi.pszText = lpszItem; 
	tvi.cchTextMax = sizeof(tvi.pszText)/sizeof(tvi.pszText[0]); 
	tvi.lParam = (LPARAM)MAKELONG( nLevel, bIsModPath );

	tvins.item = tvi; 
	tvins.hParent = hParent;
	tvins.hInsertAfter = hPrev; 

	// Add the item to the tree-view control. 
	hPrev = TreeView_InsertItem( hWndTree, &tvins ); 
	s_hPrevItems[nLevel] = hPrev;

	if ( nLevel > 0 )
	{
		// set a back link to its parent
		tvi.mask = TVIF_HANDLE;
		tvi.hItem = TreeView_GetParent( hWndTree, hPrev ); 
		TreeView_SetItem( hWndTree, &tvi ); 
	} 

	return hPrev; 
} 

//-----------------------------------------------------------------------------
// Purpose: Get list of files from current path that match pattern
//-----------------------------------------------------------------------------
static int GetFileList( const char* pDirPath, const char* pPattern, CUtlVector< CUtlString > &fileList )
{
	char	sourcePath[MAX_PATH];
	char	fullPath[MAX_PATH];
	bool	bFindDirs;

	fileList.Purge();

	strcpy( sourcePath, pDirPath );
	int len = (int)strlen( sourcePath );
	if ( !len )
	{
		strcpy( sourcePath, ".\\" );
	}
	else if ( sourcePath[len-1] != '\\' )
	{
		sourcePath[len]   = '\\';
		sourcePath[len+1] = '\0';
	}

	strcpy( fullPath, sourcePath );
	if ( pPattern[0] == '\\' && pPattern[1] == '\0' )
	{
		// find directories only
		bFindDirs = true;
		strcat( fullPath, "*" );
	}
	else
	{
		// find files, use provided pattern
		bFindDirs = false;
		strcat( fullPath, pPattern );
	}

	struct _finddata_t findData;
	intptr_t h = _findfirst( fullPath, &findData );
	if ( h == -1 )
	{
		return 0;
	}

	do
	{
		// dos attribute complexities i.e. _A_NORMAL is 0
		if ( bFindDirs )
		{
			// skip non dirs
			if ( !( findData.attrib & _A_SUBDIR ) )
				continue;
		}
		else
		{
			// skip dirs
			if ( findData.attrib & _A_SUBDIR )
				continue;
		}

		if ( !stricmp( findData.name, "." ) )
			continue;

		if ( !stricmp( findData.name, ".." ) )
			continue;

		char fileName[MAX_PATH];
		strcpy( fileName, sourcePath );
		strcat( fileName, findData.name );

		int j = fileList.AddToTail();
		fileList[j].Set( fileName );
	}
	while ( !_findnext( h, &findData ) );

	_findclose( h );

	return fileList.Count();
}

//-----------------------------------------------------------------------------
// Purpose: Recursively determine directory tree
//-----------------------------------------------------------------------------
static void RecurseFileTree_r( const char *pBasePath, const char *pDirPath, int depth, CUtlVector< CUtlString > &dirList, bool bIsModPath )
{
	if ( depth >= 2 )
	{
		// too much unecessary detail
		return;
	}

	// ignore path roots, only interested in subdirs
	const char *pSubName = pDirPath + strlen( pBasePath );
	if ( pSubName[0] )
	{
		GamePath_t gamePath;
		gamePath.pathName = pSubName;
		gamePath.bIsModPath = bIsModPath;
		
		int iIndex = g_PathTable.Find( gamePath );
		if ( iIndex == g_PathTable.InvalidIndex() )
		{
			g_PathTable.Insert( gamePath );
		}
	}

	// recurse from source directory, get directories only
	CUtlVector< CUtlString > fileList;
	int dirCount = GetFileList( pDirPath, "\\", fileList );
	if ( !dirCount )
	{
		// add directory name to search tree
		int j = dirList.AddToTail();
		dirList[j].Set( pDirPath );
		return;
	}

	for ( int i=0; i<dirCount; i++ )
	{
		// form new path name, recurse into
		RecurseFileTree_r( pBasePath, fileList[i].String(), depth+1, dirList, bIsModPath );
	}

	int j = dirList.AddToTail();
	dirList[j].Set( pDirPath );
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_SetCheckState_r
//
//	Propogate the check state to all children
//-----------------------------------------------------------------------------
void ExcludePathsDlg_SetCheckState_r( HWND hWndTree, HTREEITEM hTree, int depth, int checkState )
{
	if ( !hTree )
	{
		return;
	}

	TreeView_SetCheckState( hWndTree, hTree, ( checkState == 1 ) );

	TVITEM tvi = { 0 };
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN;
	tvi.hItem = hTree;
	if ( TreeView_GetItem( hWndTree, &tvi ) )
	{
		if ( tvi.cChildren )
		{
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
			if ( hChild )
			{
				ExcludePathsDlg_SetCheckState_r( hWndTree, hChild, depth+1, checkState );
			}
		}
	}
	else
	{
		return;
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return;
	}

	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			return;
		}

		TreeView_SetCheckState( hWndTree, hSibling, ( checkState == 1 ) );

		tvi.hItem = hSibling;
		if ( TreeView_GetItem( hWndTree, &tvi ) )
		{
			if ( tvi.cChildren )
			{
				HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
				if ( hChild )
				{
					ExcludePathsDlg_SetCheckState_r( hWndTree, hChild, depth+1, checkState );
				}
			}
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_SetCheckStateLinked_r
//
//	Propogate the check state to all "mod path" children that match the specified name.
//	A NULL name matches all.
//-----------------------------------------------------------------------------
void ExcludePathsDlg_SetCheckStateLinked_r( HWND hWndTree, HTREEITEM hTree, int depth, int checkState, const char *pName )
{
	if ( !hTree )
	{
		return;
	}

	char szNodeName[MAX_PATH];
	TVITEM tvi = { 0 };
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN | TVIF_TEXT | TVIF_PARAM;
	tvi.hItem = hTree;
	tvi.pszText = szNodeName;
	tvi.cchTextMax = sizeof( szNodeName );
	if ( TreeView_GetItem( hWndTree, &tvi ) )
	{
		bool bIsModPath = HIWORD( tvi.lParam ) != 0;
		if ( bIsModPath && ( !pName || !V_stricmp( szNodeName, pName ) ) )
		{
			TreeView_SetCheckState( hWndTree, hTree, ( checkState == 1 ) );
		}

		if ( tvi.cChildren )
		{
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
			if ( hChild )
			{
				ExcludePathsDlg_SetCheckStateLinked_r( hWndTree, hChild, depth+1, checkState, pName );
			}
		}
	}
	else
	{
		return;
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return;
	}

	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			return;
		}

		tvi.hItem = hSibling;
		if ( TreeView_GetItem( hWndTree, &tvi ) )
		{
			bool bIsModPath = HIWORD( tvi.lParam ) != 0;
			if ( bIsModPath && ( !pName || !V_stricmp( szNodeName, pName ) ) )
			{
				TreeView_SetCheckState( hWndTree, hSibling, ( checkState == 1 ) );
			}

			if ( tvi.cChildren )
			{
				HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
				if ( hChild )
				{
					ExcludePathsDlg_SetCheckStateLinked_r( hWndTree, hChild, depth+1, checkState, pName );
				}
			}
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_GetCheckState_r
//
//	Caller invokes at node. Returns with state set.
//	State 0: unchecked, 1: checked, -1:unknown.
//	Returns true if All children match, false otherwise.
//-----------------------------------------------------------------------------
bool ExcludePathsDlg_GetCheckState_r( HWND hWndTree, HTREEITEM hTree, int depth, int *pCheckState )
{
	int checkState;

	checkState = TreeView_GetCheckState( hWndTree, hTree );
	if ( *pCheckState == -1 )
	{
		*pCheckState = checkState;
	}
	else if ( *pCheckState != checkState )
	{
		// disparate state, no need to recurse
		return false;
	}

	TVITEM tvi = { 0 };
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN;
	tvi.hItem = hTree;
	if ( TreeView_GetItem( hWndTree, &tvi ) )
	{
		if ( tvi.cChildren )
		{
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
			if ( hChild )
			{
				if ( !ExcludePathsDlg_GetCheckState_r( hWndTree, hChild, depth+1, pCheckState ) )
				{
					return false;
				}
			}
		}
	}
	else
	{
		return false;
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return true;
	}

	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			break;
		}

		checkState = TreeView_GetCheckState( hWndTree, hSibling );
		if ( *pCheckState != checkState )
		{
			// disparate state, no need to recurse
			return false;
		}

		tvi.hItem = hSibling;
		if ( TreeView_GetItem( hWndTree, &tvi ) )
		{
			if ( tvi.cChildren )
			{
				HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
				if ( hChild )
				{
					if ( !ExcludePathsDlg_GetCheckState_r( hWndTree, hChild, depth+1, pCheckState ) )
					{
						return false;
					}
				}
			}
		}
	}

	return true;
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Expand_r
//
//	MaxDepth >= 0, Expand/Collapse up to specified depth.
//-----------------------------------------------------------------------------
void ExcludePathsDlg_Expand_r( HWND hWndTree, HTREEITEM hTree, int depth, int maxDepth, bool bExpand )
{
	if ( !hTree )
	{
		return;
	}

	if ( maxDepth >= 0 && depth >= maxDepth )
	{
		return;
	}

	int flags;
	if ( bExpand )
		flags = TVE_EXPAND;
	else
		flags = TVE_COLLAPSE;

	TVITEM tvi = { 0 };
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN;
	tvi.hItem = hTree;
	if ( TreeView_GetItem( hWndTree, &tvi ) )
	{
		if ( tvi.cChildren )
		{
			TreeView_Expand( hWndTree, hTree, flags );
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
			if ( hChild )
			{
				ExcludePathsDlg_Expand_r( hWndTree, hChild, depth+1, maxDepth, bExpand );
			}
		}
	}
	else
	{
		return;
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return;
	}

	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			return;
		}
		
		tvi.hItem = hSibling;
		if ( TreeView_GetItem( hWndTree, &tvi ) )
		{
			if ( tvi.cChildren )
			{
				TreeView_Expand( hWndTree, hSibling, flags );
				HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
				if ( hChild )
				{
					ExcludePathsDlg_Expand_r( hWndTree, hChild, depth+1, maxDepth, bExpand );
				}
			}
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_ExpandToShowState_r
//
//-----------------------------------------------------------------------------
void ExcludePathsDlg_ExpandToShowState_r( HWND hWndTree, HTREEITEM hTree, int depth )
{
	if ( !hTree )
	{
		return;
	}

	TVITEM tvi = { 0 };
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN;
	tvi.hItem = hTree;
	if ( TreeView_GetItem( hWndTree, &tvi ) )
	{
		int checkState = -1;
		bool bStateIsSame = ExcludePathsDlg_GetCheckState_r( hWndTree, hTree, 0, &checkState );

		if ( tvi.cChildren && !bStateIsSame )
		{
			TreeView_Expand( hWndTree, hTree, TVE_EXPAND );
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
			if ( hChild )
			{
				ExcludePathsDlg_ExpandToShowState_r( hWndTree, hChild, depth+1 );
			}
		}
	}
	else
	{
		return;
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return;
	}

	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			return;
		}

		tvi.hItem = hSibling;
		if ( TreeView_GetItem( hWndTree, &tvi ) )
		{
			int checkState = -1;
			bool bStateIsSame = ExcludePathsDlg_GetCheckState_r( hWndTree, hSibling, 0, &checkState );

			if ( tvi.cChildren && !bStateIsSame )
			{
				TreeView_Expand( hWndTree, hSibling, TVE_EXPAND );
				HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
				if ( hChild )
				{
					ExcludePathsDlg_ExpandToShowState_r( hWndTree, hChild, depth+1 );
				}
			}
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Find_r
// 
//-----------------------------------------------------------------------------
HTREEITEM ExcludePathsDlg_Find_r( HWND hWndTree, HTREEITEM hTree, int depth, const char *pBasePath, const char *pFindPath )
{
	TVITEM tvi = { 0 };
	char szName[MAX_PATH];
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN | TVIF_TEXT;
	tvi.hItem = hTree;
	tvi.pszText = szName;
	tvi.cchTextMax = sizeof( szName );
	if ( !TreeView_GetItem( hWndTree, &tvi ) )
	{
		return NULL;
	}

	char szPath[MAX_PATH];
	V_ComposeFileName( pBasePath, szName, szPath, sizeof( szPath ) );
	if ( !stricmp( szPath, pFindPath ) )
	{
		return hTree;
	}
	
	if ( tvi.cChildren )
	{
		HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
		if ( hChild )
		{
			HTREEITEM hFindTree = ExcludePathsDlg_Find_r( hWndTree, hChild, depth+1, szPath, pFindPath );
			if ( hFindTree )
			{
				return hFindTree;
			}
		}
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return NULL;
	}

	// iterate siblings
	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			break;
		}

		tvi.hItem = hSibling;
		if ( !TreeView_GetItem( hWndTree, &tvi ) )
		{
			break;
		}

		V_ComposeFileName( pBasePath, szName, szPath, sizeof( szPath ) );
		if ( !stricmp( szPath, pFindPath ) )
		{
			return hSibling;
		}

		if ( tvi.cChildren )
		{
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
			if ( hChild )
			{
				HTREEITEM hFindTree = ExcludePathsDlg_Find_r( hWndTree, hChild, depth+1, szPath, pFindPath );
				if ( hFindTree )
				{
					return hFindTree;
				}
			}
		}
	}

	return NULL;
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_BuildExcludeList_r
//
//	An exclude path represents the path head, and thus does not need to iterate its children
//	if they are deemed to the same exclusion state.
//-----------------------------------------------------------------------------
void ExcludePathsDlg_BuildExcludeList_r( HWND hWndTree, HTREEITEM hTree, int depth, const char *pPath )
{
	int checkState = -1;
	bool bStateIsSame = ExcludePathsDlg_GetCheckState_r( hWndTree, hTree, 0, &checkState );
	if ( checkState == -1 )
	{
		return;
	}

	TVITEM tvi = { 0 };
	char szName[MAX_PATH];
	tvi.mask = TVIF_HANDLE | TVIF_CHILDREN | TVIF_TEXT;
	tvi.hItem = hTree;
	tvi.pszText = szName;
	tvi.cchTextMax = sizeof( szName );
	if ( !TreeView_GetItem( hWndTree, &tvi ) )
	{
		return;
	}

	char szPath[MAX_PATH];
	V_ComposeFileName( pPath, szName, szPath, sizeof( szPath ) );

	if ( checkState == 1 && ( bStateIsSame || !tvi.cChildren ) )
	{
		// add to exclude list
		g_ExcludePaths.AddToTail( szPath );
	}

	if ( !bStateIsSame && tvi.cChildren )
	{
		// mixed states, must recurse to resolve
		HTREEITEM hChild = TreeView_GetChild( hWndTree, hTree );
		if ( hChild )
		{
			ExcludePathsDlg_BuildExcludeList_r( hWndTree, hChild, depth+1, szPath );
		}
	}

	if ( !depth )
	{
		// only iterate siblings of the parent's child
		return;
	}

	// iterate siblings
	HTREEITEM hSibling = hTree;
	while ( 1 )
	{
		hSibling = TreeView_GetNextSibling( hWndTree, hSibling );
		if ( !hSibling )
		{
			break;
		}

		checkState = -1;
		bStateIsSame = ExcludePathsDlg_GetCheckState_r( hWndTree, hSibling, 0, &checkState );
		if ( checkState == -1 )
		{
			break;
		}

		tvi.hItem = hSibling;
		if ( !TreeView_GetItem( hWndTree, &tvi ) )
		{
			break;
		}

		V_ComposeFileName( pPath, szName, szPath, sizeof( szPath ) );

		if ( checkState == 1 && ( bStateIsSame || !tvi.cChildren ) )
		{
			// add to exclude list
			g_ExcludePaths.AddToTail( szPath );
		}

		if ( !bStateIsSame && tvi.cChildren )
		{
			HTREEITEM hChild = TreeView_GetChild( hWndTree, hSibling );
			if ( hChild )
			{
				ExcludePathsDlg_BuildExcludeList_r( hWndTree, hChild, depth+1, szPath );
			}
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_SaveChanges
//
//-----------------------------------------------------------------------------
void ExcludePathsDlg_SaveChanges( HWND hWnd )
{
	g_ExcludePaths.Purge();
	HWND hWndTree = GetDlgItem( hWnd, IDC_PATHS_TREE );
	ExcludePathsDlg_BuildExcludeList_r( hWndTree, TreeView_GetRoot( hWndTree ), 0, "" );

	char szPath[MAX_PATH];
	V_ComposeFileName( g_localPath, EXCLUDEPATHS_FILE, szPath, sizeof( szPath ) );

	if ( !g_ExcludePaths.Count() )
	{
		// no exclude paths
		unlink( szPath );
	}
	else
	{
		FILE *fp = fopen( szPath, "wt" );
		if ( !fp )
		{
			Sys_MessageBox( "Error", "Could not open '%s' for writing\n", szPath );
			return;
		}

		fprintf( fp, "// Auto-Generated by VXConsole - DO NOT MODIFY!\n" );
		for ( int i = 0; i < g_ExcludePaths.Count(); i++ )
		{
			// strip expected root path
			const char *pPath = g_ExcludePaths[i].String();
			pPath += strlen( ROOT_NAME );
			if ( !pPath[0] )
			{
				// special code for root
				fprintf( fp, "*\n" );
				break;
			}
			fprintf( fp, "\"\\%s\"\n", pPath );
		}
		fclose( fp );
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Populate
//
//	Generate a path table.
//-----------------------------------------------------------------------------
void ExcludePathsDlg_Populate( HWND hWnd, bool bRefresh )
{
	struct GamePath_t 
	{
		CUtlString	pathName;
		bool		bIsModPath;
	};

	CUtlVector< GamePath_t > gamePaths;		

	// can skip the time consuming directory scan, unless forced
	if ( bRefresh || !g_PathTable.Count() )
	{
		g_PathTable.Purge();

		for ( int i = 0; i < ARRAYSIZE( g_GameNames ); i++ )
		{
			char szTargetPath[MAX_PATH];
			V_strncpy( szTargetPath, g_localPath, sizeof( szTargetPath ) );
			V_AppendSlash( szTargetPath, sizeof( szTargetPath ) );
			V_strncat( szTargetPath, g_GameNames[i].pName, sizeof( szTargetPath ) );

			GamePath_t gamePath;
			gamePath.pathName = szTargetPath;
			gamePath.bIsModPath = g_GameNames[i].bIsModPath;
			gamePaths.AddToTail( gamePath );
		}

		// iterate all game paths
		for ( int i = 0; i < gamePaths.Count(); i++ )
		{
			CUtlVector< CUtlString > dirList;
			RecurseFileTree_r( g_localPath, gamePaths[i].pathName.String(), 0, dirList, gamePaths[i].bIsModPath );
		}
	}

	// reset the tree
	HWND hWndTree = GetDlgItem( hWnd, IDC_PATHS_TREE );
	TreeView_DeleteAllItems( hWndTree );

	// reset and add root
	// only the root is at depth 0
	AddItemToTree( hWndTree, ROOT_NAME, 0, false );

	// build the tree view
	for ( int iIndex = g_PathTable.FirstInorder(); iIndex != g_PathTable.InvalidIndex(); iIndex = g_PathTable.NextInorder( iIndex ) )
	{
		// due to sorting, number of slashes is depth
		const char *pString = g_PathTable[iIndex].pathName.String();
		int depth = 0;
		for ( int j = 0; j < (int)strlen( pString ); j++ )
		{
			if ( pString[j] == '\\' )
			{
				depth++;
			}
		}
		if ( !depth )
		{
			depth = 1;
		}

		char szPath[MAX_PATH];
		V_FileBase( pString, szPath, sizeof( szPath ) );
		AddItemToTree( hWndTree, szPath, depth, g_PathTable[iIndex].bIsModPath );
	}

	HTREEITEM hTreeRoot = TreeView_GetRoot( hWndTree );
	for ( int i = 0; i < g_ExcludePaths.Count(); i++ )
	{
		HTREEITEM hTree = ExcludePathsDlg_Find_r( hWndTree, hTreeRoot, 0, "", g_ExcludePaths[i].String() );
		if ( hTree )
		{
			ExcludePathsDlg_SetCheckState_r( hWndTree, hTree, 0, true );
		}
	}

	// expand the root node to show state
	ExcludePathsDlg_ExpandToShowState_r( hWndTree, hTreeRoot, 0 );
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Setup
//
//-----------------------------------------------------------------------------
void ExcludePathsDlg_Setup( HWND hWnd )
{
	TreeView_SetBkColor( GetDlgItem( hWnd, IDC_PATHS_TREE ), g_backgroundColor );

	CheckDlgButton( hWnd, IDC_PATHS_LINKGAMEDIRS, g_bLinkGameDirs ? BST_CHECKED : BST_UNCHECKED );

	// read the exisiting exclude paths
	g_ExcludePaths.Purge();
	char szFilename[MAX_PATH];
	V_ComposeFileName( g_localPath, EXCLUDEPATHS_FILE, szFilename, sizeof( szFilename ) );
	if ( Sys_Exists( szFilename ) )
	{
		Sys_LoadScriptFile( szFilename );
		while ( 1 ) 
		{
			char *pToken = Sys_GetToken( true );
			if ( !pToken || !pToken[0] )
			{
				break;
			}
			Sys_StripQuotesFromToken( pToken );
			if ( !stricmp( pToken, "*" ) )
			{
				pToken = "";
			}
			else if ( pToken[0] == '\\' )
			{
				pToken++;
			}
		
			char szPath[MAX_PATH];
			V_ComposeFileName( ROOT_NAME, pToken, szPath, sizeof( szPath ) );
			V_FixSlashes( szPath );

			g_ExcludePaths.AddToTail( szPath );
		}
	}
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Proc
//
//-----------------------------------------------------------------------------
BOOL CALLBACK ExcludePathsDlg_Proc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) 
{
	HWND hWndTree; 
	LPNMHDR lpnmh;
	HTREEITEM hTree;
	HTREEITEM hTreeRoot;
	int checkState;
	static bool s_bAllowPopulate = 0;

	switch ( message ) 
	{ 
	case WM_INITDIALOG:
		ExcludePathsDlg_Setup( hWnd );
		s_bAllowPopulate = true;
		return TRUE;

	case WM_NOTIFY: 
		lpnmh = (LPNMHDR)lParam;
		if ( ( lpnmh->code  == NM_CLICK ) && ( lpnmh->idFrom == IDC_PATHS_TREE ) )
		{
			TVHITTESTINFO ht = {0};
			DWORD dwpos = GetMessagePos();
			ht.pt.x = GET_X_LPARAM( dwpos );
			ht.pt.y = GET_Y_LPARAM( dwpos );
			MapWindowPoints( HWND_DESKTOP, lpnmh->hwndFrom, &ht.pt, 1 );
			TreeView_HitTest( lpnmh->hwndFrom, &ht );
			if ( ht.flags & TVHT_ONITEMSTATEICON )
			{
				PostMessage( hWnd, UM_CHECKSTATECHANGE, 0, (LPARAM)ht.hItem );
			}
		}
		break; 

	case UM_CHECKSTATECHANGE:
		hWndTree = GetDlgItem( hWnd, IDC_PATHS_TREE );
		hTreeRoot = TreeView_GetRoot( hWndTree );
		hTree = (HTREEITEM)lParam;
		checkState = TreeView_GetCheckState( hWndTree, hTree );
		if ( checkState != -1 )
		{
			TVITEM tvi = { 0 };
			char szName[MAX_PATH];
			tvi.mask = TVIF_HANDLE | TVIF_PARAM | TVIF_TEXT;
			tvi.hItem = hTree;
			tvi.pszText = szName;
			tvi.cchTextMax = sizeof( szName );
			if ( !TreeView_GetItem( hWndTree, &tvi ) )
			{
				break;
			}
			int nDepth = LOWORD( tvi.lParam );
			bool bIsModPath = HIWORD( tvi.lParam ) != 0;

			if ( g_bLinkGameDirs && bIsModPath )
			{
				// a mod path root is at depth 1
				// a child of a mod path (depth > 1), will match all other children in mod paths
				// a mod path (depth = 1) will match all other modpaths
				ExcludePathsDlg_SetCheckStateLinked_r( hWndTree, hTreeRoot, 0, checkState, nDepth == 1 ? NULL : szName );
			}
			else
			{
				ExcludePathsDlg_SetCheckState_r( hWndTree, hTree, 0, checkState );
			}
		}
		break;

	case WM_PAINT:
		if ( s_bAllowPopulate )
		{
			// unfortunate, but tree view needs to paint first before its state can be set
			// related to checkbox style which doesn't manifest until after WM_INITDIALOG
			// stall the initial population until after the first paint
			s_bAllowPopulate = false;
			PostMessage( hWnd, UM_FIRSTTIMEPOPULATE, 0, 0 );
		}
		break;

	case UM_FIRSTTIMEPOPULATE:
		ExcludePathsDlg_Populate( hWnd, false );
		break;

	case WM_COMMAND: 
		switch ( LOWORD( wParam ) ) 
		{
		case IDC_OK: 
			ExcludePathsDlg_SaveChanges( hWnd );
			EndDialog( hWnd, wParam );
			return TRUE; 

		case IDC_PATHS_RESCAN:
			ExcludePathsDlg_Populate( hWnd, true );
			return TRUE;

		case IDC_PATHS_EXPAND:
			// expand from root
			hWndTree = GetDlgItem( hWnd, IDC_PATHS_TREE );
			ExcludePathsDlg_Expand_r( hWndTree, TreeView_GetRoot( hWndTree ), 0, -1, true );
			return TRUE;

		case IDC_PATHS_COLLAPSE:
			// collapse from root
			hWndTree = GetDlgItem( hWnd, IDC_PATHS_TREE );
			ExcludePathsDlg_Expand_r( hWndTree, TreeView_GetRoot( hWndTree ), 0, -1, false );
			return TRUE;

		case IDC_PATHS_LINKGAMEDIRS:
			g_bLinkGameDirs = IsDlgButtonChecked( hWnd, IDC_PATHS_LINKGAMEDIRS );
			return TRUE;

		case IDCANCEL:
		case IDC_CANCEL: 
			EndDialog( hWnd, wParam );
			return TRUE; 
		}
		break; 
	}
	return FALSE; 
} 

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_LoadConfig	
// 
//-----------------------------------------------------------------------------
void ExcludePathsDlg_LoadConfig()
{
	// get our config
	Sys_GetRegistryInteger( "ExcludePaths_LinkGameDirs", true, g_bLinkGameDirs );
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_SaveConfig
// 
//-----------------------------------------------------------------------------
void ExcludePathsDlg_SaveConfig()
{
	Sys_SetRegistryInteger( "ExcludePaths_LinkGameDirs", g_bLinkGameDirs );
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Init
// 
//-----------------------------------------------------------------------------
bool ExcludePathsDlg_Init()
{
	return true;
}

//-----------------------------------------------------------------------------
//	ExcludePathsDlg_Open	
// 
//-----------------------------------------------------------------------------
void ExcludePathsDlg_Open()
{
	ExcludePathsDlg_LoadConfig();

	int result = DialogBox( g_hInstance, MAKEINTRESOURCE( IDD_EXCLUDEPATHS ), g_hDlgMain, ( DLGPROC )ExcludePathsDlg_Proc );
	if ( LOWORD( result ) != IDC_OK )
		return;

	ExcludePathsDlg_SaveConfig();
}