import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import { useAnimationFrame } from 'framer-motion';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

const Anchor = styled.div`
  display: contents;
`;

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

const fadeOut = keyframes`
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
`;
const TooltipContent = styled.div<{
  animationState?: 'fading-in' | 'fading-out';
}>`
  pointer-events: none;
  position: fixed;
  z-index: 100000;
  animation: ${({ animationState }) =>
      animationState === 'fading-out'
        ? fadeOut
        : animationState === 'fading-in'
          ? fadeIn
          : 'none'}
    100ms ease-in-out;
`;

export default function AnchoredTooltip({
  label,
  placement,
  keepOpen,
  children,
}: {
  label?: React.ReactNode;
  placement: 'top' | 'bottom' | 'left' | 'right';
  keepOpen?: boolean;
  children: React.ReactNode;
}) {
  const [isHovering, setIsHovering] = useState(false);
  const anchorRef = useRef<HTMLDivElement>(null);
  const labelRef = useRef<HTMLDivElement>(null);
  useAnimationFrame(() => {
    const anchor = anchorRef.current;
    const firstChild = anchor?.firstChild as HTMLElement;
    const label = labelRef.current;
    if (!firstChild || !label) return;

    const firstChildRect = firstChild.getBoundingClientRect();
    const labelRect = label.getBoundingClientRect();

    if (placement === 'right') {
      label.style.left = `${firstChildRect.right}px`;
      label.style.top = `${firstChildRect.top - (labelRect.height - firstChildRect.height) / 2}px`;
    } else if (placement === 'left') {
      label.style.right = `${firstChildRect.left}px`;
      label.style.top = `${firstChildRect.top - (labelRect.height - firstChildRect.height) / 2}px`;
    } else if (placement === 'bottom') {
      label.style.left = `${firstChildRect.left - (labelRect.width - firstChildRect.width) / 2}px`;
      label.style.top = `${firstChildRect.bottom}px`;
    } else if (placement === 'top') {
      label.style.left = `${firstChildRect.left - (labelRect.width - firstChildRect.width) / 2}px`;
      label.style.top = `${firstChildRect.top - labelRect.height}px`;
    }
  });

  const clearListenersRef = useRef<() => void>(() => {});

  const receiveAnchorRef = useCallback((node: HTMLDivElement | null) => {
    clearListenersRef.current();
    anchorRef.current = node;
    if (node?.firstChild) {
      const firstChild = node.firstChild;
      firstChild.addEventListener('mouseenter', () => setIsHovering(true));
      firstChild.addEventListener('mouseleave', () => setIsHovering(false));
      clearListenersRef.current = () => {
        firstChild.removeEventListener('mouseenter', () => setIsHovering(true));
        firstChild.removeEventListener('mouseleave', () =>
          setIsHovering(false)
        );
        clearListenersRef.current = () => {};
      };
    }
  }, []);

  const [showContent, setShowContent] = useState(false);
  const [animationState, setAnimationState] = useState<
    'fading-in' | 'fading-out' | undefined
  >(undefined);
  const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const open = isHovering || keepOpen;

  const wasOpen = useRef<boolean>(false);
  useEffect(() => {
    if (animationTimeoutRef.current) {
      clearTimeout(animationTimeoutRef.current);
    }
    if (closeTimeoutRef.current) {
      clearTimeout(closeTimeoutRef.current);
    }
    if (open && !wasOpen.current) {
      setAnimationState('fading-in');
      setShowContent(true);
    } else if (!open && wasOpen.current) {
      closeTimeoutRef.current = setTimeout(() => {
        setAnimationState('fading-out');
        animationTimeoutRef.current = setTimeout(() => {
          setShowContent(false);
          setAnimationState(undefined);
        }, 100);
      }, 1);
    }
    wasOpen.current = !!open;
  }, [open]);

  return (
    <Anchor ref={receiveAnchorRef}>
      {label &&
        showContent &&
        createPortal(
          <TooltipContent animationState={animationState} ref={labelRef}>
            {label}
          </TooltipContent>,
          document.body
        )}
      {children}
    </Anchor>
  );
}
