//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "c_smoke_trail.h"
#include "smoke_fog_overlay.h"
#include "engine/IEngineTrace.h"

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

#define SF_EMISSIVE	0x00000001

// ------------------------------------------------------------------------- //
// Definitions
// ------------------------------------------------------------------------- //

static Vector s_FadePlaneDirections[] =
{
	Vector( 1,0,0),
	Vector(-1,0,0),
	Vector(0, 1,0),
	Vector(0,-1,0),
	Vector(0,0, 1),
	Vector(0,0,-1)
};
#define NUM_FADE_PLANES	(sizeof(s_FadePlaneDirections)/sizeof(s_FadePlaneDirections[0]))

// ------------------------------------------------------------------------- //
// Classes
// ------------------------------------------------------------------------- //
class C_FuncSmokeVolume : public C_BaseParticleEntity, public IPrototypeAppEffect
{
public:
	DECLARE_CLASS( C_FuncSmokeVolume, C_BaseParticleEntity );
	DECLARE_CLIENTCLASS();

	C_FuncSmokeVolume();
	~C_FuncSmokeVolume();

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

private:
	class SmokeGrenadeParticle : public Particle
	{
	public:
		float				m_RotationFactor;
		float				m_CurRotation;
		float				m_FadeAlpha;		// Set as it moves around.
		unsigned char		m_ColorInterp;		// Amount between min and max colors.
		unsigned char		m_Color[4];
	};

// C_BaseEntity.
public:
	virtual void	OnDataChanged( DataUpdateType_t updateType );
	
// IPrototypeAppEffect.
public:
	virtual void	Start( CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs );

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

private:
	// The SmokeEmitter represents a grid in 3D space.
	class SmokeParticleInfo
	{
	public:
		SmokeGrenadeParticle	*m_pParticle;
		int						m_TradeIndex;		// -1 if not exchanging yet.
		float					m_TradeClock;		// How long since they started trading.
		float					m_TradeDuration;	// How long the trade will take to finish.
		float					m_FadeAlpha;		// Calculated from nearby world geometry.
		unsigned char			m_Color[4];
	};

	inline int GetSmokeParticleIndex(int x, int y, int z)	
	{
		Assert( IsValidXYZCoords( x, y, z ) );
		return z*m_xCount*m_yCount+y*m_xCount+x;
	}

	inline SmokeParticleInfo *GetSmokeParticleInfo(int x, int y, int z)	
	{
		Assert( IsValidXYZCoords( x, y, z ) );
		return &m_pSmokeParticleInfos[GetSmokeParticleIndex(x,y,z)];
	}

	inline void	GetParticleInfoXYZ(int index, int &x, int &y, int &z)
	{
		Assert( index >= 0 && index < m_xCount * m_yCount * m_zCount );
		z = index / (m_xCount*m_yCount);
		int zIndex = z*m_xCount*m_yCount;
		y = (index - zIndex) / m_xCount;
		int yIndex = y*m_xCount;
		x = index - zIndex - yIndex;
		Assert( IsValidXYZCoords( x, y, z ) );
	}

	inline bool IsValidXYZCoords(int x, int y, int z)
	{
		return x >= 0 && y >= 0 && z >= 0 && x < m_xCount && y < m_yCount && z < m_zCount;
	}

	inline Vector GetSmokeParticlePos(int x, int y, int z )	
	{
		return WorldAlignMins() + 
			Vector( x * m_SpacingRadius * 2 + m_SpacingRadius,
				    y * m_SpacingRadius * 2 + m_SpacingRadius,
				    z * m_SpacingRadius * 2 + m_SpacingRadius );
	}

	inline Vector GetSmokeParticlePosIndex(int index)
	{
		int x, y, z;
		GetParticleInfoXYZ(index, x, y, z);
		return GetSmokeParticlePos(x, y, z);
	}

