//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Used to fire events based on the orientation of a given entity.
//
//			Looks at its target's angles every frame and fires an output if its
//			target's forward vector points at a specified lookat entity for more
//			than a specified length of time.
//
//			It also fires an output whenever the target's angles change.
//
//=============================================================================//

#include "cbase.h"
#include "entityinput.h"
#include "entityoutput.h"
#include "eventqueue.h"
#include "mathlib/mathlib.h"

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

#define SF_USE_TARGET_FACING	(1<<0)	// Use the target entity's direction instead of position

class CPointAngleSensor : public CPointEntity
{
	DECLARE_CLASS(CPointAngleSensor, CPointEntity);
public:

	bool KeyValue(const char *szKeyName, const char *szValue);
	void Activate(void);
	void Spawn(void);
	void Think(void);

	int DrawDebugTextOverlays(void);

protected:

	void Enable();
	void Disable();

	// Input handlers
	void InputEnable(inputdata_t &inputdata);
	void InputDisable(inputdata_t &inputdata);
	void InputToggle(inputdata_t &inputdata);
	void InputTest(inputdata_t &inputdata);
	void InputSetTargetEntity(inputdata_t &inputdata);

	bool IsFacingWithinTolerance(CBaseEntity *pEntity, CBaseEntity *pTarget, float flTolerance, float *pflDot = NULL);

	bool m_bDisabled;				// When disabled, we do not think or fire outputs.
	string_t m_nLookAtName;			// Name of the entity that the target must point at to fire the OnTrue output.

	EHANDLE m_hTargetEntity;		// Entity whose angles are being monitored.
	EHANDLE m_hLookAtEntity;		// Entity that the target must look at to fire the OnTrue output.

	float m_flDuration;				// Time in seconds for which the entity must point at the target.
	float m_flDotTolerance;			// Degrees of error allowed to satisfy the condition, expressed as a dot product.
	float m_flFacingTime;			// The time at which the target entity pointed at the lookat entity.
	bool m_bFired;					// Latches the output so it only fires once per true.

	// Outputs
	COutputEvent	m_OnFacingLookat;		// Fired when the target points at the lookat entity.
	COutputEvent	m_OnNotFacingLookat;	// Fired in response to a Test input if the target is not looking at the lookat entity.
	COutputVector	m_TargetDir;
	COutputFloat	m_FacingPercentage;	// Normalize value representing how close the entity is to facing directly at the target

	DECLARE_DATADESC();
};

LINK_ENTITY_TO_CLASS(point_anglesensor, CPointAngleSensor);


BEGIN_DATADESC(CPointAngleSensor)

	// Keys
	DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"),
	DEFINE_KEYFIELD(m_nLookAtName, FIELD_STRING, "lookatname"),
	DEFINE_FIELD(m_hTargetEntity, FIELD_EHANDLE),
	DEFINE_FIELD(m_hLookAtEntity, FIELD_EHANDLE),
	DEFINE_KEYFIELD(m_flDuration, FIELD_FLOAT, "duration"),
	DEFINE_FIELD(m_flDotTolerance, FIELD_FLOAT),
	DEFINE_FIELD(m_flFacingTime, FIELD_TIME),
	DEFINE_FIELD(m_bFired, FIELD_BOOLEAN),

	// Outputs
	DEFINE_OUTPUT(m_OnFacingLookat, "OnFacingLookat"),
	DEFINE_OUTPUT(m_OnNotFacingLookat, "OnNotFacingLookat"),
	DEFINE_OUTPUT(m_TargetDir, "TargetDir"),
	DEFINE_OUTPUT(m_FacingPercentage, "FacingPercentage"),

	// Inputs
	DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable),
	DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable),
	DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle),
	DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest),
	DEFINE_INPUTFUNC(FIELD_STRING, "SetTargetEntity", InputSetTargetEntity),

END_DATADESC()


//-----------------------------------------------------------------------------
// Purpose: Handles keyvalues that require special processing.
// Output : Returns true if handled, false if not.
//-----------------------------------------------------------------------------
bool CPointAngleSensor::KeyValue(const char *szKeyName, const char *szValue)
{
	if (FStrEq(szKeyName, "tolerance"))
	{
		float flTolerance = atof(szValue);
		m_flDotTolerance = cos(DEG2RAD(flTolerance));
	}
	else
	{
		return(BaseClass::KeyValue(szKeyName, szValue));
	}

	return(true);
}


//-----------------------------------------------------------------------------
// Purpose: Called when spawning after parsing keyvalues.
//-----------------------------------------------------------------------------
void CPointAngleSensor::Spawn(void)
{
	BaseClass::Spawn();
}


