//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================
#include "cbase.h"

#include "econ_notifications.h"

#include "hudelement.h"
#include "iclientmode.h"
#include "ienginevgui.h"
#include "vgui_avatarimage.h"
#include "vgui_controls/Controls.h"
#include "vgui_controls/EditablePanel.h"
#include "vgui_controls/TextImage.h"
#include "vgui/ILocalize.h"
#include "vgui/ISurface.h"
#include "vgui/IVGui.h"
#include "rtime.h"
#include "econ_controls.h"
#include "hud_basechat.h"
#include "hud_vote.h"
#include "inputsystem/iinputsystem.h"
#include "iinput.h"

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

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

ConVar cl_notifications_show_ingame( "cl_notifications_show_ingame", "1", FCVAR_ARCHIVE, "Whether notifications should show up in-game." );
ConVar cl_notifications_max_num_visible( "cl_notifications_max_num_visible", "3", FCVAR_ARCHIVE, "How many notifications are visible in-game." );
ConVar cl_notifications_move_time( "cl_notifications_move_time", "0.5", FCVAR_ARCHIVE, "How long it takes for a notification to move." );

// notification queue holds all the notifications
class CEconNotificationQueue
{
public:
	CEconNotificationQueue();
	~CEconNotificationQueue();

	int AddNotification( CEconNotification *pNotification );
	void RemoveAllNotifications();
	void RemoveNotification( int iID );
	void RemoveNotification( CEconNotification *pNotification );
	void RemoveNotifications( NotificationFilterFunc func );
	int CountNotifications( NotificationFilterFunc func );
	void VisitNotifications( CEconNotificationVisitor &visitor );
	CEconNotification *GetNotification( int iID );
	CEconNotification *GetNotificationByIndex( int idx );
	void Update();
	bool HasItems() { return m_vecNotifications.Count() != 0; }
	const CUtlVector< CEconNotification *> &GetItems() { return m_vecNotifications; }

private:
	int m_iIDGenerator;
	CUtlVector< CEconNotification *> m_vecNotifications;
};
static CEconNotificationQueue g_notificationQueue;

CEconNotificationQueue::CEconNotificationQueue()
	: m_iIDGenerator(0)
{
}

CEconNotificationQueue::~CEconNotificationQueue()
{
}

int CEconNotificationQueue::AddNotification( CEconNotification *pNotification )
{
	int iID = ++m_iIDGenerator;
	pNotification->m_iID = iID;
	m_vecNotifications.AddToTail( pNotification );
	return iID;
}

void CEconNotificationQueue::RemoveAllNotifications()
{
	m_vecNotifications.PurgeAndDeleteElements();
}

void CEconNotificationQueue::RemoveNotification( int iID )
{
	FOR_EACH_VEC( m_vecNotifications, i )
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		if ( pNotification->GetID() == iID )
		{
			delete pNotification;
			m_vecNotifications.Remove( i );
			return;
		}
	}
}

void CEconNotificationQueue::RemoveNotification( CEconNotification *pNotification )
{
	if ( pNotification )
	{
		RemoveNotification( pNotification->GetID() );
	}
}

void CEconNotificationQueue::RemoveNotifications( NotificationFilterFunc func )
{
	for ( int i = 0; i < m_vecNotifications.Count(); ++i)
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		if ( func( pNotification ) )
		{
			pNotification->MarkForDeletion();
		}
	}
}

int CEconNotificationQueue::CountNotifications( NotificationFilterFunc func )
{
	int nResult = 0;
	for ( int i = 0; i < m_vecNotifications.Count(); ++i)
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		if ( func( pNotification ) )
		{
			++nResult;
		}
	}

	return nResult;
}

void CEconNotificationQueue::VisitNotifications( CEconNotificationVisitor &visitor )
{
	for ( int i = 0; i < m_vecNotifications.Count(); ++i )
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		visitor.Visit( *pNotification );
	}
}

CEconNotification *CEconNotificationQueue::GetNotification( int iID )
{
	FOR_EACH_VEC( m_vecNotifications, i )
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		if ( pNotification->GetID() == iID )
		{
			return pNotification;
		}
	}
	return NULL;
}

CEconNotification *CEconNotificationQueue::GetNotificationByIndex( int idx )
{
	if ( idx < 0 || idx >= m_vecNotifications.Count() )
	{
		Assert( !"Invalid index passed to GetNotificationByIndex" );
		return NULL;
	}
	return m_vecNotifications[idx];
}

void CEconNotificationQueue::Update()
{
	float flNowTime = engine->Time();
	for ( int i = 0; i < m_vecNotifications.Count(); )
	{
		CEconNotification *pNotification = m_vecNotifications[i];
		if ( pNotification->GetIsInUse() == false && pNotification->GetExpireTime() >= 0 && pNotification->GetExpireTime() < flNowTime )
		{
			pNotification->Expired();
			delete pNotification;
			m_vecNotifications.Remove( i );
			continue;
		}
		pNotification->UpdateTick();
		++i;
	}
}

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

