_ = require 'underscore'
Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'

module.exports =
class BufferMarker
  headPosition: null
  tailPosition: null
  suppressObserverNotification: false
  invalidationStrategy: null

  constructor: ({@id, @buffer, range, @invalidationStrategy, noTail, reverse}) ->
    @invalidationStrategy ?= 'contains'
    @setRange(range, {noTail, reverse})

  setRange: (range, options={}) ->
    @consolidateObserverNotifications false, =>
      range = Range.fromObject(range)
      if options.reverse
        @setTailPosition(range.end) unless options.noTail
        @setHeadPosition(range.start)
      else
        @setTailPosition(range.start) unless options.noTail
        @setHeadPosition(range.end)

  isReversed: ->
    @tailPosition? and @headPosition.isLessThan(@tailPosition)

  getRange: ->
    if @tailPosition
      new Range(@tailPosition, @headPosition)
    else
      new Range(@headPosition, @headPosition)

  getHeadPosition: -> @headPosition

  getTailPosition: -> @tailPosition ? @getHeadPosition()

  setHeadPosition: (newHeadPosition, options={}) ->
    oldHeadPosition = @getHeadPosition()
    newHeadPosition = Point.fromObject(newHeadPosition)
    newHeadPosition = @buffer.clipPosition(newHeadPosition) if options.clip ? true
    return if newHeadPosition.isEqual(@headPosition)
    @headPosition = newHeadPosition
    bufferChanged = !!options.bufferChanged
    @notifyObservers({oldHeadPosition, newHeadPosition, bufferChanged})
    @headPosition

  setTailPosition: (newTailPosition, options={}) ->
    oldTailPosition = @getTailPosition()
    newTailPosition = Point.fromObject(newTailPosition)
    newTailPosition = @buffer.clipPosition(newTailPosition) if options.clip ? true
    return if newTailPosition.isEqual(@tailPosition)
    @tailPosition = newTailPosition
    bufferChanged = !!options.bufferChanged
    @notifyObservers({oldTailPosition, newTailPosition, bufferChanged})
    @tailPosition

  getStartPosition: ->
    @getRange().start

  getEndPosition: ->
    @getRange().end

  placeTail: ->
    @setTailPosition(@getHeadPosition()) unless @tailPosition

  clearTail: ->
    oldTailPosition = @getTailPosition()
    @tailPosition = null
    newTailPosition = @getTailPosition()
    @notifyObservers({oldTailPosition, newTailPosition, bufferChanged: false})

  tryToInvalidate: (changedRange) ->
    betweenStartAndEnd = @getRange().containsRange(changedRange, exclusive: false)
    containsStart = changedRange.containsPoint(@getStartPosition(), exclusive: true)
    containsEnd = changedRange.containsPoint(@getEndPosition(), exclusive: true)

    switch @invalidationStrategy
      when 'between'
        if betweenStartAndEnd or containsStart or containsEnd
          @invalidate()
          [@id]
      when 'contains'
        if containsStart or containsEnd
          @invalidate()
          [@id]
      when 'never'
        if containsStart or containsEnd
          previousRange = @getRange()
          if containsStart and containsEnd
            @setRange([changedRange.end, changedRange.end])
          else if containsStart
            @setRange([changedRange.end, @getEndPosition()])
          else
            @setRange([@getStartPosition(), changedRange.start])
          [@id, previousRange]

  handleBufferChange: (bufferChange) ->
    @consolidateObserverNotifications true, =>
      @setHeadPosition(@updatePosition(@headPosition, bufferChange, false), clip: false, bufferChanged: true)
      @setTailPosition(@updatePosition(@tailPosition, bufferChange, true), clip: false, bufferChanged: true) if @tailPosition

  updatePosition: (position, bufferChange, isFirstPoint) ->
    { oldRange, newRange } = bufferChange

    return position if oldRange.containsPoint(position, exclusive: true)
    return position if isFirstPoint and oldRange.start.isEqual(position)
    return position if position.isLessThan(oldRange.end)

    newRow = newRange.end.row
    newColumn = newRange.end.column

    if position.row == oldRange.end.row
      newColumn += position.column - oldRange.end.column
    else
      newColumn = position.column
      newRow += position.row - oldRange.end.row

    [newRow, newColumn]

  observe: (callback) ->
    @on 'changed', callback
    cancel: => @unobserve(callback)

  unobserve: (callback) ->
    @off 'changed', callback

  containsPoint: (point) ->
    @getRange().containsPoint(point)

  notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged} = {}) ->
    return if @suppressObserverNotification

    if newHeadPosition? and newTailPosition?
      return if _.isEqual(newHeadPosition, oldHeadPosition) and _.isEqual(newTailPosition, oldTailPosition)
    else if newHeadPosition?
      return if _.isEqual(newHeadPosition, oldHeadPosition)
    else if newTailPosition?
      return if _.isEqual(newTailPosition, oldTailPosition)

    oldHeadPosition ?= @getHeadPosition()
    newHeadPosition ?= @getHeadPosition()
    oldTailPosition ?= @getTailPosition()
    newTailPosition ?= @getTailPosition()
    valid = @buffer.validMarkers[@id]?
    @trigger 'changed', {oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged, valid}

  consolidateObserverNotifications: (bufferChanged, fn) ->
    @suppressObserverNotification = true
    oldHeadPosition = @getHeadPosition()
    oldTailPosition = @getTailPosition()
    fn()
    newHeadPosition = @getHeadPosition()
    newTailPosition = @getTailPosition()
    @suppressObserverNotification = false
    @notifyObservers({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged})

  invalidate: ->
    delete @buffer.validMarkers[@id]
    @buffer.invalidMarkers[@id] = this
    @notifyObservers(bufferChanged: true)

  revalidate: ->
    delete @buffer.invalidMarkers[@id]
    @buffer.validMarkers[@id] = this
    @notifyObservers(bufferChanged: true)

_.extend BufferMarker.prototype, EventEmitter
