// Provides the Materials Translator object
//
// The Materials Translator is a simple helper to substitue the effects exported by a given scene
//	with other effects and to remap the effect parameters.
//
// For example, if you export a simple mesh with a diffuse texture using Collada and don't specify
//	anything else, O3D will load the scene and apply the standard Phong effect to render your object.
//
// Unfortunately, if you don't want to use the default effect (which is usually the case), you'll
//	have to browse through the exported materials and assign a new effect that will override the
//	default one, as well as remap the default effect parameters to your own effect (e.g. like copy
//	the diffuse texture or illumination parameters).
// This is a tedious process and the Materials Translator is there to do exactly that for you.
//
// To use it, simply register some effects and specify how they will replace existing materials
//	as will be shown below.
//
// You can also optionnaly specify Source/Target parameter names either on the effect itself (LOCAL)
//	 or the Materials Translator (GLOBAL). Local translation take over global ones.
//
// Then, simply call the "TranslateMaterials()" function once the scene has loaded, given the
//	list of materials that should be replaced. Voilą !
// 
// 
// To specify a material replacement, you have several options when calling "RegisterEffectTranslation()" :
//	1) Specify an effect and a regular expression giving the name of the materials it should be applied to
//	2) Specify an effect and a regular expression giving the name of the materials it should NOT be applied to
//	3) Specify an effect and a callback function that will be called for each matching material so you have
//		the control on what translation to use
//
// You can also specify a default replacement so a default effect is used if none of the registered
//	effects found a match. You do this by calling "RegisterEffectTranslation()" passing ONLY the effect to use,
//	without the match or no-match regular expression.
//
//
// Example: These 3 lines replace all the effects from a scene with the effect called "Smurf" and remap the
//			 diffuse texture parameter.
//
//	var MyScene = (...)  // <== Some scene that was loaded and ready to use
//
//	MaterialsTranslator.RegisterEffectTranslation( Smurf, /.*/ );											// <== You don't have to specify /.*/ if it's the default effect translation but here I do for the example's sake
//	MaterialsTranslator.RegisterParameterTranslation( "diffuseSampler", "MyPersonnalSamplerVariable" );		// <== The diffuse sampler will be copied and assigned to the replacement effect's "MyPersonnalSamplerVariable" parameter instead
//	MaterialsTranslator.TranslateMaterials( SomePack.getObjectsByClassName( 'o3d.Material' ) );				// <== Translate all existing materials
// 
//
o3djs.provide( 'patapi.materialstranslator' );

o3djs.require( 'patapi' );

o3djs.require('o3djs.util');
o3djs.require('o3djs.math');
o3djs.require('o3djs.pack');
o3djs.require('o3djs.effect');


////////////////////////////////////////////////////////////////////////////
// Declares a class for the materials manager
//
//	_O3DClient, the O3D client
//	_O3DPack, the O3D pack into which create the objects
//
patapi.MaterialsTranslator = function( _O3DClient, _O3DPack )
{
	this.m_Client = _O3DClient;	// Save the client for later use
	this.m_Pack = _O3DPack;		// Save the pack for later use

	this.m_RegisteredEffectTranslations = [];		// The array of registered effect translations
	this.m_DefaultTranslation = null;				// The default translation

	this.m_RegisteredParametersTranslations = {};	// The table of global parameter translations
};

