<template>
  <div class="pdf-preview">
    <div v-if="isReady && pageUrls.length > 0" class="pdf-preview__toolbar">
      <div class="pdf-preview__toolbar-group">
        <button class="pdf-preview__toolbar-btn" type="button" :disabled="zoomLevel <= minZoom" @click="decreaseZoom">缩小</button>
        <button class="pdf-preview__toolbar-btn" type="button" :disabled="zoomLevel >= maxZoom" @click="increaseZoom">放大</button>
        <span class="pdf-preview__toolbar-zoom">{{ Math.round(zoomLevel * 100) }}%</span>
      </div>
      <div class="pdf-preview__toolbar-group pdf-preview__toolbar-group--jump">
        <label class="pdf-preview__toolbar-label" for="pdf-page-jump-input">跳转到</label>
        <input id="pdf-page-jump-input" v-model="pageJumpValue" class="pdf-preview__toolbar-input" type="number" :min="1" :max="pageUrls.length" @keydown.enter.prevent="confirmPageJump" />
        <button class="pdf-preview__toolbar-btn" type="button" @click="confirmPageJump">跳转</button>
      </div>
      <div class="pdf-preview__toolbar-group">
        <span class="pdf-preview__toolbar-page">当前页：{{ activePageNumber }} / {{ pageUrls.length }}</span>
      </div>
    </div>
    <template v-if="isReady && pageUrls.length > 0">
      <div ref="scrollContainerRef" class="pdf-preview__scroll">
        <section v-for="page in pageUrls" :key="page.pageNumber" :data-page-number="page.pageNumber" class="pdf-preview__page-card" :class="{ 'is-active': page.pageNumber === activePageNumber }">
          <div class="pdf-preview__page-title">第 {{ page.pageNumber }} 页</div>
          <canvas :ref="(el) => setCanvasRef(el, page.pageNumber)" class="pdf-preview__canvas" />
        </section>
      </div>
    </template>
    <template v-else>
      <div class="pdf-preview__fallback">
        <img class="pdf-preview__fallback-image" :src="fallbackSrc" :alt="titleText" />
        <div class="pdf-preview__fallback-title">{{ titleText }}</div>
        <div class="pdf-preview__fallback-text">{{ fallbackMessage }}</div>
        <div v-if="debugInfo" class="pdf-preview__fallback-debug">{{ debugInfo }}</div>
      </div>
    </template>
  </div>
</template>

