/**
 * properties.ColorShortcuts
 * List of default special color properties (normal and splitter properties) for the Tweener class
 * The function names are strange/inverted because it makes for easier debugging (alphabetic order). They're only for internal use (on this class) anyways.
 *
 * @author		Zeh Fernando, Nate Chatellier, Arthur Debert
 * @version		1.0.0
 */

import flash.geom.ColorTransform;
import flash.filters.ColorMatrixFilter;

import caurina.transitions.Tweener;
import caurina.transitions.AuxFunctions;

class caurina.transitions.properties.ColorShortcuts {

	// Sources:
	// http://www.graficaobscura.com/matrix/index.html
	// And mario Klingemann's ColorMatrix class as mentioned on the credits:
	// http://www.quasimondo.com/archives/000565.php
	
	// Defines luminance using sRGB luminance
	private static var LUMINANCE_R:Number = 0.212671;
	private static var LUMINANCE_G:Number = 0.715160;
	private static var LUMINANCE_B:Number = 0.072169;
	
	/**
	 * There's no constructor.
	 */
	public function ColorShortcuts () {
		trace ("This is an static class and should not be instantiated.")
	}

	/**
	 * Registers all the special properties to the Tweener class, so the Tweener knows what to do with them.
	 */
	public static function init():Void {

		// Normal properties
		Tweener.registerSpecialProperty("_color_ra", _oldColor_property_get, _oldColor_property_set, ["ra"]);
		Tweener.registerSpecialProperty("_color_rb", _oldColor_property_get, _oldColor_property_set, ["rb"]);
		Tweener.registerSpecialProperty("_color_ga", _oldColor_property_get, _oldColor_property_set, ["ga"]);
		Tweener.registerSpecialProperty("_color_gb", _oldColor_property_get, _oldColor_property_set, ["gb"]);
		Tweener.registerSpecialProperty("_color_ba", _oldColor_property_get, _oldColor_property_set, ["ba"]);
		Tweener.registerSpecialProperty("_color_bb", _oldColor_property_get, _oldColor_property_set, ["bb"]);
		Tweener.registerSpecialProperty("_color_aa", _oldColor_property_get, _oldColor_property_set, ["aa"]);
		Tweener.registerSpecialProperty("_color_ab", _oldColor_property_get, _oldColor_property_set, ["ab"]);

		Tweener.registerSpecialProperty("_color_redMultiplier", 	_color_property_get, _color_property_set, ["redMultiplier"]);
		Tweener.registerSpecialProperty("_color_redOffset",			_color_property_get, _color_property_set, ["redOffset"]);
		Tweener.registerSpecialProperty("_color_greenMultiplier",	_color_property_get, _color_property_set, ["greenMultiplier"]);
		Tweener.registerSpecialProperty("_color_greenOffset",		_color_property_get, _color_property_set, ["greenOffset"]);
		Tweener.registerSpecialProperty("_color_blueMultiplier",	_color_property_get, _color_property_set, ["blueMultiplier"]);
		Tweener.registerSpecialProperty("_color_blueOffset",		_color_property_get, _color_property_set, ["blueOffset"]);
		Tweener.registerSpecialProperty("_color_alphaMultiplier",	_color_property_get, _color_property_set, ["alphaMultiplier"]);
		Tweener.registerSpecialProperty("_color_alphaOffset",		_color_property_get, _color_property_set, ["alphaOffset"]);

		// Normal splitter properties
		Tweener.registerSpecialPropertySplitter("_color", _color_splitter);
		Tweener.registerSpecialPropertySplitter("_colorTransform", _colorTransform_splitter);

		// Color changes that depend on the ColorMatrixFilter
		Tweener.registerSpecialProperty("_brightness",		_brightness_get,	_brightness_set, [false]);
		Tweener.registerSpecialProperty("_tintBrightness",	_brightness_get,	_brightness_set, [true]);
		Tweener.registerSpecialProperty("_contrast",		_contrast_get,		_contrast_set);
		Tweener.registerSpecialProperty("_hue",				_hue_get,			_hue_set);
		Tweener.registerSpecialProperty("_saturation",		_saturation_get,	_saturation_set, [false]);
		Tweener.registerSpecialProperty("_dumbSaturation",	_saturation_get,	_saturation_set, [true]);

	}


