/* eslint-disable
    no-cond-assign,
    no-return-assign,
    no-undef,
    no-unused-vars,
*/
// 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
 * DS103: Rewrite code to no longer use __guard__
 * 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
 */
// A TP2 implementation of text, following this spec:
// http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README
//
// A document is made up of a string and a set of tombstones inserted throughout
// the string. For example, 'some ', (2 tombstones), 'string'.
//
// This is encoded in a document as: {s:'some string', t:[5, -2, 6]}
//
// Ops are lists of components which iterate over the whole document.
// Components are either:
//   N:         Skip N characters in the original document
//   {i:'str'}: Insert 'str' at the current position in the document
//   {i:N}:     Insert N tombstones at the current position in the document
//   {d:N}:     Delete (tombstone) N characters at the current position in the document
//
// Eg: [3, {i:'hi'}, 5, {d:8}]
//
// Snapshots are lists with characters and tombstones. Characters are stored in strings
// and adjacent tombstones are flattened into numbers.
//
// Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters)
// would be represented by a document snapshot of ['Hello ', 5, 'world']

let append, appendDoc, takeDoc
const type = {
  name: 'text-tp2',
  tp2: true,
  create() {
    return { charLength: 0, totalLength: 0, positionCache: [], data: [] }
  },
  serialize(doc) {
    if (!doc.data) {
      throw new Error('invalid doc snapshot')
    }
    return doc.data
  },
  deserialize(data) {
    const doc = type.create()
    doc.data = data

    for (const component of Array.from(data)) {
      if (typeof component === 'string') {
        doc.charLength += component.length
        doc.totalLength += component.length
      } else {
        doc.totalLength += component
      }
    }

    return doc
  },
}

const checkOp = function (op) {
  if (!Array.isArray(op)) {
    throw new Error('Op must be an array of components')
  }
  let last = null
  return (() => {
    const result = []
    for (const c of Array.from(op)) {
      if (typeof c === 'object') {
        if (c.i !== undefined) {
          if (
            (typeof c.i !== 'string' || !(c.i.length > 0)) &&
            (typeof c.i !== 'number' || !(c.i > 0))
          ) {
            throw new Error('Inserts must insert a string or a +ive number')
          }
        } else if (c.d !== undefined) {
          if (typeof c.d !== 'number' || !(c.d > 0)) {
            throw new Error('Deletes must be a +ive number')
          }
        } else {
          throw new Error('Operation component must define .i or .d')
        }
      } else {
        if (typeof c !== 'number') {
          throw new Error('Op components must be objects or numbers')
        }
        if (!(c > 0)) {
          throw new Error('Skip components must be a positive number')
        }
        if (typeof last === 'number') {
          throw new Error('Adjacent skip components should be combined')
        }
      }

      result.push((last = c))
    }
    return result
  })()
}

// Take the next part from the specified position in a document snapshot.
// position = {index, offset}. It will be updated.
type._takeDoc = takeDoc = function (
  doc,
  position,
  maxlength,
  tombsIndivisible
) {
  if (position.index >= doc.data.length) {
    throw new Error('Operation goes past the end of the document')
  }

  const part = doc.data[position.index]
  // peel off data[0]
  const result =
    typeof part === 'string'
      ? maxlength !== undefined
        ? part.slice(position.offset, position.offset + maxlength)
        : part.slice(position.offset)
      : maxlength === undefined || tombsIndivisible
        ? part - position.offset
        : Math.min(maxlength, part - position.offset)

  const resultLen = result.length || result

  if ((part.length || part) - position.offset > resultLen) {
    position.offset += resultLen
  } else {
    position.index++
    position.offset = 0
  }

  return result
}

