/* eslint-disable
    camelcase,
    n/handle-callback-err,
    max-len,
    no-dupe-class-members,
    no-return-assign,
    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
 * DS103: Rewrite code to no longer use __guard__
 * DS202: Simplify dynamic range loops
 * 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
 */
import './directives/fileEntity'
import './controllers/FileTreeController'
import './controllers/FileTreeEntityController'
import './controllers/FileTreeFolderController'
import '../../features/file-tree/controllers/file-tree-controller'
let FileTreeManager

export default FileTreeManager = class FileTreeManager {
  constructor(ide, $scope) {
    this.ide = ide
    this.$scope = $scope
    this.$scope.$on('project:joined', () => {
      this.loadRootFolder()
      this.loadDeletedDocs()
      return this.$scope.$emit('file-tree:initialized')
    })

    this.$scope.$on('entities:multiSelected', (_event, data) => {
      this.$scope.$apply(() => {
        this.$scope.multiSelectedCount = data.count
        this.$scope.editor.multiSelectedCount = data.count
      })
    })

    this.$scope.$watch('rootFolder', rootFolder => {
      if (rootFolder != null) {
        return this.recalculateDocList()
      }
    })

    this._bindToSocketEvents()

    this.$scope.multiSelectedCount = 0

    $(document).on('click', () => {
      this.clearMultiSelectedEntities()
      setTimeout(() => this.$scope.$digest(), 0)
    })
  }

  _bindToSocketEvents() {
    this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
      const parent_folder =
        this.findEntityById(parent_folder_id) || this.$scope.rootFolder
      return this.$scope.$apply(() => {
        parent_folder.children.push({
          name: doc.name,
          id: doc._id,
          type: 'doc',
        })
        return this.recalculateDocList()
      })
    })

    this.ide.socket.on(
      'reciveNewFile',
      (parent_folder_id, file, source, linkedFileData) => {
        const parent_folder =
          this.findEntityById(parent_folder_id) || this.$scope.rootFolder
        return this.$scope.$apply(() => {
          parent_folder.children.push({
            name: file.name,
            id: file._id,
            type: 'file',
            linkedFileData,
            created: file.created,
          })
          return this.recalculateDocList()
        })
      }
    )

    this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
      const parent_folder =
        this.findEntityById(parent_folder_id) || this.$scope.rootFolder
      return this.$scope.$apply(() => {
        parent_folder.children.push({
          name: folder.name,
          id: folder._id,
          type: 'folder',
          children: [],
        })
        return this.recalculateDocList()
      })
    })

    this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
      const entity = this.findEntityById(entity_id)
      if (entity == null) {
        return
      }
      return this.$scope.$apply(() => {
        entity.name = name
        return this.recalculateDocList()
      })
    })

    this.ide.socket.on('removeEntity', entity_id => {
      const entity = this.findEntityById(entity_id)
      if (entity == null) {
        return
      }
      this.$scope.$apply(() => {
        this._deleteEntityFromScope(entity)
        return this.recalculateDocList()
      })
      return this.$scope.$broadcast('entity:deleted', entity)
    })

    return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
      const entity = this.findEntityById(entity_id)
      const folder = this.findEntityById(folder_id)
      return this.$scope.$apply(() => {
        this._moveEntityInScope(entity, folder)
        return this.recalculateDocList()
      })
    })
  }

  selectEntity(entity) {
    this.selected_entity_id = entity.id // For reselecting after a reconnect
    this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
    return (entity.selected = true)
  }

  toggleMultiSelectEntity(entity) {
    entity.multiSelected = !entity.multiSelected
    this.$scope.multiSelectedCount = this.multiSelectedCount()
    this.$scope.editor.multiSelectedCount = this.$scope.multiSelectedCount
  }

  multiSelectedCount() {
    let count = 0
    this.forEachEntity(function (entity) {
      if (entity.multiSelected) {
        return count++
      }
    })
    return count
  }

  getMultiSelectedEntities() {
    const entities = []
    this.forEachEntity(function (e) {
      if (e.multiSelected) {
        return entities.push(e)
      }
    })
    return entities
  }

  getFullCount() {
    const entities = []
    this.forEachEntity(function (e) {
      if (!e.deleted) entities.push(e)
    })
    return entities.length
  }

  getMultiSelectedEntityChildNodes() {
    // use pathnames with a leading slash to avoid
    // problems with reserved Object properties
    const entities = this.getMultiSelectedEntities()
    const paths = {}
    for (const entity of Array.from(entities)) {
      paths['/' + this.getEntityPath(entity)] = entity
    }
    const prefixes = {}
    for (const path in paths) {
      const entity = paths[path]
      const parts = path.split('/')
      if (parts.length <= 2) {
        continue
      } else {
        // Record prefixes a/b/c.tex -> 'a' and 'a/b'
        for (
          let i = 1, end = parts.length - 1, asc = end >= 1;
          asc ? i <= end : i >= end;
          asc ? i++ : i--
        ) {
          prefixes['/' + parts.slice(0, i).join('/')] = true
        }
      }
    }
    const child_entities = []
    for (const path in paths) {
      // If the path is in the prefixes, then it's a parent folder and
      // should be ignore
      const entity = paths[path]
      if (prefixes[path] == null) {
        child_entities.push(entity)
      }
    }
    return child_entities
  }

  clearMultiSelectedEntities() {
    if (this.$scope.multiSelectedCount === 0) {
      return
    } // Be efficient, this is called a lot on 'click'
    this.forEachEntity(entity => (entity.multiSelected = false))
    return (this.$scope.multiSelectedCount = 0)
  }

  multiSelectSelectedEntity() {
    const entity = this.findSelectedEntity()
    if (entity) {
      entity.multiSelected = true
    }
    this.$scope.multiSelectedCount = this.multiSelectedCount()
  }

  existsInFolder(folder_id, name) {
    const folder = this.findEntityById(folder_id)
    if (folder == null) {
      return false
    }
    const entity = this._findEntityByPathInFolder(folder, name)
    return entity != null
  }

  findSelectedEntity() {
    let selected = null
    this.forEachEntity(function (entity) {
      if (entity.selected) {
        return (selected = entity)
      }
    })
    return selected
  }

  findEntityById(id, options) {
    if (options == null) {
      options = {}
    }
    if (this.$scope.rootFolder.id === id) {
      return this.$scope.rootFolder
    }

    let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
    if (entity != null) {
      return entity
    }

    if (options.includeDeleted) {
      for (entity of Array.from(this.$scope.deletedDocs)) {
        if (entity.id === id) {
          return entity
        }
      }
    }

    return null
  }

  _findEntityByIdInFolder(folder, id) {
    for (const entity of Array.from(folder.children || [])) {
      if (entity.id === id) {
        return entity
      } else if (entity.children != null) {
        const result = this._findEntityByIdInFolder(entity, id)
        if (result != null) {
          return result
        }
      }
    }

    return null
  }

  findEntityByPath(path) {
    return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
  }

  getPreviewByPath(path) {
    for (const suffix of [
      '',
      '.png',
      '.jpg',
      '.jpeg',
      '.pdf',
      '.PNG',
      '.JPG',
      '.JPEG',
      '.PDF',
    ]) {
      const entity = this.findEntityByPath(path + suffix)

      if (entity) {
        return {
          url: `/project/${this.$scope.project._id}/file/${entity.id}`,
          extension: entity.name.split('.').pop(),
        }
      }
    }
    return null
  }

  _findEntityByPathInFolder(folder, path) {
    if (path == null || folder == null) {
      return null
    }
    if (path === '') {
      return folder
    }

    const parts = path.split('/')
    const name = parts.shift()
    const rest = parts.join('/')

    if (name === '.') {
      return this._findEntityByPathInFolder(folder, rest)
    }

    for (const entity of Array.from(folder.children)) {
      if (entity.name === name) {
        if (rest === '') {
          return entity
        } else if (entity.type === 'folder') {
          return this._findEntityByPathInFolder(entity, rest)
        }
      }
    }
    return null
  }

  forEachEntity(callback) {
    if (callback == null) {
      callback = function () {}
    }
    this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)

    return (() => {
      const result = []
      for (const entity of Array.from(this.$scope.deletedDocs || [])) {
        result.push(callback(entity))
      }
      return result
    })()
  }

  _forEachEntityInFolder(folder, path, callback) {
    return (() => {
      const result = []
      for (const entity of Array.from(folder.children || [])) {
        let childPath
        if (path != null) {
          childPath = path + '/' + entity.name
        } else {
          childPath = entity.name
        }
        callback(entity, folder, childPath)
        if (entity.children != null) {
          result.push(this._forEachEntityInFolder(entity, childPath, callback))
        } else {
          result.push(undefined)
        }
      }
      return result
    })()
  }

  getEntityPath(entity) {
    return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
  }

  _getEntityPathInFolder(folder, entity) {
    for (const child of Array.from(folder.children || [])) {
      if (child === entity) {
        return entity.name
      } else if (child.type === 'folder') {
        const path = this._getEntityPathInFolder(child, entity)
        if (path != null) {
          return child.name + '/' + path
        }
      }
    }
    return null
  }

  getRootDocDirname() {
    const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
    if (rootDoc == null) {
      return
    }
    return this._getEntityDirname(rootDoc)
  }

  _getEntityDirname(entity) {
    const path = this.getEntityPath(entity)
    if (path == null) {
      return
    }
    return path.split('/').slice(0, -1).join('/')
  }

  _findParentFolder(entity) {
    const dirname = this._getEntityDirname(entity)
    if (dirname == null) {
      return
    }
    return this.findEntityByPath(dirname)
  }

  loadRootFolder() {
    return (this.$scope.rootFolder = this._parseFolder(
      __guard__(
        this.$scope != null ? this.$scope.project : undefined,
        x => x.rootFolder[0]
      )
    ))
  }

  _parseFolder(rawFolder) {
    const folder = {
      name: rawFolder.name,
      id: rawFolder._id,
      type: 'folder',
      children: [],
      selected: rawFolder._id === this.selected_entity_id,
    }

    for (const doc of Array.from(rawFolder.docs || [])) {
      folder.children.push({
        name: doc.name,
        id: doc._id,
        type: 'doc',
        selected: doc._id === this.selected_entity_id,
      })
    }

    for (const file of Array.from(rawFolder.fileRefs || [])) {
      folder.children.push({
        name: file.name,
        id: file._id,
        type: 'file',
        selected: file._id === this.selected_entity_id,
        linkedFileData: file.linkedFileData,
        created: file.created,
      })
    }

    for (const childFolder of Array.from(rawFolder.folders || [])) {
      folder.children.push(this._parseFolder(childFolder))
    }

    return folder
  }

  loadDeletedDocs() {
    this.$scope.deletedDocs = []
    return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
      this.$scope.deletedDocs.push({
        name: doc.name,
        id: doc._id,
        type: 'doc',
        deleted: true,
      })
    )
  }

  recalculateDocList() {
    this.$scope.docs = []
    this.forEachEntity((entity, parentFolder, path) => {
      if (entity.type === 'doc' && !entity.deleted) {
        return this.$scope.docs.push({
          doc: entity,
          path,
        })
      }
    })
    // Keep list ordered by folders, then name
    return this.$scope.docs.sort(function (a, b) {
      const aDepth = (a.path.match(/\//g) || []).length
      const bDepth = (b.path.match(/\//g) || []).length
      if (aDepth - bDepth !== 0) {
        return -(aDepth - bDepth) // Deeper path == folder first
      } else if (a.path < b.path) {
        return -1
      } else if (a.path > b.path) {
        return 1
      } else {
        return 0
      }
    })
  }

  getCurrentFolder() {
    // Return the root folder if nothing is selected
    return (
      this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
    )
  }

  _getCurrentFolder(startFolder) {
    if (startFolder == null) {
      startFolder = this.$scope.rootFolder
    }
    for (const entity of Array.from(startFolder.children || [])) {
      // The 'current' folder is either the one selected, or
      // the one containing the selected doc/file
      if (entity.selected) {
        if (entity.type === 'folder') {
          return entity
        } else {
          return startFolder
        }
      }

      if (entity.type === 'folder') {
        const result = this._getCurrentFolder(entity)
        if (result != null) {
          return result
        }
      }
    }

    return null
  }

  projectContainsFolder() {
    for (const entity of Array.from(this.$scope.rootFolder.children)) {
      if (entity.type === 'folder') {
        return true
      }
    }
    return false
  }

  existsInThisFolder(folder, name) {
    for (const entity of Array.from(
      (folder != null ? folder.children : undefined) || []
    )) {
      if (entity.name === name) {
        return true
      }
    }
    return false
  }

  nameExistsError(message) {
    if (message == null) {
      message = 'already exists'
    }
    const nameExists = this.ide.$q.defer()
    nameExists.reject({ data: message })
    return nameExists.promise
  }

  createDoc(name, parent_folder) {
    // check if a doc/file/folder already exists with this name
    if (parent_folder == null) {
      parent_folder = this.getCurrentFolder()
    }
    if (this.existsInThisFolder(parent_folder, name)) {
      return this.nameExistsError()
    }
    // We'll wait for the socket.io notification to actually
    // add the doc for us.
    return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
      name,
      parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
      _csrf: window.csrfToken,
    })
  }

  createFolder(name, parent_folder) {
    // check if a doc/file/folder already exists with this name
    if (parent_folder == null) {
      parent_folder = this.getCurrentFolder()
    }
    if (this.existsInThisFolder(parent_folder, name)) {
      return this.nameExistsError()
    }
    // We'll wait for the socket.io notification to actually
    // add the folder for us.
    return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
      name,
      parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
      _csrf: window.csrfToken,
    })
  }

  createLinkedFile(name, parent_folder, provider, data) {
    // check if a doc/file/folder already exists with this name
    if (parent_folder == null) {
      parent_folder = this.getCurrentFolder()
    }
    if (this.existsInThisFolder(parent_folder, name)) {
      return this.nameExistsError()
    }
    // We'll wait for the socket.io notification to actually
    // add the file for us.
    return this.ide.$http.post(
      `/project/${this.ide.project_id}/linked_file`,
      {
        name,
        parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
        provider,
        data,
        _csrf: window.csrfToken,
      },
      {
        disableAutoLoginRedirect: true,
      }
    )
  }

  refreshLinkedFile(file) {
    const parent_folder = this._findParentFolder(file)
    const provider =
      file.linkedFileData != null ? file.linkedFileData.provider : undefined
    if (provider == null) {
      console.warn(`>> no provider for ${file.name}`, file)
      return
    }
    return this.ide.$http.post(
      `/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
      {
        _csrf: window.csrfToken,
      },
      {
        disableAutoLoginRedirect: true,
      }
    )
  }

  renameEntity(entity, name, callback) {
    if (callback == null) {
      callback = function () {}
    }
    if (entity.name === name) {
      return
    }
    if (name.length >= 150) {
      return
    }
    // check if a doc/file/folder already exists with this name
    const parent_folder = this.getCurrentFolder()
    if (this.existsInThisFolder(parent_folder, name)) {
      return this.nameExistsError()
    }
    entity.renamingToName = name
    return this.ide.$http
      .post(
        `/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
        {
          name,
          _csrf: window.csrfToken,
        }
      )
      .then(() => (entity.name = name))
      .finally(() => (entity.renamingToName = null))
  }

  deleteEntity(entity, callback) {
    // We'll wait for the socket.io notification to
    // delete from scope.
    if (callback == null) {
      callback = function () {}
    }
    return this.ide.queuedHttp({
      method: 'DELETE',
      url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
      headers: {
        'X-Csrf-Token': window.csrfToken,
      },
    })
  }

  moveEntity(entity, parent_folder) {
    // Abort move if the folder being moved (entity) has the parent_folder as child
    // since that would break the tree structure.
    if (this._isChildFolder(entity, parent_folder)) {
      return
    }
    // check if a doc/file/folder already exists with this name
    if (this.existsInThisFolder(parent_folder, entity.name)) {
      throw new Error('file exists in this location')
    }
    // Wait for the http response before doing the move
    this.ide.queuedHttp
      .post(
        `/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
        {
          folder_id: parent_folder.id,
          _csrf: window.csrfToken,
        }
      )
      .then(() => {
        this._moveEntityInScope(entity, parent_folder)
      })
  }

  _isChildFolder(parent_folder, child_folder) {
    const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
    const child_path = this.getEntityPath(child_folder) || '' // null if root folder
    // is parent path the beginning of child path?
    return child_path.slice(0, parent_path.length) === parent_path
  }

  _deleteEntityFromScope(entity, options) {
    if (options == null) {
      options = { moveToDeleted: true }
    }
    if (entity == null) {
      return
    }
    let parent_folder = null
    this.forEachEntity(function (possible_entity, folder) {
      if (possible_entity === entity) {
        return (parent_folder = folder)
      }
    })

    if (parent_folder != null) {
      const index = parent_folder.children.indexOf(entity)
      if (index > -1) {
        parent_folder.children.splice(index, 1)
      }
    }

    if (entity.type !== 'folder' && entity.selected) {
      this.$scope.ui.view = null
    }

    if (entity.type === 'doc' && options.moveToDeleted) {
      entity.deleted = true
      return this.$scope.deletedDocs.push(entity)
    }
  }

  _moveEntityInScope(entity, parent_folder) {
    if (Array.from(parent_folder.children).includes(entity)) {
      return
    }
    this._deleteEntityFromScope(entity, { moveToDeleted: false })
    return parent_folder.children.push(entity)
  }
}

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