//-----------------------------------------------------------------------------
// Purpose: Called after all entities have spawned on new map or savegame load.
//-----------------------------------------------------------------------------
void CPointAngleSensor::Activate(void)
{
	BaseClass::Activate();

	if (!m_hTargetEntity)
	{
		m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target );
	}

	if (!m_hLookAtEntity && (m_nLookAtName != NULL_STRING))
	{
		m_hLookAtEntity = gEntList.FindEntityByName( NULL, m_nLookAtName );
		if (!m_hLookAtEntity)
		{
			DevMsg(1, "Angle sensor '%s' could not find look at entity '%s'.\n", GetDebugName(), STRING(m_nLookAtName));
		}
	}

	// It's okay to not have a look at entity, it just means we measure and output the angles
	// of the target entity without testing them against the look at entity.
	if (!m_bDisabled && m_hTargetEntity)
	{
		SetNextThink( gpGlobals->curtime );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Determines if one entity is facing within a given tolerance of another
// Input  : pEntity - 
//			pTarget - 
//			flTolerance - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CPointAngleSensor::IsFacingWithinTolerance(CBaseEntity *pEntity, CBaseEntity *pTarget, float flTolerance, float *pflDot)
{
	if (pflDot)
	{
		*pflDot = 0;
	}
	
	if ((pEntity == NULL) || (pTarget == NULL))
	{
		return(false);
	}

	Vector forward;
	pEntity->GetVectors(&forward, NULL, NULL);

	Vector dir;
	// Use either our position relative to the target, or the target's raw facing
	if ( HasSpawnFlags( SF_USE_TARGET_FACING ) )
	{
		pTarget->GetVectors(&dir, NULL, NULL);
	}
	else
	{
		dir = pTarget->GetAbsOrigin() - pEntity->GetAbsOrigin();
		VectorNormalize(dir);
	}
		
	//
	// Larger dot product corresponds to a smaller angle.
	//
	float flDot = dir.Dot(forward);
	if (pflDot)
	{
		*pflDot = flDot;
	}

	if (flDot >= m_flDotTolerance)
	{	
		return(true);
	}

	return(false);
}


//-----------------------------------------------------------------------------
// Purpose: Called every frame.
//-----------------------------------------------------------------------------
void CPointAngleSensor::Think(void)
{
	if (m_hTargetEntity != NULL)
	{
		Vector forward;
		m_hTargetEntity->GetVectors(&forward, NULL, NULL);
		m_TargetDir.Set(forward, this, this);

		if (m_hLookAtEntity != NULL)
		{
			//
			// Check to see if the measure entity's forward vector has been within
			// given tolerance of the target entity for the given period of time.
			//
			float flDot;
			if (IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance, &flDot ))
			{
				if (!m_bFired)
				{
					if (!m_flFacingTime)
					{
						m_flFacingTime = gpGlobals->curtime;
					}

					if (gpGlobals->curtime >= m_flFacingTime + m_flDuration)
					{
						m_OnFacingLookat.FireOutput(this, this);
						m_bFired = true;
					}
				}
			}
			else 
			{
				// Reset the fired state
				if ( m_bFired )
				{
					m_bFired = false;
				}

				// Always reset the time when we've lost our facing
				m_flFacingTime = 0;
			}
			
			// Output the angle range we're in
			float flPerc = RemapValClamped( flDot, 1.0f, m_flDotTolerance, 1.0f, 0.0f );
			m_FacingPercentage.Set( flPerc, this, this );
		}

		SetNextThink( gpGlobals->curtime );
	}
}


