//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $Workfile:     $
// $Date:         $
// $NoKeywords: $
//=============================================================================//


#include "render_pch.h"
#include "gl_matsysiface.h"
#include "gl_cvars.h"
#include "enginetrace.h"
#include "r_local.h"
#include "gl_model_private.h"
#include "materialsystem/imesh.h"
#include "cdll_engine_int.h"
#include "cl_main.h"
#include "debugoverlay.h"

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

static ConVar r_drawlights(  "r_drawlights", "0", FCVAR_CHEAT );
static ConVar r_drawlightinfo(  "r_drawlightinfo", "0", FCVAR_CHEAT );

static bool s_bActivateLightSprites = false;

//-----------------------------------------------------------------------------
// Should we draw light sprites over visible lights?
//-----------------------------------------------------------------------------
bool ActivateLightSprites( bool bActive )
{
	bool bOldValue = s_bActivateLightSprites;
	s_bActivateLightSprites = bActive;
	return bOldValue;
}


#define LIGHT_MIN_LIGHT_VALUE 0.03f

float ComputeLightRadius( dworldlight_t *pLight, bool bIsHDR )
{
	float flLightRadius = pLight->radius;
	if (flLightRadius == 0.0f)
	{
		// HACKHACK: Usually our designers scale the light intensity by 0.5 in HDR
		// This keeps the behavior of the cutoff radius consistent between LDR and HDR
		float minLightValue = bIsHDR ? (LIGHT_MIN_LIGHT_VALUE * 0.5f) : LIGHT_MIN_LIGHT_VALUE;

		// Compute the light range based on attenuation factors
		float flIntensity = sqrtf( DotProduct( pLight->intensity, pLight->intensity ) );
		if (pLight->quadratic_attn == 0.0f)
		{
			if (pLight->linear_attn == 0.0f)
			{
				// Infinite, but we're not going to draw it as such
				flLightRadius = 2000;
			}
			else
			{
				flLightRadius = (flIntensity / minLightValue - pLight->constant_attn) / pLight->linear_attn;
			}
		}
		else
		{
			float a = pLight->quadratic_attn;
			float b = pLight->linear_attn;
			float c = pLight->constant_attn - flIntensity / minLightValue;
			float discrim = b * b - 4 * a * c;
			if (discrim < 0.0f)
			{
				// Infinite, but we're not going to draw it as such
				flLightRadius = 2000;
			}
			else
			{
				flLightRadius = (-b + sqrtf(discrim)) / (2.0f * a);
				if (flLightRadius < 0)
					flLightRadius = 0;
			}
		}
	}

	return flLightRadius;
}


