/** Cross-browser fading effects library
 * @author sjors
 */

/** Fader::construct()
 * This object is able to fade any object-property to a specified target value.
 * Please note the start value is only optional when the property already is at the desired start value (in the same format as the target value)
 * 
 * @param object 	element		object to perform fade on
 * @param string 	property	property to fade
 * @param string 	target		target value: can be all sorts of values, like hex string ('#FFFFFF'), css values ('2em', '80px' etc) or any other value containing a hexadecimal or numeric part
 * @param int		duration	(optional, defaults to 500) duration of the fade
 * @param string	start		(optional, defaults to current value): the same types as for the target parameter are valid
 * @param int		fps			(optional, defaults to 20): smoothness in frames per second
 */
function Fader(element, property, target, duration, start, fps)
{
	// Settings
	this.element	= null;	// (object)				element to perform changes on
	this.property 	= null;	// (string) 			name of property to change
	this.duration	= 500;	// (int)				total duration in ms
	this.value		= null;	// (string) 			target value
	this.target		= null;	// (string, optional)	start value (defaults to current value)
	this.fps		= 20;	// (int)				smoothness in frames per second
	
	// Internal props
	this.running		= false;// (bool)			True if the animation is running
	this.callback		= null;	// (function)		Callback function @see attachCallacks
	this.propsObj		= 		// (object)			Object containing the splitted value-structure
	{
		'steps'			: 0,	// (int)			Amount of steps to animate
		'stepsLeft'		: 0,	// (int)			Amount of steps left
		'interval'		: 50	// (int)			Interval between steps in ms
	};
	this.intervalID		= null; // (int)			IntervalID: ID of the running interval. used to cancel the interval when ready
		
	// Init properties
	if(element && property && target)
	{
		this.element = element;
		this.property = property;
		this.target = target;
	}
	else throw("Failed to start fader");
	
	if(duration)
		this.duration = duration;
	if(start)
		this.value = start;
	if(fps)
		this.fps = fps;
	
	if(element)
	{
		if(this.splitValue())
		{		
			if(element[property] === undefined)
				element[property] = this.value;
			//if(this.start())
			return this;
		}
	}
	throw("Failed to start fader");
};

/** Fader::start()
 * Start/resume the animation
 * 
 * @param callback animator Callback function (defaults to Fader.linearAnimator): 
 * 							this function recieves propsObj (object containing properties to change, target(s), progress) 
 * 							and should move the current value(s) in the direction of the target value(s)
 * 
 * @returns {Boolean}		True on success, false otherwise
 */
Fader.prototype.start = function(animator)
{
	if(this.isRunning())
		return false;
	
	this.running = true;
	
	if(animator)
		this.animator = animator;
	else
		this.animator = this.linearAnimator;
	
	// start/resume animation
	this.propsObj.interval = 1000/this.fps;
	this.propsObj.steps = Math.ceil(this.duration/this.propsObj.interval);
	this.propsObj.stepsLeft = this.propsObj.steps;
	
	var me = this;
	clearInterval(this.intervalID);
	this.intervalID = setInterval(function(){return me.animate();}, this.propsObj.interval);
	return true;
};

/** Fader::stop()
 * Stop/pause the animation
 * @returns {Boolean} True on success, false otherwise
 */
Fader.prototype.stop = function()
{
	if(!this.isRunning())
		return false;
	// stop/pause animation
	clearInterval(this.intervalID);
	this.running = false;
	
	return true;
};

/** Fader::changeTarget()
 * Change the target value for the animation on the fly
 * 
 * @param string	newTarget	@see Fader::construct(), same possible values as for 'target'
 * @param int		newDuration (optional, defaults to current): duration for the animation to the new target value
 * @param int		newFPS		(optional, defaults to current): smoothness for the animation to the new target value
 */
Fader.prototype.changeTarget = function(newTarget, newDuration, newFPS)
{
	this.stop();
	// Init properties
	if(newTarget)
		this.target = newTarget;
	else return false;
	
	if(newDuration)
		this.duration = newDuration;
	if(newFPS)
		this.fps = newFPS;
	
	this.value = this.getCurrentValue();
	
	if(this.splitValue())
	{		
		if(this.element[this.property] === undefined)
			this.element[this.property] = this.value;
		// (re)start animation
		//this.stop();
		//return this.start();
		return true;
	}
	return false;
};

Fader.prototype.animate = function()
{
	this.propsObj.stepsLeft--;
	this.animator(this.propsObj);
	
	if(this.propsObj.stepsLeft <= 0)	// done
	{
		clearInterval(this.intervalID);
		this.doCallback();
		this.running = false;
	}
	this.element[this.property] = this.getCurrentValue();
};