static void ColorizeText( CEconNotification *pNotification, CExLabel *pControl, const wchar_t* wszText )
{
	static wchar_t wszStrippedText[2048];

	if ( pControl == NULL )
		return;

	pControl->GetTextImage()->ClearColorChangeStream();

	if ( wszText == NULL )
	{
		pControl->SetText( L"" );
		return;
	}

	Color newColor = pControl->GetFgColor();
	int endIdx = 0;
	int insertIdx = 0;
	bool bContinue = true;
	while ( bContinue )
	{
		bool bSetColor = false;
		switch ( wszText[endIdx] )
		{
			case 0:
				bContinue = false;
				break;
			case COLOR_NORMAL:
			case COLOR_USEOLDCOLORS:
				newColor = pControl->GetFgColor();
				bSetColor = true;
				break;
			case COLOR_PLAYERNAME:
				newColor = g_ColorYellow;
				bSetColor = true;
				break;
			case COLOR_LOCATION:
				newColor = g_ColorDarkGreen;
				bSetColor = true;
				break;
			case COLOR_ACHIEVEMENT:
				{
					vgui::IScheme *pSourceScheme = vgui::scheme()->GetIScheme( vgui::scheme()->GetScheme( "SourceScheme" ) ); 
					if ( pSourceScheme )
					{
						newColor = pSourceScheme->GetColor( "SteamLightGreen", pControl->GetBgColor() );
					}
					else
					{
						newColor = pControl->GetFgColor();
					}
					bSetColor = true;
				}
				break;
			case COLOR_CUSTOM:
				newColor = pControl->GetFgColor();
				KeyValues *pKeyValues = pNotification->GetKeyValues();
				if ( pKeyValues )
				{
					KeyValues* pColor = pKeyValues->FindKey( "custom_color" );
					if ( pColor )
					{
						newColor = pColor->GetColor();
					}
				}
				bSetColor = true;
				break;
		}
		if ( bSetColor )
		{
			pControl->GetTextImage()->AddColorChange( newColor, insertIdx );
		}
		else
		{
			wszStrippedText[insertIdx++] = wszText[endIdx];
		}
		++endIdx;
	}
	pControl->SetText( wszStrippedText );
}

// generic "toast" for notifications
class CGenericNotificationToast : public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CGenericNotificationToast, vgui::EditablePanel );
public:
	CGenericNotificationToast( vgui::Panel *parent, int iNotificationID, bool bMainMenu );
	virtual ~CGenericNotificationToast();

	virtual void ApplySchemeSettings( vgui::IScheme *pScheme );
	virtual void PerformLayout();
protected:
	int 				m_iNotificationID;
	vgui::Panel			*m_pAvatarBG;
	CAvatarImagePanel	*m_pAvatar;
	bool				m_bMainMenu;
};

CGenericNotificationToast::CGenericNotificationToast( vgui::Panel *parent, int iNotificationID, bool bMainMenu )
	: BaseClass( parent, "GenericNotificationToast" )
	, m_iNotificationID( iNotificationID )
	, m_pAvatar( NULL )
	, m_pAvatarBG( NULL )
	, m_bMainMenu( bMainMenu )
{
}

CGenericNotificationToast::~CGenericNotificationToast()
{
}

void CGenericNotificationToast::ApplySchemeSettings( vgui::IScheme *pScheme )
{
	BaseClass::ApplySchemeSettings( pScheme );

	CEconNotification *pNotification = NotificationQueue_Get( m_iNotificationID );
	bool bHighPriority = pNotification && pNotification->BHighPriority();
	KeyValues *pConditions = NULL;

	if ( bHighPriority )
	{
		pConditions = new KeyValues( "conditions" );
		if ( bHighPriority )
		{
			KeyValues *pSubKey = new KeyValues( "if_high_priority" );
			pConditions->AddSubKey( pSubKey );
		}
	}

	if ( m_bMainMenu )
	{
		LoadControlSettings( "Resource/UI/Econ/GenericNotificationToastMainMenu.res", NULL, NULL, pConditions );
	}
	else
	{
		LoadControlSettings( "Resource/UI/Econ/GenericNotificationToast.res", NULL, NULL, pConditions );
	}

	if ( pConditions )
	{
		pConditions->deleteThis();
	}

	m_pAvatar = dynamic_cast< CAvatarImagePanel *>( FindChildByName("AvatarImage") );
	m_pAvatarBG = FindChildByName("AvatarBGPanel");

	if ( pNotification )
	{
		if ( pNotification->GetSteamID() == CSteamID() )
		{
			ColorizeText( pNotification, dynamic_cast< CExLabel* >( FindChildByName( "TextLabel" ) ), pNotification->GetText() );
		}
		else
		{
			ColorizeText( pNotification, dynamic_cast< CExLabel* >( FindChildByName( "AvatarTextLabel" ) ), pNotification->GetText() );
		}
	}
}

void CGenericNotificationToast::PerformLayout()
{
	BaseClass::PerformLayout();

	CSteamID steamID;
	CEconNotification *pNotification = NotificationQueue_Get( m_iNotificationID );
	if ( pNotification )
	{
		steamID = pNotification->GetSteamID();
	}

	int iMinHeight = 0;
	if ( m_pAvatar )
	{
		if ( steamID != CSteamID() )
		{
			m_pAvatar->SetVisible( true );
			m_pAvatar->SetShouldDrawFriendIcon( false );
			m_pAvatar->SetPlayer( steamID, k_EAvatarSize64x64 );
			// make sure there's a minimum height
			// note we use iY to ensure that there's a buffer below too
			int iX, iY, iWidth, iHeight;
			m_pAvatar->GetBounds( iX, iY, iWidth, iHeight );
			iMinHeight = 2 * iY + iHeight;
		}
		else
		{
			m_pAvatar->SetVisible( false );
			m_pAvatar->ClearAvatar();
		}
	}
	if ( m_pAvatarBG )
	{
		m_pAvatarBG->SetVisible( m_pAvatar != NULL && m_pAvatar->IsVisible() );
	}

	const char *pTextLabelName = steamID != CSteamID() ? "AvatarTextLabel" : "TextLabel";
	CExLabel* pText = dynamic_cast< CExLabel *>( FindChildByName( pTextLabelName ) );
	if ( pText )
	{
		pText->SetVisible( true );
		pText->InvalidateLayout( true, false );
		int iWidth, iHeight;
		pText->GetSize( iWidth, iHeight );
		int iContentWidth, iContentHeight;
		pText->GetContentSize( iContentWidth, iContentHeight );
		pText->SetSize( iWidth, iContentHeight );
		int iDelta = iContentHeight - iHeight;
		// resize ourselves to fit
		int iContainerWidth, iContainerHeight;
		GetSize( iContainerWidth, iContainerHeight );
		SetSize( iContainerWidth, MAX( iContainerHeight + iDelta, iMinHeight ) );
	}
}

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

