/**
 * @version 0.5.0, 2009-08-28
 * @author Brett Pontarelli <brett@pontarelli.com>
 * @copyright Copyright (C) 2009, Brett Pontarelli
 *
 * Dependencies:
 *   Prototype
 *   Scriptaculous
 *
 * Setup and use of EffectMenu requires some specifc css styling:
 *   1. Wrap the entire list in a relatively/absolutely postioned div
 *   2. Hide all elements using "visibility: hidden;"
 *   3. Float/position absolute all hidden elements (they will be re-positioned by EffectMenu)
 */
var EffectMenu = Class.create({
	/* Options */
	_firstLevelBelow: false,  // Position of first level of sub-menus below main menu
	_displayAction: 'grow',  // Action to use to display sub-menu
	_includeActive: false,  // Include class active menus in sub-menu display/hide
	_effectDuration: 0.5,  // Time in seconds for menu effect
	_hidePause: 1000,  // Time in milliseconds before hiding sub-menu
	_subElementType: 'ul',  // Sub-menu element type e.g. ul or div
	_firstLevelOffsetX: 0,  // Additional x-offset for 1st level
	_firstLevelOffsetY: 0,  // Additional y-offset for 1st level
	
	/* Private Variables */
	_rootId: null,  /* id of root container */
	_hideTime: null,  /* Time to wait before hiding */
	_visibleSubMenus: new Array(),  /* array of open sub-menus */
	_defaultSubMenuStyles: new Array(),  /* array of default sub-menu styles */

	/* Methods */
	/**
	 * Constructor function
	 *
	 * @param	string	containerId
	 * @param	object	options
	 *
	 * Options:
	 *   bool firstLevelBelow = position first level below main level
	 *   int displayAction = use one of defined action constants
	 *   bool includeActive = Include LIs with class "active" in the events handling (default: false)
	 *   float hidePause = seconds before hide (default: 1.0)
	 *
	 * Example:
	 *   var mainMenu = new EffectMenu('containerDiv', {
	 *     'firstLevelBelow': true,
	 *     'hidePause': 0.5,
	 *     'wrapperClass': 'mega'
	 *     });
	 */
	initialize: function(containerId, options) {
		/* Save the containerId in _rootId for later setup by _doInit */
		this._rootId = containerId;
		/* Populate private variables from options */
		if (options) {
			$H(options).each(function(opt) {
				switch (opt.key) {
					case 'effectDuration': this._effectDuration = opt.value; break;
					case 'firstLevelBelow': this._firstLevelBelow = opt.value; break;
					case 'displayAction': this._displayAction = opt.value.toLowerCase(); break;
					case 'includeActive': this._includeActive = opt.value; break;
					case 'hidePause': this._hidePause = opt.value * 1000; break;
					case 'subElementType': this._subElementType = opt.value; break;
					case 'firstLevelOffsetX': this._firstLevelOffsetX = opt.value; break;
					case 'firstLevelOffsetY': this._firstLevelOffsetY = opt.value; break;
					}
				}.bind(this));
			}
		if ('grow none'.indexOf(this._displayAction) < 0) {
			this._displayAction = 'none';
			}

		/* Initialize now or set a listener to setup when dom loaded */
		if (document.loaded == true) {
			this._doInit();
			}
		else {
			document.observe('dom:loaded', this._doInit.bind(this));
			}
		},  // function initialize

	/**
	 * Private initilization function.
	 * Takes no arguments, since options and document should already be loaded.
	 */
	_doInit: function() {
		/* if mainMenuElm is not a UL get the first UL decendant (all other are 
		 * ignored). This allows passing a div id, since sometimes there is no
		 * id given to the ul of a menu.
		 */
		var mainMenuElm = $(this._rootId);
		if (mainMenuElm.type != 'ul') {
			mainMenuElm = mainMenuElm.down('ul');
			}
		this._addEvents(mainMenuElm);
		},  // function _doInit

	/**
	 * Attach events to LIs with hidden children (i.e. sub-menus)
	 *
	 * @param	Element  mainMenuUl
	 *
	 * All LIs within mainMenuUl are examined to determine if they have
	 * children. Event listners are attached to LIs with sub-menus.
	 * Hidden/visibile sub-menus are determined by class 
	 * "active/inactive" (see includeActive).
	 */
	_addEvents: function(mainMenuUl) {
		/* Find all li nodes */
		var menuId = 1;
		var effectiveLevel = 1;
		var trueLevel = 1;
		var liElement = mainMenuUl.down('li');
		var subMenu = mainMenuUl;
		var pos = [0,0];
		var styleOpts = {};
		while (liElement != undefined) {
			/* The following (includeActive == li.hasClassName('active'))
			 * is the same as 
			 *   include active is true and li class is active or
			 *   include active is false and li class is not active
			 * If li has a ul it has a sub-menu.
			 * If both are true then we can add an event to the element
			 */
			subMenu = liElement.down(this._subElementType);
			if ((this._includeActive == liElement.hasClassName('active')) && 
				(subMenu != undefined)) {
				/* Add show event to li */
				liElement.observe('mouseover', this._showSubMenu.bindAsEventListener(this, liElement, effectiveLevel));
				if (effectiveLevel == 1) {
					/* Add Hide event to li in level 1 only, since sub-sub-menus are all contained by level 1 li */
					liElement.observe('mouseout', this._hideSubMenu.bindAsEventListener(this, liElement, 1, false));
					}
				/* Give the submenu the next id */
				subMenu.id = 'EffectMenu' + menuId;
				menuId++;
				/* Position sub-menu to an absolute based on level and type */
				leftOffset = liElement.positionedOffset().left;
				topOffset = liElement.positionedOffset().top;
				if (effectiveLevel == 1) {
					// Add first level user offsets
					leftOffset += this._firstLevelOffsetX;
					topOffset += this._firstLevelOffsetY;
					// Position based to right or below
					if (this._firstLevelBelow) {
						// Add height for below
						topOffset += liElement.getHeight();
						}
					else {
						leftOffset += liElement.getWidth();
						// Add width for not-below
						}
					}
				else {
					// effectiveLevel > 1 so position sub-menu to right
					leftOffset += liElement.getWidth();
					}
				
				styleOpts = {position: 'absolute',
					left: leftOffset + 'px',
					top: topOffset + 'px'
					};
				subMenu.setStyle(styleOpts);
				}
			else {
				/* For node with no children, hide sub-menus below level */
				liElement.observe('mouseover', this._hideSubMenu.bindAsEventListener(this, liElement, effectiveLevel, true));
				}
				
			/* Get next li */
			if (liElement.down('li') != undefined) {
				/* If include active, then treat sub-menus as level 1 */
				trueLevel++;
				if (this._includeActive == liElement.hasClassName('active')) {
					effectiveLevel++;
					}
				/* If there are children go down to next level */
				liElement = liElement.down('li');
				}
			else {
				/* No children, then get next li at this level */
				if (liElement.next('li') != undefined) {
					liElement = liElement.next('li');
					}
				else {
					/* No more li at this level; If level 1, quit;
					 * If not level 1, go back up and get next
					 */
					trueLevel--;
					if (trueLevel == 0) {
						liElement = undefined;
						}
					else {
						/* Go back up, if li is active dec level if include active */
						liElement = liElement.up('li');
						if (liElement && (this._includeActive == liElement.hasClassName('active'))) {
							effectiveLevel--;
							}
						/* Get next */
						liElement = liElement.next('li');
						}
					}
				}
			} // while (liElement != undefined)
		/* First Hide then visibility=visible, this ensures the element
		 * remains hidden, but allows us to use show/hide later on.
		 * This must be done last since display:none => t,l,w,h = zero
		 */
		if (menuId > 1) {
			this._defaultSubMenuStyles = 	$R(1, menuId-1).inject(new Array(), 
				function(accArray, x, index) {
					var menuId = 'EffectMenu' + x;
					var menuElt = $(menuId);
					var opts = {
						'id': menuId, 
						'style': {
							top: menuElt.style.top,
							left: menuElt.style.left,
							height: menuElt.style.height,
							width: menuElt.style.width
							}};
					accArray.push(opts);
					menuElt.hide().setStyle({visibility: 'visible'});
					return accArray;
					});
			}
	},  // function _addEvents

	/**
	 * Clears the hide timeout if there is one.
	 */
	_clearHideTimeout: function() {
		if (this._hideTimeout) {
			window.clearTimeout(this._hideTimeout);
			}
		this._hideTimeout = null;
		},  // function _clearHideTimeout

	/**
	 * Update the effect Queue.
	 *
	 * @param string  menuId
	 * @param string  direction
	 *
   * Several problems arise when one effect is working and another is created.
   *   1. The second effect considers the current state (height, width, 
   * font-size, etc) of the element as the reset values so after execution 
   * (e.g. hide) those values are reset. This can cause serious munging of 
   * the elements style. To circumvent this running effects are canceled 
   * before new ones are started and every effect is given an after finish 
   * function that resets the inline style correctly. 
   *   2. Occasionally one effect is started before the first effect is queued. 
   * This can cause the show to be queued before the hide and thus leave it 
   * hanging on the screen. Using "limit: 1" prevents this from happening 
	 *
	 * For the future: compute the stage of the canceled event and start
	 * the new one at that stage.  This equals a smoother reversal.
	 */
	_updateEffectQueue: function(menuId, direction) {
		if (this._displayAction = 'none') {
			if (direction) $(menuId).show(); else $(menuId).hide();
			}
		else {
			// cancel all effects for scope menuId
			var q = Effect.Queues.get(menuId);
			q.each(function(eff, percent){ eff.cancel(); });

			// Build effect and options
			var effectName = '';
			var effectOptions = {};
			switch (this._displayAction) {
				case 'drop-down':
					// Not yet implemented
					break;
				case 'grow':
					effectName = $w('grow shrink')[direction];
					effectOptions = {direction: 'top-left'};
					break;
				case 'slide-out':
					// Not yet implemented
					break;
				}
			effectOptions = Object.extend({
				duration: this._effectDuration,
				queue: { position: 'end', scope: menuId, limit: 1 },
				afterFinish: function(eff) {
					var elt = eff.element ? eff.element : eff.effects[0].element;
					this._restoreElementStyle(elt);
					}.bind(this)
				}, effectOptions);
		
			// Create new effect and set after to call us when done
			var newEffect = $(menuId).visualEffect(effectName, effectOptions);
			}
		},  // function _updateEffectQueue
	
	/**
	 * Restores style of element
	 *
	 * @param	Element	 elt
	 */
	_restoreElementStyle: function(elt) {
		var defaultStyle = this._defaultSubMenuStyles.find(
			function(s){ return s.id == elt.id; }).style;
		defaultStyle = Object.extend({
			'display': elt.getStyle('display'),
			'position': 'absolute',
			'visibility': 'visible',
			'font-size': '100%'
			}, defaultStyle);
		elt.removeAttribute('style');  // clear all styles
		elt.setStyle(defaultStyle);
		},  // function _restoreElementStyle
		
	/**
	 * Event handler for showing a sub-menu.
	 *
	 * @param	Event	   evt
	 * @param	Element  eventLi
	 * @param	int      level
	 */
	_showSubMenu: function(evt, eventLi, level) {
		/* Hide all sub-menus except the one we are about to open */
		this._doHideSubMenu(level, eventLi.down(this._subElementType));
		this._doShowSubMenu(eventLi, level);
		},  // function _showSubMenu

	/**
	 * Shows the sub-menu.
	 *
	 * @param	Element  eventLi
	 * @param	int      level
	 */
	_doShowSubMenu: function(eventLi, level) {
	  /* Clear timeout if exists */
		this._clearHideTimeout();
		/* Only open sub-menu if not already visible */		
		var subMenuElm = eventLi.down(this._subElementType);  // Get sub-menu ul
		if (this._visibleSubMenus.find(function(v) {return v.id === subMenuElm.id }) == undefined) {
			this._visibleSubMenus.push({'level': level, 'subMenuElm': subMenuElm, 'id': subMenuElm.id});
			/* Clear queue and show */
			this._updateEffectQueue(subMenuElm.id, 1);
			}
		},  // function _doShowSubMenu

	/**
	 * Event handler for hiding sub-menus.
	 *
	 * @param	Event	  evt
	 * @param Element eventLi
	 * @param	int		  level
	 * @param bool    immediately
	 *
	 * The immediately flag is used to control setting of the hide timer.
	 * When leaving one li and entering another, open sub-menus are closed
	 * immediately.  Leaving the menu into the rest of the document uses
	 * a timer to hide all open menus.
	 */
	_hideSubMenu: function(evt, eventLi, level, immediately) {
		this._clearHideTimeout();  /* Clear timeout if exists */
				
		if (this._hidePause <=0 || immediately) {
			/* If there is no pause hide sub-menus directly */
			this._doHideSubMenu(level);
			}
		else {
			/* if there is a pasue set a timeout */
			this._hideTimeout = window.setTimeout(
				this._doHideSubMenu.bind(this, level),
				this._hidePause
				);
			}
		},  // function _hideSubMenu

	/**
	 * Hide sub-menus from level down.
	 *
	 * @param	int     level
	 * @param	Element excludeUl
	 *
	 * Any menu below the active level is closed.  But, if we are
	 * planning to open a sub-menu right after this we exclude
	 * it from the closing (passed as excludeUl).
	 */
	_doHideSubMenu: function(level, excludeUl) {
		this._visibleSubMenus = this._visibleSubMenus.findAll(function(vsm) {
			/* Hide sub-menu >= level */
			if (vsm.level >= level) {
				/* Hide if not excludeUl */
				var tmp = (excludeUl == undefined ? '' : excludeUl.id);
				if (vsm.id != (excludeUl == undefined ? '' : excludeUl.id)) {
					this._updateEffectQueue(vsm.subMenuElm.id, 0);
					return false;  // remove sub-menu from array
					}
				}
			return true;  // keep sub-menu in array
			}.bind(this));
		}  // function _doHideSubMenu
	}); // class EffectMenu