//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
//--------------------------------------------------------------------------------------------------------------
// download.cpp
// 
// Implementation file for optional HTTP asset downloading
// Author: Matthew D. Campbell (matt@turtlerockstudios.com), 2004
//--------------------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------------------
// Includes
//--------------------------------------------------------------------------------------------------------------

// fopen is needed for the bzip code
#undef fopen

#if defined( WIN32 ) && !defined( _X360 )
#include "winlite.h"
#include <WinInet.h>
#endif

#include <assert.h>

#include "download.h"
#include "tier0/platform.h"
#include "download_internal.h"

#include "client.h"
#include "net_chan.h"

#include <KeyValues.h>
#include "filesystem.h"
#include "filesystem_engine.h"
#include "server.h"
#include "vgui_baseui_interface.h"
#include "tier0/vcrmode.h"
#include "cdll_engine_int.h"

#include "../utils/bzip2/bzlib.h"

#if defined( _X360 )
#include "xbox/xbox_win32stubs.h"
#endif

#include "engine/idownloadsystem.h"

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

extern IFileSystem *g_pFileSystem;
static const char *CacheDirectory = "cache";
static const char *CacheFilename = "cache/DownloadCache.db";
Color DownloadColor			(   0, 200, 100, 255 );
Color DownloadErrorColor	( 200, 100, 100, 255 );
Color DownloadCompleteColor	( 100, 200, 100, 255 );

ConVar download_debug( "download_debug", "0", FCVAR_DONTRECORD );	// For debug printouts

const char k_szDownloadPathID[] = "download";

//--------------------------------------------------------------------------------------------------------------
// Class Definitions
//--------------------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------------------
// Purpose: Implements download cache manager
//--------------------------------------------------------------------------------------------------------------
class DownloadCache
{
public:
	DownloadCache();
	~DownloadCache();
	void Init();

	void GetCachedData( RequestContext_t *rc );			///< Loads cached data, if any
	void PersistToDisk( const RequestContext_t *rc );		///< Writes out a completed download to disk
	void PersistToCache( const RequestContext_t *rc );	///< Writes out a partial download (lost connection, user abort, etc) to cache

private:
	KeyValues *m_cache;

	void GetCacheFilename( const RequestContext_t *rc, char cachePath[_MAX_PATH] );
	void GenerateCacheFilename( const RequestContext_t *rc, char cachePath[_MAX_PATH] );

	void BuildKeyNames( const char *gamePath );			///< Convenience function to build the keys to index into m_cache
	char m_cachefileKey[BufferSize + 64];
	char m_timestampKey[BufferSize + 64];
};
static DownloadCache *TheDownloadCache = NULL;

//--------------------------------------------------------------------------------------------------------------
DownloadCache::DownloadCache()
{
	m_cache = NULL;
}

