//========= Copyright Valve Corporation, All rights reserved. ============//
#include "cbase.h"

#include "KeyValues.h"
#include "cdll_client_int.h"
#include "view_scene.h"
#include "viewrender.h"
#include "tier0/icommandline.h"
#include "materialsystem/imesh.h"
#include "materialsystem/imaterial.h"
#include "materialsystem/imaterialsystemhardwareconfig.h"
#include "materialsystem/imaterialvar.h"

#include "ScreenSpaceEffects.h"

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


static float g_flDX7NoiseScale = 4.0f; 

//------------------------------------------------------------------------------
// Film grain post-processing effect
//------------------------------------------------------------------------------
struct FilmDustParticle_t
{
	int	  m_nChannel;

	float m_flPositionX;
	float m_flPositionY;

	float m_flWidth;
	float m_flHeight;

	int	  m_nSplotchIndex;
	int	  m_nOrientation;
};

class CFilmGrainEffect : public IScreenSpaceEffect
{
public:													

	CFilmGrainEffect( );
   ~CFilmGrainEffect( );

	void Init( );
	void Shutdown( );

	void SetParameters( KeyValues *params );

	void Render( int x, int y, int w, int h );

	void Enable( bool bEnable );
	bool IsEnabled( );

private:

	void DrawSplotch( float x, float y, float width, float height, float u, float v, float uWidth, float vHeight, int orientation, int alpha=255 );

	bool				m_bEnable;

	CMaterialReference	m_GrainMaterial;
	CMaterialReference	m_DustMaterial;

	Vector4D			m_NoiseScale;

	int					m_nMaxDustParticles;

	float				m_flMinDustSize;
	float				m_flMaxDustSize;

	float				m_flChanceOfDust;

	float				m_flUpdateRate;

	bool				m_bEnableFlicker;
	int					m_nFlickerAlpha;

	bool				m_bSplitScreen;

	int					m_nCachedParticleTime;
	CUtlVector< FilmDustParticle_t > m_CachedParticles;

	float				m_flEffectAlpha;

	IMaterial *			m_pFlickerMaterial; 
};

ADD_SCREENSPACE_EFFECT( CFilmGrainEffect, filmgrain );

//------------------------------------------------------------------------------
// CFilmGrainEffect constructor
//------------------------------------------------------------------------------
CFilmGrainEffect::CFilmGrainEffect( )
{
	m_NoiseScale = Vector4D( 0.14f, 0.14f, 0.14f, 0.78f );

	m_nMaxDustParticles = 3;
	
	m_flMinDustSize = 0.03f;
	m_flMaxDustSize = 0.15f;

	m_flChanceOfDust = 0.75f;

	m_flUpdateRate = 24.0f;
	
	m_nCachedParticleTime = -1;

	m_bSplitScreen = false;

	m_bEnableFlicker = true;
	m_nFlickerAlpha = 90;

	m_flEffectAlpha = 1.0;

	m_pFlickerMaterial = NULL;
}


//------------------------------------------------------------------------------
// CFilmGrainEffect destructor
//------------------------------------------------------------------------------
CFilmGrainEffect::~CFilmGrainEffect( )
{
}


//------------------------------------------------------------------------------
// CFilmGrainEffect init
//------------------------------------------------------------------------------
void CFilmGrainEffect::Init( )
{
	KeyValues *pVMTKeyValues = new KeyValues( "filmgrain" );
	pVMTKeyValues->SetString( "$GRAIN_TEXTURE", "Effects/FilmScan256" );
	pVMTKeyValues->SetString( "$SCALEBIAS", "[0.0 0.0 0.0 0.0]" );
	pVMTKeyValues->SetString( "$NOISESCALE", "[0.0 1.0 0.5 1.0]" );

	m_GrainMaterial.Init( "engine/filmgrain", pVMTKeyValues );
	m_GrainMaterial->Refresh( );

	pVMTKeyValues = new KeyValues( "filmdust" );
	pVMTKeyValues->SetString( "$DUST_TEXTURE", "Effects/Splotches256" );
	pVMTKeyValues->SetString( "$SCALEBIAS", "[0.0 0.0 0.0 0.0]" );
	pVMTKeyValues->SetString( "$CHANNEL_SELECT", "[1.0 0.0 0.0 0.0]" );

	m_DustMaterial.Init( "engine/filmdust", pVMTKeyValues );
	m_DustMaterial->Refresh( );

	m_pFlickerMaterial = materials->FindMaterial( "effects/flicker_128", TEXTURE_GROUP_OTHER, false );
	if ( m_pFlickerMaterial )
	{
		m_pFlickerMaterial->AddRef();
	}
}


