import { useCallback, useContext, useEffect, useMemo } from 'react';
import { useFocusWithin } from '../use-focus-within';
import { KEY_VALUES, KEYS } from './constants';
import { collapseNode, expandNode, getKeyActionFnDefault, getPositionInSet, getSetSize, toggleSelect, getDummyNode } from './utils';
import { TreeNavigationContext } from './tree-navigation-context';
const noop = () => {};
const defaultProps = {
  onKeyDown: noop,
  onFocus: noop,
  onBlur: noop
};
export function useTreeNodeNavigation({
  id,
  onToggleExpanded = noop,
  onToggleSelected = noop,
  onToggleFocused = noop,
  componentProps = defaultProps,
  isSelected = false,
  onCustomButtonKeyDown = noop,
  getKeyActionFn = getKeyActionFnDefault,
  ignoreKeys = [KEYS.TAB],
  // ignoring [TAB] allows to navigate to the next element, close menu etc.
  preventDefault = true
}) {
  const context = useContext(TreeNavigationContext);
  const {
    nodes,
    nodesMap,
    refs,
    setBlurred,
    setFocused,
    isDisabled,
    focused
  } = context;
  // When new item is added in a tree for one render cycle it is not present in
  // a context because of useEffect.
  // This dummy node instance prevents node from being undefined and consumers
  // from accessing its property as optional (like node?.isFocused).
  const node = nodesMap.get(id) || getDummyNode(id);
  const {
    onFocus,
    onBlur
  } = useFocusWithin({
    onFocus: event => {
      if (isDisabled || node.isDisabled) {
        return;
      }
      // tree nodes are nested
      event.stopPropagation();
      setFocused(id);
      onToggleFocused(true);
      componentProps.onFocus?.(event);
    },
    onBlur: event => {
      if (isDisabled || node.isDisabled) {
        return;
      }
      // tree nodes are nested
      event.stopPropagation();
      setBlurred(id);
      onToggleFocused(false);
      componentProps.onBlur?.(event);
    },
    onChildFocus: event => {
      if (isDisabled || node.isDisabled) {
        return;
      }
      // tree nodes are nested
      event.stopPropagation();
      onToggleFocused(true);
      setFocused(id);
    }
  });
  const isFirstFocusable = useMemo(() => {
    return !isDisabled && nodes.indexOf(node) === 0;
  }, [node, nodes, isDisabled]);
  useEffect(() => {
    if (!isDisabled && focused?.id) {
      const elementToFocus = refs.current.get(focused.id);
      if (document.activeElement !== elementToFocus) {
        elementToFocus?.focus();
      }
    }
  }, [refs, focused?.id, isDisabled]);
  const setRef = useCallback(value => {
    refs.current.set(id, value);
  }, [id]);
  const onKeyDownHandler = useCallback(event => {
    const key = event.key;
    componentProps.onKeyDown?.(event);
    if (!KEY_VALUES.includes(key)) {
      return;
    }

    // allow interaction with action items of the node
    if (key === KEYS.ENTER && context.refs.current.get(id) !== event.target) {
      return;
    }

    // allow to focus on elements outside of tree nodes by default
    if (ignoreKeys?.includes(key)) {
      return;
    }
    if (node.isDisabled || isDisabled) {
      return;
    }
    const actionFn = getKeyActionFn(key, node, context);
    if (actionFn) {
      if (Array.isArray(actionFn)) {
        for (const fn of actionFn) {
          fn({
            node,
            context,
            onToggleExpanded,
            onToggleSelected,
            isSelected,
            onCustomButtonKeyDown,
            button: key
          });
        }
      } else {
        actionFn({
          node,
          context,
          onToggleExpanded,
          onToggleSelected,
          isSelected,
          onCustomButtonKeyDown,
          button: key
        });
      }
    }

    // tree nodes are nested
    event.stopPropagation();
    if (preventDefault) {
      event.preventDefault();
    }
  }, [componentProps.onKeyDown, node, context, onToggleExpanded, onToggleSelected, isSelected, isDisabled, preventDefault]);
  const toggleExpanded = useCallback(expanded => {
    const actionFn = expanded ? expandNode : collapseNode;
    if (actionFn) {
      actionFn({
        node,
        context,
        onToggleExpanded,
        onToggleSelected,
        isSelected,
        onCustomButtonKeyDown
      });
    }
  }, [node, context, onToggleExpanded, onToggleSelected, isSelected]);
  const toggleSelected = useCallback(() => {
    toggleSelect({
      node,
      context,
      onToggleExpanded: noop,
      onToggleSelected: noop,
      isSelected,
      onCustomButtonKeyDown
    });
  }, [node, context, isSelected]);
  const treeItemAttrs = useMemo(() => {
    return {
      // it should be possible to navigate to action items inside the tree item,
      // but not to the next tree item
      tabIndex: node.isDisabled || isDisabled || context.focused && !node.isFocused ? -1 : 0,
      role: 'treeitem',
      'aria-disabled': node.isDisabled || isDisabled,
      'aria-selected': node.isFocused || !!isSelected,
      'aria-level': node.parents.length + 1,
      'aria-setsize': getSetSize(node, nodes, nodesMap),
      'aria-posinset': getPositionInSet(node, nodes, nodesMap),
      onFocus,
      onBlur,
      onKeyDown: onKeyDownHandler
    };
  }, [node, nodes, nodesMap, isDisabled, onFocus, onBlur, onKeyDownHandler]);
  const api = {
    node,
    treeItemAttrs,
    ref: setRef,
    toggleExpanded,
    toggleSelected,
    setFocused,
    setBlurred,
    isFirstFocusable,
    isDisabled
  };
  return useMemo(() => api, Object.values(api));
}