import React, { useRef, useEffect, HTMLProps } from "react";
import { ReferenceObject } from "popper.js";
import { Bubble, BubbleContentProps, BubbleProps } from "../bubble";
import { TriggerProps } from "../popover";
import { isMobile } from "../_util/is-mobile";

/**
 * 使用 Tooltip 的内容需要使用容器包裹。
 *
 * 默认会使用 span 包裹内容，如有需要，可以改为使用 div 包裹。
 */
export interface TooltipProps {
  /**
   * Tooltip 放置位置
   * @default "bottom-start"
   */
  placement?: BubbleContentProps["placement"];

  /**
   * 文本提示内容
   */
  title?: React.ReactNode;

  /**
   * 要提示的对象
   */
  children?: React.ReactNode;

  /**
   * 出现延迟时间（ms）
   * @default 600
   */
  openDelay?: number;

  /**
   * 距上次 Tooltip 出现 1s 内再次出现延迟时间（ms）
   * @default 300
   */
  openDelayWhenHasInstance?: number;

  /**
   * 挂载浮层组件的节点
   * @default document.body
   * @since 2.5.4
   */
  popupContainer?: BubbleProps["popupContainer"];
}

let visibleTooltipCount = 0;

/**
 * 控制 Tooltip 的触发机制
 *
 * Tooltip 跟普通的 Bubble 不一样：
 *
 * 1. 出现位置不参考元素，参考鼠标
 * 2. 如果场上无 Tooltip 实例，则激活后 0.6s 后弹出
 * 3. 如果场上有 Tooltip 实例，则激活后 0.3s 后弹出
 *
 * 设计上面的规则，是为了对齐系统 Tooltip 的交互
 */
const TooltipTrigger = ({
  delay,
  openDelay = delay,
  closeDelay = 0,
  setVisible,
  render,
  openDelayWhenHasInstance,
}: TriggerProps & { delay: number; openDelayWhenHasInstance: number }) => {
  // 当前鼠标的位置，用作 Tooltip 弹出位置的参考
  const mouseElement = useRef<
    ReferenceObject & { scrollTop: number; scrollLeft: number }
  >(null);

  // Tooltip 确定要激活，在延时出来期间，需要监听鼠标新位置，这个引用保存监听的取消方法
  const mouseWatching = useRef<() => void>(null);

  // 从鼠标事件获得的坐标信息，构造鼠标参考位置
  const mark = useRef((evt: MouseEvent) => {
    const width = 0;
    const height = 0;
    const clientX = Math.round(evt.clientX);
    const clientY = Math.round(evt.clientY);
    mouseElement.current = {
      clientWidth: width,
      clientHeight: height,
      getBoundingClientRect: () => ({
        left: clientX,
        top: clientY,
        right: clientX + width,
        bottom: clientY + height,
        width,
        height,
      }),
      // IE10 下 Popper 读取这两个值计算
      scrollTop: 0,
      scrollLeft: 0,
    };
  });

  // 监听鼠标位置变化，更新位置
  const watch = () => {
    document.addEventListener("mousemove", mark.current);
    mouseWatching.current = () =>
      document.removeEventListener("mousemove", mark.current);
    return mouseWatching.current;
  };

  // 取消当前的位置变化监听
  const unwatch = () => {
    if (mouseWatching.current) {
      mouseWatching.current();
      mouseWatching.current = null;
    }
  };

  // 用户鼠标进入目标元素
  const enter = async (evt: React.MouseEvent) => {
    // 记录鼠标位置并跟踪
    mark.current(evt.nativeEvent);
    watch();
    // Tooltip 出来后，更新实例数量，并且可以取消跟踪了（Tooltip 出来的时候，鼠标在哪儿，就在哪儿，无需跟随）
    await setVisible(
      true,
      visibleTooltipCount > 0 ? openDelayWhenHasInstance : openDelay
    );
    visibleTooltipCount += 1;
    unwatch();
  };

  // 用户鼠标离开目标元素
  const leave = async () => {
    // 取消原有的跟踪
    unwatch();

    // 完全隐藏后，延时 1 秒更新实例数量
    // 这里延时 1 秒是给用户留足够的空间，不至于由于鼠标走的慢导致下次 Tooltip 又要等很久
    await setVisible(false, closeDelay);
    // CD 1 秒
    await new Promise(resolve => setTimeout(resolve, 1000));
    visibleTooltipCount -= 1;
  };

  // 组件 unmount 的时候，清理一切
  useEffect(
    () => () => {
      mouseElement.current = null;
      // React 18 会触发卸载🤔，先取消这里
      // mark.current = null;
      unwatch();
    },
    []
  );

  return render({
    childrenProps: {
      onMouseEnter: enter,
      onMouseLeave: leave,
    },
    overlayProps: {},
    referenceElement: mouseElement.current,
  });
};

TooltipTrigger.displayName = "TooltipTrigger";

function TooltipMobileTrigger(props: TriggerProps) {
  const { setVisible, openDelay = 1000, closeDelay = 100, render } = props;
  const commonProps: HTMLProps<HTMLElement> = {
    onTouchStart: () => setVisible(true, openDelay),
    onTouchEnd: () => setVisible(false, closeDelay),
  };

  return render({
    overlayProps: commonProps,
    childrenProps: commonProps,
  });
}

TooltipMobileTrigger.displayName = "TooltipTrigger";

export function Tooltip({
  title,
  placement = "bottom-start",
  children,
  openDelay = 600,
  openDelayWhenHasInstance = 300,
  popupContainer,
}: TooltipProps) {
  return (
    <Bubble
      tooltip
      trigger={
        isMobile
          ? TooltipMobileTrigger
          : [TooltipTrigger, { delay: openDelay, openDelayWhenHasInstance }]
      }
      closeOnScroll
      placement={placement}
      placementOffset={20}
      content={title}
      overlayStyle={{ pointerEvents: "none" }}
      updateOnDimensionChange={false}
      popupContainer={popupContainer}
    >
      {children}
    </Bubble>
  );
}

Tooltip.displayName = "Tooltip";
