import React, { useCallback, useRef } from "react";
import classNames from "classnames";
import { Overlay, OverlayLayerProps } from "../overlay";
import { useDelayVisible } from "./useDelayVisible";
import { useParentScroll } from "../_util/use-parent-scroll";
import { buildinTriggers, Trigger, TriggerWithProps } from "./trigger";
import { mergeStyle } from "../_util/merge-style";
import { DomRef } from "../domref";
import { mergeRefs } from "../_util/merge-refs";
import { attachProps } from "../_util/attach-props";
import { isChildOfType } from "../_util/is-child-of-type";
import { Bubble } from "../bubble";
import { PopConfirm } from "../popconfirm";
import { Tooltip } from "../tooltip";

export interface PopoverProps
  extends Pick<
    OverlayLayerProps,
    | "escapeWithReference"
    | "flipped"
    | "positionFixed"
    | "onExited"
    | "popupContainer"
    | "destroyOnClose"
    | "transitionTimeout"
    | "referenceElement"
    | "animationScaleFrom"
    | "updateOnDimensionChange"
  > {
  /**
   * 触发弹出层显示的事件
   *
   * - `hover`：用户鼠标进入渲染内容时，显示浮层；离开渲染内容时，隐藏浮层。
   *   使用该交互请保证 `children` 可以接收 `onMouseEnter` 和 `onMouseLeave` 事件
   *
   * - `click`：用户在渲染内容上点击鼠标时，显示浮层；在渲染内容以及浮层外点击时，隐藏浮层。
   *   使用该交互请保证 `children` 可以接收 `onClick` 事件
   *
   * - 传入自定义组件以实现自定义弹出逻辑
   *
   * @default "hover"
   */
  trigger?: keyof typeof buildinTriggers | Trigger | TriggerWithProps;

  /**
   * 弹出层的内容是否默认打开
   * @default false
   */
  defaultVisible?: boolean;

  /**
   * 传入 `visible` 则表示使用受控模式来控制弹出层的显示，请处理 `onVisibleChange` 方法
   */
  visible?: boolean;

  /**
   * `visible` 变化时回调
   */
  onVisibleChange?: (visible: boolean) => void;

  /**
   * 触发弹出层的内容，会直接渲染出来
   *
   * 内容触发弹出层的方式，由 `trigger` 属性指定
   *
   * 如果传入的不是 `ReactElement`（如文本、数字等），则渲染时会套一层 `span`
   */
  children?: React.ReactNode;

  /**
   * 弹出层内容
   * @docType React.ReactNode | ((props: OverlayContentProps) => JSX.Element)
   */
  overlay?: OverlayLayerProps["content"];

  /**
   * 打开浮层前的延时
   */
  openDelay?: number;

  /**
   * 关闭浮层前的延时
   */
  closeDelay?: number;

  /**
   * 是否在容器滚动发生关闭
   * @default false
   */
  closeOnScroll?: boolean;

  /**
   * 气泡弹出的位置
   * @default "top"
   */
  placement?: OverlayLayerProps["placement"];

  /**
   * 弹出位置偏离参考位置的位移
   *
   * 如： `10`、`"50%"`、`"10 + 10%"`、`[10, 10]`
   *
   * @default 10
   */
  placementOffset?: OverlayLayerProps["placementOffset"];

  /**
   * 覆盖层自定义类名
   */
  overlayClassName?: string;

  /**
   * 覆盖层自定义样式
   */
  overlayStyle?: React.CSSProperties;
}

export type PopoverRenderFunction = (props: PopupRenderProps) => JSX.Element;

export interface PopupRenderProps {
  ref: React.Ref<any>;
  open: () => void;
  close: () => void;
}

export type PopupTrigger = "click" | "hover";

export function Popover(props: PopoverProps) {
  const {
    overlay,
    trigger = "hover",
    placement = "top",
    placementOffset = 10,
    escapeWithReference,
    popupContainer,
    children,
    closeOnScroll,
    overlayClassName,
    overlayStyle,
    openDelay,
    closeDelay,
    updateOnDimensionChange,
    animationScaleFrom,
    transitionTimeout,
    destroyOnClose,
    positionFixed,
    flipped,
    onExited,
  } = props;

  const overlayRef = useRef(null);
  const childrenRef = useRef(null);

  const { visible, setVisible } = useDelayVisible(props);

  // 设置了 closeOnScroll，则使用
  useParentScroll(
    childrenRef,
    visible && closeOnScroll && (() => setVisible(false, closeDelay))
  );

  let Trigger: Trigger;
  let triggerExtraProps: any = {};

  // 使用指定的触发交互组件
  if (typeof trigger === "string") {
    Trigger = buildinTriggers[trigger];
  }
  // 支持附加 props 的触发器
  else if (Array.isArray(trigger)) {
    [Trigger, triggerExtraProps] = trigger;
  }
  // 本身就是 Trigger 组件
  else {
    Trigger = trigger;
  }
  // fallback
  Trigger = Trigger || buildinTriggers.empty;

  const getChildren = useCallback(
    (childrenProps: any) => {
      const hasOneValidElement = c =>
        React.Children.count(c) === 1 &&
        React.isValidElement(React.Children.toArray(c)[0]);

      // 支持元素被多层浮层组件包裹
      const traverse = (children: React.ReactNode) => {
        let child;
        if (hasOneValidElement(children)) {
          [child] = React.Children.toArray(children);
          if (
            isChildOfType(child, Bubble) ||
            isChildOfType(child, PopConfirm) ||
            isChildOfType(child, Popover) ||
            isChildOfType(child, Tooltip)
          ) {
            return React.cloneElement(
              child as any,
              {},
              traverse(child.props.children)
            );
          }
          return React.cloneElement(
            child,
            attachProps(child.props, childrenProps)
          );
        }
        child = <span {...childrenProps}>{children}</span>;

        return child;
      };

      return traverse(children);
    },
    [children]
  );

  return (
    <Trigger
      {...triggerExtraProps}
      overlayElementRef={overlayRef}
      childrenElementRef={childrenRef}
      visible={visible}
      setVisible={setVisible}
      openDelay={openDelay}
      closeDelay={closeDelay}
      render={({ overlayProps, childrenProps, referenceElement }) => (
        <Overlay
          layers={[
            <Overlay.Layer
              key="popover"
              visible={visible}
              placement={placement}
              placementOffset={placementOffset}
              animationScaleFrom={animationScaleFrom}
              destroyOnClose={destroyOnClose}
              referenceElement={props.referenceElement || referenceElement}
              content={overlay}
              overlayRef={overlayElement => {
                overlayRef.current = overlayElement;
              }}
              overlayProps={{
                ...overlayProps,
                className: classNames(overlayProps.className, overlayClassName),
                style: mergeStyle(
                  {
                    // 已经隐藏之后，动画过程不响应事件
                    pointerEvents: visible ? null : "none",
                  },
                  overlayProps.style,
                  overlayStyle
                ),
              }}
              updateOnDimensionChange={updateOnDimensionChange}
              transitionTimeout={transitionTimeout}
              escapeWithReference={escapeWithReference}
              popupContainer={popupContainer}
              onExited={onExited}
              positionFixed={positionFixed}
              flipped={flipped}
            />,
          ]}
        >
          {ref => (
            <DomRef ref={mergeRefs(ref, childrenRef)}>
              {getChildren(childrenProps)}
            </DomRef>
          )}
        </Overlay>
      )}
    />
  );
}

Popover.displayName = "Popover";