<script setup>
import { computed, markRaw, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist'
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url'

GlobalWorkerOptions.workerSrc = pdfWorkerUrl

const props = defineProps({
  pdfUrl: { type: String, default: '' },
  fileName: { type: String, default: '' },
  fallbackSrc: { type: String, default: '' },
  activePage: { type: Number, default: 1 }
})

const emit = defineEmits(['page-change'])

const objectUrl = ref('')
const scrollContainerRef = ref(null)
const pdfDocumentRef = shallowRef(null)
const canvasRefs = shallowRef(new Map())
const pageUrls = ref([])
const renderedPages = shallowRef(new Set())
const renderQueue = shallowRef(new Set())
const observerRef = shallowRef(null)
const activePageNumber = ref(1)
const pageJumpValue = ref(1)
const zoomLevel = ref(1.0)
const minZoom = 0.6
const maxZoom = 2.2
const zoomStep = 0.2
const baseScale = ref(1.0)
const titleText = computed(() => props.fileName || 'PDF 原文预览')
const isReady = computed(() => Boolean(props.pdfUrl))
const resolvedUrl = computed(() => objectUrl.value || props.pdfUrl)
const loadError = ref('')
const fallbackMessage = computed(() => loadError.value || '当前未获取到可直接预览的 PDF 地址，后续可接入 pdf.js 或后端直链。')
const debugInfo = computed(() => {
  if (!props.pdfUrl) return 'pdfUrl 为空'
  if (loadError.value) return `加载失败：${loadError.value}`
  return `pdfUrl=${props.pdfUrl}`
})

function setCanvasRef(el, pageNumber) {
  if (!el) return
  canvasRefs.value.set(pageNumber, el)
}

function getPageScale(page) {
  const viewport = page.getViewport({ scale: 1 })
  const containerWidth = scrollContainerRef.value?.clientWidth || viewport.width || 1
  const preferredScale = (containerWidth - 48) / viewport.width
  const clampedScale = Math.min(maxZoom, Math.max(minZoom, preferredScale || 1))
  return Number((clampedScale * baseScale.value).toFixed(2))
}

async function renderPage(pageNumber) {
  const pdf = pdfDocumentRef.value
  const canvas = canvasRefs.value.get(pageNumber)
  if (!pdf || !canvas || renderedPages.value.has(pageNumber) || renderQueue.value.has(pageNumber)) return
  renderQueue.value.add(pageNumber)
  try {
    const page = await pdf.getPage(pageNumber)
    const context = canvas.getContext('2d')
    if (!context) return
    const scale = getPageScale(page)
    const viewport = page.getViewport({ scale })
    const outputScale = window.devicePixelRatio || 1
    canvas.width = Math.floor(viewport.width * outputScale)
    canvas.height = Math.floor(viewport.height * outputScale)
    canvas.style.width = `${Math.floor(viewport.width)}px`
    canvas.style.height = `${Math.floor(viewport.height)}px`
    context.setTransform(outputScale, 0, 0, outputScale, 0, 0)
    context.clearRect(0, 0, canvas.width, canvas.height)
    await page.render({ canvasContext: context, viewport }).promise
    renderedPages.value.add(pageNumber)
  } catch (error) {
    loadError.value = error?.message || String(error)
    throw error
  } finally {
    renderQueue.value.delete(pageNumber)
  }
}

function observePages() {
  if (observerRef.value) {
    observerRef.value.disconnect()
    observerRef.value = null
  }
  if (typeof IntersectionObserver === 'undefined' || !scrollContainerRef.value) return
  observerRef.value = new IntersectionObserver(
    (entries) => {
      let visibleCandidate = activePageNumber.value
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return
        const pageNumber = Number(entry.target?.dataset?.pageNumber || 0)
        if (pageNumber > 0) {
          visibleCandidate = pageNumber
          renderPage(pageNumber).catch(() => {})
        }
      })
      if (visibleCandidate !== activePageNumber.value) {
        activePageNumber.value = visibleCandidate
        emit('page-change', visibleCandidate)
      }
    },
    {
      root: scrollContainerRef.value,
      rootMargin: '240px 0px',
      threshold: 0.08
    }
  )
  scrollContainerRef.value.querySelectorAll('[data-page-number]').forEach((el) => {
    observerRef.value?.observe(el)
  })
}

async function renderPdf(url) {
  if (!url) {
    loadError.value = 'PDF 地址为空'
    return
  }
  loadError.value = ''
  const loadingTask = getDocument({ url, useWorkerFetch: false, isEvalSupported: false })
  const pdf = await loadingTask.promise
  pdfDocumentRef.value = pdf
  pageUrls.value = Array.from({ length: pdf.numPages }, (_, index) => ({ pageNumber: index + 1 }))
  renderedPages.value = new Set()
  renderQueue.value = new Set()
  zoomLevel.value = Math.min(Math.max(zoomLevel.value, minZoom), maxZoom)
  activePageNumber.value = Math.min(Math.max(Number(props.activePage || 1), 1), pdf.numPages)
  baseScale.value = zoomLevel.value
  pageJumpValue.value = activePageNumber.value
  emit('page-change', activePageNumber.value)
  await nextTick()
  observePages()
  try {
    await renderPage(activePageNumber.value)
    if (activePageNumber.value > 1) {
      await renderPage(activePageNumber.value - 1)
    }
    if (activePageNumber.value < pdf.numPages) {
      await renderPage(activePageNumber.value + 1)
    }
  } catch (error) {
    loadError.value = error?.message || String(error)
  }
}

async function resetAndRender(nextUrl) {
  pageUrls.value = []
  canvasRefs.value = new Map()
  renderedPages.value = new Set()
  renderQueue.value = new Set()
  if (observerRef.value) {
    observerRef.value.disconnect()
    observerRef.value = null
  }
  if (pdfDocumentRef.value) {
    try {
      await pdfDocumentRef.value.destroy()
    } catch (error) {
      // ignore destroy failures
    }
    pdfDocumentRef.value = null
  }
  activePageNumber.value = 1
  if (!nextUrl) return
  await renderPdf(nextUrl)
}

watch(
  () => props.pdfUrl,
  async (nextUrl, prevUrl) => {
    if (prevUrl && prevUrl.startsWith('blob:') && prevUrl !== nextUrl && objectUrl.value === prevUrl) {
      URL.revokeObjectURL(prevUrl)
      objectUrl.value = ''
    }
    if (nextUrl && nextUrl.startsWith('blob:')) {
      objectUrl.value = nextUrl
    } else {
      objectUrl.value = ''
    }
    await resetAndRender(objectUrl.value || nextUrl)
  },
  { immediate: true }
)

