<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, nextTick, watch } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.min?worker';

// 使用 Vite 的 ?worker 直接注入 Worker，避免线上 mjs MIME 类型问题
pdfjsLib.GlobalWorkerOptions.workerPort = new PdfWorker();
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);
    const pageIndexCache = {};
    // pdfjs 3.x 的 renderTextLayer 在不同入口下导出不一致，这里做一次缓存 + 兜底加载
    const pdfjsApiRef = shallowRef(pdfjsLib);

    // 保存 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 pageWrap = layer.closest('.page-wrap');
        if (pageWrap) {
          const wrapMarks = pageWrap.querySelectorAll('.highlight-rect');
          wrapMarks.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 3.x）：使用 renderTextLayer（不要用 TextLayer 构造器）
      try {
        const textContent = await pdfPage.getTextContent();
        let api = pdfjsApiRef.value || pdfjsLib;
        let rt = api?.renderTextLayer;
        // 兜底：某些入口下 renderTextLayer 不在 pdfjsLib 上，尝试 legacy 入口
        if (typeof rt !== 'function') {
          try {
            const legacy = await import('pdfjs-dist/legacy/build/pdf');
            pdfjsApiRef.value = legacy;
            api = legacy;
            rt = legacy?.renderTextLayer;
          } catch (_) { }
        }
        if (typeof rt === 'function') {
          await rt({
            textContent,
            container: textLayer,
            viewport,
            // pdfjs 3.x 需要传入 textDivs 数组
            textDivs: [],
            enhanceTextSelection: false
          }).promise;
        } else if (typeof api?.TextLayer === 'function') {
          // pdfjs-dist v4/v5：renderTextLayer 已移除，改用 TextLayer.render()
          const tl = new api.TextLayer({
            textContentSource: textContent,
            container: textLayer,
            viewport
          });
          await tl.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;
      Object.keys(pageIndexCache).forEach(k => delete pageIndexCache[k]);

      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();
      // textLayer 内 span/布局在渲染 promise resolve 后仍可能有一次 DOM/布局收敛
      await nextTick();
      await new Promise((r) => requestAnimationFrame(() => r()));
      // 重新搜索时 textLayer 可能仍有增量调整；每次搜索都重建页索引，避免数量漂移
      Object.keys(pageIndexCache).forEach(k => delete pageIndexCache[k]);

      const stripSpaces = (text) => String(text ?? '').replace(/[\s\u00A0\u200B\u200C\u200D\uFEFF]+/g, '');

      const buildPageIndex = (layer) => {
        const spans = Array.from(layer.querySelectorAll('span'));
        const ranges = [];
        let rawText = '';
        for (const el of spans) {
          const t = String(el.textContent ?? '');
          const start = rawText.length;
          rawText += t;
          ranges.push({ el, start, end: rawText.length });
        }

        // 兜底：去掉空白后的文本 + 位置映射（stripIndex -> rawIndex）
        const strippedToRawIndex = [];
        let strippedText = '';
        for (let i = 0; i < rawText.length; i++) {
          const ch = rawText[i];
          if (/[\s\u00A0\u200B\u200C\u200D\uFEFF]/.test(ch)) continue;
          strippedToRawIndex.push(i);
          strippedText += ch;
        }

        const rawToSegments = (rawStart, rawEnd) => {
          const segs = [];
          for (const r of ranges) {
            const segStart = Math.max(rawStart, r.start);
            const segEnd = Math.min(rawEnd, r.end);
            if (segStart < segEnd) {
              segs.push({
                el: r.el,
                startIdx: segStart - r.start,
                endIdx: segEnd - r.start,
              });
            }
          }
          return segs;
        };

        return { rawText, strippedText, strippedToRawIndex, rawToSegments };
      };

      const getOrBuildPageIndex = (pageNum) => {
        if (pageIndexCache[pageNum]) return pageIndexCache[pageNum];
        const layer = overlayMap[pageNum];
        if (!layer) {
          pageIndexCache[pageNum] = null;
          return null;
        }
        const index = buildPageIndex(layer);
        pageIndexCache[pageNum] = index;
        return index;
      };

      const findAllIndexes = (text, needle) => {
        const list = [];
        if (!needle) return list;
        let from = 0;
        while (from <= text.length) {
          const idx = text.indexOf(needle, from);
          if (idx === -1) break;
          list.push(idx);
          from = idx + Math.max(1, needle.length);
        }
        return list;
      };

      for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
        const index = getOrBuildPageIndex(pageNum);
        const { rawText, strippedText, strippedToRawIndex, rawToSegments } = index
          ? index
          : { rawText: '', strippedText: '', strippedToRawIndex: [], rawToSegments: () => [] };

        // 第一优先：原始文本直接匹配（可跨 span）
        const rawHits = rawText ? findAllIndexes(rawText, key) : [];
        for (const hit of rawHits) {
          const segments = rawToSegments(hit, hit + key.length);
          if (segments.length) matchList.value.push({ pageNum, segments });
        }

        // 兜底：去空白匹配（常见于 PDF 将单词/中文切成多段并插入空白）
        if (!rawHits.length && strippedText) {
          const strippedKey = stripSpaces(key);
          const strippedHits = findAllIndexes(strippedText, strippedKey);
          for (const hit of strippedHits) {
            const rawStart = strippedToRawIndex[hit] ?? 0;
            const rawEndExclusiveIdx = strippedToRawIndex[Math.min(strippedToRawIndex.length - 1, hit + strippedKey.length - 1)] ?? rawStart;
            const rawEnd = rawEndExclusiveIdx + 1;
            const segments = rawToSegments(rawStart, rawEnd);
            if (segments.length) matchList.value.push({ pageNum, segments });
          }
        }

        // 最终兜底：如果该页 textLayer 不可用/为空，直接用 pdf.js 的 textContent 做“页级”检索
        // 说明：这类命中只能做到定位页码，无法保证精确高亮（因为缺少 span/Range 映射）。
        if (!rawHits.length && !strippedText) {
          try {
            const page = await doc.getPage(pageNum);
            const tc = await page.getTextContent();
            const pageText = (tc?.items || []).map(i => String(i?.str ?? '')).join('');
            if (!pageText) continue;
            const hasRaw = pageText.includes(key);
            const hasStripped = stripSpaces(pageText).includes(stripSpaces(key));
            if (hasRaw || hasStripped) {
              matchList.value.push({ pageNum, segments: [], fallback: true });
            }
          } catch (_) {
            // ignore
          }
        }
      }

      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];
      if (m?.fallback) {
        // 兜底命中：只定位页码，不做高亮
        goToPage(m.pageNum);
        return;
      }
      const firstSeg = m?.segments?.[0];
      const el = firstSeg?.el;
      if (!el) return;
      clearHighlights();
      const layer = overlayMap[m.pageNum];
      if (!layer) return;
      const pageWrap = layer.closest('.page-wrap');

      // 用 Range 精确计算“子串”在页面上的矩形位置，再画黄色块（支持跨 span）
      const containerRect = (pageWrap || layer).getBoundingClientRect();
      const segs = Array.isArray(m?.segments) ? m.segments : [];
      for (const seg of segs) {
        const segEl = seg?.el;
        if (!segEl) continue;

        const textNode = segEl.firstChild;
        if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue;
        try {
          const range = document.createRange();
          range.setStart(textNode, Math.max(0, seg.startIdx ?? 0));
          range.setEnd(textNode, Math.max(0, seg.endIdx ?? 0));
          const rectList = Array.from(range.getClientRects());
          if (rectList.length) {
            rectList.forEach(r => {
              const mark = document.createElement('div');
              mark.className = 'highlight-rect';
              mark.style.zIndex = '5';
              mark.style.left = (r.left - containerRect.left) + 'px';
              mark.style.top = (r.top - containerRect.top) + 'px';
              mark.style.width = r.width + 'px';
              mark.style.height = r.height + 'px';
              (pageWrap || layer).appendChild(mark);
            });
          } else {
            // Range 兜底为空时：用 span 自身的矩形画块（精度低，但尽量可见）
            const r = segEl.getBoundingClientRect();
            if (r.width > 0 && r.height > 0) {
              const mark = document.createElement('div');
              mark.className = 'highlight-rect';
              mark.style.zIndex = '5';
              mark.style.left = (r.left - containerRect.left) + 'px';
              mark.style.top = (r.top - containerRect.top) + 'px';
              mark.style.width = r.width + 'px';
              mark.style.height = r.height + 'px';
              (pageWrap || 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-rect) {
  position: absolute;
  background: #ff0;
  opacity: 0.6;
  border-radius: 2px;
  pointer-events: none;
  z-index: 5;
}

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

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