patapi.MaterialsTranslator.prototype = 
{
	// Gets the registered effect translations
	getRegisteredEffectTranslations : function()	{ return this.m_RegisteredEffectTranslations; },

	// Disposes of used resources
	// Call this when exiting the application
	//
	Dispose : function()
	{
		throw "TODO!"
	},

	// Registers a new effect translation
	//	_Name, the name of the effect translation
	//	_Effect, the effect to use as a replacement (NOTE: you can also pass an effect wrapped by the Effects Manager)
	//	_optMaterialMatchRegEx, the optional regular expression to match material names (e.g. all materials is simply ".*" but be careful: there can only be one translation that matches all materials : the default translation !)
	//	_optMaterialNoMatchRegEx, the optional regular expression to NOT-match material names
	//	_optMaterialMatchCallback, the optional callback function that will be called to check for a match. (function prototype is simply "function( _EffectTranslation, _Material ) : EffectTranslation" and returns the effect translation to apply to the material or null if none exists)
	//	_optTranslationCallback, the optional callback function that will be called if a material gets translated by this effect translation (function prototype is simply "function( _EffectTranslation, _Material )" and applies additional tranlation if needed)
	//
	// returns the created effect translation
	//
	RegisterEffectTranslation : function( _Name, _Effect, _optMaterialMatchRegEx, _optMaterialNoMatchRegEx, _optMaterialMatchCallback, _optTranslationCallback )
	{
		// Build significant parameters
		var	MaterialMatchRegEx = null;
		if ( _optMaterialMatchRegEx )
			MaterialMatchRegEx = _optMaterialMatchRegEx;

		var	MaterialNoMatchRegEx = null;	// Match none
		if ( _optMaterialNoMatchRegEx )
			MaterialNoMatchRegEx = _optMaterialNoMatchRegEx;

// window.alert( "MatchSource = " + MaterialMatchRegEx.source )
// window.alert( "NoMatchSource = " + MaterialNoMatchRegEx.source )

		var	Translation = new patapi.EffectTranslation( this, _Name, _Effect, MaterialMatchRegEx, MaterialNoMatchRegEx, _optMaterialMatchCallback, _optTranslationCallback );

		if ( MaterialMatchRegEx != null && MaterialMatchRegEx.source == ".*" )
		{	// Register as the default translation
			if ( this.m_DefaultTranslation != null )
				// Throw if already an existing default translation (that's to avoid mistakes)
				throw "You're attempting to register a default Effect Translation (i.e. one with a match regular expression equal to \".*\") whereas there is already an existing one.\r\nYou have to call \"ClearDefaultEffectTranslation()\" first before registering a new one.";

			this.m_DefaultTranslation = Translation;
		}
		else
		{	// Simply register the new translation
			this.m_RegisteredEffectTranslations.push( Translation );
		}

		return	Translation;
	},

	// Clears the default effect translation
	// You must call this method prior registering a new default translation (i.e. no match or no-match reg exp)
	//	 otherwise an exception is thrown.
	// That is simply to ensure you don't register multiple, overriding translations by mistake.
	//
	ClearDefaultEffectTranslation : function()
	{
		this.m_DefaultTranslation = null;
	},

	// Registers a new global parameter translation
	//	_SourceParameterName, the source parameter to translate from
	//	_TargetParameterName, the target parameter to translate into
	//
	RegisterParameterTranslation : function( _SourceParameterName, _TargetParameterName )
	{
		this.m_RegisteredParametersTranslations[_SourceParameterName] = _TargetParameterName;
	},

	// Translates the effects on the provided materials
	//	_Materials, the array of materials whose effects should be translated
	//
	TranslateMaterials : function( _Materials )
	{
		for ( var MaterialIndex=0; MaterialIndex < _Materials.length; MaterialIndex++ )
		{
			var	Material = _Materials[MaterialIndex];

			// Attempt to find a matching effect translation for the material
			var	Match = null;
			for ( var EffectTranslationIndex=0; EffectTranslationIndex < this.m_RegisteredEffectTranslations.length; EffectTranslationIndex++ )
			{
				var	Translation = this.m_RegisteredEffectTranslations[EffectTranslationIndex];

//window.alert( "Trying translation \"" + Translation.getName() + "\" for material \"" + Material.name + "\"!" );

				Match = Translation.Matches( Material );
				if ( Match != null )
				{	// Found a match! Apply translation...
//window.alert( "Found translation \"" + Match.getName() + "\" for material \"" + Material.name + "\"!" );
					Match.Translate( Material );
					break;
				}
			}

			if ( Match != null || this.m_DefaultTranslation == null )
				continue;	// We found a match for that material so simply continue...

			// Apply the default translation
			Match = this.m_DefaultTranslation.Matches( Material );
			if ( Match != null )
				Match.Translate( Material );
		}
	},

	// Binds the translator to a scene loader so it intercepts the creation and initialization
	//	of materials and translates them automatically
	//	_SceneLoader, the scene loader to bind to
	//
	BindToSceneLoader : function( _SceneLoader )
	{
		var	that = this;
		
		_SceneLoader.RegisterInitializationCallback( "o3d.Material", function( _DeSerializer, _Object, _JSON )
		{
// window.alert( "Translating material \"" + _Object.name + "\"!" );

			if ( _JSON.custom && 'ShaderURI' in _JSON.custom )
			{	// Store the shader URI as a param
				_Object.createParam( 'ShaderURI', 'o3d.ParamString' ).value = _JSON.custom['ShaderURI'];
			}

			// Immediately translate the material
			that.TranslateMaterials( [_Object] );
		} );
	}
};


