<template>
  <div class="gallery">
    <!-- 标题区 -->
    <div v-reveal="'up'" class="text-center mb-5 px-4">
      <span class="gallery-title calligraphy text-xl text-[#a88c6b] tracking-[0.3em] font-bold block mb-2">{{ page.title }}</span>
      <span class="gallery-subtitle text-[9px] text-[#8A8680] first-letter:uppercase lowercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
      <span class="gallery-desc text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
    </div>

    <!-- 单图（默认居中卡幅 / 可配置宽度铺满） -->
    <div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
         class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
      <div :class="singleFrameClass">
        <img :src="displayImg1" class="preview-img" @click.stop="previewImage(originalImg1)" />
      </div>
    </div>

    <!-- 错落双图：固定 2 槽；视口 30% / 50% 分步挂载，不停滚 -->
    <div
      v-else-if="page.type === 'overlap'"
      ref="multiRootRef"
      class="overlap-wrap relative w-full mt-2"
    >
      <div
        class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2 overlap-slot"
        :class="{ 'overlap-placeholder': shownCount < 1 }"
      >
        <div
          v-if="shownCount >= 1"
          v-reveal="{ variant: 'left', immediate: true }"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            :src="displayImg1"
            class="preview-img overlap-img"
            decoding="async"
            @click.stop="previewImage(originalImg1)"
          />
        </div>
      </div>
      <div
        class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3 overlap-slot"
        :class="{ 'overlap-placeholder': shownCount < 2 }"
      >
        <div
          v-if="shownCount >= 2"
          v-reveal="{ variant: 'right', immediate: true }"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            :src="displayImg2"
            class="preview-img overlap-img"
            decoding="async"
            @click.stop="previewImage(originalImg2)"
          />
        </div>
      </div>
    </div>

    <!-- 一大五小：固定 6 槽占位 + 串行填内容（大图就绪后再挂小图） -->
    <div
      v-else-if="page.type === 'one-large-five-small'"
      ref="multiRootRef"
      class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
    >
      <div
        class="col-span-2 row-span-2 relative olfs-slot"
        :class="{ 'olfs-placeholder': shownCount < 1 }"
      >
        <div
          v-if="shownCount >= 1"
          v-reveal="{ variant: 'up', immediate: true }"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            ref="olfsLargeImgRef"
            :src="displayAt(0)"
            class="preview-img olfs-img"
            decoding="async"
            @click.stop="previewImage(originalAt(0))"
          />
        </div>
      </div>
      <div
        v-for="i in [1, 2, 3, 4, 5]"
        :key="i"
        class="relative olfs-slot"
        :class="{ 'olfs-placeholder': shownCount <= i }"
      >
        <div
          v-if="shownCount > i"
          v-reveal="{ variant: 'up', immediate: true }"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            :src="displayAt(i)"
            class="preview-img olfs-img"
            decoding="async"
            @click.stop="previewImage(originalAt(i))"
          />
        </div>
      </div>
    </div>

    <!-- 九宫格：近视口 IO 串行挂载（双端不停滚） -->
    <div v-else-if="page.type === 'nine-grid'" ref="multiRootRef" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
      <div
        v-for="(_, imgIdx) in (page.images || [])"
        :key="imgIdx"
        class="aspect-square relative nine-grid-slot"
        :class="{ 'nine-grid-placeholder': imgIdx >= shownCount }"
      >
        <div
          v-if="imgIdx < shownCount && nineGridAnimate"
          v-reveal="getRevealConfig(imgIdx)"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            :src="displayAt(imgIdx)"
            class="nine-grid-img preview-img"
            decoding="async"
            @click.stop="previewImage(originalAt(imgIdx))"
          />
        </div>
        <div
          v-else-if="imgIdx < shownCount"
          class="absolute inset-0"
          :class="frameClass"
        >
          <img
            :src="displayAt(imgIdx)"
            class="nine-grid-img preview-img"
            loading="eager"
            decoding="async"
            @click.stop="previewImage(originalAt(imgIdx))"
          />
        </div>
      </div>
    </div>

    <!-- 瀑布流：固定槽位 + 串行挂载 -->
    <div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
      <div
        v-for="(_, imgIdx) in (page.images || [])"
        :key="imgIdx"
        class="g-masonry-slot"
        :class="{ 'g-masonry-placeholder': imgIdx >= shownCount }"
      >
        <div
          v-if="imgIdx < shownCount"
          v-reveal="{ variant: 'up', immediate: true }"
          class="g-masonry-item"
          :class="frameClass"
        >
          <img :src="displayAt(imgIdx)" class="preview-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
        </div>
      </div>
    </div>

    <!-- 轮播滑动 -->
    <div v-else-if="page.type === 'carousel'" class="relative mt-6">
      <div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
        <div v-for="(_, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
          <img :src="displayAt(imgIdx)" class="preview-img" @click.stop="previewImage(originalAt(imgIdx))" />
        </div>
      </div>
      <button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张">‹</button>
      <button class="g-arrow g-next" @click="goTo(index + 1)" aria-label="下一张">›</button>
      <div class="g-dots">
        <button v-for="(_, imgIdx) in page.images" :key="imgIdx"
                class="g-dot" :class="{ active: imgIdx === index }" @click="goTo(imgIdx)"></button>
      </div>
    </div>

    <!-- 拍立得：固定槽位 + 串行挂载 -->
    <div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
      <div
        v-for="(_, imgIdx) in (page.images || [])"
        :key="imgIdx"
        class="g-polaroid-slot"
        :class="{ 'g-polaroid-placeholder': imgIdx >= shownCount }"
        :style="imgIdx >= shownCount ? { transform: polaroidTransform(imgIdx) } : undefined"
      >
        <div
          v-if="imgIdx < shownCount"
          v-reveal="{ variant: 'scale', immediate: true }"
          class="g-polaroid-item"
          :style="{ transform: polaroidTransform(imgIdx) }"
        >
          <img :src="displayAt(imgIdx)" class="preview-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
        </div>
      </div>
    </div>

    <div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式：{{ page.type }}</div>
  </div>
