/* 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
 * 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
 */
// An alternate composable implementation for text. This is much closer
// to the implementation used by google wave.
//
// Ops are lists of components which iterate over the whole document.
// Components are either:
//   A number N: Skip N characters in the original document
//   {i:'str'}:  Insert 'str' at the current position in the document
//   {d:'str'}:  Delete 'str', which appears at the current position in the document
//
// Eg: [3, {i:'hi'}, 5, {d:'internet'}]
//
// Snapshots are strings.

let makeAppend
const p = function () {} // require('util').debug
const i = function () {} // require('util').inspect

const moduleExport =
  typeof WEB !== 'undefined' && WEB !== null ? {} : module.exports

moduleExport.name = 'text-composable'

moduleExport.create = () => ''

// -------- Utility methods

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 == null || !(c.i.length > 0)) &&
          (c.d == null || !(c.d.length > 0))
        ) {
          throw new Error(`Invalid op component: ${i(c)}`)
        }
      } 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 added')
        }
      }

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

// Makes a function for appending components to a given op.
// Exported for the randomOpGenerator.
moduleExport._makeAppend = makeAppend = op =>
  function (component) {
    if (component === 0 || component.i === '' || component.d === '') {
      return
    }
    if (op.length === 0) {
      return op.push(component)
    } else if (
      typeof component === 'number' &&
      typeof op[op.length - 1] === 'number'
    ) {
      return (op[op.length - 1] += component)
    } else if (component.i != null && op[op.length - 1].i != null) {
      return (op[op.length - 1].i += component.i)
    } else if (component.d != null && op[op.length - 1].d != null) {
      return (op[op.length - 1].d += component.d)
    } else {
      return op.push(component)
    }
  }

//  checkOp op

// 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 idx = 0
  // The offset into the component
  let offset = 0

  // Take up to length n from the front of op. If n is null, take the next
  // op component. If indivisableField == 'd', delete components won't be separated.
  // If indivisableField == 'i', insert components won't be separated.
  const take = function (n, indivisableField) {
    let c
    if (idx === op.length) {
      return null
    }
    // assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'

    if (typeof op[idx] === 'number') {
      if (n == null || op[idx] - offset <= n) {
        c = op[idx] - offset
        ++idx
        offset = 0
        return c
      } else {
        offset += n
        return n
      }
    } else {
      // Take from the string
      const field = op[idx].i ? 'i' : 'd'
      c = {}
      if (
        n == null ||
        op[idx][field].length - offset <= n ||
        field === indivisableField
      ) {
        c[field] = op[idx][field].slice(offset)
        ++idx
        offset = 0
      } else {
        c[field] = op[idx][field].slice(offset, offset + n)
        offset += n
      }
      return c
    }
  }

  const peekType = () => op[idx]

  return [take, peekType]
}

// Find and return the length of an op component
const componentLength = function (component) {
  if (typeof component === 'number') {
    return component
  } else if (component.i != null) {
    return component.i.length
  } else {
    return component.d.length
  }
}

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

// Apply the op to the string. Returns the new string.
moduleExport.apply = function (str, op) {
  p(`Applying ${i(op)} to '${str}'`)
  if (typeof str !== 'string') {
    throw new Error('Snapshot should be a string')
  }
  checkOp(op)

  const pos = 0
  const newDoc = []

  for (const component of Array.from(op)) {
    if (typeof component === 'number') {
      if (component > str.length) {
        throw new Error('The op is too long for this document')
      }
      newDoc.push(str.slice(0, component))
      str = str.slice(component)
    } else if (component.i != null) {
      newDoc.push(component.i)
    } else {
      if (component.d !== str.slice(0, component.d.length)) {
        throw new Error(
          `The deleted text '${
            component.d
          }' doesn't match the next characters in the document '${str.slice(
            0,
            component.d.length
          )}'`
        )
      }
      str = str.slice(component.d.length)
    }
  }

  if (str !== '') {
    throw new Error("The applied op doesn't traverse the entire document")
  }

  return newDoc.join('')
}

// transform op1 by op2. Return transformed version of op1.
// op1 and op2 are unchanged by transform.
moduleExport.transform = function (op, otherOp, side) {
  if (side !== 'left' && side !== 'right') {
    throw new Error(`side (${side} must be 'left' or 'right'`)
  }

  checkOp(op)
  checkOp(otherOp)
  const newOp = []

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

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

        append(chunk)
        if (typeof chunk !== 'object' || chunk.i == null) {
          length -= componentLength(chunk)
        }
      }
    } else if (component.i != null) {
      // Insert
      if (side === 'left') {
        // The left insert should go first.
        const o = peek()
        if (o != null ? o.i : undefined) {
          append(take())
        }
      }

      // Otherwise, skip the inserted text.
      append(component.i.length)
    } else {
      // Delete.
      // assert.ok component.d
      ;({ length } = component.d)
      while (length > 0) {
        chunk = take(length, 'i')
        if (chunk === null) {
          throw new Error(
            'The op traverses more elements than the document has'
          )
        }

        if (typeof chunk === 'number') {
          length -= chunk
        } else if (chunk.i != null) {
          append(chunk)
        } else {
          // assert.ok chunk.d
          // The delete is unnecessary now.
          length -= chunk.d.length
        }
      }
    }
  }

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

  return newOp
}

// Compose 2 ops into 1 op.
moduleExport.compose = function (op1, op2) {
  p(`COMPOSE ${i(op1)} + ${i(op2)}`)
  checkOp(op1)
  checkOp(op2)

  const result = []

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

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

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

        // If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
        if (typeof chunk === 'number') {
          append({ d: component.d.slice(offset, offset + chunk) })
          offset += chunk
        } else if (chunk.i != null) {
          if (component.d.slice(offset, offset + chunk.i.length) !== chunk.i) {
            throw new Error("The deleted text doesn't match the inserted text")
          }
          offset += chunk.i.length
          // The ops cancel each other out.
        } else {
          // Delete
          append(chunk)
        }
      }
    }
  }

  // Append extras from op1
  while ((component = take())) {
    if ((component != null ? component.d : undefined) == null) {
      throw new Error(`Trailing stuff in op1 ${i(component)}`)
    }
    append(component)
  }

  return result
}

const invertComponent = function (c) {
  if (typeof c === 'number') {
    return c
  } else if (c.i != null) {
    return { d: c.i }
  } else {
    return { i: c.d }
  }
}

// Invert an op
moduleExport.invert = function (op) {
  const result = []
  const append = makeAppend(result)

  for (const component of Array.from(op)) {
    append(invertComponent(component))
  }

  return result
}

if (typeof window !== 'undefined' && window !== null) {
  if (!window.ot) {
    window.ot = {}
  }
  if (!window.ot.types) {
    window.ot.types = {}
  }
  window.ot.types.text = moduleExport
}
