import P2PMesh from "./p2p-mesh.js"

import uuidv4 from "uuid/v4"
import yArray from "y-array"
import yMap from "y-map"
import yUnion, { Union } from "./y-union.js"
import yMemory from "y-memory"
import Y from "yjs"

yMemory(Y)
Y.Struct.Union = Union
yUnion(Y)
yMap(Y)
yArray(Y)

function gc(obj) {
  const objs = new Set()

  const free = (obj) => {
    if (obj == null) return

    for (const key of Object.keys(obj)) {
      if (typeof obj[key] == "object") {
        if (!objs.has(obj[key])) {
          objs.add(obj[key])
          free(obj[key])
        }
      }

      delete obj[key]
    }
  }

  free(obj)

  objs.clear()
}

export default class YjsCRDTWrapper extends Y.AbstractConnector {
  constructor(y, options) {
    if (options === undefined) {
      throw new Error("Options must not be undefined!")
    }

    options.role = "slave"
    super(y, options)

    this.y = y
    this.room = null
    this.mesh = new P2PMesh(this, options)
  }

  _initialise(room) {
    this.room = room

    super.onUserEvent((event) => {
      if (event.action == "userJoined") {
        const { user: id } = event
        this.room.dispatchEvent(new CustomEvent("userJoin", { detail: id }))
      } else if (event.action == "userLeft") {
        const { user: id } = event
        this.room.dispatchEvent(new CustomEvent("userLeave", { detail: id }))
      }
    })

    const dispatchPathUpdateEvent = (lineEvent) => {
      const pathID = lineEvent.name
      const points = this.room.getPathPoints(pathID)

      const detail = { id: pathID, points }

      this.room.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
    }

    const dispatchRemovedIntervalsEvent = (lineEvent) => {
      const pathID = lineEvent.name
      const intervals = this.room.getErasureIntervals(pathID)

      const detail = { id: pathID, intervals }

      this.room.dispatchEvent(
        new CustomEvent("removedIntervalsChange", {
          detail,
        }),
      )
    }

    this.y.share.strokePoints.observe((lineEvent) => {
      if (lineEvent.type == "add") {
        dispatchPathUpdateEvent(lineEvent)

        lineEvent.value.observe((pointEvent) => {
          if (pointEvent.type == "insert") {
            dispatchPathUpdateEvent(lineEvent)
          }
        })
      }
    })

    this.y.share.eraseIntervals.observe((lineEvent) => {
      if (lineEvent.type == "add") {
        dispatchRemovedIntervalsEvent(lineEvent)

        lineEvent.value.observe(() => {
          dispatchRemovedIntervalsEvent(lineEvent)
        })
      }
    })
  }

  destroy() {
    // yjs connectors have an optional destroy() method that is called on y.destroy()
    if (this.mesh == null) return

    this.mesh.disconnect()
    this.mesh = null

    delete this.room

    gc(this)
  }

  static async initialise(room, options) {
    const y = await Y({
      db: {
        name: "memory",
      },
      connector: Object.assign({}, options, { name: "y-crdt" }),
      share: {
        strokePoints: "Map",
        eraseIntervals: "Map",
      },
    })

    y.connector._initialise(room)

    room.crdt = y.connector
  }

  getUserID() {
    return this.y.db.userId
  }

  setUserID(uid) {
    return super.setUserId(uid)
  }

  fetchDrawingEvents() {
    // NOOP: twiddle thumbs
  }

  addPath([x, y, w, colour]) {
    const id = uuidv4()

    this.y.share.strokePoints.set(id, Y.Array).push([[x, y, w, colour]])
    this.y.share.eraseIntervals.set(id, Y.Union)

    return id
  }

  extendPath(pathID, [x, y, w, colour]) {
    const path = this.y.share.strokePoints.get(pathID)

    path.push([[x, y, w, colour]])

    return path.length
  }

  endPath(/*pathID*/) {
    // NOOP: twiddle thumbs
  }

  extendErasureIntervals(pathID, newIntervals) {
    this.y.share.eraseIntervals.get(pathID).merge(newIntervals)
  }

  getPathIDs() {
    return this.y.share.strokePoints.keys()
  }

  getPathPoints(pathID) {
    const points = this.y.share.strokePoints.get(pathID)

    if (!points) return []

    return points.toArray()
  }

  getErasureIntervals(pathID) {
    const intervals = this.y.share.eraseIntervals.get(pathID)

    if (!intervals) return []

    return intervals.get()
  }

  userJoined(uid) {
    super.userJoined(uid, "master")
  }

  userLeft(uid) {
    super.userLeft(uid)
  }

  receiveMessage(uid, message) {
    super.receiveMessage(uid, message)

    this.room.dispatchEvent(
      new CustomEvent("peerSyncedWithUs", { detail: uid }),
    )
  }

  reportConnectionQuality(uid, quality) {
    this.room.dispatchEvent(
      new CustomEvent("userConnection", { detail: { id: uid, quality } }),
    )
  }

  disconnect() {
    super.disconnect()
  }

  reconnect() {
    throw "Unsupported operation reconnect()"
  }

  send(uid, message) {
    let compressed = true

    if (message.type === "sync step 1") {
      compressed = false

      this.room.dispatchEvent(
        new CustomEvent("waitingForSyncStep", { detail: uid }),
      )
    } else if (message.type === "sync done") {
      compressed = false

      this.room.dispatchEvent(
        new CustomEvent("weSyncedWithPeer", { detail: uid }),
      )
    } else if (message.type === "sync check") {
      compressed = false
    }

    this.mesh.send(uid, message, compressed)
  }

  broadcast(message) {
    this.mesh.broadcast(message, true)
  }

  isDisconnected() {
    return false
  }
}

Y.extend("y-crdt", YjsCRDTWrapper)

export const syncStep1 = {
  uuid: "6e20b20d-e1d8-405d-8a61-d56cb1c47a24",
  message: Uint8Array.of(
    133,
    164,
    116,
    121,
    112,
    101,
    171,
    115,
    121,
    110,
    99,
    32,
    115,
    116,
    101,
    112,
    32,
    49,
    168,
    115,
    116,
    97,
    116,
    101,
    83,
    101,
    116,
    128,
    169,
    100,
    101,
    108,
    101,
    116,
    101,
    83,
    101,
    116,
    128,
    175,
    112,
    114,
    111,
    116,
    111,
    99,
    111,
    108,
    86,
    101,
    114,
    115,
    105,
    111,
    110,
    11,
    164,
    97,
    117,
    116,
    104,
    192,
  ),
  slice: 0,
  length: 1,
  compressed: false,
}

export const syncDone = {
  message: Uint8Array.of(
    129,
    164,
    116,
    121,
    112,
    101,
    169,
    115,
    121,
    110,
    99,
    32,
    100,
    111,
    110,
    101,
  ),
  slice: 0,
  length: 1,
  compressed: false,
}