/* eslint-disable
    no-return-assign,
*/
// 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
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import Core from 'overleaf-editor-core'
import logger from '@overleaf/logger'
import * as Errors from './Errors.js'

const { MoveFileOperation, AddFileOperation, EditFileOperation } = Core

export function buildDiff(chunk, fromVersion, toVersion) {
  chunk = Core.Chunk.fromRaw(chunk.chunk)
  const chunkStartVersion = chunk.getStartVersion()

  const diff = _getInitialDiffSnapshot(chunk, fromVersion)

  const changes = chunk
    .getChanges()
    .slice(fromVersion - chunkStartVersion, toVersion - chunkStartVersion)
  for (let i = 0; i < changes.length; i++) {
    const change = changes[i]
    for (const operation of Array.from(change.getOperations())) {
      if (operation.pathname === null || operation.pathname === '') {
        // skip operations for missing files
        logger.warn({ diff, operation }, 'invalid pathname in operation')
      } else if (operation instanceof EditFileOperation) {
        _applyEditFileToDiff(diff, operation)
      } else if (operation instanceof AddFileOperation) {
        _applyAddFileToDiff(diff, operation)
      } else if (operation instanceof MoveFileOperation) {
        if (operation.isRemoveFile()) {
          const deletedAtV = fromVersion + i
          _applyDeleteFileToDiff(diff, operation, deletedAtV)
        } else {
          _applyMoveFileToDiff(diff, operation)
        }
      }
    }
  }

  return Object.values(diff)
}

function _getInitialDiffSnapshot(chunk, fromVersion) {
  // Start with a 'diff' which is snapshot of the filetree at the beginning,
  // with nothing in the diff marked as changed.
  // Use a bare object to protect against reserved names.
  const diff = Object.create(null)
  const files = _getInitialFiles(chunk, fromVersion)
  for (const [pathname, file] of Object.entries(files)) {
    diff[pathname] = { pathname, editable: file.isEditable() }
  }
  return diff
}

function _getInitialFiles(chunk, fromVersion) {
  const snapshot = chunk.getSnapshot()
  const changes = chunk
    .getChanges()
    .slice(0, fromVersion - chunk.getStartVersion())
  snapshot.applyAll(changes)
  return snapshot.fileMap.files
}

function _applyAddFileToDiff(diff, operation) {
  return (diff[operation.pathname] = {
    pathname: operation.pathname,
    operation: 'added',
    editable: operation.file.isEditable(),
  })
}

function _applyEditFileToDiff(diff, operation) {
  const change = diff[operation.pathname]
  if ((change != null ? change.operation : undefined) == null) {
    // avoid exception for non-existent change
    return (diff[operation.pathname] = {
      pathname: operation.pathname,
      operation: 'edited',
    })
  }
}

function _applyMoveFileToDiff(diff, operation) {
  if (
    diff[operation.newPathname] != null &&
    diff[operation.newPathname].operation !== 'removed'
  ) {
    const err = new Errors.InconsistentChunkError(
      'trying to move to file that already exists',
      { diff, operation }
    )
    throw err
  }
  const change = diff[operation.pathname]
  if (change == null) {
    logger.warn({ diff, operation }, 'tried to rename non-existent file')
    return
  }
  change.newPathname = operation.newPathname
  if (change.operation === 'added') {
    // If this file was added this time, just leave it as an add, but
    // at the new name.
    change.pathname = operation.newPathname
    delete change.newPathname
  } else {
    change.operation = 'renamed'
  }
  diff[operation.newPathname] = change
  return delete diff[operation.pathname]
}

function _applyDeleteFileToDiff(diff, operation, deletedAtV) {
  // avoid exception for non-existent change
  if (diff[operation.pathname] != null) {
    diff[operation.pathname].operation = 'removed'
  }
  return diff[operation.pathname] != null
    ? (diff[operation.pathname].deletedAtV = deletedAtV)
    : undefined
}