	// Start filling the smoke volume
	void FillVolume();

private:
// State variables from server.
	color32 m_Color1;
	color32 m_Color2;
	char m_MaterialName[255];
	float m_ParticleDrawWidth;
	float m_ParticleSpacingDistance;
	float m_DensityRampSpeed;
	float m_RotationSpeed;
	float m_MovementSpeed;
	float m_Density;
	int	  m_spawnflags;

private:
	C_FuncSmokeVolume( const C_FuncSmokeVolume & );

	float				m_CurrentDensity;
	float				m_ParticleRadius;
	bool				m_bStarted;

	PMaterialHandle		m_MaterialHandle;

	SmokeParticleInfo	*m_pSmokeParticleInfos;
	int					m_xCount, m_yCount, m_zCount;
	float				m_SpacingRadius;

	Vector				m_MinColor;
	Vector				m_MaxColor;

	Vector m_vLastOrigin;
	QAngle m_vLastAngles;
	bool m_bFirstUpdate;
};

IMPLEMENT_CLIENTCLASS_DT( C_FuncSmokeVolume, DT_FuncSmokeVolume, CFuncSmokeVolume )
	RecvPropInt( RECVINFO( m_Color1 ), 0, RecvProxy_IntToColor32 ),
	RecvPropInt( RECVINFO( m_Color2 ), 0, RecvProxy_IntToColor32 ),
	RecvPropString( RECVINFO( m_MaterialName ) ),
	RecvPropFloat( RECVINFO( m_ParticleDrawWidth ) ),
	RecvPropFloat( RECVINFO( m_ParticleSpacingDistance ) ),
	RecvPropFloat( RECVINFO( m_DensityRampSpeed ) ),
	RecvPropFloat( RECVINFO( m_RotationSpeed ) ),
	RecvPropFloat( RECVINFO( m_MovementSpeed ) ),
	RecvPropFloat( RECVINFO( m_Density ) ),
	RecvPropInt( RECVINFO( m_spawnflags ) ),
	RecvPropDataTable( RECVINFO_DT( m_Collision ), 0, &REFERENCE_RECV_TABLE(DT_CollisionProperty) ),
END_RECV_TABLE()

// Helpers.
// ------------------------------------------------------------------------- //

static inline void InterpColor(unsigned char dest[4], unsigned char src1[4], unsigned char src2[4], float percent)
{
	dest[0] = (unsigned char)(src1[0] + (src2[0] - src1[0]) * percent);
	dest[1] = (unsigned char)(src1[1] + (src2[1] - src1[1]) * percent);
	dest[2] = (unsigned char)(src1[2] + (src2[2] - src1[2]) * percent);
}


static inline int GetWorldPointContents(const Vector &vPos)
{
#if defined(PARTICLEPROTOTYPE_APP)
	return 0;
#else
	return enginetrace->GetPointContents( vPos );
#endif
}

static inline void WorldTraceLine( const Vector &start, const Vector &end, int contentsMask, trace_t *trace )
{
#if defined(PARTICLEPROTOTYPE_APP)
	trace->fraction = 1;
#else
	UTIL_TraceLine(start, end, contentsMask, NULL, COLLISION_GROUP_NONE, trace);
#endif
}

static inline Vector EngineGetLightForPoint(const Vector &vPos)
{
#if defined(PARTICLEPROTOTYPE_APP)
	return Vector(1,1,1);
#else
	return engine->GetLightForPoint(vPos, true);
#endif
}

static inline Vector& EngineGetVecRenderOrigin()
{
#if defined(PARTICLEPROTOTYPE_APP)
	static Vector dummy(0,0,0);
	return dummy;
#else
	extern Vector g_vecRenderOrigin;
	return g_vecRenderOrigin;
#endif
}

static inline float& EngineGetSmokeFogOverlayAlpha()
{
#if defined(PARTICLEPROTOTYPE_APP)
	static float dummy;
	return dummy;
#else
	return g_SmokeFogOverlayAlpha;
#endif
}

static inline C_BaseEntity* ParticleGetEntity( int index )
{
#if defined(PARTICLEPROTOTYPE_APP)
	return NULL;
#else
	return cl_entitylist->GetEnt( index );
#endif
}