static void DrawLightSprite( dworldlight_t *pLight, float angleAttenFactor )
{
	Vector lightToEye;
	lightToEye = CurrentViewOrigin() - pLight->origin;
	VectorNormalize( lightToEye );
	Vector up( 0.0f, 0.0f, 1.0f );
	Vector right;
	CrossProduct( up, lightToEye, right );
	VectorNormalize( right );
	CrossProduct( lightToEye, right, up );
	VectorNormalize( up );

/*
	up *= dist;
	right *= dist;

	up *= ( 1.0f / 5.0f );
	right *= ( 1.0f / 5.0f );

	up *= 1.0f / sqrt( pLight->constant_attn + dist * pLight->linear_attn + dist * dist * pLight->quadratic_attn );
	right *= 1.0f / sqrt( pLight->constant_attn + dist * pLight->linear_attn + dist * dist * pLight->quadratic_attn );
*/

	//	float distFactor = 1.0f / ( pLight->constant_attn + dist * pLight->linear_attn + dist * dist * pLight->quadratic_attn );
	//float distFactor = 1.0f;
	
	Vector color = pLight->intensity;
	VectorNormalize( color );
	color *= angleAttenFactor;

	color[0] = pow( color[0], 1.0f / 2.2f );
	color[1] = pow( color[1], 1.0f / 2.2f );
	color[2] = pow( color[2], 1.0f / 2.2f );
	
	CMatRenderContextPtr pRenderContext( materials );

	pRenderContext->Bind( g_pMaterialLightSprite );
	IMesh *pMesh = pRenderContext->GetDynamicMesh( );
	CMeshBuilder meshBuilder;
	meshBuilder.Begin( pMesh, MATERIAL_QUADS, 1 );

	float radius = 16.0f;
	Vector p;
	
	ColorClamp( color );
	
	p = pLight->origin + right * radius + up * radius;
	meshBuilder.TexCoord2f( 0, 1.0f, 1.0f );
	meshBuilder.Color3fv( color.Base() );
	meshBuilder.Position3fv( p.Base() );
	meshBuilder.AdvanceVertex();

	p = pLight->origin + right * -radius + up * radius;
	meshBuilder.TexCoord2f( 0, 0.0f, 1.0f );
	meshBuilder.Color3fv( color.Base() );
	meshBuilder.Position3fv( p.Base() );
	meshBuilder.AdvanceVertex();

	p = pLight->origin + right * -radius + up * -radius;
	meshBuilder.TexCoord2f( 0, 0.0f, 0.0f );
	meshBuilder.Color3fv( color.Base() );
	meshBuilder.Position3fv( p.Base() );
	meshBuilder.AdvanceVertex();

	p = pLight->origin + right * radius + up * -radius;
	meshBuilder.TexCoord2f( 0, 1.0f, 0.0f );
	meshBuilder.Color3fv( color.Base() );
	meshBuilder.Position3fv( p.Base() );
	meshBuilder.AdvanceVertex();

	meshBuilder.End();
	pMesh->Draw();
}

#define POINT_THETA_GRID 8
#define POINT_PHI_GRID 8

