<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import gsap from 'gsap'

/**
 * 全站自定义鼠标。
 *
 * 使用方式：
 * - 默认状态是一个圆点，加上横向和纵向的十字线。
 * - 给任意元素添加 `data-cursor-label`，鼠标移入后圆点会变成胶囊提示。
 *
 * 示例：
 * <section data-cursor-label="Read more">
 *   ...
 * </section>
 *
 * 说明：
 * - 这个组件只需要在默认 layout 中挂载一次。
 * - 只在精确指针设备上运行，所以触摸屏会继续使用系统默认鼠标。
 * - 鼠标元素会 Teleport 到 body，避免被页面局部层级或布局包住。
 */

const dotRef = ref<HTMLElement | null>(null)
const horizontalLineRef = ref<HTMLElement | null>(null)
const verticalLineRef = ref<HTMLElement | null>(null)
const pillRef = ref<HTMLElement | null>(null)
const pillTextRef = ref<HTMLElement | null>(null)

let cleanupCursor: (() => void) | null = null
let cursorX = 0
let cursorY = 0
let pillWidth = 100
let isPillVisible = false
let hasCursorPosition = false

// 查找最近的 `data-cursor-label` 元素，用来判断是否需要显示胶囊态。
const getCursorTarget = (target: EventTarget | null) => {
  if (!(target instanceof Element)) {
    return null
  }

  return target.closest<HTMLElement>('[data-cursor-label]')
}

onMounted(() => {
  const finePointer = window.matchMedia('(pointer: fine)')

  // 触摸设备保留系统默认鼠标，不启用自定义鼠标。
  if (!finePointer.matches) {
    return
  }

  const cursorTargets = [
    dotRef.value,
    horizontalLineRef.value,
    verticalLineRef.value,
  ]
  const allTargets = [
    ...cursorTargets,
    pillRef.value,
    pillTextRef.value,
  ]

  document.documentElement.classList.add('has-custom-cursor')

  // 初始隐藏，避免页面刚打开、鼠标还没移动时出现闪烁。
  gsap.set(cursorTargets, {
    autoAlpha: 0,
  })
  gsap.set(pillRef.value, {
    autoAlpha: 0,
    width: 0,
    height: 0,
  })
  gsap.set(pillTextRef.value, {
    autoAlpha: 0,
  })

  // quickTo 会复用 tween，让十字线跟随更轻量，不需要每一帧都创建新动画。
  const moveLineX = gsap.quickTo(verticalLineRef.value, 'x', {
    duration: 0.08,
    ease: 'power3.out',
  })
  const moveLineY = gsap.quickTo(horizontalLineRef.value, 'y', {
    duration: 0.08,
    ease: 'power3.out',
  })

  const movePill = () => {
    // 胶囊提示始终以真实鼠标位置为中心。
    gsap.set(pillRef.value, {
      x: cursorX - pillWidth / 2,
      y: cursorY - 15,
    })
  }

  const showCursor = () => {
    if (!hasCursorPosition) {
      return
    }

    gsap.to([horizontalLineRef.value, verticalLineRef.value], {
      autoAlpha: 1,
      duration: 0.15,
      overwrite: 'auto',
    })

    if (!isPillVisible) {
      gsap.to(dotRef.value, {
        autoAlpha: 1,
        scale: 1,
        duration: 0.15,
        overwrite: 'auto',
      })
    }
  }

  const hideCursor = () => {
    isPillVisible = false
    hasCursorPosition = false

    gsap.to(allTargets, {
      autoAlpha: 0,
      duration: 0.15,
      overwrite: 'auto',
    })
  }

  const showPill = (label: string) => {
    if (!pillRef.value || !pillTextRef.value) {
      return
    }

    pillTextRef.value.textContent = label
    // 短文案保持一个基础宽度，长文案则按内容自然变宽。
    pillWidth = Math.max(100, label.length * 9 + 32)
    isPillVisible = true
    movePill()

    gsap.to(dotRef.value, {
      autoAlpha: 0,
      scale: 0,
      duration: 0.18,
      ease: 'power2.out',
      overwrite: 'auto',
    })
    gsap.to(pillRef.value, {
      autoAlpha: 1,
      width: pillWidth,
      height: 30,
      duration: 0.2,
      ease: 'power3.out',
      overwrite: 'auto',
    })
    gsap.to(pillTextRef.value, {
      autoAlpha: 1,
      delay: 0.08,
      duration: 0.18,
      overwrite: 'auto',
    })
  }

  const hidePill = () => {
    if (!isPillVisible) {
      return
    }

    isPillVisible = false

    gsap.to(pillTextRef.value, {
      autoAlpha: 0,
      duration: 0.12,
      overwrite: 'auto',
    })
    gsap.to(pillRef.value, {
      autoAlpha: 0,
      width: 0,
      height: 0,
      duration: 0.2,
      ease: 'power3.out',
      overwrite: 'auto',
    })
    gsap.to(dotRef.value, {
      autoAlpha: 1,
      scale: 1,
      delay: 0.08,
      duration: 0.18,
      ease: 'power2.out',
      overwrite: 'auto',
    })
  }

  const moveCursor = (event: MouseEvent) => {
    cursorX = event.clientX
    cursorY = event.clientY
    hasCursorPosition = true

    showCursor()
    gsap.set(dotRef.value, {
      x: cursorX - 12,
      y: cursorY - 12,
    })
    movePill()
    moveLineX(cursorX)
    moveLineY(cursorY)
  }

  const handleMouseOver = (event: MouseEvent) => {
    const target = getCursorTarget(event.target)

    if (!target) {
      return
    }

    if (!hasCursorPosition) {
      moveCursor(event)
    }

    showPill(target.dataset.cursorLabel || 'Read more')
  }

  const handleMouseOut = (event: MouseEvent) => {
    const target = getCursorTarget(event.target)
    const nextTarget = getCursorTarget(event.relatedTarget)

    // 在同一个标记区域的子元素之间移动时，不关闭胶囊提示。
    if (!target || nextTarget === target) {
      return
    }

    hidePill()
  }

  window.addEventListener('mousemove', moveCursor)
  window.addEventListener('mouseenter', moveCursor)
  window.addEventListener('mouseleave', hideCursor)
  document.addEventListener('mouseover', handleMouseOver)
  document.addEventListener('mouseout', handleMouseOut)

  cleanupCursor = () => {
    window.removeEventListener('mousemove', moveCursor)
    window.removeEventListener('mouseenter', moveCursor)
    window.removeEventListener('mouseleave', hideCursor)
    document.removeEventListener('mouseover', handleMouseOver)
    document.removeEventListener('mouseout', handleMouseOut)
    document.documentElement.classList.remove('has-custom-cursor')
    gsap.killTweensOf(allTargets)
  }
})

