//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Implements a particle system steam jet.
//
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "particle_prototype.h"
#include "particle_util.h"
#include "baseparticleentity.h"
#include "clienteffectprecachesystem.h"
#include "fx.h"

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

//NOTENOTE: Mirrored in dlls\steamjet.h
#define	STEAM_NORMAL	0
#define	STEAM_HEATWAVE	1

#define STEAMJET_NUMRAMPS			5
#define SF_EMISSIVE					0x00000001


//==================================================
// C_SteamJet
//==================================================

class C_SteamJet : public C_BaseParticleEntity, public IPrototypeAppEffect
{
public:
	DECLARE_CLIENTCLASS();
	DECLARE_CLASS( C_SteamJet, C_BaseParticleEntity );

	C_SteamJet();
	~C_SteamJet();

	class SteamJetParticle : public Particle
	{
	public:
		Vector			m_Velocity;
		float			m_flRoll;
		float			m_flRollDelta;
		float			m_Lifetime;
		float			m_DieTime;
		unsigned char	m_uchStartSize;
		unsigned char	m_uchEndSize;
	};

	int IsEmissive( void ) { return ( m_spawnflags & SF_EMISSIVE ); }

//C_BaseEntity
public:

	virtual void	OnDataChanged( DataUpdateType_t updateType );


//IPrototypeAppEffect
public:
	virtual void		Start(CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs);
	virtual bool		GetPropEditInfo(RecvTable **ppTable, void **ppObj);


//IParticleEffect
public:
	virtual void	Update(float fTimeDelta);
	virtual void RenderParticles( CParticleRenderIterator *pIterator );
	virtual void SimulateParticles( CParticleSimulateIterator *pIterator );


//Stuff from the datatable
public:

	float			m_SpreadSpeed;
	float			m_Speed;
	float			m_StartSize;
	float			m_EndSize;
	float			m_Rate;
	float			m_JetLength;	// Length of the jet. Lifetime is derived from this.

	int				m_bEmit;		// Emit particles?
	int				m_nType;		// Type of particles to emit
	bool			m_bFaceLeft;	// For support of legacy env_steamjet entity, which faced left instead of forward.

	int				m_spawnflags;
	float			m_flRollSpeed;

private:

	void			UpdateLightingRamp();

private:

	// Stored the last time it updates the lighting ramp, so it can cache the values.
	Vector			m_vLastRampUpdatePos;
	QAngle			m_vLastRampUpdateAngles;

	float			m_Lifetime;		// Calculated from m_JetLength / m_Speed;

	// We sample the world to get these colors and ramp the particles.
	Vector			m_Ramps[STEAMJET_NUMRAMPS];

	CParticleMgr		*m_pParticleMgr;
	PMaterialHandle	m_MaterialHandle;
	TimedEvent		m_ParticleSpawn;

private:
					C_SteamJet( const C_SteamJet & );
};


// ------------------------------------------------------------------------- //
// Tables.
// ------------------------------------------------------------------------- //

// Expose to the particle app.
EXPOSE_PROTOTYPE_EFFECT(SteamJet, C_SteamJet);


// Datatable..
IMPLEMENT_CLIENTCLASS_DT(C_SteamJet, DT_SteamJet, CSteamJet)
	RecvPropFloat(RECVINFO(m_SpreadSpeed), 0),
	RecvPropFloat(RECVINFO(m_Speed), 0),
	RecvPropFloat(RECVINFO(m_StartSize), 0),
	RecvPropFloat(RECVINFO(m_EndSize), 0),
	RecvPropFloat(RECVINFO(m_Rate), 0),
	RecvPropFloat(RECVINFO(m_JetLength), 0),
	RecvPropInt(RECVINFO(m_bEmit), 0),
	RecvPropInt(RECVINFO(m_bFaceLeft), 0),
	RecvPropInt(RECVINFO(m_nType), 0),
	RecvPropInt( RECVINFO( m_spawnflags ) ),
	RecvPropFloat(RECVINFO(m_flRollSpeed), 0 ),
END_RECV_TABLE()

// ------------------------------------------------------------------------- //
// C_SteamJet implementation.
// ------------------------------------------------------------------------- //
C_SteamJet::C_SteamJet()
{
	m_pParticleMgr = NULL;
	m_MaterialHandle = INVALID_MATERIAL_HANDLE;
	
	m_SpreadSpeed = 15;
	m_Speed = 120;
	m_StartSize = 10;
	m_EndSize = 25;
	m_Rate = 26;
	m_JetLength = 80;
	m_bEmit = true;
	m_bFaceLeft = false;
	m_ParticleEffect.SetAlwaysSimulate( false ); // Don't simulate outside the PVS or frustum.

	m_vLastRampUpdatePos.Init( 1e24, 1e24, 1e24 );
	m_vLastRampUpdateAngles.Init( 1e24, 1e24, 1e24 );
}