//--------------------------------------------------------------------------------------------------------------
DownloadCache::~DownloadCache()
{
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::BuildKeyNames( const char *gamePath )
{
	if ( !gamePath )
	{
		m_cachefileKey[0] = 0;
		m_timestampKey[0] = 0;
		return;
	}

	char *tmpGamePath = V_strdup( gamePath );
	char *tmp = tmpGamePath;
	while ( *tmp )
	{
		if ( *tmp == '/' || *tmp == '\\' )
		{
			*tmp = '_';
		}
		++tmp;
	}
	Q_snprintf( m_cachefileKey, sizeof( m_cachefileKey ), "cachefile_%s", tmpGamePath );
	Q_snprintf( m_timestampKey, sizeof( m_timestampKey ), "timestamp_%s", tmpGamePath );

	delete[] tmpGamePath;
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::Init()
{
	if ( m_cache )
	{
		m_cache->deleteThis();
	}

	m_cache = new KeyValues( "DownloadCache" );
	m_cache->LoadFromFile( g_pFileSystem, CacheFilename, NULL );
	g_pFileSystem->CreateDirHierarchy( CacheDirectory, "DEFAULT_WRITE_PATH" );
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::GetCachedData( RequestContext_t *rc )
{
	if ( !m_cache )
		return;

	char cachePath[_MAX_PATH];
	GetCacheFilename( rc, cachePath );

	if ( !(*cachePath) )
		return;

	FileHandle_t fp = g_pFileSystem->Open( cachePath, "rb" );

	if ( fp == FILESYSTEM_INVALID_HANDLE )
		return;

	int size = g_pFileSystem->Size(fp);
	rc->cacheData = new unsigned char[size];
	int status = g_pFileSystem->Read( rc->cacheData, size, fp );
	g_pFileSystem->Close( fp );
	if ( !status )
	{
		delete[] rc->cacheData;
		rc->cacheData = NULL;
	}
	else
	{
		BuildKeyNames( rc->gamePath );
		rc->nBytesCached = size;
		strncpy( rc->cachedTimestamp, m_cache->GetString( m_timestampKey, "" ), BufferSize );
	}
}

//--------------------------------------------------------------------------------------------------------------
/**
 *  Takes a data stream compressed with bzip2, and writes it out to disk, uncompresses it, and deletes the
 *  compressed version.
 */
static bool DecompressBZipToDisk( const char *outFilename, const char *srcFilename, char *data, int bytesTotal )
{
	if ( g_pFileSystem->FileExists( outFilename ) || !data || bytesTotal < 1 )
	{
		return false;
	}

	// Create the subdirs
	char * tmpDir = V_strdup( outFilename );
	COM_CreatePath( tmpDir );
	delete[] tmpDir;

	// open the file for writing
	char fullSrcPath[MAX_PATH];
	Q_MakeAbsolutePath( fullSrcPath, sizeof( fullSrcPath ), srcFilename, com_gamedir );

	if ( !g_pFileSystem->FileExists( fullSrcPath ) )
	{
		// Write out the .bz2 file, for simplest decompression
		FileHandle_t ifp = g_pFileSystem->Open( fullSrcPath, "wb" );
		if ( !ifp )
		{
			return false;
		}
		int bytesWritten = g_pFileSystem->Write( data, bytesTotal, ifp );
		g_pFileSystem->Close( ifp );
		if ( bytesWritten != bytesTotal )
		{
			// couldn't write out all of the .bz2 file
			g_pFileSystem->RemoveFile( srcFilename );
			return false;
		}
	}

	// Prepare the uncompressed filehandle
	FileHandle_t ofp = g_pFileSystem->Open( outFilename, "wb" );
	if ( !ofp )
	{
		g_pFileSystem->RemoveFile( srcFilename );
		return false;
	}

	// And decompress!
	const int OutBufSize = 65536;
	char    buf[ OutBufSize ];
	BZFILE *bzfp = BZ2_bzopen( fullSrcPath, "rb" );
	int totalBytes = 0;

	bool bMapFile = false;
	char szOutFilenameBase[MAX_PATH];
	Q_FileBase( outFilename, szOutFilenameBase, sizeof( szOutFilenameBase ) );
	const char *pszMapName = cl.m_szLevelBaseName;
	if ( pszMapName && pszMapName[0] )
	{
		bMapFile = ( Q_stricmp( szOutFilenameBase, pszMapName ) == 0 );
	}

	while ( 1 )
	{
		int bytesRead = BZ2_bzread( bzfp, buf, OutBufSize );
		if ( bytesRead < 0 )
		{
			break; // error out
		}

		if ( bytesRead > 0 )
		{
			int bytesWritten = g_pFileSystem->Write( buf, bytesRead, ofp );
			if ( bytesWritten != bytesRead )
			{
				break; // error out
			}
			else
			{
				totalBytes += bytesWritten;
				if ( !bMapFile ) 
				{
					if ( totalBytes > MAX_FILE_SIZE )
					{
						ConDColorMsg( DownloadErrorColor, "DecompressBZipToDisk: '%s' too big (max %i bytes).\n", srcFilename, MAX_FILE_SIZE );
						break; // error out
					}
				}
			}
		}
		else
		{
			g_pFileSystem->Close( ofp );
			BZ2_bzclose( bzfp );
			g_pFileSystem->RemoveFile( srcFilename );
			return true;
		}
	}

	// We failed somewhere, so clean up and exit
	g_pFileSystem->Close( ofp );
	BZ2_bzclose( bzfp );
	g_pFileSystem->RemoveFile( srcFilename );
	g_pFileSystem->RemoveFile( outFilename );
	return false;
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::PersistToDisk( const RequestContext_t *rc )
{
	if ( !m_cache )
		return;

	if ( rc && rc->data && rc->nBytesTotal )
	{
		char absPath[MAX_PATH];
		if ( rc->bIsBZ2 )
		{
			Q_StripExtension( rc->absLocalPath, absPath, sizeof( absPath ) );
		}
		else
		{
			Q_strncpy( absPath, rc->absLocalPath, sizeof( absPath ) );
		}

		if ( !g_pFileSystem->FileExists( absPath ) )
		{
			// Create the subdirs
			char * tmpDir = V_strdup( absPath );
			COM_CreatePath( tmpDir );
			delete[] tmpDir;

			bool success = false;
			if ( rc->bIsBZ2 )
			{
				success = DecompressBZipToDisk( absPath, rc->absLocalPath, reinterpret_cast< char * >(rc->data), rc->nBytesTotal );
			}
			else
			{
				FileHandle_t fp = g_pFileSystem->Open( absPath, "wb" );
				if ( fp )
				{
					g_pFileSystem->Write( rc->data, rc->nBytesTotal, fp );
					g_pFileSystem->Close( fp );
					success = true;
				}
			}

			if ( success )
			{
				// write succeeded.  remove any old data from the cache.
				char cachePath[_MAX_PATH];
				GetCacheFilename( rc, cachePath );
				if ( cachePath[0] )
				{
					g_pFileSystem->RemoveFile( cachePath, NULL );
				}

				BuildKeyNames( rc->gamePath );
				KeyValues *kv = m_cache->FindKey( m_cachefileKey, false );
				if ( kv )
				{
					m_cache->RemoveSubKey( kv );
				}
				kv = m_cache->FindKey( m_timestampKey, false );
				if ( kv )
				{
					m_cache->RemoveSubKey( kv );
				}
			}
		}
	}

	m_cache->SaveToFile( g_pFileSystem, CacheFilename, NULL );
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::PersistToCache( const RequestContext_t *rc )
{
	if ( !m_cache || !rc || !rc->data || !rc->nBytesTotal || !rc->nBytesCurrent )
		return;

	char cachePath[_MAX_PATH];
	GenerateCacheFilename( rc, cachePath );

	FileHandle_t fp = g_pFileSystem->Open( cachePath, "wb" );
	if ( fp )
	{
		g_pFileSystem->Write( rc->data, rc->nBytesCurrent, fp );
		g_pFileSystem->Close( fp );

		m_cache->SaveToFile( g_pFileSystem, CacheFilename, NULL );
	}
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::GetCacheFilename( const RequestContext_t *rc, char cachePath[_MAX_PATH] )
{
	BuildKeyNames( rc->gamePath );
	const char *path = m_cache->GetString( m_cachefileKey, NULL );
	if ( !path || strncmp( path, CacheDirectory, strlen(CacheDirectory) ) )
	{
		cachePath[0] = 0;
		return;
	}
	strncpy( cachePath, path, _MAX_PATH );
	cachePath[_MAX_PATH-1] = 0;
}

//--------------------------------------------------------------------------------------------------------------
void DownloadCache::GenerateCacheFilename( const RequestContext_t *rc, char cachePath[_MAX_PATH] )
{
	GetCacheFilename( rc, cachePath );
	BuildKeyNames( rc->gamePath );

	m_cache->SetString( m_timestampKey, rc->cachedTimestamp );
	//ConDColorMsg( DownloadColor,"DownloadCache::GenerateCacheFilename() set %s = %s\n", m_timestampKey, rc->cachedTimestamp );

	if ( !*cachePath )
	{
		const char * lastSlash = strrchr( rc->gamePath, '/' );
		const char * lastBackslash = strrchr( rc->gamePath, '\\' );
		const char *gameFilename = rc->gamePath;
		if ( lastSlash || lastBackslash )
		{
			gameFilename = max( lastSlash, lastBackslash ) + 1;
		}
		for( int i=0; i<1000; ++i )
		{
			Q_snprintf( cachePath, _MAX_PATH, "%s/%s%4.4d", CacheDirectory, gameFilename, i );
			if ( !g_pFileSystem->FileExists( cachePath ) )
			{
				m_cache->SetString( m_cachefileKey, cachePath );
				//ConDColorMsg( DownloadColor,"DownloadCache::GenerateCacheFilename() set %s = %s\n", m_cachefileKey, cachePath );
				return;
			}
		}
		// all 1000 were invalid?!?
		Q_snprintf( cachePath, _MAX_PATH, "%s/overflow", CacheDirectory );
		//ConDColorMsg( DownloadColor,"DownloadCache::GenerateCacheFilename() set %s = %s\n", m_cachefileKey, cachePath );
		m_cache->SetString( m_cachefileKey, cachePath );
	}
}

//--------------------------------------------------------------------------------------------------------------
// Purpose: Implements download manager class
//--------------------------------------------------------------------------------------------------------------
class CDownloadManager
{
public:
	CDownloadManager();
	~CDownloadManager();

	void Queue( const char *baseURL, const char *urlPath, const char *gamePath );
	void Stop() { Reset(); }
	int GetQueueSize() { return m_queuedRequests.Count(); }

	virtual bool Update();	///< Monitors download thread, starts new downloads, and updates progress bar

	bool FileReceived( const char *filename, unsigned int requestID );
	bool FileDenied( const char *filename, unsigned int requestID );

	bool HasMapBeenDownloadedFromServer( const char *serverMapName );
	void MarkMapAsDownloadedFromServer( const char *serverMapName );

private:
	void QueueInternal( const char *pBaseURL, const char *pURLPath, const char *pGamePath, bool bAsHttp, bool bCompressed );

protected:
	virtual void UpdateProgressBar();

	virtual RequestContext_t *NewRequestContext();///< Call this to allocate a RequestContext_t - calls setup functions for derived classes
	virtual bool ShouldAttemptCompressedFileDownload()		{ return true; }
	virtual void SetupURLPath( RequestContext_t *pRequestContext, const char *pURLPath );
	virtual void SetupServerURL( RequestContext_t *pRequestContext );

	// Event handlers
	virtual void OnHttpConnecting( RequestContext_t *pRequestContext ) {}
	virtual void OnHttpFetch( RequestContext_t *pRequestContext ) {}
	virtual void OnHttpDone( RequestContext_t *pRequestContext ) {}
	virtual void OnHttpError( RequestContext_t *pRequestContext ) {}

	void Reset();						///< Cancels any active download, as well as any queued ones

	void PruneCompletedRequests();		///< Check download requests that have been completed to see if their threads have exited
	void CheckActiveDownload();			///< Checks download status, and updates progress bar
	void StartNewDownload();			///< Starts a new download if there are queued requests

	typedef CUtlVector< RequestContext_t * > RequestVector;
	RequestVector m_queuedRequests;		///< these are requests waiting to be spawned
	RequestContext_t *m_activeRequest;	///< this is the active request being downloaded in another thread
	RequestVector m_completedRequests;	///< these are waiting for the thread to exit

	int m_lastPercent;					///< last percent value the progress bar was updated with (to avoid spamming it)
	int m_totalRequests;				///< Total number of requests (used to set the top progress bar)

	int m_RequestIDCounter;				///< global increasing request ID counter

	typedef CUtlVector< char * > StrVector;
	StrVector m_downloadedMaps;			///< List of maps for which we have already tried to download assets.
};

//--------------------------------------------------------------------------------------------------------------
static CDownloadManager s_DownloadManager;

//--------------------------------------------------------------------------------------------------------------
CDownloadManager::CDownloadManager()
{
	m_activeRequest = NULL;
	m_lastPercent = 0;
	m_totalRequests = 0;
}

//--------------------------------------------------------------------------------------------------------------
CDownloadManager::~CDownloadManager()
{
	Reset();

	for ( int i=0; i<m_downloadedMaps.Count(); ++i )
	{
		delete[] m_downloadedMaps[i];
	}
	m_downloadedMaps.RemoveAll();
}

//--------------------------------------------------------------------------------------------------------------
RequestContext_t *CDownloadManager::NewRequestContext()
{
	return new RequestContext_t;
}

//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::SetupURLPath( RequestContext_t *pRequestContext, const char *pURLPath )
{
	V_strcpy( pRequestContext->urlPath, pRequestContext->gamePath );
}
//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::SetupServerURL( RequestContext_t *pRequestContext )
{
	Q_strncpy( pRequestContext->serverURL, cl.m_NetChannel->GetRemoteAddress().ToString(), BufferSize );
}
//--------------------------------------------------------------------------------------------------------------
bool CDownloadManager::HasMapBeenDownloadedFromServer( const char *serverMapName )
{
	if ( !serverMapName )
		return false;

	for ( int i=0; i<m_downloadedMaps.Count(); ++i )
	{
		const char *oldServerMapName = m_downloadedMaps[i];
		if ( oldServerMapName && !stricmp( serverMapName, oldServerMapName ) )
		{
			return true;
		}
	}
	return false;
}

bool CDownloadManager::FileDenied( const char *filename, unsigned int requestID )
{
	if ( !m_activeRequest )
		return false;

	if ( m_activeRequest->nRequestID != requestID )
		return false;

	if ( m_activeRequest->bAsHTTP )
		return false;

	if ( download_debug.GetBool() )
	{
		ConDColorMsg( DownloadErrorColor, "Error downloading %s\n", m_activeRequest->absLocalPath );
	}

	UpdateProgressBar();

	// try to download the next file
	m_completedRequests.AddToTail( m_activeRequest );
	m_activeRequest = NULL;

	return true;
}

bool CDownloadManager::FileReceived( const char *filename, unsigned int requestID )
{
	if ( !m_activeRequest )
		return false;

	if ( m_activeRequest->nRequestID != requestID )
		return false;

	if ( m_activeRequest->bAsHTTP )
		return false;

	if ( download_debug.GetBool() )
	{
		ConDColorMsg( DownloadCompleteColor, "Download finished!\n" );
	}

	UpdateProgressBar();

	m_completedRequests.AddToTail( m_activeRequest );
	m_activeRequest = NULL;

	return true;
}

//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::MarkMapAsDownloadedFromServer( const char *serverMapName )
{
	if ( !serverMapName )
		return;

	if ( HasMapBeenDownloadedFromServer( serverMapName ) )
		return;

	m_downloadedMaps.AddToTail( V_strdup( serverMapName ) );


	return;
}

//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::QueueInternal( const char *pBaseURL, const char *pURLPath, const char *pGamePath,
									 bool bAsHttp, bool bCompressed )
{
	// NOTE: Assumes valid game path (i.e. IsGamePathValidAndSafe() has been called already)

	++m_totalRequests;

	// Initialize the download cache if necessary
	if ( !TheDownloadCache )
	{
		TheDownloadCache = new DownloadCache;
		TheDownloadCache->Init();
	}

	// Create a new context and add queue it
	RequestContext_t *rc = NewRequestContext();
	m_queuedRequests.AddToTail( rc );

	rc->bIsBZ2 = bCompressed;
	rc->bAsHTTP = bAsHttp;
	rc->status = HTTP_CONNECTING;

	// Setup base path.  We put it in the "download" search path, if they have set one
	char szBasePath[ MAX_PATH ];
	if ( g_pFileSystem->GetSearchPath( k_szDownloadPathID, false, szBasePath, sizeof(szBasePath) ) > 0 )
	{
		char *split = V_strstr( szBasePath, ";" );
		if ( split != NULL )
		{
			Warning( "Multiple download search paths?  Check gameinfo.txt" );
			*split = '\0';
		}
	}

	// Otherwise, put it in the game dir
	if ( szBasePath[0] == '\0' )
		V_strcpy_safe( szBasePath, com_gamedir );

	// Setup game path
	V_strcpy_safe( rc->gamePath, pGamePath );
	if ( bCompressed )
	{
		V_strcat_safe( rc->gamePath, ".bz2" );
	}
	Q_FixSlashes( rc->gamePath, '/' ); // only matters for debug prints, which are full URLS, so we want forward slashes

	// NOTE: Loose files on disk must always be lowercase!  At least on Linux they HAVE to be,
	// but we do the same thing on Windows to keep things consistent.
	char szGamePathLower[MAX_PATH];
	V_strcpy_safe( szGamePathLower, rc->gamePath );
	V_strlower( szGamePathLower );

	// Now set the full absolute path.  Why does the file system not provide a convenient method to
	// do stuff like this?
	V_strcpy_safe( rc->absLocalPath, szBasePath );
	V_AppendSlash( rc->absLocalPath, sizeof(rc->absLocalPath) );
	V_strcat_safe( rc->absLocalPath, szGamePathLower );
	V_FixSlashes( rc->absLocalPath );

	// Setup base URL if necessary
	if ( bAsHttp )
	{
		V_strcpy_safe( rc->baseURL, pBaseURL );
		V_StripTrailingSlash( rc->baseURL );
		V_strcat_safe( rc->baseURL, "/" );
	}

	// Call virtual methods for setting up additional context info
	SetupURLPath( rc, pURLPath );
	SetupServerURL( rc );

	if ( download_debug.GetBool() )
	{
		ConDColorMsg( DownloadColor, "Queueing %s%s.\n", rc->baseURL, pGamePath );
	}

	// Invoke the callback if appropriate
	if ( bAsHttp )
	{
		OnHttpConnecting( rc ); 
	}
}

void CDownloadManager::Queue( const char *baseURL, const char *urlPath, const char *gamePath )
{
	if ( !CL_IsGamePathValidAndSafeForDownload( gamePath ) )
		return;

	// Don't download existing files
	if ( g_pFileSystem->FileExists( gamePath ) )
		return;

#ifndef _DEBUG
	if ( sv.IsActive() )
	{
		return;	// don't try to download things for the local server (in case a map is missing sounds etc that
				// aren't needed to play.
	}
#endif

	// HTTP or NetChan?
	bool bAsHTTP = baseURL && ( !Q_strnicmp( baseURL, "http://", 7 ) || !Q_strnicmp( baseURL, "https://", 8 ) );

	// Queue up an HTTP download of the bzipped asset, in case it exists.
	// When a bzipped download finishes, we'll uncompress the file to it's
	// original destination, and the queued download of the uncompressed
	// file will abort.
	if ( bAsHTTP && ShouldAttemptCompressedFileDownload() && !g_pFileSystem->FileExists( va( "%s.bz2", gamePath ) ) )
	{
		QueueInternal( baseURL, urlPath, gamePath, true, true );
	}

	// Queue up the straight, uncompressed version
	QueueInternal( baseURL, urlPath, gamePath, bAsHTTP, false );

	if ( download_debug.GetBool() )
	{
		ConDColorMsg( DownloadColor, "Queueing %s%s.\n", baseURL, gamePath );
	}
}

//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::Reset()
{
	// ask the active request to bail
	if ( m_activeRequest )
	{
		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadColor, "Aborting download of %s\n", m_activeRequest->gamePath );
		}

		if ( m_activeRequest->nBytesTotal && m_activeRequest->nBytesCurrent )
		{
			// Persist partial data to cache
			TheDownloadCache->PersistToCache( m_activeRequest );
		}
		m_activeRequest->shouldStop = true;
		m_completedRequests.AddToTail( m_activeRequest );
		m_activeRequest = NULL;
		//TODO: StopLoadingProgressBar();
	}

	// clear out any queued requests
	for ( int i=0; i<m_queuedRequests.Count(); ++i )
	{
		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadColor, "Discarding queued download of %s\n", m_queuedRequests[i]->gamePath );
		}

		delete m_queuedRequests[i];
	}
	m_queuedRequests.RemoveAll();

	if ( TheDownloadCache )
	{
		delete TheDownloadCache;
		TheDownloadCache = NULL;
	}

	m_lastPercent = 0;
	m_totalRequests = 0;
}

//--------------------------------------------------------------------------------------------------------------
// Check download requests that have been completed to see if their threads have exited
void CDownloadManager::PruneCompletedRequests()
{
	for ( int i=m_completedRequests.Count()-1; i>=0; --i )
	{
		if ( m_completedRequests[i]->threadDone || !m_completedRequests[i]->bAsHTTP )
		{
			if ( m_completedRequests[i]->cacheData )
			{
				delete[] m_completedRequests[i]->cacheData;
			}
			delete m_completedRequests[i];
			m_completedRequests.Remove( i );
		}
	}
}

//--------------------------------------------------------------------------------------------------------------
// Checks download status, and updates progress bar
void CDownloadManager::CheckActiveDownload()
{
	if ( !m_activeRequest )
		return;

	if ( !m_activeRequest->bAsHTTP )
	{
		UpdateProgressBar();
		return;
	}
	
	// check active request for completion / error / progress update
	switch ( m_activeRequest->status )
	{
	case HTTP_DONE:
		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadCompleteColor, "Download finished!\n" );
		}

		UpdateProgressBar();
		OnHttpDone( m_activeRequest );
		if ( m_activeRequest->nBytesTotal )
		{
			// Persist complete data to disk, and remove cache entry
			//TODO: SetSecondaryProgressBarText( m_activeRequest->gamePath );
			TheDownloadCache->PersistToDisk( m_activeRequest );
			m_activeRequest->shouldStop = true;
			m_completedRequests.AddToTail( m_activeRequest );
			m_activeRequest = NULL;
			if ( !m_queuedRequests.Count() )
			{
				//TODO: StopLoadingProgressBar();
				//TODO: Cbuf_AddText("retry\n");
			}
		}
		break;
	case HTTP_ERROR:
		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadErrorColor, "Error downloading %s%s\n", m_activeRequest->baseURL, m_activeRequest->gamePath );
		}

		UpdateProgressBar();

		// try to download the next file
		m_activeRequest->shouldStop = true;
		m_completedRequests.AddToTail( m_activeRequest );
		OnHttpError( m_activeRequest );
		m_activeRequest = NULL;
		if ( !m_queuedRequests.Count() )
		{
			//TODO: StopLoadingProgressBar();
			//TODO: Cbuf_AddText("retry\n");
		}
		break;
	case HTTP_FETCH:
		UpdateProgressBar();
		// Update progress bar
		//TODO: SetSecondaryProgressBarText( m_activeRequest->gamePath );
		if ( m_activeRequest->nBytesTotal )
		{
			int percent = ( m_activeRequest->nBytesCurrent * 100 / m_activeRequest->nBytesTotal );
			if ( percent != m_lastPercent )
			{
				if ( download_debug.GetBool() )
				{
					ConDColorMsg( DownloadColor, "Downloading %s%s: %3.3d%% - %d of %d bytes\n",
						m_activeRequest->baseURL, m_activeRequest->gamePath,
						percent, m_activeRequest->nBytesCurrent, m_activeRequest->nBytesTotal );
				}

				m_lastPercent = percent;
				//TODO: SetSecondaryProgressBar( m_lastPercent * 0.01f );
			}
		}
		OnHttpFetch( m_activeRequest );
		break;
	}
}