	// ==================================================================================================================================
	// PROPERTY GROUPING/SPLITTING functions --------------------------------------------------------------------------------------------

	// ----------------------------------------------------------------------------------------------------------------------------------
	// _color

	/**
	 * Splits the _color parameter into specific color variables
	 *
	 * @param		p_value				Number		The original _color value
	 * @return							Array		An array containing the .name and .value of all new properties
	 */
	public static function _color_splitter (p_value:Number, p_parameters:Array):Array {
		var nArray:Array = new Array();
		if (p_value == null) {
			// No parameter passed, so just resets the color
			nArray.push({name:"_color_redMultiplier",	value:1});
			nArray.push({name:"_color_redOffset",		value:0});
			nArray.push({name:"_color_greenMultiplier",	value:1});
			nArray.push({name:"_color_greenOffset",		value:0});
			nArray.push({name:"_color_blueMultiplier",	value:1});
			nArray.push({name:"_color_blueOffset",		value:0});
		} else {
			// A color tinting is passed, so converts it to the object values
			nArray.push({name:"_color_redMultiplier",	value:0});
			nArray.push({name:"_color_redOffset",		value:AuxFunctions.numberToR(p_value)});
			nArray.push({name:"_color_greenMultiplier",	value:0});
			nArray.push({name:"_color_greenOffset",		value:AuxFunctions.numberToG(p_value)});
			nArray.push({name:"_color_blueMultiplier",	value:0});
			nArray.push({name:"_color_blueOffset",		value:AuxFunctions.numberToB(p_value)});
		}
		return nArray;
	}


	// ----------------------------------------------------------------------------------------------------------------------------------
	// _colorTransform

	/**
	 * Splits the _colorTransform parameter into specific color variables
	 *
	 * @param		p_value				Number		The original _colorTransform value
	 * @return							Array		An array containing the .name and .value of all new properties
	 */
	public static function _colorTransform_splitter (p_value:Object, p_parameters:Array):Array {
		var nArray:Array = new Array();
		if (p_value == null) {
			// No parameter passed, so just resets the color
			nArray.push({name:"_color_redMultiplier",	value:1});
			nArray.push({name:"_color_redOffset",		value:0});
			nArray.push({name:"_color_greenMultiplier",	value:1});
			nArray.push({name:"_color_greenOffset",		value:0});
			nArray.push({name:"_color_blueMultiplier",	value:1});
			nArray.push({name:"_color_blueOffset",		value:0});
		} else {
			// A color tinting is passed, so converts it to the object values
			if (p_value.ra != undefined) nArray.push({name:"_color_ra", value:p_value.ra});
			if (p_value.rb != undefined) nArray.push({name:"_color_rb", value:p_value.rb});
			if (p_value.ga != undefined) nArray.push({name:"_color_ba", value:p_value.ba});
			if (p_value.gb != undefined) nArray.push({name:"_color_bb", value:p_value.bb});
			if (p_value.ba != undefined) nArray.push({name:"_color_ga", value:p_value.ga});
			if (p_value.bb != undefined) nArray.push({name:"_color_gb", value:p_value.gb});
			if (p_value.aa != undefined) nArray.push({name:"_color_aa", value:p_value.aa});
			if (p_value.ab != undefined) nArray.push({name:"_color_ab", value:p_value.ab});
			if (p_value.redMultiplier != undefined)		nArray.push({name:"_color_redMultiplier", value:p_value.redMultiplier});
			if (p_value.redOffset != undefined)			nArray.push({name:"_color_redOffset", value:p_value.redOffset});
			if (p_value.blueMultiplier != undefined)	nArray.push({name:"_color_blueMultiplier", value:p_value.blueMultiplier});
			if (p_value.blueOffset != undefined)		nArray.push({name:"_color_blueOffset", value:p_value.blueOffset});
			if (p_value.greenMultiplier != undefined)	nArray.push({name:"_color_greenMultiplier", value:p_value.greenMultiplier});
			if (p_value.greenOffset != undefined)		nArray.push({name:"_color_greenOffset", value:p_value.greenOffset});
			if (p_value.alphaMultiplier != undefined)	nArray.push({name:"_color_alphaMultiplier", value:p_value.alphaMultiplier});
			if (p_value.alphaOffset != undefined)		nArray.push({name:"_color_alphaOffset", value:p_value.alphaOffset});
		}
		return nArray;
	}