class CNotificationToastControl : public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CNotificationToastControl, vgui::EditablePanel );	
public:
	CNotificationToastControl( vgui::EditablePanel *pParent, vgui::EditablePanel *pNotificationToast, int iNotificationID, bool bAddControls )
		: BaseClass( pParent, bAddControls ? "NotificationToastControl" : "NotificationToastContainer" )
		, m_pChild( pNotificationToast )
		, m_iNotificationID( iNotificationID )
		, m_bAddControls( bAddControls )
		, m_pTriggerButton( NULL )
		, m_pAcceptButton( NULL )
		, m_pDeclineButton( NULL )
		, m_iOverrideHeight( 0 )
	{
		m_pChild->SetParent( this );
	}

	virtual ~CNotificationToastControl()
	{
	}

	virtual void ApplySchemeSettings( vgui::IScheme *scheme )
	{
		CEconNotification *pNotification = g_notificationQueue.GetNotification( m_iNotificationID );

		// It is not entirely clear why pNotification is allowed to be NULL. Weapon switching
		// with pyro was causing crashes because of pNotification being NULL, and there were
		// previously existing checks, but it's not clear why.
		CEconNotification::EType eNotificationType = pNotification ? pNotification->NotificationType() \
		                                                           : CEconNotification::eType_Basic;

		bool bHighPriority = pNotification && pNotification->BHighPriority();
		bool bCanDelete = false;
		bool bCanAcceptDecline = false;
		bool bCanTrigger = false;
		bool bOneButton = false;

		switch ( eNotificationType )
		{
			case CEconNotification::eType_AcceptDecline:
				bCanAcceptDecline = true;
				break;
			case CEconNotification::eType_Basic:
				bCanDelete = true;
				bOneButton = true;
				break;
			case CEconNotification::eType_MustTrigger:
				bCanTrigger = true;
				bOneButton = true;
				break;
			case CEconNotification::eType_Trigger:
				bCanTrigger = true;
				bCanDelete = true;
				break;
			default:
				Assert( !"Unhandled enum type" );
		}

		KeyValues *pConditions = NULL;

		if ( bOneButton || bHighPriority )
		{
			pConditions = new KeyValues( "conditions" );
			if ( bOneButton )
			{
				KeyValues *pSubKey = new KeyValues( "if_one_button" );
				pConditions->AddSubKey( pSubKey );
			}
			if ( bHighPriority )
			{
				KeyValues *pSubKey = new KeyValues( "if_high_priority" );
				pConditions->AddSubKey( pSubKey );
			}
		}

		if ( m_bAddControls )
		{
			LoadControlSettings( "Resource/UI/Econ/NotificationToastControl.res", NULL, NULL, pConditions );
		}
		else
		{
			LoadControlSettings( "Resource/UI/Econ/NotificationToastContainer.res", NULL, NULL, pConditions );
		}

		if ( pConditions )
		{
			pConditions->deleteThis();
		}

		BaseClass::ApplySchemeSettings( scheme );

		GetSize( m_iOriginalWidth, m_iOriginalHeight );

		CExButton *pDeleteButton = dynamic_cast< CExButton *>( FindChildByName( "DeleteButton" ) );

		if ( pDeleteButton && bCanDelete )
		{
			pDeleteButton->AddActionSignalTarget( this );
			pDeleteButton->SetVisible ( pNotification != NULL );
		}

		if ( pNotification == NULL )
			return;

		if ( bCanAcceptDecline )
		{
			m_pAcceptButton = dynamic_cast< CExButton * >( FindChildByName( "AcceptButton" ) );
			m_pDeclineButton = dynamic_cast< CExButton * >( FindChildByName( "DeclineButton" ) );
			if ( m_pAcceptButton && m_pDeclineButton )
			{
				m_pAcceptButton->AddActionSignalTarget( this );
				m_pDeclineButton->AddActionSignalTarget( this );
				m_pAcceptButton->SetVisible( true );
				m_pDeclineButton->SetVisible( true );
				int posX, posY;
				m_pAcceptButton->GetPos( posX, posY );
				m_iButtonOffsetY = GetTall() - posY;
			}
		}
		if ( bCanTrigger )
		{
			m_pTriggerButton = dynamic_cast< CExButton *>( FindChildByName( "TriggerButton" ) );
			if ( m_pTriggerButton )
			{
				m_pTriggerButton->AddActionSignalTarget( this );
				m_pTriggerButton->SetVisible( true );
				int posX, posY;
				m_pTriggerButton->GetPos( posX, posY );
				m_iButtonOffsetY = GetTall() - posY;
			}
		}
	}

	virtual void PerformLayout()
	{
		BaseClass::PerformLayout();
		m_pChild->PerformLayout();
		int iWidth, iHeight;
		m_pChild->GetSize( iWidth, iHeight );

		// position control buttons
		if ( iHeight + m_iButtonOffsetY > m_iOriginalHeight )
		{
			if ( m_pAcceptButton && m_pDeclineButton )
			{
				int posX, posY;
				m_pAcceptButton->GetPos( posX, posY );
//				int newPosY = iHeight;
//				iHeight += m_iButtonOffsetY;
//				m_pAcceptButton->SetPos( posX, newPosY );
//				m_pDeclineButton->GetPos( posX, posY );
//				m_pDeclineButton->SetPos( posX, newPosY );
			}
			else if ( m_pTriggerButton )
			{
				int posX, posY;
				m_pTriggerButton->GetPos( posX, posY );
//				posY = iHeight;
//				iHeight += m_iButtonOffsetY;
//				m_pTriggerButton->SetPos( posX, posY );
			}
		}

		// position help label
		CEconNotification *pNotification = g_notificationQueue.GetNotification( m_iNotificationID );
		CExLabel *pHelpLabel = dynamic_cast< CExLabel* >( FindChildByName( "HelpTextLabel" ) );
		if ( pHelpLabel )
		{
			if ( pNotification )
			{
				const wchar_t *pszText = NULL;
				const char *pszTextKey = pNotification->GetUnlocalizedHelpText();
				if ( pszTextKey )
				{
					pszText = g_pVGuiLocalize->Find( pszTextKey );
				}
				if ( pszText )
				{					
					wchar_t wzFinal[512] = L"";
					if ( ::input->IsSteamControllerActive() )
					{
						UTIL_ReplaceKeyBindings( pszText, 0, wzFinal, sizeof( wzFinal ), GAME_ACTION_SET_FPSCONTROLS );
					}
					else
					{
						UTIL_ReplaceKeyBindings( pszText, 0, wzFinal, sizeof( wzFinal ) );
					}
					ColorizeText( pNotification, pHelpLabel, wzFinal );
				}
			}

			pHelpLabel->InvalidateLayout( true, false );
			int posX, posY;
			pHelpLabel->GetPos( posX, posY );
			int iContentWidth, iContentHeight;
			pHelpLabel->GetContentSize( iContentWidth, iContentHeight );
			int iLabelWidth, iLabelHeight;
			pHelpLabel->GetSize( iLabelWidth, iLabelHeight );
			int iTextInsetX, iTextInsetY;
			pHelpLabel->GetTextInset( &iTextInsetX, &iTextInsetY );
			pHelpLabel->SetSize( iLabelWidth, iContentHeight + iTextInsetY );
			posY = iHeight;
			pHelpLabel->SetPos( posX, posY - iTextInsetY );
			iHeight += iContentHeight + iTextInsetY;
		}

		// resize ourselves to fit the child height wise
		int iContainerHeight = MAX( m_iOriginalHeight, iHeight );
		SetSize( m_iOriginalWidth, m_iOverrideHeight != 0 ? m_iOverrideHeight : iContainerHeight );
	}

	virtual void OnCommand( const char *command )
	{
		CEconNotification *pNotification = g_notificationQueue.GetNotification( m_iNotificationID );
		
		if ( pNotification != NULL )
		{
			if ( !Q_strncmp( command, "delete", ARRAYSIZE( "delete" ) ) )
			{
				pNotification->Deleted();
				g_notificationQueue.RemoveNotification( m_iNotificationID );
				return;
			}
			else if ( !Q_strncmp( command, "trigger", ARRAYSIZE( "trigger" ) ) )
			{
				pNotification->Trigger();
			}
			else if ( !Q_strncmp( command, "accept", ARRAYSIZE( "accept" ) ) )
			{
				pNotification->Accept();
			}
			else if ( !Q_strncmp( command, "decline", ARRAYSIZE( "decline" ) ) )
			{
				pNotification->Decline();
			}
			else
			{
				BaseClass::OnCommand( command );
			}
		}
		else
		{
			BaseClass::OnCommand( command );
		}
	}

	int GetOverrideHeight() const 
	{
		return m_iOverrideHeight; 
	}

	void SetOverrideHeight( int iHeight )
	{
		m_iOverrideHeight = iHeight;
	}