</template>

<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { borderClass } from '@/composables/borderStyles'
import { createSequentialMount } from '@/composables/useSequentialMount'

const NINE_STEP_MS = 300
const NINE_SETTLE_MS = 400
/** 瀑布/拍立得不停滚，步进略拉长减轻同时 decode */
const MULTI_STEP_MS = 360
const MULTI_SETTLE_MS = 320
/** 错落：视口露出比例分步挂载 + 两图最小间隔 */
const OVERLAP_RATIO_FIRST = 0.3
const OVERLAP_RATIO_SECOND = 0.5
const OVERLAP_MIN_GAP_MS = 520
const OLFS_STEP_MS = 300
const OLFS_SETTLE_MS = 320
const OLFS_DECODE_TIMEOUT_ANDROID_MS = 1200
const OLFS_DECODE_TIMEOUT_IOS_MS = 400

/** 仅用于 OLFS 是否跳过强制 decode */
const isIOSClient = (() => {
  if (typeof navigator === 'undefined') return false
  const ua = navigator.userAgent || ''
  const platform = navigator.platform || ''
  if (/iPhone|iPad|iPod/i.test(ua)) return true
  if (platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1) return true
  return false
})()

/** 双端均不停滚：近视口 IO 串行挂载 */
const SEQUENTIAL_SELF_TYPES = new Set([
  'masonry',
  'polaroid',
  'nine-grid',
  'one-large-five-small',
])

const props = defineProps({
  page: { type: Object, required: true },
  /** 九宫格：开启串行 v-reveal；关闭则一次展示 */
  nineGridAnimate: { type: Boolean, default: false },
})
const emit = defineEmits(['preview-image', 'ready'])

const shownCount = ref(0)
const multiRootRef = ref(null)
const olfsLargeImgRef = ref(null)
let readyEmitted = false
let preloadStarted = false
let multiObserver = null
let multiStarted = false
let olfsToken = 0
let overlapToken = 0
let overlapFirstAt = 0
let overlapSecondTimer = null

const seq = createSequentialMount()
const frameClass = computed(() => borderClass(props.page.borderStyle))

/** 展示：缩放图优先；预览：原图优先 */
const pickDisplay = (original, resized) => resized || original || ''
const pickOriginal = (original, resized) => original || resized || ''
const displayImg1 = computed(() => pickDisplay(props.page?.img1, props.page?.img1Resized))
const displayImg2 = computed(() => pickDisplay(props.page?.img2, props.page?.img2Resized))
const originalImg1 = computed(() => pickOriginal(props.page?.img1, props.page?.img1Resized))
const originalImg2 = computed(() => pickOriginal(props.page?.img2, props.page?.img2Resized))
const displayAt = (i) => pickDisplay(props.page?.images?.[i], props.page?.imagesResized?.[i])
const originalAt = (i) => pickOriginal(props.page?.images?.[i], props.page?.imagesResized?.[i])

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

