<template>
  <div ref="scrollRef" class="sync-compare-scroll" @scroll="onScroll">
    <section
      v-for="page in pages"
      :key="page.page_idx"
      :id="`page-${page.page_idx}`"
      :ref="(element) => setPageRef(page.page_idx, element)"
      class="page-container"
      :class="{ 'page-container--active': currentPageIdx === page.page_idx }"
    >
      <PageBlock
        v-if="pageDataMap[page.page_idx]"
        :page="page"
        :nodes="pageDataMap[page.page_idx].nodes"
        :placeholder-src="placeholderSrc"
        :selected-node-key="selectedNodeKey"
        @node-select="onSelectNode"
      />
      <PageShell v-else :page="page" />
    </section>
  </div>
</template>

<script setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import PageBlock from './PageBlock.vue'
import PageShell from './PageShell.vue'

const props = defineProps({
  pages: { type: Array, default: () => [] },
  pageDataMap: { type: Object, default: () => ({}) },
  currentPageIdx: { type: Number, default: 0 },
  currentNodeKey: { type: String, default: '' },
  selectedNodeKey: { type: String, default: '' },
  placeholderSrc: { type: String, default: '' }
})

const emit = defineEmits(['page-visible', 'active-page-change', 'active-node-change', 'node-select'])
const scrollRef = ref(null)
const pageRefMap = new Map()
let observer = null
let rafId = null
let lastActiveNodeId = ''
let jumpTimerIds = []

function setPageRef(pageIdx, element) {
  if (element) {
    pageRefMap.set(pageIdx, element)
    if (observer) observer.observe(element)
    return
  }
  const oldElement = pageRefMap.get(pageIdx)
  if (oldElement && observer) observer.unobserve(oldElement)
  pageRefMap.delete(pageIdx)
}

function getVisibleHeadings() {
  const scroller = scrollRef.value
  if (!scroller) return []
  const scrollerRect = scroller.getBoundingClientRect()
  return Array.from(scroller.querySelectorAll('[data-semantic-type="heading"]'))
    .map((element) => ({
      element,
      rect: element.getBoundingClientRect()
    }))
    .filter(({ rect }) => rect.bottom > scrollerRect.top && rect.top < scrollerRect.bottom)
}

function getActiveHeading() {
  const visibleHeadings = getVisibleHeadings()
  if (!visibleHeadings.length) return null

  // 全局视窗内最靠上的标题优先；当它越过顶部后，自然切换到下一个可见标题。
  const topVisible = visibleHeadings.reduce((candidate, current) => {
    if (!candidate) return current
    if (current.rect.top < candidate.rect.top) return current
    return candidate
  }, null)

  return topVisible?.element || null
}

function emitActiveNode() {
  const activeHeadingEl = getActiveHeading()
  const nodeId = activeHeadingEl?.getAttribute('data-node-id') || ''
  if (!nodeId || nodeId === lastActiveNodeId) return
  lastActiveNodeId = nodeId
  emit('active-node-change', nodeId)
}

function onSelectNode(node) {
  emit('node-select', node)
}

function onScroll() {
  if (rafId !== null) return
  rafId = window.requestAnimationFrame(() => {
    rafId = null
    emitActiveNode()
  })
}

function scrollToNode(nodeId, options = {}) {
  const { behavior = 'auto' } = options
  const scroller = scrollRef.value
  if (!scroller || !nodeId) return false
  const targetElement = scroller.querySelector(`[data-node-id="${CSS.escape(String(nodeId))}"]`)
  if (!targetElement) return false
  const targetTop = Math.max(0, targetElement.offsetTop - 12)
  scroller.scrollTo({ top: targetTop, behavior })
  jumpTimerIds.forEach((timerId) => window.clearTimeout(timerId))
  jumpTimerIds = [
    window.setTimeout(() => {
      scroller.scrollTo({ top: Math.max(0, targetElement.offsetTop - 12), behavior: 'auto' })
      onScroll()
    }, 120),
    window.setTimeout(() => {
      scroller.scrollTo({ top: Math.max(0, targetElement.offsetTop - 12), behavior: 'auto' })
      onScroll()
    }, 280)
  ]
  return true
}

function scrollToPage(pageIdx, options = {}) {
  const { behavior = 'auto' } = options
  const scroller = scrollRef.value
  const targetElement = pageRefMap.get(pageIdx)
  if (!targetElement || !scroller) return
  const targetTop = Math.max(0, targetElement.offsetTop - 16)
  scroller.scrollTo({ top: targetTop, behavior })
  jumpTimerIds.forEach((timerId) => window.clearTimeout(timerId))
  jumpTimerIds = [
    window.setTimeout(() => {
      scroller.scrollTo({ top: Math.max(0, targetElement.offsetTop - 16), behavior: 'auto' })
      onScroll()
    }, 120),
    window.setTimeout(() => {
      scroller.scrollTo({ top: Math.max(0, targetElement.offsetTop - 16), behavior: 'auto' })
      onScroll()
    }, 280)
  ]
}

defineExpose({ scrollToPage, scrollToNode })

onMounted(async () => {
  await nextTick()
  observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return
        const pageId = String(entry.target.id || '').replace('page-', '')
        const pageIdx = Number(pageId)
        if (!Number.isNaN(pageIdx)) emit('page-visible', pageIdx)
      })
    },
    { root: scrollRef.value, rootMargin: '600px 0px 600px 0px', threshold: 0.01 }
  )
  pageRefMap.forEach((element) => observer.observe(element))
  onScroll()
})

onUnmounted(() => {
  if (observer) {
    observer.disconnect()
    observer = null
  }
  if (rafId !== null) {
    window.cancelAnimationFrame(rafId)
    rafId = null
  }
  jumpTimerIds.forEach((timerId) => window.clearTimeout(timerId))
  jumpTimerIds = []
})
</script>

<style scoped lang="scss">
.sync-compare-scroll {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  border: 1px solid #e8ecf2;
  border-radius: 8px;
  background: #fff;
  padding: 16px;
  box-sizing: border-box;
}

.page-container { padding: 0 0 18px; }
.page-container--active { background: #f8fafc; }
</style>
