<template>
  <teleport v-if="baseData.bubbleDestroy" to="body" :disabled="disabledTeleport">
    <transition name="el-fade-in-linear">
      <div v-if="showMask && bubbleVisible" :style="{ zIndex }" class="el-overlay"></div>
    </transition>
    <transition v-if="placement === 'center' || !baseData.enoughShow" key="transition-1" name="el-zoom-in-center">
      <div v-if="bubbleVisible" ref="bubbleBaseBubbleVesselRef" class="bubble-dialog" tabindex="-1" :style="centerBubbleStyle">
        <slot :padding="padding"></slot>
      </div>
    </transition>
    <transition v-else key="transition-2" :name="baseData.transition">
      <template v-if="bubbleVisible">
        <div ref="bubbleBaseBubbleVesselRef" class="bubble-base-bubble" tabindex="-1" :style="bubbleStyle">
          <span
            v-if="!props.transparentBackground"
            class="bubble-arrows"
            :class="baseData.arrowsOrientation"
            :style="{ width: `${baseData.arrowsSize}px`, height: `${baseData.arrowsSize}px`, top: baseData.arrowsTop, left: baseData.arrowsLeft }"
          ></span>
          <div class="bubble-view" :style="{ width: `${width}px`, height: `${height}px` }">
            <slot :padding="padding"></slot>
          </div>
        </div>
      </template>
    </transition>
  </teleport>
</template>

