// Signals and slots for jQuery by Franck Marcia - 2006
// Inspired by signals-and-slots-for-prototype by Andrew Tetlaw (base code)
// and Dojo Event System by Alex Russell (features)

$.chain = function(obj, fct, dly) {

	// Define default values
	if (fct === undefined && dly === undefined) {
		fct = obj;
		obj = window;
	} else if (dly === undefined) {
		if (obj.constructor == String) {
			dly = fct;
			fct = obj;
			obj = window;
		}
	}

	// Store the replacement function name
	var sig = fct + '$F';

	// Define default values because public functions may be invoked using
	// ('function_name') or (object, 'function_name') or ('function_name', delay)
	// or (object, 'function_name', delay)
	function normalize(obj, fct, dly, adv) {
		if (fct === undefined && dly === undefined) {
			return {obj:window, fct:obj, dly:false, adv:adv};
		} else if (dly === undefined) {
			return obj.constructor == String ?
				{obj:window, fct:obj, dly:fct, adv:adv} :
				{obj:obj, fct:fct, dly:false, adv:adv};
		} else {
			return {obj:obj, fct:fct, dly:dly, adv:adv};
		}
	}

	// Check if the slot (object, 'function_name') is not in slots list
	function missing(me, obj, fct) {
		if (obj.constructor == String) {
			fct = obj;
			obj = window;
		}
		for (var slt, i=0, l=me.obj[me.slt].length; i<l; ++i) {
			slt = me.obj[me.slt][i];
			if (slt && slt.obj == obj && slt.fct == fct) {
				return false;
			}
		}
		return true;
	}

	// Remove the first slot (object, 'function_name') if all==false
	// all slots (object, 'function_name') if all==true && vry==false
	// all slots if all==true && vry==true
	// from slots list
	function remove(me, obj, fct, all, vry) {
		if (fct === undefined) {
			fct = obj;
			obj = window;
		}
		for (var i=0, l=me.obj[me.slt].length; i<l; ++i) {
			var slt = me.obj[me.slt][i];
			if (vry || slt && slt.obj == obj && slt.fct == fct) {
				slt = slt.obj = null;
				if (!all) {
					break;
				}
			}
		}
	}

	var me = {

		obj: obj,
		slt: fct + '$T',
		sdl: fct + '$D',

		// Define a slot to be invoked before the signal
		from: function(obj, fct, dly) {
			this.obj[this.slt].push(normalize(obj, fct, dly, 2));
			return this;
		},

		// Define a slot to be invoked after the signal
		to: function(obj, fct, dly) {
			this.obj[this.slt].push(normalize(obj, fct, dly, 1));
			return this;
		},

		// Define a slot to be invoked instead of the signal
		into: function(obj, fct, dly) {
			this.obj[this.slt].push(normalize(obj, fct, dly, 3));
			return this;
		},

		// Define a slot to be invoked before the signal only if not already defined
		fromOnce: function(obj, fct, dly) {
			if (missing(this, obj, fct)) {
				this.obj[this.slt].push(normalize(obj, fct, dly, 2));
			}
			return this;
		},

		// Define a slot to be invoked after the signal only if not already defined
		toOnce: function(obj, fct, dly) {
			if (missing(this, obj, fct)) {
				this.obj[this.slt].push(normalize(obj, fct, dly, 1));
			}
			return this;
		},

		// Remove a slot
		remove: function(obj, fct) {
			remove(this, obj, fct, false, false);
			return this;
		},

		// Remove all slots (object, 'function_name') or
		// really all chained slots if no argument is provided and reset the delay
		removeAll: function(obj, fct) {
			remove(this, obj, fct, true, obj === undefined && fct === undefined);
			if (obj === undefined && fct === undefined) {
				this.obj[this.sdl] = undefined;
			}
			return this;
		}

	};

	// At first call...
	if (!me.obj[sig]) {

		// define the function which will replace the original (the signal)
		var rpl = function() {

			var lst = [], arg = arguments, ret;

			// . recursively invoke every slot, directly or using a delay
			// (always try to get a returned value from signal or into-slot)
			var run = function() {
				var slt, bas = {};
				slt = lst.shift();
				if (slt) {
					// signal
					if (slt.adv === 0)  {
						if (slt.dly === false || slt.dly === undefined) {
							ret = slt.obj[slt.fct].apply(slt.obj, arg);
							run();
						} else {
							setTimeout(function() {
									ret = slt.obj[slt.fct].apply(slt.obj, arg);
									run(); }, slt.dly);
						}
					// into-slot
					} else if (slt.adv === 3) {
						// prepare arguments to be used into the slot
						bas.args = arg;
						bas.run = function() {
							return me.obj[sig].apply(me.obj, bas.args);
						};
						if (slt.dly === false || slt.dly === undefined) {
							ret = slt.obj[slt.fct].apply(slt.obj, [bas]);
							run();
						} else {
							setTimeout(function() {
									ret = slt.obj[slt.fct].apply(slt.obj, [bas]);
									run(); }, slt.dly);
						}
					// other slot type
					} else {
						if (slt.dly === false || slt.dly === undefined) {
							slt.obj[slt.fct].apply(slt.obj, arg);
							run();
						} else {
							setTimeout(function() {
									slt.obj[slt.fct].apply(slt.obj, arg);
									run(); }, slt.dly);
						}
					}
				}
			};

			// . build the ordered list of functions (signal and slots)
			var i, slt, l = me.obj[me.slt].length, fnd = null;

			// .. from-slots
			for (i=0; i<l; ++i) {
				slt = me.obj[me.slt][i];
				if (slt && slt.obj && slt.obj[slt.fct]) {
					if (slt.adv == 2) {
						lst.push(slt);
					} else if (slt.adv == 3) {
						fnd = slt;
					}
				}
			}

			// .. signal or into-slot
			lst.push(fnd || {obj:me.obj, fct:sig, dly:me.obj[me.sdl], adv:0});

			// .. to-slots
			for (i=0; i<l; ++i) {
				slt = me.obj[me.slt][i];
				if (slt && slt.adv == 1 && slt.obj && slt.obj[slt.fct]) {
					lst.push(slt);
				}
			}

			// . run the list
			run();
			return ret;

		};

		// run in a anonymous function to avoid memory leaks (cf. Andrew Tetlaw's
		// code)
		(function() {
			// define slots list
			me.obj[me.slt] = [];
			// backup the original function
			me.obj[sig] = me.obj[fct];
			// then replace it
			me.obj[fct] = rpl;
			// define delay
			me.obj[me.sdl] = dly;
		})();

	} else if (dly !== undefined) {
			me.obj[me.sdl] = dly;
	}

	// return me to be chained
	return me;

};

