/*!
 * KeyboardJS
 * 
 * Copyright 2011, Robert William Hurst
 * Licenced under the BSD License.
 * See https://raw.github.com/RobertWHurst/KeyboardJS/master/license.txt
 */
(function (context, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(factory);
    } else {
        // Browser globals
	    context.k = context.KeyboardJS = factory();
    }
}(this, function() {

	//polyfills for ms's peice o' shit browsers
	function bind(target, type, handler) { if (target.addEventListener) { target.addEventListener(type, handler, false); } else { target.attachEvent("on" + type, function(event) { return handler.call(target, event); }); } }
	[].indexOf||(Array.prototype.indexOf=function(a,b,c){for(c=this.length,b=(c+~~b)%c;b<c&&(!(b in this)||this[b]!==a);b++);return b^c?b:-1;});

	//locals
	var locals = {
		'us': {
			"backspace": 8,
			"tab": 9,
			"enter": 13,
			"shift": 16,
			"ctrl": 17,
			"alt": 18,
			"pause": 19, "break": 19,
			"capslock": 20,
			"escape": 27, "esc": 27,
			"space": 32, "spacebar": 32,
			"pageup": 33,
			"pagedown": 34,
			"end": 35,
			"home": 36,
			"left": 37,
			"up": 38,
			"right": 39,
			"down": 40,
			"insert": 45,
			"delete": 46,
			"0": 48, "1": 49, "2": 50, "3": 51, "4": 52, "5": 53, "6": 54, "7": 55, "8": 56, "9": 57,
			"a": 65, "b": 66, "c": 67, "d": 68, "e": 69, "f": 70, "g": 71, "h": 72, "i": 73, "j": 74, "k": 75, "l": 76, "m": 77, "n": 78, "o": 79, "p": 80, "q": 81, "r": 82, "s": 83, "t": 84, "u": 85, "v": 86, "w": 87, "x": 88, "y": 89, "z": 90,
			"meta": 91, "command": 91, "windows": 91, "win": 91,
			"_91": 92,
			"select": 93,
			"num0": 96, "num1": 97, "num2": 98, "num3": 99, "num4": 100, "num5": 101, "num6": 102, "num7": 103, "num8": 104, "num9": 105,
			"multiply": 106,
			"add": 107,
			"subtract": 109,
			"decimal": 110,
			"divide": 111,
			"f1": 112, "f2": 113, "f3": 114, "f4": 115, "f5": 116, "f6": 117, "f7": 118, "f8": 119, "f9": 120, "f10": 121, "f11": 122, "f12": 123,
			"numlock": 144, "num": 144,
			"scrolllock": 145, "scroll": 145,
			"semicolon": 186,
			"equal": 187, "equalsign": 187,
			"comma": 188,
			"dash": 189,
			"period": 190,
			"slash": 191, "forwardslash": 191,
			"graveaccent": 192,
			"openbracket": 219,
			"backslash": 220,
			"closebracket": 221,
			"singlequote": 222
		}

		//If you create a new local please submit it as a pull request or post it in the issue tracker at
		// http://github.com/RobertWhurst/KeyboardJS/issues/
	}

	//keys
	var keys = locals['us'],
		activeKeys = [],
		activeBindings = {},
		keyBindingGroups = [];

	//adds keys to the active keys array
	bind(document, "keydown", function(event) {

		//lookup the key pressed and save it to the active keys array
		for (var key in keys) {
			if(keys.hasOwnProperty(key) && event.keyCode === keys[key]) {
				if(activeKeys.indexOf(key) < 0) {
					activeKeys.push(key);
				}
			}
		}

		//execute the first callback the longest key binding that matches the active keys
		return executeActiveKeyBindings(event);

	});

	//removes keys from the active array
	bind(document, "keyup", function (event) {

		//lookup the key released and prune it from the active keys array
		for(var key in keys) {
			if(keys.hasOwnProperty(key) && event.keyCode === keys[key]) {

				var iAK = activeKeys.indexOf(key);

				if(iAK > -1) {
					activeKeys.splice(iAK, 1);
				}
			}
		}

		//execute the end callback on the active key binding
		return pruneActiveKeyBindings(event);

	});

	//bind to the window blur event and clear all pressed keys
	bind(window, "blur", function() {
		activeKeys = [];

		//execute the end callback on the active key binding
		return pruneActiveKeyBindings(event);
	});

	/**
	 * Generates an array of active key bindings
	 */
	function queryActiveBindings() {
		var bindingStack = [];

		//loop through the key binding groups by number of keys.
		for(var keyCount = keyBindingGroups.length; keyCount > -1; keyCount -= 1) {
			if(keyBindingGroups[keyCount]) {
				var KeyBindingGroup = keyBindingGroups[keyCount];

				//loop through the key bindings of the same key length.
				for(var bindingIndex = 0; bindingIndex < KeyBindingGroup.length; bindingIndex += 1) {
					var binding = KeyBindingGroup[bindingIndex],

					//assume the binding is active till a required key is found to be unsatisfied
						keyBindingActive = true;

					//loop through each key required by the binding.
					for(var keyIndex = 0; keyIndex < binding.keys.length;  keyIndex += 1) {
						var key = binding.keys[keyIndex];

						//if the current key is not in the active keys array the mark the binding as inactive
						if(activeKeys.indexOf(key) < 0) {
							keyBindingActive = false;
						}
					}

					//if the key combo is still active then push it into the binding stack
					if(keyBindingActive) {
						bindingStack.push(binding);
					}
				}
			}
		}

		return bindingStack;
	}

	/**
	 * Collects active keys, sets active binds and fires on key down callbacks
	 * @param event
	 */
	function executeActiveKeyBindings(event) {

		if(activeKeys < 1) {
			return true;
		}

		var bindingStack = queryActiveBindings(),
			spentKeys = [],
			output;

		//loop through each active binding
		for (var bindingIndex = 0; bindingIndex < bindingStack.length; bindingIndex += 1) {
			var binding = bindingStack[bindingIndex],
				usesSpentKey = false;

			//check each of the required keys. Make sure they have not been used by another binding
			for(var keyIndex = 0; keyIndex < binding.keys.length; keyIndex += 1) {
				var key = binding.keys[keyIndex];
				if(spentKeys.indexOf(key) > -1) {
					usesSpentKey = true;
					break;
				}
			}

			//if the binding does not use a key that has been spent then execute it
			if(!usesSpentKey) {

				//fire the callback
				if(typeof binding.callback === "function") {
					if(!binding.callback(event, binding.keys, binding.keyCombo)) {
						output = false
					}
				}

				//add the binding's combo to the active bindings array
				if(!activeBindings[binding.keyCombo]) {
					activeBindings[binding.keyCombo] = binding;
				}

				//add the current key binding's keys to the spent keys array
				for(var keyIndex = 0; keyIndex < binding.keys.length; keyIndex += 1) {
					var key = binding.keys[keyIndex];
					if(spentKeys.indexOf(key) < 0) {
						spentKeys.push(key);
					}
				}
			}
		}

		//if there are spent keys then we know a binding was fired
		// and that we need to tell jQuery to prevent event bubbling.
		if(spentKeys.length) {
			return false;
		}

		return output;
	}

	/**
	 * Removes no longer active keys and fires the on key up callbacks for associated active bindings.
	 * @param event
	 */
	function pruneActiveKeyBindings(event) {
		var bindingStack = queryActiveBindings();
		var output;

		//loop through the active combos
		for(var bindingCombo in activeBindings) {
			if(activeBindings.hasOwnProperty(bindingCombo)) {
				var binding = activeBindings[bindingCombo],
					active = false;

				//loop thorugh the active bindings
				for(var bindingIndex = 0; bindingIndex < bindingStack.length; bindingIndex += 1) {
					var activeCombo = bindingStack[bindingIndex].keyCombo;

					//check to see if the combo is still active
					if(activeCombo === bindingCombo) {
						active = true;
						break;
					}
				}

				//if the combo is no longer active then fire its end callback and remove it
				if(!active) {

					if(typeof binding.endCallback === "function") {
						if(!binding.endCallback(event, binding.keys, binding.keyCombo)) {
							output = false
						}
					}

					delete activeBindings[bindingCombo];
				}
			}
		}

		return output;
	}

	/**
	 * Binds a on key down and on key up callback to a key or key combo. Accepts a string containing the name of each
	 * key you want to bind to comma separated. If you want to bind a combo the use the plus sign to link keys together.
	 * Example: 'ctrl + x, ctrl + c' Will fire if Control and x or y are pressed at the same time.
	 * @param keyCombo
	 * @param callback
	 * @param endCallback
	 */
	function bindKey(keyCombo, callback, endCallback) {

		function clear() {
			if(keys && keys.length) {
				var keyBindingGroup = keyBindingGroups[keys.length];

				if(keyBindingGroup.indexOf(keyBinding) > -1) {
					var index = keyBindingGroups[keys.length].indexOf(keyBinding);
					keyBindingGroups[keys.length].splice(index, 1);
				}
			}
		}

		//create an array of combos from the first argument
		var bindSets = keyCombo.toLowerCase().replace(/\s/g, '').split(',');

		//create a binding for each key combo
		for(var i = 0; i < bindSets.length; i += 1) {

			//split up the keys
			var keys = bindSets[i].split('+');

			//if there are keys in the current combo
			if(keys.length) {
				if(!keyBindingGroups[keys.length]) { keyBindingGroups[keys.length] = []; }

				//define the
				var keyBinding = {
					"callback": callback,
					"endCallback": endCallback,
					"keyCombo": bindSets[i],
					"keys": keys
				};

				//save the binding sorted by length
				keyBindingGroups[keys.length].push(keyBinding);
			}
		}

		return {
			"clear": clear
		}
	}

	/**
	 * Binds keys or key combos to an axis. The keys should be in the following order; up, down, left, right. If any
	 * of the the binded key or key combos are active the callback will fire. The callback will be passed an array
	 * containing two numbers. The first represents x and the second represents y. Both have a possible range of -1,
	 * 0, or 1 depending on the axis direction.
	 * @param up
	 * @param down
	 * @param left
	 * @param right
	 * @param callback
	 */
	function bindAxis(up, down, left, right, callback) {

		function clear() {
			if(typeof clearUp === 'function') { clearUp(); }
			if(typeof clearDown === 'function') { clearDown(); }
			if(typeof clearLeft === 'function') { clearLeft(); }
			if(typeof clearRight === 'function') { clearRight(); }
			if(typeof timer === 'function') { clearInterval(timer); }
		}

		var axis = [0, 0];

		if(typeof callback !== 'function') {
			return false;
		}

		//up
		var clearUp = bindKey(up, function () {
			if(axis[0] === 0) {
				axis[0] = -1;
			}
		}, function() {
			axis[0] = 0;
		}).clear;

		//down
		var clearDown = bindKey(down, function () {
			if(axis[0] === 0) {
				axis[0] = 1;
			}
		}, function() {
			axis[0] = 0;
		}).clear;

		//left
		var clearLeft = bindKey(left, function () {
			if(axis[1] === 0) {
				axis[1] = -1;
			}
		}, function() {
			axis[1] = 0;
		}).clear;

		//right
		var clearRight = bindKey(right, function () {
			if(axis[1] === 0) {
				axis[1] = 1;
			}
		}, function() {
			axis[1] = 0;
		}).clear;

		var timer = setInterval(function(){

			//NO CHANGE
			if(axis[0] === 0 && axis[1] === 0) {
				return;
			}

			//run the callback
			callback(axis);

		}, 1);

		return {
			"clear": clear
		}
	}

	/**
	 * Clears all key and key combo binds containing a given key or keys.
	 * @param keys
	 */
	function unbindKey(keys) {

		if(keys === 'all') {
			keyBindingGroups = [];
			return;
		}

		keys = keys.replace(/\s/g, '').split(',');

		//loop through the key binding groups.
		for(var iKCL = keyBindingGroups.length; iKCL > -1; iKCL -= 1) {
			if(keyBindingGroups[iKCL]) {
				var KeyBindingGroup = keyBindingGroups[iKCL];

				//loop through the key bindings.
				for(var iB = 0; iB < KeyBindingGroup.length; iB += 1) {
					var keyBinding = KeyBindingGroup[iB],
						remove = false;

					//loop through the current key binding keys.
					for(var iKB = 0; iKB < keyBinding.keys.length;  iKB += 1) {
						var key = keyBinding.keys[iKB];

						//loop through the keys to be removed
						for(var iKR = 0; iKR < keys.length; iKR += 1) {
							var keyToRemove = keys[iKR];
							if(keyToRemove === key) {
								remove = true;
								break;
							}
						}
						if(remove) { break; }
					}
					if(remove) {
						keyBindingGroups[iKCL].splice(iB, 1); iB -= 1;
						if(keyBindingGroups[iKCL].length < 1) {
							delete keyBindingGroups[iKCL];
						}
					}
				}
			}
		}
	}

	/**
	 * Gets an array of active keys
	 */
	function getActiveKeys() {
		return activeKeys;
	}

	/**
	 * Adds a new keyboard local not supported by keyboard JS
	 * @param local
	 * @param keys
	 */
	function addLocale(local, keys) {
		locals[local] = keys;
	}

	/**
	 * Changes the keyboard local
	 * @param local
	 */
	function setLocale(local) {
		if(locals[local]) {
			keys = locals[local];
		}

	}

	return {
		"bind": {
			"key": bindKey,
			"axis": bindAxis
		},
		"activeKeys": getActiveKeys,
		"unbind": {
			"key": unbindKey
		},
		"locale": {
			"add": addLocale,
			"set": setLocale
		}
	}
}));