import browser from "./browser"
import {domIndex} from "./dom"
import {hasFocusAndSelection, hasSelection, selectionToDOM} from "./selection"

const observeOptions = {childList: true, characterData: true, attributes: true, subtree: true, characterDataOldValue: true}
// IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified
const useCharData = browser.ie && browser.ie_version <= 11

class SelectionState {
  constructor() {
    this.anchorNode = this.anchorOffset = this.focusNode = this.focusOffset = null
  }

  set(sel) {
    this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
    this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
  }

  eq(sel) {
    return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
      sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
  }
}

export class DOMObserver {
  constructor(view, handleDOMChange) {
    this.view = view
    this.handleDOMChange = handleDOMChange
    this.observer = window.MutationObserver &&
      new window.MutationObserver(mutations => this.flush(mutations))
    this.currentSelection = new SelectionState
    this.charDataQueue = []
    if (useCharData) {
      this.onCharData = e => {
        this.charDataQueue.push({target: e.target, type: "characterData", oldValue: e.prevValue})
        window.setTimeout(() => this.flush(), 20)
      }
    }
    this.onSelectionChange = this.onSelectionChange.bind(this)
  }

  start() {
    if (this.observer)
      this.observer.observe(this.view.dom, observeOptions)
    if (useCharData)
      this.view.dom.addEventListener("DOMCharacterDataModified", this.onCharData)
    this.connectSelection()
  }

  stop() {
    if (this.observer) this.observer.disconnect()
    if (useCharData) this.view.dom.removeEventListener("DOMCharacterDataModified", this.onCharData)
    this.disconnectSelection()
  }

  connectSelection() {
    this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange)
  }

  disconnectSelection() {
    this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange)
  }

  onSelectionChange() {
    if (!hasFocusAndSelection(this.view)) return
    if (this.suppressSelectionUpdates) return selectionToDOM(this.view)
    this.flush()
  }

  setCurSelection() {
    this.currentSelection.set(this.view.root.getSelection())
  }

  flush(mutations) {
    if (!mutations) mutations = this.observer.takeRecords()
    if (this.charDataQueue.length) {
      mutations = this.charDataQueue.concat(mutations)
      this.charDataQueue.length = 0
    }

    let sel = this.view.root.getSelection()
    let newSel = !this.currentSelection.eq(sel) && hasSelection(this.view)

    let from = -1, to = -1, typeOver = false
    if (this.view.editable) {
      for (let i = 0; i < mutations.length; i++) {
        let result = this.registerMutation(mutations[i])
        if (result) {
          from = from < 0 ? result.from : Math.min(result.from, from)
          to = to < 0 ? result.to : Math.max(result.to, to)
          if (result.typeOver) typeOver = true
        }
      }
    }
    if (from > -1 || newSel) {
      if (from > -1) this.view.docView.markDirty(from, to)
      this.handleDOMChange(from, to, typeOver)
      if (this.view.docView.dirty) this.view.updateState(this.view.state)
      else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view)
    }
  }

  registerMutation(mut) {
    let desc = this.view.docView.nearestDesc(mut.target)
    if (mut.type == "attributes" &&
        (desc == this.view.docView || mut.attributeName == "contenteditable")) return null
    if (!desc || desc.ignoreMutation(mut)) return null

    if (mut.type == "childList") {
      let fromOffset = mut.previousSibling && mut.previousSibling.parentNode == mut.target
          ? domIndex(mut.previousSibling) + 1 : 0
      let from = desc.localPosFromDOM(mut.target, fromOffset, -1)
      let toOffset = mut.nextSibling && mut.nextSibling.parentNode == mut.target
          ? domIndex(mut.nextSibling) : mut.target.childNodes.length
      let to = desc.localPosFromDOM(mut.target, toOffset, 1)
      return {from, to}
    } else if (mut.type == "attributes") {
      return {from: desc.posAtStart - desc.border, to: desc.posAtEnd + desc.border}
    } else { // "characterData"
      return {
        from: desc.posAtStart,
        to: desc.posAtEnd,
        // An event was generated for a text change that didn't change
        // any text. Mark the dom change to fall back to assuming the
        // selection was typed over with an identical value if it can't
        // find another change.
        typeOver: mut.target.nodeValue == mut.oldValue
      }
    }
  }
}

@@ -1187,33 +1187,25 @@
 te%0A    this.
-charDataQ
+q
 ueue = %5B%5D%0A  
@@ -1265,33 +1265,25 @@
        this.
-charDataQ
+q
 ueue.push(%7Bt
@@ -1715,24 +1715,213 @@
 %0A  stop() %7B%0A
+    let take = this.observer.takeRecords()%0A    if (take.length) %7B%0A      for (let i = 0; i %3C take.length; i++) this.queue.push(take%5Bi%5D)%0A      window.setTimeout(() =%3E this.flush(), 20)%0A    %7D%0A
     if (this
@@ -2612,24 +2612,59 @@
 utations) %7B%0A
+    if (!this.view.docView) return%0A
     if (!mut
@@ -2720,33 +2720,25 @@
    if (this.
-charDataQ
+q
 ueue.length)
@@ -2759,33 +2759,25 @@
 ions = this.
-charDataQ
+q
 ueue.concat(
@@ -2802,17 +2802,9 @@
 his.
-charDataQ
+q
 ueue

Fix dropped mutation records in DOMObserver