import {
  createContext,
  useCallback,
  useContext,
  useReducer,
  useEffect,
  useMemo,
  useState,
} from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import _ from 'lodash'

import { findInTree } from '../util/find-in-tree'
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
import { useProjectContext } from '../../../shared/context/project-context'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useLayoutContext } from '../../../shared/context/layout-context'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import usePreviousValue from '../../../shared/hooks/use-previous-value'
import { useFileTreeMainContext } from '@/features/file-tree/contexts/file-tree-main'

const FileTreeSelectableContext = createContext()

const ACTION_TYPES = {
  SELECT: 'SELECT',
  MULTI_SELECT: 'MULTI_SELECT',
  UNSELECT: 'UNSELECT',
}

function fileTreeSelectableReadWriteReducer(selectedEntityIds, action) {
  switch (action.type) {
    case ACTION_TYPES.SELECT: {
      // reset selection
      return new Set(Array.isArray(action.id) ? action.id : [action.id])
    }

    case ACTION_TYPES.MULTI_SELECT: {
      const selectedEntityIdsCopy = new Set(selectedEntityIds)
      if (selectedEntityIdsCopy.has(action.id)) {
        // entity already selected
        if (selectedEntityIdsCopy.size > 1) {
          // entity already multi-selected; remove from set
          selectedEntityIdsCopy.delete(action.id)
        }
      } else {
        // entity not selected: add to set
        selectedEntityIdsCopy.add(action.id)
      }

      return selectedEntityIdsCopy
    }

    case ACTION_TYPES.UNSELECT: {
      const selectedEntityIdsCopy = new Set(selectedEntityIds)
      selectedEntityIdsCopy.delete(action.id)
      return selectedEntityIdsCopy
    }

    default:
      throw new Error(`Unknown selectable action type: ${action.type}`)
  }
}

function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
  switch (action.type) {
    case ACTION_TYPES.SELECT:
      return new Set([action.id])

    case ACTION_TYPES.MULTI_SELECT:
    case ACTION_TYPES.UNSELECT:
      return selectedEntityIds

    default:
      throw new Error(`Unknown selectable action type: ${action.type}`)
  }
}

export function FileTreeSelectableProvider({ onSelect, children }) {
  const { _id: projectId, rootDocId } = useProjectContext(
    projectContextPropTypes
  )
  const { permissionsLevel } = useEditorContext(editorContextPropTypes)

  const [initialSelectedEntityId] = usePersistedState(
    `doc.open_id.${projectId}`,
    rootDocId
  )

  const { fileTreeData, setSelectedEntities } = useFileTreeData()

  const [isRootFolderSelected, setIsRootFolderSelected] = useState(false)

  const [selectedEntityIds, dispatch] = useReducer(
    permissionsLevel === 'readOnly'
      ? fileTreeSelectableReadOnlyReducer
      : fileTreeSelectableReadWriteReducer,
    null,
    () => {
      if (!initialSelectedEntityId) return new Set()

      // the entity with id=initialSelectedEntityId might not exist in the tree
      // anymore. This checks that it exists before initialising the reducer
      // with the id.
      if (findInTree(fileTreeData, initialSelectedEntityId))
        return new Set([initialSelectedEntityId])

      // the entity doesn't exist anymore; don't select any files
      return new Set()
    }
  )

  const [selectedEntityParentIds, setSelectedEntityParentIds] = useState(
    new Set()
  )

  // fills `selectedEntityParentIds` set
  useEffect(() => {
    const ids = new Set()
    selectedEntityIds.forEach(id => {
      const found = findInTree(fileTreeData, id)
      if (found) {
        found.path.forEach(pathItem => ids.add(pathItem))
      }
    })
    setSelectedEntityParentIds(ids)
  }, [fileTreeData, selectedEntityIds])

  // calls `onSelect` on entities selection
  const previousSelectedEntityIds = usePreviousValue(selectedEntityIds)
  useEffect(() => {
    if (_.isEqual(selectedEntityIds, previousSelectedEntityIds)) {
      return
    }
    const _selectedEntities = Array.from(selectedEntityIds)
      .map(id => findInTree(fileTreeData, id))
      .filter(Boolean)
    onSelect(_selectedEntities)
    setSelectedEntities(_selectedEntities)
  }, [
    fileTreeData,
    selectedEntityIds,
    previousSelectedEntityIds,
    onSelect,
    setSelectedEntities,
  ])

  useEffect(() => {
    // listen for `editor.openDoc` and selected that doc
    function handleOpenDoc(ev) {
      const found = findInTree(fileTreeData, ev.detail)
      if (!found) return

      dispatch({ type: ACTION_TYPES.SELECT, id: found.entity._id })
    }

    window.addEventListener('editor.openDoc', handleOpenDoc)
    return () => window.removeEventListener('editor.openDoc', handleOpenDoc)
  }, [fileTreeData])

  const select = useCallback(id => {
    dispatch({ type: ACTION_TYPES.SELECT, id })
  }, [])

  const unselect = useCallback(id => {
    dispatch({ type: ACTION_TYPES.UNSELECT, id })
  }, [])

  const selectOrMultiSelectEntity = useCallback((id, isMultiSelect) => {
    const actionType = isMultiSelect
      ? ACTION_TYPES.MULTI_SELECT
      : ACTION_TYPES.SELECT

    dispatch({ type: actionType, id })
  }, [])

  const value = {
    selectedEntityIds,
    selectedEntityParentIds,
    select,
    unselect,
    selectOrMultiSelectEntity,
    isRootFolderSelected,
    setIsRootFolderSelected,
  }

  return (
    <FileTreeSelectableContext.Provider value={value}>
      {children}
    </FileTreeSelectableContext.Provider>
  )
}

