###
Copyrights 2011, the repl.it project.
Licensed under the MIT license
###

# Shorthand for jQuery.
$ = jQuery

# The states in which the console can be.
STATE_INPUT = 0
STATE_OUTPUT = 1
STATE_PROMPT = 2

# Key code values.
KEY_ENTER = 13
KEY_TAB = 9
KEY_DELETE = 46
KEY_BACKSPACE = 8
KEY_LEFT = 37
KEY_RIGHT = 39
KEY_UP = 38
KEY_DOWN = 40
KEY_HOME = 36
KEY_END = 35
KEY_PAGE_UP = 33
KEY_PAGE_DOWN = 34

# CSS classes
CLASS_PREFIX = 'jqconsole-'
CLASS_CURSOR = "#{CLASS_PREFIX}cursor"
CLASS_HEADER = "#{CLASS_PREFIX}header"
CLASS_PROMPT = "#{CLASS_PREFIX}prompt"
CLASS_OLD_PROMPT = "#{CLASS_PREFIX}old-prompt"
CLASS_INPUT = "#{CLASS_PREFIX}input"
CLASS_BLURRED = "#{CLASS_PREFIX}blurred"

# Frequently used string literals
E_KEYPRESS = 'keypress'
EMPTY_SPAN = '<span/>'
EMPTY_DIV = '<div/>'
EMPTY_SELECTOR = ':empty'
NEWLINE = '\n'

# Default prompt text for main and continuation prompts.
DEFAULT_PROMPT_LABEL = '>>> '
DEFAULT_PROMPT_CONINUE_LABEL = '... '

# The default number of spaces inserted when indenting.
DEFAULT_INDENT_WIDTH = 2

