/* eslint-disable
    no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS205: Consider reworking code to avoid use of IIFEs
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
// API for JSON OT

let json
if (typeof WEB === 'undefined') {
  json = require('./json')
}

if (typeof WEB !== 'undefined' && WEB !== null) {
  const { extendDoc } = exports
  exports.extendDoc = function (name, fn) {
    SubDoc.prototype[name] = fn
    return extendDoc(name, fn)
  }
}

const depath = function (path) {
  if (path.length === 1 && path[0].constructor === Array) {
    return path[0]
  } else {
    return path
  }
}

class SubDoc {
  constructor(doc, path) {
    this.doc = doc
    this.path = path
  }

  at(...path) {
    return this.doc.at(this.path.concat(depath(path)))
  }

  get() {
    return this.doc.getAt(this.path)
  }

  // for objects and lists
  set(value, cb) {
    return this.doc.setAt(this.path, value, cb)
  }

  // for strings and lists.
  insert(pos, value, cb) {
    return this.doc.insertAt(this.path, pos, value, cb)
  }

  // for strings
  del(pos, length, cb) {
    return this.doc.deleteTextAt(this.path, length, pos, cb)
  }

  // for objects and lists
  remove(cb) {
    return this.doc.removeAt(this.path, cb)
  }

  push(value, cb) {
    return this.insert(this.get().length, value, cb)
  }

  move(from, to, cb) {
    return this.doc.moveAt(this.path, from, to, cb)
  }

  add(amount, cb) {
    return this.doc.addAt(this.path, amount, cb)
  }

  on(event, cb) {
    return this.doc.addListener(this.path, event, cb)
  }

  removeListener(l) {
    return this.doc.removeListener(l)
  }

  // text API compatibility
  getLength() {
    return this.get().length
  }

  getText() {
    return this.get()
  }
}

const traverse = function (snapshot, path) {
  const container = { data: snapshot }
  let key = 'data'
  let elem = container
  for (const p of Array.from(path)) {
    elem = elem[key]
    key = p
    if (typeof elem === 'undefined') {
      throw new Error('bad path')
    }
  }
  return { elem, key }
}

const pathEquals = function (p1, p2) {
  if (p1.length !== p2.length) {
    return false
  }
  for (let i = 0; i < p1.length; i++) {
    const e = p1[i]
    if (e !== p2[i]) {
      return false
    }
  }
  return true
}

json.api = {
  provides: { json: true },

  at(...path) {
    return new SubDoc(this, depath(path))
  },

  get() {
    return this.snapshot
  },
  set(value, cb) {
    return this.setAt([], value, cb)
  },

  getAt(path) {
    const { elem, key } = traverse(this.snapshot, path)
    return elem[key]
  },

  setAt(path, value, cb) {
    const { elem, key } = traverse(this.snapshot, path)
    const op = { p: path }
    if (elem.constructor === Array) {
      op.li = value
      if (typeof elem[key] !== 'undefined') {
        op.ld = elem[key]
      }
    } else if (typeof elem === 'object') {
      op.oi = value
      if (typeof elem[key] !== 'undefined') {
        op.od = elem[key]
      }
    } else {
      throw new Error('bad path')
    }
    return this.submitOp([op], cb)
  },

  removeAt(path, cb) {
    const { elem, key } = traverse(this.snapshot, path)
    if (typeof elem[key] === 'undefined') {
      throw new Error('no element at that path')
    }
    const op = { p: path }
    if (elem.constructor === Array) {
      op.ld = elem[key]
    } else if (typeof elem === 'object') {
      op.od = elem[key]
    } else {
      throw new Error('bad path')
    }
    return this.submitOp([op], cb)
  },

  insertAt(path, pos, value, cb) {
    const { elem, key } = traverse(this.snapshot, path)
    const op = { p: path.concat(pos) }
    if (elem[key].constructor === Array) {
      op.li = value
    } else if (typeof elem[key] === 'string') {
      op.si = value
    }
    return this.submitOp([op], cb)
  },

  moveAt(path, from, to, cb) {
    const op = [{ p: path.concat(from), lm: to }]
    return this.submitOp(op, cb)
  },

  addAt(path, amount, cb) {
    const op = [{ p: path, na: amount }]
    return this.submitOp(op, cb)
  },

  deleteTextAt(path, length, pos, cb) {
    const { elem, key } = traverse(this.snapshot, path)
    const op = [{ p: path.concat(pos), sd: elem[key].slice(pos, pos + length) }]
    return this.submitOp(op, cb)
  },

  addListener(path, event, cb) {
    const l = { path, event, cb }
    this._listeners.push(l)
    return l
  },
  removeListener(l) {
    const i = this._listeners.indexOf(l)
    if (i < 0) {
      return false
    }
    this._listeners.splice(i, 1)
    return true
  },
  _register() {
    this._listeners = []
    this.on('change', function (op) {
      return (() => {
        const result = []
        for (const c of Array.from(op)) {
          let i
          if (c.na !== undefined || c.si !== undefined || c.sd !== undefined) {
            // no change to structure
            continue
          }
          const toRemove = []
          for (i = 0; i < this._listeners.length; i++) {
            // Transform a dummy op by the incoming op to work out what
            // should happen to the listener.
            const l = this._listeners[i]
            const dummy = { p: l.path, na: 0 }
            const xformed = this.type.transformComponent([], dummy, c, 'left')
            if (xformed.length === 0) {
              // The op was transformed to noop, so we should delete the listener.
              toRemove.push(i)
            } else if (xformed.length === 1) {
              // The op remained, so grab its new path into the listener.
              l.path = xformed[0].p
            } else {
              throw new Error(
                "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
              )
            }
          }
          toRemove.sort((a, b) => b - a)
          result.push(
            (() => {
              const result1 = []
              for (i of Array.from(toRemove)) {
                result1.push(this._listeners.splice(i, 1))
              }
              return result1
            })()
          )
        }
        return result
      })()
    })
    return this.on('remoteop', function (op) {
      return (() => {
        const result = []
        for (const c of Array.from(op)) {
          const matchPath =
            c.na === undefined ? c.p.slice(0, c.p.length - 1) : c.p
          result.push(
            (() => {
              const result1 = []
              for (const { path, event, cb } of Array.from(this._listeners)) {
                let common
                if (pathEquals(path, matchPath)) {
                  switch (event) {
                    case 'insert':
                      if (c.li !== undefined && c.ld === undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.li))
                      } else if (c.oi !== undefined && c.od === undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.oi))
                      } else if (c.si !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.si))
                      } else {
                        result1.push(undefined)
                      }
                      break
                    case 'delete':
                      if (c.li === undefined && c.ld !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.ld))
                      } else if (c.oi === undefined && c.od !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.od))
                      } else if (c.sd !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.sd))
                      } else {
                        result1.push(undefined)
                      }
                      break
                    case 'replace':
                      if (c.li !== undefined && c.ld !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.ld, c.li))
                      } else if (c.oi !== undefined && c.od !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.od, c.oi))
                      } else {
                        result1.push(undefined)
                      }
                      break
                    case 'move':
                      if (c.lm !== undefined) {
                        result1.push(cb(c.p[c.p.length - 1], c.lm))
                      } else {
                        result1.push(undefined)
                      }
                      break
                    case 'add':
                      if (c.na !== undefined) {
                        result1.push(cb(c.na))
                      } else {
                        result1.push(undefined)
                      }
                      break
                    default:
                      result1.push(undefined)
                  }
                } else if (
                  (common = this.type.commonPath(matchPath, path)) != null
                ) {
                  if (event === 'child op') {
                    if (
                      matchPath.length === path.length &&
                      path.length === common
                    ) {
                      throw new Error(
                        "paths match length and have commonality, but aren't equal?"
                      )
                    }
                    const childPath = c.p.slice(common + 1)
                    result1.push(cb(childPath, c))
                  } else {
                    result1.push(undefined)
                  }
                } else {
                  result1.push(undefined)
                }
              }
              return result1
            })()
          )
        }
        return result
      })()
    })
  },
}
