//========= Copyright © 1996-2005, 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"

// 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


CLIENTEFFECT_REGISTER_BEGIN( PrecacheSteamJet )
CLIENTEFFECT_MATERIAL( "particle/particle_smokegrenade" )
CLIENTEFFECT_MATERIAL( "sprites/heatwave" )
CLIENTEFFECT_REGISTER_END()

//==================================================
// 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;

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 ) ),
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(&g_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 = m_ParticleEffect.FindOrAddMaterial("particle/particle_smokegrenade");
		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;
}

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

	// Add new particles.
	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;
	}

	// Set the bbox so the particle manager knows when to draw this steamjet.
	Vector vEndPoint = GetAbsOrigin() + forward * m_Speed;
	Vector vMin, vMax;
	VectorMin( GetAbsOrigin(), vEndPoint, vMin );
	VectorMax( GetAbsOrigin(), vEndPoint, vMax );
	m_ParticleEffect.SetBBox( vMin, vMax );

	if( m_bEmit && m_ParticleEffect.WasDrawnPrevFrame() )
	{
		float tempDelta = fTimeDelta;
		while(m_ParticleSpawn.NextEvent(tempDelta))
		{
			// 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( -8.0f, 8.0f );
			}
		}

		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 )
{
	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;
		}
	}
}