	// ==================================================================================================================================
	// NORMAL SPECIAL PROPERTY functions ------------------------------------------------------------------------------------------------

	// ----------------------------------------------------------------------------------------------------------------------------------
	// _color_*

	/**
	 * _color_*
	 * Generic function for the ra/rb/etc components of the deprecated colorTransform object
	 */
	public static function _oldColor_property_get (p_obj:Object, p_parameters:Array):Number {
		return (new Color(p_obj)).getTransform()[p_parameters[0]];
	}
	public static function _oldColor_property_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		var cfObj:Object = new Object();
		cfObj[p_parameters[0]] = p_value; // Math.round(p_value);
		(new Color(p_obj)).setTransform(cfObj);
	}

	/**
	 * _color_*
	 * Generic function for the redMultiplier/redOffset/etc components of the new colorTransform
	 */
	public static function _color_property_get (p_obj:Object, p_parameters:Array):Number {
		return p_obj.transform.colorTransform[p_parameters[0]];
	}
	public static function _color_property_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		var cfm:ColorTransform = p_obj.transform.colorTransform;
		cfm[p_parameters[0]] = p_value;
		p_obj.transform.colorTransform = cfm;
	}

	// ----------------------------------------------------------------------------------------------------------------------------------
	// Special coloring

	/**
	 * _brightness
	 * Brightness of an object: -1 -> [0] -> +1
	 */
	public static function _brightness_get (p_obj:Object, p_parameters:Array):Number {

		var isTint:Boolean = p_parameters[0];

		/*
		// Using ColorMatrix:
		
		var mtx:Array = getObjectMatrix(p_obj);
		
		var mc:Number = 1 - ((mtx[0] + mtx[6] + mtx[12]) / 3); // Brightness as determined by the main channels
		var co:Number = (mtx[4] + mtx[9] + mtx[14]) / 3; // Brightness as determined by the offset channels
		*/

		var cfm:Object = (new Color(p_obj)).getTransform();
		var mc:Number = 1 - ((cfm.ra + cfm.ga + cfm.ba) / 300); // Brightness as determined by the main channels
		var co:Number = (cfm.rb + cfm.gb + cfm.bb) / 3;

		if (isTint) {
			// Tint style
			return co > 0 ? co / 255 : -mc;
		} else {
			// Native, Flash "Adjust Color" and Photoshop style
			return co / 100;
		}
	}
	public static function _brightness_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		//var mtx:Array = getObjectMatrix(p_obj);

		var isTint:Boolean = p_parameters[0];

		var mc:Number; // Main channel
		var co:Number; // Channel offset

		if (isTint) {
			// Tint style
			mc = 1 - Math.abs(p_value);
			co = p_value > 0 ? Math.round(p_value*255) : 0;
		} else {
			// Native, Flash "Adjust Color" and Photoshop style
			mc = 1;
			co = Math.round(p_value*100);
		}

		/*
		// Using ColorMatrix:
		var mtx:Array = [
			mc, cc, cc, cc, co,
			cc, mc, cc, cc, co,
			cc, cc, mc, cc, co,
			0,  0,  0,  1,  0
		];
		setObjectMatrix(p_obj, mtx);
		*/
		var cfm:Object = {ra:mc * 100, rb:co, ga:mc * 100, gb:co, ba:mc * 100, bb:co};
		(new Color(p_obj)).setTransform(cfm);
	}

	/**
	 * _saturation
	 * Saturation of an object: 0 -> [1] -> 2
	 */
	public static function _saturation_get (p_obj:Object, p_parameters:Array):Number {

		var mtx:Array = getObjectMatrix(p_obj);

		var isDumb:Boolean = p_parameters[0];
		var rl:Number = isDumb ? 1/3 : LUMINANCE_R;
		var gl:Number = isDumb ? 1/3 : LUMINANCE_G;
		var bl:Number = isDumb ? 1/3 : LUMINANCE_B;

		var mc:Number = ((mtx[0]-rl)/(1-rl) + (mtx[6]-gl)/(1-gl) + (mtx[12]-bl)/(1-bl)) / 3;					// Color saturation as determined by the main channels
		var cc:Number = 1 - ((mtx[1]/gl + mtx[2]/bl + mtx[5]/rl + mtx[7]/bl + mtx[10]/rl + mtx[11]/gl) / 6);	// Color saturation as determined by the other channels
		return (mc + cc) / 2;
	}
	public static function _saturation_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		
		var isDumb:Boolean = p_parameters[0];
		var rl:Number = isDumb ? 1/3 : LUMINANCE_R;
		var gl:Number = isDumb ? 1/3 : LUMINANCE_G;
		var bl:Number = isDumb ? 1/3 : LUMINANCE_B;

		var sf:Number = p_value;
		var nf:Number = 1-sf;
		var nr:Number = rl * nf;
		var ng:Number = gl * nf;
		var nb:Number = bl * nf;

		var mtx:Array = [
			nr+sf,	ng,		nb,		0,	0,
			nr,		ng+sf,	nb,		0,	0,
			nr,		ng,		nb+sf,	0,	0,
			0,  	0, 		0,  	1,  0
		];
		setObjectMatrix(p_obj, mtx);
	}

	/**
	 * _contrast
	 * Contrast of an object: -1 -> [0] -> +1
	 */
	public static function _contrast_get (p_obj:Object, p_parameters:Array):Number {

		/*
		// Using ColorMatrix:
		var mtx:Array = getObjectMatrix(p_obj);

		var mc:Number = ((mtx[0] + mtx[6] + mtx[12]) / 3) - 1;		// Contrast as determined by the main channels
		var co:Number = (mtx[4] + mtx[9] + mtx[14]) / 3 / -128;		// Contrast as determined by the offset channel
		*/
		var cfm:Object = (new Color(p_obj)).getTransform();
		var mc:Number;	// Contrast as determined by the main channels
		var co:Number;	// Contrast as determined by the offset channel
		mc = ((cfm.ra + cfm.ga + cfm.ba) / 300) - 1;
		co = (cfm.rb + cfm.gb + cfm.bb) / 3 / -128;
		/*
		if (cfm.ra < 100) {
			// Low contrast
			mc = ((cfm.ra + cfm.ga + cfm.ba) / 300) - 1;
			co = (cfm.rb + cfm.gb + cfm.bb) / 3 / -128;
		} else {
			// High contrast
			mc = (((cfm.ra + cfm.ga + cfm.ba) / 300) - 1) / 37;
			co = (cfm.rb + cfm.gb + cfm.bb) / 3 / -3840;
		}
		*/

		return (mc+co)/2;
	}
	public static function _contrast_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		
		var mc:Number;	// Main channel
		var co:Number;	// Channel offset
		mc = p_value + 1;
		co = Math.round(p_value*-128);

		/*
		if (p_value < 0) {
			// Low contrast
			mc = p_value + 1;
			co = Math.round(p_value*-128);
		} else {
			// High contrast
			mc = (p_value * 37) + 1;
			co = Math.round(p_value*-3840);
		}
		*/
		
		// Flash: * 8, * -512

		/*
		// Using ColorMatrix:
		var mtx:Array = [
			mc,	0,	0, 	0, co,
			0,	mc,	0, 	0, co,
			0,	0,	mc,	0, co,
			0,  0, 	0, 	1,  0
		];
		setObjectMatrix(p_obj, mtx);
		*/
		var cfm:Object = {ra:mc * 100, rb:co, ga:mc * 100, gb:co, ba:mc * 100, bb:co};
		(new Color(p_obj)).setTransform(cfm);
	}

	/**
	 * _hue
	 * Hue of an object: -180 -> [0] -> 180
	 */
	public static function _hue_get (p_obj:Object, p_parameters:Array):Number {

		var mtx:Array = getObjectMatrix(p_obj);

		// Find the current Hue based on a given matrix.
		// This is a kind of a brute force method by sucessive division until a close enough angle is found.
		// Reverse-engineering the hue equation would be is a better choice, but it's hard to find material
		// on the correct calculation employed by Flash.
		// This code has to run only once (before the tween starts), so it's good enough.

		var hues:Array = [];
		hues[0] = {angle:-179.9, matrix:getHueMatrix(-179.9)};
		hues[1] = {angle:180, matrix:getHueMatrix(180)};
	
		for (var i:Number = 0; i < hues.length; i++) {
			hues[i].distance = getHueDistance(mtx, hues[i].matrix);
		}

		var maxTries:Number = 15;	// Number o maximum divisions until the hue is found
		var angleToSplit:Number;

		for (var i:Number = 0; i < maxTries; i++) {
			// Find the nearest angle
			if (hues[0].distance < hues[1].distance) {
				// First is closer
				angleToSplit = 1;
			} else {
				// Second is closer
				angleToSplit = 0;
			}
			hues[angleToSplit].angle = (hues[0].angle + hues[1].angle)/2;
			hues[angleToSplit].matrix = getHueMatrix(hues[angleToSplit].angle)
			hues[angleToSplit].distance = getHueDistance(mtx, hues[angleToSplit].matrix);
		}

		return hues[angleToSplit].angle;
	}

	public static function _hue_set (p_obj:Object, p_value:Number, p_parameters:Array):Void {
		setObjectMatrix(p_obj, getHueMatrix(p_value));
	}

	public static function getHueDistance (mtx1:Array, mtx2:Array): Number {
		return (Math.abs(mtx1[0] - mtx2[0]) + Math.abs(mtx1[1] - mtx2[1]) + Math.abs(mtx1[2] - mtx2[2]));
	}

	public static function getHueMatrix (hue:Number): Array {
		var ha:Number = hue * Math.PI/180;		// Hue angle, to radians

 		var rl:Number = LUMINANCE_R;
		var gl:Number = LUMINANCE_G;
		var bl:Number = LUMINANCE_B;

		var c:Number = Math.cos(ha);
		var s:Number = Math.sin(ha);

		var mtx:Array = [
			(rl + (c * (1 - rl))) + (s * (-rl)),
			(gl + (c * (-gl))) + (s * (-gl)),
			(bl + (c * (-bl))) + (s * (1 - bl)),
			0, 0,

			(rl + (c * (-rl))) + (s * 0.143),
			(gl + (c * (1 - gl))) + (s * 0.14),
			(bl + (c * (-bl))) + (s * -0.283),
			0, 0,

			(rl + (c * (-rl))) + (s * (-(1 - rl))),
			(gl + (c * (-gl))) + (s * gl),
			(bl + (c * (1 - bl))) + (s * bl),
			0, 0,

			0, 0, 0, 1, 0
		];
		
		return mtx;
	}


	// ==================================================================================================================================
	// AUXILIARY functions --------------------------------------------------------------------------------------------------------------

	private static function getObjectMatrix(p_obj:Object): Array {
		// Get the current color matrix of an object
		for (var i:Number = 0; i < p_obj.filters.length; i++) {
			if (p_obj.filters[i] instanceof ColorMatrixFilter) {
				return p_obj.filters[i].matrix.concat();
			}
		}
		return [
			1, 0, 0, 0, 0,
			0, 1, 0, 0, 0,
			0, 0, 1, 0, 0,
			0, 0, 0, 1, 0
		];
	}

	private static function setObjectMatrix(p_obj:Object, p_matrix:Array): Void {
		// Set the current color matrix of an object
		var objFilters:Array = p_obj.filters.concat();
		var found:Boolean = false;
		for (var i:Number = 0; i < objFilters.length; i++) {
			if (objFilters[i] instanceof ColorMatrixFilter) {
				objFilters[i].matrix = p_matrix.concat();
				found = true;
			}
		}
		if (!found) {
			// Has to create a new color matrix filter
			var cmtx:ColorMatrixFilter = new ColorMatrixFilter(p_matrix);
			objFilters[objFilters.length] = cmtx;
		}
		p_obj.filters = objFilters;
	}
}
