//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//=============================================================================//

#include "cbase.h"
#include "dod_basegrenade.h"
#include "dod_player.h"
#include "dod_gamerules.h"
#include "func_break.h"
#include "physics_saverestore.h"
#include "grenadetrail.h"
#include "fx_dod_shared.h"

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

float GetCurrentGravity( void );

ConVar dod_grenadegravity( "dod_grenadegravity", "-420", FCVAR_CHEAT, "gravity applied to grenades", true, -2000, true, -300 );
extern ConVar dod_bonusround;

IMotionEvent::simresult_e	CGrenadeController::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular )
{
	linear.x = linear.y = 0;
	linear.z = dod_grenadegravity.GetFloat();

	angular.x = angular.y = angular.z = 0;

	return SIM_GLOBAL_ACCELERATION;
}

BEGIN_SIMPLE_DATADESC( CGrenadeController )
END_DATADESC()

BEGIN_DATADESC( CDODBaseGrenade )

	DEFINE_THINKFUNC( DetonateThink ),
	
	DEFINE_EMBEDDED( m_GrenadeController ),
	DEFINE_PHYSPTR( m_pMotionController ),	// probably not necessary
	
END_DATADESC()

IMPLEMENT_SERVERCLASS_ST( CDODBaseGrenade, DT_DODBaseGrenade )
	SendPropVector( SENDINFO( m_vInitialVelocity ), 
				20,		// nbits
				0,		// flags
				-3000,	// low value
				3000	// high value
				)
END_SEND_TABLE()

CDODBaseGrenade::CDODBaseGrenade()
{
}

CDODBaseGrenade::~CDODBaseGrenade( void )
{
	if ( m_pMotionController != NULL )
	{
		physenv->DestroyMotionController( m_pMotionController );
		m_pMotionController = NULL;
	}
}

void CDODBaseGrenade::Spawn( void )
{
	m_bUseVPhysics = true;

	BaseClass::Spawn();
	
	SetSolid( SOLID_BBOX );	// So it will collide with physics props!

	UTIL_SetSize( this, Vector(-4,-4,-4), Vector(4,4,4) );

	if( m_bUseVPhysics )
	{		 
		SetCollisionGroup( COLLISION_GROUP_WEAPON );
		IPhysicsObject *pPhysicsObject = VPhysicsInitNormal( SOLID_BBOX, 0, false );

		if ( pPhysicsObject )
		{
			m_pMotionController = physenv->CreateMotionController( &m_GrenadeController );
			m_pMotionController->AttachObject( pPhysicsObject, true );

			pPhysicsObject->EnableGravity( false );
		}
		
		m_takedamage	= DAMAGE_EVENTS_ONLY;
	}
	else
	{
		SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_CUSTOM );
		m_takedamage	= DAMAGE_NO;
	}

	AddSolidFlags( FSOLID_NOT_STANDABLE );

	m_iHealth		= 1;
	
	SetFriction( GetGrenadeFriction() );
	SetElasticity( GetGrenadeElasticity() );

	// Remember our owner's team
	ChangeTeam( GetThrower()->GetTeamNumber() );

	m_flDamage = 150;
	m_DmgRadius = m_flDamage * 2.5f;

	// Don't collide with players on the owner's team for the first bit of our life
	m_flCollideWithTeammatesTime = gpGlobals->curtime + 0.25;
	m_bCollideWithTeammates = false;

	SetThink( &CDODBaseGrenade::DetonateThink );
	SetNextThink( gpGlobals->curtime + 0.1 );
}

void CDODBaseGrenade::Precache( void )
{
	BaseClass::Precache();

	PrecacheParticleSystem( "grenadetrail" );
	PrecacheParticleSystem( "riflegrenadetrail" );
	PrecacheParticleSystem( "explosioncore_midair" );
	PrecacheParticleSystem( "explosioncore_floor" );
}

void CDODBaseGrenade::DetonateThink( void )
{
	if (!IsInWorld())
	{
		Remove( );
		return;
	}

	if ( gpGlobals->curtime > m_flCollideWithTeammatesTime && m_bCollideWithTeammates == false )
	{
		m_bCollideWithTeammates = true;
	}

	Vector foo;
	AngularImpulse a;	
		
	VPhysicsGetObject()->GetVelocity( &foo, &a );

	if( gpGlobals->curtime > m_flDetonateTime )
	{
		Detonate();
		return;
	}

	if (GetWaterLevel() != 0)
	{
		SetAbsVelocity( GetAbsVelocity() * 0.5 );
	}

	SetNextThink( gpGlobals->curtime + 0.2 );
}

