/*
 * 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>
 */

/**
 * Hover timing control for multiple object with a common container.
 * A new hover is disabled by default and must be enabled with the method
 * enable(). Before enabling, the width and height methods of the new object
 * must be overwritten (@see Hover2.px and Hover2.em).
 * @param {Object} parent The DOM node which contains all the nodes that need
 * a hover.
 * @param {Object} hover The DOM node of the hover. The node will be absolutely
 * positioned relative to the document body.
 */
function Hover2(parent, hover) {
	this.parent = parent;
	this.hover = hover;
	var Self = this;
	with (FSM) this.fsm = FSM("start", [
		Trans("start", "initial", Event(parent, "mousemove", parentmove)),
		Trans("initial", "initial", Event(parent, "mousemove", parentmove)),
		Trans("initial", "start", Event(parent, "mouseout", parentout)),
		Trans("initial", "display", Reset(Timeout(Hover2.initial_delay,
			function() {
				if (Self.movetarget) {
					Self.target = Self.getTarget(Self.movetarget);
					Self.movetarget = null;
				}
				if (!Self.target || Self.onShow(Self.target) === false)
					return false;
				var width = Self.width();
				var height = Self.height();
				var x = Self.clientX + 16;
				if (x + width > body.clientWidth)
					x = Math.max(0, Self.clientX - 16 - width);
				var y = Self.clientY + 16;
				if (y + height > body.clientHeight)
					y = Math.max(0, Self.clientY - 16 - height);
				displayAt(hover, x + "px", y + "px");
				Self.displayed = true;
			}))),
		Trans("display", "final", Event(parent, "mouseout", parentout)),
		Trans("display", "hover", Event(hover, "mouseover", emptyFunction)),
		Trans("final", "hover", Event(hover, "mouseover", emptyFunction)),
		Trans("final", "final", Event(parent, "mousemove", parentmove)),
		Trans("final", "final", Event(parent, "mouseout",
			function(e) {
				try {
					for (var n = e.relatedTarget || e.toElement; n; n = n.parentNode)
						if (n == parent) return;
						Self.movetarget = null;
				} catch (ex) {}
			})),
		Condition("final",
			function() { return Self.movetarget ? 0 : 1; },
			["initial", "start"],
			Timeout(Hover2.final_delay, hide)),
		Trans("hover", "display", Event(parent, "mouseover", emptyFunction)),
		Trans("hover", "final", Event(hover, "mouseout", function(e) {
			for (var n = e.relatedTarget || e.toElement; n; n = n.parentNode)
				if (n == hover) return;
		}))
	]);

	function hide() {
		if (Self.onHide(Self.target) === false) return false;
		fade(hover, 100, 0, 50, 30);
		Self.displayed = false;
	};
	
	/**
	 * Enables the hover and hides it if necessary.
	 */
	this.enable = this.hide = function() {
		if (this.displayed) hide();
		this.fsm.reset();
	};

	/**
	 * Disables and hides the hover.
	 */
	this.disable = function() {
		if (this.displayed) hide();
		this.fsm.disable();
	};
	
	function parentmove(e) {
		Self.clientX = e.clientX;
		Self.clientY = e.clientY;
		Self.movetarget = e.target || e.srcElement;
	}
	
	function parentout(e) {
		if (!("target" in Self)) Self.target = Self.getTarget(Self.movetarget);
		return Self.getTarget(e.relatedTarget || e.toElement) != Self.target;
	}
}

/**
 * Adjusts the position of an already displayed hover when its size changed.
 * The actual size of the hover is measured, therefore (at least in IE) this
 * function should be called after any manual layout changes take effect
 * (e. g. after a zero timeout).
 */
Hover2.prototype.sizeChanged = function() {
	if (this.hover.offsetTop + this.hover.offsetHeight > body.clientHeight)
		this.hover.style.top = Math.max(0, body.clientHeight - this.hover.offsetHeight) + "px";
	if (this.hover.offsetLeft + this.hover.offsetWidth > body.clientWidth)
		this.hover.style.left = Math.max(0, body.clientWidth - this.hover.offsetWidth) + "px";
}

/**
 * A callback which is called before the hover is shown.
 * @param {Object} node The return value of the getTarget method when called on
 * the DOM node (or one of its descendants) for which the hover is displayed.
 * By default, it is the target DOM node itself. 
 * @return false to prevent the displaying of the hover. No tpye conversion is
 * performed, so only a real Boolean value false will cancel the displaying.
 * Values of any other type (in particular, the default of undefined when there
 * is no or an empty return statement) are interpreted as true.
 */
Hover2.prototype.onShow = emptyFunction;

/**
 * A callback which is called before the hover is hidden.
 * @param {Object} node The return value of the getTarget method when called on
 * the DOM node (or one of its descendants) for which the hover is displayed.
 * By default, it is the target DOM node itself. 
 * @return false to prevent the hiding of the hover. No tpye conversion is
 * performed, so only a real Boolean value false will cancel the hiding. Values
 * of any other type (in particular, the default of undefined when there is no
 * or an empty return statement) are interpreted as true.
 */
Hover2.prototype.onHide = emptyFunction;

/**
 * A calback which is called to determine the actual hover target from one of
 * its descendants. This function may be called relatively often, so it should
 * return quickly.
 * @param {Object} node One of the direct or indirect descendants of the hover
 * container.
 * @type Object
 * @return The target DOM node or null. Overwriting methods can return pretty
 * much anything, as long as two calls on descendants of the same target (or
 * the target itself) return values which compare as equal when using ==, and
 * two calls on descendants of different targets return values which are not
 * equal. When called on anything but a target or a descendant of a target,
 * an overriding method should return something that converts to false and is
 * not equal to any value returned for a valid target.
 */
Hover2.prototype.getTarget = function(node) {
	while (node) {
		var p = node.parentNode;
		if (p == this.parent) return node;
		node = p;
	}
	return null;
};

/**
 * The delay in milliseconds, during which the mouse pointer must hover over
 * the same point before a hover is displayed.
 */
Hover2.initial_delay = 500;

/**
 * The delay in milliseconds, before  ahover is hidden. This gives the user
 * an opportunity to move the mouse into the hover to interact with it.
 */
Hover2.final_delay = 200;

/**
 * Returns a helper function which converts a constant em size to pixel
 * depending on the current value of pxPerEm. This is useful for overriding
 * the width and height methods when the hover has a constant em size.
 * @param {Number} em The size of the hover in em.
 * @type Function
 * @return A function without parameters which returns the size in px.
 */
Hover2.em = function(em) { return function() { return pxPerEm * em; }; };

/**
 * Returns a helper function which returns a constant.
 * This is useful for overriding the width and height methods when the hover has
 * a constant pixel size.
 * @param {Number} px The size of the hover in px.
 * @type Function
 * @return A function which returns the size in px.
 */
Hover2.px = function(px) { return function() { return px; }; };