C_SteamJet::~C_SteamJet()
{
	if(m_pParticleMgr)
		m_pParticleMgr->RemoveEffect( &m_ParticleEffect );
}


//-----------------------------------------------------------------------------
// Purpose: Called after a data update has occured
// Input  : bnewentity - 
//-----------------------------------------------------------------------------
void C_SteamJet::OnDataChanged(DataUpdateType_t updateType)
{
	C_BaseEntity::OnDataChanged(updateType);

	if(updateType == DATA_UPDATE_CREATED)
	{
		Start(ParticleMgr(), NULL);
	}

	// Recalulate lifetime in case length or speed changed.
	m_Lifetime = m_JetLength / m_Speed;
	m_ParticleEffect.SetParticleCullRadius( MAX(m_StartSize, m_EndSize) );
}


//-----------------------------------------------------------------------------
// Purpose: Starts the effect
// Input  : *pParticleMgr - 
//			*pArgs - 
//-----------------------------------------------------------------------------
void C_SteamJet::Start(CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs)
{
	pParticleMgr->AddEffect( &m_ParticleEffect, this );
	
	switch(m_nType)
	{
	case STEAM_NORMAL:
	default:
		m_MaterialHandle = g_Mat_DustPuff[0];
		break;

	case STEAM_HEATWAVE:
		m_MaterialHandle = m_ParticleEffect.FindOrAddMaterial("sprites/heatwave");
		break;
	}

	m_ParticleSpawn.Init(m_Rate);
	m_Lifetime = m_JetLength / m_Speed;
	m_pParticleMgr = pParticleMgr;

	UpdateLightingRamp();
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : **ppTable - 
//			**ppObj - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool C_SteamJet::GetPropEditInfo( RecvTable **ppTable, void **ppObj )
{
	*ppTable = &REFERENCE_RECV_TABLE(DT_SteamJet);
	*ppObj = this;
	return true;
}


// This might be useful someday.
/*
void CalcFastApproximateRenderBoundsAABB( C_BaseEntity *pEnt, float flBloatSize, Vector *pMin, Vector *pMax )
{
	C_BaseEntity *pParent = pEnt->GetMoveParent();
	if ( pParent )
	{
		// Get the parent's abs space world bounds.
		CalcFastApproximateRenderBoundsAABB( pParent, 0, pMin, pMax );

		// Add the maximum of our local render bounds. This is making the assumption that we can be at any
		// point and at any angle within the parent's world space bounds.
		Vector vAddMins, vAddMaxs;
		pEnt->GetRenderBounds( vAddMins, vAddMaxs );

		flBloatSize += MAX( vAddMins.Length(), vAddMaxs.Length() );
	}
	else
	{
		// Start out with our own render bounds. Since we don't have a parent, this won't incur any nasty 
		pEnt->GetRenderBoundsWorldspace( *pMin, *pMax );
	}

	// Bloat the box.
	if ( flBloatSize )
	{
		*pMin -= Vector( flBloatSize, flBloatSize, flBloatSize );
		*pMax += Vector( flBloatSize, flBloatSize, flBloatSize );
	}
}
*/


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : fTimeDelta - 
//-----------------------------------------------------------------------------

void C_SteamJet::Update(float fTimeDelta)
{
	if(!m_pParticleMgr)
	{
		assert(false);
		return;
	}

	if( m_bEmit )
	{
		// Add new particles.
		int nToEmit = 0;
		float tempDelta = fTimeDelta;
		while( m_ParticleSpawn.NextEvent(tempDelta) )
			++nToEmit;

		if ( nToEmit > 0 )
		{
			Vector forward, right, up;
			AngleVectors(GetAbsAngles(), &forward, &right, &up);			

			// Legacy env_steamjet entities faced left instead of forward.
			if (m_bFaceLeft)
			{
				Vector temp = forward;
				forward = -right;
				right = temp;
			}

			// EVIL: Ideally, we could tell the renderer our OBB, and let it build a big box that encloses
			// the entity with its parent so it doesn't have to setup its parent's bones here.
			Vector vEndPoint = GetAbsOrigin() + forward * m_Speed;
			Vector vMin, vMax;
			VectorMin( GetAbsOrigin(), vEndPoint, vMin );
			VectorMax( GetAbsOrigin(), vEndPoint, vMax );
			m_ParticleEffect.SetBBox( vMin, vMax );

			if ( m_ParticleEffect.WasDrawnPrevFrame() )
			{
				while ( nToEmit-- )
				{
					// Make a new particle.
					if( SteamJetParticle *pParticle = (SteamJetParticle*) m_ParticleEffect.AddParticle( sizeof(SteamJetParticle), m_MaterialHandle ) )
					{
						pParticle->m_Pos = GetAbsOrigin();
						
						pParticle->m_Velocity = 
							FRand(-m_SpreadSpeed,m_SpreadSpeed) * right +
							FRand(-m_SpreadSpeed,m_SpreadSpeed) * up +
							m_Speed * forward;
						
						pParticle->m_Lifetime	= 0;
						pParticle->m_DieTime	= m_Lifetime;

						pParticle->m_uchStartSize	= m_StartSize;
						pParticle->m_uchEndSize		= m_EndSize;

						pParticle->m_flRoll = random->RandomFloat( 0, 360 );
						pParticle->m_flRollDelta = random->RandomFloat( -m_flRollSpeed, m_flRollSpeed );
					}
				}
			}

			UpdateLightingRamp();
		}	
	}
}


// Render a quad on the screen where you pass in color and size.
// Normal is random and "flutters"
inline void RenderParticle_ColorSizePerturbNormal(
	ParticleDraw* pDraw,									
	const Vector &pos,
	const Vector &color,
	const float alpha,
	const float size
	)
{
	// Don't render totally transparent particles.
	if( alpha < 0.001f )
		return;

	CMeshBuilder *pBuilder = pDraw->GetMeshBuilder();
	if( !pBuilder )
		return;

	unsigned char ubColor[4];
	ubColor[0] = (unsigned char)RoundFloatToInt( color.x * 254.9f );
	ubColor[1] = (unsigned char)RoundFloatToInt( color.y * 254.9f );
	ubColor[2] = (unsigned char)RoundFloatToInt( color.z * 254.9f );
	ubColor[3] = (unsigned char)RoundFloatToInt( alpha * 254.9f );

	Vector vNorm;
	
	vNorm.Random( -1.0f, 1.0f );
	
	// Add the 4 corner vertices.
	pBuilder->Position3f( pos.x-size, pos.y-size, pos.z );
	pBuilder->Color4ubv( ubColor );
	pBuilder->Normal3fv( vNorm.Base() );
	pBuilder->TexCoord2f( 0, 0, 1.0f );
 	pBuilder->AdvanceVertex();

	pBuilder->Position3f( pos.x-size, pos.y+size, pos.z );
	pBuilder->Color4ubv( ubColor );
	pBuilder->Normal3fv( vNorm.Base() );
	pBuilder->TexCoord2f( 0, 0, 0 );
 	pBuilder->AdvanceVertex();

	pBuilder->Position3f( pos.x+size, pos.y+size, pos.z );
	pBuilder->Color4ubv( ubColor );
	pBuilder->Normal3fv( vNorm.Base() );
	pBuilder->TexCoord2f( 0, 1.0f, 0 );
 	pBuilder->AdvanceVertex();

	pBuilder->Position3f( pos.x+size, pos.y-size, pos.z );
	pBuilder->Color4ubv( ubColor );
	pBuilder->Normal3fv( vNorm.Base() );
	pBuilder->TexCoord2f( 0, 1.0f, 1.0f );
 	pBuilder->AdvanceVertex();
}


void C_SteamJet::RenderParticles( CParticleRenderIterator *pIterator )
{
	const SteamJetParticle *pParticle = (const SteamJetParticle*)pIterator->GetFirst();
	while ( pParticle )
	{
		// Render.
		Vector tPos;
		TransformParticle(m_pParticleMgr->GetModelView(), pParticle->m_Pos, tPos);
		float sortKey = tPos.z;

		float lifetimeT = pParticle->m_Lifetime / (pParticle->m_DieTime + 0.001);
		float fRamp = lifetimeT * (STEAMJET_NUMRAMPS-1);
		int iRamp = (int)fRamp;
		float fraction = fRamp - iRamp;
		
		Vector vRampColor = m_Ramps[iRamp] + (m_Ramps[iRamp+1] - m_Ramps[iRamp]) * fraction;

		vRampColor[0] = MIN( 1.0f, vRampColor[0] );
		vRampColor[1] = MIN( 1.0f, vRampColor[1] );
		vRampColor[2] = MIN( 1.0f, vRampColor[2] );

		float sinLifetime = sin(pParticle->m_Lifetime * 3.14159f / pParticle->m_DieTime);

		if ( m_nType == STEAM_HEATWAVE )
		{
			RenderParticle_ColorSizePerturbNormal(
				pIterator->GetParticleDraw(),
				tPos,
				vRampColor,
				sinLifetime * (m_clrRender->a/255.0f),
				FLerp(m_StartSize, m_EndSize, pParticle->m_Lifetime));
		}
		else
		{
			RenderParticle_ColorSizeAngle(
				pIterator->GetParticleDraw(),
				tPos,
				vRampColor,
				sinLifetime * (m_clrRender->a/255.0f),
				FLerp(pParticle->m_uchStartSize, pParticle->m_uchEndSize, pParticle->m_Lifetime),
				pParticle->m_flRoll );
		}

		pParticle = (const SteamJetParticle*)pIterator->GetNext( sortKey );
	}
}


void C_SteamJet::SimulateParticles( CParticleSimulateIterator *pIterator )
{
	//Don't simulate if we're emiting particles...
	//This fixes the cases where looking away from a steam jet and then looking back would cause a break on the stream.
	if ( m_ParticleEffect.WasDrawnPrevFrame() == false && m_bEmit )
		return;

	SteamJetParticle *pParticle = (SteamJetParticle*)pIterator->GetFirst();
	while ( pParticle )
	{
		// Should this particle die?
		pParticle->m_Lifetime += pIterator->GetTimeDelta();
		
		if( pParticle->m_Lifetime > pParticle->m_DieTime )
		{
			pIterator->RemoveParticle( pParticle );
		}
		else
		{
			pParticle->m_flRoll += pParticle->m_flRollDelta * pIterator->GetTimeDelta();
			pParticle->m_Pos = pParticle->m_Pos + pParticle->m_Velocity * pIterator->GetTimeDelta();
		}

		pParticle = (SteamJetParticle*)pIterator->GetNext();
	}
}


void C_SteamJet::UpdateLightingRamp()
{
	if( VectorsAreEqual( m_vLastRampUpdatePos, GetAbsOrigin(), 0.1 ) && 
		QAnglesAreEqual( m_vLastRampUpdateAngles, GetAbsAngles(), 0.1 ) )
	{
		return;
	}

	m_vLastRampUpdatePos = GetAbsOrigin();
	m_vLastRampUpdateAngles = GetAbsAngles();

	// Sample the world lighting where we think the particles will be.
	Vector forward, right, up;
	AngleVectors(GetAbsAngles(), &forward, &right, &up);

	// Legacy env_steamjet entities faced left instead of forward.
	if (m_bFaceLeft)
	{
		Vector temp = forward;
		forward = -right;
		right = temp;
	}

	Vector startPos = GetAbsOrigin();
	Vector endPos = GetAbsOrigin() + forward * (m_Speed * m_Lifetime);

	for(int iRamp=0; iRamp < STEAMJET_NUMRAMPS; iRamp++)
	{
		float t = (float)iRamp / (STEAMJET_NUMRAMPS-1);
		Vector vTestPos = startPos + (endPos - startPos) * t;
		
		Vector *pRamp = &m_Ramps[iRamp];
		*pRamp = WorldGetLightForPoint(vTestPos, false);
		
		if ( IsEmissive() )
		{
			pRamp->x += (m_clrRender->r/255.0f);
			pRamp->y += (m_clrRender->g/255.0f);
			pRamp->z += (m_clrRender->b/255.0f);

			pRamp->x = clamp( pRamp->x, 0.0f, 1.0f );
			pRamp->y = clamp( pRamp->y, 0.0f, 1.0f );
			pRamp->z = clamp( pRamp->z, 0.0f, 1.0f );
		}
		else
		{
			pRamp->x *= (m_clrRender->r/255.0f);
			pRamp->y *= (m_clrRender->g/255.0f);
			pRamp->z *= (m_clrRender->b/255.0f);
		}

		// Renormalize?
		float maxVal = MAX(pRamp->x, MAX(pRamp->y, pRamp->z));
		if(maxVal > 1)
		{
			*pRamp = *pRamp / maxVal;
		}
	}
}