//------------------------------------------------------------------------------
// CFilmGrainEffect shutdown
//------------------------------------------------------------------------------
void CFilmGrainEffect::Shutdown( )
{
	m_DustMaterial.Shutdown();
	m_GrainMaterial.Shutdown();
	
	if ( m_pFlickerMaterial )
	{
		m_pFlickerMaterial->Release();
	}
}

 
//------------------------------------------------------------------------------
// CFilmGrainEffect enable
//------------------------------------------------------------------------------
void CFilmGrainEffect::Enable( bool bEnable )
{
	m_bEnable = bEnable;
}

bool CFilmGrainEffect::IsEnabled( )
{
	return m_bEnable;
}

//------------------------------------------------------------------------------
// CFilmGrainEffect SetParameters
//------------------------------------------------------------------------------
void CFilmGrainEffect::SetParameters( KeyValues *params )
{
	if( params->FindKey( "split_screen" ) )
	{
		int ival = params->GetInt( "split_screen" );
		m_bSplitScreen = ival?true:false;

		extern void EnableHUDFilmDemo( bool bEnable, const char *left_string_id, const char *right_string_id );
		EnableHUDFilmDemo( m_bSplitScreen, "#Valve_FilmDemo_FilmGrain_LeftTitle", "#Valve_FilmDemo_FilmGrain_RightTitle" );
	}

	if( params->FindKey( "noise_scale" ) )
	{
		Color noise_color = params->GetColor( "noise_scale" );
		int r, g, b, a;
		noise_color.GetColor( r, g, b, a );
		m_NoiseScale.x = r/255.0f;;
		m_NoiseScale.y = g/255.0f;;
		m_NoiseScale.z = b/255.0f;;
		m_NoiseScale.w = a/255.0f;;
	}

	if( params->FindKey( "max_dust_particles" ) )
	{
		m_nMaxDustParticles = params->GetInt( "max_dust_particles" );
	}

	if( params->FindKey( "min_dust_size" ) )
	{
		m_flMinDustSize = params->GetFloat( "min_dust_size" );
	}

	if( params->FindKey( "max_dust_size" ) )
	{
		m_flMaxDustSize = params->GetFloat( "max_dust_size" );
	}

	if( params->FindKey( "dust_prob" ) )
	{
		m_flChanceOfDust = params->GetFloat( "dust_prob" );
	}

	if( params->FindKey( "update_rate" ) )
	{
		m_flUpdateRate = params->GetFloat( "update_rate" );
	}

	if( params->FindKey( "enable_flicker" ) )
	{
		m_bEnableFlicker = params->GetInt( "enable_flicker" );
	}

	if( params->FindKey( "flicker_alpha" ) )
	{
		m_nFlickerAlpha = params->GetInt( "flicker_alpha" );
	}

	if( params->FindKey( "effect_alpha" ) )
	{
		m_flEffectAlpha = params->GetFloat( "effect_alpha" );
	}
}