//--------------------------------------------------------------------------------------------------------------
// Starts a new download if there are queued requests
void CDownloadManager::StartNewDownload()
{
	if ( m_activeRequest || !m_queuedRequests.Count() )
		return;

	while ( !m_activeRequest && m_queuedRequests.Count() )
	{
		// Remove one request from the queue and make it active
		m_activeRequest = m_queuedRequests[0];
		m_queuedRequests.Remove( 0 );

		if ( g_pFileSystem->FileExists( m_activeRequest->absLocalPath ) )
		{
			if ( download_debug.GetBool() )
			{
				ConDColorMsg( DownloadColor, "Skipping existing file %s%s.\n", m_activeRequest->baseURL, m_activeRequest->gamePath );
			}

			m_activeRequest->shouldStop = true;
			m_activeRequest->threadDone = true;
			m_completedRequests.AddToTail( m_activeRequest );
			m_activeRequest = NULL;
		}
	}

	if ( !m_activeRequest )
		return;

	if ( g_pFileSystem->FileExists( m_activeRequest->absLocalPath ) )
	{
		m_activeRequest->shouldStop = true;
		m_activeRequest->threadDone = true;
		m_completedRequests.AddToTail( m_activeRequest );
		m_activeRequest = NULL;
		return; // don't download existing files
	}

	if ( m_activeRequest->bAsHTTP )
	{
		// Check cache for partial match
		TheDownloadCache->GetCachedData( m_activeRequest );

		//TODO: ContinueLoadingProgressBar( "Http", m_totalRequests - m_queuedRequests.Count(), 0.0f );
		//TODO: SetLoadingProgressBarStatusText( "#GameUI_VerifyingAndDownloading" );
		//TODO: SetSecondaryProgressBarText( m_activeRequest->gamePath );
		//TODO: SetSecondaryProgressBar( 0.0f );
		UpdateProgressBar();

		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadColor, "Downloading %s%s.\n", m_activeRequest->baseURL, m_activeRequest->gamePath );
		}

		m_lastPercent = 0;

		// Start the thread
		DWORD threadID;
		VCRHook_CreateThread(NULL, 0, 
#ifdef POSIX
			(void *)
#endif
			DownloadThread, m_activeRequest, 0, (unsigned long int *)&threadID );

		ThreadDetach( ( ThreadHandle_t )threadID );
	}
	else
	{
		UpdateProgressBar();

		if ( download_debug.GetBool() )
		{
			ConDColorMsg( DownloadColor, "Downloading %s.\n", m_activeRequest->gamePath );
		}

		m_lastPercent = 0;
		
		m_activeRequest->nRequestID = cl.m_NetChannel->RequestFile( m_activeRequest->gamePath );
	}
}