////////////////////////////////////////////////////////////////////////////
// Declares a class wrapping Effect Translations
//
//	_Owner, the owner materials translator
//	_Name, the name of the translation (debug purpose)
//	_Effect, the effect to apply to a material if there is a successful match
//	_MaterialMatchRegEx, the regular expression applied to the material name to see if there is a potential match
//	_MaterialNoMatchRegEx, the regular expression applied to the material name to see if there is a potential NO-match
//	_optMaterialMatchCallback, an optional callback that is called to match a material for translation (assuming the regular expressions successfully filtered the material)
//								The callback prototype should be "function( _EffectTranslation, _Material )" and return the translation to use if there is a match
//								(The default behavior is to return the passed _EffectTranslation parameter but you can return other translations as well)
//	_optTranslationCallback, the optional callback function that will be called if a material gets translated by this effect translation (function prototype is simply "function( _EffectTranslation, _Material )" and applies additional tranlation if needed)
//
patapi.EffectTranslation = function( _Owner, _Name, _Effect, _MaterialMatchRegEx, _MaterialNoMatchRegEx, _optMaterialMatchCallback, _optTranslationCallback )
{
	this.m_Owner = _Owner;
	this.m_Name = _Name;
	this.m_Effect = _Effect;
	this.m_MaterialMatchRegEx = _MaterialMatchRegEx;
	this.m_MaterialNoMatchRegEx = _MaterialNoMatchRegEx;
	this.m_MaterialMatchCallback = _optMaterialMatchCallback ? _optMaterialMatchCallback : null;
	this.m_TranslationCallback = _optTranslationCallback ? _optTranslationCallback : null;

	this.m_RegisteredParametersTranslations = {};		// The table of local parameter translations
};

