// Provides a helper to setup the tone-mapping post-process
//
o3djs.provide( 'patapi.postprocess_tonemapping' );

o3djs.require( 'patapi' );
o3djs.require( 'patapi.postprocess' );

o3djs.require('o3djs.util');
o3djs.require('o3djs.camera');
o3djs.require('o3djs.math');
o3djs.require('o3djs.rendergraph');
o3djs.require('o3djs.pack');
o3djs.require('o3djs.scene');
o3djs.require('o3djs.effect');
o3djs.require('o3djs.primitives');


////////////////////////////////////////////////////////////////////////////
// Declares a class holding informations on the Reinhard tone-mapping post-process chain
//
//	_O3DClient, the O3D client
//	_O3DPack, the O3D pack into which create the objects
//	_PostProcessChain, the post-process chain where to create the tone-mapper's passes
//	_FinalRenderTargetToToneMap, the final render target that will be tone-mapped and rendered to screen
//	_optMinAdaptedLuminance, the optional minimum adapted luminance
//	_optMaxAdaptedLuminance, the optional maximum adapted luminance
//
patapi.ToneMapper = function( _O3DClient, _O3DPack, _PostProcessChain, _FinalRenderTargetToToneMap, _optMinAdaptedLuminance, _optMaxAdaptedLuminance )
{
	this.m_Client = _O3DClient;	// Save the client for later use
	this.m_Pack = _O3DPack;		// Save the pack for later use
	this.m_PostProcessChain = _PostProcessChain;
	this.m_FinalRenderTarget = _FinalRenderTargetToToneMap;


// 	this.m_ToneMappingParameters =	[	!!_optMaxAdaptedLuminance ? _optMaxAdaptedLuminance : 10000.0,	// Max adapted luminance
// 										!!_optMinAdaptedLuminance ? _optMinAdaptedLuminance : 500.0,	// Min adapted luminance
// 										0.72,															// Middle gray
// 										1.0   															// White luminance
// 									];

	// NOTE: With skydome color multiplier, I had to decrease these values
	this.m_ToneMappingParameters =	[	!!_optMaxAdaptedLuminance ? _optMaxAdaptedLuminance : 100.0,	// Max adapted luminance
										!!_optMinAdaptedLuminance ? _optMinAdaptedLuminance : 5.0,		// Min adapted luminance
										0.72,															// Middle gray
										1.0   															// White luminance
									];

	this.m_AdaptationSpeed = 0.8;


	////////////////////////////////////////////////////////////////////////////
	// Create render targets
	//
	var	Width = this.m_FinalRenderTarget.getWidth();
	var	Height = this.m_FinalRenderTarget.getHeight();

	var	LuminanceDownSamplingLevelsCount = Math.ceil( Math.log( Math.max( Width, Height ) ) / Math.LN2 );

	var	CurrentTargetWidth = 1 << (LuminanceDownSamplingLevelsCount - 1);
	var	CurrentTargetHeight = CurrentTargetWidth * Height / Width;

	try
	{
		// Create all the render targets to downsample from screen size down to 1x1
		this.m_RenderTargets = new Array( LuminanceDownSamplingLevelsCount );
		for ( var DownsamplingLevelIndex=0; DownsamplingLevelIndex < LuminanceDownSamplingLevelsCount; DownsamplingLevelIndex++ )
		{
			this.m_RenderTargets[DownsamplingLevelIndex] = new patapi.RenderTarget( this.m_Pack, "Downsampling Level #" + DownsamplingLevelIndex, CurrentTargetWidth, CurrentTargetHeight, o3djs.base.o3d.Texture.R32F );

			// PerformDownsampling by 4 at every stage
			CurrentTargetWidth = Math.max( CurrentTargetWidth >> 1, 1 );
			CurrentTargetHeight = Math.max( CurrentTargetHeight >> 1, 1 );
		}

		// Allocate two 1x1 render targets that will be swapped for temporal adaptation
		this.m_TemporalRenderTargets = [];
		this.m_TemporalRenderTargets[0] = new patapi.RenderTarget( this.m_Pack, "Temporal 1x1 #0", 1, 1, o3djs.base.o3d.Texture.R32F );
		this.m_TemporalRenderTargets[1] = new patapi.RenderTarget( this.m_Pack, "Temporal 1x1 #1", 1, 1, o3djs.base.o3d.Texture.R32F );

			// Clear to black somehow

		// CAN'T USE CANVAS BECAUSE OF R32F FORMAT !
// 		var	canvas = this.m_Pack.createObject( 'Canvas' );
// 			canvas.setSize( 1, 1 );
// 			canvas.clear( [0,0,0,0] );
// 
// 		canvas.copyToTexture( this.m_TemporalRenderTargets[0].Texture );
// 		canvas.copyToTexture( this.m_TemporalRenderTargets[1].Texture );
// 
// 		this.m_Pack.removeObject( canvas );


		// BUG => Attempting to lock render target !
// 		var Black = [0];
// 		this.m_TemporalRenderTargets[0].Texture.set( 0, Black );
// 		this.m_TemporalRenderTargets[1].Texture.set( 0, Black );


		// So what ? Leave it untouched and pray for a valid value ?
		// Clear with a shader ? Pfff... Too much work !
	}
	catch ( e )
	{
		alert( "Exception while creating render-targets : " + e );
	}


	////////////////////////////////////////////////////////////////////////////
	// Create post-processes passes
	//
	function CreatePostMaterial( _ShaderPath )
	{
		// Load the FX
		var PostEffect = this.m_Pack.createObject( 'Effect' );
		o3djs.effect.loadEffect( PostEffect, _ShaderPath );

		// Create the material using that FX
		var PostMaterial = this.m_Pack.createObject( 'Material' );
			PostMaterial.name = "#PostProcess Material";		// <== Prefix by # so it's not overwritten by the ApplyShader function
			PostMaterial.effect = PostEffect;
	
		PostEffect.createUniformParameters( PostMaterial );

		return PostMaterial;
	}


	this.m_PostProcessPasses = [];

	this.m_TemporalLuminanceSourceTextureSamplerParam = null;
	this.m_AdaptationSpeedParam = null;

	this.m_ToneMappingLuminanceSamplerParam = null;
	this.m_ToneMappingParametersParam = null;


	var	PassIndex = 0;
	try
	{
		var that = this;

		// Build the downsampling passes
		var	CurrentTargetWidth = this.m_FinalRenderTarget.getWidth();
		var	CurrentTargetHeight = this.m_FinalRenderTarget.getHeight();

		var	PreviousTarget = null;
		var	CurrentTarget = this.m_FinalRenderTarget;

		for ( var PassIndex=0; PassIndex < this.m_RenderTargets.length; PassIndex++ )
		{
			// Scroll the render targets
			PreviousTarget = CurrentTarget;
			CurrentTarget = this.m_RenderTargets[PassIndex];

			// Bind the pass with its material
			var PostMaterial = null;
			if ( PassIndex == 0 )
				PostMaterial = CreatePostMaterial.call( this, 'Data/Shaders/PostProcess ToneMapping/PostProcess - DownSampleX2 + LogLuminance.shader' );	// First pass => Log luminance
			else if ( PassIndex == this.m_RenderTargets.length-1 )
				PostMaterial = CreatePostMaterial.call( this, 'Data/Shaders/PostProcess ToneMapping/PostProcess - DownSampleX2 + ExpLuminance.shader' );	// Last pass => Exp luminance
			else
				PostMaterial = CreatePostMaterial.call( this, 'Data/Shaders/PostProcess ToneMapping/PostProcess - DownSampleX2.shader' );					// Normal passes => Simple downsample

			var	Pass = new patapi.PostProcessPass( this.m_Client, this.m_Pack, "DownsampleX2 Pass #" + PassIndex, [PreviousTarget], CurrentTarget, PostMaterial, SetupPostProcessSamplers_Downsample );
			this.m_PostProcessChain.BindPass( Pass );
			this.m_PostProcessPasses.push( Pass );

			// Downsample by 2
			CurrentTargetWidth >>= 1;
			CurrentTargetHeight >>= 1;
			CurrentTargetWidth = Math.max( 1, CurrentTargetWidth );
			CurrentTargetHeight = Math.max( 1, CurrentTargetHeight );
		}

 		// =========== TEMPORAL LUMINANCE ADAPTATION ===========
		PostMaterial = CreatePostMaterial.call( this, 'Data/Shaders/PostProcess ToneMapping/PostProcess - TemporalAdaptation.shader' );
		this.m_TemporalAdaptationPass = new patapi.PostProcessPass( this.m_Client, this.m_Pack, "Temporal Adaptation", [this.m_RenderTargets[this.m_RenderTargets.length-1], this.m_TemporalRenderTargets[0]], this.m_TemporalRenderTargets[1], PostMaterial, SetupPostProcessSamplers_TemporalAdaptation );
		this.m_PostProcessChain.BindPass( this.m_TemporalAdaptationPass );
		this.m_PostProcessPasses.push( this.m_TemporalAdaptationPass );


		// =========== TONE MAPPING ===========
		PostMaterial = CreatePostMaterial.call( this, 'Data/Shaders/PostProcess ToneMapping/PostProcess - ToneMapping.shader' );
		Pass = new patapi.PostProcessPass( this.m_Client, this.m_Pack, "Tone Mapping", [this.m_FinalRenderTarget, this.m_TemporalRenderTargets[1]], null, PostMaterial, SetupPostProcessSamplers_ToneMapping );	// Render directly to screen
		this.m_PostProcessChain.BindPass( Pass );
		this.m_PostProcessPasses.push( Pass );
	}
	catch ( e )
	{
		alert( "Exception while creating post-process pass #" + PassIndex + " : " + e );
	}


	// Callbacks used by the post-process chain to setup the post-process passes' samplers
	//	_Pass, the pass to setup
	//	_InputSamplers, the array of input samplers the pass needs
	//	_InputTexCoords, the array of input tex coords to use to sample each sampler
	//

	function SetupPostProcessSamplers_Downsample( _Pass, _InputSamplers, _InputTexCoords )
	{
		var imageParam = _Pass.getMaterial().getParam( 'image' );
			imageParam.value = _InputSamplers[0];

		var	imageTexCoords = _Pass.getMaterial().getParam( 'imageTexCoords' );
			imageTexCoords.value = [_InputTexCoords[0][2], _InputTexCoords[0][3], _InputSamplers[0].texture.width, 1.0 / _InputSamplers[0].texture.width];
	}

	function SetupPostProcessSamplers_TemporalAdaptation( _Pass, _InputSamplers, _InputTexCoords )
	{
		var CurrentLuminanceParam = _Pass.getMaterial().getParam( 'CurrentLuminanceSourceTextureSampler' );
			CurrentLuminanceParam.value = _InputSamplers[0];

		that.m_TemporalLuminanceSourceTextureSamplerParam = _Pass.getMaterial().getParam( 'LastLuminanceSourceTextureSampler' );
		that.m_TemporalLuminanceSourceTextureSamplerParam.value = _InputSamplers[1];

		that.m_AdaptationSpeedParam = _Pass.getMaterial().getParam( 'AdaptationSpeed' );
		that.m_AdaptationSpeedParam.value = that.m_AdaptationSpeed;
	}

	function SetupPostProcessSamplers_ToneMapping( _Pass, _InputSamplers, _InputTexCoords )
	{
		var SourceTextureSamplerParam = _Pass.getMaterial().getParam( 'SourceTextureSampler' );
			SourceTextureSamplerParam.value = _InputSamplers[0];

		var	SourceImageTexCoordsParam = _Pass.getMaterial().getParam( 'SourceImageTexCoords' );
			SourceImageTexCoordsParam.value = [_InputTexCoords[0][2], _InputTexCoords[0][3], _InputSamplers[0].texture.width, 1.0 / _InputSamplers[0].texture.width];

		that.m_ToneMappingLuminanceSamplerParam = _Pass.getMaterial().getParam( 'LuminanceTextureSampler' );
		that.m_ToneMappingLuminanceSamplerParam.value = _InputSamplers[1];

		that.m_ToneMappingParametersParam = _Pass.getMaterial().getParam( 'Parameters' );
		that.m_ToneMappingParametersParam.value = that.m_ToneMappingParameters;
	}
};