// Append a part to the end of a document
type._appendDoc = appendDoc = function (doc, p) {
  if (p === 0 || p === '') {
    return
  }

  if (typeof p === 'string') {
    doc.charLength += p.length
    doc.totalLength += p.length
  } else {
    doc.totalLength += p
  }

  const { data } = doc
  if (data.length === 0) {
    data.push(p)
  } else if (typeof data[data.length - 1] === typeof p) {
    data[data.length - 1] += p
  } else {
    data.push(p)
  }
}

// Apply the op to the document. The document is not modified in the process.
type.apply = function (doc, op) {
  if (
    doc.totalLength === undefined ||
    doc.charLength === undefined ||
    doc.data.length === undefined
  ) {
    throw new Error('Snapshot is invalid')
  }

  checkOp(op)

  const newDoc = type.create()
  const position = { index: 0, offset: 0 }

  for (const component of Array.from(op)) {
    let part, remainder
    if (typeof component === 'number') {
      remainder = component
      while (remainder > 0) {
        part = takeDoc(doc, position, remainder)

        appendDoc(newDoc, part)
        remainder -= part.length || part
      }
    } else if (component.i !== undefined) {
      appendDoc(newDoc, component.i)
    } else if (component.d !== undefined) {
      remainder = component.d
      while (remainder > 0) {
        part = takeDoc(doc, position, remainder)
        remainder -= part.length || part
      }
      appendDoc(newDoc, component.d)
    }
  }

  return newDoc
}

// Append an op component to the end of the specified op.
// Exported for the randomOpGenerator.
type._append = append = function (op, component) {
  if (
    component === 0 ||
    component.i === '' ||
    component.i === 0 ||
    component.d === 0
  ) {
    return
  }
  if (op.length === 0) {
    return op.push(component)
  } else {
    const last = op[op.length - 1]
    if (typeof component === 'number' && typeof last === 'number') {
      return (op[op.length - 1] += component)
    } else if (
      component.i !== undefined &&
      last.i != null &&
      typeof last.i === typeof component.i
    ) {
      return (last.i += component.i)
    } else if (component.d !== undefined && last.d != null) {
      return (last.d += component.d)
    } else {
      return op.push(component)
    }
  }
}

// Makes 2 functions for taking components from the start of an op, and for peeking
// at the next op that could be taken.
const makeTake = function (op) {
  // The index of the next component to take
  let index = 0
  // The offset into the component
  let offset = 0

  // Take up to length maxlength from the op. If maxlength is not defined, there is no max.
  // If insertsIndivisible is true, inserts (& insert tombstones) won't be separated.
  //
  // Returns null when op is fully consumed.
  const take = function (maxlength, insertsIndivisible) {
    let current
    if (index === op.length) {
      return null
    }

    const e = op[index]
    if (
      typeof (current = e) === 'number' ||
      typeof (current = e.i) === 'number' ||
      (current = e.d) !== undefined
    ) {
      let c
      if (
        maxlength == null ||
        current - offset <= maxlength ||
        (insertsIndivisible && e.i !== undefined)
      ) {
        // Return the rest of the current element.
        c = current - offset
        ++index
        offset = 0
      } else {
        offset += maxlength
        c = maxlength
      }
      if (e.i !== undefined) {
        return { i: c }
      } else if (e.d !== undefined) {
        return { d: c }
      } else {
        return c
      }
    } else {
      // Take from the inserted string
      let result
      if (
        maxlength == null ||
        e.i.length - offset <= maxlength ||
        insertsIndivisible
      ) {
        result = { i: e.i.slice(offset) }
        ++index
        offset = 0
      } else {
        result = { i: e.i.slice(offset, offset + maxlength) }
        offset += maxlength
      }
      return result
    }
  }

  const peekType = () => op[index]

  return [take, peekType]
}

// Find and return the length of an op component
const componentLength = function (component) {
  if (typeof component === 'number') {
    return component
  } else if (typeof component.i === 'string') {
    return component.i.length
  } else {
    // This should work because c.d and c.i must be +ive.
    return component.d || component.i
  }
}

// Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
// adjacent inserts and deletes.
type.normalize = function (op) {
  const newOp = []
  for (const component of Array.from(op)) {
    append(newOp, component)
  }
  return newOp
}

// This is a helper method to transform and prune. goForwards is true for transform, false for prune.
const transformer = function (op, otherOp, goForwards, side) {
  let component
  checkOp(op)
  checkOp(otherOp)
  const newOp = []

  const [take, peek] = Array.from(makeTake(op))

  for (component of Array.from(otherOp)) {
    let chunk
    let length = componentLength(component)

    if (component.i !== undefined) {
      // Insert text or tombs
      if (goForwards) {
        // transform - insert skips over inserted parts
        if (side === 'left') {
          // The left insert should go first.
          while (__guard__(peek(), x => x.i) !== undefined) {
            append(newOp, take())
          }
        }

        // In any case, skip the inserted text.
        append(newOp, length)
      } else {
        // Prune. Remove skips for inserts.
        while (length > 0) {
          chunk = take(length, true)

          if (chunk === null) {
            throw new Error('The transformed op is invalid')
          }
          if (chunk.d !== undefined) {
            throw new Error(
              'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'
            )
          }

          if (typeof chunk === 'number') {
            length -= chunk
          } else {
            append(newOp, chunk)
          }
        }
      }
    } else {
      // Skip or delete
      while (length > 0) {
        chunk = take(length, true)
        if (chunk === null) {
          throw new Error(
            'The op traverses more elements than the document has'
          )
        }

        append(newOp, chunk)
        if (!chunk.i) {
          length -= componentLength(chunk)
        }
      }
    }
  }

  // Append extras from op1
  while ((component = take())) {
    if (component.i === undefined) {
      throw new Error(`Remaining fragments in the op: ${component}`)
    }
    append(newOp, component)
  }

  return newOp
}

// transform op1 by op2. Return transformed version of op1.
// op1 and op2 are unchanged by transform.
// side should be 'left' or 'right', depending on if op1.id <> op2.id. 'left' == client op.
type.transform = function (op, otherOp, side) {
  if (side !== 'left' && side !== 'right') {
    throw new Error(`side (${side}) should be 'left' or 'right'`)
  }
  return transformer(op, otherOp, true, side)
}

// Prune is the inverse of transform.
type.prune = (op, otherOp) => transformer(op, otherOp, false)

// Compose 2 ops into 1 op.
type.compose = function (op1, op2) {
  let component
  if (op1 === null || op1 === undefined) {
    return op2
  }

  checkOp(op1)
  checkOp(op2)

  const result = []

  const [take, _] = Array.from(makeTake(op1))

  for (component of Array.from(op2)) {
    let chunk, length
    if (typeof component === 'number') {
      // Skip
      // Just copy from op1.
      length = component
      while (length > 0) {
        chunk = take(length)
        if (chunk === null) {
          throw new Error(
            'The op traverses more elements than the document has'
          )
        }

        append(result, chunk)
        length -= componentLength(chunk)
      }
    } else if (component.i !== undefined) {
      // Insert
      append(result, { i: component.i })
    } else {
      // Delete
      length = component.d
      while (length > 0) {
        chunk = take(length)
        if (chunk === null) {
          throw new Error(
            'The op traverses more elements than the document has'
          )
        }

        const chunkLength = componentLength(chunk)
        if (chunk.i !== undefined) {
          append(result, { i: chunkLength })
        } else {
          append(result, { d: chunkLength })
        }

        length -= chunkLength
      }
    }
  }

  // Append extras from op1
  while ((component = take())) {
    if (component.i === undefined) {
      throw new Error(`Remaining fragments in op1: ${component}`)
    }
    append(result, component)
  }

  return result
}

if (typeof WEB !== 'undefined' && WEB !== null) {
  exports.types['text-tp2'] = type
} else {
  module.exports = type
}

function __guard__(value, transform) {
  return typeof value !== 'undefined' && value !== null
    ? transform(value)
    : undefined
}
