<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 字段，用 shallowRef 保持为原始对象引用
		const pdfDocRef = shallowRef(null);
		const searchKey = ref("");
		const matchList = ref([]);
		const matchIdx = ref(0);
		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;
		};

		// 清空所有高亮
		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
		const renderPage = async (pdf, pageNum) => {
			const pdfPage = await pdf.getPage(pageNum);
			const canvas = canvasMap[pageNum];
			const textLayer = overlayMap[pageNum];

			if (!canvas || !textLayer) return;

			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.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 = "";
			textLayer.style.setProperty("--scale-factor", String(viewport.scale || 1));

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

			try {
				const textContent = await pdfPage.getTextContent();
				let api = pdfjsApiRef.value || pdfjsLib;
				let rt = api?.renderTextLayer;
				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,
						textDivs: [],
						enhanceTextSelection: false
					}).promise;
				}
			} 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;
			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();

			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
				}
			}

			// 滚动到匹配位置
			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" });
			}
		};

		// 获取容器元素（用于同步滚动）
		const getContainer = () => {
			const firstCanvas = canvasMap[1];
			return firstCanvas ? firstCanvas.closest(".report-box") : null;
		};

		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,
			getContainer
		};
	}
};
</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;
	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>
