import React, { createContext, useReducer, useContext } from 'react'
import PropTypes from 'prop-types'

import { mapSeries } from '../../../infrastructure/promise'

import {
  syncRename,
  syncDelete,
  syncMove,
  syncCreateEntity
} from '../util/sync-mutation'
import { findInTreeOrThrow } from '../util/find-in-tree'

import { FileTreeMainContext } from './file-tree-main'
import { useFileTreeMutable } from './file-tree-mutable'
import { useFileTreeSelectable } from './file-tree-selectable'

const FileTreeActionableContext = createContext()

const ACTION_TYPES = {
  START_RENAME: 'START_RENAME',
  START_DELETE: 'START_DELETE',
  DELETING: 'DELETING',
  START_CREATE_FOLDER: 'START_CREATE_FOLDER',
  CREATING_FOLDER: 'CREATING_FOLDER',
  CANCEL: 'CANCEL',
  CLEAR: 'CLEAR',
  ERROR: 'ERROR'
}

const defaultState = {
  isDeleting: false,
  isRenaming: false,
  isCreatingFolder: false,
  inFlight: false,
  actionedEntities: null,
  error: null
}

function fileTreeActionableReadOnlyReducer(state) {
  return state
}

function fileTreeActionableReducer(state, action) {
  switch (action.type) {
    case ACTION_TYPES.START_RENAME:
      return { ...defaultState, isRenaming: true }
    case ACTION_TYPES.START_DELETE:
      return {
        ...defaultState,
        isDeleting: true,
        actionedEntities: action.actionedEntities
      }
    case ACTION_TYPES.START_CREATE_FOLDER:
      return { ...defaultState, isCreatingFolder: true }
    case ACTION_TYPES.CREATING_FOLDER:
      return { ...defaultState, isCreatingFolder: true, inFlight: true }
    case ACTION_TYPES.DELETING:
      // keep `actionedEntities` so the entities list remains displayed in the
      // delete modal
      return {
        ...defaultState,
        isDeleting: true,
        inFlight: true,
        actionedEntities: state.actionedEntities
      }
    case ACTION_TYPES.CLEAR:
      return { ...defaultState }
    case ACTION_TYPES.CANCEL:
      if (state.inFlight) return state
      return { ...defaultState }
    case ACTION_TYPES.ERROR:
      return { ...state, inFlight: false, error: action.error }
    default:
      throw new Error(`Unknown user action type: ${action.type}`)
  }
}

export function FileTreeActionableProvider({ hasWritePermissions, children }) {
  const [state, dispatch] = useReducer(
    hasWritePermissions
      ? fileTreeActionableReducer
      : fileTreeActionableReadOnlyReducer,
    defaultState
  )

  return (
    <FileTreeActionableContext.Provider value={{ ...state, dispatch }}>
      {children}
    </FileTreeActionableContext.Provider>
  )
}

FileTreeActionableProvider.propTypes = {
  hasWritePermissions: PropTypes.bool.isRequired,
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired
}

