/* eslint-disable
    camelcase,
    node/handle-callback-err,
    max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * 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
 */
let UpdateMerger
const OError = require('@overleaf/o-error')
const _ = require('underscore')
const async = require('async')
const fs = require('fs')
const logger = require('logger-sharelatex')
const EditorController = require('../Editor/EditorController')
const FileTypeManager = require('../Uploads/FileTypeManager')
const FileWriter = require('../../infrastructure/FileWriter')
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')

module.exports = UpdateMerger = {
  mergeUpdate(user_id, project_id, path, updateRequest, source, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    return FileWriter.writeStreamToDisk(
      project_id,
      updateRequest,
      function (err, fsPath) {
        if (err != null) {
          return callback(err)
        }
        return UpdateMerger._mergeUpdate(
          user_id,
          project_id,
          path,
          fsPath,
          source,
          mergeErr =>
            fs.unlink(fsPath, function (deleteErr) {
              if (deleteErr != null) {
                logger.err({ project_id, fsPath }, 'error deleting file')
              }
              return callback(mergeErr)
            })
        )
      }
    )
  },

  _findExistingFileType(project_id, path, callback) {
    ProjectEntityHandler.getAllEntities(
      project_id,
      function (err, docs, files) {
        if (err != null) {
          return callback(err)
        }
        var existingFileType = null
        if (_.some(files, f => f.path === path)) {
          existingFileType = 'file'
        }
        if (_.some(docs, d => d.path === path)) {
          existingFileType = 'doc'
        }
        callback(null, existingFileType)
      }
    )
  },

  _determineFileType(project_id, path, fsPath, callback) {
    if (callback == null) {
      callback = function (err, fileType) {}
    }
    // check if there is an existing file with the same path (we either need
    // to overwrite it or delete it)
    UpdateMerger._findExistingFileType(
      project_id,
      path,
      function (err, existingFileType) {
        if (err) {
          return callback(err)
        }
        // determine whether the update should create a doc or binary file
        FileTypeManager.getType(
          path,
          fsPath,
          function (err, { binary, encoding }) {
            if (err != null) {
              return callback(err)
            }

            // If we receive a non-utf8 encoding, we won't be able to keep things in
            // sync, so we'll treat non-utf8 files as binary
            const isBinary = binary || encoding !== 'utf-8'

            // Existing | Update    | Action
            // ---------|-----------|-------
            // file     | isBinary  | existing-file
            // file     | !isBinary | existing-file
            // doc      | isBinary  | new-file, delete-existing-doc
            // doc      | !isBinary | existing-doc
            // null     | isBinary  | new-file
            // null     | !isBinary | new-doc

            // if a binary file already exists, always keep it as a binary file
            // even if the update looks like a text file
            if (existingFileType === 'file') {
              return callback(null, 'existing-file')
            }

            // if there is an existing doc, keep it as a doc except when the
            // incoming update is binary. In that case delete the doc and replace
            // it with a new file.
            if (existingFileType === 'doc') {
              if (isBinary) {
                return callback(null, 'new-file', 'delete-existing-doc')
              } else {
                return callback(null, 'existing-doc')
              }
            }
            // if there no existing file, create a file or doc as needed
            return callback(null, isBinary ? 'new-file' : 'new-doc')
          }
        )
      }
    )
  },

  _mergeUpdate(user_id, project_id, path, fsPath, source, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    return UpdateMerger._determineFileType(
      project_id,
      path,
      fsPath,
      function (err, fileType, deleteOriginalEntity) {
        if (err != null) {
          return callback(err)
        }
        async.series(
          [
            function (cb) {
              if (deleteOriginalEntity) {
                // currently we only delete docs
                UpdateMerger.deleteUpdate(user_id, project_id, path, source, cb)
              } else {
                cb()
              }
            },
            function (cb) {
              if (['existing-file', 'new-file'].includes(fileType)) {
                return UpdateMerger.p.processFile(
                  project_id,
                  fsPath,
                  path,
                  source,
                  user_id,
                  cb
                )
              } else if (['existing-doc', 'new-doc'].includes(fileType)) {
                return UpdateMerger.p.processDoc(
                  project_id,
                  user_id,
                  fsPath,
                  path,
                  source,
                  cb
                )
              } else {
                return cb(new Error('unrecognized file'))
              }
            }
          ],
          callback
        )
      }
    )
  },

  deleteUpdate(user_id, project_id, path, source, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return EditorController.deleteEntityWithPath(
      project_id,
      path,
      source,
      user_id,
      function () {
        return callback()
      }
    )
  },

  p: {
    processDoc(project_id, user_id, fsPath, path, source, callback) {
      return UpdateMerger.p.readFileIntoTextArray(
        fsPath,
        function (err, docLines) {
          if (err != null) {
            OError.tag(
              err,
              'error reading file into text array for process doc update',
              {
                project_id
              }
            )
            return callback(err)
          }
          logger.log({ docLines }, 'processing doc update from tpds')
          return EditorController.upsertDocWithPath(
            project_id,
            path,
            docLines,
            source,
            user_id,
            function (err) {
              return callback(err)
            }
          )
        }
      )
    },

    processFile(project_id, fsPath, path, source, user_id, callback) {
      return EditorController.upsertFileWithPath(
        project_id,
        path,
        fsPath,
        null,
        source,
        user_id,
        function (err) {
          return callback(err)
        }
      )
    },

    readFileIntoTextArray(path, callback) {
      return fs.readFile(path, 'utf8', function (error, content) {
        if (content == null) {
          content = ''
        }
        if (error != null) {
          OError.tag(error, 'error reading file into text array', {
            path
          })
          return callback(error)
        }
        const lines = content.split(/\r\n|\n|\r/)
        return callback(error, lines)
      })
    }
  }
}
