<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'

const props = defineProps({
  end: { type: Number, required: true },
  duration: { type: Number, default: 900 },
  prefix: { type: String, default: '' },
  suffix: { type: String, default: '' },
  decimals: { type: Number, default: 0 },
})

const el = ref(null)
const current = ref(0)
const started = ref(false)

let rafId = null
let observer = null

const prefersReducedMotion = () =>
  typeof window !== 'undefined' &&
  window.matchMedia &&
  window.matchMedia('(prefers-reduced-motion: reduce)').matches

function formatNumber(value) {
  const fixed = Number(value).toFixed(props.decimals)
  const numberValue = Number(fixed)
  return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: props.decimals }).format(numberValue)
}

const text = computed(() => `${props.prefix}${formatNumber(current.value)}${props.suffix}`)

function start() {
  if (started.value) return
  started.value = true

  if (prefersReducedMotion()) {
    current.value = props.end
    return
  }

  const startTs = performance.now()
  const from = 0
  const to = props.end

  const tick = (ts) => {
    const t = Math.min(1, (ts - startTs) / props.duration)
    const eased = 1 - Math.pow(1 - t, 3)
    current.value = from + (to - from) * eased
    if (t < 1) rafId = requestAnimationFrame(tick)
  }

  rafId = requestAnimationFrame(tick)
}

onMounted(() => {
  if (typeof window === 'undefined') return
  if (!('IntersectionObserver' in window)) {
    start()
    return
  }

  observer = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue
        start()
        if (observer) observer.disconnect()
      }
    },
    { threshold: 0.5 },
  )

  if (el.value) observer.observe(el.value)
})

onBeforeUnmount(() => {
  if (observer) observer.disconnect()
  if (rafId) cancelAnimationFrame(rafId)
})
</script>

<template>
  <span ref="el">{{ text }}</span>
</template>

