import * as React from 'react';
import { debounce, noop } from '@vkontakte/vkjs';
import { getWindow, isHTMLElement } from '@vkontakte/vkui-floating-ui/utils/dom';
import { useCustomEnsuredControl } from '../../../hooks/useEnsuredControl';
import { useGlobalOnClickOutside } from '../../../hooks/useGlobalOnClickOutside';
import { useStableCallback } from '../../../hooks/useStableCallback';
import { contains, getActiveElementByAnotherElement } from '../../dom';
import { useIsomorphicLayoutEffect } from '../../useIsomorphicLayoutEffect';
import { LockFloatingPositionContext } from '../LockFloatingPosition/LockFloatingPosition';
import { autoUpdateFloatingElement, useFloating } from '../adapters';
import { convertFloatingDataToReactCSSProperties } from '../functions';
import { type UseFloatingOptions } from '../types/common';
import { DEFAULT_TRIGGER } from './constants';
import type {
  FloatingProps,
  ReferenceProps,
  ShownChangeReason,
  UseFloatingWithInteractionsProps,
  UseFloatingWithInteractionsReturn,
} from './types';
import { useResolveTriggerType } from './useResolveTriggerType';

type LocalState = { shown: boolean; reason?: ShownChangeReason };

const whileElementsMounted: UseFloatingOptions['whileElementsMounted'] = (...args) =>
  /* istanbul ignore next: не знаю как проверить */
  autoUpdateFloatingElement(...args, { elementResize: true });

/**
 * @private
 */
