//====== Copyright © 1996-2005, Valve Corporation, All rights reserved. =======
//
// Purpose: 
//
//=============================================================================

#include "cbase.h"
#include "achievementmgr.h"
#include "icommandline.h"
#ifdef CLIENT_DLL
#include "tier3/tier3.h"
#include "vgui/ilocalize.h"
#include "achievement_notification_panel.h"
#include "fmtstr.h"
#endif // CLIENT_DLL

CBaseAchievementHelper *CBaseAchievementHelper::s_pFirst = NULL;

BEGIN_DATADESC_NO_BASE( CBaseAchievement )
DEFINE_FIELD( m_iCount,						FIELD_INTEGER ),
END_DATADESC()

BEGIN_DATADESC( CFailableAchievement )
DEFINE_FIELD( m_bActivated,					FIELD_BOOLEAN ),
DEFINE_FIELD( m_bFailed,					FIELD_BOOLEAN ),
END_DATADESC()

//-----------------------------------------------------------------------------
// Purpose: constructor
//-----------------------------------------------------------------------------
CBaseAchievement::CBaseAchievement()
{
	m_iFlags = 0;
	m_iGoal = 0;
	m_iProgressMsgIncrement = 0;
	m_iProgressMsgMinimum = 0;
	m_iAchievementID = 0;
	m_iPointValue = 0;
	m_bHideUntilAchieved = false;
	m_bStoreProgressInSteam = false;
	m_pVictimClassNameFilter = NULL;
	m_pAttackerClassNameFilter = NULL;
	m_pInflictorClassNameFilter = NULL;
	m_pInflictorEntityNameFilter = NULL;
	m_pMapNameFilter = NULL;
	m_pGameDirFilter = NULL;
	m_pszComponentNames = NULL;
	m_pszComponentPrefix = NULL;
	m_iNumComponents = 0;
	m_iComponentPrefixLen = 0;
	m_iComponentBits = 0;
	m_iCount = 0;
	m_iProgressShown = 0;
	m_bAchieved = false;
	m_pAchievementMgr = NULL;
}

//-----------------------------------------------------------------------------
// Purpose: sets flags
//-----------------------------------------------------------------------------
void CBaseAchievement::SetFlags( int iFlags ) 
{ 
	// must always specify a save method
	Assert( iFlags & ( ACH_SAVE_WITH_GAME | ACH_SAVE_GLOBAL ) );

	m_iFlags = iFlags; 
}

//-----------------------------------------------------------------------------
// Purpose: called when a game event being listened for is dispatched
//-----------------------------------------------------------------------------
void CBaseAchievement::FireGameEvent( IGameEvent *event )
{
	// perform common filtering to make it simpler to write achievements
	if ( !IsActive() )
		return;

	// if the achievement only applies to a specific map, and it's not the current map, skip it
	if ( m_pMapNameFilter && ( 0 != Q_strcmp( m_pAchievementMgr->GetMapName(), m_pMapNameFilter ) ) )
		return;

	const char *name = event->GetName();
	if ( 0 == Q_strcmp( name, "teamplay_round_win" ) )
	{
		// if this is a round win and the achievement wants full round events only, filter this out
		// if this is not the end of a full round
		if ( ( m_iFlags & ACH_FILTER_FULL_ROUND_ONLY ) && ( false == event->GetBool( "full_round" ) ) )
			return;
	}

	// let the achievement handle the event
	FireGameEvent_Internal( event );
}


//-----------------------------------------------------------------------------
// Purpose: sets victim class to filter with
//-----------------------------------------------------------------------------
void CBaseAchievement::SetVictimFilter( const char *pClassName )
{
	m_pVictimClassNameFilter = pClassName;
}

//-----------------------------------------------------------------------------
// Purpose: sets attacker class to filter with
//-----------------------------------------------------------------------------
void CBaseAchievement::SetAttackerFilter( const char *pClassName )
{
	m_pAttackerClassNameFilter = pClassName;
}

//-----------------------------------------------------------------------------
// Purpose: sets inflictor class to filter with
//-----------------------------------------------------------------------------
void CBaseAchievement::SetInflictorFilter( const char *pClassName )
{
	m_pInflictorClassNameFilter = pClassName;
}

//-----------------------------------------------------------------------------
// Purpose: sets inflictor entity name to filter with
//-----------------------------------------------------------------------------
void CBaseAchievement::SetInflictorEntityNameFilter( const char *pEntityName )
{
	m_pInflictorEntityNameFilter = pEntityName;
}

