/**
*	@name Keys
*	@version 1.0.0
*	@description Binds events and/or stops propagation (if so desired) for the specified keys.
*	@author Trey Shugart
*	@date 2008-03-18
*	@copyright Copyright 2008 Trey Shugart
*	@license GNU LGPL (http://www.gnu.org/licenses/lgpl.html)
*	
*	@example
*	The following example would stop the usual behavior when the tab key is pressed
*	and when the shift plus tab key's are pressed on all input elements and alert a
*	message saying that tabs aren't allowed when one of those key/combos are released.
*	$('input').keys('tab, shift+[9]', {propagate: false}, function() {
*		return false;
*	}, function() {
*		alert('Tabs aren\'t allowed!');
*	});
*	
*	NOTICE!
*	You have to be careful when using alerts on the keydown/keypress events because the 
*	key cache won't be cleared if you release the buttons while the alert box is present.
*
*	@requires Array.remove, which is included
*/
(function($) {
	// Thank you John Resig for this great method
	Array.prototype.remove = function(from, to) {
		var rest = this.slice((to || from) + 1 || this.length);
		this.length = from < 0 ? this.length + from : from;
		return this.push.apply(this, rest);
	};
	
	$.extend({
		// shortcut if not specifying a selector
		keys: function(keySelectors, options, callback1, callback2) {
			$(window).keys(keySelectors, options, callback1, callback2);
		},
			
		/**
		*	@private
		*	@param event e The event to capture the key code from
		*	Retrieves the current key being pressed
		*/
		keyCode: function(e) {
			var e = (!e) ? window.event : e;
			return k = (e.keyCode) ? e.keyCode : e.which;
		},
		
		// map special keys
		keyName: function(e) {
			var keyMap = {
				8: 'backspace',
				9: 'tab',
				13: 'enter',
				16: 'shift',
				17: 'ctrl',
				18: 'alt',
				19: 'pause',
				19: 'break',
				20: 'caps lock',
				27: 'escape',
				32: 'space',
				33: 'page up',
				34: 'page down',
				35: 'end',
				36: 'home',
				37: 'left arrow',
				38: 'up arrow',
				39: 'right arrow',
				40: 'down arrow',
				45: 'insert',
				46: 'delete',
				47: 'left window key',
				92: 'right window key',
				93: 'select key',
				96: 'numpad 0',
				97: 'numpad 1',
				98: 'numpad 2',
				99: 'numpad 3',
				100: 'numpad 4',
				101: 'numpad 5',
				102: 'numpad 6',
				103: 'numpad 7',
				104: 'numpad 8',
				105: 'numpad 9',
				106: 'numpad *',
				107: 'numpad +',
				109: 'numpad -',
				110: 'numpad .',
				111: 'numpad /',
				112: 'f1',
				113: 'f2',
				114: 'f3',
				115: 'f4',
				116: 'f5',
				117: 'f6',
				118: 'f7',
				119: 'f8',
				120: 'f9',
				121: 'f10',
				122: 'f11',
				123: 'f12',
				144: 'num lock',
				145: 'scroll lock',
				186: ';',
				187: '=',
				188: ',',
				189: '-',
				190: '.',
				191: '/',
				192: '`',
				219: '[',
				220: '\\',
				221: ']',
				222: "'"
			};
			
			// map the number keys
			for (var i = 48; i <= 57; i++)
				keyMap[i] = i - 48;
			
			// map alpha characters
			for (var i = 65; i <= 90; i++) {
				var alpha = 'abcdefghijklmnopqrstuvwxyz';
				keyMap[i] = alpha.charAt(i - 65);
			}
			
			return keyMap[$.keyCode(e)];
		}
	});
	
	$.fn.extend({
		// if specifying a selector
		keys: function(keySelectors, options, callback1, callback2) {
			// if it's not a string, we can't do anything with it
			if (typeof keySelectors !== 'string')
				return false;
			
			var $$ = $(this), all, currentKeys = new Array();
			
			// msie likes 'body', everything else runs on window
			if ($$ === ('body' || window))
				$$ = ($.browser.msie) ? 'body' : window;
			
			// just in case they didn't pass any options
			if ($.isFunction(options)) {
				callback2 = callback1;
				callback1 = options;
				options = {};
			}
			
			options = $.extend({
				type: 'keydown',
				propagate: true
			}, options);
			
			// variable event
			$$[options.type](function(e) {
				var k = $.keyCode(e);
				
				// if the key is already stored in the cache, there is no reason to append it again; prevents multiple entries such as when holding down a key
				if ($.inArray(k, currentKeys) === -1)
					currentKeys[currentKeys.length] = k;
				
				// separate keys and key combos are separated by an unescaped comma, if no unescaped comma is found, there is only one set
				var sets = (keySelectors.match(/\\{0},/)) ? keySelectors.split(/\\{0},/) : new Array(keySelectors);
				$.each(sets, function(i, set) {
					var set = $.trim(set), combo = new Array, matches = 0, setArr = set.split(/\\{0}\+/);
					
					$.each(currentKeys, function(i, key) {
						// see if the user used a literal key number selector i.e. [119]+[16]+tab
						combo[i] = set.match(/\+?\[[0-9]+\]\+?/) ? '['+key+']' : $.keyName(e);
						$.each(setArr, function(ii, match) {
							// trim whitespace for comparison later
							setArr[ii] = $.trim(match);
							if ($.trim(match.toLowerCase()) === combo[i])
								matches++;
						});
					});
					// allow for key combinations to be in any order; combos must also not have extra keys being pressed before the match
					if ((matches === combo.length && matches === setArr.length) || set === '*') {
						// if a callback was passed, run it
						if (typeof callback1 === 'function')
							callback1(e);
						// if we are stopping the usual operation of the key/combo being pressed, only do so if we have the right combo
						if (!options.propagate) {
							e.stopPropagation();
							e.preventDefault();
							return false;
						}
					}
				});
			});
			
			// on key up, we must always reset the key combinations that were pressed, along with a callback if passed
			$$.keyup(function(e) {
				var k = $.keyCode(e);
				// remove the key that was just released from the current keys array
				if (currentKeys.length > 0)
					currentKeys.remove($.inArray(k, currentKeys));
				// execute a  callback if passed
				if (typeof callback2 === 'function')
					callback2(e);
			});
		}
	});
})(jQuery);