/**
 * @name          Keys
 * @description Another jQuery hotkey plugin
 * @version       1.0
 * @date           2008-02-12
 * 
 * @copyright
 * Copyright (c) 2008 Trey Shugart (shugartweb.com/jquery/)
 * 
 * @license
 * MIT - (http://www.opensource.org/licenses/mit-license.php) 
 * GPL - (http://www.gnu.org/licenses/gpl.txt)
 * 
 * @p
 * A jquery hotkey plugin that allows for the manipulation of spcific key bindings, combinations, etc. through
 * key selectors that are similar in syntax to jQuery selectors.
 *
 * @p
 * 
 * 
 * @p
 * <strong>NOTE:</strong> 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.
 */
(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({
		/**
		 * @name jQuery.keys
		 * @chainable true
		 * @description Shortcut for calling Keys on the window object
		 * @return Object jQuery
		 * @param String keySelectors - They keys to bind the callbacks to
		 * @param Object options        - A set of options to use
		 * @param Function callback1    - A callback to execute when a selected key/key combo is pressed
		 * @param Function callback2   - A callback to execute when a selected key/key combo is released
		 * @example
		 * // same as the call to jQuery(window).keys() in the example under jQuery.fn.keys
		 * jQuery.keys('ctrl + s', function() {
		 * 	jQuery.post('/url/to/save/script');
		 * 	
		 * 	return false;
		 * });
		 */
		keys: function(keySelectors, options, callback1, callback2) {
			return $(window).keys(keySelectors, options, callback1, callback2);
		},
			
		/**
		 * @name jQuery.keyCode
		 * @description returns the keyCode of the key that was pressed
		 * @return Int
		 * @param event e - The event to capture the key code from
		 */
		keyCode: function(e) {
			var e = (!e) ? window.event : e;
			return k = (e.keyCode) ? e.keyCode : e.which;
		},
			
		/**
		 * @name jQuery.keyName
		 * @description returns the name of the key that was pressed
		 * @return String
		 * @param event e - The event to capture the key code from
		 */
		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({
		/**
		 * @name jQuery.fn.keys
		 * @return Object jQuery
		 * @chainable true
		 * @param String keySelectors - They keys to bind the callbacks to
		 * @param Object options        - A set of options to use
		 * @param Function callback1    - A callback to execute when a selected key/key combo is pressed
		 * @param Function callback2   - A callback to execute when a selected key/key combo is released
		 * @example
		 * // Doesn't allow number keys
		 * jQuery('input').keys('0, 1, 2, 3, 4, 5, 6, 7, 8, 9', function() {
		 * 	return false;
		 * });
		 * @example
		 * // posts to a url when control and s are pressed together and stops bubbling and propagation.
		 * jQuery(window).keys('ctrl + s', function() {
		 * 	jQuery.post('/url/to/save/script');
		 * 	
		 * 	return false;
		 * });
		 */
		// 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 $$ = $.browser.msie && this.get(0) == window ? $('body') : this;
			var all, currentKeys = [];
			
			// 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);
				var element = $(this);
				
				// 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, and a '*' can be passed to do the action for any key
					if ((matches === combo.length && matches === setArr.length) || set === '*') {
						// if a callback was passed, run it
						if (typeof callback1 === 'function')
							callback1(e, element);
						// 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);
			});
			
			return $$;
		}
	});
})(jQuery);
