//========= Copyright © 1996-2002, Valve LLC, All rights reserved. ============
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================
#include "cbase.h"
#include "tf_obj.h"
#include "tf_obj_mapdefined.h"
#include "engine/IEngineSound.h"
#include "entityoutput.h"
#include "tf_shareddefs.h"
#include "triggers.h"
#include "shake.h"
#include "tf_player.h"
#include "tf_gamerules.h"

#define TUNNEL_THINK_INTERVAL	0.1f
#define TUNNEL_FADE_TIME		1.0f
#define MAX_TUNNEL_DURATION		30.0f
// If tunneling takes longer than this, use a countdown
#define TUNNEL_DURATION_MESSAGE_NEEDED	3.0f

// It takes this long to tunnel
static ConVar tf_tunnel_time( "tf_tunnel_time", "2", 0, "Takes this long to traverse a tunnel." );

class CObjectTunnel : public CObjectMapDefined
{
	DECLARE_CLASS( CObjectTunnel, CObjectMapDefined );
public:

	DECLARE_SERVERCLASS();

	virtual void Spawn( void );
	virtual void Killed( void );

	// Tunnels are important. Transmit to everyone
	bool ShouldTransmit( const edict_t *recipient, const void *pvs, int clientArea )
	{
		return true;
	}

private:
};

IMPLEMENT_SERVERCLASS_ST(CObjectTunnel, DT_ObjectTunnel)
END_SEND_TABLE();

void CObjectTunnel::Spawn( void )
{
	BaseClass::Spawn();

	AddFlag( FL_NOTARGET );
	SetType( OBJ_TUNNEL );
}

LINK_ENTITY_TO_CLASS(obj_tunnel,CObjectTunnel);
LINK_ENTITY_TO_CLASS(obj_tunnel_prop,CObjectTunnel);

//-----------------------------------------------------------------------------
// Purpose: Object has been blown up. Tunnels are never fully destroyed, so they stay on the minimap.
//-----------------------------------------------------------------------------
void CObjectTunnel::Killed( void )
{
	m_bDying = true;

	RemoveAllSappers( this );

	// Do an explosion.
	CPASFilter filter( GetAbsOrigin() );
	te->Explosion( 
		filter, 
		0.0,				
		&GetAbsOrigin(),
		g_sModelIndexFireball,
		5.4,		// radius
		15,
		TE_EXPLFLAG_NODLIGHTS,
		256,
		200);

	// Become non-solid and invisible
	VPhysicsDestroyObject();
	AddSolidFlags( FSOLID_NOT_SOLID );
	m_takedamage = DAMAGE_NO;
	m_fEffects |= EF_NODRAW;
	UTIL_Relink( this );
}

class CInfoTunnelExit : public CPointEntity
{
public:
	DECLARE_CLASS( CInfoTunnelExit, CPointEntity );
private:
};

LINK_ENTITY_TO_CLASS(info_tunnel_exit,CInfoTunnelExit);

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
class CObjectTunnelTrigger : public CBaseTrigger
{
	DECLARE_CLASS( CObjectTunnelTrigger, CBaseTrigger );
public:
	CObjectTunnelTrigger();

	DECLARE_DATADESC();

	virtual void Spawn();
	virtual void Activate();

	virtual void StartTouch( CBaseEntity *pOther );

	void		SetActive( bool active );
	bool		GetActive( void ) const;

	void	InputSetActive( inputdata_t &inputdata );
	void	InputSetInactive( inputdata_t &inputdata );
	void	InputToggleActive( inputdata_t &inputdata );

	void	InputSetTarget( inputdata_t &inputdata );
	void	InputSetTeleportDuration( inputdata_t &inputdata );
	void	InputSetTeleportVelocity( inputdata_t &inputdata );

	virtual void	TunnelThink();
private:
	float						GetTeleportDuration( void );

	bool m_bActive;
	CHandle< CInfoTunnelExit > m_hTunnelExit;

	COutputEvent	OnTunnelTriggerStart;
	COutputEvent	OnTunnelTriggerEnd;

	struct TunnelPlayer
	{
		CHandle< CBaseTFPlayer >	player;
		Vector						startpos;
		float						tunnelstarted;
		float						duration;
		float						teleporttime;
		float						fadeintime;
		bool						exitstarted;
		float						fadetime;
		int							iremaining;
		int							ilastremaining;
		bool						needremainigcounter;
	};

