import {defineOption} from "../edit"
import {eventMixin} from "../util/event"
import {Transform} from "../transform"

import {rebaseSteps} from "./rebase"
export {rebaseSteps}

// !! This module implements an API into which a communication channel
// for collaborative editing can be hooked. See [this
// guide](guide/collab.html) for more details and an example.

// :: ?Object #path=collab #kind=option
//
// When given, enables the collaborative editing framework for the
// editor. Will register itself of the `Collab` class as
// `.mod.collab`.
//
// If the object given has a `version` property, that will determine
// the starting version number of the collaborative editing.

defineOption("collab", false, (pm, value) => {
  if (pm.mod.collab) {
    pm.mod.collab.detach()
    pm.mod.collab = null
  }

  if (value) {
    pm.mod.collab = new Collab(pm, value)
  }
})

function randomID() { return Math.floor(Math.random() * 0xFFFFFFFF) }

// ;; This class accumulates changes that have to be sent to the
// central authority in the collaborating group, signals an event when
// it has something to send, and makes it possible to integrate
// changes made by peers into our local document. It is created and
// attached to the editor (under `.mod.collab`) by setting the
// `collab` option.
//
// Includes the [event mixin](#EventMixin).
class Collab {
  constructor(pm, options) {
    this.pm = pm
    this.options = options
    this.clientID = randomID()

    // :: number
    // The version number of the last update received from the central
    // authority. Starts at 0 or the value of the `version` property
    // in the option object, for the editor's value when the option
    // was enabled.
    this.version = options.version || 0
    this.versionDoc = pm.doc
    pm.history.preserveItems++

    this.unconfirmedSteps = []
    this.unconfirmedMaps = []

    pm.on("transform", this.onTransform = transform => {
      for (let i = 0; i < transform.steps.length; i++) {
        this.unconfirmedSteps.push(transform.steps[i])
        this.unconfirmedMaps.push(transform.maps[i])
      }
      // :: () #path=Collab#events#mustSend
      // Fired when there are new steps to send to the central
      // authority. Consumers should respond by calling
      // `sendableSteps` and pushing those to the authority.
      this.signal("mustSend")
    })
    pm.on("beforeSetDoc", this.onSetDoc = () => {
      throw new RangeError("setDoc is not supported on a collaborative editor")
    })
  }

  detach() {
    this.pm.off("transform", this.onTransform)
    this.pm.off("beforeSetDoc", this.onSetDoc)
    this.pm.history.preserveItems++
  }

  // :: () → bool
  // Reports whether the editor has any unsent steps.
  hasSendableSteps() {
    return this.unconfirmedSteps.length > 0
  }

  // :: () → {version: number, steps: [Step]}
  // Provides the data describing the editor's unconfirmed steps. The
  // version and array of steps are the things you'd send to the
  // central authority. The whole return value must be passed to
  // [`confirmSteps`](#Collab.confirmSteps) when the steps go through.
  sendableSteps() {
    return {
      version: this.version,
      steps: this.unconfirmedSteps.slice(),
      clientID: this.clientID
    }
  }

  // :: ([Step], [number]) → [PosMap]
  // Pushes a set of steps (received from the central authority) into
  // the editor. Will recognize its own changes, and confirm
  // unconfirmed steps as appropriate. Remaining unconfirmed steps
  // will be rebased over remote steps.
  //
  // Returns the [position maps](#PosMap) produced by applying the
  // steps.
  receive(steps, clientIDs) {
    // Find out which prefix of the steps originated with us
    let ours = 0
    while (ours < clientIDs.length && clientIDs[ours] == this.clientID) ++ours

    this.version += steps.length
    if (ours == clientIDs.length && ours == this.unconfirmedSteps.length) {
      // If all steps originated with us, and we didn't make any new
      // steps in the meantime, we simply forward the confirmed state
      // to the current state.
      this.versionDoc = this.pm.doc
      this.unconfirmedSteps.length = this.unconfirmedMaps.length = 0
      return []
    }

    let transform = new Transform(this.versionDoc)
    steps.forEach(step => transform.step(step))
    this.versionDoc = transform.doc

    // Move the remaining unconfirmed steps across the new steps
    let newMaps = transform.maps.slice(ours)
    let rebased = rebaseSteps(transform.doc, newMaps,
                              this.unconfirmedSteps.slice(ours), this.unconfirmedMaps.slice(ours))
    this.unconfirmedSteps = rebased.transform.steps.slice()
    this.unconfirmedMaps = rebased.transform.maps.slice()

    let selectionBefore = this.pm.selection
    this.pm.updateDoc(rebased.doc, rebased.mapping)
    this.pm.history.rebased(newMaps, rebased.transform, rebased.positions)
    // :: (transform: Transform, selectionBeforeTransform: Selection) #path=Collab#events#collabTransform
    // Signals that a transformation has been aplied to the editor. Passes the `Transform` and the selection
    // before the transform as arguments to the handler.
    this.signal("collabTransform", transform, selectionBefore)
    return transform.maps
  }
}

eventMixin(Collab)

@@ -1,27 +1,21 @@
 import %7B
-defineOptio
+Plugi
 n%7D from 
@@ -356,519 +356,8 @@
 e.%0A%0A
-// :: ?Object #path=collab #kind=option%0A//%0A// When given, enables the collaborative editing framework for the%0A// editor. Will register itself of the %60Collab%60 class as%0A// %60.mod.collab%60.%0A//%0A// If the object given has a %60version%60 property, that will determine%0A// the starting version number of the collaborative editing.%0A%0AdefineOption(%22collab%22, false, (pm, value) =%3E %7B%0A  if (pm.mod.collab) %7B%0A    pm.mod.collab.detach()%0A    pm.mod.collab = null%0A  %7D%0A%0A  if (value) %7B%0A    pm.mod.collab = new Collab(pm, value)%0A  %7D%0A%7D)%0A%0A
 func
@@ -721,63 +721,119 @@
 tor 
-(under %60.mod.collab%60) by setting 
+when the %5Bplugin%5D(#collabEditing) is enabled,%0A// and can be accessed wi
 th
-e
 %0A// 
+%5B
 %60collab
-%60 option
+Editing.get%60%5D(#Plugin.get)
 .%0A//
@@ -942,35 +942,8 @@
  pm%0A
-    this.options = options%0A
     
@@ -1215,16 +1215,16 @@
 nabled.%0A
+
     this
@@ -1253,13 +1253,8 @@
 sion
- %7C%7C 0
 %0A   
@@ -4793,8 +4793,290 @@
 Collab)%0A
+%0A// :: Plugin%0A//%0A// Enables the collaborative editing framework for the editor.%0A//%0A// You can pass a %60version%60 option, which determines the starting%0A// version number of the collaborative editing, and defaults to 0.%0Aexport const collabEditing = new Plugin(Collab, %7B%0A  version: 0%0A%7D)%0A

Replace external defineOption with Plugin object