/* eslint-disable react/jsx-props-no-spreading */
import { Children, ReactElement, ReactNode, cloneElement, useEffect, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import { CSSTransition } from 'react-transition-group';

import { useLinkComponent } from '../../core/hooks/useLinkComponent';
import { useId } from '../../hooks/useId';
import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { CommonProps } from '../../types';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { isIntrinsicElement } from '../../utils/isIntrinsicElement';
import { makeTestId } from '../../utils/makeTestId';
import { setForwarderRef } from '../../utils/setForwarderRef';
import { Portal } from '../Portal/Portal';

import { TooltipPosition } from './constants';
import { StyledArrow, StyledTooltip, transitionDuration } from './styled';

/** Props for {@link Tooltip} */
export interface TooltipProps extends Omit<CommonProps, 'ariaDescribedBy'> {
  /** Text that should be shown in {@link Tooltip} */
  text: ReactNode;
  /** Preferred position, if not set tooltip position will be `top` */
  position?: TooltipPosition;
  fallbackPositions?: TooltipPosition[];
  /** Delay before showing tooltip */
  delay?: number;
  children: ReactNode;
  /* Tooltip will be shown by focus and hidden by blur event */
  activateByFocus?: boolean;
}

/**
 * Hint that will be visible when user hover target element.
 *
 * {@link Tooltip} is a11y friendly, they add `aria-desribedby` to target element with link to
 * tooltip text, that allows screen readers announce this text as element description.
 *
 * ```tsx
 * <Tooltip text='Tooltip text'>
 *   <Button>Some text</Button>
 * </Tooltip>
 * ```
 *
 * ### Position
 *
 * Usually you should allow {@link Tooltip} to choice the top position, but you can
 * set preferred position by {@link TooltipProps.position} prop.
 *
 * ```tsx
 * <Tooltip text='Tooltip text' position={TooltipPosition.Right}>
 *   <Button>Some text</Button>
 * </Tooltip>
 * ```
 */
export function Tooltip(props: TooltipProps) {
  const {
    position = TooltipPosition.Top,
    fallbackPositions = [TooltipPosition.Top, TooltipPosition.Right],
    text,
    children,
    className,
    testId,
    delay,
    activateByFocus = false,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();
  const [visible, setVisible] = useState(false);
  const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null);
  const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
  const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null);

  const delayTimeoutRef = useRef<any>(null);

  const tooltipId = useId();

  useEffect(() => {
    const show = () => {
      if (delay) {
        delayTimeoutRef.current = setTimeout(() => {
          setVisible(true);
        }, delay);
      } else {
        setVisible(true);
      }
    };

    const hide = () => {
      if (delay && delayTimeoutRef.current) {
        clearTimeout(delayTimeoutRef.current);
      }
      setVisible(false);
    };

    referenceElement?.addEventListener('mouseenter', show);
    referenceElement?.addEventListener('mouseleave', hide);

    if (activateByFocus) {
      referenceElement?.addEventListener('focus', show);
      referenceElement?.addEventListener('blur', hide);
    }

    return () => {
      referenceElement?.removeEventListener('mouseenter', show);
      referenceElement?.removeEventListener('mouseleave', hide);

      if (activateByFocus) {
        referenceElement?.removeEventListener('focus', show);
        referenceElement?.removeEventListener('blur', hide);
      }
    };
  }, [referenceElement, activateByFocus, delay]);

  const child = Children.only(children) as ReactElement;

  const Link = useLinkComponent();
  const ariaDescribedByProperty =
    isIntrinsicElement(child) || child.type === Link ? 'aria-describedby' : 'ariaDescribedBy';

  const triggerElement = cloneElement(child, {
    ref: (ref: HTMLElement) => {
      setReferenceElement(ref);
      setForwarderRef((child as any).ref, ref);
    },
    [ariaDescribedByProperty]: tooltipId,
  });

  const { styles, attributes } = usePopper(referenceElement, popperElement, {
    strategy: 'fixed',
    placement: position,
    modifiers: [
      { name: 'arrow', options: { element: arrowElement } },
      {
        name: 'offset',
        options: {
          offset: [0, 8],
        },
      },
      {
        name: 'flip',
        options: {
          fallbackPlacements: fallbackPositions,
        },
      },
      { name: 'eventListeners', enabled: visible },
    ],
  });

  if (!text) {
    return triggerElement;
  }

  return (
    <>
      {triggerElement}
      <Portal>
        <CSSTransition in={visible} nodeRef={{ current: popperElement }} timeout={transitionDuration}>
          <StyledTooltip
            ref={setPopperElement}
            className={className}
            id={tooltipId}
            role="tooltip"
            style={styles.popper}
            {...attributes.popper}
            {...{ [testIdAttribute]: testId }}
          >
            {text}
            <StyledArrow
              ref={setArrowElement}
              style={styles.arrow}
              {...{ [testIdAttribute]: makeTestId(testId, 'arrow') }}
            />
          </StyledTooltip>
        </CSSTransition>
      </Portal>
    </>
  );
}
