/*
 * All content on this website (including text, images, source
 * code and any other original works), unless otherwise noted,
 * is licensed under a Creative Commons License.
 * 
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * 
 * Copyright (C) 2004-2007 Open-Xchange, Inc.
 * Mail: info@open-xchange.com 
 * 
 * @author Viktor Pracht <Viktor.Pracht@open-xchange.org>
 */

/**
 * Creates a finite state machine (FSM).
 * The FSM is defines by an array of transition objects. A transition object can
 * be created and modifies by the static methods of the class FSM.
 * @param {String} startState The initial state of the FSM.
 * @param {Array} transitions An array with all transitions of the FSM.
 * Each element of the array is a transition object, which can be created and
 * modified by the static methods of the class FSM.
 * @type Function
 * @return An object which has the following methods:
 * <dl><dt>enable</dt><dd>takes no parameters and sets up all event handlers
 * for the current state.</dd>
 * <dt>disable</dt><dd>takes no parameters and removes all event handlers for
 * the current state.</dd>
 * <dt>enter</dt><dd>takes a state name as parameter and enters this state. This
 * method is less efficient than FSM transactions.</dd>
 * <dt>reset</dt><dd>takes no parameters and enters the start state.</dd>
 * Methods of this object should not be called from a transition callback.
 */
function FSM(startState, transitions) {
	var states = {};
	for (var i in transitions) {
		var t = transitions[i];
		if (!states[t.from]) states[t.from] = {};
		t.from = states[t.from];
		for (var j in t.to) {
			var s = t.to[j];
			if (!states[s]) states[s] = {};
			t.to[j] = states[s];
		}
		t.from[i] = t;
		t.f = (function(t) {
			return function(e) {
				if (t.callback(e) === false) return;
				var ix = t.condition();
				var actions = t.actions[ix];
				for (var i = 0; i < actions.length; i++) actions[i]();
				current = t.to[ix];
			};
		})(t);
	}
	for (var i in transitions) {
		var t = transitions[i];
		t.actions = new Array(t.to.length);
		for (var i = 0; i < t.to.length; i++) {
			var actions = t.actions[i] = [];
			var to = t.to[i];
			if (t.from == to) {
				for (var j in t.from)
					if (t.from[j].reset) actions.push(t.from[j].reset);
			} else {
				for (var j in t.from) actions.push(t.from[j].disable);
				for (var j in to) actions.push(to[j].enable);
			}
		}
	}
	var current = states[startState];
	var enabled = false;
	var fsm = {
		enable: function() {
			if (!enabled) for (var i in current) current[i].enable();
			enabled = true;
		},
		disable: function() {
			if (enabled) for (var i in current) current[i].disable();
			enabled = false;
		},
		enter: function(state) {
			this.disable();
			current = states[state];
			this.enable();
		},
		reset : function() { this.enter(startState); }
	};
	fsm.enable();
	return fsm;
}

/**
 * Creates a transition which is triggered by a DOM event.
 * When then FSM enters the source state of the transition, an event handler is
 * attached to the specified node with addDOMEvent, and is removed with
 * removeDOMEvent when FSM leaves the source state.
 * @param {Object} node The DOM node to which the event handler is attached.
 * @param {String} name The name of the DOM event, without the leading "on".
 * @param {Function} callback The event handler which is called when
 * the transition fires. If the callback returns false, the transition does not
 * fire. No type conversion is performed, so only a real Boolean value false
 * will cancel the transaction. Values of any other type (in particular,
 * the default of undefined when there is no or an empty return statement) are
 * interpreted as true.
 * @type Object
 * @return The newly created transition object.
 */
FSM.Event = function(node, name, callback) {
	var Self = {
		callback: callback,
		enable: function() {
			addDOMEvent(node, name, Self.f);
		},
		disable: function() {
			removeDOMEvent(node, name, Self.f);
		}
	};
	return Self;
};

/**
 * Creates a transition which fires after a timeout.
 * When the FSM enters the source state of the transition, a timeout is started
 * with setTimeout and is cancelled with clearTimeout when the FSM leaves the
 * source state. Transitions which do not leave the source state of this
 * transition do not reset the timer by default (see FSM.Reset for that).
 * @param {Number} delay The timeout in milliseconds.
 * @param {Function} callback A callback function which is called after
 * the timeout. If the callback returns false, the transition does not
 * fire. No type conversion is performed, so only a real Boolean value false
 * will cancel the transaction. Values of any other type (in particular,
 * the default of undefined when there is no or an empty return statement) are
 * interpreted as true.
 * other type which merely converts to false), the transition does not fire.
 * @type Object
 * @return The newly created transition object.
 */
FSM.Timeout = function(delay, callback) {
	var timer = null;
	var Self = {
		callback: callback,
		enable: function() {
			if (timer !== null) clearTimeout(timer);
			timer = setTimeout(Self.f, delay);
		},
		disable: function() {
			if (timer !== null) clearTimeout(timer);
			timer = null;
		}
	};
	return Self;
};

/**
 * Configures a transition object with a source and a destination state.
 * Every transition object must be modified by this function to specify
 * the source and destination states, before being passed to FSM().
 * The modification occurs in-place, so that a transition object can not be
 * reused for multiple transitions.
 * @param {String} from The source state of the transition. The transition can
 * only fire when the FSM is in this state.
 * @param {String} to The destination state of the transition. The FSM enters
 * this state when the transition fires.
 * @param {Object} transition An unmodified transition object which does not
 * have any states associated with it yet.
 * @type Object
 * @return The same transition object as passed in the last parameter, but now
 * it is associated with a source and a destination state and can be passed to
 * FSM().
 */
FSM.Trans = function(from, to, transition) {
	transition.from = from;
	transition.condition = FSM.zero;
	transition.to = [to];
	return transition;
};

/**
 * @private
 */
FSM.zero = function() { return 0; };

/**
 * Configures a transition object with a source state and a list of possible
 * destination states. The actual destination state is selected by the specified
 * callback when the transition fires.
 * @param {String} from The source state of the transition. The transition can
 * only fire when the FSM is in this state.
 * @param {Function} callback A callback function which is called when
 * the transaction fires and returns an index into the array of possible
 * destination states to select the next state of the FSM.
 * @param {Array} to An array with possible destination states as strings.
 * The FSM enters one of these states when the transaction fires. The actual
 * state is selected by the specified callback function.
 * @param {Object} transition An unmodified transition object which does not
 * have any states associated with it yet.
 * @type Object
 * @return The same transition object as passed in the last parameter, but now
 * it is associate with a source state and a list of destination states, and can
 * be passed to FSM().
 */
FSM.Condition = function(from, condition, to, transition) {
	transition.from = from;
	transition.condition = condition;
	transition.to = to;
	return transition;
}

/**
 * Modifies a transition to be reset when another transition fires but does not
 * change the FSM state.
 * By default, all transition objects do nothing when a transition fires and
 * the new FSM state is the same as the old state. In the case that a transition
 * must be reset (e. g. restarting a timeout transition), the transition which
 * needs restarting should be modified by passing it to this function.
 * The relative order of modifications of the same transition object is not
 * relevant.
 * @param {Object} transition A transition object which should be modified.
 * @type Object
 * @return The same object as passed in the first parameter, but now it will be
 * reset whenever other transitions fire without changing the FSM state.
 */
FSM.Reset = function(transition) {
	transition.reset = function() {
		transition.disable();
		transition.enable();
	};
	return transition;
};