CLASS_ANSI = "#{CLASS_PREFIX}ansi-"
ESCAPE_CHAR = '\x1B'
ESCAPE_SYNTAX = /\[(\d*)(?:;(\d*))*m/

class Ansi
  COLORS: ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']

  constructor: ->
    @klasses = [];

  _append: (klass) =>
    klass = "#{CLASS_ANSI}#{klass}"
    if @klasses.indexOf(klass) is -1
      @klasses.push klass

  _remove: (klasses...) =>
    for klass in klasses
      if klass in ['fonts', 'color', 'background-color']
        @klasses = (cls for cls in @klasses when cls.indexOf(klass) isnt CLASS_ANSI.length)
      else
        klass = "#{CLASS_ANSI}#{klass}"
        @klasses = (cls for cls in @klasses when cls isnt klass)

  _color: (i) => @COLORS[i]

  _style: (code) =>
    code = 0 if code == ''
    code = parseInt code

    return if isNaN code

    switch code
      when 0  then @klasses = []
      when 1  then @_append 'bold'
      when 2  then @_append 'lighter'
      when 3  then @_append 'italic'
      when 4  then @_append 'underline'
      when 5  then @_append 'blink'
      when 6  then @_append 'blink-rapid'
      when 8  then @_append 'hidden'
      when 9  then @_append 'line-through'
      when 10 then @_remove 'fonts'
      when 11,12,13,14,15,16,17,18,19
        @_remove 'fonts'
        @_append "fonts-#{code - 10}"
      when 20 then @_append 'fraktur'
      when 21 then @_remove 'bold', 'lighter'
      when 22 then @_remove 'bold', 'lighter'
      when 23 then @_remove 'italic', 'fraktur'
      when 24 then @_remove 'underline'
      when 25 then @_remove 'blink', 'blink-rapid'
      when 28 then @_remove 'hidden'
      when 29 then @_remove 'line-through'
      when 30,31,32,33,34,35,36,37
        @_remove 'color'
        @_append 'color-' + @_color code - 30
      when 39 then @_remove 'color'
      when 40,41,42,43,44,45,46,47
        @_remove 'background-color'
        @_append 'background-color-' + @_color code - 40
      when 49 then @_remove 'background-color'
      when 51 then @_append 'framed'
      when 53 then @_append 'overline'
      when 54 then @_remove 'framed'
      when 55 then @_remove 'overline'

  getClasses: => @klasses.join ' '

  _openSpan: (text) => "<span class=\"#{@getClasses()}\">#{text}"
  _closeSpan: (text) => "#{text}</span>"

  stylize: (text) =>
    text = @_openSpan text

    i = 0
    while (i = text.indexOf(ESCAPE_CHAR ,i)) and i isnt -1
      if d = text[i...].match ESCAPE_SYNTAX
        @_style code for code in d[1...]
        text = @_closeSpan(text[0...i]) + @_openSpan text[i + 1 + d[0].length...]
      else i++

    return @_closeSpan text

# Helper functions
spanHtml = (klass, content) -> "<span class=\"#{klass}\">#{content or ''}</span>"

class JQConsole
  # Creates a console.
  #   @arg container: The DOM element into which the console is inserted.
  #   @arg header: Text to print at the top of the console on reset. Optional.
  #     Defaults to an empty string.
  #   @arg prompt_label: The label to show before the command prompt. Optional.
  #     Defaults to DEFAULT_PROMPT_LABEL.
  #   @arg prompt_continue: The label to show before continuation lines of the
  #     command prompt. Optional. Defaults to DEFAULT_PROMPT_CONINUE_LABEL.
  constructor: (outer_container, header, prompt_label, prompt_continue_label) ->
    # Mobile devices supported sniff.
    @isMobile = !!navigator.userAgent.match /iPhone|iPad|iPod|Android/i
    @isIos = !!navigator.userAgent.match /iPhone|iPad|iPod/i
    @isAndroid = !!navigator.userAgent.match /Android/i

    @$window = $(window)

    # The header written when the console is reset.
    @header = header or ''

    # The prompt label used by Prompt().
    @prompt_label_main = if typeof prompt_label == 'string'
      prompt_label
    else
      DEFAULT_PROMPT_LABEL
    @prompt_label_continue = (prompt_continue_label or
                              DEFAULT_PROMPT_CONINUE_LABEL)

    # How many spaces are inserted when a tab character is pressed.
    @indent_width = DEFAULT_INDENT_WIDTH

    # By default, the console is in the output state.
    @state = STATE_OUTPUT

    # A queue of input/prompt operations waiting to be called. The items are
    # bound functions ready to be called.
    @input_queue = []

    # The function to call when input is accepted. Valid only in
    # input/prompt mode.
    @input_callback = null
    # The function to call to determine whether the input should continue to the
    # next line.
    @multiline_callback = null

    # A table of all "recorded" inputs given so far.
    @history = []
    # The index of the currently selected history item. If this is past the end
    # of @history, then the user has not selected a history item.
    @history_index = 0
    # The command which the user was typing before browsing history. Keeping
    # track of this allows us to restore the user's command if they browse the
    # history then decide to go back to what they were typing.
    @history_new = ''
    # Whether the current input operation is using history.
    @history_active = false

    # A table of custom shortcuts, mapping character codes to callbacks.
    @shortcuts = {}

    @$container = $('<div/>').appendTo outer_container
    @$container.css
      'top': 0
      'left': 0
      'right': 0
      'bottom': 0
      'position': 'absolute'
      'overflow': 'auto'

    # The main console area. Everything else happens inside this.
    @$console = $('<pre class="jqconsole"/>').appendTo @$container
    @$console.css
      'margin': 0
      'position': 'relative'
      'min-height': '100%'
      'box-sizing': 'border-box'
      '-moz-box-sizing': 'border-box'
      '-webkit-box-sizing': 'border-box'

    # Whether the console currently has focus.
    @$console_focused = true

    # On screen somehow invisible textbox for input.
    # Copied from codemirror2, this works for both mobile and desktop browsers.
    @$input_container = $(EMPTY_DIV).appendTo @$container
    @$input_container.css
      position: 'absolute'
      width: 1
      height: 0
      overflow: 'hidden'

    # On android autocapitlize works for input.
    @$input_source = if @isAndroid then $('<input/>') else $('<textarea/>')
    @$input_source.attr
      wrap: 'off'
      autocapitalize: 'off'
      autocorrect: 'off'
      spellcheck: 'false'
      autocomplete: 'off'
    @$input_source.css
      position: 'absolute'
      width: 2
    @$input_source.appendTo @$input_container

    @$composition = $(EMPTY_DIV)
    @$composition.addClass "#{CLASS_PREFIX}composition"
    @$composition.css
      display: 'inline'
      position: 'relative'

    # Hash containing all matching settings
    # openings/closings[char] = matching_config
    # where char is the opening/closing character.
    # clss is an array of classes for fast unhighlighting
    # for matching_config see Match method
    @matchings =
      openings: {}
      closings: {}
      clss: []

    @ansi = new Ansi()

    # Prepare console for interaction.
    @_InitPrompt()
    @_SetupEvents()
    @Write @header, CLASS_HEADER

    # Save this instance to be accessed if lost.
    $(outer_container).data 'jqconsole', this

  #### Reset methods

  # Resets the history into intitial state.
  ResetHistory: ->
    @SetHistory []

  # Resets the shortcut configuration.
  ResetShortcuts: ->
    @shortcuts = {}

  # Resets the matching configuration.
  ResetMatchings: ->
    @matchings =
      openings: {}
      closings: {}
      clss: []

  # Resets the console to its initial state.
  Reset: ->
    if @state != STATE_OUTPUT then @ClearPromptText true
    @state = STATE_OUTPUT
    @input_queue = []
    @input_callback = null
    @multiline_callback = null
    @ResetHistory()
    @ResetShortcuts()
    @ResetMatchings()
    @$prompt.detach()
    @$input_container.detach()
    @$console.html ''
    @$prompt.appendTo @$console
    @$input_container.appendTo @$container
    @Write @header, CLASS_HEADER
    return undefined

  #### History Methods

  # Get the current history
  GetHistory: ->
  	@history

  # Set the history
  SetHistory: (history) ->
  	@history = history.slice()
  	@history_index = @history.length

  ###------------------------ Shortcut Methods -----------------------------###

  # Checks the type/value of key codes passed in for registering/unregistering
  #   shortcuts and handles accordingly.
  _CheckKeyCode: (key_code) ->
    if isNaN key_code
      key_code = key_code.charCodeAt 0
    else
      key_code = parseInt key_code, 10

    if not (0 < key_code < 256) or isNaN key_code
      throw new Error 'Key code must be a number between 0 and 256 exclusive.'

    return key_code

  # A helper function responsible for calling the register/unregister callback
  #   twice passing in both the upper and lower case letters.
  _LetterCaseHelper: (key_code, callback)->
    callback key_code
    if 65 <= key_code <= 90 then callback key_code + 32
    if 97 <= key_code <= 122 then callback key_code - 32

  # Registers a Ctrl+Key shortcut.
  #   @arg key_code: The code of the key pressing which (when Ctrl is held) will
  #     trigger this shortcut. If a string is provided, the character code of
  #     the first character is taken.
  #   @arg callback: A function called when the shortcut is pressed; "this" will
  #     point to the JQConsole object.
  RegisterShortcut: (key_code, callback) ->
    key_code = @_CheckKeyCode key_code
    if typeof callback != 'function'
      throw new Error 'Callback must be a function, not ' + callback + '.'

    addShortcut = (key) =>
      if key not of @shortcuts then @shortcuts[key] = []
      @shortcuts[key].push callback

    @_LetterCaseHelper key_code, addShortcut
    return undefined

  # Removes a Ctrl+Key shortcut from shortcut registry.
  #   @arg key_code: The code of the key pressing which (when Ctrl is held) will
  #     trigger this shortcut. If a string is provided, the character code of
  #     the first character is taken.
  #   @arg handler: The handler that was used when registering the shortcut,
  #     if not supplied then all shortcut handlers corrosponding to the key
  #     would be removed.
  UnRegisterShortcut: (key_code, handler) ->
    key_code = @_CheckKeyCode key_code

    removeShortcut = (key)=>
      if key of @shortcuts
        if handler
          @shortcuts[key].splice @shortcuts[key].indexOf(handler), 1
        else
          delete @shortcuts[key]

    @_LetterCaseHelper key_code, removeShortcut
    return undefined

  ###---------------------- END Shortcut Methods ---------------------------###

  # Returns the 0-based number of the column on which the cursor currently is.
  GetColumn: ->
    @$prompt_right.detach()
    @$prompt_cursor.text ''
    lines = @$console.text().split NEWLINE
    @$prompt_cursor.html '&nbsp;'
    @$prompt_cursor.after @$prompt_right
    return lines[lines.length - 1].length

  # Returns the 0-based number of the line on which the cursor currently is.
  GetLine: ->
    return @$console.text().split(NEWLINE).length - 1

  # Clears the contents of the prompt.
  #   @arg clear_label: If true, also clears the main prompt label (e.g. ">>>").
  ClearPromptText: (clear_label) ->
    if @state == STATE_OUTPUT
      throw new Error 'ClearPromptText() is not allowed in output state.'
    @$prompt_before.html ''
    @$prompt_after.html ''
    @$prompt_label.text if clear_label then '' else @_SelectPromptLabel false
    @$prompt_left.text ''
    @$prompt_right.text ''
    return undefined

  # Returns the contents of the prompt.
  #   @arg full: If true, also includes the prompt labels (e.g. ">>>").
  GetPromptText: (full) ->
    if @state == STATE_OUTPUT
      throw new Error 'GetPromptText() is not allowed in output state.'

    if full
      @$prompt_cursor.text ''
      text = @$prompt.text()
      @$prompt_cursor.html '&nbsp;'
      return text
    else
      getPromptLines = (node) ->
        buffer = []
        node.children().each -> buffer.push $(@).children().last().text()
        return buffer.join NEWLINE

      before = getPromptLines @$prompt_before
      if before then before += NEWLINE

      current = @$prompt_left.text() + @$prompt_right.text()

      after = getPromptLines @$prompt_after
      if after then after = NEWLINE + after

      return before + current + after

  # Sets the contents of the prompt.
  #   @arg text: The text to put in the prompt. May contain multiple lines.
  SetPromptText: (text) ->
    if @state == STATE_OUTPUT
      throw new Error 'SetPromptText() is not allowed in output state.'
    @ClearPromptText false
    @_AppendPromptText text
    @_ScrollToEnd()
    return undefined

  # Replaces the main prompt label.
  #   @arg main_label: The new main label for the next prompt.
  #   @arg continue_label: The new continuation label for the next prompt. Optional.
  SetPromptLabel: (main_label, continue_label) ->
    @prompt_label_main = main_label
    if continue_label?
      @prompt_label_continue = continue_label
    return undefined

  # Writes the given text to the console in a <span>, with an optional class.
  #   @arg text: The text to write.
  #   @arg cls: The class to give the span containing the text. Optional.
  Write: (text, cls, escape=true) ->
    if escape
      text = @ansi.stylize $(EMPTY_SPAN).text(text).html()

    span = $(EMPTY_SPAN).html text
    if cls? then span.addClass cls
    @Append span

  # Adds a dom node, where any text would have been inserted
  #   @arg node: The node to insert.
  Append: (node) ->
  	$node = $(node).insertBefore @$prompt
  	@_ScrollToEnd()
  	# Force reclaculation of the cursor's position.
  	@$prompt_cursor.detach().insertAfter @$prompt_left
  	return $node

  # Starts an input operation. If another input or prompt operation is currently
  # underway, the new input operation is enqueued and will be called when the
  # current operation and all previously enqueued operations finish.
  #   @arg input_callback: A function called with the user's input when the
  #     user presses Enter and the input operation is complete.
  Input: (input_callback) ->
    if @state == STATE_PROMPT
      # Input operation has a higher priority, Abort and defer current prompt
      # by putting it on top of the queue.
      current_input_callback = @input_callback
      current_multiline_callback = @multiline_callback
      current_history_active = @history_active
      current_async_multiline = @async_multiline
      @AbortPrompt()
      @input_queue.unshift => @Prompt current_history_active,
                                      current_input_callback,
                                      current_multiline_callback,
                                      current_async_multiline
    else if @state != STATE_OUTPUT
      @input_queue.push => @Input input_callback
      return
    @history_active = false
    @input_callback = input_callback
    @multiline_callback = null
    @state = STATE_INPUT
    @$prompt.attr 'class', CLASS_INPUT
    @$prompt_label.text @_SelectPromptLabel false
    @Focus()
    @_ScrollToEnd()
    return undefined

  # Starts a command prompt operation. If another input or prompt operation is
  # currently underway, the new prompt operation is enqueued and will be called
  # when the current operation and all previously enqueued operations finish.
  #   @arg history_enabled: Whether this input should use history. If true, the
  #     user can select the input from history, and their input will also be
  #     added as a new history item.
  #   @arg result_callback: A function called with the user's input when the
  #     user presses Enter and the prompt operation is complete.
  #   @arg multiline_callback: If specified, this function is called when the
  #     user presses Enter to check whether the input should continue to the
  #     next line. The function must return one of the following values:
  #       false: the input operation is completed.
  #       0: the input continues to the next line with the current indent.
  #       N (int): the input continues to the next line, and the current indent
  #         is adjusted by N, e.g. -2 to unindent two levels.
  Prompt: (history_enabled, result_callback, multiline_callback, async_multiline) ->
    if @state != STATE_OUTPUT
      @input_queue.push =>
        @Prompt history_enabled, result_callback, multiline_callback, async_multiline
      return
    @history_active = history_enabled
    @input_callback = result_callback
    @multiline_callback = multiline_callback
    @async_multiline = async_multiline
    @state = STATE_PROMPT
    @$prompt.attr 'class', CLASS_PROMPT + ' ' + @ansi.getClasses()
    @$prompt_label.text @_SelectPromptLabel false
    @Focus()
    @_ScrollToEnd()
    return undefined

  # Aborts the current prompt operation and returns to output mode or the next
  # queued input/prompt operation.
  AbortPrompt: ->
    if @state != STATE_PROMPT
      throw new Error 'Cannot abort prompt when not in prompt state.'
    @Write @GetPromptText(true) + NEWLINE, CLASS_OLD_PROMPT
    @ClearPromptText true
    @state = STATE_OUTPUT
    @input_callback = @multiline_callback = null
    @_CheckInputQueue()
    return undefined

  # Sets focus on the console's hidden input box so input can be read.
  Focus: ->
    @$input_source.focus() if not @IsDisabled()
    return undefined

  # Sets the number of spaces inserted when indenting.
  SetIndentWidth: (width) ->
    @indent_width = width

  # Returns the number of spaces inserted when indenting.
  GetIndentWidth: ->
    return @indent_width

  # Registers character matching settings for a single matching
  #   @arg open: the openning character
  #   @arg close: the closing character
  #   @arg cls: the html class to add to the matched characters
  RegisterMatching: (open, close, cls) ->
      match_config =
        opening_char: open
        closing_char: close
        cls: cls

      @matchings.clss.push(cls)
      @matchings.openings[open] = match_config
      @matchings.closings[close] = match_config

  # Unregisters a character matching. cls is optional.
  UnRegisterMatching: (open, close) ->
    cls = @matchings.openings[open].cls
    delete @matchings.openings[open]
    delete @matchings.closings[close]
    @matchings.clss.splice @matchings.clss.indexOf(cls), 1

  # Dumps the content of the console before the current prompt.
  Dump: ->
    $elems = @$console
      .find(".#{CLASS_HEADER}")
      .nextUntil(".#{CLASS_PROMPT}")
      .addBack()

    return (
      for elem in $elems
        if $(elem).is ".#{CLASS_OLD_PROMPT}"
          $(elem).text().replace /^\s+/, '>>> '
        else
          $(elem).text()
    ).join ''

  # Gets the current prompt state.
  GetState: ->
    return if @state is STATE_INPUT
      'input'
     else if @state is STATE_OUTPUT
      'output'
    else
      'prompt'

  # Disables focus and input on the console.
  Disable: ->
    @$input_source.attr 'disabled', on
    @$input_source.blur();

  # Enables focus and input on the console.
  Enable: ->
    @$input_source.attr 'disabled', off

  # Returns true if the console is disabled.
  IsDisabled: ->
    return Boolean @$input_source.attr 'disabled'

  # Moves the cursor to the start of the current prompt line.
  #   @arg all_lines: If true, moves to the beginning of the first prompt line,
  #     instead of the beginning of the current.
  MoveToStart: (all_lines) ->
    @_MoveTo all_lines, true
    return undefined

  # Moves the cursor to the end of the current prompt line.
  MoveToEnd: (all_lines) ->
    @_MoveTo all_lines, false
    return undefined

  # Clear the console keeping only the prompt.
  Clear: ->
    @$console
      .find(".#{CLASS_HEADER}")
      .nextUntil(".#{CLASS_PROMPT}")
      .addBack()
      .text ''
    # Bug in Chrome were the cursor's position is not recalculated
    @$prompt_cursor.detach()
    @$prompt_after.before @$prompt_cursor

  ###------------------------ Private Methods -------------------------------###

  _CheckInputQueue: ->
    if @input_queue.length
      @input_queue.shift()()

  # Creates the movable prompt span. When the console is in input mode, this is
  # shown and allows user input. The structure of the spans are as follows:
  # $prompt
  #   $prompt_before
  #     line1
  #       prompt_label
  #       prompt_content
  #     ...
  #     lineN
  #       prompt_label
  #       prompt_content
  #   $prompt_current
  #     $prompt_label
  #     $prompt_left
  #     $prompt_cursor
  #     $prompt_right
  #   $prompt_after
  #     line1
  #       prompt_label
  #       prompt_content
  #     ...
  #     lineN
  #       prompt_label
  #       prompt_content
  _InitPrompt: ->
    # The main prompt container.
    @$prompt = $(spanHtml(CLASS_INPUT)).appendTo @$console
    # The main divisions of the prompt - the lines before the current line, the
    # current line, and the lines after it.
    @$prompt_before = $(EMPTY_SPAN).appendTo @$prompt
    @$prompt_current = $(EMPTY_SPAN).appendTo @$prompt
    @$prompt_after = $(EMPTY_SPAN).appendTo @$prompt

    # The subdivisions of the current prompt line - the static prompt label
    # (e.g. ">>> "), and the editable text to the left and right of the cursor.
    @$prompt_label = $(EMPTY_SPAN).appendTo @$prompt_current
    @$prompt_left = $(EMPTY_SPAN).appendTo @$prompt_current
    @$prompt_right = $(EMPTY_SPAN).appendTo @$prompt_current

    # Needed for the CSS z-index on the cursor to work.
    @$prompt_right.css position: 'relative'

    # The cursor. A span containing a space that shades its following character.
    # If the font of the prompt is not monospace, the content should be set to
    # the first character of @$prompt_right to get the appropriate width.
    @$prompt_cursor = $(spanHtml(CLASS_CURSOR, '&nbsp;'))
    @$prompt_cursor.insertBefore @$prompt_right
    @$prompt_cursor.css
      color: 'transparent'
      display: 'inline'
      zIndex: 0
    @$prompt_cursor.css('position', 'absolute') if not @isMobile

  # Binds all the required input and focus events.
  _SetupEvents: ->

    # Click to focus.
    if @isMobile
      @$console.click (e) =>
        e.preventDefault()
        @Focus()
    else
      @$console.mouseup (e) =>
        # Focus immediatly when it's the middle click to support
        # paste on linux desktop.
        if e.which == 2
          @Focus()
        else
          fn = =>
            if not window.getSelection().toString()
              e.preventDefault()
              @Focus()
          # Force selection update.
          setTimeout fn, 0

    # Mark the console with a style when it loses focus.
    @$input_source.focus =>
      @_ScrollToEnd()
      @$console_focused = true
      @$console.removeClass CLASS_BLURRED
      removeClass = =>
        if @$console_focused then @$console.removeClass CLASS_BLURRED
      setTimeout removeClass, 100
      hideTextInput = =>
        if @isIos and @$console_focused then @$input_source.hide()
      setTimeout hideTextInput, 500

    @$input_source.blur =>
      @$console_focused = false
      if @isIos then @$input_source.show()
      addClass = =>
        if not @$console_focused then @$console.addClass CLASS_BLURRED
      setTimeout addClass, 100

    # Intercept pasting.
    @$input_source.bind 'paste', =>
      handlePaste = =>
        # Opera fires input on composition end.
        return if @in_composition
        @_AppendPromptText @$input_source.val()
        @$input_source.val ''
        @Focus()
      # Wait until the browser has handled the paste event before scraping.
      setTimeout handlePaste, 0

    # Actual key-by-key handling.
    @$input_source.keypress @_HandleChar
    @$input_source.keydown @_HandleKey
    @$input_source.keydown @_CheckComposition

    # Firefox don't fire any key event for composition characters, so we listen
    # for the unstandard composition-events.
    @$input_source.bind 'compositionstart', @_StartComposition
    @$input_source.bind 'compositionend', (e) =>
      # Wait for the input element to update so we don't rely on buggy e.data
      setTimeout((=> @_EndComposition(e)), 0)

    # Android has an out of screen text input for autocorrect and autocomplete
    # and since it doesn't allow disabling we use composition events and more
    # hacks to get input to work.
    if @isAndroid
      # Text is handled via composition events but for things like spaces
      # we need to emulate a composition start.
      @$input_source.bind 'input', @_StartComposition
      @$input_source.bind 'input', @_UpdateComposition
    else
      @$input_source.bind 'text', @_UpdateComposition

  # Handles a character key press.
  #   @arg event: The jQuery keyboard Event object to handle.
  _HandleChar: (event) =>
    # We let the browser take over during output mode.
    # Skip everything when a modifier key other than shift is held.
    # Allow alt key to pass through for unicode & multibyte characters.
    if @state == STATE_OUTPUT or event.metaKey or event.ctrlKey
      return true

    # IE & Chrome capture non-control characters and Enter.
    # Mozilla and Opera capture everything.

    # This is the most reliable cross-browser; charCode/keyCode break on Opera.
    char_code = event.which

    # Skip Enter on IE and Chrome and Tab & backspace on Opera.
    # These are handled in _HandleKey().
    if char_code in [8, 9, 13] then return false

    @$prompt_left.text @$prompt_left.text() + String.fromCharCode char_code
    @_ScrollToEnd()
    return false

  # Handles a key up event and dispatches specific handlers.
  #   @arg event: The jQuery keyboard Event object to handle.
  _HandleKey: (event) =>
    # We let the browser take over during output mode.
    if @state == STATE_OUTPUT then return true

    key = event.keyCode or event.which

    # Check for char matching next time the callstack unwinds.
    setTimeout $.proxy(@_CheckMatchings, this), 0

    # Don't care about alt-modifier.
    if event.altKey
      return true
    # Handle shortcuts.
    else if event.ctrlKey or event.metaKey
      return @_HandleCtrlShortcut key
    else if event.shiftKey
      # Shift-modifier shortcut.
      switch key
        when KEY_ENTER then @_HandleEnter true
        when KEY_TAB then @_Unindent()
        when KEY_UP then  @_MoveUp()
        when KEY_DOWN then @_MoveDown()
        when KEY_PAGE_UP then @_ScrollPage 'up'
        when KEY_PAGE_DOWN then @_ScrollPage 'down'
        # Allow other Shift shortcuts to pass through to the browser.
        else return true
      return false
    else
      # Not a modifier shortcut.
      switch key
        when KEY_ENTER then @_HandleEnter false
        when KEY_TAB then @_Indent()
        when KEY_DELETE then @_Delete false
        when KEY_BACKSPACE then @_Backspace false
        when KEY_LEFT then @_MoveLeft false
        when KEY_RIGHT then @_MoveRight false
        when KEY_UP then @_HistoryPrevious()
        when KEY_DOWN then @_HistoryNext()
        when KEY_HOME then @MoveToStart false
        when KEY_END then @MoveToEnd false
        when KEY_PAGE_UP then @_ScrollPage 'up'
        when KEY_PAGE_DOWN then @_ScrollPage 'down'
        # Let any other key continue its way to keypress.
        else return true
      return false

  # Handles a Ctrl+Key shortcut.
  #   @arg key: The keyCode of the pressed key.
  _HandleCtrlShortcut: (key) ->
    switch key
      when KEY_DELETE then @_Delete true
      when KEY_BACKSPACE then @_Backspace true
      when KEY_LEFT then @_MoveLeft true
      when KEY_RIGHT then @_MoveRight true
      when KEY_UP then  @_MoveUp()
      when KEY_DOWN then @_MoveDown()
      when KEY_END then @MoveToEnd true
      when KEY_HOME then @MoveToStart true
      else
        if key of @shortcuts
          # Execute custom shortcuts.
          handler.call(this) for handler in @shortcuts[key]
          return false
        else
          # Allow unhandled Ctrl shortcuts.
          return true
    # Block handled shortcuts.
    return false

  # Handles the user pressing the Enter key.
  #   @arg shift: Whether the shift key is held.
  _HandleEnter: (shift) ->
    @_EndComposition()
    if shift
      @_InsertNewLine true
    else
      text = @GetPromptText()
      continuation = (indent) =>
        if indent isnt false
          @MoveToEnd true
          @_InsertNewLine true
          for _ in [0...Math.abs indent]
            if indent > 0 then @_Indent() else @_Unindent()
        else
          # Done with input.
          cls_suffix = if @state == STATE_INPUT then 'input' else 'prompt'
          @Write @GetPromptText(true) + NEWLINE, "#{CLASS_PREFIX}old-" + cls_suffix
          @ClearPromptText true
          if @history_active
            if not @history.length or @history[@history.length - 1] != text
              @history.push text
            @history_index = @history.length
          @state = STATE_OUTPUT
          callback = @input_callback
          @input_callback = null
          if callback then callback text
          @_CheckInputQueue()

      if @multiline_callback
        if @async_multiline
          @multiline_callback text, continuation
        else
          continuation @multiline_callback text
      else
        continuation false


  # Returns the appropriate variables for usage in methods that depends on the
  #   direction of the interaction with the console.
  _GetDirectionals: (back) ->
    $prompt_which = if back then @$prompt_left else @$prompt_right
    $prompt_opposite = if back then @$prompt_right else @$prompt_left
    $prompt_relative = if back then @$prompt_before else @$prompt_after
    $prompt_rel_opposite = if back then @$prompt_after else @$prompt_before
    MoveToLimit = if back
      $.proxy @MoveToStart, @
    else
      $.proxy @MoveToEnd, @
    MoveDirection = if back
      $.proxy @_MoveLeft, @
    else
      $.proxy @_MoveRight, @
    which_end = if back then 'last' else 'first'
    where_append = if back then 'prependTo' else 'appendTo'
    return {
      $prompt_which
      $prompt_opposite
      $prompt_relative
      $prompt_rel_opposite
      MoveToLimit
      MoveDirection
      which_end
      where_append
    }

  # Moves the cursor vertically in the current prompt,
  #   in the same column. (Used by _MoveUp, _MoveDown)
  _VerticalMove: (up) ->
    {
      $prompt_which
      $prompt_opposite
      $prompt_relative
      MoveToLimit
      MoveDirection
    } = @_GetDirectionals(up)

    if $prompt_relative.is EMPTY_SELECTOR then return
    pos = @$prompt_left.text().length
    MoveToLimit()
    MoveDirection()
    text = $prompt_which.text()
    $prompt_opposite.text if up then text[pos..] else text[...pos]
    $prompt_which.text if up then text[...pos] else text[pos..]


  # Moves the cursor to the line above the current one, in the same column.
  _MoveUp: ->
    @_VerticalMove true

  # Moves the cursor to the line below the current one, in the same column.
  _MoveDown: ->
    @_VerticalMove()

  # Moves the cursor horizontally in the current prompt.
  #   Used by _MoveLeft, _MoveRight
  _HorizontalMove: (whole_word, back) ->
    {
      $prompt_which
      $prompt_opposite
      $prompt_relative
      $prompt_rel_opposite
      which_end
      where_append
    } = @_GetDirectionals(back)
    regexp = if back then /\w*\W*$/ else /^\w*\W*/

    text = $prompt_which.text()
    if text
      if whole_word
        word = text.match regexp
        if not word then return
        word = word[0]
        tmp = $prompt_opposite.text()
        $prompt_opposite.text if back then word + tmp else tmp + word
        len = word.length
        $prompt_which.text if back then text[...-len] else text[len..]
      else
        tmp = $prompt_opposite.text()
        $prompt_opposite.text if back then text[-1...] + tmp else tmp + text[0]
        $prompt_which.text if back then text[...-1] else text[1...]
    else if not $prompt_relative.is EMPTY_SELECTOR
      $which_line = $(EMPTY_SPAN)[where_append] $prompt_rel_opposite
      $which_line.append $(EMPTY_SPAN).text @$prompt_label.text()
      $which_line.append $(EMPTY_SPAN).text $prompt_opposite.text()

      $opposite_line = $prompt_relative.children()[which_end]().detach()
      @$prompt_label.text $opposite_line.children().first().text()
      $prompt_which.text $opposite_line.children().last().text()
      $prompt_opposite.text ''

  # Moves the cursor to the left.
  #   @arg whole_word: Whether to move by a whole word rather than a character.
  _MoveLeft: (whole_word) ->
    @_HorizontalMove whole_word, true

  # Moves the cursor to the right.
  #   @arg whole_word: Whether to move by a whole word rather than a character.
  _MoveRight: (whole_word) ->
    @_HorizontalMove whole_word

  # Moves the cursor either to the start or end of the current prompt line(s).
  _MoveTo: (all_lines, back) ->
    {
      $prompt_which
      $prompt_opposite
      $prompt_relative
      MoveToLimit
      MoveDirection
    } = @_GetDirectionals(back)

    if all_lines
      # Warning! FF 3.6 hangs on is(EMPTY_SELECTOR)
      until $prompt_relative.is(EMPTY_SELECTOR) and $prompt_which.text() == ''
        MoveToLimit false
        MoveDirection false
    else
      $prompt_opposite.text @$prompt_left.text() + @$prompt_right.text()
      $prompt_which.text ''

  # Deletes the character or word following the cursor.
  #   @arg whole_word: Whether to delete a whole word rather than a character.
  _Delete: (whole_word) ->
    text = @$prompt_right.text()
    if text
      if whole_word
        word = text.match /^\w*\W*/
        if not word then return
        word = word[0]
        @$prompt_right.text text[word.length...]
      else
        @$prompt_right.text text[1...]
    else if not @$prompt_after.is EMPTY_SELECTOR
      $lower_line = @$prompt_after.children().first().detach()
      @$prompt_right.text $lower_line.children().last().text()

  # Deletes the character or word preceding the cursor.
  #   @arg whole_word: Whether to delete a whole word rather than a character.
  _Backspace: (whole_word) ->
    setTimeout $.proxy(@_ScrollToEnd, @), 0
    text = @$prompt_left.text()
    if text
      if whole_word
        word = text.match /\w*\W*$/
        if not word then return
        word = word[0]
        @$prompt_left.text text[...-word.length]
      else
        @$prompt_left.text text[...-1]
    else if not @$prompt_before.is EMPTY_SELECTOR
      $upper_line = @$prompt_before.children().last().detach()
      @$prompt_label.text $upper_line.children().first().text()
      @$prompt_left.text $upper_line.children().last().text()

  # Indents the current line.
  _Indent: ->
    @$prompt_left.prepend (' ' for _ in [1..@indent_width]).join ''

  # Unindents the current line.
  _Unindent: ->
    line_text = @$prompt_left.text() + @$prompt_right.text()
    for _ in [1..@indent_width]
      if not /^ /.test(line_text) then break
      if @$prompt_left.text()
        @$prompt_left.text @$prompt_left.text()[1..]
      else
        @$prompt_right.text @$prompt_right.text()[1..]
      line_text = line_text[1..]

  # Inserts a new line at the cursor position.
  #   @arg indent: If specified and true, the inserted line is indented to the
  #     same column as the last line.
  _InsertNewLine: (indent = false) ->
    old_prompt = @_SelectPromptLabel not @$prompt_before.is EMPTY_SELECTOR
    $old_line = $(EMPTY_SPAN).appendTo @$prompt_before
    $old_line.append $(EMPTY_SPAN).text old_prompt
    $old_line.append $(EMPTY_SPAN).text @$prompt_left.text()

    @$prompt_label.text @_SelectPromptLabel true
    if indent and match = @$prompt_left.text().match /^\s+/
      @$prompt_left.text match[0]
    else
      @$prompt_left.text ''
    @_ScrollToEnd()

  # Appends the given text to the prompt.
  #   @arg text: The text to append. Can contain multiple lines.
  _AppendPromptText: (text) ->
    lines = text.split NEWLINE
    @$prompt_left.text @$prompt_left.text() + lines[0]
    for line in lines[1..]
      @_InsertNewLine()
      @$prompt_left.text line

  # Scrolls the console area down/up one page (with animation).
  _ScrollPage: (dir) ->
    target = @$container[0].scrollTop
    if dir == 'up'
      target -= @$container.height()
    else
      target += @$container.height()
    @$container.stop().animate {scrollTop: target}, 'fast'

  # Scrolls the console area to its bottom;
  # Scrolls the window to the cursor vertical position.
  # Called with every input/output to the console.
  _ScrollToEnd: ->
    # Scroll console to the bottom.
    @$container.scrollTop @$container[0].scrollHeight

    # Move the input element to the cursor position.
    pos = @$prompt_cursor.position()
    @$input_container.css
      left: pos.left
      top: pos.top

    # Give time for mobile browsers to zoom in on textarea
    setTimeout @ScrollWindowToPrompt.bind(@), 50

  ScrollWindowToPrompt: ->
    # The cursor's top position is effected by the scroll-top of the console
    # so we need to this asynchronously to give the browser a chance to
    # reflow and recaluclate the cursor's possition.
    line_height = @$prompt_cursor.height()
    screen_top = @$window.scrollTop()
    screen_left = @$window.scrollLeft()
    doc_height = document.documentElement.clientHeight
    pos = @$prompt_cursor.offset()

    optimal_pos = pos.top - (2 * line_height)

    # Follow the cursor vertically on mobile and desktop.
    if @isMobile and orientation?
      # Since the keyboard takes up most of the screen, we don't care about how
      # far the the cursor position from the screen top is. We just follow it.
      if screen_top < pos.top or screen_top > pos.top
        @$window.scrollTop optimal_pos
    else
      if screen_top + doc_height < pos.top
        # Scroll just to a place where the cursor is in the view port.
        @$window.scrollTop pos.top - doc_height + line_height
      else if screen_top > optimal_pos
        # If the window is scrolled beyond the cursor, scroll to the cursor's
        # position and give two line to the top.
        @$window.scrollTop pos.top

  # Selects the prompt label appropriate to the current mode.
  #   @arg continuation: If true, returns the continuation prompt rather than
  #     the main one.
  _SelectPromptLabel: (continuation) ->
    if @state == STATE_PROMPT
      return if continuation then (' \n' + @prompt_label_continue) else @prompt_label_main
    else
      return if continuation then '\n ' else ' '

  # Wraps a single character in an element with a <span> having a class
  #   @arg $elem: The JqDom element in question
  #   @arg index: the index of the character to be wrapped
  #   @arg cls: the html class to be given to the wrapping <span>
  _Wrap: ($elem, index, cls) ->
    text = $elem.html()
    html = text[0...index]+
           spanHtml(cls, text[index])+
           text[index + 1...]
    $elem.html html

  # Walks a string of characters incrementing current_count each time a char is found
  # and decrementing each time an opposing char is found.
  #   @arg text: the text in question
  #   @arg char: the char that would increment the counter
  #   @arg opposing_char: the char that would decrement the counter
  #   @arg back: specifies whether the walking should be done backwards.
  _WalkCharacters: (text, char, opposing_char, current_count, back) ->
    index = if back then text.length else 0
    text = text.split ''
    read_char = () ->
      if back
        [text..., ret] = text
      else
        [ret, text...] = text
      if ret
        index = index + if back then -1 else +1
      ret

    while ch = read_char()
      if ch is char
        current_count++
      else if ch is opposing_char
        current_count--
      if current_count is 0
        return {index: index, current_count: current_count}

    return {index: -1, current_count: current_count}

  _ProcessMatch: (config, back, before_char) =>
      [char, opposing_char] = if back
        [
          config['closing_char']
          config['opening_char']
        ]
      else
        [
          config['opening_char']
          config['closing_char']
        ]
      {$prompt_which, $prompt_relative} = @_GetDirectionals(back)

      current_count = 1
      found = false
      # check current line first
      text = $prompt_which.html()
      # When on the same line discard checking the first character, going backwards
      # is not an issue since the cursor's current character is found in $prompt_right.
      if !back then text = text[1...]
      if before_char and back then text = text[...-1]
      {index, current_count} = @_WalkCharacters text, char, opposing_char, current_count, back
      if index > -1
        @_Wrap $prompt_which, index, config.cls
        found = true
      else
        $collection = $prompt_relative.children()
        # When going backwards we have to the reverse our jQuery collection
        # for fair matchings
        $collection = if back then Array.prototype.reverse.call($collection) else $collection
        $collection.each (i, elem) =>
          $elem = $(elem).children().last()
          text = $elem.html()
          {index, current_count} = @_WalkCharacters text, char, opposing_char, current_count, back
          if index > -1
            # When checking for matchings ona different line going forward we must decrement
            # the index since the current char is not included
            if !back then index--
            @_Wrap $elem, index, config.cls
            found = true
            return false

      return found

  # Unrwaps all prevoisly matched characters.
  # Checks if the cursor's current character is one to be matched, then walks
  # the following/preceeding characters to look for the opposing character that
  # would satisfy the match. If found both characters would be wrapped with a
  # span and applied the html class that was found in the match_config.
  _CheckMatchings: (before_char) ->
    current_char = if before_char then @$prompt_left.text()[@$prompt_left.text().length - 1...] else @$prompt_right.text()[0]
    # on every move unwrap all matched elements
    # TODO(amasad): cache previous matched elements since this must be costly
    $('.' + cls, @$console).contents().unwrap() for cls in @matchings.clss

    if config = @matchings.closings[current_char]
      found = @_ProcessMatch config, true, before_char
    else if config = @matchings.openings[current_char]
      found = @_ProcessMatch config, false, before_char
    else if not before_char
      @_CheckMatchings true

    if before_char
      @_Wrap @$prompt_left, @$prompt_left.html().length - 1, config.cls if found
    else
    # Wrap current element when a matching was found
      @_Wrap @$prompt_right, 0, config.cls if found


  # Sets the prompt to the previous history item.
  _HistoryPrevious: ->
    if not @history_active then return
    if @history_index <= 0 then return
    if @history_index == @history.length
      @history_new = @GetPromptText()
    @SetPromptText @history[--@history_index]

  # Sets the prompt to the next history item.
  _HistoryNext: ->
    if not @history_active then return
    if @history_index >= @history.length then return
    if @history_index == @history.length - 1
      @history_index++
      @SetPromptText @history_new
    else
      @SetPromptText @history[++@history_index]

  # Check if this could be the start of a composition or an update to it.
  _CheckComposition: (e) =>
    key = e.keyCode or e.which
    if key == 229
      if @in_composition then @_UpdateComposition() else @_StartComposition()

  # Starts a multibyte character composition.
  _StartComposition: =>
    return if @in_composition
    @in_composition = true
    @_ShowComposition()
    setTimeout @_UpdateComposition, 0

  # Ends a multibyte character composition.
  _EndComposition: =>
    return if not @in_composition
    @_HideComposition()
    @$prompt_left.text @$prompt_left.text() + @$composition.text()
    @$composition.text ''
    @$input_source.val ''
    @in_composition = false

  # Updates a multibyte character composition.
  _UpdateComposition: (e) =>
    cb =  =>
      return if not @in_composition
      @$composition.text @$input_source.val()
    setTimeout cb, 0

  # Shows a multibyte character composition.
  _ShowComposition: =>
    @$composition.css 'height', @$prompt_cursor.height()
    @$composition.empty()
    @$composition.appendTo @$prompt_left

  # Hides a multibyte character composition.
  _HideComposition: =>
    # We just detach the element because by now the text value of this element
    # is already extracted and has been put on the left of the prompt.
    @$composition.detach()

$.fn.jqconsole = (header, prompt_main, prompt_continue) ->
  new JQConsole this, header, prompt_main, prompt_continue

$.fn.jqconsole.JQConsole = JQConsole
$.fn.jqconsole.Ansi = Ansi