//--------------------------------------------------------------------------------------------------------------
void CDownloadManager::UpdateProgressBar()
{
	if ( !m_activeRequest )
	{
		return;
	}

	wchar_t filenameBuf[MAX_OSPATH];
	float progress = 0.0f;

	if ( m_activeRequest->bAsHTTP )
	{
		int overallPercent = (m_totalRequests - m_queuedRequests.Count() - 1) * 100 / m_totalRequests;
		int filePercent = 0;
		if ( m_activeRequest->nBytesTotal > 0 )
		{	
			filePercent = ( m_activeRequest->nBytesCurrent * 100 / m_activeRequest->nBytesTotal );
		}

		progress = (overallPercent + filePercent * 1.0f / m_totalRequests) * 0.01f;
	}
	else
	{
		int received, total;
		cl.m_NetChannel->GetStreamProgress( FLOW_INCOMING, &received, &total );
		
		progress = (float)(received)/(float)(total);
	}

#ifndef DEDICATED
	_snwprintf( filenameBuf, 256, L"Downloading %hs", m_activeRequest->gamePath );
	EngineVGui()->UpdateCustomProgressBar( progress, filenameBuf );
#endif
}

//--------------------------------------------------------------------------------------------------------------
// Monitors download thread, starts new downloads, and updates progress bar
bool CDownloadManager::Update()
{
	PruneCompletedRequests();
	CheckActiveDownload();
	StartNewDownload();

	return m_activeRequest != NULL;
}