static void DrawPointLight( const Vector &vecOrigin, float flLightRadius )
{
	int nVertCount = POINT_THETA_GRID * (POINT_PHI_GRID + 1);
	int nIndexCount = 8 * POINT_THETA_GRID * POINT_PHI_GRID;

	CMatRenderContextPtr pRenderContext( materials );

	pRenderContext->Bind( g_materialWorldWireframeZBuffer );
	IMesh *pMesh = pRenderContext->GetDynamicMesh( );
	CMeshBuilder meshBuilder;
	meshBuilder.Begin( pMesh, MATERIAL_LINES, nVertCount, nIndexCount );

	float dTheta = 360.0f / POINT_THETA_GRID;
	float dPhi = 180.0f / POINT_PHI_GRID;

	Vector pt;
	int i;
	float flPhi = 0;
	for ( i = 0; i <= POINT_PHI_GRID; ++i )
	{
		float flSinPhi = sin(DEG2RAD(flPhi));
		float flCosPhi = cos(DEG2RAD(flPhi));
		float flTheta = 0;
		for ( int j = 0; j < POINT_THETA_GRID; ++j )
		{
			pt = vecOrigin;
			pt.x += flLightRadius * cos(DEG2RAD(flTheta)) * flSinPhi;
			pt.y += flLightRadius * sin(DEG2RAD(flTheta)) * flSinPhi;
			pt.z += flLightRadius * flCosPhi;

			meshBuilder.Position3fv( pt.Base() );
			meshBuilder.AdvanceVertex();

			flTheta += dTheta;
		}

		flPhi += dPhi;
	}

	for ( i = 0; i < POINT_THETA_GRID; ++i )
	{
		for ( int j = 0; j < POINT_PHI_GRID; ++j )
		{
			int nNextIndex = (j != POINT_PHI_GRID - 1) ? j + 1 : 0;

			meshBuilder.Index( i * POINT_PHI_GRID + j );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( (i + 1) * POINT_PHI_GRID + j );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( (i + 1) * POINT_PHI_GRID + j );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( (i + 1) * POINT_PHI_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( (i + 1) * POINT_PHI_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( i * POINT_PHI_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( i * POINT_PHI_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( i * POINT_PHI_GRID + j );
			meshBuilder.AdvanceIndex();
		}
	}

	meshBuilder.End();
	pMesh->Draw();
}

//-----------------------------------------------------------------------------
// Draws the spot light
//-----------------------------------------------------------------------------
#define SPOT_GRID_LINE_COUNT 20
#define SPOT_GRID_LINE_DISTANCE 50
#define SPOT_RADIAL_GRID 8

void DrawSpotLight( dworldlight_t *pLight )
{
	float flLightRadius = ComputeLightRadius( pLight, false );

	int nGridLines = (int)(flLightRadius / SPOT_GRID_LINE_DISTANCE) + 1;
	int nVertCount = SPOT_RADIAL_GRID * (nGridLines + 1);
	int nIndexCount = 8 * SPOT_RADIAL_GRID * nGridLines;

	// Compute a basis perpendicular to the normal
	Vector xaxis, yaxis;
	int nMinIndex = fabs(pLight->normal[0]) < fabs(pLight->normal[1]) ? 0 : 1;
	nMinIndex = fabs(pLight->normal[nMinIndex]) < fabs(pLight->normal[2]) ? nMinIndex : 2;
	Vector perp = vec3_origin;
	perp[nMinIndex] = 1.0f;
	CrossProduct( perp, pLight->normal, xaxis );
	VectorNormalize( xaxis );
	CrossProduct( pLight->normal, xaxis, yaxis ); 

	CMatRenderContextPtr pRenderContext( materials );

	pRenderContext->Bind( g_materialWorldWireframeZBuffer );
	IMesh *pMesh = pRenderContext->GetDynamicMesh( );
	CMeshBuilder meshBuilder;
	meshBuilder.Begin( pMesh, MATERIAL_LINES, nVertCount, nIndexCount );

	float flAngle = acos(pLight->stopdot2);
	float flTanAngle = tan(flAngle);
	float dTheta = 360.0f / SPOT_RADIAL_GRID;
	float flDist = 0.0f;

	int i;
	for ( i = 0; i <= nGridLines; ++i )
	{
		Vector pt, vecCenter;
		VectorMA( pLight->origin, flDist, pLight->normal, vecCenter );
		
		float flRadius = flDist * flTanAngle;

		float flTempAngle = 0;
		for ( int j = 0; j < SPOT_RADIAL_GRID; ++j )
		{
			float flSin = sin( DEG2RAD( flTempAngle ) );
			float flCos = cos( DEG2RAD( flTempAngle ) );
			VectorMA( vecCenter, flRadius * flCos, xaxis, pt );
			VectorMA( pt, flRadius * flSin, yaxis, pt );

			meshBuilder.Position3fv( pt.Base() );
			meshBuilder.AdvanceVertex();

			flTempAngle += dTheta;
		}

		flDist += SPOT_GRID_LINE_DISTANCE;
	}

	for ( i = 0; i < nGridLines; ++i )
	{
		for ( int j = 0; j < SPOT_RADIAL_GRID; ++j )
		{
			int nNextIndex = (j != SPOT_RADIAL_GRID - 1) ? j + 1 : 0;

			meshBuilder.Index( i * SPOT_RADIAL_GRID + j );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( (i + 1) * SPOT_RADIAL_GRID + j );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( (i + 1) * SPOT_RADIAL_GRID + j );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( (i + 1) * SPOT_RADIAL_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( (i + 1) * SPOT_RADIAL_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( i * SPOT_RADIAL_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();

			meshBuilder.Index( i * SPOT_RADIAL_GRID + nNextIndex );
			meshBuilder.AdvanceIndex();
			meshBuilder.Index( i * SPOT_RADIAL_GRID + j );
			meshBuilder.AdvanceIndex();
		}
	}

	meshBuilder.End();
	pMesh->Draw();
}


//-----------------------------------------------------------------------------
// Draws sprites over all visible lights
// NOTE: This is used to render env-cubemaps
//-----------------------------------------------------------------------------
void DrawLightSprites( void )
{
	if (!s_bActivateLightSprites)
		return;

	int i;	
	for (i = 0; i < host_state.worldbrush->numworldlights; i++)
	{
		dworldlight_t *pLight = &host_state.worldbrush->worldlights[i];
		trace_t tr;
		CTraceFilterWorldAndPropsOnly traceFilter;
		Ray_t ray;
		ray.Init( CurrentViewOrigin(), pLight->origin );
		g_pEngineTraceClient->TraceRay( ray, MASK_OPAQUE, &traceFilter, &tr );
		if( tr.fraction < 1.0f )
			continue;

		float angleAttenFactor = 0.0f;
		Vector lightToEye;
		lightToEye = CurrentViewOrigin() - pLight->origin;
		VectorNormalize( lightToEye );
		switch( pLight->type )
		{
		case emit_point:
			angleAttenFactor = 1.0f;
			break;
		case emit_spotlight:
			continue;
			break;
		case emit_surface:
			// garymcthack - don't do surface lights
			continue;
			if( DotProduct( lightToEye, pLight->normal ) < 0.0f )
			{
				continue;
			}
			angleAttenFactor = 1.0f;
			break;
		case emit_skylight:
		case emit_skyambient:
			continue;
		default:
			assert( 0 );
			continue;
		}
		DrawLightSprite( pLight, angleAttenFactor );
	}
}


//-----------------------------------------------------------------------------
// Draws debugging information for the lights
//-----------------------------------------------------------------------------
void DrawLightDebuggingInfo( void )
{
	int		i;
	char	buf[256];
	int		lineOffset;

	int nLight = r_drawlights.GetInt();

	if ( r_drawlightinfo.GetBool() )
	{
		for (i = 0; i < host_state.worldbrush->numworldlights; i++)
		{	
			dworldlight_t *pLight = &host_state.worldbrush->worldlights[i];

			lineOffset = 0;
			Q_snprintf( buf, sizeof( buf ), "light:  %d\n", i+1 );
			CDebugOverlay::AddTextOverlay( pLight->origin, lineOffset++, 0, buf );	
			Q_snprintf( buf, sizeof( buf ), "origin: <%d, %d, %d>\n", (int)pLight->origin[0], (int)pLight->origin[1], (int)pLight->origin[2] );
			CDebugOverlay::AddTextOverlay( pLight->origin, lineOffset++, 0, buf );	

			if (!nLight)
			{
				// avoid a double debug draw
				DrawLightSprite( pLight, 1.0f );
			}
		}
	}

	if (!nLight)
		return;

	for (i = 0; i < host_state.worldbrush->numworldlights; i++)
	{
		if ((nLight > 0) && (i != nLight-1))
			continue;

		dworldlight_t *pLight = &host_state.worldbrush->worldlights[i];
		Vector lightToEye;
		float angleAttenFactor = 0.0f;
		switch( pLight->type )
		{
		case emit_point:
			angleAttenFactor = 1.0f;
			DrawPointLight( pLight->origin, ComputeLightRadius( pLight, false ) );
			break;
		case emit_spotlight:
			angleAttenFactor = 1.0f;
			DrawSpotLight( pLight );
			break;
		case emit_surface:
			// garymcthack - don't do surface lights
			continue;
			lightToEye = CurrentViewOrigin() - pLight->origin;
			VectorNormalize( lightToEye );
			if( DotProduct( lightToEye, pLight->normal ) < 0.0f )
			{
				continue;
			}
			angleAttenFactor = 1.0f;
			break;
		case emit_skylight:
		case emit_skyambient:
			continue;
		default:
			assert( 0 );
			continue;
		}
		DrawLightSprite( pLight, angleAttenFactor );
	}

	int	lnum;
	for (lnum=0 ; lnum<MAX_DLIGHTS ; lnum++)
	{
		// If the light's not active, then continue
		if ( (r_dlightactive & (1 << lnum)) == 0 )
			continue;

		DrawPointLight( cl_dlights[lnum].origin, cl_dlights[lnum].GetRadius() );
	}
}