private:
	vgui::EditablePanel* m_pChild;
	vgui::Panel* m_pTriggerButton;
	vgui::Panel* m_pAcceptButton;
	vgui::Panel* m_pDeclineButton;
	int m_iNotificationID;
	int m_iOriginalWidth;
	int m_iOriginalHeight;
	int m_iButtonOffsetY;
	int m_iOverrideHeight;
	bool m_bAddControls;
};

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

struct NotificationUIInfo_t
{
	CNotificationToastControl *m_pPanel;
	int m_iStartPosX;
	int m_iStartPosY;
};

// notification queue panel that is a HUD element
// this is the visualization of the notifications while in game
class CNotificationQueuePanel : public CHudElement, public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CNotificationQueuePanel, vgui::EditablePanel );
public:
	CNotificationQueuePanel( const char *pElementName ) 
		: CHudElement( pElementName )
		, BaseClass( NULL, "NotificationQueuePanel" )
		, m_mapNotificationPanels( DefLessFunc(int) )
		, m_flInvalidateTime( 0.0f )
		, m_bInvalidated( false )
	{
		vgui::Panel *pParent = g_pClientMode->GetViewport();
		SetParent( pParent );

		SetHiddenBits( HIDEHUD_MISCSTATUS );
	}

	virtual ~CNotificationQueuePanel()
	{
	}

	virtual bool ShouldDraw( void )
	{
		if ( !CHudElement::ShouldDraw() )
		{
			return false;
		}

		if ( engine->IsPlayingDemo() )
		{
			return false;
		}

		if ( cl_notifications_show_ingame.GetInt() == 0 )
		{
			return false;
		}

		CHudVote *pHudVote = GET_HUDELEMENT( CHudVote );
		if ( pHudVote && pHudVote->IsVoteUIActive() )
		{
			return false;
		}
		
		return m_mapNotificationPanels.Count() > 0 || g_notificationQueue.HasItems();
	}

	virtual void PerformLayout( void )
	{
		BaseClass::PerformLayout();

		// Get filtered list of only the notifications that show some in-game content
		CUtlVector< CEconNotification *> notifications;
		GetNotifications( notifications );

		const float flMoveTime = cl_notifications_move_time.GetFloat();
		float lerpPercentage = flMoveTime > 0 ? clamp( ( flMoveTime - m_flInvalidateTime ) / flMoveTime, 0.0f, 1.0f ) : 1.0f;
		float flCurrTime = engine->Time();

		// move the notifications around
		const int kMaxVisibleNotifications = cl_notifications_max_num_visible.GetInt();

		int iPosY = MIN( notifications.Count() - 1, kMaxVisibleNotifications - 1 ) * m_iOverlapOffset_Y;
		int iPosX = MIN( notifications.Count() - 1, kMaxVisibleNotifications - 1 ) * m_iOverlapOffset_X;
		int zpos = 100;
		int iPreviousHeight = 0;
		for ( int i = 0; i < notifications.Count(); ++i )
		{
			CEconNotification *pNotification = notifications[i];
			int mapIdx = m_mapNotificationPanels.Find( pNotification->GetID() );
			if ( m_mapNotificationPanels.IsValidIndex( mapIdx ) == false || pNotification->GetInGameLifeTime() < flCurrTime )
			{
				continue;
			}
			NotificationUIInfo_t &info = m_mapNotificationPanels[mapIdx];
			CNotificationToastControl *pPanel = info.m_pPanel;
			if ( pPanel )
			{
				if ( pPanel->IsVisible() == false )
				{
					pPanel->SetVisible( true );
				}
				if ( i == 0 && pPanel->GetOverrideHeight() != 0 )
				{
					pPanel->SetOverrideHeight( 0 );
					pPanel->InvalidateLayout( true, false );
				}
				int iPanelX;
				int iPanelY;
				pPanel->GetPos( iPanelX, iPanelY );
				if ( m_bInvalidated )
				{
					info.m_iStartPosX = iPanelX;
					info.m_iStartPosY = iPanelY;
				}
				int iNewPosX = iPosX;
				int iNewPosY = iPosY;
				iNewPosX = Lerp( lerpPercentage, info.m_iStartPosX, iNewPosX );
				iNewPosY = Lerp( lerpPercentage, info.m_iStartPosY, iNewPosY );
				pPanel->SetPos( iNewPosX, iNewPosY );
				pPanel->SetZPos( --zpos );
				bool bStoppedMoving = iNewPosX == iPanelX && iNewPosY == iPosY;
				// only show panels that are more than we want visible if they are moving
				if ( i > kMaxVisibleNotifications - 1 )
				{
					if ( bStoppedMoving )
					{
						pPanel->SetVisible( false );
					}
					continue;
				}
				// don't poke out underneath if we are visible and stopped moving
				if ( i != 0 && pPanel->GetOverrideHeight() == 0 && bStoppedMoving )
				{
					pPanel->SetOverrideHeight( MIN( pPanel->GetTall(), iPreviousHeight ) );
					pPanel->InvalidateLayout( true, false );
				}
				iPreviousHeight = pPanel->GetTall();

				iPosY = MAX( 0, iPosY - m_iOverlapOffset_Y );
				iPosX = MAX( 0, iPosX - m_iOverlapOffset_X );
			}
		}
		m_bInvalidated = false;
		SetTall( ScreenHeight() - m_iOriginalY );
	}

	virtual void ApplySchemeSettings( vgui::IScheme *scheme )
	{
		LoadControlSettings( "Resource/UI/Econ/NotificationQueuePanel.res" );
		GetBounds( m_iOriginalX, m_iOriginalY, m_iOriginalWidth, m_iOriginalHeight );

		BaseClass::ApplySchemeSettings( scheme );
	}

	virtual void OnThink()
	{
		BaseClass::OnThink();
		
		if ( IsVisible() == false )
		{
			return;
		}

		// Get filtered list of only the notifications that show some in-game content
		CUtlVector< CEconNotification *> notifications;
		GetNotifications( notifications );

		float flCurrTime = engine->Time();

		// check to see if we have a panel for each notification
		int i = 0;
		for ( i = 0; i < notifications.Count(); ++i )
		{
			CEconNotification *pNotification = notifications[i];
			int mapIdx = m_mapNotificationPanels.Find( pNotification->GetID() );
			if ( m_mapNotificationPanels.IsValidIndex( mapIdx ) == false || pNotification->GetInGameLifeTime() < flCurrTime )
			{
				m_bInvalidated = true;
				// create the panel and add it to the UI
				// have it slide from the bottom
				CNotificationToastControl *pControl = NULL;
				int iPosX = 0, iPosY = 0;
				vgui::EditablePanel *pPanel = pNotification->CreateUIElement( false );
				if ( pPanel )
				{
					pControl = new CNotificationToastControl( this, pPanel, pNotification->GetID(), false );
					pControl->GetPos( iPosX, iPosY );
					pControl->SetPos( iPosX, ScreenHeight() );
					iPosY = ScreenHeight();
				}
				NotificationUIInfo_t info = { pControl, iPosX, iPosY };
				m_mapNotificationPanels.Insert( pNotification->GetID(), info );
			}
		}

		// now check to see if we have panels and there is no matching notification
		i = m_mapNotificationPanels.FirstInorder();
		while ( m_mapNotificationPanels.IsValidIndex( i ) )
		{
			int idx = i;
			i = m_mapNotificationPanels.NextInorder( i );
			int iID = m_mapNotificationPanels.Key( idx );

			CEconNotification *pNotification = g_notificationQueue.GetNotification( iID );
			if ( pNotification == NULL || pNotification->GetInGameLifeTime() < flCurrTime )
			{
				// fade here, cause we don't really want to re-layout
				NotificationUIInfo_t &info = m_mapNotificationPanels[idx];
				vgui::EditablePanel *pPanel = info.m_pPanel;
				if ( pPanel )
				{
					pPanel->MarkForDeletion();
				}
				m_mapNotificationPanels.RemoveAt( idx );
				m_bInvalidated = true;
			}
		}

		if ( m_bInvalidated )
		{
			m_flInvalidateTime = MAX( cl_notifications_move_time.GetFloat(), 0.0f );
		}
		if ( m_flInvalidateTime > 0 )
		{
			m_flInvalidateTime -= gpGlobals->frametime;
			InvalidateLayout( true, false );
		}
	}

