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

#include "cl_recordingsession.h"
#include "cl_sessioninfodownloader.h"
#include "cl_recordingsessionmanager.h"
#include "cl_replaymanager.h"
#include "cl_recordingsessionblock.h"
#include "cl_sessionblockdownloader.h"
#include "replay/ienginereplay.h"
#include "KeyValues.h"

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

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

extern IEngineReplay *g_pEngine;

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

#define MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS	3

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

CClientRecordingSession::CClientRecordingSession( IReplayContext *pContext )
:	CBaseRecordingSession( pContext ),
	m_iLastBlockToDownload( -1 ),
	m_iGreatestConsecutiveBlockDownloaded( -1 ),
	m_nSessionInfoDownloadAttempts( 0 ),
	m_flLastUpdateTime( -1.0f ),
	m_pSessionInfoDownloader( NULL ),
	m_bTimedOut( false ),
	m_bAllBlocksDownloaded( false )
{
}

CClientRecordingSession::~CClientRecordingSession()
{
	delete m_pSessionInfoDownloader;
}

bool CClientRecordingSession::AllReplaysReconstructed() const
{
	FOR_EACH_LL( m_lstReplays, it )
	{
		const CReplay *pCurReplay = m_lstReplays[ it ];
		if ( !pCurReplay->HasReconstructedReplay() )
			return false;
	}

	return true;
}

void CClientRecordingSession::DeleteBlocks()
{
	// Only delete blocks if all replays have been reconstructed for this session
	if ( !AllReplaysReconstructed() )
		return;

	// Delete each block
	FOR_EACH_VEC( m_vecBlocks, i )
	{
		m_pContext->GetRecordingSessionBlockManager()->DeleteBlock( m_vecBlocks[ i ] );
	}

	// Clear out the list
	m_vecBlocks.RemoveAll();

	// Clear these out so we don't try to download the blocks again
	m_iLastBlockToDownload = -1;
	m_iGreatestConsecutiveBlockDownloaded = -1;
}

void CClientRecordingSession::SyncSessionBlocks()
{
	// If the last update time hasn't been initialized yet, initialize it now since this will be the first time
	// we are attempting to download the session info file.
	if ( m_flLastUpdateTime < 0.0f )
	{
		m_flLastUpdateTime = g_pEngine->GetHostTime();
	}

	Assert( !m_pSessionInfoDownloader );
	IF_REPLAY_DBG( Warning( "Downloading session info...\n" ) );
	m_pSessionInfoDownloader = new CSessionInfoDownloader();
	m_pSessionInfoDownloader->DownloadSessionInfoAndUpdateBlocks( this );
}

void CClientRecordingSession::OnReplayDeleted( CReplay *pReplay )
{
	m_lstReplays.FindAndRemove( pReplay );

	// This will load session blocks and delete them from disk if possible.  In the case
	// that all other replays for a session have already been reconstructed and pReplay
	// was the last replay that was never reconstructed, we should delete session's blocks now.
	// Note that these calls will only (a) load blocks if they aren't loaded already, and
	// (b) Delete blocks if all associated replays have been reconstructed.
	LoadBlocksForSession();
	DeleteBlocks();
}

bool CClientRecordingSession::Read( KeyValues *pIn )
{
	if ( !BaseClass::Read( pIn ) )
		return false;

	m_iLastBlockToDownload = pIn->GetInt( "last_block_to_download", -1 );
	m_iGreatestConsecutiveBlockDownloaded = pIn->GetInt( "last_consec_block_downloaded", -1 );
//	m_bTimedOut = pIn->GetBool( "timed_out" );
	m_uServerSessionID = pIn->GetUint64( "server_session_id" );
	m_bAllBlocksDownloaded = pIn->GetBool( "all_blocks_downloaded" );

	return true;
}

void CClientRecordingSession::Write( KeyValues *pOut )
{
	BaseClass::Write( pOut );

	pOut->SetInt( "last_block_to_download", m_iLastBlockToDownload );
	pOut->SetInt( "last_consec_block_downloaded", m_iGreatestConsecutiveBlockDownloaded );
//	pOut->SetInt( "timed_out", (int)m_bTimedOut );
	pOut->SetUint64( "server_session_id", m_uServerSessionID );
	pOut->SetInt( "all_blocks_downloaded", (int)m_bAllBlocksDownloaded );
}