//-----------------------------------------------------------------------------
// Purpose: Input handler for forcing an instantaneous test of the condition.
//-----------------------------------------------------------------------------
void CPointAngleSensor::InputTest(inputdata_t &inputdata)
{
	if (IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance))
	{
		m_OnFacingLookat.FireOutput(inputdata.pActivator, this);
	}
	else
	{
		m_OnNotFacingLookat.FireOutput(inputdata.pActivator, this);
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointAngleSensor::InputSetTargetEntity(inputdata_t &inputdata)
{
	if ((inputdata.value.String() == NULL) || (inputdata.value.StringID() == NULL_STRING) || (inputdata.value.String()[0] == '\0'))
	{
		m_target = NULL_STRING;
		m_hTargetEntity = NULL;
		SetNextThink( TICK_NEVER_THINK );
	}
	else
	{
		m_target = AllocPooledString(inputdata.value.String());
		m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target, NULL, inputdata.pActivator, inputdata.pCaller );
		if (!m_bDisabled && m_hTargetEntity)
		{
			SetNextThink( gpGlobals->curtime );
		}
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointAngleSensor::InputEnable(inputdata_t &inputdata)
{
	Enable();
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointAngleSensor::InputDisable(inputdata_t &inputdata)
{
	Disable();
}


//-----------------------------------------------------------------------------
// Purpose: I like separators between my functions.
//-----------------------------------------------------------------------------
void CPointAngleSensor::InputToggle(inputdata_t &inputdata)
{
	if (m_bDisabled)
	{
		Enable();
	}
	else
	{
		Disable();
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointAngleSensor::Enable()
{
	m_bDisabled = false;
	if (m_hTargetEntity)
	{
		SetNextThink(gpGlobals->curtime);
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointAngleSensor::Disable()
{
	m_bDisabled = true;
	SetNextThink(TICK_NEVER_THINK);
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
int CPointAngleSensor::DrawDebugTextOverlays(void)
{
	int nOffset = BaseClass::DrawDebugTextOverlays();

	if (m_debugOverlays & OVERLAY_TEXT_BIT) 
	{
		float flDot;
		bool bFacing = IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance, &flDot);

		char tempstr[512];
		Q_snprintf(tempstr, sizeof(tempstr), "delta ang (dot)    : %.2f (%f)", RAD2DEG(acos(flDot)), flDot);
		EntityText( nOffset, tempstr, 0);
		nOffset++;

		Q_snprintf(tempstr, sizeof(tempstr), "tolerance ang (dot): %.2f (%f)", RAD2DEG(acos(m_flDotTolerance)), m_flDotTolerance);
		EntityText( nOffset, tempstr, 0);
		nOffset++;

		Q_snprintf(tempstr, sizeof(tempstr), "facing: %s", bFacing ? "yes" : "no");
		EntityText( nOffset, tempstr, 0);
		nOffset++;
	}

	return nOffset;
}

// ====================================================================
//  Proximity sensor
// ====================================================================

#define SF_PROXIMITY_TEST_AGAINST_AXIS	(1<<0)

class CPointProximitySensor : public CPointEntity
{
	DECLARE_CLASS( CPointProximitySensor, CPointEntity );

public:

	virtual void Activate( void );

protected:

	void Think( void );
	void Enable( void );
	void Disable( void );

	// Input handlers
	void InputEnable(inputdata_t &inputdata);
	void InputDisable(inputdata_t &inputdata);
	void InputToggle(inputdata_t &inputdata);
	void InputSetTargetEntity(inputdata_t &inputdata);

private:

	bool	m_bDisabled;			// When disabled, we do not think or fire outputs.
	EHANDLE m_hTargetEntity;		// Entity whose angles are being monitored.

	COutputFloat	m_Distance;

	DECLARE_DATADESC();
};

LINK_ENTITY_TO_CLASS( point_proximity_sensor, CPointProximitySensor );

BEGIN_DATADESC( CPointProximitySensor )

	// Keys
	DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ),
	DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ),

	// Outputs
	DEFINE_OUTPUT( m_Distance, "Distance"),

	// Inputs
	DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable),
	DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable),
	DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle),
	DEFINE_INPUTFUNC(FIELD_STRING, "SetTargetEntity", InputSetTargetEntity),

END_DATADESC()

//-----------------------------------------------------------------------------
// Purpose: Called after all entities have spawned on new map or savegame load.
//-----------------------------------------------------------------------------
void CPointProximitySensor::Activate( void )
{
	BaseClass::Activate();

	if ( m_hTargetEntity == NULL )
	{
		m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target );
	}

	if ( m_bDisabled == false && m_hTargetEntity != NULL )
	{
		SetNextThink( gpGlobals->curtime );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointProximitySensor::InputSetTargetEntity(inputdata_t &inputdata)
{
	if ((inputdata.value.String() == NULL) || (inputdata.value.StringID() == NULL_STRING) || (inputdata.value.String()[0] == '\0'))
	{
		m_target = NULL_STRING;
		m_hTargetEntity = NULL;
		SetNextThink( TICK_NEVER_THINK );
	}
	else
	{
		m_target = AllocPooledString(inputdata.value.String());
		m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target, NULL, inputdata.pActivator, inputdata.pCaller );
		if (!m_bDisabled && m_hTargetEntity)
		{
			SetNextThink( gpGlobals->curtime );
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointProximitySensor::InputEnable( inputdata_t &inputdata )
{
	Enable();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointProximitySensor::InputDisable( inputdata_t &inputdata )
{
	Disable();
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CPointProximitySensor::InputToggle( inputdata_t &inputdata )
{
	if ( m_bDisabled )
	{
		Enable();
	}
	else
	{
		Disable();
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointProximitySensor::Enable( void )
{
	m_bDisabled = false;
	if ( m_hTargetEntity )
	{
		SetNextThink( gpGlobals->curtime );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CPointProximitySensor::Disable( void )
{
	m_bDisabled = true;
	SetNextThink( TICK_NEVER_THINK );
}

//-----------------------------------------------------------------------------
// Purpose: Called every frame
//-----------------------------------------------------------------------------
void CPointProximitySensor::Think( void )
{
	if ( m_hTargetEntity != NULL )
	{
		Vector vecTestDir = ( m_hTargetEntity->GetAbsOrigin() - GetAbsOrigin() );
		float flDist = VectorNormalize( vecTestDir );

		// If we're only interested in the distance along a vector, modify the length the accomodate that
		if ( HasSpawnFlags( SF_PROXIMITY_TEST_AGAINST_AXIS ) )
		{
			Vector vecDir;
			GetVectors( &vecDir, NULL, NULL );

			float flDot = DotProduct( vecTestDir, vecDir );
			flDist *= fabs( flDot );
		}

		m_Distance.Set( flDist, this, this );
		SetNextThink( gpGlobals->curtime );
	}
}
