/// This file hosts an adaptation of the article "A Practical Analytic Model for Daylight" by Preetham et al.
/// It is capable of returning the color of a natural sky for a set of directions expressed in spherical coordinates.
/// It's also capable of computing the associated extinction & in-scattering tables (without the turbidity) that
///  can be used for computation of aerial perspective.
/// 
/// Phi is the azimuth and Theta is the elevation where 0 is the dome's zenith.
/// 
/// The convention for transformation between spherical and cartesian coordinates should be chosen to be the same
///	as for Spherical Harmonics (cf. SphericalHarmonics.SHFunctions) :
///
///		_ Azimuth Phi is zero on +Z and increases CW (i.e. PI/2 at -X, PI at -Z and 3PI/2 at +X)
///		_ Elevation Theta is zero on +Y and PI on -Y
/// 
/// 
///                     Y Theta=0
///                     |
///                     |
///                     |
///  Phi=PI/2 -X........o------+X Phi=3PI/2
///                    /
///                   / 
///                 +Z Phi=0
/// 
/// 
/// Here is the sample conversion from spherical to cartesian coordinates :
///		X = Sin( Theta ) * Sin( -Phi )
///		Y = Cos( Theta )
///		Z = Sin( Theta ) * Cos( -Phi )
/// 
/// So cartesian to polar coordinates is computed this way:
///		Theta = acos( Y );
///		Phi = -atan2( X, Z );
/// 
///
o3djs.provide( 'patapi.skydome' );

o3djs.require( 'patapi' );
o3djs.require( 'patapi.skydome_SHCoefficients' )

o3djs.require( 'patapi.effectsmanager' );

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');

var Math3D = o3djs.math;	// O3D Math


