text: 'use strict';
  
  var specialKeyCodeMap;
  
  specialKeyCodeMap = {
  	9: 'tab',
  	27: 'esc',
  	37: 'left',
  	39: 'right',
  	13: 'enter',
  	38: 'up',
  	40: 'down'
  };
  
  var _ = require('../common/utils.js');
  var DOM = require('../common/dom.js');
  var EventEmitter = require('./event_emitter.js');
  
  // constructor
  // -----------
  
  function Input(o) {
  	var that = this;
  	var onBlur;
  	var onFocus;
  	var onKeydown;
  	var onInput;
  
  	o = o || {};
  
  	if (!o.input) {
  		_.error('input is missing');
  	}
  
  	// bound functions
  	onBlur = _.bind(this._onBlur, this);
  	onFocus = _.bind(this._onFocus, this);
  	onKeydown = _.bind(this._onKeydown, this);
  	onInput = _.bind(this._onInput, this);
  
  	this.$hint = DOM.element(o.hint);
  	this.$input = DOM.element(o.input)
  		.on('blur.aa', onBlur)
  		.on('focus.aa', onFocus)
  		.on('keydown.aa', onKeydown);
  
  	// if no hint, noop all the hint related functions
  	if (this.$hint.length === 0) {
  		this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid =
  			_.noop;
  	}
  
  	// ie7 and ie8 don't support the input event
  	// ie9 doesn't fire the input event when characters are removed
  	// not sure if ie10 is compatible
  	if (!_.isMsie()) {
  		this.$input.on('input.aa', onInput);
  	} else {
  		this.$input.on('keydown.aa keypress.aa cut.aa paste.aa', function($e) {
  			// if a special key triggered this, ignore it
  			if (specialKeyCodeMap[$e.which || $e.keyCode]) {
  				return;
  			}
  
  			// give the browser a chance to update the value of the input
  			// before checking to see if the query changed
  			_.defer(_.bind(that._onInput, that, $e));
  		});
  	}
  
  	// the query defaults to whatever the value of the input is
  	// on initialization, it'll most likely be an empty string
  	this.query = this.$input.val();
  
  	// helps with calculating the width of the input's value
  	this.$overflowHelper = buildOverflowHelper(this.$input);
  }
  
  // static methods
  // --------------
  
  Input.normalizeQuery = function(str) {
  	// strips leading whitespace and condenses all whitespace
  	return (str || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' ');
  };
  
  // instance methods
  // ----------------
  
  _.mixin(Input.prototype, EventEmitter, {
  	// ### private
  
  	_onBlur: function onBlur() {
  		this.resetInputValue();
  		this.$input.removeAttr('aria-activedescendant');
  		this.trigger('blurred');
  	},
  
  	_onFocus: function onFocus() {
  		this.trigger('focused');
  	},
  
  	_onKeydown: function onKeydown($e) {
  		// which is normalized and consistent (but not for ie)
  		var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
  
  		this._managePreventDefault(keyName, $e);
  		if (keyName && this._shouldTrigger(keyName, $e)) {
  			this.trigger(keyName + 'Keyed', $e);
  		}
  	},
  
  	_onInput: function onInput() {
  		this._checkInputValue();
  	},
  
  	_managePreventDefault: function managePreventDefault(keyName, $e) {
  		var preventDefault;
  		var hintValue;
  		var inputValue;
  
  		switch (keyName) {
  			case 'tab':
  				hintValue = this.getHint();
  				inputValue = this.getInputValue();
  
  				preventDefault =
  					hintValue && hintValue !== inputValue && !withModifier($e);
  				break;
  
  			case 'up':
  			case 'down':
  				preventDefault = !withModifier($e);
  				break;
  
  			default:
  				preventDefault = false;
  		}
  
  		if (preventDefault) {
  			$e.preventDefault();
  		}
  	},
  
  	_shouldTrigger: function shouldTrigger(keyName, $e) {
  		var trigger;
  
  		switch (keyName) {
  			case 'tab':
  				trigger = !withModifier($e);
  				break;
  
  			default:
  				trigger = true;
  		}
  
  		return trigger;
  	},
  
  	_checkInputValue: function checkInputValue() {
  		var inputValue;
  		var areEquivalent;
  		var hasDifferentWhitespace;
  
  		inputValue = this.getInputValue();
  		areEquivalent = areQueriesEquivalent(inputValue, this.query);
  		hasDifferentWhitespace =
  			areEquivalent && this.query
  				? this.query.length !== inputValue.length
  				: false;
  
  		this.query = inputValue;
  
  		if (!areEquivalent) {
  			this.trigger('queryChanged', this.query);
  		} else if (hasDifferentWhitespace) {
  			this.trigger('whitespaceChanged', this.query);
  		}
  	},
  
  	// ### public
  
  	focus: function focus() {
  		this.$input.focus();
  	},
  
  	blur: function blur() {
  		this.$input.blur();
  	},
  
  	getQuery: function getQuery() {
  		return this.query;
  	},
  
  	setQuery: function setQuery(query) {
  		this.query = query;
  	},
  
  	getInputValue: function getInputValue() {
  		return this.$input.val();
  	},
  
  	setInputValue: function setInputValue(value, silent) {
  		if (typeof value === 'undefined') {
  			value = this.query;
  		}
  		this.$input.val(value);
  
  		// silent prevents any additional events from being triggered
  		if (silent) {
  			this.clearHint();
  		} else {
  			this._checkInputValue();
  		}
  	},
  
  	expand: function expand() {
  		this.$input.attr('aria-expanded', 'true');
  	},
  
  	collapse: function collapse() {
  		this.$input.attr('aria-expanded', 'false');
  	},
  
  	setActiveDescendant: function setActiveDescendant(activedescendantId) {
  		this.$input.attr('aria-activedescendant', activedescendantId);
  	},
  
  	removeActiveDescendant: function removeActiveDescendant() {
  		this.$input.removeAttr('aria-activedescendant');
  	},
  
  	resetInputValue: function resetInputValue() {
  		this.setInputValue(this.query, true);
  	},
  
  	getHint: function getHint() {
  		return this.$hint.val();
  	},
  
  	setHint: function setHint(value) {
  		this.$hint.val(value);
  	},
  
  	clearHint: function clearHint() {
  		this.setHint('');
  	},
  
  	clearHintIfInvalid: function clearHintIfInvalid() {
  		var val;
  		var hint;
  		var valIsPrefixOfHint;
  		var isValid;
  
  		val = this.getInputValue();
  		hint = this.getHint();
  		valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
  		isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow();
  
  		if (!isValid) {
  			this.clearHint();
  		}
  	},
  
  	getLanguageDirection: function getLanguageDirection() {
  		return (this.$input.css('direction') || 'ltr').toLowerCase();
  	},
  
  	hasOverflow: function hasOverflow() {
  		// 2 is arbitrary, just picking a small number to handle edge cases
  		var constraint = this.$input.width() - 2;
  
  		this.$overflowHelper.text(this.getInputValue());
  
  		return this.$overflowHelper.width() >= constraint;
  	},
  
  	isCursorAtEnd: function() {
  		var valueLength;
  		var selectionStart;
  		var range;
  
  		valueLength = this.$input.val().length;
  		selectionStart = this.$input[0].selectionStart;
  
  		if (_.isNumber(selectionStart)) {
  			return selectionStart === valueLength;
  		} else if (document.selection) {
  			// NOTE: this won't work unless the input has focus, the good news
  			// is this code should only get called when the input has focus
  			range = document.selection.createRange();
  			range.moveStart('character', -valueLength);
  
  			return valueLength === range.text.length;
  		}
  
  		return true;
  	},
  
  	destroy: function destroy() {
  		this.$hint.off('.aa');
  		this.$input.off('.aa');
  
  		this.$hint = this.$input = this.$overflowHelper = null;
  	}
  });
  
  // helper functions
  // ----------------
  
  function buildOverflowHelper($input) {
  	return DOM.element('<pre aria-hidden="true"></pre>')
  		.css({
  			// position helper off-screen
  			position: 'absolute',
  			visibility: 'hidden',
  			// avoid line breaks and whitespace collapsing
  			whiteSpace: 'pre',
  			// use same font css as input to calculate accurate width
  			fontFamily: $input.css('font-family'),
  			fontSize: $input.css('font-size'),
  			fontStyle: $input.css('font-style'),
  			fontVariant: $input.css('font-variant'),
  			fontWeight: $input.css('font-weight'),
  			wordSpacing: $input.css('word-spacing'),
  			letterSpacing: $input.css('letter-spacing'),
  			textIndent: $input.css('text-indent'),
  			textRendering: $input.css('text-rendering'),
  			textTransform: $input.css('text-transform')
  		})
  		.insertAfter($input);
  }
  
  function areQueriesEquivalent(a, b) {
  	return Input.normalizeQuery(a) === Input.normalizeQuery(b);
  }
  
  function withModifier($e) {
  	return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
  }
  
  module.exports = Input;
  