<script setup lang="ts">
import { Icon } from '@iconify/vue'

const colorMode = useColorMode()
const buttonRef = ref<HTMLButtonElement | null>(null)
const isAnimating = ref(false)
const isMounted = ref(false)

type Mode = 'system' | 'dark' | 'light'
type ViewTransition = {
  ready: Promise<void>
  finished: Promise<void>
  updateCallbackDone: Promise<void>
}

type DocumentWithViewTransition = Document & {
  startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition
}

const iconMap: Record<Mode, string> = {
  system: 'mdi:monitor',
  dark: 'mdi:weather-night',
  light: 'mdi:weather-sunny',
}

const labelMap: Record<Mode, string> = {
  system: '跟随系统',
  dark: '深色模式',
  light: '浅色模式',
}

const cycleOrder = computed<Mode[]>(() => {
  const isDark = colorMode.value === 'dark'
  return isDark ? ['system', 'light', 'dark'] : ['system', 'dark', 'light']
})

const currentMode = computed(() => colorMode.preference as Mode)
const displayMode = computed<Mode>(() => isMounted.value ? currentMode.value : 'system')
const currentIcon = computed(() => iconMap[displayMode.value] || iconMap.system)
const currentLabel = computed(() => labelMap[displayMode.value] || labelMap.system)
const resolvedMode = computed(() => colorMode.value)
const displayResolvedMode = computed(() => isMounted.value ? resolvedMode.value : 'light')

onMounted(() => {
  isMounted.value = true
})

function getNextMode() {
  const order = cycleOrder.value
  const idx = order.indexOf(currentMode.value)
  return order[(idx + 1) % order.length]
}

function getTransitionOrigin() {
  const rect = buttonRef.value?.getBoundingClientRect()
  if (!rect) {
    return {
      x: window.innerWidth / 2,
      y: window.innerHeight / 2,
    }
  }

  return {
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
  }
}

function getMaxRadius(x: number, y: number) {
  return Math.hypot(
    Math.max(x, window.innerWidth - x),
    Math.max(y, window.innerHeight - y)
  )
}

function wait(ms: number) {
  return new Promise(resolve => window.setTimeout(resolve, ms))
}

async function cycle() {
  if (isAnimating.value) return

  const nextMode = getNextMode()

  if (!import.meta.client) {
    colorMode.preference = nextMode
    return
  }

  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const documentWithTransition = document as DocumentWithViewTransition

  if (prefersReducedMotion || !documentWithTransition.startViewTransition) {
    colorMode.preference = nextMode
    return
  }

  const { x, y } = getTransitionOrigin()
  const endRadius = getMaxRadius(x, y)

  isAnimating.value = true
  const fallbackTimer = window.setTimeout(() => {
    isAnimating.value = false
  }, 900)

  const applyMode = async () => {
    colorMode.preference = nextMode
    await nextTick()
  }

  let transition: ViewTransition

  try {
    transition = documentWithTransition.startViewTransition(applyMode)
  } catch {
    await applyMode()
    window.clearTimeout(fallbackTimer)
    isAnimating.value = false
    return
  }

  try {
    await transition.ready

    const animation = document.documentElement.animate(
      {
        clipPath: [
          `circle(0px at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 620,
        easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
        pseudoElement: '::view-transition-new(root)',
      } as KeyframeAnimationOptions
    )

    await Promise.race([
      animation.finished.catch(() => undefined),
      wait(700),
    ])
    void transition.finished.catch(() => undefined)
  } catch {
    await transition.updateCallbackDone.catch(applyMode)
  } finally {
    window.clearTimeout(fallbackTimer)
    isAnimating.value = false
  }
}
</script>

<template>
  <button
    ref="buttonRef"
    @click="cycle"
    class="theme-toggle inline-flex items-center justify-center rounded-md w-8 h-8 transition-colors"
    style="color: var(--color-text-secondary)"
    :class="[
      'hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text)]',
    ]"
    :disabled="isAnimating"
    :aria-label="currentLabel"
    :aria-pressed="displayResolvedMode === 'dark'"
    :title="currentLabel"
  >
    <span
      class="theme-toggle-icon"
      :class="{ 'is-dark': displayResolvedMode === 'dark' }"
    >
      <Icon
        :key="currentIcon"
        :icon="currentIcon"
        class="w-5 h-5"
      />
    </span>
  </button>
</template>

<style scoped>
.theme-toggle {
  overflow: hidden;
  isolation: isolate;
}

.theme-toggle:disabled {
  cursor: wait;
}

.theme-toggle-icon {
  display: grid;
  place-items: center;
  transition:
    transform 0.28s ease,
    opacity 0.2s ease;
}

.theme-toggle-icon.is-dark {
  transform: rotate(-18deg) scale(0.96);
}

.theme-toggle-icon :deep(svg) {
  animation: theme-icon-pop 0.28s ease;
}

@keyframes theme-icon-pop {
  from {
    opacity: 0;
    transform: rotate(-45deg) scale(0.72);
  }

  to {
    opacity: 1;
    transform: rotate(0deg) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle-icon,
  .theme-toggle-icon :deep(svg) {
    animation: none;
    transition: none;
  }
}
</style>