//--------------------------------------------------------------------------------------------------------------
// Externally-visible function definitions
//--------------------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------------------
bool CL_DownloadUpdate(void)
{
	return s_DownloadManager.Update();
}

//--------------------------------------------------------------------------------------------------------------
void CL_HTTPStop_f(void)
{
	s_DownloadManager.Stop();
}

bool CL_FileReceived( const char *filename, unsigned int requestID )
{
	return s_DownloadManager.FileReceived( filename, requestID );
}

bool CL_FileDenied( const char *filename, unsigned int requestID )
{
	return s_DownloadManager.FileDenied( filename, requestID );
}

//--------------------------------------------------------------------------------------------------------------
extern ConVar sv_downloadurl;
void CL_QueueDownload( const char *filename )
{
	s_DownloadManager.Queue( sv_downloadurl.GetString(), NULL, filename );
}

//--------------------------------------------------------------------------------------------------------------

int CL_GetDownloadQueueSize(void)
{
	return s_DownloadManager.GetQueueSize();
}

//--------------------------------------------------------------------------------------------------------------

int CL_CanUseHTTPDownload(void)
{
	if ( sv_downloadurl.GetString()[0] )
	{
		const char *serverMapName = va( "%s:%s", sv_downloadurl.GetString(), cl.m_szLevelFileName );
		return !s_DownloadManager.HasMapBeenDownloadedFromServer( serverMapName );
	}
	return 0;
}

