import _ from 'lodash'
import Core from 'overleaf-editor-core'
import * as Errors from './Errors.js'
import * as OperationsCompressor from './OperationsCompressor.js'

export function convertToChanges(projectId, updatesWithBlobs, callback) {
  let changes
  try {
    // convert update to change
    changes = updatesWithBlobs.map(update =>
      _convertToChange(projectId, update)
    )
  } catch (error1) {
    const error = error1
    if (
      error instanceof Errors.UpdateWithUnknownFormatError ||
      error instanceof Errors.UnexpectedOpTypeError
    ) {
      return callback(error)
    } else {
      throw error
    }
  }

  callback(null, changes)
}

function _convertToChange(projectId, updateWithBlob) {
  let operations
  const { update } = updateWithBlob

  let projectVersion = null
  const v2DocVersions = {}

  if (_isRenameUpdate(update)) {
    operations = [
      {
        pathname: _convertPathname(update.pathname),
        newPathname: _convertPathname(update.new_pathname),
      },
    ]
    projectVersion = update.version
  } else if (isAddUpdate(update)) {
    operations = [
      {
        pathname: _convertPathname(update.pathname),
        file: {
          hash: updateWithBlob.blobHash,
        },
      },
    ]
    projectVersion = update.version
  } else if (isTextUpdate(update)) {
    const docLength = update.meta.doc_length
    let pathname = update.meta.pathname

    pathname = _convertPathname(pathname)
    const builder = new TextOperationsBuilder(update.doc, docLength, pathname)
    // convert ops
    for (const op of update.op) {
      builder.addOp(op)
    } // if this throws an exception it will be caught in convertToChanges
    operations = builder.finish()
    // add doc version information if present
    if (update.v != null) {
      v2DocVersions[update.doc] = { pathname, v: update.v }
    }
  } else {
    const error = new Errors.UpdateWithUnknownFormatError(
      'update with unknown format',
      { projectId, update }
    )
    throw error
  }

  let v2Authors
  if (update.meta.user_id === 'anonymous-user') {
    // history-v1 uses null to represent an anonymous author
    v2Authors = [null]
  } else {
    // user_id is missing on resync operations that update the contents of a doc
    v2Authors = _.compact([update.meta.user_id])
  }

  const rawChange = {
    operations,
    v2Authors,
    timestamp: new Date(update.meta.ts).toISOString(),
    projectVersion,
    v2DocVersions: Object.keys(v2DocVersions).length ? v2DocVersions : null,
  }
  if (update.meta.origin) {
    rawChange.origin = update.meta.origin
  } else if (update.meta.type === 'external' && update.meta.source) {
    rawChange.origin = { kind: update.meta.source }
  }
  const change = Core.Change.fromRaw(rawChange)

  change.operations = OperationsCompressor.compressOperations(change.operations)

  return change
}

function _isRenameUpdate(update) {
  return update.new_pathname != null
}

function _isAddDocUpdate(update) {
  return update.doc != null && update.docLines != null
}

function _isAddFileUpdate(update) {
  return update.file != null && update.url != null
}

export function isTextUpdate(update) {
  return (
    update.doc != null &&
    update.op != null &&
    update.meta.pathname != null &&
    update.meta.doc_length != null
  )
}

export function isProjectStructureUpdate(update) {
  return isAddUpdate(update) || _isRenameUpdate(update)
}

export function isAddUpdate(update) {
  return _isAddDocUpdate(update) || _isAddFileUpdate(update)
}

export function _convertPathname(pathname) {
  // Strip leading /
  pathname = pathname.replace(/^\//, '')
  // Replace \\ with _. Backslashes are no longer allowed
  // in projects in web, but we have some which have gone through
  // into history before this restriction was added. This makes
  // them valid for the history store.
  // See https://github.com/overleaf/write_latex/issues/4471
  pathname = pathname.replace(/\\/g, '_')
  // workaround for filenames containing asterisks, this will
  // fail if a corresponding replacement file already exists but it
  // would fail anyway without this attempt to fix the pathname.
  // See https://github.com/overleaf/sharelatex/issues/900
  pathname = pathname.replace(/\*/g, '__ASTERISK__')
  // workaround for filenames beginning with spaces
  // See https://github.com/overleaf/sharelatex/issues/1404
  // note: we have already stripped any leading slash above
  pathname = pathname.replace(/^ /, '__SPACE__') // handle top-level
  pathname = pathname.replace(/\/ /g, '/__SPACE__') // handle folders
  return pathname
}

class TextOperationsBuilder {
  constructor(docId, docLength, pathname) {
    this.operations = []
    this.doc_id = docId
    this.doc_length = docLength
    this.pathname = pathname
  }

  addOp(op) {
    let retain
    if (op.c != null) {
      return // ignore comment op
    }
    if (op.i == null && op.d == null) {
      throw new Errors.UnexpectedOpTypeError('unexpected op type', { op })
    }

    // We sometimes receive operations that operate at positions outside the
    // doc_length. Document updater coerces the position to the end of the
    // document. We do the same here.
    const pos = Math.min(op.p, this.doc_length)

    const textOperation = []
    if (pos > 0) {
      textOperation.push(pos)
    }

    if (op.i != null) {
      textOperation.push(op.i)
      retain = this.doc_length - pos
      this.doc_length += op.i.length
    }

    if (op.d != null) {
      textOperation.push(-op.d.length)
      retain = this.doc_length - pos - op.d.length
      this.doc_length -= op.d.length
    }

    if (retain > 0) {
      textOperation.push(retain)
    }
    this.pushTextOperation(textOperation)
  }

  pushTextOperation(textOperation) {
    this.operations.push({
      pathname: this.pathname,
      textOperation,
    })
  }

  finish() {
    return this.operations
  }
}