void CClientRecordingSession::AdjustLastBlockToDownload( int iNewLastBlockToDownload )
{
	Assert( m_iLastBlockToDownload > iNewLastBlockToDownload );
	m_iLastBlockToDownload = iNewLastBlockToDownload;

	// Adjust any replays that refer to this session
	FOR_EACH_LL( m_lstReplays, i )
	{
		CReplay *pCurReplay = m_lstReplays[ i ];
		if ( pCurReplay->m_iMaxSessionBlockRequired > iNewLastBlockToDownload )
		{
			// Adjust replay
			pCurReplay->m_iMaxSessionBlockRequired = iNewLastBlockToDownload;
		}
	}
}

int CClientRecordingSession::UpdateLastBlockToDownload()
{
	// Here we calculate the block we'll need in order to reconstruct the replay at the post-death time,
	// based on replay_postdeathrecordtime, NOT the current time.  The index calculated here may be greater
	// than the actual last block the server writes, since the round may end or the map may change.  This
	// is adjusted for when we actually download the blocks.
	extern ConVar replay_postdeathrecordtime;
	CClientRecordingSessionManager::ServerRecordingState_t *pServerState = &CL_GetRecordingSessionManager()->m_ServerRecordingState;

	const int nCurBlock = pServerState->m_nCurrentBlock;
	const int nDumpInterval = pServerState->m_nDumpInterval;		Assert( nDumpInterval > 0 );
	const int nAddedBlocks = (int)ceil( replay_postdeathrecordtime.GetFloat() / nDumpInterval );	// Round up
	const int iPostDeathBlock = nCurBlock + nAddedBlocks;

	IF_REPLAY_DBG( Warning( "nCurBlock: %i\n", nCurBlock ) );
	IF_REPLAY_DBG( Warning( "nDumpInterval: %i\n", nDumpInterval ) );
	IF_REPLAY_DBG( Warning( "nAddedBlocks: %i\n", nAddedBlocks ) );
	IF_REPLAY_DBG( Warning( "iPostDeathBlock: %i\n", iPostDeathBlock ) );

	// Never assign less blocks than we already need
	m_iLastBlockToDownload = MAX( m_iLastBlockToDownload, iPostDeathBlock );

	CL_GetRecordingSessionManager()->FlagForFlush( this, false );
	
	IF_REPLAY_DBG( ConColorMsg( 0, Color(0,255,0), "Max block currently needed: %i\n", m_iLastBlockToDownload ) );

	return m_iLastBlockToDownload;
}

void CClientRecordingSession::Think()
{
	CBaseThinker::Think();

	// If the session info downloader's done and can be deleted, free it.
	if ( m_pSessionInfoDownloader &&
		 m_pSessionInfoDownloader->IsDone() &&
		 m_pSessionInfoDownloader->CanDelete() )
	{
		// Failure?
		if ( m_pSessionInfoDownloader->m_nError != CSessionInfoDownloader::ERROR_NONE )
		{
			// If there was an error, increment the error count and update the appropriate replays if
			// we've tried a sufficient number of times.
			++m_nSessionInfoDownloadAttempts;
			if ( m_nSessionInfoDownloadAttempts >= MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS )
			{
				FOR_EACH_LL( m_lstReplays, i )
				{
					CReplay *pCurReplay = m_lstReplays[ i ];

					// If this replay has already been set to "ready to convert" state (or beyond), skip.
					if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
						continue;

					// Update status
					pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;

					// Display an error message
					ShowDownloadFailedMessage( pCurReplay );

					// Save now
					CL_GetReplayManager()->FlagReplayForFlush( pCurReplay, true );
				}
			}
		}

		IF_REPLAY_DBG( Warning( "...session info download complete.  Freeing.\n" ) );
		delete m_pSessionInfoDownloader;
		m_pSessionInfoDownloader = NULL;
	}
}

float CClientRecordingSession::GetNextThinkTime() const
{
	return g_pEngine->GetHostTime() + 0.5f;
}