patapi.EffectTranslation.prototype =
{
	// Gets the effect name
	getName : function()							{ return this.m_Name; },

	// Gets or sets the effect used as replacement
	getEffect : function()							{ return this.m_Effect; },
	setEffect : function( value )					{ this.m_Effect = value; },

	// Gets or sets regular expression used for matching
	getMaterialMatchRegEx : function()				{ return this.m_MaterialMatchRegEx; },
	setMaterialMatchRegEx : function( value )		{ this.m_MaterialMatchRegEx = value; },

	// Gets or sets regular expression used for NO-matching
	getMaterialNoMatchRegEx : function()			{ return this.m_MaterialNoMatchRegEx; },
	setMaterialNoMatchRegEx : function( value )		{ this.m_MaterialNoMatchRegEx = value; },

	// Gets or sets the optional callback called for material matching
	getMaterialMatchCallback : function()			{ return this.m_MaterialMatchCallback; },
	setMaterialMatchCallback : function( value )	{ this.m_MaterialMatchCallback = value; },

	// Gets or sets the optional callback called for translation
	getTranslationCallback : function()				{ return this.m_TranslationCallback; },
	setTranslationCallback : function( value )		{ this.m_TranslationCallback = value; },

	// Test the material for a match with this translation
	//	_Material, the material to test
	//
	// returns the translation to use if a successful match or null otherwise
	//
	Matches : function( _Material )
	{
		// Test for a match
		if ( this.m_MaterialMatchRegEx != null && this.m_MaterialMatchRegEx.exec( _Material.name ) == null )
			return	null;	// No match

		// Test for a no-match
		if ( this.m_MaterialNoMatchRegEx != null && this.m_MaterialNoMatchRegEx.exec( _Material.name ) != null )
			return	null;	// It's no-match

		// Test using callback
		if ( this.m_MaterialMatchCallback == null )
			return	this;
			
		try
		{
			return	this.m_MaterialMatchCallback( this, _Material );
		}
		catch ( e )
		{
			throw "An error occurred while attempting to call translation callback in EffectTranslation \"" + this.m_Name + "\" with material \"" + _Material.name + "\" !\r\n" + e;
		}
	},

	// Translates a material
	//	_Material, the material to translate
	//
	Translate : function( _Material )
	{
		// ================================================================
		// 1] Build the list of translated parameters
		var	TranslatedParameters = {};	// The table of translated parameters : keys are translated parameter names and values are source parameter values
		var	MaterialParams = _Material.params;

		function	TranslateParameters( _MaterialParams, _RegisteredTranslations, _TranslatedParameters )
		{
			for ( var MaterialParamIndex=0; MaterialParamIndex < _MaterialParams.length; MaterialParamIndex++ )
			{
				var	MaterialParam = _MaterialParams[MaterialParamIndex];

				// Try and find that parameter in the list of registered translations
				for ( var TranslationSource in _RegisteredTranslations )
					if ( MaterialParam.name == TranslationSource )
					{	// Found a translation for that parameter
						if ( MaterialParam.name in _TranslatedParameters )
							break;	// Already translated...

						// Store the value of the parameter attached to its translated name
						_TranslatedParameters[_RegisteredTranslations[TranslationSource]] = MaterialParam.value;						

						break;
					}
			}
		}

			// 1.1] Local parameters (valid for that effect translation only)
		TranslateParameters( MaterialParams, this.m_RegisteredParametersTranslations, TranslatedParameters );

			// 1.2] Global parameters (valid for all effect translations)
		TranslateParameters( MaterialParams, this.m_Owner.m_RegisteredParametersTranslations, TranslatedParameters );

//window.alert( "Translated parameters:\r\n\r\n" + patapi.helpers.EnumerateProperties( TranslatedParameters ) );


		// ================================================================
		// 2] Build the actual parameters needed for the replacement effect
		this.m_Effect.createUniformParameters( _Material );	// <== Will perform a "Bind()" if the effect is Wrapped Effect


		// ================================================================
		// 3] Initialize them with parameter values from the original effect
		MaterialParams = _Material.params;	// Retrieve the NEW params from the new effect

		for ( var MaterialParamIndex=0; MaterialParamIndex < MaterialParams.length; MaterialParamIndex++ )
		{
			var	MaterialParam = MaterialParams[MaterialParamIndex];
			for ( var TranslatedParameter in TranslatedParameters )
				if ( MaterialParam.name == TranslatedParameter )
				{	// Assign the original value
					MaterialParam.value = TranslatedParameters[MaterialParam.name];
					break;
				}
		}


		// ================================================================
		// 4] Eventually, call the user callback for additional translation
		if ( this.m_TranslationCallback != null )
			this.m_TranslationCallback( this, _Material );
 

//window.alert( "Final material params:\r\n\r\n" + patapi.helpers.EnumerateProperties( MaterialParams ) );
	},

	// Registers a new global parameter translation
	//	_SourceParameterName, the source parameter to translate from
	//	_TargetParameterName, the target parameter to translate into
	//
	RegisterParameterTranslation : function( _SourceParameterName, _TargetParameterName )
	{
		this.m_RegisteredParametersTranslations[_SourceParameterName] = _TargetParameterName;
	}
};