FileTreeSelectableProvider.propTypes = {
  onSelect: PropTypes.func.isRequired,
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node,
  ]).isRequired,
}

const projectContextPropTypes = {
  _id: PropTypes.string.isRequired,
  rootDocId: PropTypes.string,
}

const editorContextPropTypes = {
  permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}

const isMac = /Mac/.test(window.navigator?.platform)

export function useSelectableEntity(id, isFile) {
  const { view, setView } = useLayoutContext(layoutContextPropTypes)
  const { setContextMenuCoords } = useFileTreeMainContext()
  const {
    selectedEntityIds,
    selectOrMultiSelectEntity,
    isRootFolderSelected,
    setIsRootFolderSelected,
  } = useContext(FileTreeSelectableContext)

  const isSelected = selectedEntityIds.has(id)

  const handleEvent = useCallback(
    ev => {
      ev.stopPropagation()
      // use Command (macOS) or Ctrl (other OS) to select multiple items,
      // as long as the root folder wasn't selected
      const multiSelect =
        !isRootFolderSelected && (isMac ? ev.metaKey : ev.ctrlKey)
      setIsRootFolderSelected(false)
      selectOrMultiSelectEntity(id, multiSelect)
      setView(isFile ? 'file' : 'editor')
    },
    [
      id,
      isRootFolderSelected,
      setIsRootFolderSelected,
      selectOrMultiSelectEntity,
      setView,
      isFile,
    ]
  )

  const handleClick = useCallback(
    ev => {
      handleEvent(ev)
      if (!ev.ctrlKey && !ev.metaKey) {
        setContextMenuCoords(null)
      }
    },
    [handleEvent, setContextMenuCoords]
  )

  const handleKeyPress = useCallback(
    ev => {
      if (ev.key === 'Enter' || ev.key === ' ') {
        handleEvent(ev)
      }
    },
    [handleEvent]
  )

  const handleContextMenu = useCallback(
    ev => {
      // make sure the right-clicked entity gets selected
      if (!selectedEntityIds.has(id)) {
        handleEvent(ev)
      }
    },
    [id, handleEvent, selectedEntityIds]
  )

  const isVisuallySelected =
    !isRootFolderSelected && isSelected && view !== 'pdf'
  const props = useMemo(
    () => ({
      className: classNames({ selected: isVisuallySelected }),
      'aria-selected': isVisuallySelected,
      onClick: handleClick,
      onContextMenu: handleContextMenu,
      onKeyPress: handleKeyPress,
    }),
    [handleClick, handleContextMenu, handleKeyPress, isVisuallySelected]
  )

  return { isSelected, props }
}

const layoutContextPropTypes = {
  view: PropTypes.string,
  setView: PropTypes.func.isRequired,
}

export function useFileTreeSelectable() {
  const context = useContext(FileTreeSelectableContext)

  if (!context) {
    throw new Error(
      `useFileTreeSelectable is only available inside FileTreeSelectableProvider`
    )
  }

  return context
}