protected:
	typedef CUtlMap< int, NotificationUIInfo_t > tNotificationPanels;
	tNotificationPanels m_mapNotificationPanels;
	int m_iOriginalX;
	int m_iOriginalY;
	int m_iOriginalWidth;
	int m_iOriginalHeight;
	float m_flInvalidateTime;
	bool m_bInvalidated;

	CPanelAnimationVar( int, m_iVisibleBuffer, "buffer_between_visible", "5" );
	CPanelAnimationVar( int, m_iOverlapOffset_X, "overlap_offset_x", "10" );
	CPanelAnimationVar( int, m_iOverlapOffset_Y, "overlap_offset_y", "10" );

	void GetNotifications(CUtlVector< CEconNotification *> &notifications )
	{
		const CUtlVector< CEconNotification *> &allNotifications = g_notificationQueue.GetItems();

		for (int i = 0 ; i < allNotifications.Count() ; ++i )
		{
			CEconNotification *pNotification = allNotifications[i];
			if ( pNotification->BShowInGameElements() )
			{
				notifications.AddToTail( pNotification );
			}
		}
	}
};

DECLARE_HUDELEMENT( CNotificationQueuePanel );

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

CEconNotification::CEconNotification()
	: m_pText("")
	, m_pSoundFilename( NULL )
	, m_flExpireTime( engine->Time() + 10.0f )
	, m_pKeyValues( NULL )
	, m_bInUse( false )
	, m_steamID()
{
}

