//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Local fast collision system for particles
//
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "particle_collision.h"
#include "engine/IVDebugOverlay.h"

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

#ifdef _XBOX
#define	__DEBUG_PARTICLE_COLLISION_RETEST			0
#else
#define	__DEBUG_PARTICLE_COLLISION_RETEST			1
#endif // _XBOX

#define	__DEBUG_PARTICLE_COLLISION_OVERLAY			0
#define	__DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME	0.1f

#define	NUM_DISCREET_STEPS		8.0f
#define	NUM_SIMULATION_SECONDS	2.0f

#define	COLLISION_EPSILON		0.01f

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CBaseSimpleCollision::CBaseSimpleCollision( void )
{
	ClearActivePlanes();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &origin - 
//			radius - 
//-----------------------------------------------------------------------------
void CBaseSimpleCollision::Setup( const Vector &origin, float speed, float gravity )
{
	TestForPlane( origin, Vector(  1,  0,  0 ), speed, gravity );
	TestForPlane( origin, Vector( -1,  0,  0 ), speed, gravity );
	TestForPlane( origin, Vector(  0,  1,  0 ), speed, gravity );
	TestForPlane( origin, Vector(  0, -1,  0 ), speed, gravity );
	TestForPlane( origin, Vector(  0,  0,  1 ), speed, gravity );
	TestForPlane( origin, Vector(  0,  0, -1 ), speed, gravity );
}

//-----------------------------------------------------------------------------
// Purpose: Trace line for super-simplified traces
// Input  : &start - start position
//			&end - end position
//			*pTrace - trace structure to fill
//			coarse - tests again with a real trace unless coarse is set
//-----------------------------------------------------------------------------
void CBaseSimpleCollision::TraceLine( const Vector &start, const Vector &end, trace_t *pTrace, bool coarse )
{
	//Iterate over all active planes
	for ( int i = 0; i < m_nActivePlanes; i++ )
	{
		//Must be a valid plane
		if ( m_collisionPlanes[i].m_Dist == -1.0f )
			continue;

		//Get our information about the relation to this plane
		float dot1 = m_collisionPlanes[i].DistTo(start);
		float dot2 = m_collisionPlanes[i].DistTo(end);

		//Don't consider particles on the backside of planes
		if ( dot1 < -COLLISION_EPSILON )
			continue;

		//Must be crossing the plane's boundary
		if ( ( dot1 > COLLISION_EPSILON ) == ( dot2 > COLLISION_EPSILON ) )
			continue;

		//Find the intersection point
		float	t = dot1 / (dot1 - dot2);
		Vector	vIntersection = start + (end - start) * t;

		//Fake the collision info
		pTrace->endpos			= vIntersection;
		pTrace->fraction		= t	- COLLISION_EPSILON;
		pTrace->plane.normal	= m_collisionPlanes[i].m_Normal;
		pTrace->plane.dist		= m_collisionPlanes[i].m_Dist;
		
		//If we need an exact trace, test again on a successful hit
		if ( ( coarse == false ) && ( pTrace->fraction < 1.0f ) )
		{
			UTIL_TraceLine( start, end, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, pTrace );
		}

		#if	__DEBUG_PARTICLE_COLLISION_OVERLAY
		debugoverlay->AddBoxOverlay( vIntersection, Vector(-1,-1,-1), Vector(1,1,1), QAngle(0,0,0), 0, 255, 0, 16, __DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME );
		#endif	//__DEBUG_PARTICLE_COLLISION_OVERLAY
		
		//Done
		return;
	}

	//Fell through, so clear all the fields
	pTrace->plane.normal[0] = 0.0f;
	pTrace->plane.normal[1] = 0.0f;
	pTrace->plane.normal[2] = 0.0f;
	pTrace->plane.dist		= 0.0f;
	pTrace->fraction		= 1.0f;
	pTrace->allsolid		= false;
	pTrace->startsolid		= false;
	pTrace->m_pEnt			= NULL;
}

//-----------------------------------------------------------------------------
// Purpose: Tests the planes against all others for validity
// Input  : *plane - plane to test
//-----------------------------------------------------------------------------
void CBaseSimpleCollision::ConsiderPlane( cplane_t *plane )
{
	//Test against all other active planes
	for ( int i = 0; i < m_nActivePlanes; i++ )
	{
		if ( m_collisionPlanes[i].m_Dist != -1.0f )
		{
			//Test for coplanar
			if ( ( m_collisionPlanes[i].m_Normal == plane->normal ) && ( m_collisionPlanes[i].m_Dist == plane->dist ) )
				return;
		}
	}

	//Don't overrun
	if ( m_nActivePlanes >= MAX_COLLISION_PLANES )
		return;

	//Take it
	m_collisionPlanes[m_nActivePlanes].m_Dist	= plane->dist;
	m_collisionPlanes[m_nActivePlanes].m_Normal	= plane->normal;
	m_nActivePlanes++;
}

//-----------------------------------------------------------------------------
// Purpose: Runs a simulation of the average particle's movement, looking for collisions along the way
// Input  : &start - start of the simulation
//			&dir - direction of travel
//			speed - speed of the particle
//			gravity - gravity being used
//-----------------------------------------------------------------------------
void CBaseSimpleCollision::TestForPlane( const Vector &start, const Vector &dir, float speed, float gravity )
{
	trace_t	tr;
	Vector	testStart, testEnd;

	testStart = start;

	//Setup our step increments
	float	dStepTime = (NUM_SIMULATION_SECONDS/NUM_DISCREET_STEPS);
	Vector	vStepIncr = dir * ( speed * dStepTime );
	float	flGravIncr = gravity*dStepTime;

	//Simulate collsions in discreet steps
	for ( int i = 1; i <= NUM_DISCREET_STEPS; i++ )
	{
		testEnd		= testStart + vStepIncr;
		testEnd[2] -= flGravIncr * (0.5f*(dStepTime*i)*(dStepTime*i) );

		//Trace the line
		UTIL_TraceLine( testStart, testEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr );

		//See if we found one
		if ( tr.fraction != 1.0f )
		{
			#if	__DEBUG_PARTICLE_COLLISION_OVERLAY
			debugoverlay->AddLineOverlay( testStart, tr.endpos, 255, 0, 0, true, __DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME );
			
			QAngle angles;

			VectorAngles( tr.plane.normal,angles );
			angles[PITCH] += 90;

			debugoverlay->AddBoxOverlay( tr.endpos, Vector(-64,-64,0), Vector(64,64,0), angles, 255, 0, 0, 16, __DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME );
			#endif	//__DEBUG_PARTICLE_COLLISION_OVERLAY
			
			//Test the plane against a set of criteria
			ConsiderPlane( &tr.plane );

			return;
		}

		//We missed
		#if	__DEBUG_PARTICLE_COLLISION_OVERLAY
		debugoverlay->AddLineOverlay( testStart, tr.endpos, 0, 128.0f+(128.0f*((float)i/(float)NUM_DISCREET_STEPS)), 0, true, __DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME );
		#endif	//__DEBUG_PARTICLE_COLLISION_OVERLAY
		
		//Save that position for the next round
		testStart = testEnd;
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CBaseSimpleCollision::ClearActivePlanes( void )
{ 
	for ( int i = 0; i < MAX_COLLISION_PLANES; i++ )
	{
		m_collisionPlanes[i].m_Dist	= -1.0f;
		m_collisionPlanes[i].m_Normal.Init();
	}

	m_nActivePlanes = 0; 
}

//
// CParticleCollision
//

//-----------------------------------------------------------------------------
// Constructor 
//-----------------------------------------------------------------------------
CParticleCollision::CParticleCollision( void )
{
	m_flGravity					= 800.0f;
	m_flCollisionDampen			= 0.5f;
	m_flAngularCollisionDampen	= 0.25f;

	ClearActivePlanes();
}

//-----------------------------------------------------------------------------
// Purpose: Test for surrounding collision surfaces for quick collision testing for the particle system
// Input  : &origin - starting position
//			*dir - direction of movement (if NULL, will do a point emission test in four directions)
//			angularSpread - looseness of the spread
//			minSpeed - minimum speed
//			maxSpeed - maximum speed
//			gravity - particle gravity for the sytem
//			dampen - dampening amount on collisions
//-----------------------------------------------------------------------------
void CParticleCollision::Setup( const Vector &origin, const Vector *dir, float angularSpread, float minSpeed, float maxSpeed, float gravity, float dampen )
{
	//Take the information for this simulation
	m_flGravity				= gravity;
	m_flCollisionDampen		= dampen;
	m_nActivePlanes			= 0;

	//We take a rough estimation of the spray
	float	speedAvg	= (minSpeed+maxSpeed)*0.5f;

	//Point or directed?
	if ( dir == NULL )
	{
		//Test all around
		TestForPlane( origin, Vector(  1,  0,  0 ), speedAvg, gravity );
		TestForPlane( origin, Vector( -1,  0,  0 ), speedAvg, gravity );
		TestForPlane( origin, Vector(  0,  1,  0 ), speedAvg, gravity );
		TestForPlane( origin, Vector(  0, -1,  0 ), speedAvg, gravity );
		TestForPlane( origin, Vector(  0,  0,  1 ), speedAvg, gravity );
		TestForPlane( origin, Vector(  0,  0, -1 ), speedAvg, gravity );
	}
	else
	{
		Vector	vSkewDir, vRight;
		QAngle  vAngles;

		//FIXME: Quicker conversion?
		//FIXME: We need to factor in the angular spread instead
		VectorAngles( *dir, vAngles );
		AngleVectors( vAngles, NULL, &vRight, NULL );

		//Test straight
		TestForPlane( origin, *dir, speedAvg, gravity );

		vSkewDir = vRight;

		//Test right
		TestForPlane( origin, vSkewDir, speedAvg, gravity );

		vSkewDir *= -1.0f;

		//Test left
		TestForPlane( origin, vSkewDir, speedAvg, gravity );

		#if	__DEBUG_PARTICLE_COLLISION_OVERLAY
		DevMsg( 1, "CParticleCollision: Found %d active plane(s)\n", m_nActivePlanes );
		#endif	//__DEBUG_PARTICLE_COLLISION_OVERLAY
	}
}

//-----------------------------------------------------------------------------
// Purpose: Simulate movement, with collision
// Input  : &origin - position of the particle
//			&velocity - velocity of the particle
//			&rollDelta - roll delta of the particle
//			timeDelta - time step
//-----------------------------------------------------------------------------
bool CParticleCollision::MoveParticle( Vector &origin, Vector &velocity, float *rollDelta, float timeDelta, trace_t *pTrace )
{	
	//Don't bother with non-moving particles
	if ( velocity == vec3_origin )
		return false;

	//Factor in gravity
	velocity[2] -= m_flGravity * timeDelta;

	//Move
	Vector testPosition = ( origin + ( velocity * timeDelta ) );	

	//Only collide if we have active planes
	if ( m_nActivePlanes > 0 )
	{
		//Collide
		TraceLine( origin, testPosition, pTrace );

		//See if we hit something
		if ( pTrace->fraction != 1.0f )
		{
			#if	__DEBUG_PARTICLE_COLLISION_RETEST
			//Retest the collision with a true trace line to avoid errant collisions
			UTIL_TraceLine( origin, testPosition, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, pTrace );
			#endif	//__DEBUG_RETEST_COLLISION

			//Did we hit anything?
			if ( pTrace->fraction != 1.0f )
			{
				//See if we've settled
				if ( ( pTrace->plane.normal[2] >= 0.5f ) && ( fabs( velocity[2] ) <= 48.0f ) )
				{		
					//Leave the particle at the collision point
					origin += velocity * ( (pTrace->fraction-COLLISION_EPSILON) * timeDelta );
					
					//Stop the particle
					velocity	= vec3_origin;
					
					if ( rollDelta != NULL )
					{
						*rollDelta	= 0.0f;
					}
					
					return false;
				}
				else
				{
					//Move the particle to the collision point
					origin += velocity * ( (pTrace->fraction-COLLISION_EPSILON) * timeDelta );
					
					//Find the reflection vector
					float proj = velocity.Dot( pTrace->plane.normal );
					velocity += pTrace->plane.normal * (-proj*2.0f);
					
					//Apply dampening
					velocity *= random->RandomFloat( (m_flCollisionDampen-0.1f), (m_flCollisionDampen+0.1f) );

					//Dampen the roll of the particles
					if ( rollDelta != NULL )
					{
						(*rollDelta) *= -0.25f;
					}

					return true;
				}
			}
			else
			{
				#if	__DEBUG_PARTICLE_COLLISION_OVERLAY
				//Display a false hit
				debugoverlay->AddBoxOverlay( pTrace->endpos, Vector(-1,-1,-1), Vector(1,1,1), QAngle(0,0,0), 255, 0, 0, 16, __DEBUG_PARTICLE_COLLISION_OVERLAY_LIFETIME );
				#endif	//__DEBUG_PARTICLE_COLLISION_OVERLAY
			}
		}
	}

	//Simple move, no collision
	origin = testPosition;
	
	return false;
}