////////////////////////////////////////////////////////////////////////////
// Declares a class holding informations on the skydome
// The skydome parameters are SunDirection and Turbidity
//
//	_O3DClient, the O3D client
//	_O3DPack, the O3D pack into which create the objects
//	_DomeSubdivisionsPhi, the amount of subdivisions on Phi
//	_DomeSubdivisionsTheta, the amount of subdivisions on Theta
//	_optSunDirection, the optional vector pointing toward the Sun
//	_optTurbidity, the optional atmospheric turbidity (amount of pollution in the air)
//	_optUpdateCallback, the optional callback that will be called on every update (prototype is "Callback( _SkyDome )")
//
patapi.SkyDome = function( _O3DClient, _O3DPack, _DomeSubdivisionsPhi, _DomeSubdivisionsTheta, _optSunDirection, _optTurbidity, _optUpdateCallback )
{
	this.m_Client = _O3DClient;	// Save the client for later use
	this.m_Pack = _O3DPack;		// Save the pack for later use

	this.m_SunDirection = _optSunDirection ? Math3D.normalize( _optSunDirection ) : Math3D.normalize( [1,1,1] );
	this.m_Turbidity = _optTurbidity ? _optTurbidity : 4.0;
	this.m_GlobalLuminanceFactor = 1.0;
	this.m_bUpdating = false;

	this.m_UpdateCallback = _optUpdateCallback ? _optUpdateCallback : null;

	this.m_ShaderDataProviderDirectional = null;
	this.m_ShaderDataProviderAmbient = null;
	this.m_ShaderDataProviderSH = null;


	////////////////////////////////////////////////////////////////////////////
	// Create the dome material using the skydome shader and bind parameters
	//
	var Effect = this.m_Pack.createObject( 'Effect' );
	o3djs.effect.loadEffect( Effect, 'Data/Shaders/SkyDome/SkyDome.shader' );
	
	// Create the material using that FX
	this.m_Material = this.m_Pack.createObject( 'Material' );
	this.m_Material.name = "#SkyDome Material";		// <== Prefix by # so it's not overwritten by the ApplyShader function
	this.m_Material.effect = Effect;

	Effect.createUniformParameters( this.m_Material );

	this.m_SunDirectionParam = this.m_Material.getParam( 'SunDirection' );
	this.m_Coeffs_x0Param = this.m_Material.getParam( 'Coeffs_x0' );
	this.m_Coeffs_x1Param = this.m_Material.getParam( 'Coeffs_x1' );
	this.m_Coeffs_y0Param = this.m_Material.getParam( 'Coeffs_y0' );
	this.m_Coeffs_y1Param = this.m_Material.getParam( 'Coeffs_y1' );
	this.m_Coeffs_Y0Param = this.m_Material.getParam( 'Coeffs_Y0' );
	this.m_Coeffs_Y1Param = this.m_Material.getParam( 'Coeffs_Y1' );
	this.m_ZenithCoefficientsParam = this.m_Material.getParam( 'ZenithCoefficients' );


	////////////////////////////////////////////////////////////////////////////
	// Create the dome geometry
	//
	var vertexInfo = o3djs.primitives.createVertexInfo();
	var PositionStream = vertexInfo.addStream( 3, o3djs.base.o3d.Stream.POSITION );
	var DirectionStream = vertexInfo.addStream( 4, o3djs.base.o3d.Stream.TEXCOORD, 0 );		// The TEXCOORD stream has 4 coordinates => 3 for the direction and 1 storing 1/cos(theta)


//	var	SkydomeRadius = 1.0;		// Ideally, the skydome should have a unit radius...
	var	SkydomeRadius = 1000.0;		// But since I can't find how to prevent it from being culled when moving away, I simply make it huge


	// First singular vertex at the pole
	PositionStream.addElementVector( [0.0, SkydomeRadius, 0.0] );
	DirectionStream.addElementVector( [0.0, 1.0, 0.0, 1.0] );

	// Add regular vertices
	for ( var ThetaIndex=0; ThetaIndex < _DomeSubdivisionsTheta; ThetaIndex++ )
	{
		var	Theta = Math.asin( Math.sqrt( (1.0 + ThetaIndex) / _DomeSubdivisionsTheta ) );

		for ( var PhiIndex=0; PhiIndex < _DomeSubdivisionsPhi; PhiIndex++ )
		{
			var	Phi = 2.0 * Math.PI * PhiIndex / _DomeSubdivisionsPhi;

			var	Position = [Math.sin( Theta ) * Math.sin( -Phi ),
							Math.cos( Theta ),
							Math.sin( Theta ) * Math.cos( -Phi )];

			PositionStream.addElementVector( Math3D.mulScalarVector( SkydomeRadius, Position ) );
			DirectionStream.addElementVector( Position.concat( 1.0 / Math.max( 1e-5, Position[1] ) ) );
		}
	}

	// Last singular vertex a little below the center
	PositionStream.addElementVector( [0.0, -SkydomeRadius, 0.0] );
	DirectionStream.addElementVector( [0.0, -1.0, 0.0, 1e5] );

	var	VerticesCount = 1 + _DomeSubdivisionsPhi*_DomeSubdivisionsTheta + 1;

	// ===== Build the indices =====

	// Start with the first band linking the north pole
	for ( var PhiIndex=0; PhiIndex < _DomeSubdivisionsPhi; PhiIndex++ )
	{
		vertexInfo.addTriangle(	0,	// Always the singularity
								1 + PhiIndex,
								1 + ((PhiIndex+1) % _DomeSubdivisionsPhi)
							  );
	}

	// Regular bands
	for ( var PhiIndex=0; PhiIndex < _DomeSubdivisionsPhi; PhiIndex++ )
		for ( var ThetaIndex=0; ThetaIndex < _DomeSubdivisionsTheta-1; ThetaIndex++ )
		{
			vertexInfo.addTriangle( 1 + _DomeSubdivisionsPhi*ThetaIndex + PhiIndex,
									1 + _DomeSubdivisionsPhi*(ThetaIndex+1) + PhiIndex,
									1 + _DomeSubdivisionsPhi*(ThetaIndex+1) + ((PhiIndex+1) % _DomeSubdivisionsPhi)
								  );

			vertexInfo.addTriangle(	1 + _DomeSubdivisionsPhi*ThetaIndex + PhiIndex,
									1 + _DomeSubdivisionsPhi*(ThetaIndex+1) + ((PhiIndex+1) % _DomeSubdivisionsPhi),
									1 + _DomeSubdivisionsPhi*ThetaIndex + ((PhiIndex+1) % _DomeSubdivisionsPhi)
								  );
		}

	// Last band of triangles closing the dome
	for ( var PhiIndex=0; PhiIndex < _DomeSubdivisionsPhi; PhiIndex++ )
	{
		vertexInfo.addTriangle(	1 + _DomeSubdivisionsPhi*(_DomeSubdivisionsTheta-1) + PhiIndex,
								VerticesCount-1,	// Singularity...
								1 + _DomeSubdivisionsPhi*(_DomeSubdivisionsTheta-1) + ((PhiIndex+1) % _DomeSubdivisionsPhi)
							  );
	}

	// Build the skydome shape
	try
	{
		this.m_SkyDomeShape = vertexInfo.createShape( this.m_Pack, this.m_Material );
		this.m_SkyDomeShape.name = "SkyDome Shape";

// 		// Clear the culling flag for each element
// 		var	Elements = this.m_SkyDomeShape.elements;
// 		for ( var DrawElementIndex=0; DrawElementIndex < Elements.length; DrawElementIndex++ )
// 		{
// 			var	Element = Elements[DrawElementIndex];
// 				Element.cull = false;
// 				Element.boundingBox = BoundingBox;
// 
// // alert( "Element #" + DrawElementIndex + "\n\n" + patapi.helpers.EnumerateProperties( Element ) )
// 		}
	}
	catch ( e )
	{
		throw "Exception while creating skydome shape : " + e
	};

	// Build the transform
	this.m_SkyDomeTransform = this.m_Pack.createObject( 'Transform' );		// Create a Root Transform for the skydome scene
	this.m_SkyDomeTransform.addShape( this.m_SkyDomeShape );				// Attach the shape to the scene.
//	this.m_SkyDomeTransform.cull = false;
//	var	BoundingBox = g_o3d.BoundingBox( [-10000, -10000, -10000], [10000, 10000, 10000] );
//	this.m_SkyDomeTransform.boundingBox = BoundingBox;

//alert( patapi.helpers.EnumerateProperties( this.m_SkyDomeTransform ) )


	////////////////////////////////////////////////////////////////////////////
	// Create the render nodes
	//

	// Create our single state-set
	// The state-set will be the root of every drawpasses, one drawpass per post-process
	var State = this.m_Pack.createObject( 'State' );
		State.getStateParam( 'o3d.ZComparisonFunction' ).value = o3djs.base.o3d.State.CMP_ALWAYS;	// Always authorize writing (so the shader can be used to initialize the ZBuffer)

// alert( State.getStateParam( 'o3d.ZComparisonFunction' ).value + "\n" +
// 		"o3djs.base.o3d.State.CMP_ALWAYS = " + o3djs.base.o3d.State.CMP_ALWAYS + "\n" +
// 		"o3djs.base.o3d.State.CMP_NEVER = " + o3djs.base.o3d.State.CMP_NEVER + "\n" +
// 		"o3djs.base.o3d.State.CMP_LESS = " + o3djs.base.o3d.State.CMP_LESS + "\n" +
// 		"o3djs.base.o3d.State.CMP_EQUAL = " + o3djs.base.o3d.State.CMP_EQUAL + "\n" +
// 		"o3djs.base.o3d.State.CMP_LEQUAL = " + o3djs.base.o3d.State.CMP_LEQUAL + "\n"
// 		 );


	this.m_StateSet = this.m_Pack.createObject( 'StateSet' );
	this.m_StateSet.name = "SkyDome StateSet";
	this.m_StateSet.state = State;

	// Create our single draw-list
	this.m_DrawList = this.m_Pack.createObject( 'DrawList' );
	this.m_DrawList.name = "SkyDome DrawList";

	// Create a tree-traversal render node bound to our single object
	this.m_TreeTraversal = this.m_Pack.createObject( 'TreeTraversal' );
	this.m_TreeTraversal.name = "SkyDome TreeTraversal";
	this.m_TreeTraversal.transform = this.m_SkyDomeTransform;								// Tie to the transform as it's the only object we're going to render
	this.m_TreeTraversal.parent = this.m_StateSet;
	this.m_TreeTraversal.priority = 0;

	// Create a single drawpass for that post-process
	this.m_DrawPass = this.m_Pack.createObject( 'DrawPass' );
	this.m_DrawPass.name = "SkyDome DrawPass";
	this.m_DrawPass.drawList = this.m_DrawList;
	this.m_DrawPass.parent = this.m_StateSet;
	this.m_DrawPass.priority = 1;

	// Assign the material's draw list
	this.m_Material.drawList = this.m_DrawList;


	////////////////////////////////////////////////////////////////////////////
	// Perform a single dummy update to initialize parameters
	//
	this.Update();
};