//-----------------------------------------------------------------------------
// Purpose: sets map name to filter with
//-----------------------------------------------------------------------------
void CBaseAchievement::SetMapNameFilter( const char *pMapName )
{
	m_pMapNameFilter = pMapName;
}

//-----------------------------------------------------------------------------
// Purpose: sets game dir to filter with.  Note: in general, achievements should
//			only be compiled into products they pertain to.  But if there are
//			any game-specific achievements which need to be in a binary shared
//			across products (e.g. Ep1 & Ep2), use the game dir as a runtime
//			filter.
//-----------------------------------------------------------------------------
void CBaseAchievement::SetGameDirFilter( const char *pGameDir )
{
	m_pGameDirFilter = pGameDir;
}

//-----------------------------------------------------------------------------
// Purpose: sets prefix to look for in map event string to identify a component
//			for this achievement
//-----------------------------------------------------------------------------
void CBaseAchievement::SetComponentPrefix( const char *pPrefix )
{
	m_pszComponentPrefix = pPrefix;
	m_iComponentPrefixLen = Q_strlen( pPrefix );
}

//-----------------------------------------------------------------------------
// Purpose: called when a kill that passes filter critera occurs.  This
//			is the default implementation, achievements can override to
//			do special handling
//-----------------------------------------------------------------------------
void CBaseAchievement::Event_EntityKilled( CBaseEntity *pVictim, CBaseEntity *pAttacker, CBaseEntity *pInflictor, IGameEvent *event )
{
	// extra paranoid check: should only get here if registered as a kill event listener
	Assert( GetFlags() & ACH_LISTEN_KILL_EVENTS );
	if ( !( GetFlags() & ACH_LISTEN_KILL_EVENTS ) )
		return;

	// default implementation is just to increase count when filter criteria pass
	IncrementCount();
}

//-----------------------------------------------------------------------------
// Purpose: called when an event that counts toward an achievement occurs
//-----------------------------------------------------------------------------
void CBaseAchievement::IncrementCount()
{
	if ( !IsAchieved() )
	{
		if ( !m_pAchievementMgr->CheckAchievementsEnabled() )
		{
			Msg( "Achievements disabled, ignoring achievement progress for %s\n", GetName() );
			return;
		}

		// on client, where the count is kept, increment count
		m_iCount++;
		// if this achievement gets saved w/global state, flag our global state as dirty
		if ( GetFlags() & ACH_SAVE_GLOBAL )
		{
			m_pAchievementMgr->SetDirty( true );
		}

		if ( cc_achievement_debug.GetInt() )
		{
			Msg( "Achievement count increased for %s: %d/%d\n", GetName(), m_iCount, m_iGoal );
		}

		// if this achievement's progress should be stored in Steam, set the steam stat for it
		if ( StoreProgressInSteam() && steamapicontext->SteamUserStats() )
		{
			// Set the Steam stat with the same name as the achievement.  Only cached locally until we upload it.
			char pszProgressName[1024];
			Q_snprintf( pszProgressName, 1024, "%s_STAT", GetName() );
			bool bRet = steamapicontext->SteamUserStats()->SetStat( CGameID( engine->GetAppID() ), pszProgressName, m_iCount );
			if ( !bRet )
			{
				DevMsg( "ISteamUserStats::GetStat failed to set progress value in Steam for achievement %s\n", pszProgressName );
			}

			// Upload user data to commit the change to Steam so if the client crashes, progress isn't lost.
			// Only upload if we haven't uploaded recently, to keep us from spamming Steam with uploads.  If we don't
			// upload now, it will get uploaded no later than level shutdown.
			if ( ( m_pAchievementMgr->GetTimeLastUpload() == 0 ) || ( Plat_FloatTime() - m_pAchievementMgr->GetTimeLastUpload() > 60 * 15 ) )
			{
				m_pAchievementMgr->UploadUserData();
			}
		}

		// if we've hit goal, award the achievement
		if ( m_iGoal > 0 )
		{
			if ( m_iCount >= m_iGoal )
			{
				AwardAchievement();
			}
			else
			{	
				HandleProgressUpdate();
			}
		}
	}
}