// ------------------------------------------------------------------------- //
// C_FuncSmokeVolume
// ------------------------------------------------------------------------- //
C_FuncSmokeVolume::C_FuncSmokeVolume()
{
	m_bFirstUpdate = true;
	m_vLastOrigin.Init();
	m_vLastAngles.Init();

	m_pSmokeParticleInfos = NULL;
	m_SpacingRadius = 0.0f;
	m_ParticleRadius = 0.0f;
	m_MinColor.Init( 1.0, 1.0, 1.0 );
	m_MaxColor.Init( 1.0, 1.0, 1.0 );
}

C_FuncSmokeVolume::~C_FuncSmokeVolume()
{
	delete [] m_pSmokeParticleInfos;
}

static ConVar mat_reduceparticles( "mat_reduceparticles", "0" );

void C_FuncSmokeVolume::OnDataChanged( DataUpdateType_t updateType )
{		
	m_MinColor[0] = ( 1.0f / 255.0f ) * m_Color1.r;
	m_MinColor[1] = ( 1.0f / 255.0f ) * m_Color1.g;
	m_MinColor[2] = ( 1.0f / 255.0f ) * m_Color1.b;

	m_MaxColor[0] = ( 1.0f / 255.0f ) * m_Color2.r;
	m_MaxColor[1] = ( 1.0f / 255.0f ) * m_Color2.g;
	m_MaxColor[2] = ( 1.0f / 255.0f ) * m_Color2.b;

	if ( mat_reduceparticles.GetBool() )
	{
		m_Density *= 0.5f;
		m_ParticleSpacingDistance *= 1.5f;
	}

	m_ParticleRadius = m_ParticleDrawWidth * 0.5f;
	m_SpacingRadius = m_ParticleSpacingDistance * 0.5f;

	m_ParticleEffect.SetParticleCullRadius( m_ParticleRadius );

//	Warning( "m_Density: %f\n", m_Density );
//	Warning( "m_MovementSpeed: %f\n", m_MovementSpeed );
	
	if( updateType == DATA_UPDATE_CREATED )
	{
		Vector size = WorldAlignMaxs() - WorldAlignMins();
		m_xCount = 0.5f + ( size.x / ( m_SpacingRadius * 2.0f ) );
		m_yCount = 0.5f + ( size.y / ( m_SpacingRadius * 2.0f ) );
		m_zCount = 0.5f + ( size.z / ( m_SpacingRadius * 2.0f ) );
		m_CurrentDensity = m_Density;

		delete [] m_pSmokeParticleInfos;
		m_pSmokeParticleInfos = new SmokeParticleInfo[m_xCount * m_yCount * m_zCount];
		Start( ParticleMgr(), NULL );
	}
	BaseClass::OnDataChanged( updateType );
}

void C_FuncSmokeVolume::Start( CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs )
{
	if( !pParticleMgr->AddEffect( &m_ParticleEffect, this ) )
		return;

	m_MaterialHandle = m_ParticleEffect.FindOrAddMaterial( m_MaterialName );
	FillVolume();

	m_bStarted = true;
}


