/**
 * @module bindings/textarea
 */

import { createMutex } from '../lib/mutex.js'
import * as math from '../lib/math.js'
import * as ypos from '../utils/relativePosition.js'

const typeObserver = (binding, event) => {
  binding._mux(() => {
    const cm = binding.target
    cm.operation(() => {
      const delta = event.delta
      let index = 0
      for (let i = 0; i < event.delta.length; i++) {
        const d = delta[i]
        if (d.retain) {
          index += d.retain
        } else if (d.insert) {
          const pos = cm.posFromIndex(index)
          cm.replaceRange(d.insert, pos, pos, 'prosemirror-binding')
          index += d.insert.length
        } else if (d.delete) {
          const start = cm.posFromIndex(index)
          const end = cm.posFromIndex(index + d.delete)
          cm.replaceRange('', start, end, 'prosemirror-binding')
        }
      }
    })
  })
}

const targetObserver = (binding, change) => {
  binding._mux(() => {
    const start = binding.target.indexFromPos(change.from)
    const delLen = change.removed.map(s => s.length).reduce(math.add) + change.removed.length - 1
    if (delLen > 0) {
      binding.type.delete(start, delLen)
    }
    if (change.text.length > 0) {
      binding.type.insert(start, change.text.join('\n'))
    }
  })
}

const createRemoteCaret = (username, color) => {
  const caret = document.createElement('span')
  caret.classList.add('remote-caret')
  caret.setAttribute('style', `border-color: ${color}`)
  const userDiv = document.createElement('div')
  userDiv.setAttribute('style', `background-color: ${color}`)
  userDiv.insertBefore(document.createTextNode(username), null)
  caret.insertBefore(userDiv, null)
  return caret
}

const updateRemoteSelection = (y, cm, type, cursors, clientId) => {
  // destroy current text mark
  const m = cursors.get(clientId)
  if (m !== undefined) {
    m.caret.clear()
    if (m.sel !== null) {
      m.sel.clear()
    }
    cursors.delete(clientId)
  }
  // redraw caret and selection for clientId
  const aw = y.awareness.get(clientId)
  if (aw === undefined) {
    return
  }
  const user = aw.user || {}
  if (user.color == null) {
    user.color = '#ffa500'
  }
  if (user.name == null) {
    user.name = `User: ${clientId}`
  }
  const cursor = aw.cursor
  if (cursor == null || cursor.anchor == null || cursor.head == null) {
    return
  }
  const anchor = ypos.fromRelativePosition(y, cursor.anchor || null)
  const head = ypos.fromRelativePosition(y, cursor.head || null)
  if (anchor !== null && head !== null && anchor.type === type && head.type === type) {
    const headpos = cm.posFromIndex(head.offset)
    const anchorpos = cm.posFromIndex(anchor.offset)
    let from, to
    if (head.offset < anchor.offset) {
      from = headpos
      to = anchorpos
    } else {
      from = anchorpos
      to = headpos
    }
    const caretEl = createRemoteCaret(user.name, user.color)
    const caret = cm.setBookmark(headpos, { widget: caretEl, insertLeft: true })
    let sel = null
    if (head.offset !== anchor.offset) {
      sel = cm.markText(from, to, { css: `background-color: ${user.color}70`, inclusiveRight: true, inclusiveLeft: false })
    }
    cursors.set(clientId, { caret, sel })
  }
}

const prosemirrorCursorActivity = (y, cm, type) => {
  if (!cm.hasFocus()) {
    return
  }
  const aw = y.getLocalAwarenessInfo()
  const anchor = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('anchor')))
  const head = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('head')))
  if (aw.cursor == null || !ypos.equal(aw.cursor.anchor, anchor) || !ypos.equal(aw.cursor.head, head)) {
    y.setAwarenessField('cursor', {
      anchor, head
    })
  }
}

/**
 * A binding that binds a YText to a CodeMirror editor.
 *
 * @example
 *   const ytext = ydocument.define('codemirror', Y.Text)
 *   const editor = new CodeMirror(document.querySelector('#container'), {
 *     mode: 'javascript',
 *     lineNumbers: true
 *   })
 *   const binding = new CodeMirrorBinding(editor)
 *
 */
export class CodeMirrorBinding {
  /**
   * @param {YText} textType
   * @param {CodeMirror} codeMirror
   * @param {Object} [options={cursors: true}]
   */
  constructor (textType, codeMirror, { cursors = true } = {}) {
    const y = textType._y
    this.type = textType
    this.target = codeMirror
    /**
     * @private
     */
    this._mux = createMutex()
    // set initial value
    codeMirror.setValue(textType.toString())
    // observe type and target
    this._typeObserver = event => typeObserver(this, event)
    this._targetObserver = (_, change) => targetObserver(this, change)
    this._cursors = new Map()
    this._awarenessListener = event => {
      const f = clientId => updateRemoteSelection(y, codeMirror, textType, this._cursors, clientId)
      event.added.forEach(f)
      event.removed.forEach(f)
      event.updated.forEach(f)
    }
    this._cursorListener = () => prosemirrorCursorActivity(y, codeMirror, textType)
    this._blurListeer = () =>
      y.setAwarenessField('cursor', null)
    textType.observe(this._typeObserver)
    codeMirror.on('change', this._targetObserver)
    if (cursors) {
      y.on('awareness', this._awarenessListener)
      codeMirror.on('cursorActivity', this._cursorListener)
      codeMirror.on('blur', this._blurListeer)
      codeMirror.on('focus', this._cursorListener)
    }
  }
  destroy () {
    this.type.unobserve(this._typeObserver)
    this.target.off('change', this._targetObserver)
    this.type.off('awareness', this._awarenessListener)
    this.target.off('cursorActivity', this._cursorListener)
    this.target.off('focus', this._cursorListener)
    this.target.off('blur', this._blurListeer)
    this.type = null
    this.target = null
  }
}