patapi.ToneMapper.prototype = 
{
	// Gets or sets the maximum luminance that can be adapted by the tone-mapper
	//
	getMaxAdaptedLuminance : function()	{ return this.m_ToneMappingParameters[0]; },
	setMaxAdaptedLuminance : function( value )
	{
		this.m_ToneMappingParameters[0] = value;

		// Also update the shader parameter
		this.m_ToneMappingParametersParam.value = this.m_ToneMappingParameters;
	},

	// Gets or sets the middle gray value
	//
	getMiddleGray : function()	{ return this.m_ToneMappingParameters[1]; },
	setMiddleGray : function( value )
	{
		this.m_ToneMappingParameters[1] = value;

		// Also update the shader parameter
		this.m_ToneMappingParametersParam.value = this.m_ToneMappingParameters;
	},

	// Gets or sets the white luminance (i.e. the luminance to which the maximum luminance will map to)
	//
	getWhiteLuminance : function()	{ return this.m_ToneMappingParameters[2]; },
	setWhiteLuminance : function( value )
	{
		this.m_ToneMappingParameters[2] = value;

		// Also update the shader parameter
		this.m_ToneMappingParametersParam.value = this.m_ToneMappingParameters;
	},

	// Gets or sets the adaptation speed
	//
	getAdaptationSpeed : function()	{ return this.m_AdaptationSpeed; },
	setAdaptationSpeed : function( value )
	{
		this.m_AdaptationSpeed = value;

		// Also update the shader parameters
		this.m_AdaptationSpeedParam.value = this.m_AdaptationSpeed;
	},


	// Disposes of used resources
	// Call this when exiting the application
	//
	Dispose : function()
	{
		throw "TODO!"
	},

	// Function to call on every render
	// This is important as it swaps the temporal render targets and makes the tone-mapper operator slowly adapt the luminance
	//
	OnRender : function( _RenderEvent )
	{
		// Tone-mapping temporal textures swap
		var Temp = this.m_TemporalRenderTargets[0];
		this.m_TemporalRenderTargets[0] = this.m_TemporalRenderTargets[1];
		this.m_TemporalRenderTargets[1] = Temp;

 		this.m_TemporalLuminanceSourceTextureSamplerParam.value = this.m_TemporalRenderTargets[0].getSampler();					// Replace the source sampler
 		this.m_TemporalAdaptationPass.getParentRenderSurfaceSet().renderSurface = this.m_TemporalRenderTargets[1].getSurface();	// Replace the target render surface

		// Feed the current adapted luminance to the tone mapping pass
		this.m_ToneMappingLuminanceSamplerParam.value = this.m_TemporalRenderTargets[1].getSampler();
	}

};