void CClientRecordingSession::UpdateAllBlocksDownloaded()
{
	// We're only "done" if this session is no longer recording and all blocks are downloaded.
	const bool bOld = m_bAllBlocksDownloaded;
	m_bAllBlocksDownloaded = !m_bRecording && ( m_iGreatestConsecutiveBlockDownloaded >= m_iLastBlockToDownload );

	// Flag as modified if changed
	if ( bOld != m_bAllBlocksDownloaded )
	{
		CL_GetRecordingSessionManager()->FlagForFlush( this, false );
	}
}

void CClientRecordingSession::EnsureDownloadingEnabled()
{
	m_bAllBlocksDownloaded = false;
}

void CClientRecordingSession::UpdateGreatestConsecutiveBlockDownloaded()
{
	// Assumes m_vecBlocks is sorted in ascending order (for both reconstruction indices and handle, which should be parallel)
	int j = 0;
	int iGreatestConsecutiveBlockDownloaded = 0;
	FOR_EACH_VEC( m_vecBlocks, i )
	{
		CClientRecordingSessionBlock *pCurBlock = CL_CastBlock( m_vecBlocks[ i ] );

		AssertMsg( pCurBlock->m_iReconstruction == j, "Session blocks must be sorted!" );

		// If the block hasn't been downloaded, stop here
		if ( pCurBlock->m_nDownloadStatus != CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED )
			break;

		// Block has been downloaded - update the counter
		iGreatestConsecutiveBlockDownloaded = MAX( iGreatestConsecutiveBlockDownloaded, pCurBlock->m_iReconstruction );

		++j;
	}

	Assert( iGreatestConsecutiveBlockDownloaded >= 0 );
	Assert( iGreatestConsecutiveBlockDownloaded < m_vecBlocks.Count() );

	// Cache
	m_iGreatestConsecutiveBlockDownloaded = iGreatestConsecutiveBlockDownloaded;

	// Mark session as dirty
	CL_GetRecordingSessionManager()->FlagForFlush( this, false );
}

void CClientRecordingSession::UpdateReplayStatuses( CClientRecordingSessionBlock *pBlock )
{
	AssertMsg( m_vecBlocks.Find( pBlock ) != m_vecBlocks.InvalidIndex(), "Block doesn't belong to session or was not added" );

	// If the download was successful, update the greatest consecutive block downloaded index
	if ( pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED )
	{
		UpdateGreatestConsecutiveBlockDownloaded();
		UpdateAllBlocksDownloaded();
	}

	// Block in error state?
	const bool bFailed = pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_ERROR;

	// Go through all replays that refer to this session and update their status if necessary
	FOR_EACH_LL( m_lstReplays, i )
	{
		CReplay *pCurReplay = m_lstReplays[ i ];

		// If this replay has already been set to "ready to convert" state (or beyond), skip.
		if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
			continue;

		bool bFlush = false;

		// If the download failed and the block is required for this replay, mark as such
		if ( bFailed && pCurReplay->m_iMaxSessionBlockRequired >= pBlock->m_iReconstruction )
		{
			pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;
			bFlush = true;

			// Display an error message
			ShowDownloadFailedMessage( pCurReplay );
		}

		// Have we downloaded all blocks required for the given replay?
		else if ( !bFailed && pCurReplay->m_iMaxSessionBlockRequired <= m_iGreatestConsecutiveBlockDownloaded )
		{
			// Update replay's status and mark as dirty
			pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_READYTOCONVERT;

			// Display a message on the client
			g_pClient->DisplayReplayMessage( "#Replay_DownloadComplete", false, false, "replay\\downloadcomplete.wav" );

			bFlush = true;
		}

		// Mark replay as dirty?
		if ( bFlush )
		{
			CL_GetReplayManager()->FlagForFlush( pCurReplay, false );
		}
	}
}

void CClientRecordingSession::OnDownloadTimeout()
{
	m_bTimedOut = true;

	// Go through all replays that refer to this session and update their status if necessary
	FOR_EACH_LL( m_lstReplays, i )
	{
		CReplay *pCurReplay = m_lstReplays[ i ];

		// If this replay has already been set to "ready to convert" state (or beyond), skip.
		if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
			continue;

		// Check to see if we have enough block info for the current replay
		if ( m_iGreatestConsecutiveBlockDownloaded >= pCurReplay->m_iMaxSessionBlockRequired )
			continue;

		// Update replay status
		pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;

		// Display an error message
		ShowDownloadFailedMessage( pCurReplay );

		// Save the replay
		CL_GetReplayManager()->FlagForFlush( pCurReplay, false );
	}
}