//--------------------------------------------------------------------------------------------------------------

void CL_MarkMapAsUsingHTTPDownload(void)
{
	const char *serverMapName = va( "%s:%s", sv_downloadurl.GetString(), cl.m_szLevelFileName );
	s_DownloadManager.MarkMapAsDownloadedFromServer( serverMapName );
}

//--------------------------------------------------------------------------------------------------------------

bool CL_IsGamePathValidAndSafeForDownload( const char *pGamePath )
{
	if ( !CNetChan::IsValidFileForTransfer( pGamePath ) )
		return false;

	return true;
}

//--------------------------------------------------------------------------------------------------------------

class CDownloadSystem : public IDownloadSystem
{
public:
	virtual DWORD CreateDownloadThread( RequestContext_t *pContext )
	{
		DWORD nThreadID;
		VCRHook_CreateThread(NULL, 0,
#ifdef POSIX
		 	(void*)
#endif
		 	DownloadThread, pContext, 0, (unsigned long int *)&nThreadID );

		ThreadDetach( ( ThreadHandle_t )nThreadID );
		return nThreadID;
	}
};

//--------------------------------------------------------------------------------------------------------------

static CDownloadSystem s_DownloadSystem;
EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CDownloadSystem, IDownloadSystem, INTERFACEVERSION_DOWNLOADSYSTEM, s_DownloadSystem );

//--------------------------------------------------------------------------------------------------------------