void C_FuncSmokeVolume::Update( float fTimeDelta )
{
	// Update our world space bbox if we've moved at all.
	// We do this manually because sometimes people make HUGE bboxes, and if they're constantly changing because their
	// particles wander outside the current bounds sometimes, it'll be linking them into all the leaves repeatedly.
	const Vector &curOrigin = GetAbsOrigin();
	const QAngle &curAngles = GetAbsAngles();
	if ( !VectorsAreEqual( curOrigin, m_vLastOrigin, 0.1 ) || 
		fabs( curAngles.x - m_vLastAngles.x ) > 0.1 || 
		fabs( curAngles.y - m_vLastAngles.y ) > 0.1 || 
		fabs( curAngles.z - m_vLastAngles.z ) > 0.1 ||
		m_bFirstUpdate )
	{
		m_bFirstUpdate = false;
		m_vLastAngles = curAngles;
		m_vLastOrigin = curOrigin;

		Vector vWorldMins, vWorldMaxs;
		CollisionProp()->WorldSpaceAABB( &vWorldMins, &vWorldMaxs );
		vWorldMins -= Vector( m_ParticleRadius, m_ParticleRadius, m_ParticleRadius );
		vWorldMaxs += Vector( m_ParticleRadius, m_ParticleRadius, m_ParticleRadius );

		m_ParticleEffect.SetBBox( vWorldMins, vWorldMaxs );
	}
		
	// lerp m_CurrentDensity towards m_Density at a rate of m_DensityRampSpeed
	if( m_CurrentDensity < m_Density )
	{
		m_CurrentDensity += m_DensityRampSpeed * fTimeDelta;
		if( m_CurrentDensity > m_Density )
		{
			m_CurrentDensity = m_Density;
		}
	}
	else if( m_CurrentDensity > m_Density )
	{
		m_CurrentDensity -= m_DensityRampSpeed * fTimeDelta;
		if( m_CurrentDensity < m_Density )
		{
			m_CurrentDensity = m_Density;
		}
	}

	if( m_CurrentDensity == 0.0f )
	{
		return;
	}
	
	// This is used to randomize the direction it chooses to move a particle in.

	int offsetLookup[3] = {-1,0,1};

	float tradeDurationMax = m_ParticleSpacingDistance / ( m_MovementSpeed + 0.1f );
	float tradeDurationMin = tradeDurationMax * 0.5f;

	if ( IS_NAN( tradeDurationMax ) || IS_NAN( tradeDurationMin ) )
		return;

//	Warning( "tradeDuration: [%f,%f]\n", tradeDurationMin, tradeDurationMax );
	
	// Update all the moving traders and establish new ones.
	int nTotal = m_xCount * m_yCount * m_zCount;
	for( int i=0; i < nTotal; i++ )
	{
		SmokeParticleInfo *pInfo = &m_pSmokeParticleInfos[i];

		if(!pInfo->m_pParticle)
			continue;
	
		if(pInfo->m_TradeIndex == -1)
		{
			pInfo->m_pParticle->m_FadeAlpha = pInfo->m_FadeAlpha;
			pInfo->m_pParticle->m_Color[0] = pInfo->m_Color[0];
			pInfo->m_pParticle->m_Color[1] = pInfo->m_Color[1];
			pInfo->m_pParticle->m_Color[2] = pInfo->m_Color[2];

			// Is there an adjacent one that's not trading?
			int x, y, z;
			GetParticleInfoXYZ(i, x, y, z);

			int xCountOffset = rand();
			int yCountOffset = rand();
			int zCountOffset = rand();

			bool bFound = false;
			for(int xCount=0; xCount < 3 && !bFound; xCount++)
			{
				for(int yCount=0; yCount < 3 && !bFound; yCount++)
				{
					for(int zCount=0; zCount < 3; zCount++)
					{
						int testX = x + offsetLookup[(xCount+xCountOffset) % 3];
						int testY = y + offsetLookup[(yCount+yCountOffset) % 3];
						int testZ = z + offsetLookup[(zCount+zCountOffset) % 3];

						if(testX == x && testY == y && testZ == z)
							continue;

						if(IsValidXYZCoords(testX, testY, testZ))
						{
							SmokeParticleInfo *pOther = GetSmokeParticleInfo(testX, testY, testZ);
							if(pOther->m_pParticle && pOther->m_TradeIndex == -1)
							{
								// Ok, this one is looking to trade also.
								pInfo->m_TradeIndex = GetSmokeParticleIndex(testX, testY, testZ);
								pOther->m_TradeIndex = i;
								pInfo->m_TradeClock = pOther->m_TradeClock = 0;
								pOther->m_TradeDuration = pInfo->m_TradeDuration = FRand( tradeDurationMin, tradeDurationMax );
								
								bFound = true;
								break;
							}
						}
					}
				}
			}
		}
		else
		{
			SmokeParticleInfo *pOther = &m_pSmokeParticleInfos[pInfo->m_TradeIndex];
			assert(pOther->m_TradeIndex == i);
			
			// This makes sure the trade only gets updated once per frame.
			if(pInfo < pOther)
			{
				// Increment the trade clock..
				pInfo->m_TradeClock = (pOther->m_TradeClock += fTimeDelta);
				int x, y, z;
				GetParticleInfoXYZ(i, x, y, z);
				Vector myPos = GetSmokeParticlePos(x, y, z);
				
				int otherX, otherY, otherZ;
				GetParticleInfoXYZ(pInfo->m_TradeIndex, otherX, otherY, otherZ);
				Vector otherPos = GetSmokeParticlePos(otherX, otherY, otherZ);

				// Is the trade finished?
				if(pInfo->m_TradeClock >= pInfo->m_TradeDuration)
				{
					pInfo->m_TradeIndex = pOther->m_TradeIndex = -1;
					
					pInfo->m_pParticle->m_Pos = otherPos;
					pOther->m_pParticle->m_Pos = myPos;

					SmokeGrenadeParticle *temp = pInfo->m_pParticle;
					pInfo->m_pParticle = pOther->m_pParticle;
					pOther->m_pParticle = temp;
				}
				else
				{			
					// Ok, move them closer.
					float percent = (float)cos(pInfo->m_TradeClock * 2 * 1.57079632f / pInfo->m_TradeDuration);
					percent = percent * 0.5 + 0.5;
					
					pInfo->m_pParticle->m_FadeAlpha  = pInfo->m_FadeAlpha + (pOther->m_FadeAlpha - pInfo->m_FadeAlpha) * (1 - percent);
					pOther->m_pParticle->m_FadeAlpha = pInfo->m_FadeAlpha + (pOther->m_FadeAlpha - pInfo->m_FadeAlpha) * percent;

					InterpColor(pInfo->m_pParticle->m_Color,  pInfo->m_Color, pOther->m_Color, 1-percent);
					InterpColor(pOther->m_pParticle->m_Color, pInfo->m_Color, pOther->m_Color, percent);

					pInfo->m_pParticle->m_Pos  = myPos + (otherPos - myPos) * (1 - percent);
					pOther->m_pParticle->m_Pos = myPos + (otherPos - myPos) * percent;
				}
			}
		}
	}
}


