//! A Handy-Dandy Fader Constructor
//! Copyright 2005-2006 Tom W.M. (http://freecog.net/)
//! 2:15 p.m. Saturday, April 8, 2006
//! Free to use for any purpose, provided this notice is retained.
//! No dependencies.


// new Fader() -> Fader object
// Arguments:
//   object(s): string ID or HTMLElement or array of the same
//   start: start opacity (0-1); defaults to 1
//   end: end opacity (0-1); defaults to 0
//   duration: duration of the animation in ms
//   callbacks...: an unlimited number of functions to call back upon 
//     completion of the animation, in the form 
//     callback(objects[0], Fader object).  The strange first argument
//     for the sake of backwards compatibility, and ease of use when 
//     dealing with only a single element.

var Fader = function(objects, start, end, duration/*, callbacks...*/) {
	var self = this;
	
	this.id = Fader.guid++;
	this.objects = []; // Contains the elements being faded.
	this.callbacks = []; // Contains the callback functions called at the end.
	
	// Put all object(s) into the objects array, in HTMLElement form.
	if (!(typeof(objects) == 'object' && objects.length >= 1)) { // It's not an array.
		objects = [objects];
	}
	for (var i = 0, j = objects.length; i < j; i++) {
		this.objects.push(Fader.getElement(objects[i]));
	}
	
	// Add any callbacks at the end of the arguments array
	for (var i = 4, j = arguments.length; i < j; i++) {
		this.addCallback(arguments[i]);
	}
	
	// Apply the default values.
	start = (typeof(start) == 'undefined') ? 1 : start;
	end = end || 0;
	duration = duration || 500;
	
	// Validate the input.
	if (typeof(duration) != 'undefined' && duration <= 0) {
		throw new Error('Invalid duration passed to Fader constructor: ' + duration);
	}
	if (start < 0 || start > 1) {
		throw new Error('Invalid start passed to Fader constructor: ' + start);
	}
	if (end < 0 || end > 1) {
		throw new Error('Invalid end passed to Fader constructor: ' + end);
	}
	
	this.object = this.objects[0]; // For backward compatibility and convenience when only one object is faded.
	this.startOpacity = start;
	this.endOpacity = end;
	this.currentOpacity = start;
	this.duration = duration;
	
	// Status properties.
	this.isCompleted = false;
	this.isFadingIn = function() { return start < end; }
	this.isFadingOut = function() { return end < start; }
	
	this.getTimeElapsed = function() {
		var time = Fader.now() - startTime;
		return (time <= duration) ? time : duration; // No more than the duration
	};
	
	this.getTimeRemaining = function() {
		var time = duration - self.getTimeElapsed();
		return (time >= 0) ? time : 0; // No less than zero
	}
	
	var range = end - start;
	var startTime = Fader.now();
	
	function set(toWhat) {
		var toWhatSafe = (toWhat == 1) ? 0.9999 : toWhat;
		for (var i = 0, j = self.objects.length; i < j; i++) {
			var s = self.objects[i].style;
			s.KhtmlOpacity = toWhatSafe;
			s.MozOpacity = toWhatSafe;
			s.filter = "alpha(opacity=" + (toWhat * 100) + ")";
			s.opacity = toWhat;
		}
	};
	
	this.step = function() {
		var percent = self.getTimeElapsed() / duration;
		if (percent < 1) { // End the animation.
			self.currentOpacity = start + (percent * range);
			set(self.currentOpacity);
		} else {
			self.currentOpacity = end;
			set(end);
			self.cancel();
		}
	};
	
	set(start);
	Fader.register(this);
};

// STATIC PROPERTIES
Fader.INTERVAL_STEP = 30;
Fader.guid = 0;


(function(){
// The Fader registry -- contains all of the
// currently active Fader objects, and calls
// their step() methods at regular intervals.

var registry = [];
var interval = null;

// Calls the step() methods of fader objects
// in the registry.
function step() {
	// Copy all of the objects into a separate
	// array in case the registry is mutated
	// (this will happen with a completed fader
	// removes itself from the registry).
	var objs = []; 
	for (var i = 0, j = registry.length; i < j; i++) {
		objs.push(registry[i]);
	}
	for (var i = 0, j = objs.length; i < j; i++) {
		objs[i].step();
	}
}

Fader.register = function(fader) {
	registry.push(fader);
	if (!interval) {
		interval = window.setInterval(step, Fader.INTERVAL_STEP);
	}
};

Fader.unregister = function(fader) {
	for (var i = registry.length - 1; i >= 0; i--) {
		if (registry[i].id === fader.id) {
			registry.splice(i, 1);
		}
	}
	if (!registry.length && interval) {
		window.clearInterval(interval);
		interval = null;
	}
}

})();




Fader.prototype = {
	_callCallbacks: function() {
		for (var i = 0, j = this.callbacks.length; i < j; i++) {
			this.callbacks[i](this.object, this);
		}
	},
	cancelSilently: function() {
		// Stop the fade without calling the callbacks
		this.isCompleted = true;
		Fader.unregister(this);
	},
	cancel: function() {
		// Stop the fade and call the callbacks
		this.cancelSilently();
		this._callCallbacks();
	},
	addCallback: function(func) {
		this.callbacks.push(func);
	},
	removeCallback: function(func) {
		for (var i = 0, j = this.callbacks.length; i < j; i++) {
			if (this.callbacks[i] === func) {
				this.callbacks.splice(i, 1);
				return true; // Only remove one
			}
		}
		return false;
	},
	toString: function() {
		return "[object Fader #" + this.id + " (" + 
			Math.round( this.currentOpacity * 100 ) + 
			"% opacity, " + this.objects.length + 
			" object" + 
			(this.objects.length == 1 ? '' : "s") + 
			")]";
	}
};




// STATIC METHODS -- Some utility functions
Fader.now = function() {
	return (new Date()).getTime();
};

Fader.getElement = function(obj) {
	return (typeof obj == 'string') ? document.getElementById(obj) : obj;
};

Fader.toString = function() {
	return "[object Fader]";
};