CEconNotification::~CEconNotification()
{
	if ( m_pKeyValues )
	{
		m_pKeyValues->deleteThis();
	}
}

void CEconNotification::SetText( const char *pText )
{
	m_pText = pText;
}

void CEconNotification::AddStringToken( const char* pToken, const wchar_t* pValue )
{
	if ( m_pKeyValues == NULL )
	{
		m_pKeyValues = new KeyValues( "CEconNotification" );
	}
	m_pKeyValues->SetWString( pToken, pValue );
}

void CEconNotification::SetKeyValues( KeyValues *pKeyValues )
{
	if ( m_pKeyValues != NULL )
	{
		m_pKeyValues->deleteThis();
	}
	m_pKeyValues = pKeyValues->MakeCopy();
}

KeyValues *CEconNotification::GetKeyValues() const
{
	return m_pKeyValues;
}

const wchar_t *CEconNotification::GetText()
{
	g_pVGuiLocalize->ConstructString_safe( m_wszBuffer, m_pText, m_pKeyValues );
	return m_wszBuffer;
}

int CEconNotification::GetID() const
{
	return m_iID;
}

void CEconNotification::SetLifetime( float flSeconds )
{
	m_flExpireTime = engine->Time() + flSeconds;
}

float CEconNotification::GetExpireTime() const
{
	return m_flExpireTime;
}

float CEconNotification::GetInGameLifeTime() const
{
	return m_flExpireTime;	// default's to passed in time unless otherwise set (for derived classes)
}

void CEconNotification::SetIsInUse( bool bInUse)
{
	m_bInUse = bInUse;
}

bool CEconNotification::GetIsInUse() const
{
	return m_bInUse;
}

void CEconNotification::SetSteamID( const CSteamID &steamID )
{
	m_steamID = steamID;
}

const CSteamID &CEconNotification::GetSteamID() const
{
	return m_steamID;
}

void CEconNotification::MarkForDeletion()
{
	// to be deleted ASAP
	m_flExpireTime = 0.0f;
}

CEconNotification::EType CEconNotification::NotificationType()
{
	return eType_Basic;
}

bool CEconNotification::BHighPriority()
{
	return false;
}

void CEconNotification::Trigger()
{
}

void CEconNotification::Accept()
{
}

void CEconNotification::Decline()
{
}

void CEconNotification::Deleted()
{
}

void CEconNotification::Expired()
{
}
vgui::EditablePanel *CEconNotification::CreateUIElement( bool bMainMenu ) const
{
	CGenericNotificationToast *pToast = new CGenericNotificationToast( NULL, m_iID, bMainMenu );
	return pToast;
}

