/*******************************************************************************
 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2008, 2009. All Rights Reserved.
 * 
 * Note to U.S. Government Users Restricted Rights:
 * Use, duplication or disclosure restricted by GSA ADP Schedule
 * Contract with IBM Corp.
 *******************************************************************************/
dojo.provide("jazz.ui.HoverPopup");

dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.registerModulePath("jazz.ui", "/_scripts");

(function() {
	
/*
 * PROVISIONAL: This API is considered provisional and may be changed or removed in the
 * future with minimal or no warning.
 */

dojo.declare("jazz.ui.HoverPopup", dijit._Widget, {
	/*
	 * The HoverPopup is a more sophisticated version of a basic tooltip. Must like a tooltip, it is associated
	 * to a given dom node and is shown when the user hovers their mouse over it. Its defining quality is that
	 * it remains open when the user moves their mouse from the hovered artifact and into the popup itself. This
	 * allows the content of the popup to be interactive, contain links, etc.
	 * 
	 * To attach a popup to a node, you simply instantiate a HoverPopup and pass a reference to the desired node
	 * to the contructor in the 'around' parameter. This will connect the necessary listeners to the node and
	 * will differ creation of the nodes for the popup itself until the user hovers the target.
	 * 
	 * around (node or string array): Array containing node references or node ids. Can also pass a single
	 * 		reference/id. This is the only required field for the constructor and will be deleted after
	 * 		it is read upon creation.
	 * 
	 * html (string): Can be used to create a basic popup that contains html. If specified, no createContents
	 * 		method is necessary.
	 * createContents (function): A function that will be called before the popup is opened for the first
	 * 		time. It will be passed one argument, which will be a reference to the popup widget. Consumers
	 * 		must return a valid dom node in this method. The default implementation returns a node populated
	 * 		based on the html attribute. If this method is overriden, the html property will be ignored. If
	 * 		defered population is required, a dom node is still required to be returned and its content can
	 *		be updated after the data is available by holding a reference to the node and repopulating it
	 *		later.
	 * onOpen (function): A function that will be called before the popup is opened. It will be passed two
	 * 		arguments. The first is a reference to the popup widget, the second is the time the popup last
	 * 		opened.
	 * onClose (function): A function that will be called before the popup is closed. It will be passed one
	 * 		argument, which will be a reference to the popup widget.
	 * 
	 * openDelay (int): Time (in milliseconds) node must be hovered before popup opens.
	 * closeDelay (int): Delay (in milliseconds) before popup closes after mouse out.
	 * 
	 * persist (boolean): If true, the popup will stay open when the mouse is over it (default true).
	 * enabled (boolean): If false, the popup will not display on hover (default true).
	 * programmatic (boolean): If true, the popup will only be opened/closed by programmatic calls (default
	 * 		false).
	 * orientation (char): Force the tip to open to the left/right of the node (valid values are "L" or "R").
	 */
	content: null, // reference to the dom node that will be displayed in the tooltip
	around: null,
	
	text: null,
	onOpen: null,
	onClose: null,
	
	openDelay: 600,
	closeDelay: 400,
	
	persist: true,
	enabled: true,
	programmatic: false,
	orientation: null,
	
	// PUBLIC API
	
	/*
	 * Can be called to programatically open the tooltip. This is only intended to be called programatically if
	 * the widget is configured to be used this way (this.programatic is set to true). Must supply the node to
	 * position the popup around (target). If no target is supplied, but the popup is only connected to one node,
	 * that node will be used as the target automatically.
	 */
	open: function(target) {
		if (!this.isEnabled())
			return;
		if (!target) {
			if (this._aroundNodes && this._aroundNodes.length == 1)
				target = this._aroundNodes[0];
			else
				return;
		}
		if (this._openTimer) {
			clearTimeout(this._openTimer);
			delete this._openTimer;
		}
		var m = jazz.ui._getMasterPopup();
		if (this.persist) {
			this._events = [];
			this._events.push(this.connect(m.domNode, "onmouseover", this._hoverPopup));
			this._events.push(this.connect(m.domNode, "onmouseout", this._mouseOutPopup));
			this._events.push(this.connect(m.domNode, "onhover", this._hoverPopup));
			this._events.push(this.connect(m.domNode, "onunhover", this._unHoverPopup));
		}
		this._isOpen = true;
		this._target = target;
		if (this._lastOpen == -1 && this.createContents && dojo.isFunction(this.createContents)) {
			var result = this.createContents(this);
			if (result)
				this.content = result;
		}
		if (this.onOpen && dojo.isFunction(this.onOpen))
			this.onOpen(this,this._lastOpen);
		this._lastOpen = new Date().getTime();
		if (!this.content) {
			console.error("HoverPopup: The createContents function must return a valid dom node.");
			this.close();
			return;
		}
		m.setContent(this.content);
		this._doManagedConnects();
		m.show(target, this);
		this._validate();
	},
	
	/*
	 * Can be called to programatically close the tooltip. This is only intended to be called programatically if
	 * the widget is configured to be used this way (this.programatic is set to true).
	 */
	close: function() {
		if (this._closeTimer) {
			clearTimeout(this._closeTimer);
			delete this._closeTimer;
		}
		if (this._validateTimeout) {
			clearTimeout(this._validateTimeout)
			delete this._validateTimeout;
		}
		if (this._events) {
			while (this._events.length > 0)
				this.disconnect(this._events.pop())
		}
		this._isOpen = false;
		this._target = null;
		this._doManagedDisconnects();
		jazz.ui._getMasterPopup().hide();
		if (this.onClose && dojo.isFunction(this.onClose))
			this.onClose(this);
	},
	
	/*
	 * Defines an event handler that will only be active while the popup is open. The widget is responsible for
	 * connecting/disconnecting the handler every time the popup is opened/closed. Takes the same parameters as
	 * dojo.connect.
	 */
	managedConnect: function(obj, event, context, method, dontFix) {
		var args = {arg1: obj, arg2: event, arg3: context, arg4: method, arg5: dontFix};
		this._mConnects.push(args);
		if (this._isOpen)
			this._doManagedConnect(args);
	},
	
	/*
	 * This is called by the widget before it responds to mouse over events on nodes the popup is connected to.
	 * The default implementation just returns the this.enabled flag. This function can be overriden if consumers
	 * want to use a more dynamic solution than manually toggling the enabled flag.
	 */
	isEnabled: function() {
		return this.enabled;
	},
		
	position: function(node) {
		var master = jazz.ui._getMasterPopup();
		var posAbs = dojo.coords(node, true);
		var posRel = dojo.coords(node, false);
		
		var v = dijit.getViewport();
		
		// horizontal
		if (this.orientation == "L" || this.orientation == "l")
			var left = true;
		else if (this.orientation == "R" || this.orientation == "r")
			var left = false;
		else {
			var l = posRel.x;
			var r = v.w - l - posRel.w;
			var left = l > r;
		}
		if (left) {
			// more room on left
			var x = v.w - posAbs.x + "px";
			master.domNode.style.right = x;
			dojo.addClass(master.wrapper, "left");
		}
		else {
			// more room on right
			var x = posAbs.x + posAbs.w + "px";
			master.domNode.style.left = x;
			dojo.addClass(master.wrapper, "right");
		}
		
		// vertical
		var t = posRel.y;
		var b = v.h - t - posRel.h;
		if (b > t) {
			t = posAbs.y;
			var d = 11 - (posAbs.h / 2);
			if (t > d) {
				var p = t - d;
				var y = 4;
			} else {
				var p = 0;
				var y = d - t;
			}
			master.domNode.style.top = p + "px";
			master.arrow.style.top = y + "px";
		}
		else {
			b = v.h - posAbs.y - posAbs.h;
			// viewport height minus document height
			var wt = window.top;
			var min = v.h - (wt.innerHeight && wt.scrollMaxY ? wt.dijit.getViewport().h + wt.scrollMaxY : wt.document.body.scrollHeight);
			var d = 15 - (posAbs.h / 2);
			if (b > min + d) {
				var p = b - d;
				var y = 7;
			} else {
				var p = min;
				var y = d - b;
			}
			master.domNode.style.bottom = p + "px";
			master.arrow.style.bottom = y + "px";
		}
	},

	/*
	 * Destroys the widget.
	 */
	destroy: function() {
		this.content = null;
		this._doManagedDisconnects();
		this.inherited(arguments);
	},
	
	// INTERNAL FUNCTIONS
	
	postCreate: function() {
		this._lastOpen = -1;
		this._aroundNodes = [];
		this._mConnects = [];
		this._mHandlers = [];
		this._attachArounds();
	},
	
	// default implementation of createContents
	// fills the tooltip with the html attribute if it has been provided
	createContents: function(tip) {
		var d = document.createElement("div");
		if (this.html)
			d.innerHTML = this.html;
		return d;
	},
	
	_attachArounds: function() {
		var a = this.around;
		if (dojo.isArray(a)) {
			for (var i = 0; i < a.length; i++)
				this._attachAround(a[i]);
		}
		else
			this._attachAround(a);
		delete this.around;
	},
	
	_attachAround: function(c) {
		if (!c)
			return;
		if (c.nodeType)
			var node = c;
		else
			var node = dojo.byId(c);
		if (node) {
			this._aroundNodes.push(node);
			this.connect(node, "onmouseover", this._hover);
			this.connect(node, "onmouseout", this._mouseOut);
			this.connect(node, "onhover", this._hover);
			this.connect(node, "onunhover", this._unHover);
		}
	},
	
	_doManagedConnects: function() {
		for (var i = 0; i < this._mConnects.length; i++) {
			this._doManagedConnect(this._mConnects[i]);
		}
	},
	
	_doManagedConnect: function(args) {
		this._mHandlers.push(dojo.connect(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5));
	},
	
	_doManagedDisconnects: function() {
		while (this._mHandlers.length > 0)
			dojo.disconnect(this._mHandlers.pop());
	},
	
	_validate: function() {
		if (dojo.isDescendant(this._target, document.body) == true) {
			this._validateTimeout = setTimeout(dojo.hitch(this,this._validate),500);
			return;
		}
		this.close();
	},
	
	_mouseOut: function(e) {
		// need to use == true since isDescendant returns -1 when it catches an exception
		// see http://trac.dojotoolkit.org/ticket/5464
		if(dojo.isDescendant(e.relatedTarget, e.target) == true){
			return;
		}
		this._unHover(e);
	},
	
	_hover: function(e) {
		if (this.programmatic)
			return;
		if(this._closeTimer){
			clearTimeout(this._closeTimer);
			delete this._closeTimer;
		}
		if (!this._isOpen && !this._openTimer) {
			var target = e.target;
			this._openTimer = setTimeout(dojo.hitch(this,function(){
				this.open(this._determineTarget(target));
			}), this.openDelay);
		}
	},
	
	_unHover: function(e) {
		if (this.programmatic)
			return;
		if(this._openTimer){
			clearTimeout(this._openTimer);
			delete this._openTimer;
		}
		this._closeTimer = setTimeout(dojo.hitch(this, function() {
			if (!this._isTipHovered) {
				this.close();
			}
		}), this.closeDelay)
	},
	
	_hoverPopup: function(e) {
		this._isTipHovered = true;
	},
	
	_mouseOutPopup: function(e) {
		if(dojo.isDescendant(e.relatedTarget, jazz.ui._getMasterPopup().domNode) == true){
			// false event; just moved from target to target child; ignore.
			return;
		}
		this._unHoverPopup(e);
	},
	
	_unHoverPopup: function(e) {
		this._isTipHovered = false;
		if (e.relatedTarget == this._target || dojo.isDescendant(e.relatedTarget, this._target) == true) {
			// moved from tooltip back to connectNode; ignore.
			return;
		}
		this._unHover(e);
	},
	
	_determineTarget: function(/*DomNode*/ target) {
		if (!target)
			return target;
		var newTarget = null;
		for (var i = 0; i < this._aroundNodes.length; i++) {
			if (!this._aroundNodes[i])
				continue;
			// if the target is this connect node, return it
			if (this._aroundNodes[i] == target)
				return target;
			// if the target is a descendant of this connect node
			// make sure we return the lowest level connect node that is a parent of the target
			// (e.g. as close to the target as possible)
			if (dojo.isDescendant(target, this._aroundNodes[i]) == true &&
					(!newTarget || dojo.isDescendant(this._aroundNodes[i], newTarget) == true)) {
				newTarget = this._aroundNodes[i];
			}
		}
		// return the suitable parent if one was found or the original target otherwise
		return newTarget ? newTarget : target;
	}
	
});

dojo.declare("jazz.ui.internal._MasterPopup", [dijit._Widget, dijit._Templated], {

	templatePath: dojo.moduleUrl("jazz.ui","templates/HoverPopup.html"),
	
	postCreate: function() {
		this.domNode.style.display = "none";
		dojo.body().appendChild(this.domNode);
	},
	
	show: function(target, source) {
		if (this._showing)
			this._showing.close();
		this._showing = source;
		if (source.customClass)
			dojo.addClass(this.domNode, source.customClass);
		this.domNode.style.visibility = "hidden";
		this.domNode.style.display = "block";
		try {
			source.position(target);
			this.content.scrollTop = 0;
			this.domNode.style.visibility = "";
		} catch (e) {
			// suppress errors in IE when the connected node is hidden, see work item 66523
			source.close();
			if (djConfig.isDebug)
				console.error(e);
		}
	},
	
	hide: function() {
		this.domNode.style.display = "none";
		this.domNode.style.right = "";
		this.domNode.style.left = "";
		this.domNode.style.top = "";
		this.domNode.style.bottom = "";
		this.arrow.style.top = "";
		this.arrow.style.bottom = "";
		this.arrow.style.display = "";
		this.content.maxWidth = "";
		this.content.maxHeight = "";
		dojo.removeClass(this.wrapper, "left");
		dojo.removeClass(this.wrapper, "right");
		if (this._showing && this._showing.customClass)
			dojo.removeClass(this.domNode, this._showing.customClass);
		this._showing = null;
	},
	
	setContent: function(newNode) {
		if (this.content.firstChild)
			this.content.replaceChild(newNode, this.content.firstChild);
		else
			this.content.appendChild(newNode);
	}
	
});

jazz.ui._getMasterPopup = function() {
	if (!jazz.ui._masterPopup)
		jazz.ui._masterPopup = new jazz.ui.internal._MasterPopup();
	return jazz.ui._masterPopup;
}

})();