const clearOverlapSecondTimer = () => {
  clearTimeout(overlapSecondTimer)
  overlapSecondTimer = null
}

/** iOS 跳过 img.decode（手动滚动时强制 decode 易触发内存压力）；仅等 load/complete */
const waitImgReady = (img, timeoutMs) => {
  if (!img) return Promise.resolve()
  const waitLoad = () => {
    if (img.complete) return Promise.resolve()
    return new Promise((resolve) => {
      img.addEventListener('load', resolve, { once: true })
      img.addEventListener('error', resolve, { once: true })
    })
  }
  const ready = isIOSClient
    ? waitLoad()
    : (typeof img.decode === 'function' ? img.decode().catch(() => {}) : waitLoad())
  return Promise.race([
    ready,
    new Promise((resolve) => setTimeout(resolve, timeoutMs)),
  ])
}

const emitReady = () => {
  if (readyEmitted) return
  readyEmitted = true
  emit('ready')
}

/** 关闭九宫格动画时提前暖缓存（优先展示用缩放图） */
const preloadNineGridImages = () => {
  if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
  const urls = (props.page.images || [])
    .map((_, i) => displayAt(i))
    .filter(Boolean)
  if (!urls.length) return
  preloadStarted = true
  for (const url of urls) {
    const img = new Image()
    img.decoding = 'async'
    img.src = url
  }
}

/** 注：曾尝试给安卓在 onMounted 阶段后台预热所有图，
 *  实测反而更卡顿（下载集中在画廊接近视口时）。已移除。
 *  若需预热，应只针对单张且错开更长时间间隔，且仅在用户即将到达时触发。 */

const runNineSequential = async () => {
  const total = (props.page.images || []).length
  const { cancelled } = await seq.run({
    total,
    stepMs: NINE_STEP_MS,
    settleMs: NINE_SETTLE_MS,
    setCount: (n) => { shownCount.value = n },
  })
  if (!cancelled) emitReady()
}

const showNineInstant = async () => {
  shownCount.value = (props.page.images || []).length
  await nextTick()
  emitReady()
}

/** 一大五小：先大图并等就绪，再慢挂小图 */
const runOlfsSequential = async () => {
  const token = ++olfsToken
  const total = Math.min(6, (props.page.images || []).length)
  if (!total) {
    emitReady()
    return
  }

  shownCount.value = 0
  await nextTick()
  if (token !== olfsToken) return

  await sleep(OLFS_STEP_MS)
  if (token !== olfsToken) return
  shownCount.value = 1
  await nextTick()
  const timeout = isIOSClient ? OLFS_DECODE_TIMEOUT_IOS_MS : OLFS_DECODE_TIMEOUT_ANDROID_MS
  await waitImgReady(olfsLargeImgRef.value, timeout)
  if (token !== olfsToken) return

  for (let n = 2; n <= total; n += 1) {
    await sleep(OLFS_STEP_MS)
    if (token !== olfsToken) return
    shownCount.value = n
    await nextTick()
  }

  if (token !== olfsToken) return
  await sleep(OLFS_SETTLE_MS)
  emitReady()
}

const runMultiSequential = async () => {
  const total = (props.page.images || []).length
  const { cancelled } = await seq.run({
    total,
    stepMs: MULTI_STEP_MS,
    settleMs: MULTI_SETTLE_MS,
    setCount: (n) => { shownCount.value = n },
  })
  if (!cancelled) emitReady()
}

const unbindMultiObserver = () => {
  multiObserver?.disconnect()
  multiObserver = null
}

const showOverlapSecond = (token) => {
  if (token !== overlapToken) return
  if (shownCount.value >= 2) return
  shownCount.value = 2
  clearOverlapSecondTimer()
  unbindMultiObserver()
  emitReady()
}

const scheduleOverlapSecond = (token) => {
  if (token !== overlapToken || shownCount.value >= 2 || overlapSecondTimer) return
  const elapsed = overlapFirstAt ? performance.now() - overlapFirstAt : 0
  const wait = Math.max(0, OVERLAP_MIN_GAP_MS - elapsed)
  overlapSecondTimer = setTimeout(() => showOverlapSecond(token), wait)
}

