//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Holds the CAccountDetails class.
//
//=============================================================================

#include "stdafx.h"
#include "accountdetails.h"
#include "rtime.h"
#include "gcsdk_gcmessages.pb.h"

#include "memdbgon.h" // needs to be the last include in the file

namespace GCSDK
{
GCConVar cv_account_details_cache_time( "account_details_cache_time", "600" );
GCConVar cv_account_details_failure_cache_time( "account_details_failure_cache_time", "10" );
GCConVar account_details_timeout( "account_details_timeout", "10" );
GCConVar cv_persona_name_cache_time( "persona_name_cache_time", "60" );
GCConVar cv_persona_name_failure_cache_time( "persona_name_failure_cache_time", "10" );
GCConVar cv_persona_name_batch_size( "persona_name_batch_size", "100" );
GCConVar persona_name_timeout( "persona_name_timeout", "10" );

const char *kszAccountDetailsKey = "AccountDetails-v001";

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CAccountDetails::CAccountDetails()
:	m_rtimeCached( CRTime::RTime32TimeCur() ),
	m_bValid( false ),
	m_bPublicProfile( false ),
	m_bVacBanned( false ),
	m_bCyberCafe( false ),
	m_bSchoolAccount( false ),
	m_bFreeTrialAccount( false ),
	m_bSubscribed( false ),
	m_bLowViolence( false ),
	m_bLimited( false ),
	m_bAccountLocked( false ),
	m_bCommunityBanned( false ),
	m_bTradeBanned( false ),
	m_bIsSteamGuardEnabled( false ),
	m_bIsPhoneVerified( false ),
	m_bIsTwoFactorAuthEnabled( false ),
	m_bIsPhoneIdentifying( false ),
	m_unPackage( 0 ),
	m_rtimeVACBanEnd( 0 ),
	m_unSteamLevel( 0 ),
	m_unFriendCount( 0 ),
	m_rtimeAccountCreated( 0 ),
	m_rtimeTwoFactorEnabled( 0 ),
	m_rtimePhoneVerified( 0 ),
	m_unPhoneID( 0 )
{
}

//-----------------------------------------------------------------------------
// Purpose: Initialize a fresh CAccountDetails with data from Steam
//-----------------------------------------------------------------------------
void CAccountDetails::Init( CGCSystemMsg_GetAccountDetails_Response &msgResponse )
{
	m_bValid = true;
	m_sAccountName = msgResponse.account_name().c_str();
	m_bPublicProfile = msgResponse.is_profile_public();
	m_bPublicInventory = msgResponse.is_inventory_public();
	m_bVacBanned = msgResponse.is_vac_banned(); 
	m_bCyberCafe = msgResponse.is_cyber_cafe(); 
	m_bSchoolAccount = msgResponse.is_school_account(); 
	m_bFreeTrialAccount = msgResponse.is_free_trial_account(); 
	m_bSubscribed = msgResponse.is_subscribed(); 
	m_bLowViolence = msgResponse.is_low_violence(); 
	m_bLimited = msgResponse.is_limited(); 
	m_bAccountLocked = msgResponse.is_account_locked_down();
	m_bCommunityBanned = msgResponse.is_community_banned();
	m_bTradeBanned = msgResponse.is_trade_banned();
	m_unPackage = msgResponse.package();
	m_rtimeVACBanEnd = msgResponse.suspension_end_time();
	m_sCurrency = msgResponse.currency().c_str();
	m_unSteamLevel = msgResponse.steam_level();
	m_unFriendCount = msgResponse.friend_count();
	m_rtimeAccountCreated = msgResponse.account_creation_time();
	m_bIsSteamGuardEnabled = msgResponse.is_steamguard_enabled();
	m_bIsPhoneVerified = msgResponse.is_phone_verified();
	m_bIsTwoFactorAuthEnabled = msgResponse.is_two_factor_auth_enabled();
	m_bIsPhoneIdentifying = msgResponse.is_phone_identifying();
	m_rtimeTwoFactorEnabled = msgResponse.two_factor_enabled_time();
	m_rtimePhoneVerified = msgResponse.phone_verification_time();
	m_unPhoneID = msgResponse.phone_id();
}


//-----------------------------------------------------------------------------
// Purpose: Returns true if it's time to remove this entry from the cache
//-----------------------------------------------------------------------------
bool CAccountDetails::BIsExpired() const
{
	int nCacheSeconds = BIsValid() ? cv_account_details_cache_time.GetInt() : cv_account_details_failure_cache_time.GetInt();

	return m_rtimeCached + nCacheSeconds < CRTime::RTime32TimeCur();
}


//-----------------------------------------------------------------------------
// Purpose: Reverts this to an invalid record
//-----------------------------------------------------------------------------
void CAccountDetails::Reset()
{
	m_bValid = false;
	m_rtimeCached = CRTime::RTime32TimeCur();
}


#ifdef DBGFLAG_VALIDATE
//-----------------------------------------------------------------------------
// Purpose: Claims all the memory for the AccountDetails object
//-----------------------------------------------------------------------------
void CAccountDetails::Validate( CValidator &validator, const char *pchName )
{
	VALIDATE_SCOPE();
	ValidateObj( m_sAccountName );
}
#endif // DBGFLAG_VALIDATE


//-----------------------------------------------------------------------------
// Purpose: Sends a message to Steam to get a CAccountDetails object
//-----------------------------------------------------------------------------
class CGCJobSendGetAccountDetailsRequest : public CGCJob
{
	CAccountDetailsManager *m_pManager;
	CSteamID m_SteamID;

public:
	CGCJobSendGetAccountDetailsRequest( CGCBase *pGC, CAccountDetailsManager *pManager, const CSteamID &steamID )
		: CGCJob( pGC ), m_pManager( pManager ), m_SteamID( steamID ) {}
	virtual bool BYieldingRunGCJob()
	{
		// Yield immediately to be sure that the calling job gets in the wakeup list
		BYield();

		// These requests should come back very quickly, so if they don't we shouldn't wait very long
		// jamming up the system
		SetJobTimeout( account_details_timeout.GetInt() );

		// Get an empty account details object
		CAccountDetails *pAccount = m_pManager->m_hashAccountDetailsCache.PvRecordFind( m_SteamID.GetAccountID() );
		if ( NULL == pAccount )
		{
			pAccount = m_pManager->m_hashAccountDetailsCache.PvRecordInsert( m_SteamID.GetAccountID() );
		}
		else
		{
			// If the record isn't expired, why is it there?
			Assert( pAccount->BIsExpired() );
			pAccount->Reset();
		}		

		CProtoBufMsg< CGCSystemMsg_GetAccountDetails > msgReqeust( k_EGCMsgGetAccountDetails );
		CProtoBufMsg< CGCSystemMsg_GetAccountDetails_Response > msgReply;
		msgReqeust.Body().set_steamid( m_SteamID.ConvertToUint64() );
		msgReqeust.Body().set_appid( GGCBase()->GetAppID() );
		msgReqeust.ExpectingReply( GJobCur().GetJobID() );

		// try to get the account details at most 2 times
		const int kMaxTries = 2;
		for ( int iTries = 0; iTries < kMaxTries; iTries++ )
		{
			if( !m_pGC->BSendSystemMessage( msgReqeust ) )
			{
				EmitWarning( SPEW_GC, 2, "Unable to send GetAccountDetails system message\n" );
				continue;
			}

			// All of our request messages are identical, so if we get our replies
			// mixed up, it's OK.  Bypass the system used to protect us against
			// mismatched replies.
			ClearFailedToReceivedMsgType( k_EGCMsgGetAccountDetailsResponse );

			// Wait for the reply
			if( !BYieldingWaitForMsg( &msgReply, k_EGCMsgGetAccountDetailsResponse ) )
			{
				EmitWarning( SPEW_GC, 2, "Timeout waiting for GetAccountDetails reply for SteamID %s\n", m_SteamID.Render() );
				continue;
			}

			if ( k_EResultOK != msgReply.GetEResult() )
			{
				EmitInfo( SPEW_GC, 4, 4, "GetAccountDetails request failed with result %d for SteamID %s\n", msgReply.GetEResult(), m_SteamID.Render() );
				break;
			}

			Assert( msgReply.Body().eresult_deprecated() == k_EResultOK );

			// Sanity check the response
			if ( msgReply.Body().has_accountid() && msgReply.Body().accountid() != m_SteamID.GetAccountID() )
			{
				static bool bHasAlerted = false;
				if ( !bHasAlerted )
				{
					GGCBase()->PostAlert( k_EAlertTypeInfo, true, CFmtStr( "GetAccountDetails got a response for account %d, but we were expecting a response for account %s\n", msgReply.Body().accountid(), m_SteamID.Render() ) );
					bHasAlerted = true;
				}

				EmitError( SPEW_GC, "GetAccountDetails got a response for account %d, but we were expecting a response for account %s\n", msgReply.Body().accountid(), m_SteamID.Render() );
				break;
			}

			// All responses should have this
			if ( !msgReply.Body().has_account_name() )
			{
				EmitError( SPEW_GC, "GetAccountDetails got a response with missing fields for SteamID %s\n", m_SteamID.Render() );
				break;
			}

			pAccount->Init( msgReply.Body() );
			m_pManager->CachePersonaName( CSteamID( m_SteamID ), msgReply.Body().persona_name().c_str() );

			// We got a response, so we shouldn't try again
			break;
		}

		m_pManager->WakeWaitingAccountDetailsJobs( m_SteamID );
		return true;
	}
};


//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CCachedPersonaName::CCachedPersonaName()
	:	m_rtimeCached( 0 )	// This initialization value is important because it makes it expired on creation, 
							// and the rest of the code will only ask Steam for a name if the cached entry is expired
	,	m_nLoading( 0 )
	,	m_bPreloading( false )
{
}


//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CCachedPersonaName::~CCachedPersonaName()
{
	Assert( !BIsLoading() );
}


//-----------------------------------------------------------------------------
// Purpose: Sets the newly retrieved persona name
//-----------------------------------------------------------------------------
void CCachedPersonaName::Init( const char *pchPersonaName )
{
	m_sPersonaName = pchPersonaName;
	m_rtimeCached = CRTime::RTime32TimeCur();
	m_bPreloading = false;
}


//-----------------------------------------------------------------------------
// Purpose: Returns true if it's time to remove this entry from the cache
//-----------------------------------------------------------------------------
bool CCachedPersonaName::BIsExpired() const
{
	int nCacheSeconds = BIsValid() ? cv_persona_name_cache_time.GetInt() : cv_persona_name_failure_cache_time.GetInt();

	return m_rtimeCached + nCacheSeconds < CRTime::RTime32TimeCur();
}


//-----------------------------------------------------------------------------
// Purpose: Returns true if there is an actual cached name
//-----------------------------------------------------------------------------
bool CCachedPersonaName::BIsValid() const
{
	return !m_sPersonaName.IsEmpty();
}


//-----------------------------------------------------------------------------
// Purpose: Reverts this to an invalid record
//-----------------------------------------------------------------------------
void CCachedPersonaName::Reset()
{
	m_sPersonaName.Clear();
	m_rtimeCached = CRTime::RTime32TimeCur();
	m_bPreloading = false;
}


//-----------------------------------------------------------------------------
// Purpose: Gets the cached string
//-----------------------------------------------------------------------------
const char *CCachedPersonaName::GetPersonaName() const
{
	return BIsValid() ? m_sPersonaName.Get() : "[unknown]";
}


//-----------------------------------------------------------------------------
// Purpose: Returns if this name is loading
//-----------------------------------------------------------------------------
bool CCachedPersonaName::BIsLoading() const
{
	return m_nLoading > 0 || m_bPreloading;
}


//-----------------------------------------------------------------------------
// Purpose: Sets that this name has been preloaded
//-----------------------------------------------------------------------------
void CCachedPersonaName::SetPreloading()
{
	m_bPreloading = true;
}


//-----------------------------------------------------------------------------
// Purpose: Sets that a job is waiting for this name to be loaded
//-----------------------------------------------------------------------------
void CCachedPersonaName::AddLoadingRef()
{
	m_nLoading++;
}


//-----------------------------------------------------------------------------
// Purpose: Releases the loading ref
//-----------------------------------------------------------------------------
void CCachedPersonaName::ReleaseLoadingRef()
{
	DbgVerify( --m_nLoading >= 0 );
}


#ifdef DBGFLAG_VALIDATE
//-----------------------------------------------------------------------------
// Purpose: Claims all the memory for the CCachedPersonaName object
//-----------------------------------------------------------------------------
void CCachedPersonaName::Validate( CValidator &validator, const char *pchName )
{
	VALIDATE_SCOPE();
	ValidateObj( m_sPersonaName );
}
#endif // DBGFLAG_VALIDATE


//-----------------------------------------------------------------------------
// Purpose: Sends a message to Steam to get a CAccountDetails object
//-----------------------------------------------------------------------------
class CGCJobSendGetPersonaNamesRequest : public CGCJob
{
	CAccountDetailsManager *m_pManager;
	CUtlVector<CSteamID> m_vecSteamIDs;

public:
	CGCJobSendGetPersonaNamesRequest( CGCBase *pGC, CAccountDetailsManager *pManager, CUtlVector<CSteamID> &vecSteamIDs )
		: CGCJob( pGC ), m_pManager( pManager )
	{
		m_vecSteamIDs.Swap( vecSteamIDs );
	}