<script setup lang="ts">
  import type { $Placement, $baseData, $bubbleLocation } from './bubble-base-bubble';
  import { useThrottleFn } from '@vueuse/core';
  import { type CSSProperties, computed, inject, nextTick, provide, ref, shallowReactive, shallowRef, watch } from 'vue';

  const props = defineProps({
    /** 是否禁止传送到body */
    disabledTeleport: { type: Boolean, default: false },
    /** 点击tab键是否关闭并切换到下一个弹出框 */
    tabClose: { type: Boolean, default: true },
    /** 弹出框两边的padding */
    padding: { type: Number, default: 0 },
    /** 弹出框的zIndex层级 */
    zIndex: { type: Number, default: 3000 },
    /** 弹出框是否自动关闭 */
    autoClose: { type: Boolean, default: true },
    /** 弹出框主体内容的高宽度 (不含padding) */
    width: { type: Number, default: 300 },
    /** 弹出框主体内容的高度 (不含padding) */
    height: { type: Number, default: 400 },
    /** 弹出框出现的位置 */
    placement: { type: String as PropType<$Placement | 'auto'>, default: 'auto' },
    /** 是否为透明背景 */
    transparentBackground: { type: Boolean, default: false },
    /** 是否显示遮罩 */
    showMask: { type: Boolean, default: false },
    /** 强制设置弹出框的left */
    left: { type: String, default: undefined },
    /** 强制设置弹出框的top */
    top: { type: String, default: undefined },
  });

  defineSlots<{
    /** 主体内容 */
    default: (data: { padding: number }) => any;
  }>();

  const emit = defineEmits<{
    (e: 'change', state: boolean): void;
    (e: 'keydownEvent', event: KeyboardEvent): void;
  }>();

  /** 修改父级组件中孩子组件弹出框状态 (可能有嵌套) */
  const editChildBubbleVisibleFun = inject<false | ((state: boolean) => unknown)>('edit-child-bubble-state', false);

  const bubbleBaseBubbleVesselRef = ref<HTMLDivElement | null>(null);

  const baseData = shallowReactive<$baseData>({
    transition: '',
    enoughShow: true,
    target: undefined as $BubbleHTMLElement | undefined,
    bubbleTop: '0px',
    bubbleLeft: '0px',
    arrowsSize: 10,
    arrowsTop: '0px',
    arrowsLeft: '0px',
    arrowsOrientation: 'top',
    bubbleDestroy: false,
    bubbleDestroyTimeout: undefined as NodeJS.Timeout | undefined,
  });

  /** 弹出框的显示状态 */
  const bubbleVisible = ref(false);

  /** 最终弹出框的宽度 (props.width + 两边的padding + 两边border) */
  const newWidth = computed(() => `${props.width + props.padding * 2 + 2}px`);

  /** 最终弹出框的高度 (props.height + 两边的padding + 两边border) */
  const newHeight = computed(() => `${props.height + props.padding * 2 + 2}px`);

  const userBubbleStyle = ref<CSSProperties>({});
  const bubbleStyle = computed<CSSProperties>(() => {
    const style: CSSProperties = {
      zIndex: props.zIndex,
      top: props.top || baseData.bubbleTop,
      left: props.left || baseData.bubbleLeft,
      width: newWidth.value,
      height: newHeight.value,
      padding: `${props.padding}px`,
    };

    if (props.transparentBackground) {
      style.background = 'rgba(0,0,0,0)';
      style.border = 'none';
      style.borderRadius = '0px';
      style.boxShadow = 'none';
    }

    return { ...style, ...userBubbleStyle.value };
  });

  const centerBubbleStyle = computed<CSSProperties>(() => {
    const style: CSSProperties = {
      zIndex: props.zIndex,
      width: newWidth.value,
      height: newHeight.value,
      padding: `${props.padding}px`,
      left: `calc((100vw - ${newWidth.value}) / 2)`,
      top: `calc((100vh - ${newHeight.value}) / 2)`,
    };

    if (props.transparentBackground) {
      style.background = 'rgba(0,0,0,0)';
      style.border = 'none';
      style.borderRadius = '0px';
      style.boxShadow = 'none';
    }
    return style;
  });

  const debouncedFn = useThrottleFn(() => getBubblePosition, 50);
  const myObserver = new ResizeObserver(debouncedFn);

  watch(
    () => bubbleVisible.value,
    (state) => {
      editChildBubbleVisibleFun && editChildBubbleVisibleFun(state);
      baseData.bubbleDestroyTimeout && clearTimeout(baseData.bubbleDestroyTimeout);
      if (state) {
        baseData.bubbleDestroy = true;
        document.addEventListener('keydown', documentKeydownEvent);
        props.autoClose && document.addEventListener('mousedown', documentMousedownEvent);
      } else {
        document.removeEventListener('keydown', documentKeydownEvent);
        props.autoClose && document.removeEventListener('mousedown', documentMousedownEvent);
        baseData.bubbleDestroyTimeout = setTimeout(() => {
          baseData.bubbleDestroyTimeout = undefined;
          baseData.bubbleDestroy = false;
        }, 310);
      }
      // 用于直接设置 <box-table-select>、<box-select-input> 等组件的箭头旋转 [bubbleVisible是在<box-table-select>、<box-select-input>定义的]
      if (baseData?.target?.bubbleVisible) {
        baseData.target.bubbleVisible.value = state;
      }
      emit('change', state);
    }
  );

  watch(() => [props.height, props.width], getBubblePosition);
  watch(bubbleVisible, () => {
    if (baseData.target && bubbleVisible.value) {
      nextTick(() => {
        window.removeEventListener('resize', debouncedFn);
        window.addEventListener('resize', debouncedFn);
        myObserver.disconnect();
        baseData.target && myObserver.observe(baseData.target as Element);
      });
    } else {
      window.removeEventListener('resize', debouncedFn);
      myObserver.disconnect();
    }
  });

  /**
   *得到Bubble弹出位置
   */
  function getBubblePosition() {
    if (baseData.target) {
      const { enoughShow, transition, bubbleTop, bubbleLeft, arrowsTop, arrowsLeft, arrowsOrientation } = getPlacement(baseData.target);
      baseData.arrowsOrientation = arrowsOrientation;
      baseData.transition = transition;
      baseData.bubbleTop = bubbleTop;
      baseData.bubbleLeft = bubbleLeft;
      baseData.arrowsTop = arrowsTop;
      baseData.arrowsLeft = arrowsLeft;
      baseData.enoughShow = enoughShow;
    }
  }

  /**
   * 设置弹出框的宽高和padding
   */
  function setBubbleStyle(style: CSSProperties) {
    userBubbleStyle.value = style;
  }

  /**
   *得到布局
   */
  function getPlacement(target: $BubbleHTMLElement): $bubbleLocation {
    const margin = 12; // 箭头距离触发元素的距离
    const mar = 12; // 距离屏幕边缘的最小距离
    const arrowsRadius = baseData.arrowsSize / 2; // 半径

    // 屏幕宽高
    const pageWidth = document.body.clientWidth;
    const pageHeight = document.body.clientHeight;
    // 点击dom宽高
    const targetWidth = target.offsetWidth;
    const targetHeight = target.offsetHeight;
    // 点击dom右上角的坐标
    const targeX = target.getBoundingClientRect().left;
    const targeY = target.getBoundingClientRect().top;
    // 点击dom中心点坐标
    const centerX = targeX + Math.trunc(targetWidth / 2);
    const centerY = targeY + Math.trunc(targetHeight / 2);
    // 弹出框的宽高
    const bubbleWidth = props.width + props.padding * 2;
    const bubbleHeight = props.height + props.padding * 2;

    // 上面足够
    const topEnough = Boolean(bubbleHeight + margin + mar < targeY);
    // 下面足够
    const bottomEnough = Boolean(targeY + targetHeight + bubbleHeight + margin + mar < pageHeight);
    // 左面足够
    const leftEnough = Boolean(targeX - bubbleWidth - margin - mar >= 0);
    // 右面足够
    const rightEnough = Boolean(targeX + targetWidth + bubbleWidth + margin + mar < pageWidth);
    // 屏幕宽度足够
    const widthEnough = Boolean(pageWidth > bubbleWidth + mar * 2);
    // 屏幕高度足够
    const heightEnough = Boolean(pageHeight > bubbleHeight + mar * 2);
    if (['auto', 'top'].includes(props.placement) && topEnough && widthEnough) {
      return getTopBottomPlacement('top');
    } else if (['auto', 'bottom'].includes(props.placement) && bottomEnough && widthEnough) {
      return getTopBottomPlacement('bottom');
    } else if (['auto', 'left'].includes(props.placement) && leftEnough && heightEnough) {
      return getLeftRightPlacement('left');
    } else if (['auto', 'right'].includes(props.placement) && rightEnough && heightEnough) {
      return getLeftRightPlacement('right');
    } else {
      const bubbleTop = Math.trunc(centerY + targetHeight / 2 + margin);
      return {
        enoughShow: false,
        transition: 'el-fade-in-linear',
        arrowsOrientation: 'top',
        bubbleLeft: '0px',
        bubbleTop: `${bubbleTop}px`,
        arrowsTop: '0px',
        arrowsLeft: '0px',
      };
    }

    /** 上或者下足够并且宽度也足够 */
    function getTopBottomPlacement(placement: $Placement): $bubbleLocation {
      const bubbleTop = Math.trunc(placement === 'bottom' ? targeY + targetHeight + margin : targeY - bubbleHeight - margin);
      const rightX = Math.trunc(centerX - bubbleWidth / 2);
      const leftX = Math.trunc(centerX + bubbleWidth / 2);
      const transition = placement === 'bottom' ? 'el-zoom-in-top' : 'el-zoom-in-bottom';
      let bubbleLeft = mar;

      if (rightX >= mar && pageWidth - leftX >= mar) {
        bubbleLeft = rightX;
      } else if (pageWidth - leftX >= mar) {
        bubbleLeft = mar;
      } else if (rightX >= mar) {
        bubbleLeft = pageWidth - bubbleWidth - mar;
      }
      const arrowsTop = placement === 'bottom' ? -arrowsRadius : bubbleHeight - arrowsRadius;
      const arrowsLeft = centerX - bubbleLeft - arrowsRadius;
      return {
        enoughShow: true,
        transition,
        arrowsOrientation: placement,
        bubbleTop: `${bubbleTop}px`,
        bubbleLeft: `${bubbleLeft}px`,
        arrowsTop: `${arrowsTop}px`,
        arrowsLeft: `${arrowsLeft}px`,
      };
    }

    /** 左或者右足够并且高度也足够 */
    function getLeftRightPlacement(placement: $Placement): $bubbleLocation {
      const bubbleLeft = Math.trunc(placement === 'left' ? targeX - bubbleWidth - margin : targeX + targetWidth + margin);
      const topY = Math.trunc(centerY - bubbleHeight / 2);
      const bottomY = Math.trunc(centerY + bubbleHeight / 2);
      const transition = placement === 'left' ? 'my-zoom-in-left' : 'my-zoom-in-right';
      let bubbleTop = mar;
      if (topY >= mar && pageHeight - bottomY >= mar) {
        // 水平居中
        bubbleTop = topY;
      } else if (pageHeight - bottomY >= mar) {
        bubbleTop = mar;
      } else {
        bubbleTop = pageHeight - bubbleHeight - mar;
      }
      const arrowsTop = centerY - bubbleTop - arrowsRadius;
      const arrowsLeft = placement === 'left' ? bubbleWidth - arrowsRadius : -arrowsRadius;
      return {
        enoughShow: true,
        transition,
        arrowsOrientation: placement,
        bubbleTop: `${bubbleTop}px`,
        bubbleLeft: `${bubbleLeft}px`,
        arrowsTop: `${arrowsTop}px`,
        arrowsLeft: `${arrowsLeft}px`,
      };
    }
  }

  // 子级组件弹出框状态(打开还是关闭)
  const childBubbleVisible = shallowRef(false);

  /** 用于修改子级组件弹出框状态 (向后代传递) */
  provide('edit-child-bubble-state', editChildBubbleVisible);

  /**
   * 修改子级组件弹出框状态
   */
  function editChildBubbleVisible(state: boolean) {
    childBubbleVisible.value = state;
  }

  /**
   * 显示
   */
  function show(tarOrEve: $BubbleEvent | $BubbleHTMLElement, isHide = true) {
    const target = tarOrEve instanceof HTMLElement ? tarOrEve : tarOrEve?.myTarget || tarOrEve?.currentTarget || tarOrEve.target || undefined;
    baseData.target = target ? (target as $BubbleHTMLElement) : undefined;
    userBubbleStyle.value = {};
    // 给传过来的target添加一个事件,用于直接在触发元素中关闭弹出框
    baseData.target && (baseData.target.hideBubble = hide);
    const _show = () => {
      getBubblePosition();
      baseData.bubbleDestroy = true;
      setTimeout(() => {
        bubbleVisible.value = true;
      }, 0);
    };
    if (baseData.target !== target) {
      _show();
    } else if (isHide) {
      if (bubbleVisible.value) {
        hide();
      } else {
        _show();
      }
    } else {
      _show();
    }
  }

  /**
   * 隐藏
   */
  function hide() {
    bubbleVisible.value = false;
  }

  /**
   * 鼠标点击事件(用于关闭弹出框)
   * 如果有子组件未关闭或在弹出框内点击就不关闭弹出框
   */
  function documentMousedownEvent(event: Event) {
    if (bubbleVisible.value) {
      const bT = baseData.target as HTMLElement;
      const eT = event.target as HTMLElement;
      const myRef = bubbleBaseBubbleVesselRef.value || false;
      if (!props.autoClose || childBubbleVisible.value || bT.contains(eT) || bT === eT || !myRef || myRef.contains(eT)) {
        return;
      }
      hide();
    }
  }

  /**
   * 键盘按下事件
   */
  function documentKeydownEvent(event: KeyboardEvent) {
    const codeNumb = event.which || event.keyCode || -1;
    const myRef = bubbleBaseBubbleVesselRef.value || undefined;
    const activityDoms = myRef?.querySelectorAll('button,input') || false;
    const lastDom = activityDoms && activityDoms.length > 0 ? activityDoms[activityDoms.length - 1] : false;
    // 如果不允许tab键自动关闭那么当tab键跳转到最后一个时也强制关闭
    const isTabClose = props.tabClose || (!props.tabClose && lastDom === event.target);
    // 用于关闭弹出框
    if ((event.key === 'Escape' || codeNumb === 27) && props.autoClose && !childBubbleVisible.value) {
      hide();
    } else if (isTabClose && (event.key === 'Tab' || codeNumb === 9)) {
      hide();
      const dom: $BubbleHTMLElement | undefined = baseData.target?.parentNode?.querySelector('button,input') || undefined;
      dom && dom.focus();
    } else {
      emit('keydownEvent', event);
    }
  }

  defineExpose({ hide, show, isShow: () => bubbleVisible.value, setBubbleStyle });
</script>

<style lang="scss" scoped src="./bubble-base-bubble.scss"></style>