const char *CEconNotification::GetUnlocalizedHelpText()
{
	switch ( NotificationType() )
	{
		case eType_AcceptDecline:
			return "#Notification_AcceptOrDecline_Help";
		case eType_MustTrigger:
		case eType_Trigger:
			return "#Notification_CanTrigger_Help";
		default:
			Assert( !"Unhandled enum value" );
			// ---v
		case eType_Basic:
			return "#Notification_Remove_Help";
	}

}

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

class CMainMenuNotificationsControl : public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CMainMenuNotificationsControl, vgui::EditablePanel );
public:
	CMainMenuNotificationsControl( vgui::EditablePanel *pParent, const char *pElementName ) 
		: BaseClass( pParent, pElementName )
		, m_mapNotificationPanels( DefLessFunc(int) )
		, m_iNumItems( 0 )
	{
		vgui::ivgui()->AddTickSignal( GetVPanel(), 250 );
	}

	virtual ~CMainMenuNotificationsControl()
	{
		vgui::ivgui()->RemoveTickSignal( GetVPanel() );
	}

	virtual void PerformLayout( void )
	{
		BaseClass::PerformLayout();

		const CUtlVector< CEconNotification *> &notifications = g_notificationQueue.GetItems();

		// position the notifications around
		// grow down
		int iTotalHeight = 0;
		const int kBuffer = 5;
		for ( int i = 0; i < notifications.Count(); ++i )
		{
			CEconNotification *pNotification = notifications[i];
			int mapIdx = m_mapNotificationPanels.Find( pNotification->GetID() );
			if ( m_mapNotificationPanels.IsValidIndex( mapIdx ) == false )
			{
				continue;
			}
			NotificationUIInfo_t &info = m_mapNotificationPanels[mapIdx];
			vgui::EditablePanel *pPanel = info.m_pPanel;
			if ( pPanel )
			{
				int iPanelX;
				int iPanelY;
				int iWidth;
				int iHeight;				
				pPanel->GetBounds( iPanelX, iPanelY, iWidth, iHeight );
				int iNewPosX = iPanelX;
				int iNewPosY = iTotalHeight;
				pPanel->SetPos( iNewPosX, iNewPosY );
				iTotalHeight += iHeight + kBuffer;
			}
		}
		int iWidth, iHeight;
		GetSize( iWidth, iHeight );
		SetSize( iWidth, iTotalHeight );
		if ( iTotalHeight != iHeight )
		{
			GetParent()->InvalidateLayout( false, false );
		}
	}

	virtual void OnTick()
	{
		if ( m_iNumItems != g_notificationQueue.GetItems().Count() )
		{
			m_iNumItems = g_notificationQueue.GetItems().Count();
			PostActionSignal( new KeyValues("Command", "command", "notifications_update" ) );
		}
	}

	virtual void OnThink()
	{
		BaseClass::OnThink();
		
		if ( IsVisible() == false )
		{
			return;
		}

		const CUtlVector< CEconNotification *> &notifications = g_notificationQueue.GetItems();
		bool bInvalidated = false;

		// check to see if we have a panel for each notification
		int i = 0;
		for ( i = 0; i < notifications.Count(); ++i )
		{
			CEconNotification *pNotification = notifications[i];
			int mapIdx = m_mapNotificationPanels.Find( pNotification->GetID() );
			if ( m_mapNotificationPanels.IsValidIndex( mapIdx ) == false )
			{
				bInvalidated = true;
				CNotificationToastControl *pControl = NULL;
				vgui::EditablePanel *pPanel = pNotification->CreateUIElement( true );
				if ( pPanel )
				{
					pControl = new CNotificationToastControl( this, pPanel, pNotification->GetID(), true );
				}
				NotificationUIInfo_t info = { pControl, 0, 0 };
				m_mapNotificationPanels.Insert( pNotification->GetID(), info );
			}
		}

		// now check to see if we have panels and there is no matching notification
		i = m_mapNotificationPanels.FirstInorder();
		while ( m_mapNotificationPanels.IsValidIndex( i ) )
		{
			int idx = i;
			i = m_mapNotificationPanels.NextInorder( i );
			int iID = m_mapNotificationPanels.Key( idx );
			if ( g_notificationQueue.GetNotification( iID ) == NULL )
			{
				NotificationUIInfo_t &info = m_mapNotificationPanels[idx];
				vgui::EditablePanel *pPanel = info.m_pPanel;
				if ( pPanel )
				{
					pPanel->MarkForDeletion();
				}
				m_mapNotificationPanels.RemoveAt( idx );
				bInvalidated = true;
			}
		}

		if ( bInvalidated )
		{
			InvalidateLayout( true, false );
		}
	}

protected:
	typedef CUtlMap< int, NotificationUIInfo_t > tNotificationPanels;
	tNotificationPanels m_mapNotificationPanels;
	int m_iNumItems;
};

// Show in UI
class CNotificationsPresentPanel : public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CNotificationsPresentPanel, vgui::EditablePanel );
public:
	CNotificationsPresentPanel( vgui::Panel *pParent, const char* pElementName ) : vgui::EditablePanel( pParent, "NotificationsPresentPanel" )
	{
		SetMouseInputEnabled( true );
	}

	virtual ~CNotificationsPresentPanel()
	{
	}

	virtual void ApplySchemeSettings( vgui::IScheme *pScheme )
	{
		BaseClass::ApplySchemeSettings( pScheme );
		
		LoadControlSettings( "Resource/UI/Econ/NotificationsPresentPanel.res" );

		for ( int i = 0; i < GetChildCount(); i++ )
		{
			vgui::Panel *pChild = GetChild( i );
			pChild->SetMouseInputEnabled( false );
		}
	}
	
	virtual void OnMousePressed(vgui::MouseCode code)
	{
		if ( code != MOUSE_LEFT )
			return;

		PostActionSignal( new KeyValues("Close") );

		// audible feedback
		const char *soundFilename = "ui/buttonclick.wav";

		vgui::surface()->PlaySound( soundFilename );
	}
};