//-----------------------------------------------------------------------------
// Draws a quad to resolve accumulation buffer samples as needed
//-----------------------------------------------------------------------------
void CFilmGrainEffect::DrawSplotch( float x, float y, float flWidth, float flHeight, float u, float v, float uWidth, float vWidth, int orientation, int alpha )
{
	float flAlpha = ( alpha / 255.0f ) * m_flEffectAlpha;

	float tempU[4] = { u, u, u+uWidth, u+uWidth };
	float tempV[4] = { v, v+vWidth, v+vWidth, v };

	float _u[4], _v[4];
	for( int i=0;i<4;i++ )
	{
		_u[ (i + orientation)%4 ] = tempU[ i ];
		_v[ (i + orientation)%4 ] = tempV[ i ];
	}

	CMatRenderContextPtr pRenderContext( materials );
	IMesh *pMesh = pRenderContext->GetDynamicMesh();
	CMeshBuilder meshBuilder;

	pRenderContext->MatrixMode( MATERIAL_VIEW );
	pRenderContext->PushMatrix();
	pRenderContext->LoadIdentity();

	pRenderContext->MatrixMode( MATERIAL_PROJECTION );
	pRenderContext->PushMatrix();
	pRenderContext->LoadIdentity();

	meshBuilder.Begin( pMesh, MATERIAL_QUADS, 1 );

	meshBuilder.Position3f( x, y, 0.0f );	// Upper left
	meshBuilder.TexCoord2f( 0, _u[0], _v[0] );
	meshBuilder.Color4f( 1.0f, 1.0f, 1.0f, flAlpha );
	meshBuilder.AdvanceVertex();

	meshBuilder.Position3f( x, y+flHeight, 0.0f );	// Lower left
	meshBuilder.TexCoord2f( 0, _u[1], _v[1] );
	meshBuilder.Color4f( 1.0f, 1.0f, 1.0f, flAlpha );
	meshBuilder.AdvanceVertex();

	meshBuilder.Position3f( x+flWidth, y+flHeight, 0.0 );		// Lower right
	meshBuilder.TexCoord2f( 0, _u[2], _v[2] );
	meshBuilder.Color4f( 1.0f, 1.0f, 1.0f, flAlpha );
	meshBuilder.AdvanceVertex();

	meshBuilder.Position3f( x+flWidth, y, 0.0 );		// Upper right
	meshBuilder.TexCoord2f( 0, _u[3], _v[3] );
	meshBuilder.Color4f( 1.0f, 1.0f, 1.0f, flAlpha );
	meshBuilder.AdvanceVertex();

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

	pRenderContext->MatrixMode( MATERIAL_VIEW );
	pRenderContext->PopMatrix();

	pRenderContext->MatrixMode( MATERIAL_PROJECTION );
	pRenderContext->PopMatrix();
}