void C_FuncSmokeVolume::RenderParticles( CParticleRenderIterator *pIterator )
{
	if ( m_CurrentDensity == 0 )
		return;

	const SmokeGrenadeParticle *pParticle = (const SmokeGrenadeParticle*)pIterator->GetFirst();
	while ( pParticle )
	{
		Vector renderPos = pParticle->m_Pos;

		// Fade out globally.
		float alpha = m_CurrentDensity;

		// Apply the precalculated fade alpha from world geometry.
		alpha *= pParticle->m_FadeAlpha;
		
		// TODO: optimize this whole routine!
		Vector color = m_MinColor + (m_MaxColor - m_MinColor) * (pParticle->m_ColorInterp / 255.1f);
		if ( IsEmissive() )
		{
			color.x += pParticle->m_Color[0] / 255.0f;
			color.y += pParticle->m_Color[1] / 255.0f;
			color.z += pParticle->m_Color[2] / 255.0f;

			color.x = clamp( color.x, 0.0f, 1.0f );
			color.y = clamp( color.y, 0.0f, 1.0f );
			color.z = clamp( color.z, 0.0f, 1.0f );
		}
		else
		{
			color.x *= pParticle->m_Color[0] / 255.0f;
			color.y *= pParticle->m_Color[1] / 255.0f;
			color.z *= pParticle->m_Color[2] / 255.0f;
		}
		
		Vector tRenderPos;
		TransformParticle( ParticleMgr()->GetModelView(), renderPos, tRenderPos );
		float sortKey = 1;//tRenderPos.z;

		// If we're reducing particle cost, only render sufficiently opaque particles 
		if ( ( alpha > 0.05f ) || !mat_reduceparticles.GetBool() )
		{
			RenderParticle_ColorSizeAngle(
				pIterator->GetParticleDraw(),
				tRenderPos,
				color,
				alpha * GetAlphaDistanceFade(tRenderPos, 10, 30),	// Alpha
				m_ParticleRadius,
				pParticle->m_CurRotation
				);
		}

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


void C_FuncSmokeVolume::SimulateParticles( CParticleSimulateIterator *pIterator )
{
	if ( m_CurrentDensity == 0 )
		return;

	SmokeGrenadeParticle *pParticle = (SmokeGrenadeParticle*)pIterator->GetFirst();
	while ( pParticle )
	{
		pParticle->m_CurRotation += pParticle->m_RotationFactor * ( M_PI / 180.0f ) * m_RotationSpeed * pIterator->GetTimeDelta();
		pParticle = (SmokeGrenadeParticle*)pIterator->GetNext();
	}
}


void C_FuncSmokeVolume::NotifyRemove()
{
	m_xCount = m_yCount = m_zCount = 0;
}


void C_FuncSmokeVolume::FillVolume()
{
	Vector vPos;
	for(int x=0; x < m_xCount; x++)
	{
		for(int y=0; y < m_yCount; y++)
		{
			for(int z=0; z < m_zCount; z++)
			{
				vPos = GetSmokeParticlePos( x, y, z );
				if(SmokeParticleInfo *pInfo = GetSmokeParticleInfo(x,y,z))
				{
					int contents = GetWorldPointContents(vPos);
					if(contents & CONTENTS_SOLID)
					{
						pInfo->m_pParticle = NULL;
					}
					else
					{
						SmokeGrenadeParticle *pParticle = 
							(SmokeGrenadeParticle*)m_ParticleEffect.AddParticle(sizeof(SmokeGrenadeParticle), m_MaterialHandle);

						if(pParticle)
						{
							pParticle->m_Pos = vPos;
							pParticle->m_ColorInterp = (unsigned char)((rand() * 255) / VALVE_RAND_MAX);
							pParticle->m_RotationFactor = FRand( -1.0f, 1.0f ); // Rotation factor.
							pParticle->m_CurRotation = FRand( -m_RotationSpeed, m_RotationSpeed );
						}

#ifdef _DEBUG
						int testX, testY, testZ;
						int index = GetSmokeParticleIndex(x,y,z);
						GetParticleInfoXYZ(index, testX, testY, testZ);
						assert(testX == x && testY == y && testZ == z);
#endif

						Vector vColor = EngineGetLightForPoint(vPos);
						pInfo->m_Color[0] = LinearToTexture( vColor.x );
						pInfo->m_Color[1] = LinearToTexture( vColor.y );
						pInfo->m_Color[2] = LinearToTexture( vColor.z );

						// Cast some rays and if it's too close to anything, fade its alpha down.
						pInfo->m_FadeAlpha = 1;

						for(int i=0; i < NUM_FADE_PLANES; i++)
						{
							trace_t trace;
							WorldTraceLine(vPos, vPos + s_FadePlaneDirections[i] * 100, MASK_SOLID_BRUSHONLY, &trace);
							if(trace.fraction < 1.0f)
							{
								float dist = DotProduct(trace.plane.normal, vPos) - trace.plane.dist;
								if(dist < 0)
								{
									pInfo->m_FadeAlpha = 0;
								}
								else if(dist < m_ParticleRadius)
								{
									float alphaScale = dist / m_ParticleRadius;
									alphaScale *= alphaScale * alphaScale;
									pInfo->m_FadeAlpha *= alphaScale;
								}
							}
						}

						pInfo->m_pParticle = pParticle;
						pInfo->m_TradeIndex = -1;
					}
				}
			}
		}
	}
}