/*
 * 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-2010 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 Hover.px and Hover.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 Hover(parent, hover) {
	this.parent = parent;
	this.hover = this.node = 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(Hover.getInitialDelay,
			function() {
				if (Hover.disabled || MousePopup.active) return false;
				if (Self.movetarget) {
					Self.target = Self.getTarget(Self.movetarget);
					Self.movetarget = null;
				}
				if (!Self.target || Self.onShow(Self.target) === false)
					return false;
                hover.style.visibility = "hidden";
                hover.style.display = "";
				setTimeout(function() {
                    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);
                    hover.style.visibility = "";
    				Self.hover.style.left = x + "px";
	                Self.hover.style.top = y + "px";
					Self.show();
                }, 0);
                Self.displayed = true;
				Hover.current = Self;
			}))),
		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 false;
					Self.movetarget = null;
				} catch (ex) {}
			})),
		Condition("final",
			function() { return Self.movetarget ? 0 : 1; },
			["initial", "start"],
			Timeout(Hover.getFinalDelay, hide)),
		Trans("hover", "display", Event(parent, "mouseover", emptyFunction)),
		Trans("hover", "final", Event(hover, "mouseout", function(e) {
			try {
				for (var n = e.relatedTarget || e.toElement; n; n = n.parentNode)
					if (n == hover) return false;
			} catch (ex) {}
		}))
	]);

	function hide() {
		if (Self.onHide(Self.target) === false) return false;
		Self.hide();
		Self.displayed = false;
		Hover.current = null;
	};
	
	/**
	 * Enables the hover and hides it if necessary.
	 */
	this.enable = 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;
	}
}

/**
 * @private
 */
Hover.disabled = false;

/**
 * Temporarily disabled all hovers. Must be followed by a call to
 * Hover.enable().
 */
Hover.disable = function() { Hover.disabled++; }

/**
 * Reenables hovers after a call to Hover.disabled().
 */
Hover.enable = function() { Hover.disabled--; }

Hover.prototype = extend(Popup, {});

/**
 * 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).
 */
Hover.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 type 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.
 */
Hover.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 type 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.
 */
Hover.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.
 */
Hover.prototype.getTarget = function(node) {
    try {
    	while (node) {
    		var p = node.parentNode;
    		if (p == this.parent) return node;
    		node = p;
    	}
    } catch (e) {
    	// Stupid Firefox likes to pass the scroll bar as event.relatedTarget
        // and then complain about JavaScript not having the permission
        // to access it.
    }
    return null;
};

/**
 * Couples the size of the hover to the size of a DOM node.
 * @param {DOM node} node the DOM node which has the same size as the hover.
 */
Hover.prototype.setSize = function(node) {
    this.width = function() { return node.offsetWidth; };
    this.height = function() { return node.offsetHeight; };
};

/**
 * The delay in milliseconds, during which the mouse pointer must hover over
 * the same point before a hover is displayed.
 */
Hover.initial_delay = 1000;
Hover.getInitialDelay = function () { return Hover.initial_delay; }
/**
 * The delay in milliseconds, before a hover is hidden. This gives the user
 * an opportunity to move the mouse into the hover to interact with it.
 */
Hover.final_delay = 400;
Hover.getFinalDelay = function () { return Hover.final_delay; }
/**
 * 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.
 */
Hover.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.
 */
Hover.px = function(px) {
    return function() { return px; };
};