//Sets the time at which the grenade will explode
void CDODBaseGrenade::SetDetonateTimerLength( float timer )
{
	m_flDetonateTime = gpGlobals->curtime + timer;
}

void CDODBaseGrenade::ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity )
{
	//Assume all surfaces have the same elasticity
	float flSurfaceElasticity = 1.0;

	//Don't bounce off of players with perfect elasticity
	if( trace.m_pEnt && trace.m_pEnt->IsPlayer() )
	{
		flSurfaceElasticity = 0.3;
	}
	
	float flTotalElasticity = GetElasticity() * flSurfaceElasticity;
	flTotalElasticity = clamp( flTotalElasticity, 0.0f, 0.9f );

	// NOTE: A backoff of 2.0f is a reflection
	Vector vecAbsVelocity;
	PhysicsClipVelocity( GetAbsVelocity(), trace.plane.normal, vecAbsVelocity, 2.0f );
	vecAbsVelocity *= flTotalElasticity;

	// Get the total velocity (player + conveyors, etc.)
	VectorAdd( vecAbsVelocity, GetBaseVelocity(), vecVelocity );
	float flSpeedSqr = DotProduct( vecVelocity, vecVelocity );

	// Stop if on ground.
	if ( trace.plane.normal.z > 0.7f )			// Floor
	{
		// Verify that we have an entity.
		CBaseEntity *pEntity = trace.m_pEnt;
		Assert( pEntity );

		// Are we on the ground?
		if ( vecVelocity.z < ( GetCurrentGravity() * gpGlobals->frametime ) )
		{
			if ( pEntity->IsStandable() )
			{
				SetGroundEntity( pEntity );
			}

			vecAbsVelocity.z = 0.0f;
		}
		SetAbsVelocity( vecAbsVelocity );

		if ( flSpeedSqr < ( 30 * 30 ) )
		{
			if ( pEntity->IsStandable() )
			{
				SetGroundEntity( pEntity );
			}

			// Reset velocities.
			SetAbsVelocity( vec3_origin );
			SetLocalAngularVelocity( vec3_angle );
		}
		else
		{
			Vector vecDelta = GetBaseVelocity() - vecAbsVelocity;	
			Vector vecBaseDir = GetBaseVelocity();
			VectorNormalize( vecBaseDir );
			float flScale = vecDelta.Dot( vecBaseDir );

			VectorScale( vecAbsVelocity, ( 1.0f - trace.fraction ) * gpGlobals->frametime, vecVelocity ); 
			VectorMA( vecVelocity, ( 1.0f - trace.fraction ) * gpGlobals->frametime, GetBaseVelocity() * flScale, vecVelocity );
			PhysicsPushEntity( vecVelocity, &trace );
		}
	}
	else
	{
		// If we get *too* slow, we'll stick without ever coming to rest because
		// we'll get pushed down by gravity faster than we can escape from the wall.
		if ( flSpeedSqr < ( 30 * 30 ) )
		{
			// Reset velocities.
			SetAbsVelocity( vec3_origin );
			SetLocalAngularVelocity( vec3_angle );
		}
		else
		{
			SetAbsVelocity( vecAbsVelocity );
		}
	}
	
	BounceSound();
}

char *CDODBaseGrenade::GetExplodingClassname( void )
{
	Assert( !"Baseclass must implement this" );
	return NULL;
}

