/* eslint-disable
    camelcase,
    handle-callback-err,
    new-cap,
    no-throw-literal,
    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
 * DS103: Rewrite code to no longer use __guard__
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
let oneMinute, twoMegabytes, UpdateCompressor
const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
const strRemove = (s1, pos, length) => s1.slice(0, pos) + s1.slice(pos + length)

const { diff_match_patch } = require('../lib/diff_match_patch')
const dmp = new diff_match_patch()

module.exports = UpdateCompressor = {
  NOOP: 'noop',

  // Updates come from the doc updater in format
  // {
  // 	op:   [ { ... op1 ... }, { ... op2 ... } ]
  // 	meta: { ts: ..., user_id: ... }
  // }
  // but it's easier to work with on op per update, so convert these updates to
  // our compressed format
  // [{
  // 	op: op1
  // 	meta: { start_ts: ... , end_ts: ..., user_id: ... }
  // }, {
  // 	op: op2
  // 	meta: { start_ts: ... , end_ts: ..., user_id: ... }
  // }]
  convertToSingleOpUpdates(updates) {
    const splitUpdates = []
    for (const update of Array.from(updates)) {
      // Reject any non-insert or delete ops, i.e. comments
      const ops = update.op.filter((o) => o.i != null || o.d != null)
      if (ops.length === 0) {
        splitUpdates.push({
          op: UpdateCompressor.NOOP,
          meta: {
            start_ts: update.meta.start_ts || update.meta.ts,
            end_ts: update.meta.end_ts || update.meta.ts,
            user_id: update.meta.user_id
          },
          v: update.v
        })
      } else {
        for (const op of Array.from(ops)) {
          splitUpdates.push({
            op,
            meta: {
              start_ts: update.meta.start_ts || update.meta.ts,
              end_ts: update.meta.end_ts || update.meta.ts,
              user_id: update.meta.user_id
            },
            v: update.v
          })
        }
      }
    }
    return splitUpdates
  },

  concatUpdatesWithSameVersion(updates) {
    const concattedUpdates = []
    for (const update of Array.from(updates)) {
      const lastUpdate = concattedUpdates[concattedUpdates.length - 1]
      if (lastUpdate != null && lastUpdate.v === update.v) {
        if (update.op !== UpdateCompressor.NOOP) {
          lastUpdate.op.push(update.op)
        }
      } else {
        const nextUpdate = {
          op: [],
          meta: update.meta,
          v: update.v
        }
        if (update.op !== UpdateCompressor.NOOP) {
          nextUpdate.op.push(update.op)
        }
        concattedUpdates.push(nextUpdate)
      }
    }
    return concattedUpdates
  },

  compressRawUpdates(lastPreviousUpdate, rawUpdates) {
    if (
      __guard__(
        lastPreviousUpdate != null ? lastPreviousUpdate.op : undefined,
        (x) => x.length
      ) > 1
    ) {
      // if the last previous update was an array op, don't compress onto it.
      // The avoids cases where array length changes but version number doesn't
      return [lastPreviousUpdate].concat(
        UpdateCompressor.compressRawUpdates(null, rawUpdates)
      )
    }
    if (lastPreviousUpdate != null) {
      rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
    }
    let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
    updates = UpdateCompressor.compressUpdates(updates)
    return UpdateCompressor.concatUpdatesWithSameVersion(updates)
  },

  compressUpdates(updates) {
    if (updates.length === 0) {
      return []
    }

    let compressedUpdates = [updates.shift()]
    for (const update of Array.from(updates)) {
      const lastCompressedUpdate = compressedUpdates.pop()
      if (lastCompressedUpdate != null) {
        compressedUpdates = compressedUpdates.concat(
          UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update)
        )
      } else {
        compressedUpdates.push(update)
      }
    }

    return compressedUpdates
  },

  MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000),
  MAX_UPDATE_SIZE: (twoMegabytes = 2 * 1024 * 1024),

  _concatTwoUpdates(firstUpdate, secondUpdate) {
    let offset
    firstUpdate = {
      op: firstUpdate.op,
      meta: {
        user_id: firstUpdate.meta.user_id || null,
        start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts,
        end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts
      },
      v: firstUpdate.v
    }
    secondUpdate = {
      op: secondUpdate.op,
      meta: {
        user_id: secondUpdate.meta.user_id || null,
        start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts,
        end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts
      },
      v: secondUpdate.v
    }

    if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) {
      return [firstUpdate, secondUpdate]
    }

    if (
      secondUpdate.meta.start_ts - firstUpdate.meta.end_ts >
      UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
    ) {
      return [firstUpdate, secondUpdate]
    }

    const firstOp = firstUpdate.op
    const secondOp = secondUpdate.op

    const firstSize =
      (firstOp.i != null ? firstOp.i.length : undefined) ||
      (firstOp.d != null ? firstOp.d.length : undefined)
    const secondSize =
      (secondOp.i != null ? secondOp.i.length : undefined) ||
      (secondOp.d != null ? secondOp.d.length : undefined)

    // Two inserts
    if (
      firstOp.i != null &&
      secondOp.i != null &&
      firstOp.p <= secondOp.p &&
      secondOp.p <= firstOp.p + firstOp.i.length &&
      firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
    ) {
      return [
        {
          meta: {
            start_ts: firstUpdate.meta.start_ts,
            end_ts: secondUpdate.meta.end_ts,
            user_id: firstUpdate.meta.user_id
          },
          op: {
            p: firstOp.p,
            i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
          },
          v: secondUpdate.v
        }
      ]
      // Two deletes
    } else if (
      firstOp.d != null &&
      secondOp.d != null &&
      secondOp.p <= firstOp.p &&
      firstOp.p <= secondOp.p + secondOp.d.length &&
      firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
    ) {
      return [
        {
          meta: {
            start_ts: firstUpdate.meta.start_ts,
            end_ts: secondUpdate.meta.end_ts,
            user_id: firstUpdate.meta.user_id
          },
          op: {
            p: secondOp.p,
            d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
          },
          v: secondUpdate.v
        }
      ]
      // An insert and then a delete
    } else if (
      firstOp.i != null &&
      secondOp.d != null &&
      firstOp.p <= secondOp.p &&
      secondOp.p <= firstOp.p + firstOp.i.length
    ) {
      offset = secondOp.p - firstOp.p
      const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
      // Only trim the insert when the delete is fully contained within in it
      if (insertedText === secondOp.d) {
        const insert = strRemove(firstOp.i, offset, secondOp.d.length)
        return [
          {
            meta: {
              start_ts: firstUpdate.meta.start_ts,
              end_ts: secondUpdate.meta.end_ts,
              user_id: firstUpdate.meta.user_id
            },
            op: {
              p: firstOp.p,
              i: insert
            },
            v: secondUpdate.v
          }
        ]
      } else {
        // This will only happen if the delete extends outside the insert
        return [firstUpdate, secondUpdate]
      }

      // A delete then an insert at the same place, likely a copy-paste of a chunk of content
    } else if (
      firstOp.d != null &&
      secondOp.i != null &&
      firstOp.p === secondOp.p
    ) {
      offset = firstOp.p
      const diff_ops = this.diffAsShareJsOps(firstOp.d, secondOp.i)
      if (diff_ops.length === 0) {
        return [
          {
            // Noop
            meta: {
              start_ts: firstUpdate.meta.start_ts,
              end_ts: secondUpdate.meta.end_ts,
              user_id: firstUpdate.meta.user_id
            },
            op: {
              p: firstOp.p,
              i: ''
            },
            v: secondUpdate.v
          }
        ]
      } else {
        return diff_ops.map(function (op) {
          op.p += offset
          return {
            meta: {
              start_ts: firstUpdate.meta.start_ts,
              end_ts: secondUpdate.meta.end_ts,
              user_id: firstUpdate.meta.user_id
            },
            op,
            v: secondUpdate.v
          }
        })
      }
    } else {
      return [firstUpdate, secondUpdate]
    }
  },

  ADDED: 1,
  REMOVED: -1,
  UNCHANGED: 0,
  diffAsShareJsOps(before, after, callback) {
    if (callback == null) {
      callback = function (error, ops) {}
    }
    const diffs = dmp.diff_main(before, after)
    dmp.diff_cleanupSemantic(diffs)

    const ops = []
    let position = 0
    for (const diff of Array.from(diffs)) {
      const type = diff[0]
      const content = diff[1]
      if (type === this.ADDED) {
        ops.push({
          i: content,
          p: position
        })
        position += content.length
      } else if (type === this.REMOVED) {
        ops.push({
          d: content,
          p: position
        })
      } else if (type === this.UNCHANGED) {
        position += content.length
      } else {
        throw 'Unknown type'
      }
    }
    return ops
  }
}

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