export const useFloatingWithInteractions = <T extends HTMLElement = HTMLElement>({
  trigger = DEFAULT_TRIGGER,

  // UseFloating
  placement: placementProp = 'bottom',
  strategy: strategyProp = 'fixed',
  middlewares,
  hoverDelay = 0,
  closeAfterClick = false,

  // disables
  disabled = false,
  disableInteractive = false,
  disableCloseOnClickOutside = false,
  disableCloseOnEscKey = false,

  // uncontrolled
  defaultShown = false,

  // controlled
  shown: shownProp,
  onShownChange: onShownChangeProp,
  onShownChanged: onShownChangedProp,
}: UseFloatingWithInteractionsProps): UseFloatingWithInteractionsReturn<T> => {
  const memoizedValue = React.useMemo<LocalState | undefined>(
    () => (shownProp !== undefined ? { shown: shownProp } : undefined),
    [shownProp],
  );
  const [shownLocalState, setShownLocalState] = useCustomEnsuredControl<LocalState>({
    value: memoizedValue,
    disabled,
    defaultValue: { shown: defaultShown },
    onChange: useStableCallback(({ shown, reason }) => {
      if (onShownChangeProp) {
        onShownChangeProp(shown, reason);
      }
    }),
  });
  const onShownChanged = useStableCallback(onShownChangedProp ? onShownChangedProp : noop);
  const [shownFinalState, setShownFinalState] = React.useState(() => shownLocalState.shown);
  const [willBeHide, setWillBeHide] = React.useState(false);

  const hasCSSAnimation = React.useRef(false);

  const blockMouseEnterRef = React.useRef(false);
  const blockFocusRef = React.useRef(false);
  const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  const handleCloseOnReferenceClickOutsideDisabled =
    disabled || disableCloseOnClickOutside || willBeHide || !shownLocalState.shown;
  const handleCloseOnFloatingClickOutsideDisabled =
    disableInteractive || handleCloseOnReferenceClickOutsideDisabled;

  const { triggerOnFocus, triggerOnClick, triggerOnHover } = useResolveTriggerType(trigger);

  const isLock = React.useContext(LockFloatingPositionContext);

  // Библиотека `floating-ui`
  const { placement, x, y, strategy, refs, middlewareData } = useFloating<T>({
    strategy: strategyProp,
    placement: placementProp,
    middleware: middlewares,
    whileElementsMounted: isLock ? undefined : whileElementsMounted,
  });

  const commitShownLocalState = React.useCallback(
    (nextShown: boolean, reason: ShownChangeReason) => {
      setShownLocalState((prevState) => {
        if (prevState.shown !== nextShown || prevState.reason !== reason) {
          return {
            shown: nextShown,
            reason,
          };
        }
        /* istanbul ignore next: страховка, если вдруг на момент вызова обновления состояния, оно уже будет актуальным */
        return prevState;
      });
    },
    [setShownLocalState],
  );

  const [mouseEnterDelay, mouseLeaveDelay] =
    typeof hoverDelay === 'number' ? [hoverDelay, hoverDelay] : hoverDelay;

  const showWithDelay = React.useMemo(
    () => debounce(() => commitShownLocalState(true, 'hover'), mouseEnterDelay),
    [mouseEnterDelay, commitShownLocalState],
  );

  const hideWithDelay = React.useMemo(
    () => debounce(() => commitShownLocalState(false, 'hover'), mouseLeaveDelay),
    [mouseLeaveDelay, commitShownLocalState],
  );

  const handleFocusOnReference = useStableCallback(() => {
    // Повторный вызов события фокуса - следствие клика на reference-элемент
    if (shownLocalState.shown) {
      if (!closeAfterClick && shownLocalState.reason === 'hover') {
        return;
      }
      commitShownLocalState(false, 'focus');
      return;
    }
    if (blockFocusRef.current) {
      /* istanbul ignore next: в Vitest не воспроизводится баг на вебе (cм. onRestoreFocus) */
      blockFocusRef.current = false;
      return;
    }

    commitShownLocalState(true, 'focus');
  });

  const handleBlurOnReference = useStableCallback((event: React.FocusEvent) => {
    blockFocusRef.current = false;
    blockMouseEnterRef.current = false;

    if (!shownLocalState.shown) {
      clearTimeout(blurTimeoutRef.current);
      return;
    }

    const relatedTarget = event.relatedTarget;
    blurTimeoutRef.current = setTimeout(function waitWindowBlurFire() {
      const reference = refs.reference.current;
      // Если пользователь покинул текущее окно в открытом состоянии, то
      // не закрываем всплывающий элемент.
      /* istanbul ignore if: не умеем симулировать уход из текущего окна */
      if (!relatedTarget && getActiveElementByAnotherElement(reference) === reference) {
        /* istanbul ignore next */
        return;
      }

      // Если пользователь нажал на всплывающий элемент, то не закрываем всплывающий элемент.
      // Note: для этого элемент должен быть фокусируемый (например, за счёт `tabindex="-1"`).
      if (contains(refs.floating.current, relatedTarget) || contains(reference, relatedTarget)) {
        return;
      }

      commitShownLocalState(false, 'focus');
    });
  });

  const handleClickOnReference = useStableCallback(() => {
    // Предыдущий триггер (фокус) уже вызвал открытие/закрытие всплывающего окна, игнорируем вызов
    if (shownLocalState.reason === 'focus') {
      commitShownLocalState(shownLocalState.shown, 'click');
      return;
    }
    commitShownLocalState(!shownLocalState.shown, 'click');
  });

  const handleClickOnReferenceForOnlyClose = useStableCallback(() => {
    blockMouseEnterRef.current = true;
    commitShownLocalState(false, 'click');
  });

  const handleMouseEnterOnBoth = useStableCallback((event: React.MouseEvent<HTMLElement>) => {
    if (willBeHide && event.currentTarget === refs.floating.current) {
      return;
    }

    showWithDelay.cancel();
    hideWithDelay.cancel();

    if (!blockMouseEnterRef.current && !shownLocalState.shown) {
      showWithDelay();
    }
  });

  const handleMouseLeaveOnBothForHoverAndFocusStates = useStableCallback(
    (event: React.MouseEvent<HTMLElement>) => {
      if (willBeHide && event.currentTarget === refs.floating.current) {
        return;
      }

      blockFocusRef.current = false;
      blockMouseEnterRef.current = false;

      if (triggerOnHover) {
        showWithDelay.cancel();
        hideWithDelay.cancel();

        hideWithDelay();
      }
    },
  );

  const handleFloatingAnimationStart = () => {
    hasCSSAnimation.current = true;
  };

  const handleFloatingAnimationEnd = () => {
    if (willBeHide) {
      setShownFinalState(false);
      setWillBeHide(false);
      onShownChanged(false, shownLocalState.reason);
    }
  };

  const handleOnClose = React.useCallback(() => {
    blockFocusRef.current = true;
    commitShownLocalState(false, 'callback');
  }, [commitShownLocalState]);

  const handleRestoreFocus: UseFloatingWithInteractionsReturn['onRestoreFocus'] = React.useCallback(
    (restoreFocus = true) => {
      if (!restoreFocus) {
        return false;
      }
      if (restoreFocus === true) {
        return triggerOnFocus ? blockFocusRef.current : true;
      } else if (restoreFocus === 'anchor-element') {
        return refs.reference.current as HTMLElement;
      } else if (restoreFocus instanceof HTMLElement) {
        return restoreFocus;
      }
      return false;
    },
    [refs.reference, triggerOnFocus],
  );

  const handleEscapeKeyDown = React.useCallback(() => {
    blockFocusRef.current = true;
    commitShownLocalState(false, 'escape-key');
  }, [commitShownLocalState]);

  const handleClickOutside = React.useCallback(() => {
    blockFocusRef.current = true;
    commitShownLocalState(false, 'click-outside');
  }, [commitShownLocalState]);

  useGlobalOnClickOutside(
    handleClickOutside,
    handleCloseOnReferenceClickOutsideDisabled ? null : refs.reference,
    handleCloseOnFloatingClickOutsideDisabled ? null : refs.floating,
  );

  useIsomorphicLayoutEffect(
    /**
     * Если пользователь покинул активное окно и:
     * 1. целевой элемент был в состоянии фокуса;
     * 2. всплывающий элемент был закрытом состоянии;
     * то фокус должен быть заблокирован, когда пользователь вернётся обратно. Иначе покажется
     * всплывающий элемент.
     */
    function setGlobalBlurForTriggerOnFocus() {
      if (!triggerOnFocus || !refs.reference.current) {
        return;
      }

      const handleGlobalBlur = () => {
        /* istanbul ignore next */
        const reference = refs.reference.current;
        /* istanbul ignore if: не умеем симулировать уход из текущего окна */
        if (
          !shownLocalState.shown &&
          isHTMLElement(reference) &&
          reference === getActiveElementByAnotherElement(reference)
        ) {
          /* istanbul ignore next */
          blockFocusRef.current = true;
        }
      };

      const win = getWindow(refs.reference.current);
      win.addEventListener('blur', handleGlobalBlur);
      return () => {
        win.removeEventListener('blur', handleGlobalBlur);
      };
    },
    [triggerOnFocus, refs.reference, shownLocalState],
  );

  useIsomorphicLayoutEffect(
    function resolveShownStates() {
      if (willBeHide || shownLocalState.shown === shownFinalState) {
        return;
      }

      if (shownLocalState.shown) {
        setShownFinalState(true);
        onShownChanged(true, shownLocalState.reason);
      } else if (hasCSSAnimation.current && !willBeHide) {
        setWillBeHide(true);
      } else {
        setShownFinalState(false);
      }

      return () => {
        clearTimeout(blurTimeoutRef.current);
      };
    },
    [shownLocalState, shownFinalState, willBeHide, onShownChanged],
  );

  const referencePropsRef = React.useRef<ReferenceProps>({});
  const floatingPropsRef = React.useRef<FloatingProps>({ style: {} });

  useIsomorphicLayoutEffect(() => {
    referencePropsRef.current = {};
  }, [triggerOnHover, triggerOnFocus, triggerOnClick]);

  if (shownFinalState) {
    floatingPropsRef.current.style = convertFloatingDataToReactCSSProperties({
      strategy,
      x,
      y,
      middlewareData,
    });

    if (disableInteractive) {
      floatingPropsRef.current.style.pointerEvents = 'none';
    }
  }

  if (triggerOnFocus) {
    referencePropsRef.current.onFocus = handleFocusOnReference;
    referencePropsRef.current.onBlur = handleBlurOnReference;
  }

  if (triggerOnClick) {
    referencePropsRef.current.onClick = handleClickOnReference;
  }

  if (triggerOnHover) {
    referencePropsRef.current.onMouseOver = handleMouseEnterOnBoth;

    if (closeAfterClick && !triggerOnClick) {
      referencePropsRef.current.onClick = handleClickOnReferenceForOnlyClose;
    }

    if (!disableInteractive) {
      floatingPropsRef.current.onMouseOver = handleMouseEnterOnBoth;
    }
  }

  if (triggerOnHover || triggerOnFocus) {
    referencePropsRef.current.onMouseLeave = handleMouseLeaveOnBothForHoverAndFocusStates;

    if (!disableInteractive) {
      floatingPropsRef.current.onMouseLeave = handleMouseLeaveOnBothForHoverAndFocusStates;
    }
  }

  if (shownFinalState) {
    floatingPropsRef.current.onAnimationStart = handleFloatingAnimationStart;
    floatingPropsRef.current.onAnimationEnd = handleFloatingAnimationEnd;
  }

  return {
    placement,
    shown: shownFinalState,
    willBeHide,
    refs,
    referenceProps: referencePropsRef.current,
    floatingProps: floatingPropsRef.current,
    middlewareData,
    onClose: handleOnClose,
    // FocusTrap уже определяет нажатие на ESC, поэтому название события содержит конкретный код
    // кнопки вместо просто onKeyDown.
    onEscapeKeyDown: !shownFinalState || disableCloseOnEscKey ? undefined : handleEscapeKeyDown,
    // [Обход баги с FocusTrap]
    //
    // Если сфокусироваться на целевой элемент через нажатие, а потом нажать в область за пределами
    // целевого и всплывающего элемента, то появляется моргание из-за того, что FocusTrap
    // восстанавливает фокус, из-за чего всплывающий элемент снова показывается за счёт
    // `handleFocusOnReference`, а потом скрывается за счёт `handleBlurOnReference`.
    onRestoreFocus: handleRestoreFocus,
  };
};
