Range = require 'range'
_ = require 'underscore'

### Internal ###

module.exports =
class BufferChangeOperation
  buffer: null
  oldRange: null
  oldText: null
  newRange: null
  newText: null
  markersToRestoreOnUndo: null
  markersToRestoreOnRedo: null

  constructor: ({@buffer, @oldRange, @newText, @options}) ->
    @options ?= {}

  do: ->
    @buffer.pauseEvents()
    @pauseMarkerObservation()
    @markersToRestoreOnUndo = @invalidateMarkers(@oldRange)
    if @oldRange?
      @oldText = @buffer.getTextInRange(@oldRange)
      @newRange = @calculateNewRange(@oldRange, @newText)
      newRange = @changeBuffer
        oldRange: @oldRange
        newRange: @newRange
        oldText: @oldText
        newText: @newText
    @restoreMarkers(@markersToRestoreOnRedo) if @markersToRestoreOnRedo
    @buffer.resumeEvents()
    @resumeMarkerObservation()
    newRange

  undo: ->
    @buffer.pauseEvents()
    @pauseMarkerObservation()
    @markersToRestoreOnRedo = @invalidateMarkers(@newRange)
    if @oldRange?
      @changeBuffer
        oldRange: @newRange
        newRange: @oldRange
        oldText: @newText
        newText: @oldText
    @restoreMarkers(@markersToRestoreOnUndo)
    @buffer.resumeEvents()
    @resumeMarkerObservation()

  splitLines: (text) ->
    lines = text.split('\n')
    lineEndings = []
    for line, index in lines
      if _.endsWith(line, '\r')
        lines[index] = line[...-1]
        lineEndings[index] = '\r\n'
      else
        lineEndings[index] = '\n'
    {lines, lineEndings}

  changeBuffer: ({ oldRange, newRange, newText, oldText }) ->
    { prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange)
    {lines, lineEndings} = @splitLines(newText)
    lastLineIndex = lines.length - 1

    if lines.length == 1
      lines = [prefix + newText + suffix]
    else
      lines[0] = prefix + lines[0]
      lines[lastLineIndex] += suffix

    startRow = oldRange.start.row
    endRow = oldRange.end.row

    normalizeLineEndings = @options.normalizeLineEndings ? true
    if normalizeLineEndings and suggestedLineEnding = @buffer.suggestedLineEndingForRow(startRow)
      lineEndings[index] = suggestedLineEnding for index in [0..lastLineIndex]

    _.spliceWithArray(@buffer.lines, startRow, endRow - startRow + 1, lines)
    _.spliceWithArray(@buffer.lineEndings, startRow, endRow - startRow + 1, lineEndings)
    @buffer.cachedMemoryContents = null
    @buffer.conflict = false if @buffer.conflict and !@buffer.isModified()

    event = { oldRange, newRange, oldText, newText }
    @updateMarkers(event)
    @buffer.trigger 'changed', event
    @buffer.scheduleModifiedEvents()

    newRange

  calculateNewRange: (oldRange, newText) ->
    newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
    {lines} = @splitLines(newText)
    if lines.length == 1
      newRange.end.column += newText.length
    else
      lastLineIndex = lines.length - 1
      newRange.end.row += lastLineIndex
      newRange.end.column = lines[lastLineIndex].length
    newRange

  invalidateMarkers: (oldRange) ->
    @buffer.getMarkers().map (marker) -> marker.tryToInvalidate(oldRange)

  pauseMarkerObservation: ->
    marker.pauseEvents() for marker in @buffer.getMarkers(includeInvalid: true)

  resumeMarkerObservation: ->
    marker.resumeEvents() for marker in @buffer.getMarkers(includeInvalid: true)
    @buffer.trigger 'markers-updated' if @oldRange?

  updateMarkers: (bufferChange) ->
    marker.handleBufferChange(bufferChange) for marker in @buffer.getMarkers()

  restoreMarkers: (markersToRestore) ->
    for [id, previousRange] in markersToRestore
      if validMarker = @buffer.validMarkers[id]
        validMarker.setRange(previousRange)
      else if invalidMarker = @buffer.invalidMarkers[id]
        invalidMarker.revalidate()