export function useFileTreeActionable() {
  const {
    isDeleting,
    isRenaming,
    isCreatingFolder,
    inFlight,
    error,
    actionedEntities,
    dispatch
  } = useContext(FileTreeActionableContext)
  const { projectId } = useContext(FileTreeMainContext)
  const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable()
  const { selectedEntityIds } = useFileTreeSelectable()

  function startRenaming() {
    dispatch({ type: ACTION_TYPES.START_RENAME })
  }

  // update the entity with the new name immediately in the tree, but revert to
  // the old name if the sync fails
  function finishRenaming(newName) {
    dispatch({ type: ACTION_TYPES.CLEAR })
    const selectedEntityId = Array.from(selectedEntityIds)[0]
    const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
    const oldName = found.entity.name
    dispatchRename(selectedEntityId, newName)
    return syncRename(projectId, found.type, found.entity._id, newName).catch(
      error => {
        dispatch({ type: ACTION_TYPES.ERROR, error })
        dispatchRename(selectedEntityId, oldName)
      }
    )
  }

  // init deletion flow (this will open the delete modal).
  // A copy of the selected entities is set as `actionedEntities` so it is kept
  // unchanged as the entities are deleted and the selection is updated
  function startDeleting() {
    const actionedEntities = Array.from(selectedEntityIds).map(
      entityId => findInTreeOrThrow(fileTreeData, entityId).entity
    )
    dispatch({ type: ACTION_TYPES.START_DELETE, actionedEntities })
  }

  // deletes entities in serie. Tree will be updated via the socket event
  function finishDeleting() {
    dispatch({ type: ACTION_TYPES.DELETING })

    return mapSeries(Array.from(selectedEntityIds), id => {
      const found = findInTreeOrThrow(fileTreeData, id)
      return syncDelete(projectId, found.type, found.entity._id).catch(
        error => {
          // throw unless 404
          if (error.message !== '404') {
            throw error
          }
        }
      )
    })
      .then(() => {
        dispatch({ type: ACTION_TYPES.CLEAR })
      })
      .catch(error => {
        // set an error and allow user to retry
        dispatch({ type: ACTION_TYPES.ERROR, error })
      })
  }

  // moves entities. Tree is updated immediately and data are sync'd after.
  function finishMoving(toFolderId, draggedEntityIds) {
    draggedEntityIds.forEach(selectedEntityId => {
      dispatchMove(selectedEntityId, toFolderId)
    })

    return mapSeries(Array.from(draggedEntityIds), id => {
      const found = findInTreeOrThrow(fileTreeData, id)
      return syncMove(projectId, found.type, found.entity._id, toFolderId)
    })
  }

  function startCreatingFolder() {
    dispatch({ type: ACTION_TYPES.START_CREATE_FOLDER })
  }

  function finishCreatingEntity(entity) {
    const selectedEntityId = Array.from(selectedEntityIds)[0]
    const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
    const parentFolderId =
      found.type === 'folder' ? found.entity._id : found.parentFolderId

    return syncCreateEntity(projectId, parentFolderId, entity)
  }

  function finishCreatingFolder(name) {
    dispatch({ type: ACTION_TYPES.CREATING_FOLDER })
    return finishCreatingEntity({ endpoint: 'folder', name })
      .then(() => {
        dispatch({ type: ACTION_TYPES.CLEAR })
      })
      .catch(error => {
        dispatch({ type: ACTION_TYPES.ERROR, error })
      })
  }

  // bypass React file tree entirely; requesting the Angular new doc or file
  // modal instead
  function startCreatingDocOrFile() {
    const selectedEntityId = Array.from(selectedEntityIds)[0]
    const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
    const parentFolderId =
      found.type === 'folder' ? found.entity._id : found.parentFolderId
    window.dispatchEvent(
      new CustomEvent('FileTreeReactBridge.openNewDocModal', {
        detail: {
          mode: 'doc',
          parentFolderId
        }
      })
    )
  }

  function startUploadingDocOrFile() {
    const selectedEntityId = Array.from(selectedEntityIds)[0]
    const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
    const parentFolderId =
      found.type === 'folder' ? found.entity._id : found.parentFolderId
    window.dispatchEvent(
      new CustomEvent('FileTreeReactBridge.openNewDocModal', {
        detail: {
          mode: 'upload',
          parentFolderId
        }
      })
    )
  }

  function finishCreatingDocOrFile(entity) {
    return finishCreatingEntity(entity)
      .then(() => {
        // dispatch FileTreeReactBridge event to update the Angular modal
        window.dispatchEvent(
          new CustomEvent('FileTreeReactBridge.openNewFileModal', {
            detail: {
              done: true
            }
          })
        )
      })
      .catch(error => {
        // dispatch FileTreeReactBridge event to update the Angular modal with
        // an error
        window.dispatchEvent(
          new CustomEvent('FileTreeReactBridge.openNewFileModal', {
            detail: {
              error: true,
              data: error.message
            }
          })
        )
      })
  }

  function finishCreatingDoc(entity) {
    entity.endpoint = 'doc'
    return finishCreatingDocOrFile(entity)
  }

  function finishCreatingLinkedFile(entity) {
    entity.endpoint = 'linked_file'
    return finishCreatingDocOrFile(entity)
  }

  function cancel() {
    dispatch({ type: ACTION_TYPES.CANCEL })
  }

  return {
    canDelete: selectedEntityIds.size > 0,
    canRename: selectedEntityIds.size === 1,
    canCreate: selectedEntityIds.size === 1,
    isDeleting,
    isRenaming,
    isCreatingFolder,
    inFlight,
    actionedEntities,
    error,
    startRenaming,
    finishRenaming,
    startDeleting,
    finishDeleting,
    finishMoving,
    startCreatingFolder,
    finishCreatingFolder,
    startCreatingDocOrFile,
    startUploadingDocOrFile,
    finishCreatingDoc,
    finishCreatingLinkedFile,
    cancel
  }
}
