<template>
  <div class="pdf-viewer">
    <!-- PDF 页面：canvas + textLayer 必须在同一容器内渲染 -->
    <div class="page-wrap" v-for="page in pageCount" :key="page">
      <canvas :ref="el => setCanvasRef(page, el)"></canvas>
      <div :ref="el => setOverlayRef(page, el)" class="textLayer"></div>
    </div>

    <div v-if="loading" class="loading">加载中...</div>
  </div>
</template>

<script>
import { ref, shallowRef, onMounted, nextTick, watch } from 'vue';
import * as pdfjsLib from 'pdfjs-dist';
import { TextLayer } from 'pdfjs-dist';

pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url
).href;

export default {
  name: 'PdfViewer',
  props: {
    pdfUrl: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const canvasMap = {};
    const overlayMap = {};
    const pageCount = ref(0);
    const loading = ref(true);
    const renderedPageCount = ref(0);
    let resolveRenderAll = null;
    const waitAllPagesRendered = () => {
      if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
        return Promise.resolve();
      }
      return new Promise((resolve) => {
        resolveRenderAll = resolve;
      });
    };
    // pdfjs 的 document 对象内部使用 #private 字段，
    // 若被 Vue 响应式深度代理会触发 "Cannot read from private field"。
    // 因此用 shallowRef 保持为原始对象引用。
    const pdfDocRef = shallowRef(null);
    const searchKey = ref('');
    const matchList = ref([]);
    const matchIdx = ref(0);

    // 保存 canvas
    const setCanvasRef = (page, el) => {
      if (!el) return;
      canvasMap[page] = el;
    };

    // 保存 textLayer 容器（用于搜索高亮）
    const setOverlayRef = (page, el) => {
      if (!el) return;
      overlayMap[page] = el;
    };

    // 清理 URL
    const parsePdfUrl = (pdfUrl) => {
      if (!pdfUrl || typeof pdfUrl !== 'string') return '';
      const [urlPart] = pdfUrl.split('#');
      return urlPart;
    };

    // 清空所有高亮（不销毁 textLayer）
    const clearHighlights = () => {
      Object.values(overlayMap).forEach(layer => {
        if (!layer) return;
        const rects = layer.querySelectorAll('.highlight-rect');
        rects.forEach(n => n.remove());
      });
    };

    // 重置搜索状态：清空关键词、匹配列表与高亮
    const clearSearch = () => {
      searchKey.value = '';
      matchList.value = [];
      matchIdx.value = 0;
      clearHighlights();
    };

    // 渲染单页 PDF（canvas + textLayer）
    const renderPage = async (pdf, pageNum) => {
      const pdfPage = await pdf.getPage(pageNum);
      const canvas = canvasMap[pageNum];
      const textLayer = overlayMap[pageNum];

      if (!canvas || !textLayer) return;

      // 以画布的可视宽度为基准自适应缩放，避免 CSS 强行拉伸导致 textLayer/高亮错位
      const baseViewport = pdfPage.getViewport({ scale: 1 });
      const desiredWidth = canvas.clientWidth || 726;
      const scale = desiredWidth / baseViewport.width;
      const viewport = pdfPage.getViewport({ scale });

      const context = canvas.getContext('2d');
      canvas.width = viewport.width;
      canvas.height = viewport.height;
      // 保证 canvas 不再被 CSS 拉伸，和 textLayer 共享同一坐标系
      canvas.style.width = `${viewport.width}px`;
      canvas.style.height = `${viewport.height}px`;
      textLayer.style.width = canvas.width + 'px';
      textLayer.style.height = canvas.height + 'px';
      textLayer.innerHTML = '';
      // pdf.js v5 text layer 依赖 scale-factor 参与定位计算
      textLayer.style.setProperty('--scale-factor', String(viewport.scale || 1));

      await pdfPage.render({ canvasContext: context, viewport }).promise;

      // 渲染 textLayer：pdfjs-dist v5 推荐用 TextLayer，renderTextLayer 可能不存在
      try {
        const textContent = await pdfPage.getTextContent();
        const layer = new TextLayer({
          textContentSource: textContent,
          container: textLayer,
          viewport
        });
        await layer.render();
      } catch (e) {
        console.warn('textLayer 渲染失败', e);
      }

      renderedPageCount.value += 1;
      if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
        if (typeof resolveRenderAll === 'function') {
          const fn = resolveRenderAll;
          resolveRenderAll = null;
          fn();
        }
      }
    };

    // 渲染 PDF
    const renderPdf = async (pdfUrl) => {
      const url = parsePdfUrl(pdfUrl);
      if (!url) return;

      loading.value = true;
      pdfDocRef.value = null;
      clearHighlights();
      matchList.value = [];
      searchKey.value = '';
      renderedPageCount.value = 0;
      resolveRenderAll = null;

      try {
        const pdf = await pdfjsLib.getDocument(url).promise;
        pdfDocRef.value = pdf;
        pageCount.value = pdf.numPages;
        await nextTick();

        for (let p = 1; p <= pdf.numPages; p++) {
          await renderPage(pdf, p);
        }
      } catch (err) {
        console.error('PDF 加载失败', err);
      } finally {
        loading.value = false;
      }
    };

    // 搜索关键词 + 高亮（记录每个命中的子串范围）
    const doSearch = async () => {
      const doc = pdfDocRef.value;
      const key = searchKey.value.trim();
      clearHighlights();
      matchList.value = [];
      matchIdx.value = 0;

      if (!doc || !key) return;
      // 首次搜索时确保所有页的 textLayer 已渲染完成，避免“越搜越多”
      await waitAllPagesRendered();

      for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
        const layer = overlayMap[pageNum];
        if (!layer) continue;
        const nodes = Array.from(layer.querySelectorAll('span'));
        for (const el of nodes) {
          const t = (el.textContent || '');
          if (!t) continue;
          let start = 0;
          while (true) {
            const idx = t.indexOf(key, start);
            if (idx === -1) break;
            matchList.value.push({ pageNum, el, startIdx: idx, endIdx: idx + key.length });
            start = idx + Math.max(1, key.length);
          }
        }
      }

      if (matchList.value.length > 0) jumpTo(0);
    };

    // 跳转到第 N 个匹配项
    const jumpTo = (idx) => {
      if (idx < 0 || idx >= matchList.value.length) return;
      matchIdx.value = idx;
      const m = matchList.value[idx];
      const el = m?.el;
      if (!el) return;
      clearHighlights();
      // 用 Range 精确计算“子串”在页面上的矩形位置，再画黄色块，避免把整段 span 都标黄
      const textNode = el.firstChild;
      if (textNode && textNode.nodeType === Node.TEXT_NODE) {
        try {
          const range = document.createRange();
          range.setStart(textNode, Math.max(0, m.startIdx ?? 0));
          range.setEnd(textNode, Math.max(0, m.endIdx ?? 0));
          const rectList = Array.from(range.getClientRects());
          const pageWrap = el.closest('.page-wrap');
          const layer = overlayMap[m.pageNum];
          if (pageWrap && layer && rectList.length) {
            const pageRect = pageWrap.getBoundingClientRect();
            rectList.forEach(r => {
              const mark = document.createElement('div');
              mark.className = 'highlight-rect';
              mark.style.left = (r.left - pageRect.left) + 'px';
              mark.style.top = (r.top - pageRect.top) + 'px';
              mark.style.width = r.width + 'px';
              mark.style.height = r.height + 'px';
              layer.appendChild(mark);
            });
          }
          range.detach?.();
        } catch (e) {
          // ignore
        }
      }

      // 优先只滚动右侧 report-box，避免触发整页滚动导致 header 遮挡
      const container = el.closest('.report-box');
      if (container) {
        const TOP_OFFSET = 72;
        const containerRect = container.getBoundingClientRect();
        const elRect = el.getBoundingClientRect();
        const targetTop = (elRect.top - containerRect.top) + container.scrollTop - TOP_OFFSET;
        container.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
      } else {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    };

    const prevMatch = () => jumpTo(matchIdx.value - 1);
    const nextMatch = () => jumpTo(matchIdx.value + 1);

    const getMatchInfo = () => {
      const total = matchList.value.length;
      const current = total ? matchIdx.value + 1 : 0;
      return { current, total };
    };

    // 外部调用方法
    const searchKeyword = async (keyword) => {
      searchKey.value = keyword;
      await doSearch();
      return matchList.value.length > 0 ? matchList.value[0].pageNum : 0;
    };

    const goToPage = (pageNum) => {
      const canvasEl = canvasMap[pageNum];
      if (!canvasEl) return;
      const container = canvasEl.closest('.report-box');
      if (container) {
        const containerRect = container.getBoundingClientRect();
        const canvasRect = canvasEl.getBoundingClientRect();
        const targetTop =
          (canvasRect.top - containerRect.top) + container.scrollTop;
        container.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
      } else {
        canvasEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
    };

    watch(() => props.pdfUrl, (newVal) => {
      if (newVal) renderPdf(newVal);
    }, { immediate: true });

    return {
      pageCount,
      setCanvasRef,
      setOverlayRef,
      loading,
      searchKey,
      doSearch,
      prevMatch,
      nextMatch,
      getMatchInfo,
      matchList,
      matchIdx,
      searchKeyword,
      clearSearch,
      goToPage
    };
  }
};
</script>

<style scoped>
.pdf-viewer {
  position: relative;
  width: 100%;
}

.page-wrap {
  position: relative;
  margin-bottom: 16px;
  width: 100%;
}

canvas {
  width: 100%;
  height: auto;
  display: block;
}

.textLayer {
  position: absolute;
  left: 0;
  top: 0;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  line-height: 1;
}

/* 不展示整页“文字层”，只在命中时显示黄色背景 */
.textLayer :deep(span) {
  position: absolute;
  transform-origin: 0% 0%;
  white-space: pre;
  line-height: 1;
  /* pdf.js v5 TextLayer：用变量计算真实字形盒子尺寸，否则背景宽高会不准 */
  font-size: calc(var(--font-height, 0px) * var(--scale-factor, 1));
  transform: scaleX(var(--scale-x, 1));
  color: transparent;
}

.textLayer :deep(.highlight-text) {
  background: #ff0;
  opacity: 0.6;
  padding: 0 1px;
  border-radius: 2px;
}

.textLayer :deep(.highlight-rect) {
  position: absolute;
  background: #ff0;
  opacity: 0.6;
  border-radius: 2px;
  pointer-events: none;
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 18px;
  color: #333;
}
</style>