void CDODBaseGrenade::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
	if ( !CanBePickedUp() )
		return;

	if ( !pActivator->IsPlayer() )
		return;

	CDODPlayer *pPlayer = ToDODPlayer( pActivator );

	//Don't pick up grenades while deployed
	CBaseCombatWeapon *pWpn = pPlayer->GetActiveWeapon();
	if ( pWpn && !pWpn->CanHolster() )
	{
		return;
	}

	DODRoundState state = DODGameRules()->State_Get();

	if ( dod_bonusround.GetBool() )
	{
		int team = pPlayer->GetTeamNumber();

		// if its after the round and bonus round is on, we can only pick it up if we are winners

		if ( team == TEAM_ALLIES && state == STATE_AXIS_WIN )
			return;

		if ( team == TEAM_AXIS && state == STATE_ALLIES_WIN )
			return;
	}
	else
	{
		// if its after the round, and bonus round is off, don't allow anyone to pick it up
		if ( state != STATE_RND_RUNNING )
			return;
	}	

	OnPickedUp();

	char *szClsName = GetExplodingClassname();

	Assert( szClsName );

	CBaseCombatWeapon *pWeapon = dynamic_cast<CBaseCombatWeapon *>( pPlayer->GiveNamedItem( szClsName ) );
	
	Assert( pWeapon && "Wpn pointer has to be valid for us to pick up this grenade" );

	if( pWeapon )
	{
		variant_t flDetTime;
		flDetTime.SetFloat( m_flDetonateTime );
		pWeapon->AcceptInput( "DetonateTime", this, this, flDetTime, 0 );

#ifdef DBGFLAG_ASSERT
		bool bSuccess = 
#endif
			pPlayer->Weapon_Switch( pWeapon );

		Assert( bSuccess );		
		
		//Remove the one we picked up
		SetThink( NULL );
		UTIL_Remove( this );
	}
}

int CDODBaseGrenade::OnTakeDamage( const CTakeDamageInfo &info )
{
	if( info.GetDamageType() & DMG_BULLET )
	{
		// Don't allow players to shoot grenades
		return 0;
	}
	else if( info.GetDamageType() & DMG_BLAST )
	{
		// Don't allow explosion force to move grenades
		return 0;
	}

	VPhysicsTakeDamage( info );

	return BaseClass::OnTakeDamage( info );
}

void CDODBaseGrenade::Detonate()
{
	// Don't explode after the round has ended
	if ( dod_bonusround.GetBool() == false && DODGameRules()->State_Get() != STATE_RND_RUNNING )
	{
		SetDamage( 0 );
	}

	// stun players in a radius
	const float flStunDamage = 100;

	CTakeDamageInfo info( this, GetThrower(), GetBlastForce(), GetAbsOrigin(), flStunDamage, DMG_STUN );
	DODGameRules()->RadiusStun( info, GetAbsOrigin(), m_DmgRadius );

	BaseClass::Detonate();
}

bool CDODBaseGrenade::CreateVPhysics()
{
	// Create the object in the physics system
	VPhysicsInitNormal( SOLID_BBOX, 0, false );
	return true;
}

void CDODBaseGrenade::Explode( trace_t *pTrace, int bitsDamageType )
{
	SetModelName( NULL_STRING );//invisible
	AddSolidFlags( FSOLID_NOT_SOLID );

	m_takedamage = DAMAGE_NO;

	// Pull out of the wall a bit
	if ( pTrace->fraction != 1.0 )
	{
		SetAbsOrigin( pTrace->endpos + (pTrace->plane.normal * 0.6) );
	}

	// Explosion effect on client
	Vector vecOrigin = GetAbsOrigin();
	CPVSFilter filter( vecOrigin );
	TE_DODExplosion( filter, 0.0f, vecOrigin, pTrace->plane.normal );

	// Use the thrower's position as the reported position
	Vector vecReported = GetThrower() ? GetThrower()->GetAbsOrigin() : vec3_origin;

	CTakeDamageInfo info( this, GetThrower(), GetBlastForce(), GetAbsOrigin(), m_flDamage, bitsDamageType, 0, &vecReported );

	RadiusDamage( info, vecOrigin, GetDamageRadius(), CLASS_NONE, NULL );

	// Don't decal players with scorch.
	if ( pTrace->m_pEnt && !pTrace->m_pEnt->IsPlayer() )
	{
		UTIL_DecalTrace( pTrace, "Scorch" );
	}

	SetThink( &CBaseGrenade::SUB_Remove );
	SetTouch( NULL );

	AddEffects( EF_NODRAW );
	SetAbsVelocity( vec3_origin );
	SetNextThink( gpGlobals->curtime );
}

// this will hit only things that are in newCollisionGroup, but NOT in collisionGroupAlreadyChecked
class CTraceFilterCollisionGroupDelta : public CTraceFilterEntitiesOnly
{
public:
	// It does have a base, but we'll never network anything below here..
	DECLARE_CLASS_NOBASE( CTraceFilterCollisionGroupDelta );

	CTraceFilterCollisionGroupDelta( const IHandleEntity *passentity, int collisionGroupAlreadyChecked, int newCollisionGroup )
		: m_pPassEnt(passentity), m_collisionGroupAlreadyChecked( collisionGroupAlreadyChecked ), m_newCollisionGroup( newCollisionGroup )
	{
	}

	virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask )
	{
		if ( !PassServerEntityFilter( pHandleEntity, m_pPassEnt ) )
			return false;
		CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity );

		if ( pEntity )
		{
			if ( g_pGameRules->ShouldCollide( m_collisionGroupAlreadyChecked, pEntity->GetCollisionGroup() ) )
				return false;
			if ( g_pGameRules->ShouldCollide( m_newCollisionGroup, pEntity->GetCollisionGroup() ) )
				return true;
		}

		return false;
	}

protected:
	const IHandleEntity *m_pPassEnt;
	int		m_collisionGroupAlreadyChecked;
	int		m_newCollisionGroup;
};


const float GRENADE_COEFFICIENT_OF_RESTITUTION = 0.2f;

void CDODBaseGrenade::VPhysicsUpdate( IPhysicsObject *pPhysics )
{
	BaseClass::VPhysicsUpdate( pPhysics );
	Vector vel;
	AngularImpulse angVel;
	pPhysics->GetVelocity( &vel, &angVel );

	Vector start = GetAbsOrigin();
	// find all entities that my collision group wouldn't hit, but COLLISION_GROUP_NONE would and bounce off of them as a ray cast
	CTraceFilterCollisionGroupDelta filter( this, GetCollisionGroup(), COLLISION_GROUP_NONE );
	trace_t tr;

	UTIL_TraceLine( start, start + vel * gpGlobals->frametime, CONTENTS_HITBOX|CONTENTS_MONSTER|CONTENTS_SOLID, &filter, &tr );

	bool bHitTeammate = false;

	if ( m_bCollideWithTeammates == false && tr.m_pEnt && tr.m_pEnt->IsPlayer() && tr.m_pEnt->GetTeamNumber() == GetTeamNumber() )
	{
		bHitTeammate = true;
	}

	if ( tr.startsolid )
	{
		if ( m_bInSolid == false && bHitTeammate == false )
		{
			// UNDONE: Do a better contact solution that uses relative velocity?
			vel *= -GRENADE_COEFFICIENT_OF_RESTITUTION; // bounce backwards
			pPhysics->SetVelocity( &vel, NULL );
		}
		m_bInSolid = true;
		return;
	}
	m_bInSolid = false;
	if ( tr.DidHit() && bHitTeammate == false )
	{
		Vector dir = vel;
		VectorNormalize(dir);

		float flPercent = vel.Length() / 2000;
		float flDmg = 5 * flPercent;

		// send a tiny amount of damage so the character will react to getting bonked
		CTakeDamageInfo info( this, GetThrower(), pPhysics->GetMass() * vel, GetAbsOrigin(), flDmg, DMG_CRUSH );
		tr.m_pEnt->DispatchTraceAttack( info, dir, &tr );
		ApplyMultiDamage();

		if ( vel.Length() > 1000 )
		{
			CTakeDamageInfo stunInfo( this, GetThrower(), vec3_origin, GetAbsOrigin(), flDmg, DMG_STUN );
			tr.m_pEnt->TakeDamage( stunInfo );
		}

		// reflect velocity around normal
		vel = -2.0f * tr.plane.normal * DotProduct(vel,tr.plane.normal) + vel;

		// absorb 80% in impact
		vel *= GetElasticity();
		angVel *= -0.5f;
		pPhysics->SetVelocity( &vel, &angVel );
	}
}

float CDODBaseGrenade::GetElasticity( void )
{
	return GRENADE_COEFFICIENT_OF_RESTITUTION;
}

const float DOD_GRENADE_WINDOW_BREAK_DAMPING_AMOUNT = 0.5f;

// If we hit a window, let it break and continue on our way
// with a damped speed
void CDODBaseGrenade::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent )
{
	CBreakable *pBreakable = dynamic_cast<CBreakable*>( pEvent->pEntities[!index] );
	if ( pBreakable && pBreakable->GetMaterialType() == matGlass && VPhysicsGetObject() )
	{
		// don't stop, go through this entity after breaking it
		Vector dampedVelocity = DOD_GRENADE_WINDOW_BREAK_DAMPING_AMOUNT * pEvent->preVelocity[index];
		VPhysicsGetObject()->SetVelocity( &dampedVelocity, &pEvent->preAngularVelocity[index] );
	}	
	else
		BaseClass::VPhysicsCollision( index, pEvent );
}