/** 错落双图：不停滚；露出 30% 挂第一张，50% 挂第二张（并保证最小间隔） */
const bindOverlapObserver = async () => {
  unbindMultiObserver()
  clearOverlapSecondTimer()
  const token = ++overlapToken
  overlapFirstAt = 0
  await nextTick()
  const el = multiRootRef.value
  if (!el || typeof IntersectionObserver === 'undefined') {
    shownCount.value = 2
    emitReady()
    return
  }
  multiObserver = new IntersectionObserver(
    (entries) => {
      if (token !== overlapToken) return
      for (const entry of entries) {
        const ratio = entry.intersectionRatio
        if (ratio >= OVERLAP_RATIO_FIRST && shownCount.value < 1) {
          shownCount.value = 1
          overlapFirstAt = performance.now()
        }
        if (ratio >= OVERLAP_RATIO_SECOND && shownCount.value >= 1) {
          scheduleOverlapSecond(token)
        }
      }
    },
    { threshold: [0, OVERLAP_RATIO_FIRST, OVERLAP_RATIO_SECOND, 0.75, 1] }
  )
  multiObserver.observe(el)
}

/** 按 page.type 调度对应的串行挂载函数 */
const runSequentialByType = async () => {
  const type = props.page?.type
  if (type === 'nine-grid') {
    if (props.nineGridAnimate) await runNineSequential()
    else await showNineInstant()
    return
  }
  if (type === 'one-large-five-small') {
    await runOlfsSequential()
    return
  }
  if (type === 'masonry' || type === 'polaroid') {
    await runMultiSequential()
  }
}

const startSequentialMount = async () => {
  if (multiStarted || readyEmitted) return
  multiStarted = true
  await runSequentialByType()
}

const bindMultiObserver = async () => {
  unbindMultiObserver()
  if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return
  await nextTick()
  const el = multiRootRef.value
  if (!el || typeof IntersectionObserver === 'undefined') {
    await startSequentialMount()
    return
  }
  // 近视口再串行，避免页面加载即全量 decode（不停滚）
  multiObserver = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue
        multiObserver?.disconnect()
        multiObserver = null
        startSequentialMount()
        break
      }
    },
    { threshold: [0, 0.1, 0.2, 0.3, 0.5], rootMargin: '0px 0px -10% 0px' }
  )
  multiObserver.observe(el)
}

const bindByType = async () => {
  const type = props.page?.type
  if (type === 'overlap') {
    await bindOverlapObserver()
    return
  }
  if (SEQUENTIAL_SELF_TYPES.has(type)) {
    await bindMultiObserver()
  }
}

watch(
  () => [
    props.page?.type,
    props.page?.images,
    props.page?.imagesResized,
    props.page?.img1,
    props.page?.img2,
    props.page?.img1Resized,
    props.page?.img2Resized,
  ],
  async () => {
    seq.cancel()
    olfsToken += 1
    overlapToken += 1
    clearOverlapSecondTimer()
    unbindMultiObserver()
    multiStarted = false
    shownCount.value = 0
    readyEmitted = false
    preloadStarted = false
    overlapFirstAt = 0
    if (props.page?.type === 'nine-grid' && !props.nineGridAnimate) {
      preloadNineGridImages()
    }
    await bindByType()
  },
  { deep: true, flush: 'post' }
)

watch(
  () => props.nineGridAnimate,
  () => {
    if (!props.nineGridAnimate) preloadNineGridImages()
  }
)

onMounted(() => {
  if (!props.nineGridAnimate) preloadNineGridImages()
  bindByType()
})

onUnmounted(() => {
  seq.cancel()
  olfsToken += 1
  overlapToken += 1
  clearOverlapSecondTimer()
  unbindMultiObserver()
})

function getRevealConfig(imgIdx) {
  const row = Math.floor(imgIdx / 3)
  const col = imgIdx % 3

  if (imgIdx === 4) {
    return { variant: 'scale', delay: 0, immediate: true }
  }
  if (col === 1) {
    return {
      variant: row === 0 ? 'down' : 'up',
      delay: 0,
      immediate: true,
    }
  }
  return {
    variant: col === 0 ? 'left' : 'right',
    delay: 0,
    immediate: true,
  }
}