patapi.SkyDome.prototype = 
{
	// Call this before you start modifying Sun's direction, turbidity or global luminance so the "Update()" function is innefective until calling EndUpdate()
	BeginUpdate : function()				{ this.m_bUpdating = true; },
	// Call this after you finish modifying Sun's direction, turbidity or global luminance so the "Update()" function is called
	EndUpdate : function()					{ this.m_bUpdating = false; this.Update(); },
	
	// Gets or sets the sun direction
	getSunDirection : function()			{ return this.m_SunDirection; },
	setSunDirection : function( value )
	{
		if ( value == null )
			value = [1,1,1];	// Prevent stupid assignment

		this.m_SunDirection = Math3D.normalize( value );

		// Update the shader constants
		this.Update();
	},

	// Gets or sets the atmospheric turbidity
	getTurbidity : function()				{ return this.m_Turbidity; },
	setTurbidity : function( value )
	{
		this.m_Turbidity = Math.max( 2.0, value );

		// Update the shader constants
		this.Update();
	},

	// Gets or sets the global luminance factor that will be applied to Sun & Sky & SH coefficients
	getGlobalLuminanceFactor : function()			{ return this.m_GlobalLuminanceFactor; },
	setGlobalLuminanceFactor : function( value )
	{
		this.m_GlobalLuminanceFactor = value;

		// Update the shader constants
		this.Update();
	},

	// Gets the Sun color from the last Update
	getSunColor : function()				{ return this.m_SunColor; },

	// Gets the ambient Sky color from the last Update
	getSkyColor : function()				{ return this.m_SkyColor; },

	// Gets the sky's SH coefficients (first 3 bands => an array of 9 RGB vectors)
	getSkySHCoefficients : function()		{ return this.m_SkySHCoefficients; },

	// Gets or sets the callback that will be called on every update
	getUpdateCallback : function()			{ return this.m_UpdateCallback; },
	setUpdateCallback : function( value )	{ this.m_UpdateCallback = value; },

	// Disposes of used resources
	// Call this when exiting the application
	//
	Dispose : function()
	{
		throw "TODO!"
	},

	// Binds the skydome to the scene
	//	_SceneViewInfos, the standard ViewInfo structure created by a call to o3djs.rendergraph.createView()
	//
	BindToScene : function(	_SceneViewInfos )
	{
		if ( _SceneViewInfos == null || typeof(_SceneViewInfos) == typeof(undefined) )
			throw "Invalid SceneViewInfo !";

		this.m_SceneViewInfos = _SceneViewInfos;

		// We're assuming the created render graph is something like:
		//
		//        [Viewport]
		//            |
		//     +------+--------+------------------+---------------------+
		//     |               |                  |                     |
		// [ClearBuffer] [TreeTraversal] [Performance StateSet] [ZOrdered StateSet]
		//                                        |                     |
		//                               [Performance DrawPass] [ZOrdered DrawPass]
		//
		//
		// Modify it a bit so it doesn't call the "ClearBuffer" render node but renders our skydome instead
		//

		this.m_StateSet.parent = this.m_SceneViewInfos.root;
		this.m_StateSet.priority = this.m_SceneViewInfos.clearBuffer.priority;
		this.m_TreeTraversal.registerDrawList( this.m_DrawList, this.m_SceneViewInfos.drawContext, true );	// Tie to our only draw list

		// Detach the clear buffer node
		this.m_SceneViewInfos.clearBuffer.parent = null;

		// We should now have something more like:
		//
		//                   [Viewport]
		//                       |
		//                +------+--------+------------------+---------------------+
		//                |               |                  |                     |
		//          [SkyStateSet]   [TreeTraversal] [Performance StateSet] [ZOrdered StateSet]
		//                |                                  |                     |
		//        +-------+-------+               [Performance DrawPass]   [ZOrdered DrawPass]
		//        |               |
		// [TreeTraversal]   [DrawPass]
	},

	// Binds the skydome to the effects manager by adding some shader interface providers
	//	_Manager, the manager to bind the skydome to
	//
	// The skydome can provide data for 3 recognized shader interfaces:
	//	==> Directional Light, we provide light world direction and color
	//	==> Ambient Light, we provide ambient sky color
	//	==> SkyDome SH, we provide the 9 SH Coefficients encoding the skydome's luminance
	//
	BindToEffectsManager : function( _Manager )
	{
		var	that = this;

		this.m_ShaderDataProviderDirectional = _Manager.CreateInterfaceDataProvider( "Skydome Directional Light", "Directional Light", patapi.EFFECTS_MANAGER_PROVIDER_TYPE.INIT, function( _Object )
		{
			_Object.getParam( 'LightWorldDirection' ).value = that.m_SunDirection;
			_Object.getParam( 'LightColor' ).value = that.getSunColor().concat( 1 );
		} );

		this.m_ShaderDataProviderAmbient = _Manager.CreateInterfaceDataProvider( "Skydome Ambient Light", "Ambient Light", patapi.EFFECTS_MANAGER_PROVIDER_TYPE.INIT, function( _Object )
		{
			_Object.getParam( 'AmbientColor' ).value = that.getSkyColor().concat( 1 );
		} );

		this.m_ShaderDataProviderSH = _Manager.CreateInterfaceDataProvider( "Skydome SH Coefficients", "SkyDome SH", patapi.EFFECTS_MANAGER_PROVIDER_TYPE.INIT, function( _Object )
		{
			for ( var CoeffIndex=0; CoeffIndex < 3*3; CoeffIndex++ )
				_Object.getParam( 'SkyAmbientSH' + CoeffIndex ).value = that.m_SkySHCoefficients[CoeffIndex];
		} );
	},

	// Updates the skydome parameters given the current sun direction and ambient turbidity (avoid values below 2!)
	//
	Update : function()
	{
		if ( this.m_bUpdating )
			return;	// Can't update right now...
	
		var	BELOW_HORIZON_ANGLE = 1.7308109;

		var SunDirection = this.m_SunDirection;
			SunDirection[1] = Math.max( Math.cos( BELOW_HORIZON_ANGLE ), SunDirection[1] );	// Here, we clamp the Sun's elevation so we can't reach "underground" sky values which are buggy (out of sampling range)

		var	fSunTheta = Math.acos( SunDirection[1] );


		////////////////////////////////////////////////////////////////////////////
		// Compute the Perez functions' coefficients
		var	Coefficients_x = [	-0.0193 * this.m_Turbidity - 0.2592,
								-0.0665 * this.m_Turbidity + 0.0008,
								-0.0004 * this.m_Turbidity + 0.2125,
								-0.0641 * this.m_Turbidity - 0.8989,
								-0.0033 * this.m_Turbidity + 0.0452 ];
		var	Coefficients_y = [	-0.0167 * this.m_Turbidity - 0.2608,
								-0.0950 * this.m_Turbidity + 0.0092,
								-0.0079 * this.m_Turbidity + 0.2102,
								-0.0441 * this.m_Turbidity - 1.6537,
								-0.0109 * this.m_Turbidity + 0.0529 ];
		var	Coefficients_Y = [	+0.1787 * this.m_Turbidity - 1.4630,
								-0.3554 * this.m_Turbidity + 0.4275,
								-0.0227 * this.m_Turbidity + 5.3251,
								+0.1206 * this.m_Turbidity - 2.5771,
								-0.0670 * this.m_Turbidity + 0.3703 ];

		var	SKY_ZENITH_x = [[ +0.00165, -0.00374,  0.00208,  0.00000 ],
							[ -0.02902,  0.06377, -0.03202,  0.00394 ],
							[ +0.11693, -0.21196,  0.06052,  0.25885 ]];

		var	SKY_ZENITH_y = [[ +0.00275, -0.00610,  0.00316,  0.00000 ],
							[ -0.04214,  0.08970, -0.04153,  0.00515 ],
							[ +0.15346, -0.26756,  0.06669,  0.26688 ]];

		////////////////////////////////////////////////////////////////////////////
		// Compute the sky's zenith values
		var	Chi = (4.0 / 9.0 - this.m_Turbidity / 120.0) * (Math.PI - 2.0 * fSunTheta);
		var	ZenithConstants = [						( (SKY_ZENITH_x[2][3] + fSunTheta * (SKY_ZENITH_x[2][2] + fSunTheta * (SKY_ZENITH_x[2][1] + fSunTheta * SKY_ZENITH_x[2][0]))) +
								this.m_Turbidity *	( (SKY_ZENITH_x[1][3] + fSunTheta * (SKY_ZENITH_x[1][2] + fSunTheta * (SKY_ZENITH_x[1][1] + fSunTheta * SKY_ZENITH_x[1][0]))) +
								this.m_Turbidity *	  (SKY_ZENITH_x[0][3] + fSunTheta * (SKY_ZENITH_x[0][2] + fSunTheta * (SKY_ZENITH_x[0][1] + fSunTheta * SKY_ZENITH_x[0][0]))) ) ),

													( (SKY_ZENITH_y[2][3] + fSunTheta * (SKY_ZENITH_y[2][2] + fSunTheta * (SKY_ZENITH_y[2][1] + fSunTheta * SKY_ZENITH_y[2][0]))) +
								this.m_Turbidity *	( (SKY_ZENITH_y[1][3] + fSunTheta * (SKY_ZENITH_y[1][2] + fSunTheta * (SKY_ZENITH_y[1][1] + fSunTheta * SKY_ZENITH_y[1][0]))) +
								this.m_Turbidity *	  (SKY_ZENITH_y[0][3] + fSunTheta * (SKY_ZENITH_y[0][2] + fSunTheta * (SKY_ZENITH_y[0][1] + fSunTheta * SKY_ZENITH_y[0][0]))) ) ),

								1000.0 * ((4.0453 * this.m_Turbidity - 4.9710) * Math.tan( Chi ) - 0.2155 * this.m_Turbidity + 2.4192)	// in cd/mē
							];

		////////////////////////////////////////////////////////////////////////////
		// Compute zenith Perez values
		var	Zenith = [	ZenithConstants[0] / this.Perez_( Coefficients_x, 0.0, fSunTheta ),
						ZenithConstants[1] / this.Perez_( Coefficients_y, 0.0, fSunTheta ),
						ZenithConstants[2] / this.Perez_( Coefficients_Y, 0.0, fSunTheta )
					 ];


		// TEST
		Zenith[2] *= 0.01 * this.m_GlobalLuminanceFactor;
		// TEST


		////////////////////////////////////////////////////////////////////////////
		// Update the shader parameters
		if ( this.m_SunDirectionParam )
			this.m_SunDirectionParam.value = SunDirection;
		if ( this.m_SunDirectionParam )
		{
			this.m_Coeffs_x0Param.value = Coefficients_x.slice( 0, 4 );
			this.m_Coeffs_x1Param.value = Coefficients_x[4];
			this.m_Coeffs_y0Param.value = Coefficients_y.slice( 0, 4 );
			this.m_Coeffs_y1Param.value = Coefficients_y[4];
			this.m_Coeffs_Y0Param.value = Coefficients_Y.slice( 0, 4 );
			this.m_Coeffs_Y1Param.value = Coefficients_Y[4];
		}
		if ( this.m_ZenithCoefficientsParam )
			this.m_ZenithCoefficientsParam.value = Zenith;


		////////////////////////////////////////////////////////////////////////////
		// Compute Sun & Sky colors for scene lighting
		function	ComputeColor( _Direction )
		{
			var	fTheta = Math.acos( _Direction[1] );
			var	fGamma = Math.acos( Math.min( 1.0, Math3D.dot( _Direction, SunDirection ) ) );

			var	SkyColorxyY = [	this.Perez_( Coefficients_x, fTheta, fGamma ) * Zenith[0],
								this.Perez_( Coefficients_y, fTheta, fGamma ) * Zenith[1],
								this.Perez_( Coefficients_Y, fTheta, fGamma ) * Zenith[2] ];

			var XYZ_TO_RGB		= [ [  3.240790, -0.969256,  0.055648 ],
									[ -1.537150,  1.875992, -0.204043 ],
									[ -0.498535,  0.041556,  1.057311 ] ];
			
			var	SkyColorXYZ = [	SkyColorxyY[0] * SkyColorxyY[2] / SkyColorxyY[1],
								SkyColorxyY[2],
								(1.0 - SkyColorxyY[0] - SkyColorxyY[1]) * SkyColorxyY[2] / SkyColorxyY[1] ];

			var SkyColorRGB = Math3D.mulVectorMatrix( SkyColorXYZ, XYZ_TO_RGB );

			return	SkyColorRGB;
		}

		this.m_SunColor = ComputeColor.call( this, SunDirection );	// Sample the sky in the Sun direction for an approximation
		this.m_SkyColor = ComputeColor.call( this, [-SunDirection[0], Math.min( 0.7, SunDirection[1] ), -SunDirection[2]] );	// This is a complete approximation, waiting for SH coefficients !


		////////////////////////////////////////////////////////////////////////////
		// Compute SH coefficients for the first 3 bands
		// Code borrowed from http://www.cg.tuwien.ac.at/research/publications/2008/Habel_08_SSH/
		//
		/// <param name="_SunDirection">The direction of the Sun in spherical coordinates (i.e. Phi, Theta)</param>
		/// <param name="_Turbidity">The atmospheric turbidity</param>
		/// <param name="_fScale">The scale factor to apply to the SH coefficients</param>
		/// <returns>The resulting array of SH Coefficients</returns>
		//
		function	ComputeSkyDomeSHCoefficients( _SunPhi, _SunTheta, _Turbidity, _fScale )
		{
			var	fTheta = Math.min( 0.45 * Math.PI, _SunTheta );

			var	SHCoefficients = new Array( 3 * 3 );	// An array of 9 SH Coefficients (the first 3 bands)
			for ( var i=0; i < SHCoefficients.length; i++ )
				SHCoefficients[i] = [0, 0, 0];

 			// Generate the parameter matrix
			var	Matrix = new Array( 14 );//[14,8];
			for ( var i=0; i < 14; i++ )
				Matrix[i] = new Array( 8 );

			var	ThetaPow = new Array( 14 );
			var	TurbidityPow = new Array( 8 );

			ThetaPow[0] = Matrix[0][0] = 1.0;
			ThetaPow[1] = Matrix[1][0] = fTheta;
			for ( var i=2; i < 14; ++i )
			{
				ThetaPow[i] = ThetaPow[i-1] * fTheta;
				Matrix[i][0] = ThetaPow[i];
			}

			TurbidityPow[0] = 1.0;
			TurbidityPow[1] = Matrix[0][1] = _Turbidity;
			for ( var j=2; j < 8; ++j )
			{
				TurbidityPow[j] = TurbidityPow[j-1] * _Turbidity;
				Matrix[0][j] = TurbidityPow[j];
			}

			for ( var i=0; i < 14; ++i )
				for ( var j = 0; j < 8; ++j )
					Matrix[i][j] = ThetaPow[i] * TurbidityPow[j];

// alert( "Matrix = " + patapi.helpers.EnumerateProperties( Matrix ) );
// alert( "Machin Fetch (1)[1,1,1] = " + patapi.Skydome_Internal_GetSHBand( 1 )[1][1][1] )

			// Execute coefficient multiplication for each coefficient
			for ( var l=0; l < 3; ++l )
				for ( var m=-l; m <= l ; ++m )
				{
					var bandindex = l+m;

					var	cr = 0, cg = 0, cb = 0;
				
					for ( var i=0; i < 14; ++i )
						for ( var j=0; j < 8; ++j )
						{
							cr += Matrix[i][j] * patapi.Skydome_Internal_GetSHBand( l )[bandindex][i][j][0];
							cg += Matrix[i][j] * patapi.Skydome_Internal_GetSHBand( l )[bandindex][i][j][1];
							cb += Matrix[i][j] * patapi.Skydome_Internal_GetSHBand( l )[bandindex][i][j][2];
						}

					var	k = l*(l+1) + m;

					SHCoefficients[k][0] = cr;
					SHCoefficients[k][1] = cg;
					SHCoefficients[k][2] = cb;
				}

			for ( var l=0; l < 3; ++l )
				for ( var m=1; m <= l; ++m )
				{
					var k_m = l*(l+1) + m;
					var k_minus_m = l*(l+1) - m;

					var	c_m_r = SHCoefficients[k_m][0];
					var	c_m_g = SHCoefficients[k_m][1];
					var	c_m_b = SHCoefficients[k_m][2];

					var	c_minus_m_r = SHCoefficients[k_minus_m][0];
					var	c_minus_m_g = SHCoefficients[k_minus_m][1];
					var	c_minus_m_b = SHCoefficients[k_minus_m][2];
					
					var	tcos = Math.cos( m * _SunPhi );
					var	tsin = Math.sin( m * _SunPhi );

					SHCoefficients[k_m][0] = c_m_r*tcos - c_minus_m_r*tsin;
					SHCoefficients[k_m][1] = c_m_g*tcos - c_minus_m_g*tsin;
					SHCoefficients[k_m][2] = c_m_b*tcos - c_minus_m_b*tsin;
					
					SHCoefficients[k_minus_m][0] = c_minus_m_r*tcos + c_m_r*tsin;
					SHCoefficients[k_minus_m][1] = c_minus_m_g*tcos + c_m_g*tsin;
					SHCoefficients[k_minus_m][2] = c_minus_m_b*tcos + c_m_b*tsin;
				}

			// Gibbs suppression (avoids ringing)
			for ( var l=1; l < 3; ++l )
			{
				var	k = l*(l+1);
				var	fAngle = Math.PI * l / 3;
				var	fFactor = Math.sin( fAngle ) / fAngle;

				SHCoefficients[k][0] *= fFactor;
				SHCoefficients[k][1] *= fFactor;
				SHCoefficients[k][2] *= fFactor;
			}

			// Apply scaling
 			for ( var i=0; i < 3 * 3; ++i )
 				SHCoefficients[i] = Math3D.mulScalarVector( _fScale, SHCoefficients[i] );

			return	SHCoefficients;
		}

		var	fSunPhi = 0.5 * Math.PI - Math.atan2( this.m_SunDirection[2], this.m_SunDirection[0] );
		var	fClampedTurbidity = Math.max( 2.0, Math.min( 7.0, this.m_Turbidity ) );		// After testing, using a turbidity out of that range yields too strange results

//		this.m_SkySHCoefficients = ComputeSkyDomeSHCoefficients( fSunPhi, fSunTheta, fClampedTurbidity, 1.0 );
		// TEST
		this.m_SkySHCoefficients = ComputeSkyDomeSHCoefficients( fSunPhi, fSunTheta, fClampedTurbidity, 0.01 * this.m_GlobalLuminanceFactor );
		// TEST

//alert( "SHCoeffs =\n" + patapi.helpers.EnumerateProperties( this.m_SkySHCoefficients ) );


		////////////////////////////////////////////////////////////////////////////
		// Manually update all shaders publishing the interfaces we support
		if ( this.m_ShaderDataProviderDirectional != null )
		{
			this.m_ShaderDataProviderDirectional.ProvideDataAll();
			this.m_ShaderDataProviderAmbient.ProvideDataAll();
			this.m_ShaderDataProviderSH.ProvideDataAll();
		}

		////////////////////////////////////////////////////////////////////////////
		// Notify if the callback is valid
		if ( this.m_UpdateCallback != null )
			this.m_UpdateCallback( this );
	},

	// Helper function that computes the vector pointing toward the Sun given the position on Earth, and the day and time of the year
	//	_Longitude, the longitude in [0,2PI]
	//	_Latitude, the latitude in [0,PI] (starting from north pole)
	//	_JulianDay, the day of year in [0,365]
	//	_TimeOfDay, the time of day in [0,24]
	//
	ComputeSunPosition : function( _Longitude, _Latitude, _JulianDay, _TimeOfDay )
	{
		// Compute solar time
		var	fSolarTime = _TimeOfDay + 0.17 * Math.sin( 4.0 * Math.PI * (_JulianDay - 80) / 373 ) - 0.129 * Math.sin( 2.0 * Math.PI * (_JulianDay - 8) / 355 ) - 12.0 * _Longitude / Math.PI;

		// Compute solar declination
		var	fSolarDeclination = 0.4093 * Math.sin( 2.0 * Math.PI * (_JulianDay - 81) / 368 );

		// Compute solar position in spherical coordinates
		var SunDirectionSpherical = [	Math.atan2( -Math.cos( fSolarDeclination ) * Math.sin( Math.PI * fSolarTime / 12.0 ), Math.cos( _Latitude ) * Math.sin( fSolarDeclination ) - Math.sin( _Latitude ) * Math.cos( fSolarDeclination ) * Math.cos( Math.PI * fSolarTime / 12.0 ) ),
										((.5 * Math.PI - Math.asin( Math.sin( _Latitude ) * Math.sin( fSolarDeclination ) - Math.cos( _Latitude ) * Math.cos( fSolarDeclination ) * Math.cos( Math.PI * fSolarTime / 12.0 ) )))
									];

		// Transform into cartesian coordinates
		var Theta = SunDirectionSpherical[0];
		var Phi = SunDirectionSpherical[1];


// 		// [DEBUG] To debug light direction
// 		Phi = 2.0 * Math.PI * _TimeOfDay / 24.0;	// Turns about vertical axis
// 		Theta = 0.5 * Math.PI;						// Almost at the horizon
// 		// [DEBUG]

		return	[	Math.sin( Theta ) * Math.sin( -Phi ),
					Math.cos( Theta ),
					Math.sin( Theta ) * Math.cos( -Phi )];
	},

	// Updates the position of the skydome given the current camera position
	//	_CameraPosition, the camera position in WORLD space (a [X,Y,Z] vector)
	//
	// NOTE: The skydome covers a sphere of radius 1000 so for small scenes, it's usually
	//			useless to move the skydome but for scenes covering large areas, the camera
	//			may exit the skydome and the skydome won't show because it's being culled
	//			(it's a bug I notified but no one gave me answers about that yet)
	//
	UpdateCameraPosition : function( _CameraPosition )
	{
		var	CurrentTransform = this.m_SkyDomeTransform.localMatrix;
			CurrentTransform[3] = [_CameraPosition[0], 0, _CameraPosition[2], 1];

		this.m_SkyDomeTransform.localMatrix = CurrentTransform;
	},

	// The monochromatic Perez function modeling sky appearance using a single parameter : Turbidity
	// \param the 5 coefficients for the Perez function
	// \param the polar angle for the Sun
	// \param the phase angle between Sun direction and view direction
	// \return the result of the Perez function
	//
	Perez_ : function( _Coefficients, _Theta, _Phase )
	{
		// Horizon is a singularity... Make sure we're always a bit above it...
		_Theta = Math.min( 0.5 * Math.PI - 0.00001, _Theta );

		var		fCosGamma = Math.cos( _Phase );
		return	(1.0 + _Coefficients[0] * Math.exp( _Coefficients[1] / Math.cos( _Theta ) )) *
				(1.0 + _Coefficients[2] * Math.exp( _Coefficients[3] * _Phase ) + _Coefficients[4] * fCosGamma * fCosGamma);
	}
};