void CBaseAchievement::HandleProgressUpdate()
{
	// if we've hit the right # of progress steps to show a progress notification, show it
	if ( ( m_iProgressMsgIncrement > 0 ) && m_iCount >= m_iProgressMsgMinimum && ( 0 == ( m_iCount % m_iProgressMsgIncrement ) ) )
	{
		// which notification is this
		int iProgress = m_iCount / m_iProgressMsgIncrement;
		// if we haven't already shown this progress step, show it
		if ( iProgress > m_iProgressShown )
		{
			ShowProgressNotification();
			// remember progress step shown so we don't show it again if the player loads an earlier save game
			// and gets past this point again
			m_iProgressShown = iProgress;
			m_pAchievementMgr->SetDirty( true );
		}					
	}
}

//-----------------------------------------------------------------------------
// Purpose: calculates at how many steps we should show a progress notification
//-----------------------------------------------------------------------------
void CBaseAchievement::CalcProgressMsgIncrement()
{
	// by default, show progress at every 25%
	m_iProgressMsgIncrement = m_iGoal / 4;
	// if goal is not evenly divisible by 4, try some other values
	if ( 0 != ( m_iGoal % 4 ) )
	{
		if ( 0 == ( m_iGoal % 3 ) )
		{
			// if evenly divisible by 3, use that
			m_iProgressMsgIncrement = m_iGoal / 3;
		}
		else if ( 0 == ( m_iGoal % 5 ) )
		{
			// if evenly divisible by 5, use that
			m_iProgressMsgIncrement = m_iGoal / 5;
		}
		// otherwise stick with divided by 4, rounded off
	}

	// don't show progress notifications for less than 5 things
	if ( m_iProgressMsgIncrement < 5 )
	{
		m_iProgressMsgIncrement = 0;
	}
}

//-----------------------------------------------------------------------------
// Purpose: see if we should award an achievement based on what just happened
//-----------------------------------------------------------------------------
void CBaseAchievement::EvaluateNewAchievement()
{
	if ( !IsAchieved() && m_iGoal > 0 && m_iCount >= m_iGoal )
	{
		AwardAchievement();
	}
}

//-----------------------------------------------------------------------------
// Purpose: determine if we should set this achievement to be achieved based
//			on other state.  Used at init time.
//-----------------------------------------------------------------------------
void CBaseAchievement::EvaluateIsAlreadyAchieved()
{
	if ( !IsAchieved() && m_iGoal > 0 && m_iCount >= m_iGoal )
	{
		m_bAchieved = true;
	}
}

//-----------------------------------------------------------------------------
// Purpose: called a map event for this achievement occurs
//-----------------------------------------------------------------------------
void CBaseAchievement::OnMapEvent( const char *pEventName )
{
	Assert( m_iFlags & ACH_LISTEN_MAP_EVENTS );

	if ( 0 == Q_stricmp( pEventName, GetName() ) )
	{
		IncrementCount();
	}
}

//-----------------------------------------------------------------------------
// Purpose: called when an achievement is awarded
//-----------------------------------------------------------------------------
void CBaseAchievement::AwardAchievement()
{
	Assert( !IsAchieved() );
	if ( IsAchieved() )
		return;

	m_pAchievementMgr->AwardAchievement( m_iAchievementID );
}