const singleWrapClass = computed(() =>
  props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
)
const singleWrapStyle = computed(() => {
  if (props.page.singleWidth === 'full') {
    const bleed = { width: 'calc(100% + 48px)', marginLeft: '-24px', marginRight: '-24px' }
    return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: '4 / 5' }
  }
  return { aspectRatio: '4 / 5' }
})
const singleFrameClass = computed(() =>
  frameClass.value + (props.page.singleWidth === 'full' && props.page.singleKeepRatio ? ' gf-natural' : '')
)

const index = ref(0)
const trackRef = ref(null)

function goTo(i) {
  const len = props.page.images?.length || 0
  if (!len) return
  index.value = (i + len) % len
  const el = trackRef.value
  if (el) el.scrollTo({ left: index.value * el.clientWidth, behavior: 'smooth' })
}
function onScroll() {
  const el = trackRef.value
  if (!el) return
  const i = Math.round(el.scrollLeft / el.clientWidth)
  if (i !== index.value) index.value = i
}

function previewImage(src) {
  const url = typeof src === 'string' ? src.trim() : String(src?.value || src || '').trim()
  if (url && url !== '[object Object]') emit('preview-image', url)
}

const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]
const polaroidShift = [0, 10, -8, 6, -10, 8, 4, -6]
function polaroidTransform(i) {
  const a = polaroidAngles[i % polaroidAngles.length]
  const s = polaroidShift[i % polaroidShift.length]
  return `rotate(${a}deg) translateY(${s}px)`
}

</script>

<style scoped>
.gallery { width: 100%; font-family: var(--font-sans); }
.preview-img { cursor: zoom-in; }
.gallery-title {
  font-family: var(--font-calligraphy);
  font-weight: 700;
}
.gallery-desc { font-family: var(--font-kai); }

.nine-grid-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.nine-grid-slot {
  background: transparent;
  contain: layout paint;
}
.nine-grid-placeholder {
  visibility: hidden;
  pointer-events: none;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
}
.olfs-slot {
  min-width: 0;
  min-height: 0;
  contain: layout paint;
}
.olfs-placeholder {
  visibility: hidden;
  pointer-events: none;
  background: transparent !important;
}
.olfs-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.overlap-wrap {
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
  height: clamp(360px, 56vh, 520px);
  padding: 12px 16px;
  box-sizing: border-box;
}
.overlap-slot {
  contain: layout paint;
}
.overlap-placeholder {
  visibility: hidden;
  pointer-events: none;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
}
.overlap-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.g-masonry { column-count: 2; column-gap: 10px; }
.g-masonry-slot {
  break-inside: avoid;
  margin-bottom: 10px;
  contain: layout paint;
}
.g-masonry-placeholder {
  visibility: hidden;
  pointer-events: none;
  min-height: 120px;
}
.g-masonry-item { border-radius: 6px; }

.g-carousel-track {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  border-radius: 10px; scrollbar-width: none;
}
.g-carousel-slide { flex: 0 0 100%; min-width: 100%; height: 52vh; scroll-snap-align: center; }
.g-carousel-slide img { width: 100%; height: 100%; object-fit: cover; }
.g-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; border-radius: 9999px; border: none;
  background: rgba(255,255,255,0.78); color: #a88c6b; font-size: 20px; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.12); backdrop-filter: blur(6px);
}
.g-prev { left: 8px; }
.g-next { right: 8px; }
.g-dots { display: flex; justify-content: center; gap: 6px; margin-top: 10px; }
.g-dot { width: 7px; height: 7px; border-radius: 9999px; border: none; background: rgba(168,140,107,0.3); padding: 0; cursor: pointer; transition: all 0.3s; }
.g-dot.active { background: #a88c6b; width: 18px; border-radius: 9999px; }

.g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; }
.g-polaroid-slot {
  width: 42%;
  contain: layout paint;
}
.g-polaroid-placeholder {
  visibility: hidden;
  pointer-events: none;
  min-height: 160px;
}
.g-polaroid-item {
  width: 100%; background: #fff; padding: 8px 8px 24px; border-radius: 2px;
  box-shadow: 0 6px 16px rgba(0,0,0,0.12); transition: transform 0.4s ease;
}
.g-polaroid-item:hover { z-index: 10; }
.g-polaroid-item img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }

.no-scrollbar::-webkit-scrollbar { display: none; }
</style>