void CClientRecordingSession::RefreshLastUpdateTime()
{
	m_flLastUpdateTime = g_pEngine->GetHostTime();
}

void CClientRecordingSession::ShowDownloadFailedMessage( const CReplay *pReplay )
{
	// Don't show the download failed message for replays that were saved during this run of the game.
	if ( !pReplay || !pReplay->m_bSavedDuringThisSession )
		return;

	// Display an error message
	g_pClient->DisplayReplayMessage( "#Replay_DownloadFailed", true, false, "replay\\downloadfailed.wav" );
}

void CClientRecordingSession::CacheReplay( CReplay *pReplay )
{
	Assert( m_lstReplays.Find( pReplay ) == m_lstReplays.InvalidIndex() );
	m_lstReplays.AddToTail( pReplay );

	// We should no longer auto-delete this session if CacheReplay() is being called.  This
	// can happen if the user connects to a server, saves a replay, deletes the replay (at
	// which point auto-delete is flagged for the recording session), and then saves another
	// replay.  In this situation, we obviously don't want to delete the session anymore.
	if ( m_bAutoDelete )
	{
		m_bAutoDelete = false;
	}
}

bool CClientRecordingSession::ShouldSyncBlocksWithServer() const
{
	// Already downloaded all blocks?
	if ( m_bAllBlocksDownloaded )
		return false;

	// If block count is out of sync with the m_iLastBlockDownloaded we need to sync up
	const bool bReachedMaxDownloadAttempts = m_nSessionInfoDownloadAttempts >= MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS;
	const bool bNeedToDownloadBlocks = m_iLastBlockToDownload >= 0;
//	const bool bAlreadyDownloadedAllNeededBlocks = m_iLastBlockToDownload <= m_iGreatestConsecutiveBlockDownloaded;
	const bool bAlreadyDownloadedAllNeededBlocks = m_iLastBlockToDownload < m_vecBlocks.Count();	// NOTE/TODO: Shouldn't this look at m_iGreatestConsecutiveBlockDownloaded?  Tried for a week, but it caused bugs.  Reverting for now.  TODO
	const bool bTimedOut = false;//TimedOut();

	const bool bResult = !bReachedMaxDownloadAttempts &&
						  bNeedToDownloadBlocks &&
						 !bAlreadyDownloadedAllNeededBlocks &&
						 !bTimedOut;

	if ( bResult )
	{
		IF_REPLAY_DBG( Warning( "Blocks out of sync for session %i - downloading session info now.\n", GetHandle() ) );
	}
	else
	{
		DBG3( "NOT syncing because:\n" );
		if ( bReachedMaxDownloadAttempts )			DBG3( "   - Reached maximum download attempts\n" );
		if ( !bNeedToDownloadBlocks )				DBG3( "   - No replay saved yet\n" );
		if ( bAlreadyDownloadedAllNeededBlocks )	DBG3( "   - Already downloaded all needed blocks\n" );
		if ( bTimedOut )							DBG3( "   - Download timed out (session info file didn't change after 90 seconds)\n" );
	}

	return bResult;
}

void CClientRecordingSession::PopulateWithRecordingData( int nCurrentRecordingStartTick )
{
	BaseClass::PopulateWithRecordingData( nCurrentRecordingStartTick );

	CClientRecordingSessionManager::ServerRecordingState_t *pServerState = &CL_GetRecordingSessionManager()->m_ServerRecordingState;
	m_strName = pServerState->m_strSessionName;

	// Get download URL from replicated cvars
	m_strBaseDownloadURL = Replay_GetDownloadURL();

	// Get server session ID
	m_uServerSessionID = g_pClient->GetServerSessionId();
}

bool CClientRecordingSession::ShouldDitchSession() const
{
	return BaseClass::ShouldDitchSession() || m_lstReplays.Count() == 0;
}

void CClientRecordingSession::OnDelete()
{
	// Abort any session block downloads now
	CL_GetSessionBlockDownloader()->AbortDownloadsAndCleanup( this );
	if ( m_pSessionInfoDownloader )
	{
		m_pSessionInfoDownloader->CleanupDownloader();
	}

	// Delete blocks
	BaseClass::OnDelete();
}

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