onBeforeUnmount(() => {
  cleanupCursor?.()
  cleanupCursor = null
})
</script>

<template>
  <Teleport to="body">
    <div aria-hidden="true">
      <div ref="dotRef" class="custom-cursor-dot" />
      <div ref="horizontalLineRef" class="custom-cursor-line custom-cursor-line-x" />
      <div ref="verticalLineRef" class="custom-cursor-line custom-cursor-line-y" />
      <div ref="pillRef" class="custom-cursor-pill">
        <div ref="pillTextRef" class="custom-cursor-pill-text">Read more</div>
      </div>
    </div>
  </Teleport>
</template>

<style scoped>
.custom-cursor-dot,
.custom-cursor-line,
.custom-cursor-pill,
.custom-cursor-pill-text {
  opacity: 0;
  visibility: hidden;
}

.custom-cursor-dot,
.custom-cursor-line {
  position: fixed;
  left: 0;
  top: 0;
  pointer-events: none;
  mix-blend-mode: difference;
}

.custom-cursor-dot {
  z-index: 2147483647;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: #fff;
}

.custom-cursor-line {
  z-index: 2147483646;
  background: #888;
}

.custom-cursor-line-x {
  right: 0;
  height: 0.5px;
}

.custom-cursor-line-y {
  width: 0.5px;
  height: 100vh;
}

.custom-cursor-pill {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 2147483647;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 30px;
  color: #fff;
  background: rgb(0 0 0 / 35%);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.custom-cursor-pill-text {
  position: absolute;
  width: 100%;
  overflow-wrap: normal;
  white-space: nowrap;
  text-align: center;
  font-size: 12px;
  line-height: 1;
}

@media (pointer: fine) {
  :global(.has-custom-cursor),
  :global(.has-custom-cursor *) {
    cursor: none !important;
  }
}
</style>
