/**
 * @class       Skii.ContextualMenu
 * @version     0.1
 * @author      Charles Demers - @charles_demers
 *
 * @requires    [core.js, jquery-1.5+]
 */

Skii.ContextualMenu = function(opts) {
	
	// Props
	this.classPrefix = 'skii-contextualmenu';
	this.selectableOptionSelector = 'li:not(.' + this.classPrefix + '-non-selectable)';
	
	this.keyDownInterval = null;
	this.keyDownIntervalDelay = 40;
	this.keyDownTimeout = null;
	this.keyDownThreshold = 200;
	this.keyDownRepeat = false;
	
	this.canSelect = true;
	this.selectionThreshold = 300;
	
	// Metrics
	this.currentOffsetX = 0;
	this.currentOffsetY = 0;
	this.totalHeight = 0;
	
	// Option
	this.shouldFitInViewport = (typeof opts.shouldFitInViewport) ? opts.shouldFitInViewport : true;
	this.isFixed = (typeof opts.isFixed == 'boolean') ? opts.isFixed : true;
	this.canHaveScrollbar = opts.canHaveScrollbar || false;
	this.canBeOnTop = opts.canBeOnTop || false;
	
	this.isAnimated = opts.isAnimated || false;
	
	// States
	this.isVisible = false;
	this.hasScrollbar = false;
	this.shouldUpdateOptionList = true;
	this.shouldUpdateBrowserMetrics = true;
	
	// Elements
	this.$selectedOption = $(0);
	this.$focusedOption = $(0);
	this.$lastFocusedElement = $(0);
	
	this.$list = $('<ul>', {
		'class': this.classPrefix,
		css: {
			minWidth: (Skii.typeOf(opts.width) != 'undefined' ? opts.width : 0)
		}
	});
	this.$list.appendTo('body');
	this.rebuildOptions(opts.options);
	
	this._$options = this.getSelectableOptions();
	
	$(Skii.ContextualMenu.Notifier).trigger('skii.contextualmenu.creation', [this]);
};

// Only use internally
// This is just an observer that notifies the instances
// that they should update their browser metrics
(function() {

	Skii.ContextualMenu.Notifier = {
		instances: {},
		notify: function() {
			var instances = Skii.ContextualMenu.Notifier.instances;
			for (var guid in instances) {
				instances[guid].shouldUpdateBrowserMetrics = true;
			}
		}
	};
	
	$(Skii.ContextualMenu.Notifier).bind('skii.contextualmenu.creation', function(e, instance) {
		var guid = Skii.guid();
		Skii.ContextualMenu.Notifier.instances[guid] = instance;
		instance.guid = guid;
	});
	
	$(Skii.ContextualMenu.Notifier).bind('skii.contextualmenu.destruction', function(e, instance) {
		delete Skii.ContextualMenu.Notifier.instances[instance.guid];
	});
	
	// Check for viewport resize and scroll to notify instances 
	$(window).bind('resize', Skii.ContextualMenu.Notifier.notify);
	$(document).bind('scroll', Skii.ContextualMenu.Notifier.notify);
	
})();