watch(
  () => props.activePage,
  async (nextPage) => {
    const targetPage = Number(nextPage || 1)
    if (!targetPage || targetPage === activePageNumber.value) return
    activePageNumber.value = targetPage
    pageJumpValue.value = targetPage
    emit('page-change', targetPage)
    await renderPage(targetPage)
  }
)

function confirmPageJump() {
  const targetPage = Math.min(Math.max(Number(pageJumpValue.value || 1), 1), pageUrls.value.length || 1)
  pageJumpValue.value = targetPage
  activePageNumber.value = targetPage
  emit('page-change', targetPage)
  const targetElement = scrollContainerRef.value?.querySelector(`[data-page-number="${targetPage}"]`)
  if (targetElement?.scrollIntoView) {
    targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
  }
}

onBeforeUnmount(() => {
  if (observerRef.value) {
    observerRef.value.disconnect()
    observerRef.value = null
  }
  if (objectUrl.value && objectUrl.value.startsWith('blob:')) {
    URL.revokeObjectURL(objectUrl.value)
  }
  if (pdfDocumentRef.value) {
    pdfDocumentRef.value.destroy().catch(() => {})
  }
})

function increaseZoom() {
  zoomLevel.value = Math.min(maxZoom, Number((zoomLevel.value + zoomStep).toFixed(1)))
  baseScale.value = zoomLevel.value
  if (resolvedUrl.value) {
    resetAndRender(resolvedUrl.value)
  }
}

function decreaseZoom() {
  zoomLevel.value = Math.max(minZoom, Number((zoomLevel.value - zoomStep).toFixed(1)))
  baseScale.value = zoomLevel.value
  if (resolvedUrl.value) {
    resetAndRender(resolvedUrl.value)
  }
}
</script>

<style scoped lang="scss">
.pdf-preview {
  flex: 1;
  min-height: 0;
  background: #f8fafc;
}

.pdf-preview__toolbar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid #e8ecf2;
  background: #fff;
}

.pdf-preview__toolbar-group {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.pdf-preview__toolbar-group--jump {
  flex: 1;
  justify-content: center;
}

.pdf-preview__toolbar-btn {
  height: 32px;
  padding: 0 12px;
  border: 1px solid #d0d5dd;
  border-radius: 8px;
  background: #fff;
  cursor: pointer;
}

.pdf-preview__toolbar-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.pdf-preview__toolbar-zoom,
.pdf-preview__toolbar-page,
.pdf-preview__toolbar-label {
  font-size: 13px;
  color: #344054;
}

.pdf-preview__toolbar-input {
  width: 84px;
  height: 32px;
  padding: 0 10px;
  box-sizing: border-box;
  border: 1px solid #d0d5dd;
  border-radius: 8px;
  outline: none;
}

.pdf-preview__scroll {
  height: 100%;
  overflow: auto;
  padding: 16px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.pdf-preview__page-card {
  background: #fff;
  border: 1px solid #e8ecf2;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
}

.pdf-preview__page-card.is-active {
  border-color: #2563eb;
  box-shadow: 0 8px 24px rgba(37, 99, 235, 0.12);
}

.pdf-preview__page-title {
  padding: 10px 14px;
  font-size: 13px;
  color: #667085;
  border-bottom: 1px solid #eef2f7;
}

.pdf-preview__canvas {
  display: block;
  width: 100%;
  height: auto;
}

.pdf-preview__fallback {
  height: 100%;
  min-height: 520px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
  box-sizing: border-box;
  text-align: center;
  gap: 12px;
}

.pdf-preview__fallback-image {
  max-width: min(100%, 480px);
  max-height: 360px;
  object-fit: contain;
  display: block;
}

.pdf-preview__fallback-title {
  font-size: 16px;
  line-height: 24px;
  font-weight: 700;
  color: #101828;
}

.pdf-preview__fallback-text {
  max-width: 520px;
  font-size: 14px;
  line-height: 22px;
  color: #667085;
}

.pdf-preview__fallback-debug {
  max-width: 520px;
  padding: 8px 10px;
  border-radius: 8px;
  background: #fef3f2;
  color: #b42318;
  font-size: 12px;
  line-height: 18px;
  word-break: break-all;
  text-align: left;
}
</style>
