{Point, Range} = require 'text-buffer'
{Emitter} = require 'emissary'
_ = require 'underscore-plus'

# Public: The `Cursor` class represents the little blinking line identifying
# where text can be inserted.
#
# Cursors belong to {Editor}s and have some metadata attached in the form
# of a {Marker}.
module.exports =
class Cursor
  Emitter.includeInto(this)

  screenPosition: null
  bufferPosition: null
  goalColumn: null
  visible: true
  needsAutoscroll: null

  # Instantiated by an {Editor}
  constructor: ({@editor, @marker}) ->
    @updateVisibility()
    @marker.on 'changed', (e) =>
      @updateVisibility()
      {oldHeadScreenPosition, newHeadScreenPosition} = e
      {oldHeadBufferPosition, newHeadBufferPosition} = e
      {textChanged} = e
      return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)

      @needsAutoscroll ?= @isLastCursor() and !textChanged
      @goalColumn = null

      movedEvent =
        oldBufferPosition: oldHeadBufferPosition
        oldScreenPosition: oldHeadScreenPosition
        newBufferPosition: newHeadBufferPosition
        newScreenPosition: newHeadScreenPosition
        textChanged: textChanged

      @emit 'moved', movedEvent
      @editor.emit 'cursor-moved', movedEvent
    @marker.on 'destroyed', =>
      @destroyed = true
      @editor.removeCursor(this)
      @emit 'destroyed'
    @needsAutoscroll = true

  destroy: ->
    @marker.destroy()

  changePosition: (options, fn) ->
    @clearSelection()
    @needsAutoscroll = options.autoscroll ? @isLastCursor()
    unless fn()
      @emit 'autoscrolled' if @needsAutoscroll

  # Public: Moves a cursor to a given screen position.
  #
  # screenPosition - An {Array} of two numbers: the screen row, and the screen
  #                  column.
  # options - An {Object} with the following keys:
  #   :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever
  #                 the cursor moves to.
  setScreenPosition: (screenPosition, options={}) ->
    @changePosition options, =>
      @marker.setHeadScreenPosition(screenPosition, options)

  # Public: Returns the screen position of the cursor as an Array.
  getScreenPosition: ->
    @marker.getHeadScreenPosition()

  # Public: Moves a cursor to a given buffer position.
  #
  # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer
  #                  column.
  # options - An {Object} with the following keys:
  #   :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever
  #                 the cursor moves to.
  setBufferPosition: (bufferPosition, options={}) ->
    @changePosition options, =>
      @marker.setHeadBufferPosition(bufferPosition, options)

  # Public: Returns the current buffer position as an Array.
  getBufferPosition: ->
    @marker.getHeadBufferPosition()

  # Public: If the marker range is empty, the cursor is marked as being visible.
  updateVisibility: ->
    @setVisible(@marker.getBufferRange().isEmpty())

  # Public: Sets whether the cursor is visible.
  setVisible: (visible) ->
    if @visible != visible
      @visible = visible
      @needsAutoscroll ?= true if @visible and @isLastCursor()
      @emit 'visibility-changed', @visible

  # Public: Returns the visibility of the cursor.
  isVisible: -> @visible

  # Public: Get the RegExp used by the cursor to determine what a "word" is.
  #
  # options: An {Object} with the following keys:
  #   :includeNonWordCharacters - A {Boolean} indicating whether to include
  #                               non-word characters in the regex.
  #
  # Returns a {RegExp}.
  wordRegExp: ({includeNonWordCharacters}={})->
    includeNonWordCharacters ?= true
    nonWordCharacters = atom.config.get('editor.nonWordCharacters')
    segments = ["^[\t ]*$"]
    segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+")
    if includeNonWordCharacters
      segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+")
    new RegExp(segments.join("|"), "g")

  # Public: Identifies if this cursor is the last in the {Editor}.
  #
  # "Last" is defined as the most recently added cursor.
  #
  # Returns a {Boolean}.
  isLastCursor: ->
    this == @editor.getCursor()

  # Public: Identifies if the cursor is surrounded by whitespace.
  #
  # "Surrounded" here means that all characters before and after the cursor is
  # whitespace.
  #
  # Returns a {Boolean}.
  isSurroundedByWhitespace: ->
    {row, column} = @getBufferPosition()
    range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]]
    /^\s+$/.test @editor.getTextInBufferRange(range)

  # Public: Returns whether the cursor is currently between a word and non-word
  # character. The non-word characters are defined by the
  # `editor.nonWordCharacters` config value.
  #
  # This method returns false if the character before or after the cursor is
  # whitespace.
  #
  # Returns a Boolean.
  isBetweenWordAndNonWord: ->
    return false if @isAtBeginningOfLine() or @isAtEndOfLine()

    {row, column} = @getBufferPosition()
    range = [[row, column - 1], [row, column + 1]]
    [before, after] = @editor.getTextInBufferRange(range)
    return false if /\s/.test(before) or /\s/.test(after)

    nonWordCharacters = atom.config.get('editor.nonWordCharacters').split('')
    _.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after)

  # Public: Returns whether this cursor is between a word's start and end.
  isInsideWord: ->
    {row, column} = @getBufferPosition()
    range = [[row, column], [row, Infinity]]
    @editor.getTextInBufferRange(range).search(@wordRegExp()) == 0

  # Public: Prevents this cursor from causing scrolling.
  clearAutoscroll: ->
    @needsAutoscroll = null

  # Public: Deselects the current selection.
  clearSelection: ->
    @selection?.clear()

  # Public: Returns the cursor's current screen row.
  getScreenRow: ->
    @getScreenPosition().row

  # Public: Returns the cursor's current screen column.
  getScreenColumn: ->
    @getScreenPosition().column

  # Public: Retrieves the cursor's current buffer row.
  getBufferRow: ->
    @getBufferPosition().row

  # Public: Returns the cursor's current buffer column.
  getBufferColumn: ->
    @getBufferPosition().column

  # Public: Returns the cursor's current buffer row of text excluding its line
  # ending.
  getCurrentBufferLine: ->
    @editor.lineForBufferRow(@getBufferRow())

  # Public: Moves the cursor up one screen row.
  moveUp: (rowCount = 1, {moveToEndOfSelection}={}) ->
    range = @marker.getScreenRange()
    if moveToEndOfSelection and not range.isEmpty()
      { row, column } = range.start
    else
      { row, column } = @getScreenPosition()

    column = @goalColumn if @goalColumn?
    @setScreenPosition({row: row - rowCount, column: column})
    @goalColumn = column

  # Public: Moves the cursor down one screen row.
  moveDown: (rowCount = 1, {moveToEndOfSelection}={}) ->
    range = @marker.getScreenRange()
    if moveToEndOfSelection and not range.isEmpty()
      { row, column } = range.end
    else
      { row, column } = @getScreenPosition()

    column = @goalColumn if @goalColumn?
    @setScreenPosition({row: row + rowCount, column: column})
    @goalColumn = column

  # Public: Moves the cursor left one screen column.
  #
  # options - An {Object} with the following keys:
  #   :moveToEndOfSelection - if true, move to the left of the selection if a
  #                           selection exists.
  moveLeft: ({moveToEndOfSelection}={}) ->
    range = @marker.getScreenRange()
    if moveToEndOfSelection and not range.isEmpty()
      @setScreenPosition(range.start)
    else
      {row, column} = @getScreenPosition()
      [row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
      @setScreenPosition({row, column})

  # Public: Moves the cursor right one screen column.
  #
  # options - An {Object} with the following keys:
  #   :moveToEndOfSelection - if true, move to the right of the selection if a
  #                           selection exists.
  moveRight: ({moveToEndOfSelection}={}) ->
    range = @marker.getScreenRange()
    if moveToEndOfSelection and not range.isEmpty()
      @setScreenPosition(range.end)
    else
      { row, column } = @getScreenPosition()
      @setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true)

  # Public: Moves the cursor to the top of the buffer.
  moveToTop: ->
    @setBufferPosition([0,0])

  # Public: Moves the cursor to the bottom of the buffer.
  moveToBottom: ->
    @setBufferPosition(@editor.getEofBufferPosition())

  # Public: Moves the cursor to the beginning of the line.
  moveToBeginningOfScreenLine: ->
    @setScreenPosition([@getScreenRow(), 0])

  # Public: Moves the cursor to the beginning of the buffer line.
  moveToBeginningOfLine: ->
    @setBufferPosition([@getBufferRow(), 0])

  # Public: Moves the cursor to the beginning of the first character in the
  # line.
  moveToFirstCharacterOfLine: ->
    {row, column} = @getScreenPosition()
    screenline = @editor.lineForScreenRow(row)

    goalColumn = screenline.text.search(/\S/)
    goalColumn = 0 if goalColumn == column or goalColumn == -1
    @setScreenPosition([row, goalColumn])

  # Public: Moves the cursor to the beginning of the buffer line, skipping all
  # whitespace.
  skipLeadingWhitespace: ->
    position = @getBufferPosition()
    scanRange = @getCurrentLineBufferRange()
    endOfLeadingWhitespace = null
    @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) =>
      endOfLeadingWhitespace = range.end

    @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position)

  # Public: Moves the cursor to the end of the line.
  moveToEndOfScreenLine: ->
    @setScreenPosition([@getScreenRow(), Infinity])

  # Public: Moves the cursor to the end of the buffer line.
  moveToEndOfLine: ->
    @setBufferPosition([@getBufferRow(), Infinity])

  # Public: Moves the cursor to the beginning of the word.
  moveToBeginningOfWord: ->
    @setBufferPosition(@getBeginningOfCurrentWordBufferPosition())

  # Public: Moves the cursor to the end of the word.
  moveToEndOfWord: ->
    if position = @getEndOfCurrentWordBufferPosition()
      @setBufferPosition(position)

  # Public: Moves the cursor to the beginning of the next word.
  moveToBeginningOfNextWord: ->
    if position = @getBeginningOfNextWordBufferPosition()
      @setBufferPosition(position)

  # Public: Moves the cursor to the previous word boundary.
  moveToPreviousWordBoundary: ->
    if position = @getPreviousWordBoundaryBufferPosition()
      @setBufferPosition(position)

  # Public: Moves the cursor to the next word boundary.
  moveToNextWordBoundary: ->
    if position = @getMoveNextWordBoundaryBufferPosition()
      @setBufferPosition(position)

  # Public: Retrieves the buffer position of where the current word starts.
  #
  # options - An {Object} with the following keys:
  #   :wordRegex - A {RegExp} indicating what constitutes a "word"
  #                (default: {::wordRegExp}).
  #   :includeNonWordCharacters - A {Boolean} indicating whether to include
  #                               non-word characters in the default word regex.
  #                               Has no effect if wordRegex is set.
  #
  # Returns a {Range}.
  getBeginningOfCurrentWordBufferPosition: (options = {}) ->
    allowPrevious = options.allowPrevious ? true
    currentBufferPosition = @getBufferPosition()
    previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row)
    scanRange = [[previousNonBlankRow, 0], currentBufferPosition]

    beginningOfWordPosition = null
    @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) =>
      if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
        beginningOfWordPosition = range.start
      if not beginningOfWordPosition?.isEqual(currentBufferPosition)
        stop()

    beginningOfWordPosition or currentBufferPosition

  # Public: Retrieves buffer position of previous word boundary. It might be on
  # the current word, or the previous word.
  getPreviousWordBoundaryBufferPosition: (options = {}) ->
    currentBufferPosition = @getBufferPosition()
    previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row)
    scanRange = [[previousNonBlankRow, 0], currentBufferPosition]

    beginningOfWordPosition = null
    @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) =>
      if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0
        # force it to stop at the beginning of each line
        beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
      else if range.end.isLessThan(currentBufferPosition)
        beginningOfWordPosition = range.end
      else
        beginningOfWordPosition = range.start

      if not beginningOfWordPosition?.isEqual(currentBufferPosition)
        stop()

    beginningOfWordPosition or currentBufferPosition

  # Public: Retrieves buffer position of the next word boundary. It might be on
  # the current word, or the previous word.
  getMoveNextWordBoundaryBufferPosition: (options = {}) ->
    currentBufferPosition = @getBufferPosition()
    scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]

    endOfWordPosition = null
    @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) =>
      if range.start.row > currentBufferPosition.row
        # force it to stop at the beginning of each line
        endOfWordPosition = new Point(range.start.row, 0)
      else if range.start.isGreaterThan(currentBufferPosition)
        endOfWordPosition = range.start
      else
        endOfWordPosition = range.end

      if not endOfWordPosition?.isEqual(currentBufferPosition)
        stop()

    endOfWordPosition or currentBufferPosition

  # Public: Retrieves the buffer position of where the current word ends.
  #
  # options - An {Object} with the following keys:
  #   :wordRegex - A {RegExp} indicating what constitutes a "word"
  #                (default: {::wordRegExp})
  #   :includeNonWordCharacters - A Boolean indicating whether to include
  #                               non-word characters in the default word regex.
  #                               Has no effect if wordRegex is set.
  #
  # Returns a {Range}.
  getEndOfCurrentWordBufferPosition: (options = {}) ->
    allowNext = options.allowNext ? true
    currentBufferPosition = @getBufferPosition()
    scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]

    endOfWordPosition = null
    @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) =>
      if range.start.isLessThanOrEqual(currentBufferPosition) or allowNext
        endOfWordPosition = range.end
      if not endOfWordPosition?.isEqual(currentBufferPosition)
        stop()

    endOfWordPosition ? currentBufferPosition

  # Public: Retrieves the buffer position of where the next word starts.
  #
  # options -
  #   :wordRegex - A {RegExp} indicating what constitutes a "word"
  #                (default: {::wordRegExp}).
  #
  # Returns a {Range}.
  getBeginningOfNextWordBufferPosition: (options = {}) ->
    currentBufferPosition = @getBufferPosition()
    start = if @isInsideWord() then @getEndOfCurrentWordBufferPosition() else currentBufferPosition
    scanRange = [start, @editor.getEofBufferPosition()]

    beginningOfNextWordPosition = null
    @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) =>
      beginningOfNextWordPosition = range.start
      stop()

    beginningOfNextWordPosition or currentBufferPosition

  # Public: Returns the buffer Range occupied by the word located under the cursor.
  #
  # options -
  #   :wordRegex - A {RegExp} indicating what constitutes a "word"
  #                (default: {::wordRegExp}).
  getCurrentWordBufferRange: (options={}) ->
    startOptions = _.extend(_.clone(options), allowPrevious: false)
    endOptions = _.extend(_.clone(options), allowNext: false)
    new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions))

  # Public: Returns the buffer Range for the current line.
  #
  # options -
  #   :includeNewline: - A {Boolean} which controls whether the Range should
  #                      include the newline.
  getCurrentLineBufferRange: (options) ->
    @editor.bufferRangeForBufferRow(@getBufferRow(), options)

  # Public: Retrieves the range for the current paragraph.
  #
  # A paragraph is defined as a block of text surrounded by empty lines.
  #
  # Returns a {Range}.
  getCurrentParagraphBufferRange: ->
    @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow())

  # Public: Returns the characters preceeding the cursor in the current word.
  getCurrentWordPrefix: ->
    @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])

  # Public: Returns whether the cursor is at the start of a line.
  isAtBeginningOfLine: ->
    @getBufferPosition().column == 0

  # Public: Returns the indentation level of the current line.
  getIndentLevel: ->
    if @editor.getSoftTabs()
      @getBufferColumn() / @editor.getTabLength()
    else
      @getBufferColumn()

  # Public: Returns whether the cursor is on the line return character.
  isAtEndOfLine: ->
    @getBufferPosition().isEqual(@getCurrentLineBufferRange().end)

  # Public: Retrieves the grammar's token scopes for the line.
  #
  # Returns an {Array} of {String}s.
  getScopes: ->
    @editor.scopesForBufferPosition(@getBufferPosition())

  # Public: Returns true if this cursor has no non-whitespace characters before
  # its current position.
  hasPrecedingCharactersOnLine: ->
    bufferPosition = @getBufferPosition()
    line = @editor.lineForBufferRow(bufferPosition.row)
    firstCharacterColumn = line.search(/\S/)

    if firstCharacterColumn is -1
      false
    else
      bufferPosition.column > firstCharacterColumn