Fader.prototype.splitValue = function()
{
	var arr_res = [];
	
	var str1 = this.value + '';
	var str2 = this.target + '';
	
	if(str1.length * str2.length > 0)
	{
		var index = 0;
		var suffix = '';
		var pos2 = 1;
	
		while(pos2 > 0)
		{
		    var pos1 = str1.search(/[A-Fa-f0-9]+/g);
		    pos2 = str2.search(/[A-Fa-f0-9]+/g);   
	
		    if((pos1 < 0) && (pos2 < 0))
		    {
		    	//suffix = str1;
		    	break;
		    }
		    else if(pos2 == pos1)
		    {
		        var item1 = str1.match(/[A-Fa-f0-9]+/);
		        var item2 = str2.match(/[A-Fa-f0-9]+/);
		        
		        if(item1 && item2)
		        {
		            item1 = item1[0];
		            item2 = item2[0];
		            index+= pos1;
		            
		            if(item1 != item2)
		            {
		            	var type = 'dec';
		            	if(this.value[index-1] == '#')
		            		type = 'hex';
		            	
		                arr_res.push
		                ({
		                    'prefix'	: str1.substring(0,pos1),
		                    'value'		: item1 + '',
		                    'target'	: item2 + '',
		                    'current'	: item1 + '',
		                    'type'		: type
		                });
		            }
		            
		            str1 = str1.substr(pos1+item1.length);
		            str2 = str2.substr(pos2+item2.length);
		            index+= item1.length;
		            continue;
		        }
		    }
		    arr_res = [];
		    break;
		}
	}
	// FIN var obj_res
	if(arr_res.length > 0)
	{
		this.propsObj.parts = arr_res;
		this.propsObj.suffix = str1;
		
		return true;
	}
	else
		return false;	
};

Fader.prototype.getCurrentValue = function()
{
	if(this.propsObj)
	{
		var val = '';
		var parts = this.propsObj.parts;
		var len = parts.length;
		for(var i=0;i<len;i++)
		{
			val+= '' + parts[i].prefix + '' + parts[i].current;
		}
		val+= '' + this.propsObj.suffix;
		return val;
	}
	return '';
};

Fader.prototype.isRunning = function()
{
	return this.running;
};


Fader.prototype.attachCallback = function(func)
{
	this.callback = func;
};

Fader.prototype.removeCallback = function()
{
	this.callback = null;
};

Fader.prototype.doCallback = function()
{
	if(this.callback !== null)
		this.callback();
};

//perform linear animation on the properties in a given propsObj
Fader.prototype.linearAnimator = function(propsObj)
{
	var progress = (propsObj.steps - propsObj.stepsLeft)/propsObj.steps;	// progress (0-1)
		
	var parts = propsObj.parts;
	var len = parts.length;	
	for(var i=0;i<len;i++)
	{
		var newValue = progress;	// linear algorithm itself
		
		var type = parts[i].type;
		var value;
		var target;
		var current;
		
		if(type == 'hex')
		{
			var targetWidth = (parts[i].target + '').length;
			
			// CLEAN code looks messy: DRY
			if(targetWidth == 6)
			{
				value = [hexStr2dec(parts[i].value.substr(0,2)), hexStr2dec(parts[i].value.substr(2,2)), hexStr2dec(parts[i].value.substr(4,2))];
				target = [hexStr2dec(parts[i].target.substr(0,2)), hexStr2dec(parts[i].target.substr(2,2)), hexStr2dec(parts[i].target.substr(4,2))];
				//current=0;
				current=[];
				for(var n=0;n<3;n++)
				{
					//current+= Math.pow(0xFF,2-n)*Math.round(newValue * ((target[n] - value[n]) + value[n]));
					current[n] = Math.round(newValue * (target[n] - value[n]) + value[n]);
				}
			}
			else
			{			
				value 	= hexStr2dec(parts[i].value);
				target	= hexStr2dec(parts[i].target);
				
				current = Math.round(newValue * (target - value) + value);
			}
		}
		else
		{
			value 	= 1*parts[i].value;
			target	= 1*parts[i].target;
			
			current = Math.round(newValue * (target - value) + value);
		}
		
		if(propsObj.stepsLeft <= 0)
			current = target;
		
		// CLEAN code looks messy: DRY
		if(type == 'hex')
		{
			if(targetWidth == 6)
			{
				var curr = '';
				for(var n=0;n<3;n++)
				{
					curr+= '' + dec2hexStr(current[n],2);
				}
				parts[i].current = curr;
			}
			else
				parts[i].current = dec2hexStr(current, targetWidth);
		}
		else
			parts[i].current = '' + current;
	}
};




var Fade =
{
	// Inner props
	'faders'		: {},	// (object)				currently running faders
	'lastID'		: 0		// (int)				last generated ID
};

Fade.fade = function(element, property, target, duration, start, fps, override)
{
	if(element)
	{
		if(!element.id)
			element.id = this.createID();
		var UID = element.id + '::' + property;
		
		if(override !== true)
			override = false;
		
		if(this.faders[UID] !== undefined)
		{
			if(override)
				return this.faders[UID].changeTarget(target, duration, fps) ? this.faders[UID] : null;
			else
				return this.faders[UID].changeTarget(target) ? this.faders[UID] : null;
		}
		else
		{
			try
			{
				var fader = new Fader(element, property, target, duration, start, fps);
			}
			catch(e)
			{
				return null;
			}		
			this.faders[UID] = fader;
			
			return fader;
		}
		
	}
	return null;	
};

Fade.createID = function()
{
	return 'generated_fade_id_' + this.lastID++;
};