//------------------------------------------------------------------------------
// CFilmGrainEffect render
//------------------------------------------------------------------------------
void CFilmGrainEffect::Render( int x, int y, int w, int h )
{
	if( !m_bEnable )
		return;

	int nTime = (int)(gpGlobals->curtime * m_flUpdateRate);

	// Set up random offsets for grain texture
	float flBiasU  = RandomFloat();
	float flBiasV  = RandomFloat();
	float flScaleU = w / 256.0f;
	float flScaleV = h / 256.0f;

	flBiasU *= w;
	flBiasV *= h;

	int paramCount = m_GrainMaterial->ShaderParamCount();
	IMaterialVar **pParams = m_GrainMaterial->GetShaderParams();
	for( int i=0;i<paramCount;i++ )
	{
		IMaterialVar *pVar = pParams[i];

		// alpha operates from 1.0 -> m_NoiseScale.w
		float alpha = 1.0 - ( 1.0 - m_NoiseScale.w ) * m_flEffectAlpha;
		
		if( !Q_stricmp( pVar->GetName(), "$noisescale" ) )
		{
			if( g_pMaterialSystemHardwareConfig->GetDXSupportLevel()<=70 )
			{
			 	pVar->SetVecValue( m_NoiseScale.x*g_flDX7NoiseScale * m_flEffectAlpha,
									m_NoiseScale.y*g_flDX7NoiseScale * m_flEffectAlpha,
									m_NoiseScale.z*g_flDX7NoiseScale * m_flEffectAlpha,
									alpha );
			}
			else
			{
			 	pVar->SetVecValue( m_NoiseScale.x * m_flEffectAlpha,
									m_NoiseScale.y * m_flEffectAlpha,
									m_NoiseScale.z * m_flEffectAlpha,
									alpha );
			}
		}
	}

	// Render Effect
	CMatRenderContextPtr pRenderContext( materials );

	pRenderContext->Bind( m_GrainMaterial );
	if( m_bSplitScreen )
	{
		DrawSplotch(  0.0f, -1.0f, 2.0f, 2.0f, flBiasU, flBiasV, flScaleU, flScaleV, 0 );
	}
	else
	{
		DrawSplotch( -1.0f, -1.0f, 2.0f, 2.0f, flBiasU, flBiasV, flScaleU, flScaleV, 0 );
	}

	int screenWidth, screenHeight;
 	pRenderContext->GetRenderTargetDimensions( screenWidth, screenHeight );

	// Now let's do the flicker
	if( m_bEnableFlicker )
	{
		if( !IsErrorMaterial( m_pFlickerMaterial ) )
		{
			m_pFlickerMaterial->Refresh();
			m_pFlickerMaterial->SetMaterialVarFlag( MATERIAL_VAR_VERTEXALPHA, true );
			pRenderContext->Bind( m_pFlickerMaterial );

			int nFlickerAlpha = (int)( m_nFlickerAlpha * m_flEffectAlpha );

			if( !m_bSplitScreen )
			{
				DrawSplotch( -1.0f, -1.0f, 2.0f, 2.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0, nFlickerAlpha );
			}
			else
			{
				DrawSplotch(  0.0f, -1.0f, 2.0f, 2.0f, 0.5f, 1.0f, 0.5f, -1.0f, 0, nFlickerAlpha );
			}
		}
	}

	// Now for some dust particles
	if( nTime!=m_nCachedParticleTime )
	{
		// Need to regenerate our dust particles
		m_CachedParticles.RemoveAll();
		m_nCachedParticleTime = nTime;

		float flDustProb = RandomFloat( 0.0f, 1.0f );
		if( flDustProb < m_flChanceOfDust )
		{
			int numDustParticles = RandomInt( 0, m_nMaxDustParticles );
			for( int i=0;i<numDustParticles;i++ )
			{
				FilmDustParticle_t particle;

				particle.m_nChannel = RandomInt( 0, 2 );				

				particle.m_flPositionX = RandomFloat( -1.0f, 1.0f );
				particle.m_flPositionY = RandomFloat( -1.0f, 1.0f );

				particle.m_nOrientation = RandomInt( 0, 3 );

				particle.m_nSplotchIndex = RandomInt( 0, 15 );
				clamp( particle.m_nSplotchIndex, 0, 15 );
                                      
				particle.m_flHeight = RandomFloat( m_flMinDustSize, m_flMaxDustSize );
				particle.m_flWidth = particle.m_flHeight * (float)screenHeight / (float)screenWidth;

				if( (!m_bSplitScreen) || (particle.m_flPositionX-particle.m_flWidth > 0.0f) )
				{
					m_CachedParticles.AddToTail( particle );
				}
			}
		}
	}

	for( int i=0;i<m_CachedParticles.Count();i++ )
	{
		FilmDustParticle_t *particle = &m_CachedParticles[i];

		float channelSelect[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
 		channelSelect[ particle->m_nChannel ] = 1.0f;

		paramCount = m_DustMaterial->ShaderParamCount();
		pParams = m_DustMaterial->GetShaderParams();
		for( int i=0;i<paramCount;i++ )
		{
			IMaterialVar *pVar = pParams[i];
			
			if( !Q_stricmp( pVar->GetName(), "$channel_select" ) )
			{
				pVar->SetVecValue( channelSelect[0], channelSelect[1], channelSelect[2], channelSelect[3] );
			}
		}

        float flUOffset = (particle->m_nSplotchIndex % 4) / 4.0f;
		float flVOffset = (particle->m_nSplotchIndex / 4) / 4.0f;

		pRenderContext->Bind( m_DustMaterial );

		DrawSplotch( particle->m_flPositionX, particle->m_flPositionY, particle->m_flWidth * m_flEffectAlpha, particle->m_flHeight * m_flEffectAlpha,
						flUOffset, flVOffset, 0.25f, 0.25f, particle->m_nOrientation );
	}
}


//------------------------------------------------------------------------------
// Console Interface
//------------------------------------------------------------------------------
static void EnableFilmGrain( IConVar *pConVar, char const *pOldString, float flOldValue )
{
	ConVarRef var( pConVar );
	if( var.GetBool() )
	{
		g_pScreenSpaceEffects->EnableScreenSpaceEffect( "filmgrain" );
	}
	else
	{
		g_pScreenSpaceEffects->DisableScreenSpaceEffect( "filmgrain" );
	}
}

static ConVar mat_filmgrain( "mat_filmgrain", "0", FCVAR_CLIENTDLL, "Enable/disable film grain post effect", EnableFilmGrain );