 
 DEFAULT_API_OPTIONS = { silent: false, source: 'api' }
 
-
-_checkUpdate = ->
-  if @innerHTML != @root.innerHTML
-    delta = this.update()
-    @innerHTML = @root.innerHTML
-    return delta
-  else
-    return false
-
-_doAt = (fn, options) ->
-  delta = false
-  this.doSilently( =>
-    delta = _trackDelta.call(this, =>
-      fn.call(this)
-    , options)
-  )
-  return delta
 
 _deleteAt = (index, length) ->
   return if length <= 0

  )

_trackDelta = (fn, options) ->
  oldDelta = @doc.toDelta()
  oldIndex = @savedRange?.start.index
  fn()
  newDelta = @doc.toDelta()
  try
    newIndex = @selection.getRange()?.start.index     # this.getSelection() triggers infinite loop
    if oldIndex? and newIndex? and oldIndex <= oldDelta.endLength and newIndex <= newDelta.endLength
      [oldLeftDelta, oldRightDelta] = oldDelta.split(oldIndex)
      [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
      decomposeLeft = newLeftDelta.decompose(oldLeftDelta)
      decomposeRight = newRightDelta.decompose(oldRightDelta)
      decomposeA = decomposeLeft.merge(decomposeRight)
  catch ignored
  decomposeB = newDelta.decompose(oldDelta)
  if decomposeA and decomposeB
    decompose = if decomposeA.ops.length < decomposeB.ops.length then decomposeA else decomposeB
  else
    decompose = decomposeA or decomposeB
  if !decompose.isIdentity() and !options.silent
    eventName = if options.source == 'api' then ScribeEditor.events.API_TEXT_CHANGE else ScribeEditor.events.USER_TEXT_CHANGE
    this.emit(eventName, decompose)
    return decompose
  return false
  

class ScribeEditor extends EventEmitter2
  @editors: []

  @ID_PREFIX: 'editor-'

  @DEFAULTS:
    cursor: 0
    enabled: true
    onReady: ->
    formatManager: {}
    renderer: {}
    undoManager: {}

  @events:
    API_TEXT_CHANGE  : 'api-text-change'
    PRE_EVENT        : 'pre-event'
    POST_EVENT       : 'post-event'
    SELECTION_CHANGE : 'selection-change'
    USER_TEXT_CHANGE : 'user-text-change'


  constructor: (@iframeContainer, options = {}) ->
    @id = _.uniqueId(ScribeEditor.ID_PREFIX)
    @options = _.defaults(options, ScribeEditor.DEFAULTS)
    @options.renderer['id'] = @id
    @iframeContainer = document.getElementById(@iframeContainer) if _.isString(@iframeContainer)
    this.reset(true)
    setInterval( =>
      changed = _checkUpdate.call(this)
      @selection.update(changed)
    , 100)
    this.on(ScribeEditor.events.SELECTION_CHANGE, (range) =>
      @savedRange = range
    )
    this.enable() if @options.enabled

  disable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', false)
    )

  enable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', true)
    )

  reset: (keepHTML = false) ->
    @ignoreDomChanges = true
    @options.renderer.keepHTML = keepHTML
    @iframeContainer.innerHTML = @root.innerHTML if @root?
    @renderer = new ScribeRenderer(@iframeContainer, @options)
    @contentWindow = @renderer.iframe.contentWindow
    @root = @renderer.root
    @doc = new ScribeDocument(@root, @options)
    @keyboard = new ScribeKeyboard(this)
    @selection = new ScribeSelection(this)
    @undoManager = new ScribeUndoManager(this, @options)
    @pasteManager = new ScribePasteManager(this)
    @renderer.runWhenLoaded(@options.onReady)
    @ignoreDomChanges = false
    ScribeEditor.editors.push(this)

  applyDelta: (delta, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    # Make exception for systems that assume editors start with empty text
    if delta.startLength == 0 and this.getLength() == 1
      return this.setDelta(delta, options)
    return if delta.isIdentity()
    this.doSilently( =>
      throw new Error("Trying to apply delta to incorrect doc length") unless delta.startLength == this.getLength()
      oldDelta = @doc.toDelta()
      delta.apply(_insertAt, _deleteAt, _formatAt, this)
      unless options.silent
        eventName = if options.source == 'api' then ScribeEditor.events.API_TEXT_CHANGE else ScribeEditor.events.USER_TEXT_CHANGE
        this.emit(eventName, delta)
      # TODO enable when we figure out addNewline issue, currently will fail if we do add newline
      # console.assert(delta.endLength == this.getLength(), "Applying delta resulted in incorrect end length", delta, this.getLength())
      _forceTrailingNewline.call(this)
    )

  emit: (eventName, args...) ->
    super(ScribeEditor.events.PRE_EVENT, eventName, args...)
    super(eventName, args...)
    super(ScribeEditor.events.POST_EVENT, eventName, args...)

  deleteAt: (index, length, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    _doAt.call(this, =>
      _deleteAt.call(this, index, length)
      _forceTrailingNewline.call(this)
    , options)

  doSilently: (fn) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    fn()
    @ignoreDomChanges = oldIgnoreDomChange

  formatAt: (index, length, name, value, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    _doAt.call(this, =>
      _formatAt.call(this, index, length, name, value)
    , options)
    
  getDelta: ->
    _checkUpdate.call(this)
    return @doc.toDelta()

  getLength: ->
    return this.getDelta().endLength

  getSelection: ->
    _checkUpdate.call(this)
    return @selection.getRange()

  insertAt: (index, text, formatting = {}, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    _doAt.call(this, =>
      _insertAt.call(this, index, text, formatting)
      _forceTrailingNewline.call(this)
    , options)

  setDelta: (delta) ->
    oldLength = delta.startLength
    delta.startLength = this.getLength()
    this.applyDelta(delta, { silent: true })
    @undoManager.clear()
    delta.startLength = oldLength

  setSelection: (range, silent = false) ->
    @selection.setRange(range, silent)

  update: ->
    return _doAt.call(this, =>
      @selection.preserve( =>
        ScribeNormalizer.breakBlocks(@root)
        lines = @doc.lines.toArray()

          newLine = @doc.appendLine(lineNode)
          lineNode = lineNode.nextSibling
      )
    )
  )
  return false if delta.isIdentity()
  oldDelta = @delta
  @delta = oldDelta.compose(delta)
  this.emit(ScribeEditor.events.USER_TEXT_CHANGE, delta, @delta)
  return delta
  

class ScribeEditor extends EventEmitter2
  @editors: []

  @ID_PREFIX: 'editor-'

  @DEFAULTS:
    cursor: 0
    enabled: true
    onReady: ->
    pollInterval: 100
    formatManager: {}
    renderer: {}
    undoManager: {}

  @events:
    API_TEXT_CHANGE  : 'api-text-change'
    PRE_EVENT        : 'pre-event'
    POST_EVENT       : 'post-event'
    SELECTION_CHANGE : 'selection-change'
    USER_TEXT_CHANGE : 'user-text-change'


  constructor: (@iframeContainer, options = {}) ->
    @id = _.uniqueId(ScribeEditor.ID_PREFIX)
    @options = _.defaults(options, ScribeEditor.DEFAULTS)
    @options.renderer['id'] = @id
    @iframeContainer = document.getElementById(@iframeContainer) if _.isString(@iframeContainer)
    this.reset(true)
    setInterval( =>
      changed = this.update()
      @selection.update(changed)
    , @options.pollInterval)
    this.on(ScribeEditor.events.SELECTION_CHANGE, (range) =>
      @savedRange = range
    )
    this.enable() if @options.enabled

  disable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', false)
    )

  enable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', true)
    )

  reset: (keepHTML = false) ->
    @ignoreDomChanges = true
    @options.renderer.keepHTML = keepHTML
    @iframeContainer.innerHTML = @root.innerHTML if @root?
    @renderer = new ScribeRenderer(@iframeContainer, @options)
    @contentWindow = @renderer.iframe.contentWindow
    @root = @renderer.root
    @doc = new ScribeDocument(@root, @options)
    @delta = @doc.toDelta()
    @keyboard = new ScribeKeyboard(this)
    @selection = new ScribeSelection(this)
    @undoManager = new ScribeUndoManager(this, @options)
    @pasteManager = new ScribePasteManager(this)
    @renderer.runWhenLoaded(@options.onReady)
    @ignoreDomChanges = false
    ScribeEditor.editors.push(this)

  applyDelta: (delta, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    return if delta.isIdentity()
    # Make exception for systems that assume editors start with empty text
    if delta.startLength == 0 and this.getLength() == 1
      return this.setDelta(delta, options)
    this.doSilently( =>
      throw new Error("Trying to apply delta to incorrect doc length") unless delta.startLength == @delta.endLength
      #localDelta = this.update()
      #delta = delta.follows(localDelta, false) if localDelta
      delta.apply(_insertAt, _deleteAt, _formatAt, this)
      oldDelta = @delta
      @delta = oldDelta.compose(delta)
      unless options.silent
        eventName = if options.source == 'api' then ScribeEditor.events.API_TEXT_CHANGE else ScribeEditor.events.USER_TEXT_CHANGE
        this.emit(eventName, delta, @delta)
      # TODO enable when we figure out addNewline issue, currently will fail if we do add newline
      # console.assert(delta.endLength == this.getLength(), "Applying delta resulted in incorrect end length", delta, this.getLength())
      _forceTrailingNewline.call(this)
    )

  emit: (eventName, args...) ->
    super(ScribeEditor.events.PRE_EVENT, eventName, args...)
    super(eventName, args...)
    super(ScribeEditor.events.POST_EVENT, eventName, args...)

  deleteAt: (index, length, options = {}) ->
    this.applyDelta(Tandem.Delta.makeDeleteDelta(@delta.endLength, index, length), options)

  doSilently: (fn) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    fn()
    @ignoreDomChanges = oldIgnoreDomChange

  formatAt: (index, length, name, value, options = {}) ->
    attribute = {}
    attribute[name] = value
    this.applyDelta(Tandem.Delta.makeRetainDelta(@delta.endLength, index, length, attribute), options)
    
  getDelta: ->
    return @delta

  getLength: ->
    return @delta.endLength

  getSelection: ->
    return @selection.getRange()

  insertAt: (index, text, formatting = {}, options = {}) ->
    this.applyDelta(Tandem.Delta.makeInsertDelta(@delta.endLength, index, text, formatting), options)

  setDelta: (delta) ->
    oldLength = delta.startLength
    delta.startLength = this.getLength()
    this.applyDelta(delta, { silent: true })
    @undoManager.clear()
    delta.startLength = oldLength

  setSelection: (range, silent = false) ->
    @selection.setRange(range, silent)

  update: ->
    if @innerHTML != @root.innerHTML
      delta = _update.call(this)
      @innerHTML = @root.innerHTML
      return delta
    else
      return false


module.exports = ScribeEditor