// jQuery plugins
// use 'chain' property which will carry the 'me' object to provide
// "chainability" within jQuery

$.fn.chain = function(obj, fct, dly) {
	this.chain = $.chain(obj, fct, dly);
	return this;
};

$.fn.from = function(obj, fct, dly) {
	if (this.chain) {
		this.chain.from.apply(this.chain, arguments);
	}
	return this;
};

$.fn.to = function(obj, fct, dly) {
	if (this.chain) {
		this.chain.to.apply(this.chain, arguments);
	}
	return this;
};

$.fn.into = function(obj, fct, dly) {
	if (this.chain) {
		this.chain.into.apply(this.chain, arguments);
	}
	return this;
};

$.fn.fromOnce = function(obj, fct, dly) {
	if (this.chain) {
		this.chain.fromOnce.apply(this.chain, arguments);
	}
	return this;
};

$.fn.toOnce = function(obj, fct, dly) {
	if (this.chain) {
		this.chain.toOnce.apply(this.chain, arguments);
	}
	return this;
};

$.fn.removeChain = function(obj, fct) {
	if (this.chain) {
		this.chain.remove.apply(this.chain, arguments);
	}
	return this;
};

$.fn.removeAllChain = function(obj, fct) {
	if (this.chain) {
		this.chain.removeAll.apply(this.chain, arguments);
	}
	return this;
};
