//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: AI system that makes NPCs verbally respond to game events
//
//=============================================================================

#include "cbase.h"
#include "ai_eventresponse.h"
#include "ai_basenpc.h"

ConVar	ai_debug_eventresponses( "ai_debug_eventresponses", "0", FCVAR_NONE, "Set to 1 to see all NPC response events trigger, and which NPCs choose to respond to them." );

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CNPCEventResponseSystem g_NPCEventResponseSystem( "CNPCEventResponseSystem" );

CNPCEventResponseSystem *NPCEventResponse()
{
	return &g_NPCEventResponseSystem;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystem::LevelInitPreEntity( void )
{
	m_ActiveEvents.Purge();
	m_flNextEventPoll = 0;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystem::TriggerEvent( const char *pResponse, bool bForce, bool bCancelScript )
{
	m_flNextEventPoll = gpGlobals->curtime;

	// Find the event by name
	int iIndex = m_ActiveEvents.Find( pResponse );
	if ( iIndex == m_ActiveEvents.InvalidIndex() )
	{
		storedevent_t newEvent;
		newEvent.flEventTime = gpGlobals->curtime;
		newEvent.flNextResponseTime = 0;
		newEvent.bForce = bForce;
		newEvent.bCancelScript = bCancelScript;
		newEvent.bPreventExpiration = false;
		m_ActiveEvents.Insert( pResponse, newEvent );

		if ( ai_debug_eventresponses.GetBool() ) 
		{
			Msg( "NPCEVENTRESPONSE: (%.2f) Trigger fired for event named: %s\n", gpGlobals->curtime, pResponse );
		}
	}
	else 
	{
		// Update the trigger time
		m_ActiveEvents[iIndex].flEventTime = gpGlobals->curtime;
		m_ActiveEvents[iIndex].bForce = bForce;
		m_ActiveEvents[iIndex].bCancelScript = bCancelScript;

		if ( ai_debug_eventresponses.GetBool() ) 
		{
			Msg( "NPCEVENTRESPONSE: (%.2f) Trigger resetting already-active event firing named: %s\n", gpGlobals->curtime, pResponse );
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystem::FrameUpdatePreEntityThink()
{
 	if ( !m_ActiveEvents.Count() || !AI_IsSinglePlayer() || !UTIL_GetLocalPlayer() )
		return;

	if ( m_flNextEventPoll > gpGlobals->curtime )
		return;
	m_flNextEventPoll = gpGlobals->curtime + 0.2;

	// Move through all events, removing expired ones and finding NPCs for active ones.
	for ( int i = m_ActiveEvents.First(); i != m_ActiveEvents.InvalidIndex(); )
	{	
 		float flTime = m_ActiveEvents[i].flEventTime;
		const char *pResponse = m_ActiveEvents.GetElementName(i);

		// Save off the next index so we can safely remove this one
		int iNext = m_ActiveEvents.Next(i); 

		// Should it have expired by now?
		if ( !m_ActiveEvents[i].bPreventExpiration && (flTime + NPCEVENTRESPONSE_GIVEUP_TIME) < gpGlobals->curtime )
		{
			if ( ai_debug_eventresponses.GetBool() ) 
			{
				Msg( "NPCEVENTRESPONSE: (%.2f) Removing expired event named: %s\n", gpGlobals->curtime, pResponse );
			}

			m_ActiveEvents.RemoveAt(i);
		}
		else if ( m_ActiveEvents[i].flNextResponseTime < gpGlobals->curtime )
		{
			// If we've fired once, and our current event should expire now, then expire.
			if ( m_ActiveEvents[i].bPreventExpiration && (flTime + NPCEVENTRESPONSE_GIVEUP_TIME) < gpGlobals->curtime )
			{
				if ( ai_debug_eventresponses.GetBool() ) 
				{
					Msg( "NPCEVENTRESPONSE: (%.2f) Removing expired fired event named: %s\n", gpGlobals->curtime, pResponse );
				}

				m_ActiveEvents.RemoveAt(i);
			}
			else
			{
				float flNearestDist = NPCEVENTRESPONSE_DISTANCE_SQR;
				CAI_BaseNPC *pNearestNPC = NULL;
				Vector vecPlayerCenter = UTIL_GetLocalPlayer()->WorldSpaceCenter();

				// Try and find the nearest NPC to the player
				CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
				for ( int j = 0; j < g_AI_Manager.NumAIs(); j++ )
				{
					if ( ppAIs[j]->CanRespondToEvent( pResponse ))
					{
						float flDistToPlayer = ( vecPlayerCenter - ppAIs[j]->WorldSpaceCenter()).LengthSqr();
						if ( flDistToPlayer < flNearestDist )
						{
							flNearestDist = flDistToPlayer;
							pNearestNPC = ppAIs[j];
						}
					}
				}

				// Found one? 
				if ( pNearestNPC )
				{
					if ( pNearestNPC->RespondedTo( pResponse, m_ActiveEvents[i].bForce, m_ActiveEvents[i].bCancelScript ) )
					{
						// Don't remove the response yet. Leave it around until the refire time has expired.
						// This stops repeated firings of the same concept from spamming the NPCs.
						m_ActiveEvents[i].bPreventExpiration = true;
						m_ActiveEvents[i].flNextResponseTime = gpGlobals->curtime + NPCEVENTRESPONSE_REFIRE_TIME;

						if ( ai_debug_eventresponses.GetBool() ) 
						{
							Msg( "NPCEVENTRESPONSE: (%.2f) Event '%s' responded to by NPC '%s'. Refire available at: %.2f\n", gpGlobals->curtime, pResponse, pNearestNPC->GetDebugName(), m_ActiveEvents[i].flNextResponseTime );
						}

						// Don't issue multiple responses at once
						return;
					}
				}
			}
		}

		i = iNext;
	}
}

//---------------------------------------------------------------------------------------------
// Entity version for mapmaker to hook into the system
//---------------------------------------------------------------------------------------------
class CNPCEventResponseSystemEntity : public CBaseEntity
{
	DECLARE_CLASS( CNPCEventResponseSystemEntity, CBaseEntity );
public:
	DECLARE_DATADESC();

	void	Spawn();
	void 	InputTriggerResponseEvent( inputdata_t &inputdata );
	void 	InputForceTriggerResponseEvent( inputdata_t &inputdata );
	void 	InputForceTriggerResponseEventNoCancel( inputdata_t &inputdata );
};

LINK_ENTITY_TO_CLASS( ai_npc_eventresponsesystem, CNPCEventResponseSystemEntity );

BEGIN_DATADESC( CNPCEventResponseSystemEntity )
	DEFINE_INPUTFUNC( FIELD_STRING,	"TriggerResponseEvent",	InputTriggerResponseEvent ),
	DEFINE_INPUTFUNC( FIELD_STRING,	"ForceTriggerResponseEvent", InputForceTriggerResponseEvent ),
	DEFINE_INPUTFUNC( FIELD_STRING,	"ForceTriggerResponseEventNoCancel", InputForceTriggerResponseEventNoCancel ),
END_DATADESC()

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystemEntity::Spawn( void )
{
	// Invisible, non solid.
	AddSolidFlags( FSOLID_NOT_SOLID );
	AddEffects( EF_NODRAW );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystemEntity::InputTriggerResponseEvent( inputdata_t &inputdata )
{
	NPCEventResponse()->TriggerEvent( inputdata.value.String(), false, false );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystemEntity::InputForceTriggerResponseEvent( inputdata_t &inputdata )
{
	NPCEventResponse()->TriggerEvent( inputdata.value.String(), true, true );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CNPCEventResponseSystemEntity::InputForceTriggerResponseEventNoCancel( inputdata_t &inputdata )
{
	NPCEventResponse()->TriggerEvent( inputdata.value.String(), true, false );
}