/**
 * Helpers for keyboard-related functionality.
 * @file CW-Keyboard.js
 */
cw.provide("CW-Keyboard", function(){

if (typeof jQuery == "undefined") {
    cw.error("jQuery is required for the Keyboard module");
}

var GenerateRandomString = cw.require("CW-Helpers").GenerateRandomString;

// -----------------------------------------------------------------------------

/**
 * Maps mnemonic descriptions to key codes for a US keyboard. When there are
 * multiple parts to the name, the primary part comes first and the secondary
 * part afterwards, e.g., SLASH_FORWARD and ARROW_UP.
 */
var KeyCodes = {
  "BACKSPACE": 8,
  "TAB": 9,
  "CLEAR": 12,
  "RETURN": 13,
  "SHIFT": 16,
  "CONTROL": 17,
  "ALT": 18,
  "PAUSE": 19,
  "CAPS_LOCK": 20,
  "ESCAPE": 27,
  "SPACE": 32,
  "PAGE_UP": 33,
  "PAGE_DOWN": 34,
  "END": 35,
  "HOME": 36,
  "ARROW_LEFT": 37,
  "ARROW_UP": 38,
  "ARROW_RIGHT": 39,
  "ARROW_DOWN": 40,
  "INSERT": 45,
  "DELETE": 46,
  "ZERO": 48,
  "ONE": 49,
  "TWO": 50,
  "THREE": 51,
  "FOUR": 52,
  "FIVE": 53,
  "SIX": 54,
  "SEVEN": 55,
  "EIGHT": 56,
  "NINE": 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,
  "ZERO_NUMPAD": 96,
  "ONE_NUMPAD": 97,
  "TWO_NUMPAD": 98,
  "THREE_NUMPAD": 99,
  "FOUR_NUMPAD": 100,
  "FIVE_NUMPAD": 101,
  "SIX_NUMPAD": 102,
  "SEVEN_NUMPAD": 103,
  "EIGHT_NUMPAD": 104,
  "NINE_NUMPAD": 105,
  "MULTIPLY": 106,
  "ADD": 107,
  "SUBTRACT": 109,
  "POINT_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,
  "SEMICOLON": 186,
  "EQUALS": 187,
  "COMMA": 188,
  "HYPHEN": 189,
  "PERIOD": 190,
  "SLASH_FORWARD": 191,
  "ACCENT_GRAVE": 192,
  "BRACKET_LEFT": 219,
  "SLASH_BACKWARD": 220,
  "BRACKET_RIGHT": 221,
  "QUOTE_SINGLE": 222,
  "INPUT_METHOD_EDITOR": 229,

  // Windows only (some incompatibilities)
  "WINDOWS_LEFT": 91,
  "WINDOWS_RIGHT": 92,
  "SELECT": 93,

  // OS X only (some incompatibilities)
  "OPTION": 18,
  "COMMAND_LEFT": 91,
  "COMMAND_RIGHT": 93,
  "F13": 124,
  "F14": 125,
  "F15": 126,
  "F16": 127,
  "F17": 128,
  "F18": 129,
  "F19": 130
};

/**
 * Determine if the given key code matches a key code for a character key, i.e.,
 * a key in the main section of the keyboard that causes a character to be
 * displayed.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a character key
 */
KeyCodes.isCharacterKey = function(keyCode) {
  // an alpha character
  if (KeyCodes.isAlphaKey(keyCode)) {
    return true;
  }

  // a number character
  if (KeyCodes.isNumberKey(keyCode)) {
    return true;
  }

  // a punctuation character
  if (KeyCodes.isPunctuationKey(keyCode)) {
    return true;
  }

  // add and subtract
  if (keyCode == KeyCodes.ADD || keyCode == KeyCodes.SUBTRACT) {
    return true;
  }

  // grave accent character
  if (keyCode == KeyCodes.ACCENT_GRAVE) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a key in the alphabet.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a key in the alphabet
 */
KeyCodes.isAlphaKey = function(keyCode) {
  return keyCode >= KeyCodes.A && keyCode <= KeyCodes.Z;
};

/**
 * Determine if the given key code matches a key code for a number key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a number key
 */
KeyCodes.isNumberKey = function(keyCode) {
  // number keys above letters
  if (keyCode >= KeyCodes.ZERO && keyCode <= KeyCodes.NINE) {
    return true;
  }

  // number pad keys
  if (keyCode >= KeyCodes.ZERO_NUMPAD && keyCode <= KeyCodes.NINE_NUMPAD) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a punctuation key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a punctuation key
 */
KeyCodes.isPunctuationKey = function(keyCode) {
  // space key
  if (keyCode == KeyCodes.SPACE) {
    return true;
  }

  // semicolon, equals, comma, hyphen, period, forward slash
  if (keyCode >= KeyCodes.SEMICOLON && keyCode <= KeyCodes.SLASH_FORWARD) {
    return true;
  }

  // left bracket, backward slash, right bracket, single quote
  if (keyCode >= KeyCodes.BRACKET_LEFT && keyCode <= KeyCodes.QUOTE_SINGLE) {
    return true;
  }

  // multiply and divide
  if (keyCode == KeyCodes.MULTIPLY || keyCode == KeyCodes.DIVIDE) {
    return true;
  }

  // decimal point
  if (keyCode == KeyCodes.POINT_DECIMAL) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a modifier key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a modifier key
 */
KeyCodes.isModifierKey = function(keyCode) {
  // shift, control, alt
  if (keyCode >= KeyCodes.SHIFT && keyCode <= KeyCodes.ALT) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a system key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a system key
 */
KeyCodes.isSystemKey = function(keyCode) {
  // left and right command as well as the left and right windows key and select
  if (keyCode >= KeyCodes.COMMAND_LEFT && keyCode <= KeyCodes.COMMAND_RIGHT) {
    return true;
  }

  // escape key
  if (keyCode == KeyCodes.ESCAPE) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a navigation key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a navigation key
 */
KeyCodes.isNavigationKey = function(keyCode) {
  // arrow key
  if (KeyCodes.isArrowKey(keyCode)) {
    return true;
  }

  // page up and down
  if (keyCode == KeyCodes.PAGE_UP || keyCode == KeyCodes.PAGE_DOWN) {
    return true;
  }

  // home and end
  if (keyCode == KeyCodes.HOME || keyCode == KeyCodes.END) {
    return true;
  }

  // tab key
  if (keyCode == KeyCodes.TAB) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for an arrow key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for an arrow key
 */
KeyCodes.isArrowKey = function(keyCode) {
  return keyCode >= KeyCodes.ARROW_LEFT && keyCode <= KeyCodes.ARROW_DOWN;
};

/**
 * Determine if the given key code matches a key code for an editing key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for an editing key
 */
KeyCodes.isEditingKey = function(keyCode) {
  // return and insert
  if (keyCode == KeyCodes.RETURN || keyCode == KeyCodes.INSERT) {
    return true;
  }

  // backspace and delete
  if (keyCode == KeyCodes.BACKSPACE || keyCode == KeyCodes.DELETE) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a key that removes a
 * character.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a key that removes a
 *         character
 */
KeyCodes.isRemovalKey = function(keyCode) {
  // backspace and delete
  if (keyCode == KeyCodes.BACKSPACE || keyCode == KeyCodes.DELETE) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a function key.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a function key
 */
KeyCodes.isFunctionKey = function(keyCode) {
  // regular function keys (F1 - F12)
  if (keyCode >= KeyCodes.F1 && keyCode <= KeyCodes.F12) {
    return true;
  }

  // addition function keys on OS X (F13 - F19)
  if (keyCode >= KeyCodes.F13 && keyCode <= KeyCodes.F19) {
    return true;
  }

  return false;
};

/**
 * Determine if the given key code matches a key code for a numpad key. Doesn't
 * match equals or enter because those are can't be differentiated from the keys
 * in the main part of the keyboard.
 * @param int keyCode key code
 * @return true if the key code matches a key code for a numpad key
 */
KeyCodes.isNumpadKey = function(keyCode) {
  // numpad numbers
  if (keyCode >= KeyCodes.ZERO_NUMPAD && keyCode <= KeyCodes.NINE_NUMPAD) {
    return true;
  }

  // add and subtract
  if (keyCode == KeyCodes.ADD || keyCode == KeyCodes.SUBTRACT) {
    return true;
  }

  // multiply and divide
  if (keyCode == KeyCodes.MULTIPLY || keyCode == KeyCodes.DIVIDE) {
    return true;
  }

  // decimal point and clear
  if (keyCode == KeyCodes.POINT_DECIMAL || keyCode == KeyCodes.CLEAR) {
    return true;
  }

  return false;
};

// -----------------------------------------------------------------------------

/**
 * Provides a stream of key and key combination presses from trigger elements
 * to stream listeners.
 * @see KeyPressStream::addTriggerElements()
 * @see KeyPressStream::addStreamListener()
 * @see KeyPressStream::addKeyCodeSequenceListener()
 */
function KeyPressStream() {
  // important: this method should be called first because much of this class
  // depends on having a unique identifier
  this.generateIdentifier();

  // finish initialization
  this.clearTriggerElements();
  this.clearStreamListeners();
  this.clearKeyCodeSequenceListeners();
  this.clearKeyComboStack();
  this.clearKeyCodeListenerIdentifiers();
  this.clearWasKeyComboPressed();
}

/**
 * Add the given elements to the list of the elements that trigger key presses
 * for the stream.
 * @param jQuery elements elements that trigger key presses
 * @see KeyPressStream::removeTriggerElements()
 * @see KeyPressStream::clearTriggerElements()
 */
KeyPressStream.prototype.addTriggerElements = function(elements) {
  var eventSignatureNamespace = this.getEventSignatureNamespace();

  // remove the event listeners if already applied so that they don't fire twice
  elements.off(eventSignatureNamespace);

  // add the keydown event listener
  elements.on(
    "keydown" + eventSignatureNamespace,
    cw.bindObjectMethodCallback(this, this.keyDownListener));

  // add the keyup event listener
  elements.on(
    "keyup" + eventSignatureNamespace,
    cw.bindObjectMethodCallback(this, this.keyUpListener));

  // add the elements to the list of those that are bound
  this._triggerElements = this._triggerElements.add(elements);
};

/**
 * Remove the given elements from the list of the elements that trigger key
 * presses for the stream.
 * @param jQuery elements elements to remove from the trigger elements
 * @see KeyPressStream::addTriggerElements()
 * @see KeyPressStream::clearTriggerElements()
 */
KeyPressStream.prototype.removeTriggerElements = function(elements) {
  // remove the event listeners
  elements.off(this.getEventSignatureNamespace());

  // remove the elements from the list of those that are bound
  this._triggerElements = this._triggerElements.filter(elements);
};

/**
 * Remove all event listeners from all the elements that trigger keydown and
 * keyup events for key code sequences and then clear the list of elements.
 * @see KeyPressStream::addTriggerElements()
 * @see KeyPressStream::removeTriggerElements()
 */
KeyPressStream.prototype.clearTriggerElements = function() {
  // remove event listeners if any elements have been bound
  if (this._triggerElements) {
    this._triggerElements.off(this.getEventSignatureNamespace());
  }

  // reset the list of bound elements
  this._triggerElements = jQuery();
};

/**
 * Add a stream listener callback.
 * @param callback listener listener callback
 * @return the identifier of the listener callback
 * @see KeyPressStream::removeStreamListener()
 * @see KeyPressStream::clearStreamListeners()
 * @see KeyPressStream::addKeyCodeSequenceListener()
 * @see KeyPressStream::removeKeyCodeSequenceListener()
 */
KeyPressStream.prototype.addStreamListener = function(listener) {
  var identifier;

  do {
    // generate a new, unused identifier for the listener
    identifier = this.generateListenerIdentifier();
  } while (this._streamListeners[identifier]);

  // add the listener to the list
  this._streamListeners[identifier] = listener;

  // return the listener identifier so that it can be removed
  return identifier;
};

/**
 * Remove a stream listener callback by its identifier.
 * @param string identifier the identifier of the listener callback
 * @see KeyPressStream::addStreamListener()
 * @see KeyPressStream::clearStreamListeners()
 * @see KeyPressStream::addKeyCodeSequenceListener()
 * @see KeyPressStream::removeKeyCodeSequenceListener()
 */
KeyPressStream.prototype.removeStreamListener = function(identifier) {
  delete this._streamListeners[identifier];
};

/**
 * Clear the array of stream listeners executed whenever a key or key
 * combination is pressed.
 * @see KeyPressStream::addStreamListener()
 * @see KeyPressStream::removeStreamListener()
 */
KeyPressStream.prototype.clearStreamListeners = function() {
  this._streamListeners = {};
};

/**
 * Add a listener callback for a specific key code or key code combination
 * sequence.
 * @param callback listener listener callback
 * @param keyCodeSequence key code or key code combination sequence
 * @return the identifier of the listener callback (so it can be removed)
 * @see KeyPressStream::removeKeyCodeSequenceListener()
 * @see KeyPressStream::clearKeyCodeSequenceListeners()
 * @see KeyPressStream::addStreamListener()
 * @see KeyPressStream::removeStreamListener()
 */
KeyPressStream.prototype.addKeyCodeSequenceListener = function(listener, keyCodeSequence) {
  var keyCodeSequenceIdentifier = this.getKeyCodeSequenceIdentifier(keyCodeSequence),
      listenerIdentifier = GenerateRandomString(5);

  // initialize the list of key code sequence listeners for the specific key
  // sequence, if necessary
  if (!this._keyCodeSequenceListeners[keyCodeSequenceIdentifier]) {
    this._keyCodeSequenceListeners[keyCodeSequenceIdentifier] = {};
  }

  // add the listener to the list of listener callbacks for the specific key
  // code sequence
  this._keyCodeSequenceListeners[keyCodeSequenceIdentifier][listenerIdentifier] = listener;

  // map the listener identifier to the key code sequence it's attached to so
  // that it can be quickly removed
  this._keyCodeListenerIdentifiers[listenerIdentifier] = keyCodeSequenceIdentifier;

  return listenerIdentifier;
};

/**
 * Remove a listener callback for a specific key code or key code combination
 * sequence.
 * @param string identifier the identifier of the listener callback
 * @see KeyPressStream::addKeyCodeSequenceListener()
 * @see KeyPressStream::clearKeyCodeSequenceListeners()
 * @see KeyPressStream::addStreamListener()
 * @see KeyPressStream::removeStreamListener()
 */
KeyPressStream.prototype.removeKeyCodeSequenceListener = function(identifier) {
  var keyCodeSequenceIdentifier = this._keyCodeListenerIdentifiers[identifier];

  // remove the listener identifer
  delete this._keyCodeListenerIdentifiers[identifier];

  // remove the key sequence listener callback
  if (this._keyCodeSequenceListeners[keySequenceIdentifier]) {
    delete this._keyCodeSequenceListeners[keySequenceIdentifier][identifier];
  }
};

/**
 * Clear the array of stream listeners executed whenever a specific key or key
 * combination is pressed.
 * @see KeyPressStream::addKeyCodeSequenceListener()
 * @see KeyPressStream::removeKeyCodeSequenceListener()
 */
KeyPressStream.prototype.clearKeyCodeSequenceListeners = function() {
  this._keyCodeSequenceListeners = {};
};

/**
 * The event listener used for keydown events from the trigger elements.
 * @param object event standard JavaScript event object
 * @protected
 */
KeyPressStream.prototype.keyDownListener = function(event) {
  var keyCode = event.which,
      isComboKey = this.isComboKey(keyCode);

  // clear the flag that keeps combos from firing when releasing keys
  this.clearWasKeyComboPressed();

  // immediately fire off the listeners if the key pressed isn't a combo key
  // and there isn't a combo sequence on the stack. this avoids a race condition
  // caused by the asynchronous nature of keydown events and fast typing
  if (!isComboKey && !this.isComboSequence()) {
    this.executeStreamListeners([keyCode], event);
  }

  // if the key code is a repeat, i.e., the key is being held down, and not a
  // combo key, then execute the listeners if the key is repeat a key code for
  // non-combo keys. waiting until there is a repeat allows for a short delay
  // and for key sequences with multiple non-combo keys after one or more
  // combo keys
  else if (!isComboKey && keyCode == this.peekKeyComboStack()) {
    this.executeStreamListeners(this.getKeyComboStack(), event);
  }

  // otherwise add it to the key combo stack because it's part of a sequence
  else {
    this.pushKeyComboStack(keyCode);
  }
};

/**
 * The event listener used for keyup events from the trigger elements.
 * @param object event standard JavaScript event object
 * @protected
 */
KeyPressStream.prototype.keyUpListener = function(event) {
  var keyCode = event.which;

  if (this.isComboSequence()) {
    if (!this.wasKeyComboPressed()) {
      this.keyComboWasPressed();
      this.executeStreamListeners(this.getKeyComboStack(), event);
    }

    // need to pop until seeing the key code because some keys won't fire a key
    // up listener after an external command executes, e.g., selecting all using
    // CMD+A
    this.popKeyComboStack(keyCode);
  }
};

/**
 * Execute the stream listeners using the given key codes as the key code
 * sequence.
 * @param array keyCodes an array of key codes
 * @param object event event object
 * @protected
 */
KeyPressStream.prototype.executeStreamListeners = function(keyCodes, event) {
  var keyCodeSequenceListeners, listener;
  var keyCodeSequence = new KeyCodeSequence(keyCodes),
      keyCodeSequenceIdentifier = this.getKeyCodeSequenceIdentifier(keyCodeSequence),
      triggerElement = jQuery(event.target);

  // execute all of the listener callbacks that receive every key code sequence
  for (var streamListenerIdentifier in this._streamListeners) {
    listener = this._streamListeners[streamListenerIdentifier];

    // use a short timeout to workaround issues with the trigger's value being
    // out of sync
    setTimeout(cw.bindObjectMethodCallback(
      listener, listener, keyCodeSequence, triggerElement), 0);
  }

  // execute the listener callbacks for specific key code sequences
  if (this._keyCodeSequenceListeners[keyCodeSequenceIdentifier]) {
    keyCodeSequenceListeners = this._keyCodeSequenceListeners[keyCodeSequenceIdentifier];
    for (var keyCodeSequenceListenerIdentifier in keyCodeSequenceListeners) {
      listener = keyCodeSequenceListeners[keyCodeSequenceListenerIdentifier];

      // use a short timeout to workaround issues with the trigger's value being
      // out of sync
      setTimeout(cw.bindObjectMethodCallback(
        listener, listener, triggerElement), 0);
    }
  }
};

/**
 * Generate an identifier string for a stream listener.
 * @return an identifier string for a stream listener
 * @protected
 */
KeyPressStream.prototype.generateListenerIdentifier = function() {
  return GenerateRandomString(5);
};

/**
 * Determine if the given key code represents a key that starts a key
 * combination, i.e., if it's a modifier or system key.
 * @param int keyCode key code
 * @return true if the key code represents the start of a key combination
 * @protected
 */
KeyPressStream.prototype.isComboKey = function(keyCode) {
  // modifier or system key
  return KeyCodes.isModifierKey(keyCode) || KeyCodes.isSystemKey(keyCode);
};

/**
 * Determine if the current key sequence is a key combination by looking at the
 * stack size.
 * @return true if the current key sequence is a key combination
 * @protected
 */
KeyPressStream.prototype.isComboSequence = function() {
  return this.getKeyComboStack().length > 0;
};

/**
 * Generates and sets the unique identifier for the object. The identifier is
 * used when creating event signatures.
 * @see KeyPressStream::getIdentifier()
 * @protected
 */
KeyPressStream.prototype.generateIdentifier = function() {
  this._identifier = GenerateRandomString(5);
};

/**
 * Fetch the identifier used to create event signatures.
 * @return the identifier used to create event signatures
 * @see KeyPressStream::generateIdentifer()
 * @protected
 */
KeyPressStream.prototype.getIdentifier = function() {
  return this._identifier;
};

/**
 * Get the namespace used for event listener signatures.
 * @return the namespace for event listener signatures
 * @protected
 */
KeyPressStream.prototype.getEventSignatureNamespace = function() {
  return ".keyboard.keypressstream." + this.getIdentifier();
};

/**
 * Get the identifier for the given key code sequence.
 * @param KeyCodeSequence keyCodeSequence key code or key code combo sequence
 * @return key code sequence identifier
 * @protected
 */
KeyPressStream.prototype.getKeyCodeSequenceIdentifier = function(keyCodeSequence) {
  return keyCodeSequence.toArray().toString();
};

/**
 * Clear the stack that is used to hold the key code sequence for a key
 * combination.
 * @see KeyPressStream::getKeyComboStack()
 * @see KeyPressStream::peekKeyComboStack()
 * @see KeyPressStream::pushKeyComboStack()
 * @see KeyPressStream::popKeyComboStack()
 * @protected
 */
KeyPressStream.prototype.clearKeyComboStack = function() {
  this._keyComboStack = [];
};

/**
 * Fetch the stack used to hold the key codes of a key combination.
 * @return the stack (array) used to hold the key codes of a key combination
 * @return the top key code on the stack or undefined if the stack is empty
 * @see KeyPressStream::clearKeyComboStack()
 * @see KeyPressStream::peekKeyComboStack()
 * @see KeyPressStream::pushKeyComboStack()
 * @see KeyPressStream::popKeyComboStack()
 * @protected
 */
KeyPressStream.prototype.getKeyComboStack = function() {
  // slice allows the class to return a copy instead of a reference
  return this._keyComboStack.slice(0);
};

/**
 * Fetch but don't remove the key code on the top of the key combination stack.
 * @return the top key code on the stack or undefined if the stack is empty
 * @see KeyPressStream::clearKeyComboStack()
 * @see KeyPressStream::getKeyComboStack()
 * @see KeyPressStream::pushKeyComboStack()
 * @see KeyPressStream::popKeyComboStack()
 * @protected
 */
KeyPressStream.prototype.peekKeyComboStack = function() {
  var keyCode;

  // only perform the stack operations if there is anything on it
  if (this.getKeyComboStack().length > 0) {
    // JavaScript doesn't have an Array.peek() function, so first the key code
    // needs to be popped off...
    keyCode = this.popKeyComboStack();

    // ...and then it needs to be pushed back on
    this.pushKeyComboStack(keyCode);
  }

  return keyCode;
};

/**
 * Push a key code onto the key combination stack.
 * @param int keyCode key code
 * @see KeyPressStream::clearKeyComboStack()
 * @see KeyPressStream::getKeyComboStack()
 * @see KeyPressStream::peekKeyComboStack()
 * @see KeyPressStream::popKeyComboStack()
 * @protected
 */
KeyPressStream.prototype.pushKeyComboStack = function(keyCode) {
  this._keyComboStack.push(keyCode);
};

/**
 * Remove and return a key code from the key combination stack. Optionally
 * allows key codes to be popped until a specific key code is seen, which is
 * useful because some key presses won't trigger a keyup event when pressed in
 * conjunction with a system key.
 * @param int until pop key codes until this key code is seen
 * @return the popped key code, the value of until, or undefined if empty
 * @see KeyPressStream::clearKeyComboStack()
 * @see KeyPressStream::getKeyComboStack()
 * @see KeyPressStream::peekKeyComboStack()
 * @see KeyPressStream::pushKeyComboStack()
 * @protected
 */
KeyPressStream.prototype.popKeyComboStack = function(until) {
  var keyCode;

  // just pop the top value off of the stack if not given a parameter
  if (typeof until == "undefined") {
    return this._keyComboStack.pop();
  }

  do {
    // pop from the stack until the key code is found or the stack becomes empty
    keyCode = this._keyComboStack.pop();
  } while (keyCode != until && this._keyComboStack.length);

  return keyCode;
};

/**
 * Clear the array of stream listener identifiers.
 * @see KeyPressStream::addKeyCodeSequenceListener()
 * @see KeyPressStream::removeKeyCodeSequenceListener()
 * @protected
 */
KeyPressStream.prototype.clearKeyCodeListenerIdentifiers = function() {
  this._keyCodeListenerIdentifiers = {};
};

/**
 * Reset the value used to flag when a key combination was pressed.
 * @see KeyPressStream::keyComboWasPressed()
 * @see KeyPressStream::wasKeyComboPressed()
 * @protected
 */
KeyPressStream.prototype.clearWasKeyComboPressed = function() {
  this._wasKeyComboPressed = false;
};

/**
 * Set the value used to flag when a key combination was pressed to true.
 * @see KeyPressStream::clearWasKeyComboPressed()
 * @see KeyPressStream::wasKeyComboPressed()
 * @protected
 */
KeyPressStream.prototype.keyComboWasPressed = function() {
  this._wasKeyComboPressed = true;
};

/**
 * Return the value used to flag when a key combination was pressed.
 * @return bool the value used to flag when a key combination was pressed
 * @see KeyPressStream::clearWasKeyComboPressed()
 * @see KeyPressStream::keyComboWasPressed()
 * @protected
 */
KeyPressStream.prototype.wasKeyComboPressed = function() {
  return this._wasKeyComboPressed;
};

/**
 * The unique identifier for the constructed object. This is used to create
 * unique event signatures for the keydown and keyup event listeners on a
 * per-object basis.
 * @var string _identifier
 * @protected
 */
KeyPressStream.prototype._identifier;

/**
 * Holds the elements that trigger the keydown and keyup events that are used to
 * grab key sequences.
 * @var jQuery _triggerElements
 * @protected
 */
KeyPressStream.prototype._triggerElements;

/**
 * Holds the stack that is used to hold the key codes of a key combination.
 * @var array _keyComboStack
 * @protected
 */
KeyPressStream.prototype._keyComboStack;

/**
 * Holds an array of listener callbacks executed whenever a key or key
 * combination is pressed.
 * @var array _streamListeners
 * @protected
 */
KeyPressStream.prototype._streamListeners;

/**
 * Holds an array of listener callbacks executed whenever a specific key or
 * key combination is pressed.
 * @var object _keyCodeSequenceListeners
 * @protected
 */
KeyPressStream.prototype._keyCodeSequenceListeners;

/**
 * Holds an array of listener callback identifiers mapped to specific key code
 * or key code combination sequences.
 * @var object _keyCodeListenerIdentifers
 */
KeyPressStream.prototype._keyCodeListenerIdentifiers;

/**
 * Predicate value used to flag when a key combination was pressed. This is
 * used to keep key combos from firing when keys are released, e.g., stop
 * CTRL+ALT from firing when releasing DELETE after pressing CTRL+ALT+DELETE.
 * @var bool _wasKeyComboPressed
 * @protected
 */
KeyPressStream.prototype._wasKeyComboPressed;

// -----------------------------------------------------------------------------

/**
 * Encapsulates a key code sequence, which could be a single key or a key
 * combination. These are typically from the KeyPressStream class.  The key
 * codes can be passed in an array or as separate arguments.
 * @param array keyCodeSequence the key code sequence to encapsulate
 * @see KeyPressStream
 */
function KeyCodeSequence(keyCodeSequence) {
  // use all of the arguments as the key code sequence
  if (!(keyCodeSequence instanceof Array)) {
    keyCodeSequence = Array.prototype.slice.call(arguments);
  }

  this._keyCodeSequence = keyCodeSequence;
}

/**
 * Get the key code at the given index within the sequence.
 * @param int index key code index in the key code sequence
 * @return key code at the index or undefined if none
 */
KeyCodeSequence.prototype.getKeyCodeAt = function(index) {
  return this._keyCodeSequence[index];
};

/**
 * Get the array representation of this key code sequence.
 * @return the array representation of this key code sequence
 */
KeyCodeSequence.prototype.toArray = function() {
  // slice allows the class to return a copy instead of a reference
  return this._keyCodeSequence.slice(0);
};

/**
 * Get the string representation of this key code sequence.
 * @return the string representation of this key code sequence
 */
KeyCodeSequence.prototype.toString = function() {
  return this._keyCodeSequence.toString();
};

/**
 * Determine whether the key code sequence is a key combination as opposed to a
 * single key code.
 * @return true if the key code sequence is a key combination or false otherwise
 */
KeyCodeSequence.prototype.isKeyCombo = function() {
  return this._keyCodeSequence.length > 1;
};

/**
 * Determine if the given key code sequence is equal to this key code sequence.
 * @param KeyCodeSequence keyCodeSequence key code sequence for checking
 * @return true if the key code sequences are equal and false otherwise
 */
KeyCodeSequence.prototype.isEqualToKeyCodeSequence = function(keyCodeSequence) {
  return keyCodeSequence.toString() == this.toString();
};

/**
 * Determine if the given key code sequence is equal to the sequence of key
 * codes passed in as arguments. The key codes can be passed in an array or as
 * separate arguments.
 * @param array keyCodes key codes
 * @return true if the key code sequences is equal to the key codes given
 */
KeyCodeSequence.prototype.isEqualToKeyCodes = function(keyCodes) {
  // use all of the arguments as the key codes
  if (!(keyCodes instanceof Array)) {
    keyCodes = Array.prototype.slice.call(arguments);
  }

  return keyCodes.toString() == this.toString();
};

/**
 * Holds the array representation of the key code sequence
 * @var array _keyCodeSequence
 * @protected
 */
KeyCodeSequence.prototype._keyCodeSequence;

// -----------------------------------------------------------------------------

/**
 * Prevents the browser from performing default actions when certain key
 * sequences are pressed within some trigger elements.
 * @extends KeyPressStream
 * @see DefaultActionBlocker::addKeyCodeSequenceToBlock()
 */
function DefaultActionBlocker() {
  DefaultActionBlocker.base.call(this);

  // finish initialization
  this.clearKeyCodeSequenceTriggers();
} cw.extend(DefaultActionBlocker, KeyPressStream);

/**
 * Add a key code sequence that should block a default action of the browser
 * when it is encountered.
 * @param KeyCodeSequence keyCodeSequence key code sequence
 * @see DefaultActionBlocker::removeKeyCodeSequenceToBlock()
 * @see DefaultActionBlocker::clearKeyCodeSequencesToBlock()
 */
DefaultActionBlocker.prototype.addKeyCodeSequenceTrigger= function(keyCodeSequence) {
  this._keyCodeSequenceTriggers[keyCodeSequence.toString()] = keyCodeSequence;
};

/**
 * Remove a key code sequence that should block a default action of the browser
 * when it is encountered.
 * @param KeyCodeSequence keyCodeSequence key code sequence
 * @see DefaultActionBlocker::addKeyCodeSequenceToBlock()
 * @see DefaultActionBlocker::clearKeyCodeSequencesToBlock()
 */
DefaultActionBlocker.prototype.removeKeyCodeSequenceTrigger = function(keyCodeSequence) {
  delete this._keyCodeSequenceTriggers[keyCodeSequence.toString()];
};

/**
 * Remove all key code sequences that should block a default action of the
 * browser when encountered.
 * @see DefaultActionBlocker::addKeyCodeSequenceToBlock()
 * @see DefaultActionBlocker::removeKeyCodeSequencesToBlock()
 */
DefaultActionBlocker.prototype.clearKeyCodeSequenceTriggers = function() {
  this._keyCodeSequenceTriggers = {};
};

/**
 * The event listener used for keydown events from the trigger elements.
 * @param object event standard JavaScript event object
 * @protected
 */
DefaultActionBlocker.prototype.keyDownListener = function(event) {
  var keyCode = event.which,
      isComboKey = this.isComboKey(keyCode);

  // clear the flag that keeps combos from firing when releasing keys
  this.clearWasKeyComboPressed();

  // immediately prevent a default action if the key pressed isn't a combo key
  // and there isn't a combo sequence on the stack. this avoids a race condition
  // caused by the asynchronous nature of keydown events and fast typing
  if (!isComboKey && !this.isComboSequence()) {
    this.preventDefaultActionIfNecessary([keyCode], event);
  }

  // if the key code is a repeat, i.e., the key is being held down, and not a
  // combo key, then prevent a default action if the key is repeat a key code
  // for non-combo keys. waiting until there is a repeat allows for a short
  // delay and for key sequences with multiple non-combo keys after one or more
  // combo keys
  else if (!isComboKey && keyCode == this.peekKeyComboStack()) {
    this.preventDefaultActionIfNecessary(this.getKeyComboStack(), event);
  }

  // otherwise add it to the key combo stack because it's part of a sequence and
  // see if the current sequence should prevent a default browser action
  else {
    this.pushKeyComboStack(keyCode);
    this.preventDefaultActionIfNecessary(this.getKeyComboStack(), event);
  }
};

/**
 * The event listener used for keyup events from the trigger elements.
 * @param object event standard JavaScript event object
 * @protected
 */
DefaultActionBlocker.prototype.keyUpListener = function(event) {
  var keyCode = event.which;

  if (this.isComboSequence()) {
    if (!this.wasKeyComboPressed()) {
      this.keyComboWasPressed();
      this.preventDefaultActionIfNecessary(this.getKeyComboStack(), event);
    }

    // need to pop until seeing the key code because some keys won't fire a key
    // up listener after an external command executes, e.g., selecting all using
    // CMD+A
    this.popKeyComboStack(keyCode);
  }
};

/**
 * Determine if the given array of key codes is a sequence that should prevent a
 * default browser action and block the action if so.
 * @param array keyCodes array of key codes
 * @param object event standard JavaScript event object
 * @protected
 */
DefaultActionBlocker.prototype.preventDefaultActionIfNecessary = function(keyCodes, event) {
  for (var i in this._keyCodeSequenceTriggers) {
    if (this._keyCodeSequenceTriggers[i].isEqualToKeyCodes(keyCodes)) {
      event.preventDefault();
      event.stopPropagation();
      return;
    }
  }
};

/**
 * Get the namespace used for event listener signatures.
 * @return the namespace for event listener signatures
 * @extends KeyPressStream::getEventSignatureNamespace()
 * @protected
 */
DefaultActionBlocker.prototype.getEventSignatureNamespace = function() {
  return ".keyboard.defaultactionblocker." + this.getIdentifier();
};

/**
 * Holds the array of key code sequences that are used to determine when a
 * default action of the browser should be blocked.
 * @var array _keyCodeSequenceTriggers
 * @protected
 */
DefaultActionBlocker.prototype._keyCodeSequenceTriggers;

// -----------------------------------------------------------------------------

/**
 * Namespace object for key codes used to determine whether a given key code is
 * equal to a standard key combination in various contexts.
 */
var StandardKeyCombos = {};

// -----------------------------------------------------------------------------

/**
 * Namespace for key code constants used to determine whether a given key code
 * sequence is equal to a key combination in Emacs.
 */
StandardKeyCombos.Emacs = {};

/**
 * Key codes for the next-line key combination in Emacs.
 */
StandardKeyCombos.Emacs.NEXT_LINE = [KeyCodes.CONTROL, KeyCodes.N];

/**
 * Key codes for the previous-line key combination in Emacs.
 */
StandardKeyCombos.Emacs.PREVIOUS_LINE = [KeyCodes.CONTROL, KeyCodes.P];

/**
 * Key codes for the beginning-of-line key combination in Emacs.
 */
StandardKeyCombos.Emacs.BEGINNING_OF_LINE = [KeyCodes.CONTROL, KeyCodes.A];

/**
 * Key codes for the end-of-line key combination in Emacs.
 */
StandardKeyCombos.Emacs.END_OF_LINE = [KeyCodes.CONTROL, KeyCodes.E];

/**
 * Key codes for the scroll-up key combination in Emacs.
 */
StandardKeyCombos.Emacs.SCROLL_UP = [KeyCodes.CONTROL, KeyCodes.V];

/**
 * Key codes for the scroll-down key combination in Emacs.
 */
StandardKeyCombos.Emacs.SCROLL_DOWN = [KeyCodes.ALT, KeyCodes.V];

/**
 * Key codes for the recenter key combination in Emacs.
 */
StandardKeyCombos.Emacs.RECENTER = [KeyCodes.CONTROL, KeyCodes.L];

/**
 * Key codes for the beginning-of-buffer key combination in Emacs.
 */
StandardKeyCombos.Emacs.BEGINNING_OF_BUFFER = [
  KeyCodes.ALT,
  KeyCodes.SHIFT,
  KeyCodes.COMMA];

/**
 * Key codes for the end-of-buffer key combination in Emacs.
 */
StandardKeyCombos.Emacs.END_OF_BUFFER = [
  KeyCodes.ALT,
  KeyCodes.SHIFT,
  KeyCodes.PERIOD];

// -----------------------------------------------------------------------------

// exported items
this.KeyCodes = KeyCodes;
this.KeyPressStream = KeyPressStream;
this.KeyCodeSequence = KeyCodeSequence;
this.DefaultActionBlocker = DefaultActionBlocker;
this.StandardKeyCombos = StandardKeyCombos;

});