//-----------------------------------------------------------------------------
// Purpose: called when a component of a multi-component event is found
//-----------------------------------------------------------------------------
void CBaseAchievement::OnComponentEvent( const char *pchComponentName )
{
	// find the component name in our list
	for ( int i = 0; i < m_iNumComponents; i++ )
	{
		if ( 0 == Q_strcmp( pchComponentName, m_pszComponentNames[i] ) )
		{
			EnsureComponentBitSetAndEvaluate( i );
			return;
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: sets the specified component bit # if it is not already.
//			If it does get set, evaluate if this satisfies an achievement
//-----------------------------------------------------------------------------
void CBaseAchievement::EnsureComponentBitSetAndEvaluate( int iBitNumber )
{
	Assert( iBitNumber < 64 );	// this is bit #, not a bit mask

	if ( IsAchieved() )
		return;

	// calculate which bit this component corresponds to
	uint64 iBitMask = ( (uint64) 1 ) << iBitNumber;

	// see if we already have gotten this component
	if ( 0 == ( iBitMask & m_iComponentBits ) )
	{				
		if ( !m_pAchievementMgr->CheckAchievementsEnabled() )
		{
			Msg( "Achievements disabled, ignoring achievement component for %s\n", GetName() );
			return;
		}

		// new component, set the bit and increment the count
		SetComponentBits( m_iComponentBits | iBitMask );
		Assert( m_iCount <= m_iGoal );
		if ( m_iCount == m_iGoal )
		{
			// all components found, award the achievement (and save state)
			AwardAchievement();
		}
		else
		{
			// save our state at the next good opportunity
			m_pAchievementMgr->SetDirty( true );		

			if ( cc_achievement_debug.GetInt() )
			{
				Msg( "Component %d for achievement %s found\n", iBitNumber, GetName() );
			}

			ShowProgressNotification();
		}				
	}
	else
	{
		if ( cc_achievement_debug.GetInt() )
		{
			Msg( "Component %d for achievement %s found, but already had that component\n", iBitNumber, GetName() );
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: displays achievement progress notification in the HUD
//-----------------------------------------------------------------------------
void CBaseAchievement::ShowProgressNotification()
{
	if ( !ShouldShowProgressNotification() )
		return;

	IGameEvent *event = gameeventmanager->CreateEvent( "achievement_event" );
	if ( event )
	{
		event->SetString( "achievement_name", GetName() );
		event->SetInt( "cur_val", m_iCount );
		event->SetInt( "max_val", m_iGoal );
#ifdef GAME_DLL
		gameeventmanager->FireEvent( event );
#else
		gameeventmanager->FireEventClientSide( event );
#endif
	}	
}

//-----------------------------------------------------------------------------
// Purpose: clears dynamic state for this achievement
//-----------------------------------------------------------------------------
void CBaseAchievement::PreRestoreSavedGame()
{
	// if this achievement gets saved with the game, clear its state
	if ( m_iFlags & ACH_SAVE_WITH_GAME )
	{
		m_iCount = 0;
	}
}

//-----------------------------------------------------------------------------
// Purpose: called after the data in this achievement has been restored from saved game
//-----------------------------------------------------------------------------
void CBaseAchievement::PostRestoreSavedGame()
{
	EvaluateIsAlreadyAchieved();
}

//-----------------------------------------------------------------------------
// Purpose: sets component bits for this achievement
//-----------------------------------------------------------------------------
void CBaseAchievement::SetComponentBits( uint64 iComponentBits ) 
{ 
	Assert( m_iFlags & ACH_HAS_COMPONENTS );
	// set the bit field
	m_iComponentBits = iComponentBits; 
	// count how many bits are set and save that as the count
	int iNumBitsSet = 0;
	while ( iComponentBits > 0 )
	{
		if ( iComponentBits & 1 )
		{
			iNumBitsSet++;
		}
		iComponentBits >>= 1;
	}
	m_iCount = iNumBitsSet;
}

//-----------------------------------------------------------------------------
// Purpose: returns whether we should save this achievement with a save game
//-----------------------------------------------------------------------------
bool CBaseAchievement::ShouldSaveWithGame() 
{ 
	// save if we should get saved with the game, have a non-zero count, and have not
	// been achieved (at which point the achievement state gets saved globally)
	return ( ( m_iFlags & ACH_SAVE_WITH_GAME ) > 0 && ( GetCount() > 0 ) && !IsAchieved() );
}

//-----------------------------------------------------------------------------
// Purpose: returns whether we should save this achievement to the global file
//-----------------------------------------------------------------------------
bool CBaseAchievement::ShouldSaveGlobal() 
{ 
	// save if we should get saved globally and have a non-zero count, or if we have been achieved
	return ( ( ( m_iFlags & ACH_SAVE_GLOBAL ) > 0 && ( GetCount() > 0 ) ) || IsAchieved() || ( m_iProgressShown > 0 ) );
}

//-----------------------------------------------------------------------------
// Purpose: returns whether this achievement is active
//-----------------------------------------------------------------------------
bool CBaseAchievement::IsActive()
{
	// we're not active if already achieved
	if ( IsAchieved() )	
		return false;
	
	// if there's a map filter and we're not on the specified map, we're not active
	if ( ( m_pMapNameFilter ) && ( 0 != Q_strcmp( m_pAchievementMgr->GetMapName(), m_pMapNameFilter ) ) )
		return false;

	return true;
}

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CFailableAchievement::CFailableAchievement() : CBaseAchievement()
{
	m_bFailed = false;
	m_bActivated = false;
}

//-----------------------------------------------------------------------------
// Purpose: returns whether we should save this achievement with a save game
//-----------------------------------------------------------------------------
bool CFailableAchievement::ShouldSaveWithGame() 
{ 
	// save if we should get saved with the game, and are active or have failed
	return ( ( ( m_iFlags & ACH_SAVE_WITH_GAME ) > 0 ) && ( m_bActivated || m_bFailed ) );
}

//-----------------------------------------------------------------------------
// Purpose: clears dynamic state for this achievement
//-----------------------------------------------------------------------------
void CFailableAchievement::PreRestoreSavedGame() 
{ 
	m_bFailed = false;
	m_bActivated = false;

	BaseClass::PreRestoreSavedGame();
}


//-----------------------------------------------------------------------------
// Purpose: called after the data in this achievement has been restored from saved game
//-----------------------------------------------------------------------------
void CFailableAchievement::PostRestoreSavedGame()
{
	// if there is no activation event set for this achievement, it is always active, activate it now
	if ( !m_bFailed && !GetActivationEventName()[0] )
	{
		m_bActivated = true;
	}

	if ( m_bActivated )
	{
		Activate();
	}

	BaseClass::PostRestoreSavedGame();
}

//-----------------------------------------------------------------------------
// Purpose: called when a map event occurs
//-----------------------------------------------------------------------------
void CFailableAchievement::OnMapEvent( const char *pEventName )
{
	// if we're not activated and we got the activation event, activate
	if ( !m_bActivated && ( 0 == Q_stricmp( pEventName, GetActivationEventName() ) ) )
	{
		OnActivationEvent();
	}
	// if this is the evaluation event, see if we've failed or not
	else if ( m_bActivated && 0 == Q_stricmp( pEventName, GetEvaluationEventName() ) )
	{
		OnEvaluationEvent();
	}
}

//-----------------------------------------------------------------------------
// Purpose: Called when this failable achievement is activated
//-----------------------------------------------------------------------------
void CFailableAchievement::Activate()
{
	m_bActivated = true;
	ListenForEvents();
	if ( cc_achievement_debug.GetInt() )
	{
		Msg( "Failable achievement %s now active\n", GetName() );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Called when this failable achievement should be evaluated
//-----------------------------------------------------------------------------
void CFailableAchievement::OnEvaluationEvent()
{
	if ( !m_bFailed )
	{
		// we haven't failed and we reached the evaluation point, we've succeeded
		IncrementCount();
	}
	
	if ( cc_achievement_debug.GetInt() )
	{
		Msg( "Failable achievement %s has been evaluated (%s), now inactive\n", GetName(), m_bFailed ? "FAILED" : "AWARDED" );
	}
}


//-----------------------------------------------------------------------------
// Purpose: Sets this achievement to failed
//-----------------------------------------------------------------------------
void CFailableAchievement::SetFailed()
{
	if ( !m_bFailed )
	{
		m_bFailed = true;

		if ( cc_achievement_debug.GetInt() )
		{
			Msg( "Achievement failed: %s (%s)\n", GetName(), GetName() );
		}	
	}	
}

//===========================================

void CAchievement_AchievedCount::Init() 
{
	SetFlags( ACH_SAVE_GLOBAL );
	SetGoal( 1 );
	SetAchievementsRequired( 0, 0, 0 );
}

// Count how many achievements have been earned in our range
void CAchievement_AchievedCount::OnSteamUserStatsStored( void )
{
	int iAllAchievements = m_pAchievementMgr->GetAchievementCount();
	int iAchieved = 0;

	for ( int i=0; i<iAllAchievements; ++i )
	{		
		IAchievement* pCurAchievement = (IAchievement*)m_pAchievementMgr->GetAchievementByIndex( i );
		Assert ( pCurAchievement );

		int iAchievementID = pCurAchievement->GetAchievementID();
		if ( iAchievementID < m_iLowRange || iAchievementID > m_iHighRange )
			continue;

		if ( pCurAchievement->IsAchieved() )
		{
			iAchieved++;
		}
	}

	if ( iAchieved >= m_iNumRequired )
	{
		IncrementCount();
	}
}

void CAchievement_AchievedCount::SetAchievementsRequired( int iNumRequired, int iLowRange, int iHighRange )
{
	m_iNumRequired = iNumRequired;
	m_iLowRange = iLowRange;
	m_iHighRange = iHighRange;
}