Skii.ContextualMenu.prototype = {
	
	_onKeyUp: function(e) {
		if (this.isVisible) {
			
			var direction;
			
			switch (e.which) {
				case Skii.KeyCodes.ENTER:
				case Skii.KeyCodes.SPACE:
					e.preventDefault();
					this.selectOption(e.currentTarget);
					break;

				case Skii.KeyCodes.ESCAPE:
					e.preventDefault();
					this.hide();
					break;

				case Skii.KeyCodes.ARROW_UP:
				case Skii.KeyCodes.ARROW_DOWN:
					
					e.preventDefault();
					clearInterval(this.keyDownInterval);
					this.keyDownInterval = null;
					
					clearTimeout(this.keyDownTimeout);
					this.keyDownTimeout = null;
					
					this.$list.delay(this.keyDownIntervalDelay).delegate(this.selectableOptionSelector, 'mouseover', $.proxy(this, '_focusOption'));
					
					this.keyDownRepeat = false;
					
					break;
					
				case Skii.KeyCodes.PAGE_UP:
				case Skii.KeyCodes.PAGE_DOWN:
					e.preventDefault();
					direction = (e.which == Skii.KeyCodes.PAGE_UP) ? 'first' : 'last';
					this.moveFocus(direction);
					break;
			}
			
		}
	},
	
	_onKeyDown: function(e) {
		
		switch (e.which) {
			case Skii.KeyCodes.SPACE:
			case Skii.KeyCodes.TAB:
			case Skii.KeyCodes.ESCAPE:
			case Skii.KeyCodes.PAGE_UP:
			case Skii.KeyCodes.PAGE_DOWN:
				e.preventDefault();
				return false;
				break;
			case Skii.KeyCodes.ARROW_UP:
			case Skii.KeyCodes.ARROW_DOWN:
				e.preventDefault();
				
				var _this = this;
				
				if (!this.keyDownRepeat) {

					if (!e.metaKey && !e.altKey) {
						direction = (e.which == Skii.KeyCodes.ARROW_UP) ? 'up' : 'down';
					} else {
						direction = (e.which == Skii.KeyCodes.ARROW_UP) ? 'first' : 'last';
					}
					this.moveFocus(direction);
				}
				
				if (Skii.typeOf(this.keyDownTimeout) == 'null') {
					
					this.keyDownTimeout = setTimeout(function() {
						
						_this.$list.undelegate(_this.selectableOptionSelector, 'mouseover', _this._focusOption);
						
						_this.keyDownInterval = setInterval(function() {
							_this.keyDownRepeat = true;
							var direction = (e.which == Skii.KeyCodes.ARROW_UP) ? 'up' : 'down';
							_this.moveFocus(direction);
						}, _this.keyDownIntervalDelay);
						
					}, this.keyDownThreshold);
				}
				
				break;
		}
	},
	
	_focusOption: function($option) {
		$option = ($option instanceof jQuery.Event) ? $($option.currentTarget) : $option;

		$option.addClass('hover').attr('tabindex', '0').focus();
		
		if ($option.get(0) != this.$focusedOption.get(0)) {
			$option.addClass('hover');
			this.$focusedOption.removeClass('hover').removeAttr('tabindex');
			this.$focusedOption = $option;
		}
	},
	
	_onOptionSelect: function(e) {
		if (this.canSelect) {
			e.preventDefault();
			if (this.isVisible) {
				this.selectOption(e.currentTarget);
			}
		}
	},
	
	_cancelScroll: function(e) {
		if (this.isVisible) {
			e.preventDefault();
		}
	},
	
	getSelectableOptions: function() {
		if (this.shouldUpdateOptionList) {
			this.shouldUpdateOptionList = false;
			this._$options = this.$list.find(this.selectableOptionSelector);
		}
		return this._$options;
	},
	
	focus: function() {
		this.$lastFocusedElement = $(document.activeElement);
		
		if (!this.$selectedOption.length) {
			this.$selectedOption = this.getSelectableOptions().first();
		}

		this._focusOption(this.$selectedOption);
	},
	
	blur: function() {
		if (this.$selectedOption.length) {
			this.$selectedOption.blur().removeAttr('tabindex', '0');
		}
		this.$lastFocusedElement.length && this.$lastFocusedElement.focus();
	},
	
	moveFocus: function(direction) {
		var	$options = this.getSelectableOptions(),
			currentIndex = $options.index(this.$focusedOption);
		
		if (direction == 'up' || direction == 'down') {
			var newIndex = currentIndex + ((direction == 'up') ? -1 : 1);
			if (newIndex >= 0 && newIndex < $options.length) {
				this._focusOption($($options[newIndex]));
			}
		} else if (direction == 'first' || direction == 'last') {
			this._focusOption($options[direction]());
		}
	},
	
	selectOption: function(option) {
		option = $(option);
		
		if (option.length) {
			
			var currentSelected = this.$list.find("li.selected");
			currentSelected.removeClass("selected");
			option.addClass("selected");
			this.$selectedOption = option;
		}
		
		$(this).trigger('skii.contextualmenu.selection', [option, this.getSelectableOptions().index(option), option.data('value')]);
	},
	
	enable: function() {
		this.$list.delegate(this.selectableOptionSelector, 'mouseover', $.proxy(this, '_focusOption'));
		this.$list.delegate(this.selectableOptionSelector, 'mouseup', $.proxy(this, '_onOptionSelect'));
		this.$list.delegate(this.selectableOptionSelector, 'keyup', $.proxy(this, '_onKeyUp'));
		this.$list.delegate(this.selectableOptionSelector, 'keydown', $.proxy(this, '_onKeyDown'));
		$(window).bind('resize', $.proxy(this, 'hide'));
		$(document).bind('scroll', $.proxy(this, '_cancelScroll'));
	},
	
	disable: function() {
		this.$list.undelegate(this.selectableOptionSelector, 'mouseover', this._focusOption);
		this.$list.undelegate(this.selectableOptionSelector, 'click', this._onOptionSelect);
		this.$list.undelegate(this.selectableOptionSelector, 'keyup', this._onKeyUp);
		this.$list.undelegate(this.selectableOptionSelector, 'keydown', this._onKeyDown);
		$(window).unbind('resize', this.hide);
		$(document).unbind('scroll', this._cancelScroll);
	},

	rebuildOptions: function(optionsArray) {
		var _this = this;

		var buildOptions = function(optionsArray) {
			
			var optionsMarkup = [];
			
			for (var i=0, l=optionsArray.length; i<l; i++) {
				
				var currentOpt = optionsArray[i],
					data = '',
					classes = '';
				
				for (var prop in currentOpt) {
					if ((Skii.typeOf(currentOpt[prop]) == 'string' || Skii.typeOf(currentOpt[prop]) == 'number') && prop != 'label' && prop != 'selectable') {
						data = 'data-' + prop + '="' + currentOpt[prop] + '" ';
					}
				}
				
				if (currentOpt.selectable === false) {
					classes = _this.classPrefix + '-non-selectable';
				}
				
				if (Skii.typeOf(currentOpt.value) == 'array') {
					
					classes += ' ' + _this.classPrefix + '-group';
					optionsMarkup.push('<li class="' + classes + '" ' + data + '><p>' + currentOpt.label + '</p><ul>');
					optionsMarkup.push(buildOptions(currentOpt.value));
					optionsMarkup.push('</ul></li>');
					
				} else {
					optionsMarkup.push('<li class="' + classes + '" ' + data + '><span class="' + _this.classPrefix + '-check"></span>' + currentOpt.label + '</li>');
				}
			}
			return optionsMarkup.join('');
		};

		this.$list.html(buildOptions(optionsArray));
		
		this.totalHeight = this.$list.outerHeight();
		
		this.$selectedOption = $(0);
		this.$focusedOption = $(0);
		this.shouldUpdateOptionList = true;
	},
	
	toggleVisibility: function(offset) {
		if (this.isVisible) {
			this.hide();
		} else {
			this.show(offset);
		}
	},
	
	show: function(offset) {
		var _this = this;
		this.canSelect = false;
		
		this.updatePosition(offset);
		
		if (!this.isAnimated) {
			this.$list.css('display', 'block');
		} else {
			this.$list.fadeIn(150);
		}
		this.isVisible = true;
		this.enable();
		
		setTimeout(function() {
			_this.canSelect = true;
		}, this.selectionThreshold);
	},
	
	hide: function() {
		this.isVisible = false;
		
		this.disable();
 		this.blur();
		
		if (!this.isAnimated) {
			$(this.$list).css('display', 'none');
		} else {
			$(this.$list).fadeOut(150);
		}
	},
	
	updatePosition: function(offset) {
		
		if (this.shouldUpdateBrowserMetrics) {
			this.shouldUpdateBrowserMetrics = false;
			var doc = $(document),
				viewport = $(window);
				
			this.browserMetrics = {
				docWidth: doc.width(),
				docHeight: doc.height(),

				viewportWidth: viewport.width(),
				viewportHeight: viewport.height(),

				scrollTop: doc.scrollTop(),
				scrollLeft: doc.scrollLeft()
			};
		}
			
		var bounds,
			browserMetrics = this.browserMetrics;
		
		// if it must fit in viewport or that the document is smaller than the viewport
		if (this.shouldFitInViewport || (browserMetrics.docWidth < browserMetrics.viewportWidth || browserMetrics.docHeight < browserMetrics.viewportHeight)) {
				
			bounds = {
				topLeft: {
					x: browserMetrics.scrollLeft,
					y: browserMetrics.scrollTop
				},
				bottomRight: {
					x: browserMetrics.scrollLeft + browserMetrics.viewportWidth,
					y: browserMetrics.scrollTop + browserMetrics.viewportHeight
				}
			};
				
		// make it fit to document at least
		} else {
			
			bounds = {
				topLeft: {
					x: 0,
					y: 0
				},
				bottomRight: {
					x: browserMetrics.docWidth,
					y: browserMetrics.docHeight
				}
			};
		}
		
		this.fitToBounds(offset, bounds);
		
	},
	
	fitToBounds: function(offset, bounds) {
		
		var metrics = {},
			
			boundsPadding = 15,
			
			offsetX = (offset && offset.x) || this.currentOffsetX,
			offsetY = (offset && offset.y) || this.currentOffsetY,
			
			list = this.$list,
			listHeight = this.totalHeight,
			
			// we fetch this value every time as it might change with scrollbars visible or not
			listWidth = this.$list.outerWidth(),
			
			availableDistanceToBottom = bounds.bottomRight.y - offsetY - boundsPadding,
			availableDistanceToRight = bounds.bottomRight.x - offsetX - boundsPadding,
			availableDistanceToTop = offsetY - boundsPadding,
		
			heightFitsInBounds = (listHeight + (2 * boundsPadding)) < (bounds.bottomRight.y - bounds.topLeft.y),
			widthFitsInBounds = (listWidth + (2 * boundsPadding)) < (bounds.bottomRight.x - bounds.topLeft.x);
			
		// does the list fit under the offset ?
		if (availableDistanceToBottom > (listHeight + boundsPadding)) {
			
			// move it to offset
			metrics.top = offsetY;
			
		// it doesn't fit under
		} else {
			
			// can we put it on top ? does it fit on top ?
			if (this.canBeOnTop && (listHeight + boundsPadding) < availableDistanceToTop) {
				
				// move it on top of the offset
				metrics.top = offsetY - (listHeight + boundsPadding);
				
				
			// it is not fixed, maybe we can fit it in the bounds
			} else if (!this.isFixed && heightFitsInBounds) {
				
				// stick it to bottom
				metrics.top = offsetY - ((listHeight + boundsPadding) - availableDistanceToBottom);
				
				
			// we cannot put it on top, can we put it under with a scrollbar ? (also check if there is enough space)
			} else if (this.canHaveScrollbar && availableDistanceToBottom > (2 * boundsPadding)) {
				
				// move it to offset and make scrollbar
				metrics.top = offsetY;
				metrics.height = availableDistanceToBottom - boundsPadding;
				
			// it doesn't fit anywhere, not even in the bounds
			} else {
				
				// force scrollbar and resize to bounds height
				metrics.top = bounds.topLeft.y + boundsPadding;
				metrics.height = (bounds.bottomRight.y - bounds.topLeft.y) - (2 * boundsPadding) - parseInt(this.$list.css('padding-top'), 10) - parseInt(this.$list.css('padding-bottom'), 10);
				
			}
				
		}
		
		if (metrics.height) {
			this.addScrollbar();
		} else {
			this.removeScrollbar();
		}
		
		listWidth = this.$list.outerWidth();
		
		// if it doesn't fit at the right of the offset, switch it to the left
		metrics.left = (availableDistanceToRight > listWidth) ? offsetX : offsetX - (listWidth - availableDistanceToRight);
		
		this.$list.css(metrics);
		this.currentOffsetX = metrics.top;
		this.currentOffsetY = metrics.left;
		
	},
	
	addScrollbar: function() {

		if (!this.hasScrollbar) {
			this.hasScrollbar = true;
			this.$list.css({
				overflowY: 'auto',
				overflowX: 'hidden'
			});
		}
		
	},
	removeScrollbar: function() {

		if (this.hasScrollbar) {
			this.hasScrollbar = false;
			this.$list.css({
				height: 'auto',
				overflowY: 'hidden',
				overflowX: 'hidden'
			});
		}
	},

	destroy: function() {

		$(Skii.ContextualMenu.Notifier).trigger('skii.contextualmenu.destruction', [this]);

		if(this.$selectedOption) {
			this.$selectedOption.remove();
		}
		this.$list.remove();
		this._$options.remove();

		for(var p in this) {
			delete this[p];
		}
	}
	
};
