/**
 * AW Masked Input
 * @version 1.4
 * @author Kendall Conrad
 * @url http://www.angelwatt.com/coding/masked_input.php
 * @created 2008-12-16
 * @modified 2015-02-25
 * @license This work is licensed under a Creative Commons
 *  Attribution-Share Alike 3.0 United States License
 *  http://creativecommons.org/licenses/by-sa/3.0/us/
 *
 * @param scope The object to attach MaskedInput to.
 */
(function(scope) {
	'use strict';

	/**
	 * MaskedInput takes many possible arguments described below.
	 * Note: req = required, opt = optional
	 * @param {object} args {
	 *  -elm [req] text input node to apply the mask on
	 *  -format [req] string format for the mask
	 *  -allowed [opt, '0123456789'] string with chars allowed to be typed
	 *  -allowedfx [opt] function to call for additional validation on a
	 *   given character at an index. Sends arguments character, index.
	 *   The index starts at 1 for the first character.
	 *  -sep [opt, '\/:-'] string of char(s) used as separators in mask
	 *  -typeon [opt, '_YMDhms'] string of chars in mask that can be typed on
	 *  -onfilled [opt, null] function to run when the format is filled in
	 *  -onbadkey [opt, null] function to run when user types a unallowed key
	 *  -badkeywait [opt, 0] used with onbadkey. Indicates how long (in ms)
	 *   to lock text input for onbadkey function to run
	 *  -preserve [opt, true] whether to preserve existing text in
	 *   field during init.
	 * }
	 * @returns MaskedInput
	 */
	scope.MaskedInput = function(args) {
		// Ensure passing in valid argument
		if (!args || !args.elm || !args.format) {
			return null;
		}
		// Ensure use of 'new'
		if (!(this instanceof scope.MaskedInput)) {
			return new scope.MaskedInput(args);
		}
		// Initialize variables
		var self = this,
			el = args.elm,
			format = args.format,
			allowed = args.allowed || '0123456789',
			allowedfx = args.allowedfx || function() {return true;},
			sep = args.separator || '\/:-',
			open = args.typeon || '_YMDhms',
			onbadkey = args.onbadkey || function() {},
			onfilled = args.onfilled || function() {},
			badwait = args.badkeywait || 0,
			preserve = args.hasOwnProperty('preserve') ? !!args.preserve : true,
			// ----
			enabled = true,
			locked = false,
			startText = format,
		/**
		 * Add events to objects.
		 */
		evtAdd = (function() {
			if (window.addEventListener) {
				return function(obj, type, fx, capture) {
					obj.addEventListener(type, fx,
							(capture === undefined) ? false : capture);
				};
			}
			if (window.attachEvent) {
				return function(obj, type, fx) {
					obj.attachEvent('on' + type, fx);
				};
			}
			return function(obj, type, fx) {
				obj['on' + type] = fx;
			};
		}()),
		/**
		 * Checks whether the format has been completely filled out.
		 * @return boolean if all typeon chars have been filled.
		 */
		isFilled = function() {
			// Check if any typeon characters are left
			// Work from end of string as it's usually last filled
			for (var a = el.value.length - 1; a >= 0; a--) {
				// Check against each typeon character
				for (var c = 0, d = open.length; c < d; c++) {
					// If one matches we don't need to check anymore
					if (el.value[a] === open[c]) {
						return false;
					}
				}
			}
			return true;
		},
		/**
		 * Gets the current position of the text cursor in a text field.
		 * @param node a input or textarea HTML node.
		 * @return int text cursor position index, or -1 if there was a problem.
		 */
		getTextCursor = function(node) {
			try {
				node.focus();
				if (node.selectionStart >= 0) {
					return node.selectionStart;
				}
				if (document.selection) {// IE
					var rng = document.selection.createRange();
					return -rng.moveStart('character', -node.value.length);
				}
				return -1;
			}
			catch (e) {
				return -1;
			}
		},
		/**
		 * Sets the text cursor in a text field to a specific position.
		 * @param node a input or textarea HTML node.
		 * @param pos int of the position to be placed.
		 * @return boolean true is successful, false otherwise.
		 */
		setTextCursor = function(node, pos) {
			try {
				if (node.selectionStart) {
					node.focus();
					node.setSelectionRange(pos, pos);
				}
				else if (node.createTextRange) { // IE
					var rng = node.createTextRange();
					rng.move('character', pos);
					rng.select();
				}
			}
			catch (e) {
				return false;
			}
			return true;
		},
		/**
		 * Gets the keyboard input in usable way.
		 * @param code integer character code
		 * @return string representing character code
		 */
		getKey = function(code) {
			code = code || window.event;
			var ch = '',
				keyCode = code.which,
				evt = code.type;
			if (keyCode === undefined || keyCode === null) {
				keyCode = code.keyCode;
			}
			// no key, no play
			if (keyCode === undefined || keyCode === null) {
				return '';
			}
			// deal with special keys
			switch (keyCode) {
				case 8:
					ch = 'bksp';
					break;
				case 46: // handle del and . both being 46
					ch = (evt === 'keydown') ? 'del' : '.';
					break;
				case 16:
					ch = 'shift';
					break;
				case 0: /*CRAP*/
				case 9: /*TAB*/
				case 13:/*ENTER*/
					ch = 'etc';
					break;
				case 37:
				case 38:
				case 39:
				case 40: // arrow keys
					ch = (!code.shiftKey &&
							(code.charCode !== 39 && code.charCode !== undefined)) ?
							'etc' : String.fromCharCode(keyCode);
					break;
					// default to thinking it's a character or digit
				default:
					ch = String.fromCharCode(keyCode);
					break;
			}
			return ch;
		},
		/**
		 * Stop the event propogation chain.
		 * @param evt Event to stop
		 * @param ret boolean, used for IE to prevent default event
		 */
		stopEvent = function(evt, ret) {
			// Stop default behavior the standard way
			if (evt.preventDefault) {
				evt.preventDefault();
			}
			// Then there's IE
			evt.returnValue = ret || false;
		},
		/**
		 * Updates the text field with the given key.
		 * @param key string keyboard input.
		 */
		update = function(key) {
			var p = getTextCursor(el),
				c = el.value,
				val = '',
				cond = true;
			// Handle keys now
			switch (cond) {
				// Allowed characters
				case (allowed.indexOf(key) !== -1):
					p = p + 1;
					// if text cursor at end
					if (p > format.length) {
						return false;
					}
					// Handle cases where user places cursor before separator
					while (sep.indexOf(c.charAt(p - 1)) !== -1 && p <= format.length) {
						p = p + 1;
					}
					// Check against any custom validation
					if (!allowedfx(key, p)) {
						onbadkey(key);
						return false;
					}
					val = c.substr(0, p - 1) + key + c.substr(p);
					// Move csor up a spot if next char is a separator char
					if (allowed.indexOf(c.charAt(p)) === -1
							&& open.indexOf(c.charAt(p)) === -1) {
						p = p + 1;
					}
					break;
				case (key === 'bksp'): // backspace
					p = p - 1;
					// at start of field
					if (p < 0) {
						return false;
					}
					// If previous char is a separator, move a little more
					while (allowed.indexOf(c.charAt(p)) === -1
							&& open.indexOf(c.charAt(p)) === -1
							&& p > 1) {
						p = p - 1;
					}
					val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
					break;
				case (key === 'del'): // forward delete
					// at end of field
					if (p >= c.length) {
						return false;
					}
					// If next char is a separator and not the end of the text field
					while (sep.indexOf(c.charAt(p)) !== -1
							&& c.charAt(p) !== '') {
						p = p + 1;
					}
					val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
					p = p + 1; // Move position forward
					break;
				case (key === 'etc'):
					// Catch other allowed chars
					return true;
				default:
					return false; // Ignore the rest
			}
			el.value = ''; // blank it first (Firefox issue)
			el.value = val; // put updated value back in
			setTextCursor(el, p); // Set the text cursor
			return false;
		},
		/**
		 * Returns whether or not a given input is valid for the mask.
		 * @param k string of character to check.
		 * @return bool true if it's a valid character.
		 */
		goodOnes = function(k) {
			// if not in allowed list, or invisible key action
			if (allowed.indexOf(k) === -1 && k !== 'bksp' && k !== 'del' && k !== 'etc') {
				// Need to ensure cursor position not lost
				var p = getTextCursor(el);
				locked = true;
				onbadkey(k);
				// Hold lock long enough for onbadkey function to run
				setTimeout(function() {
					locked = false;
					setTextCursor(el, p);
				}, badwait);
				return false;
			}
			return true;
		},
		/**
		 * Handles the key down events.
		 * @param e Event
		 */
		keyHandlerDown = function(e) {
			if (!enabled) {
				return true;
			}
			e = e || event;
			if (locked) {
				stopEvent(e);
				return false;
			}
			var key = getKey(e);
			// Stop copy and paste
			if ((e.metaKey || e.ctrlKey) && (key === 'X' || key === 'V')) {
				stopEvent(e);
				return false;
			}
			// Allow for OS commands
			if (e.metaKey || e.ctrlKey) {
				return true;
			}
			if (el.value === '') {
				el.value = format;
				setTextCursor(el, 0);
			}
			// Only do update for bksp del
			if (key === 'bksp' || key === 'del') {
				update(key);
				stopEvent(e);
				return false;
			}
			return true;
		},
		/**
		 * Handles the key press events.
		 * @param e Event
		 */
		keyHandlerPress = function(e) {
			if (!enabled) {
				return true;
			}
			e = e || event;
			if (locked) {
				stopEvent(e);
				return false;
			}
			var key = getKey(e);
			// Check if modifier key is being pressed; command
			if (key === 'etc' || e.metaKey || e.ctrlKey || e.altKey) {
				return true;
			}
			if (key !== 'bksp' && key !== 'del' && key !== 'shift') {
				if (!goodOnes(key)) {
					stopEvent(e);
					return false;
				}
				if (update(key)) {
					if (isFilled()) {
						onfilled();
					}
					stopEvent(e, true);
					return true;
				}
				if (isFilled()) {
					onfilled();
				}
				stopEvent(e);
				return false;
			}
			return false;
		},
		/**
		 * Initialize the object.
		 */
		init = function() {
			// Check if an input or textarea tag was passed in
			if (!el.tagName || (el.tagName.toUpperCase() !== 'INPUT'
					&& el.tagName.toUpperCase() !== 'TEXTAREA')) {
				return null;
			}
			// Only place formatted text in field when not preserving
			// text or it's empty.
			if (!preserve || el.value === '') {
				el.value = format;
			}
			// Assign events
			evtAdd(el, 'keydown', function(e) {
				keyHandlerDown(e);
			});
			evtAdd(el, 'keypress', function(e) {
				keyHandlerPress(e);
			});
			// Let us set the initial text state when focused
			evtAdd(el, 'focus', function() {
				startText = el.value;
			});
			// Handle onChange event manually
			evtAdd(el, 'blur', function() {
				if (el.value !== startText && el.onchange) {
					el.onchange();
				}
			});
			return self;
		};

		/**
		 * Resets the text field so just the format is present.
		 */
		self.resetField = function() {
			el.value = format;
		};

		/**
		 * Set the allowed characters that can be used in the mask.
		 * @param a string of characters that can be used.
		 */
		self.setAllowed = function(a) {
			allowed = a;
			self.resetField();
		};

		/**
		 * The format to be used in the mask.
		 * @param f string of the format.
		 */
		self.setFormat = function(f) {
			format = f;
			self.resetField();
		};

		/**
		 * Set the characters to be used as separators.
		 * @param s string representing the separator characters.
		 */
		self.setSeparator = function(s) {
			sep = s;
			self.resetField();
		};

		/**
		 * Set the characters that the user will be typing over.
		 * @param t string representing the characters that will be typed over.
		 */
		self.setTypeon = function(t) {
			open = t;
			self.resetField();
		};

		/**
		 * Sets whether the mask is active.
		 */
		self.setEnabled = function(enable) {
			enabled = enable;
		};

		return init();
	};
}(window));