	virtual bool BYieldingRunGCJob()
	{
		// Yield immediately to be sure that the calling job gets in the wakeup list
		BYield();

		// These requests should come back very quickly, so if they don't we shouldn't wait very long
		// jamming up the system
		SetJobTimeout( persona_name_timeout.GetInt() );

		CProtoBufMsg< CMsgGCGetPersonaNames > msgReqeust( k_EGCMsgGetPersonaNames );
		msgReqeust.ExpectingReply( GJobCur().GetJobID() );

		FOR_EACH_VEC( m_vecSteamIDs, i )
		{
			msgReqeust.Body().add_steamids( m_vecSteamIDs[i].ConvertToUint64() );
		}
		
		CProtoBufMsg< CMsgGCGetPersonaNames_Response > msgReply;
		if( !m_pGC->BSendSystemMessage( msgReqeust ) ||
			!BYieldingWaitForMsg( &msgReply, k_EGCMsgGetPersonaNamesResponse ) )
		{
			FOR_EACH_VEC( m_vecSteamIDs, i )
			{
				m_pManager->CachePersonaNameFailure( m_vecSteamIDs[i] );
			}

			//if we are shutting down, don't bother reporting this issue, we know we won't be able to get persona names
			if( !GGCBase()->GetIsShuttingDown() )
			{
				EmitWarning( SPEW_GC, 2, "GetPersonaNames request failed for %d IDs:", m_vecSteamIDs.Count() );
				FOR_EACH_VEC( m_vecSteamIDs, nID )
				{
					EmitWarning( SPEW_GC, 2, " %s", m_vecSteamIDs[ nID ].Render() );
				}
				EmitWarning( SPEW_GC, 2, "\n" );
			}
		}
		else
		{
			for ( int i = 0; i < msgReply.Body().succeeded_lookups_size(); i++ )
			{
				const CMsgGCGetPersonaNames_Response_PersonaName &result = msgReply.Body().succeeded_lookups( i );
				m_pManager->CachePersonaName( CSteamID( result.steamid() ), result.persona_name().c_str() );
			}
			for ( int i = 0; i < msgReply.Body().failed_lookup_steamids_size(); i++ )
			{
				m_pManager->CachePersonaNameFailure( CSteamID( msgReply.Body().failed_lookup_steamids( i ) ) );
			}
		}

		FOR_EACH_VEC( m_vecSteamIDs, i )
		{
			m_pManager->WakeWaitingPersonaNameJobs( m_vecSteamIDs[i] );
		}
		return true;
	}
};


//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CAccountDetailsManager::CAccountDetailsManager()
	: m_hashAccountDetailsCache( k_nAccountDetailsRunInterval / k_cMicroSecPerShellFrame )
	, m_hashPersonaNameCache( k_nAccountDetailsRunInterval / k_cMicroSecPerShellFrame )
{
	m_hashAccountDetailsCache.Init( k_cAccountDetailsInit, k_cBucketAccountDetails );
	m_hashPersonaNameCache.Init( k_cAccountDetailsInit, k_cBucketAccountDetails );
}


//-----------------------------------------------------------------------------
// Purpose: Work to be done once per frame
//-----------------------------------------------------------------------------
void CAccountDetailsManager::MarkFrame()
{
	m_hashAccountDetailsCache.StartFrameSchedule( true );
	m_hashPersonaNameCache.StartFrameSchedule( true );
	SendBatchedPersonaNamesRequest();
}


//-----------------------------------------------------------------------------
// Purpose: Gets a CAccountDetails object
//-----------------------------------------------------------------------------
CAccountDetails *CAccountDetailsManager::YieldingGetAccountDetails( const CSteamID &steamID, bool bForceReload )
{
	AssertRunningJob();
	if( !steamID.IsValid() || !steamID.BIndividualAccount() )
		return NULL;

	// Check the local cache
	CAccountDetails *pAccountDetails = NULL;
	if ( BFindAccountDetailsInLocalCache( steamID, &pAccountDetails ) )
	{
		if ( pAccountDetails && bForceReload )
		{
			// Clear it, continue with fresh load
			m_hashAccountDetailsCache.Remove( pAccountDetails );
		}
		else
		{
			return pAccountDetails;
		}
	}

	// Not in the local cache, ask Steam
	int iMapIndex = m_mapQueuedAccountDetailsRequests.Find( steamID );
	if ( !m_mapQueuedAccountDetailsRequests.IsValidIndex( iMapIndex ) )
	{
		iMapIndex = m_mapQueuedAccountDetailsRequests.Insert( steamID );
		CGCJobSendGetAccountDetailsRequest *pJob = new CGCJobSendGetAccountDetailsRequest( GGCBase(), this, steamID );
		pJob->StartJob( NULL );
	}

	m_mapQueuedAccountDetailsRequests[iMapIndex].AddToTail( GJobCur().GetJobID() );
	GJobCur().BYieldingWaitForWorkItem();

	// Check again, if it's not there then it's not there
	BFindAccountDetailsInLocalCache( steamID, &pAccountDetails );
	return pAccountDetails;
}


//-----------------------------------------------------------------------------
// Purpose: Finds an AccountDetails object in the local cache. Returns true
//	if it was found, false if it should be checked remotely
//-----------------------------------------------------------------------------
bool CAccountDetailsManager::BFindAccountDetailsInLocalCache( const CSteamID &steamID, CAccountDetails **ppAccount )
{
	CAccountDetails *pAccountLocal = m_hashAccountDetailsCache.PvRecordFind( steamID.GetAccountID() );
	if( NULL == pAccountLocal || pAccountLocal->BIsExpired() )
		return false;

	*ppAccount = pAccountLocal->BIsValid() ? pAccountLocal : NULL;
	return true;
}


//-----------------------------------------------------------------------------
// Purpose: Wakes any jobs waiting on this result
//-----------------------------------------------------------------------------
void CAccountDetailsManager::WakeWaitingAccountDetailsJobs( const CSteamID &steamID )
{
	int iMapIndex = m_mapQueuedAccountDetailsRequests.Find( steamID );
	if ( !m_mapQueuedAccountDetailsRequests.IsValidIndex( iMapIndex ) )
		return; 

	CCopyableUtlVector<JobID_t> &vecJobsWaiting = m_mapQueuedAccountDetailsRequests[iMapIndex];
	FOR_EACH_VEC( vecJobsWaiting, i )
	{
		GGCBase()->GetJobMgr().BRouteWorkItemCompletedDelayed( vecJobsWaiting[i], false );
	}
	m_mapQueuedAccountDetailsRequests.RemoveAt( iMapIndex );
}


//-----------------------------------------------------------------------------
// Purpose: Gets a persona name for a user
//-----------------------------------------------------------------------------
const char *CAccountDetailsManager::YieldingGetPersonaName( const CSteamID &steamID )
{
	VPROF_BUDGET( "CAccountDetailsManager::YieldingGetPersonaName", VPROF_BUDGETGROUP_STEAM );

	AssertRunningJob();

	if( !steamID.IsValid() || !steamID.BIndividualAccount() )
		return "[unknown]";

	// Check the local cache
	CCachedPersonaName *pPersonaName = FindOrCreateCachedPersonaName( steamID );
	if ( !pPersonaName->BIsExpired() )
		return pPersonaName->GetPersonaName(); 

	// Not in the local cache, ask Steam
	pPersonaName->AddLoadingRef();

	// Queue the request and start a lookup job if we have enough pending
	int iMapIndex = m_mapQueuedPersonaNameRequests.Find( steamID );
	if ( !m_mapQueuedPersonaNameRequests.IsValidIndex( iMapIndex ) )
	{
		iMapIndex = m_mapQueuedPersonaNameRequests.Insert( steamID );
		m_vecPendingPersonaNameLookups.AddToTail( steamID );
		if ( m_vecPendingPersonaNameLookups.Count() >= cv_persona_name_batch_size.GetInt() )
		{
			SendBatchedPersonaNamesRequest();
		}
	}

	m_mapQueuedPersonaNameRequests[iMapIndex].AddToTail( GJobCur().GetJobID() );
	GJobCur().BYieldingWaitForWorkItem();

	// At this point we'll either have a persona name or we won't
	pPersonaName->ReleaseLoadingRef();
	return pPersonaName->GetPersonaName();
}


//-----------------------------------------------------------------------------
// Purpose: Let's the system know that we should load the persona name for this
//	user, but does not block on it
//-----------------------------------------------------------------------------
void CAccountDetailsManager::PreloadPersonaName( const CSteamID &steamID )
{
	if( !steamID.IsValid() || !steamID.BIndividualAccount() )
		return;

	// See if we already have it
	CCachedPersonaName *pCachedName = FindOrCreateCachedPersonaName( steamID );
	if ( !pCachedName->BIsExpired() || pCachedName->BIsLoading() )
		return;

	// Queue the request and start a lookup job if we have enough pending
	pCachedName->SetPreloading();
	m_mapQueuedPersonaNameRequests.Insert( steamID );
	m_vecPendingPersonaNameLookups.AddToTail( steamID );
	if ( m_vecPendingPersonaNameLookups.Count() >= cv_persona_name_batch_size.GetInt() )
	{
		SendBatchedPersonaNamesRequest();
	}
}


//-----------------------------------------------------------------------------
// Purpose: Sends a batch of persona name requests
//-----------------------------------------------------------------------------
void CAccountDetailsManager::SendBatchedPersonaNamesRequest()
{
	if ( 0 == m_vecPendingPersonaNameLookups.Count() )
		return;

	// Start the job. This swaps out our buffer with an empty one
	CGCJobSendGetPersonaNamesRequest *pJob = new CGCJobSendGetPersonaNamesRequest( GGCBase(), this, m_vecPendingPersonaNameLookups );
	pJob->StartJob( NULL );
}


//-----------------------------------------------------------------------------
// Purpose: Caches a persona name
//-----------------------------------------------------------------------------
void CAccountDetailsManager::CachePersonaName( const CSteamID &steamID, const char *pchPersonaName )
{
	CCachedPersonaName *pCachedPersonaName = FindOrCreateCachedPersonaName( steamID );
	pCachedPersonaName->Init( pchPersonaName );
}


//-----------------------------------------------------------------------------
// Purpose: Remembers that we failed to cache a persona name
//-----------------------------------------------------------------------------
void CAccountDetailsManager::CachePersonaNameFailure( const CSteamID &steamID )
{
	CCachedPersonaName *pCachedPersonaName = FindOrCreateCachedPersonaName( steamID );
	pCachedPersonaName->Reset();
}


//-----------------------------------------------------------------------------
// Purpose: Purges a specific persona name from the cache
//-----------------------------------------------------------------------------
void CAccountDetailsManager::ClearCachedPersonaName( const CSteamID &steamID )
{
	CCachedPersonaName *pPersonaNameLocal = m_hashPersonaNameCache.PvRecordFind( steamID.GetAccountID() );
	if( NULL != pPersonaNameLocal && !pPersonaNameLocal->BIsLoading() )
	{
		m_hashPersonaNameCache.Remove( pPersonaNameLocal );
	}
}


//-----------------------------------------------------------------------------
// Purpose: Gets a CCachedPersonaName record
//-----------------------------------------------------------------------------
CCachedPersonaName *CAccountDetailsManager::FindOrCreateCachedPersonaName( const CSteamID &steamID )
{
	CCachedPersonaName *pPersonaName = m_hashPersonaNameCache.PvRecordFind( steamID.GetAccountID() );
	if ( NULL != pPersonaName )
		return pPersonaName;
	else
		return m_hashPersonaNameCache.PvRecordInsert( steamID.GetAccountID() );
}


//-----------------------------------------------------------------------------
// Purpose: Wakes any jobs waiting on this result
//-----------------------------------------------------------------------------
void CAccountDetailsManager::WakeWaitingPersonaNameJobs( const CSteamID &steamID )
{
	int iMapIndex = m_mapQueuedPersonaNameRequests.Find( steamID );
	if ( !m_mapQueuedPersonaNameRequests.IsValidIndex( iMapIndex ) )
		return; 

	CCopyableUtlVector<JobID_t> &vecJobsWaiting = m_mapQueuedPersonaNameRequests[iMapIndex];
	FOR_EACH_VEC( vecJobsWaiting, i )
	{
		GGCBase()->GetJobMgr().BRouteWorkItemCompletedDelayed( vecJobsWaiting[i], false );
	}
	m_mapQueuedPersonaNameRequests.RemoveAt( iMapIndex );
}


//-----------------------------------------------------------------------------
// Purpose: Purges old data from the cache. Returns true if there is more
//	work to do
//-----------------------------------------------------------------------------
bool CAccountDetailsManager::BExpireRecords( CLimitTimer &limitTimer )
{
	VPROF_BUDGET( "Expire account details", VPROF_BUDGETGROUP_STEAM );

	for ( CAccountDetails *pDetails = m_hashAccountDetailsCache.PvRecordRun(); NULL != pDetails; pDetails = m_hashAccountDetailsCache.PvRecordRun() )
	{
		if ( pDetails->BIsExpired() )
		{
			m_hashAccountDetailsCache.Remove( pDetails );
		}

		if ( limitTimer.BLimitReached() )
			return true;
	}

	for ( CCachedPersonaName *pPersonaName = m_hashPersonaNameCache.PvRecordRun(); NULL != pPersonaName; pPersonaName = m_hashPersonaNameCache.PvRecordRun() )
	{
		if ( pPersonaName->BIsExpired() && !pPersonaName->BIsLoading() )
		{
			m_hashPersonaNameCache.Remove( pPersonaName );
		}

		if ( limitTimer.BLimitReached() )
			return true;
	}

	return false;
}


//-----------------------------------------------------------------------------
// Purpose: Prints status
//-----------------------------------------------------------------------------
void CAccountDetailsManager::Dump() const
{
	int nJobsWaiting = 0;
	FOR_EACH_MAP_FAST( m_mapQueuedAccountDetailsRequests, iMap )
	{
		nJobsWaiting += m_mapQueuedAccountDetailsRequests[iMap].Count();
	}

	EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tAccount Details: %d cached, %d lookups in flight, %d jobs waiting\n", m_hashAccountDetailsCache.Count(), m_mapQueuedAccountDetailsRequests.Count(), nJobsWaiting );

	nJobsWaiting = 0;
	FOR_EACH_MAP_FAST( m_mapQueuedPersonaNameRequests, iMap )
	{
		nJobsWaiting += m_mapQueuedPersonaNameRequests[iMap].Count();
	}

	EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tPersona Names: %d cached, %d lookups in flight, %d jobs waiting\n", m_hashPersonaNameCache.Count(), m_mapQueuedPersonaNameRequests.Count(), nJobsWaiting );
}

} // namespace GCSDK