_ = require 'underscore-plus'
{$$, Range} = require 'atom'
{ViewModel} = require '../view-models/view-model'

class OperatorError
  constructor: (@message) ->
    @name = 'Operator Error'

class Operator
  vimState: null
  motion: null
  complete: null
  selectOptions: null

  # selectOptions - The options object to pass through to the motion when
  #                 selecting.
  constructor: (@editor, @vimState, {@selectOptions}={}) ->
    @complete = false

  # Public: Determines when the command can be executed.
  #
  # Returns true if ready to execute and false otherwise.
  isComplete: -> @complete

  # Public: Determines if this command should be recorded in the command
  # history for repeats.
  #
  # Returns true if this command should be recorded.
  isRecordable: -> true

  # Public: Marks this as ready to execute and saves the motion.
  #
  # motion - The motion used to select what to operate on.
  #
  # Returns nothing.
  compose: (motion) ->
    if not motion.select
      throw new OperatorError('Must compose with a motion')

    @motion = motion
    @complete = true

  canComposeWith: (operation) -> operation.select?

  # Protected: Wraps the function within an single undo step.
  #
  # fn - The function to wrap.
  #
  # Returns nothing.
  undoTransaction: (fn) ->
    @editor.getBuffer().transact(fn)

# Public: Generic class for an operator that requires extra input
class OperatorWithInput extends Operator
  constructor: (@editorView, @vimState) ->
    @editor = @editorView.editor
    @complete = false

  canComposeWith: (operation) -> operation.characters?

  compose: (input) ->
    if not input.characters
      throw new OperatorError('Must compose with an Input')

    @input = input
    @complete = true

#
# It deletes everything selected by the following motion.
#
class Delete extends Operator
  allowEOL: null

  # allowEOL - Determines whether the cursor should be allowed to rest on the
  #            end of line character or not.
  constructor: (@editor, @vimState, {@allowEOL, @selectOptions}={}) ->
    @complete = false
    @selectOptions ?= {}
    @selectOptions.requireEOL ?= true

  # Public: Deletes the text selected by the given motion.
  #
  # count - The number of times to execute.
  #
  # Returns nothing.
  execute: (count=1) ->
    cursor = @editor.getCursor()

    if _.contains(@motion.select(count, @selectOptions), true)
      validSelection = true

    if validSelection?
      @editor.delete()
      if !@allowEOL and cursor.isAtEndOfLine() and !@motion.isLinewise?()
        @editor.moveCursorLeft()

    if @motion.isLinewise?()
      @editor.setCursorScreenPosition([cursor.getScreenRow(), 0])

    @vimState.activateCommandMode()
#
# It toggles the case of everything selected by the following motion
#
class ToggleCase extends Operator

  constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true

  execute: (count=1) ->
    pos = @editor.getCursorBufferPosition()
    lastCharIndex = @editor.lineLengthForBufferRow(pos.row) - 1
    count = Math.min count, @editor.lineLengthForBufferRow(pos.row) - pos.column

    # Do nothing on an empty line
    return if @editor.getBuffer().isRowBlank(pos.row)

    @undoTransaction =>
      _.times count, =>
        point = @editor.getCursorBufferPosition()
        range = Range.fromPointWithDelta(point, 0, 1)
        char = @editor.getTextInBufferRange(range)

        if char is char.toLowerCase()
          @editor.setTextInBufferRange(range, char.toUpperCase())
        else
          @editor.setTextInBufferRange(range, char.toLowerCase())

        unless point.column >= lastCharIndex
          @editor.moveCursorRight()

    @vimState.activateCommandMode()

#
# It copies everything selected by the following motion.
#
class Yank extends Operator
  register: '"'

  # Public: Copies the text selected by the given motion.
  #
  # count - The number of times to execute.
  #
  # Returns nothing.
  execute: (count=1) ->
    originalPosition = @editor.getCursorScreenPosition()

    if _.contains(@motion.select(count), true)
      text = @editor.getSelection().getText()
    else
      text = ''
    type = if @motion.isLinewise?() then 'linewise' else 'character'

    @vimState.setRegister(@register, {text, type})

    if @motion.isLinewise?()
      @editor.setCursorScreenPosition(originalPosition)
    else
      @editor.clearSelections()

    @vimState.activateCommandMode()

#
# It combines the current line with the following line.
#
class Join extends Operator
  constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true

  # Public: Combines the current with the following lines
  #
  # count - The number of times to execute.
  #
  # Returns nothing.
  execute: (count=1) ->
    @undoTransaction =>
      _.times count, =>
        @editor.joinLines()
    @vimState.activateCommandMode()

#
# Repeat the last operation
#
class Repeat extends Operator
  constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true

  isRecordable: -> false

  execute: (count=1) ->
    @undoTransaction =>
      _.times count, =>
        cmd = @vimState.history[0]
        cmd?.execute()
#
# It creates a mark at the current cursor position
#
class Mark extends OperatorWithInput
  constructor: (@editorView, @vimState, {@selectOptions}={}) ->
    super(@editorView, @vimState)
    @viewModel = new ViewModel(@, class: 'mark', singleChar: true, hidden: true)

  # Public: Creates the mark in the specified mark register (from user input)
  # at the current position
  #
  # Returns nothing.
  execute: () ->
    @vimState.setMark(@input.characters, @editorView.editor.getCursorBufferPosition())
    @vimState.activateCommandMode()

module.exports = {
  Operator, OperatorWithInput, OperatorError, Delete, ToggleCase,
  Yank, Join, Repeat, Mark
}