DECLARE_BUILD_FACTORY( CNotificationsPresentPanel );

//-----------------------------------------------------------------------------
// External interface for the notification queue

int NotificationQueue_Add( CEconNotification *pNotification )
{
	if ( !engine->IsInGame() || (cl_notifications_show_ingame.GetBool() && pNotification->BShowInGameElements()) )
	{
		vgui::surface()->PlaySound( pNotification->GetSoundFilename() );
	}
	return g_notificationQueue.AddNotification( pNotification );
}

CEconNotification *NotificationQueue_Get( int iID )
{
	return g_notificationQueue.GetNotification( iID );
}

CEconNotification *NotificationQueue_GetByIndex( int idx )
{
	return g_notificationQueue.GetNotificationByIndex( idx );
}

void NotificationQueue_RemoveAll()
{
	g_notificationQueue.RemoveAllNotifications();
}

void NotificationQueue_Remove( int iID )
{
	g_notificationQueue.RemoveNotification( iID );
}

void NotificationQueue_Remove( CEconNotification *pNotification )
{
	g_notificationQueue.RemoveNotification( pNotification );
}

void NotificationQueue_Remove( NotificationFilterFunc func )
{
	g_notificationQueue.RemoveNotifications( func );
}

int NotificationQueue_Count( NotificationFilterFunc func )
{
	return g_notificationQueue.CountNotifications( func );
}

void NotificationQueue_Visit( CEconNotificationVisitor &visitor )
{
	g_notificationQueue.VisitNotifications( visitor );
}

void NotificationQueue_Update()
{
	g_notificationQueue.Update();
}

int NotificationQueue_GetNumNotifications()
{
	return g_notificationQueue.GetItems().Count();
}

vgui::EditablePanel* NotificationQueue_CreateMainMenuUIElement( vgui::EditablePanel *pParent, const char *pElementName )
{
	CMainMenuNotificationsControl *pControl = new CMainMenuNotificationsControl( pParent, pElementName );
	pControl->AddActionSignalTarget( pParent );
	return pControl;
}

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

CON_COMMAND( cl_trigger_first_notification, "Tries to accept/trigger the first notification" )
{
	const CUtlVector< CEconNotification *> &notifications = g_notificationQueue.GetItems();
	if ( notifications.Count() > 0 )
	{
		CEconNotification *pNotification = notifications[0];
		switch ( pNotification->NotificationType() )
		{
			case CEconNotification::eType_AcceptDecline:
				pNotification->Accept();
				break;
			case CEconNotification::eType_MustTrigger:
			case CEconNotification::eType_Trigger:
				pNotification->Trigger();
			case CEconNotification::eType_Basic:
				break;
			default:
				Assert( !"Unhandled enum value" );
		}
	}
}

CON_COMMAND( cl_decline_first_notification, "Tries to decline/remove the first notification" )
{
	const CUtlVector< CEconNotification *> &notifications = g_notificationQueue.GetItems();
	if ( notifications.Count() > 0 )
	{
		CEconNotification *pNotification = notifications[0];
		switch ( pNotification->NotificationType() )
		{
			case CEconNotification::eType_AcceptDecline:
				pNotification->Decline();
				break;
			case CEconNotification::eType_MustTrigger:
				break; // YOU MUUUSSTTTTT
			case CEconNotification::eType_Trigger:
			case CEconNotification::eType_Basic:
				pNotification->Deleted();
				pNotification->MarkForDeletion();
				break;
			default:
				Assert( !"Unhandled enum value" );
		}
	}
}

#ifdef _DEBUG

#include "confirm_dialog.h"

class CTFTestNotification : public CEconNotification
{
public:
	CTFTestNotification( const char* pText, EType eType )
		: CEconNotification()
		, m_pText( pText )
		, m_eType( eType )
	{
	}

	virtual EType NotificationType() OVERRIDE { return m_eType; }

	virtual void Trigger() OVERRIDE
	{
		ShowMessageBox( "", m_pText, "#GameUI_OK" );
		MarkForDeletion();
	}
	virtual void Accept() OVERRIDE
	{
		ShowMessageBox( "Accept", m_pText, "#GameUI_OK" );
		MarkForDeletion();
	}
	virtual void Decline() OVERRIDE
	{
		ShowMessageBox( "Decline", m_pText, "#GameUI_OK" );
		MarkForDeletion();
	}
private:
	const char *m_pText;
	EType m_eType;
};

CON_COMMAND( cl_add_notification, "Adds a notification" )
{
	if ( args.ArgC() >= 2 )
	{
		CEconNotification::EType eType = CEconNotification::eType_Basic;
		if ( args.ArgC() >= 5 )
		{
			eType = (CEconNotification::EType)atoi( args[4] );
		}

		CEconNotification *pNotification = new CTFTestNotification( args[1], eType );

		pNotification->SetText( args[1] );
		if ( args.ArgC() >= 3 )
		{
			int iLifetime = atoi( args[2] );
			pNotification->SetLifetime( iLifetime );
		}
		if ( args.ArgC() >= 4 )
		{
			if ( steamapicontext && steamapicontext->SteamUser() )
			{
				pNotification->SetSteamID( steamapicontext->SteamUser()->GetSteamID() );
			}
		}
		int id = NotificationQueue_Add( pNotification );
		Msg( "Added notification %d\n", id);
	}
}

#endif