<template>
  <span
    ref="triggerRef"
    class="inline-flex max-w-full min-w-0 align-middle outline-none"
    tabindex="0"
    @mouseenter="showTip"
    @mouseleave="hideTip"
    @focus="showTip"
    @blur="hideTip"
  >
    <slot></slot>
  </span>

  <Teleport to="body">
    <div
      v-if="visible"
      ref="tipRef"
      class="fixed z-[9999] max-w-[min(38rem,calc(100vw-2rem))] px-[1.5rem] py-[1rem] text-[#D9F7FF] text-[2rem] leading-[1.25] break-all pointer-events-none border border-[rgba(132,232,255,0.55)] rounded-[0.75rem] bg-[linear-gradient(180deg,rgba(10,58,132,0.96)_0%,rgba(7,35,98,0.98)_100%)] shadow-[0_0_1.5rem_rgba(68,193,239,0.28),inset_0_0_1rem_rgba(68,193,239,0.14)] transition-opacity duration-150 ease-out"
      :class="positioned ? 'opacity-100' : 'opacity-0'"
      :style="tipStyle"
      role="tooltip"
    >
      <span class="block">{{ text }}</span>
      <span
        class="absolute h-[0.85rem] w-[0.85rem] rotate-45 border-[rgba(132,232,255,0.55)] border-solid bg-[rgba(7,35,98,0.98)]"
        :class="arrowPlacementClass"
      ></span>
    </div>
  </Teleport>
</template>

<script lang="ts" setup>
  import { computed, nextTick, onBeforeUnmount, reactive, ref } from "vue";

  type TooltipPosition = "top" | "bottom" | "left" | "right";

  const props = withDefaults(
    defineProps<{
      text: string;
      position?: TooltipPosition;
      delay?: number;
    }>(),
    {
      position: "top",
      delay: 160,
    },
  );

  const triggerRef = ref<HTMLElement | null>(null);
  const tipRef = ref<HTMLElement | null>(null);
  const visible = ref(false);
  const positioned = ref(false);
  const isListening = ref(false);
  const timer = ref<number | null>(null);
  const revealFrame = ref<number | null>(null);

  const gap = 10;
  const viewportPadding = 8;
  const tipStyle = reactive({
    top: "0px",
    left: "0px",
  });

  const arrowPlacementClass = computed(() => {
    const positions: Record<TooltipPosition, string> = {
      top: "bottom-[-0.48rem] left-1/2 translate-x-[-50%] border-r border-b",
      bottom: "top-[-0.48rem] left-1/2 translate-x-[-50%] border-t border-l",
      left: "right-[-0.48rem] top-1/2 translate-y-[-50%] border-t border-r",
      right: "left-[-0.48rem] top-1/2 translate-y-[-50%] border-b border-l",
    };

    return positions[props.position];
  });

  const clearTimer = () => {
    if (timer.value) {
      window.clearTimeout(timer.value);
      timer.value = null;
    }
  };

  const clearRevealFrame = () => {
    if (revealFrame.value) {
      window.cancelAnimationFrame(revealFrame.value);
      revealFrame.value = null;
    }
  };

  const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

  const addPositionListeners = () => {
    if (isListening.value) return;
    window.addEventListener("scroll", calculatePosition, true);
    window.addEventListener("resize", calculatePosition);
    isListening.value = true;
  };

  const removePositionListeners = () => {
    if (!isListening.value) return;
    window.removeEventListener("scroll", calculatePosition, true);
    window.removeEventListener("resize", calculatePosition);
    isListening.value = false;
  };

  const showTip = () => {
    if (!props.text) return;

    if (visible.value) {
      calculatePosition();
      positioned.value = true;
      return;
    }

    clearTimer();
    timer.value = window.setTimeout(async () => {
      timer.value = null;
      positioned.value = false;
      visible.value = true;
      await nextTick();
      calculatePosition();
      revealFrame.value = window.requestAnimationFrame(() => {
        positioned.value = true;
        revealFrame.value = null;
      });
      addPositionListeners();
    }, props.delay);
  };

  const hideTip = () => {
    clearTimer();
    clearRevealFrame();
    positioned.value = false;
    visible.value = false;
    removePositionListeners();
  };

  const calculatePosition = () => {
    if (!triggerRef.value || !tipRef.value) return;

    const triggerRect = triggerRef.value.getBoundingClientRect();
    const tipRect = tipRef.value.getBoundingClientRect();
    let top = triggerRect.top - tipRect.height - gap;
    let left = triggerRect.left + (triggerRect.width - tipRect.width) / 2;

    switch (props.position) {
      case "bottom":
        top = triggerRect.bottom + gap;
        left = triggerRect.left + (triggerRect.width - tipRect.width) / 2;
        break;
      case "left":
        top = triggerRect.top + (triggerRect.height - tipRect.height) / 2;
        left = triggerRect.left - tipRect.width - gap;
        break;
      case "right":
        top = triggerRect.top + (triggerRect.height - tipRect.height) / 2;
        left = triggerRect.right + gap;
        break;
      case "top":
      default:
        break;
    }

    tipStyle.top = `${clamp(top, viewportPadding, window.innerHeight - tipRect.height - viewportPadding)}px`;
    tipStyle.left = `${clamp(left, viewportPadding, window.innerWidth - tipRect.width - viewportPadding)}px`;
  };

  onBeforeUnmount(() => {
    hideTip();
  });
</script>