	CUtlVector< TunnelPlayer > m_Tunneling;

	void		StartTunneling( CBaseTFPlayer *player );
	bool		KeepTunneling( TunnelPlayer *tunnel );
	

	float							m_flTeleportDuration;
	float							m_flTeleportVelocity;
};


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CObjectTunnelTrigger::CObjectTunnelTrigger()
{
	m_flTeleportDuration = -1.0f;
	m_flTeleportVelocity = 0.0f;

	NetworkStateManualMode( true );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *player - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::StartTunneling( CBaseTFPlayer *player )
{
	if ( !player )
		return;

	// Ignore if it's already in the list
	int c = m_Tunneling.Count();
	for ( int i = 0 ; i < c; i++ )
	{
		TunnelPlayer *tp = &m_Tunneling[ i ];
		if ( tp->player == player )
		{
			return;
		}
	}

	TunnelPlayer tunnel;
	tunnel.player = player;
	tunnel.tunnelstarted = gpGlobals->curtime;
	tunnel.duration =  GetTeleportDuration();
	tunnel.teleporttime = tunnel.tunnelstarted + tunnel.duration;
	tunnel.exitstarted = false;
	tunnel.startpos = player->GetAbsOrigin();
	tunnel.iremaining = (int)tunnel.duration;
	tunnel.ilastremaining = tunnel.iremaining;
	tunnel.needremainigcounter = ( tunnel.iremaining > TUNNEL_DURATION_MESSAGE_NEEDED ) ? true : false;

	// Fade user screen to black
	color32 black = {0,0,0,255};

	float duration = tunnel.duration;
	float fadeouttime = TUNNEL_FADE_TIME;
	float holdtime = 0.0f;
	if ( duration < 2 * TUNNEL_FADE_TIME )
	{
		fadeouttime = duration * 0.5f;
	}
	else
	{
		fadeouttime = TUNNEL_FADE_TIME;
		holdtime = duration - 2 * fadeouttime;
	}

	tunnel.fadetime = fadeouttime;
	tunnel.fadeintime = tunnel.tunnelstarted + fadeouttime + holdtime;

	UTIL_ScreenFade( player, black, fadeouttime, holdtime, FFADE_OUT | FFADE_STAYOUT | FFADE_PURGE );

	m_Tunneling.AddToTail( tunnel );

	player->SetMoveType( MOVETYPE_NONE );
	player->EnableControl( false );
	player->m_fEffects |= EF_NODRAW;
	player->AddSolidFlags( FSOLID_NOT_SOLID );
	player->Relink();

	CPASAttenuationFilter filter( player, "ObjectTunnelTrigger.TeleportSound" );
	EmitSound( filter, player->entindex(), "ObjectTunnelTrigger.TeleportSound" );

	OnTunnelTriggerStart.FireOutput( player, this );
}

float CObjectTunnelTrigger::GetTeleportDuration( void )
{
	float duration = m_flTeleportDuration;

	if ( m_flTeleportVelocity > 0.0f && m_hTunnelExit != NULL )
	{
		Vector delta = m_hTunnelExit->GetAbsOrigin() - GetAbsOrigin();
		float dist = delta.Length();
		duration = dist / m_flTeleportVelocity;
	}
	else if ( m_flTeleportDuration == -1.0f )
	{
		Msg( "obj_tunnel_trigger:  must set TeleportVelocity or TeleportDuration" );
		m_flTeleportDuration = tf_tunnel_time.GetFloat();
	}

	duration = min( duration, MAX_TUNNEL_DURATION );
	return duration;
}

bool CObjectTunnelTrigger::KeepTunneling( TunnelPlayer *tunnel )
{
	if ( !tunnel || ( tunnel->player == NULL ) )
	{
		return false;
	}

	float remaining = tunnel->teleporttime - gpGlobals->curtime + 0.5f;
	remaining = max( 0.0f, remaining );

	tunnel->iremaining = (int)( remaining );

	if ( !tunnel->exitstarted )
	{
		if ( gpGlobals->curtime > tunnel->fadeintime )
		{
			tunnel->exitstarted = true;
			// Fade user screen to black
			color32 black = {0,0,0,255};
			UTIL_ScreenFade( tunnel->player, black, tunnel->fadetime, 0.0, FFADE_IN | FFADE_PURGE );

			// Move to tunnel exit spot now that we're half-way through teleport
			if ( m_hTunnelExit != NULL )
			{
				tunnel->player->EnableControl( true );
				tunnel->player->m_fEffects &= ~EF_NODRAW;

				// Change the player to non-solid before the teleport, so the physics system doesn't think he
				// actually moved this distance:
				int OriginalSolidFlags = tunnel->player->GetSolidFlags();
				tunnel->player->AddSolidFlags( FSOLID_NOT_SOLID);

				// Do a placement test to prevent the player from teleporting inside another player, the ground, or just to help
				// prevent badly placed tunnels from causing stuck situations.
				Vector vTarget = m_hTunnelExit->GetAbsOrigin();
				Vector vOriginal = vTarget; 

				if ( !EntityPlacementTest( tunnel->player, vOriginal, vTarget, true ) )
				{
					Warning("Couldn't place entity after tunnel teleport.\n");
				}


				tunnel->player->Teleport( &vTarget /*m_hTunnelExit->GetAbsOrigin()*/, &m_hTunnelExit->GetAbsAngles(), NULL );
				tunnel->player->SnapEyeAngles( m_hTunnelExit->GetAbsAngles() );
				tunnel->player->SetAbsVelocity( vec3_origin );

				// Restore the player's solid flags.
				tunnel->player->SetSolidFlags(OriginalSolidFlags);

			}
		}
// Can't quite do this because the player's weapons are still visible flying across the map even if
//  he is hidden
#if 0
		else if ( gpGlobals->curtime > tunnel->tunnelstarted + tunnel->fadetime )
		{
			float travel_time = tunnel->duration - 2 * tunnel->fadetime;
			if ( travel_time > 0.0f )
			{
				float f = ( gpGlobals->curtime - tunnel->tunnelstarted - tunnel->fadetime  ) / travel_time;
				f = clamp( f, 0.0f, 1.0f );
				if ( m_hTunnelExit != NULL )
				{
					Vector delta = m_hTunnelExit->GetAbsOrigin() - tunnel->startpos;
					Vector currentPos;
					VectorMA( tunnel->startpos, f, delta, currentPos );

					tunnel->player->Teleport( &currentPos, NULL, NULL );
				}
			}
		}
#endif
	}

	if ( tunnel->ilastremaining != tunnel->iremaining &&
		tunnel->needremainigcounter &&
		tunnel->iremaining >= 1 &&
		tunnel->player != NULL )
	{
		// Counter
		ClientPrint( tunnel->player, HUD_PRINTCENTER, UTIL_VarArgs("\nExiting tunnel in %d %s\n", tunnel->iremaining, tunnel->iremaining > 1 ? "seconds" : "second" ) );
	}

	tunnel->ilastremaining = tunnel->iremaining;

	// TODO: Play footstep or some other teleport sounds occasionaly to this player?

	bool done = ( gpGlobals->curtime > tunnel->teleporttime ) ? true : false;
	if ( done )
	{
		color32 black = {0,0,0,255};
		UTIL_ScreenFade( tunnel->player, black, 0.0f, 0.0f, FFADE_IN | FFADE_PURGE );

		tunnel->player->SetMoveType( MOVETYPE_WALK );
		tunnel->player->EnableControl( true );
		tunnel->player->m_fEffects &= ~EF_NODRAW;
		tunnel->player->RemoveSolidFlags( FSOLID_NOT_SOLID );
		tunnel->player->Relink();

		// TODO:  Play an exit sound??
		OnTunnelTriggerEnd.FireOutput( tunnel->player, this );
	}

	return !done;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::TunnelThink()
{
	// Make sure it's not already in the list
	int c = m_Tunneling.Count();
	for ( int i = c - 1; i >= 0; i-- )
	{
		TunnelPlayer *tp = &m_Tunneling[ i ];

		if ( !KeepTunneling( tp ) )
		{
			m_Tunneling.Remove( i );
		}
	}

	SetNextThink( gpGlobals->curtime + TUNNEL_THINK_INTERVAL );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::Spawn()
{
	Precache();

	SetSolid( SOLID_BSP );
	AddSolidFlags( FSOLID_TRIGGER );
	SetMoveType( MOVETYPE_NONE );
	m_fEffects |= EF_NODRAW;
	SetModel( STRING( GetModelName() ) );
	AddFlag( FL_NOTARGET );

	m_bActive = false;

	NetworkStateChanged();
}

//-----------------------------------------------------------------------------
// Purpose: See if we've got a gather point specified 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::Activate( void )
{
	BaseClass::Activate();

	if (m_target != NULL_STRING)
	{
		CBaseEntity	*pEnt = gEntList.FindEntityByName( NULL, m_target, NULL );
		if ( pEnt && FClassnameIs( pEnt, "info_tunnel_exit" ) )
		{
			m_hTunnelExit = static_cast< CInfoTunnelExit * >( pEnt );
		}
		else
		{
			Msg( "CObjectTunnelTrigger::Activate, unable to connect tunnel to target %s\n",
				STRING( m_target ) );
		}
	}
	else
	{
		Msg( "CObjectTunnelTrigger::Activate, missing target\n" );
	}

	SetActive( true );

	SetThink( TunnelThink );
	SetNextThink( gpGlobals->curtime + TUNNEL_THINK_INTERVAL );

	NetworkStateChanged();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : active - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::SetActive( bool active )
{
	m_bActive = active;
	NetworkStateChanged();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CObjectTunnelTrigger::GetActive( void ) const
{
	return m_bActive;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputSetActive( inputdata_t &inputdata )
{
	SetActive( true );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputSetInactive( inputdata_t &inputdata )
{
	SetActive( false );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputToggleActive( inputdata_t &inputdata )
{
	if ( m_bActive )
	{
		SetActive( false );
	}
	else
	{
		SetActive( true );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &inputdata - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputSetTarget( inputdata_t &inputdata )
{
	CBaseEntity	*pEnt = gEntList.FindEntityByName( NULL, inputdata.value.String(), NULL );
	if ( pEnt && FClassnameIs( pEnt, "info_tunnel_exit" ) )
	{
		m_hTunnelExit = static_cast< CInfoTunnelExit * >( pEnt );
	}
	else
	{
		Msg( "CObjectTunnelTrigger::InputSetTarget: Couldn't find info_tunnel_exit named %s\n",
			inputdata.value.String() );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &inputdata - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputSetTeleportDuration( inputdata_t &inputdata )
{
	m_flTeleportDuration = inputdata.value.Float();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &inputdata - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::InputSetTeleportVelocity( inputdata_t &inputdata )
{
	m_flTeleportVelocity = inputdata.value.Float();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *pOther - 
//-----------------------------------------------------------------------------
void CObjectTunnelTrigger::StartTouch( CBaseEntity *pOther )
{
	if ( !pOther || !pOther->IsPlayer() )
		return;

	// Only works for my team, of course
	if ( !pOther->InSameTeam(  this ) )
		return;

	if ( m_hTunnelExit == NULL )
		return;

	// It's been damaged to the point of being disabled
	if ( !GetActive() )
	{
		// Play a deny sound
		CPASAttenuationFilter filter( pOther, "ObjectTunnelTrigger.DisabledSound" );
		EmitSound( filter, pOther->entindex(), "ObjectTunnelTrigger.DisabledSound" );
		return;
	}

	StartTunneling( (CBaseTFPlayer *)pOther );
}

LINK_ENTITY_TO_CLASS(obj_tunnel_trigger,CObjectTunnelTrigger);

BEGIN_DATADESC( CObjectTunnelTrigger )
	// inputs
	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_VOID, "SetActive", InputSetActive ),
	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_VOID, "SetInactive", InputSetInactive ),
	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_VOID, "ToggleActive", InputToggleActive ),

	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_STRING, "SetTarget", InputSetTarget ),
	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_FLOAT, "SetTeleportDuration", InputSetTeleportDuration ),
	DEFINE_INPUTFUNC( CObjectTunnelTrigger, FIELD_FLOAT, "SetTeleportVelocity", InputSetTeleportVelocity ),

	// outputs
	DEFINE_OUTPUT( CObjectTunnelTrigger, OnTunnelTriggerStart, "OnTunnelTriggerStart" ),
	DEFINE_OUTPUT( CObjectTunnelTrigger, OnTunnelTriggerEnd, "OnTunnelTriggerEnd" ),

	// keyvalues
	DEFINE_KEYFIELD_NOT_SAVED( CObjectTunnelTrigger, m_flTeleportDuration, FIELD_FLOAT, "TeleportDuration" ),
	DEFINE_KEYFIELD_NOT_SAVED( CObjectTunnelTrigger, m_flTeleportVelocity, FIELD_FLOAT, "TeleportVelocity" ),
END_DATADESC()
