<template>
    <div class="document-search">
        <div class="viewer-container">
            <transition name="slide-fade">
                <div v-if="showSearchBar" class="floating-search">
                    <div class="search-toolbar" :class="{ 'is-searching': searching }">
                        <div class="search-box">
                            <el-input v-model="keyword" class="search-input" placeholder="输入关键字搜索" clearable
                                ref="keywordInput" @keyup.enter.native="handleSearch" @clear="resetSearch">
                            </el-input>
                            <button class="icon-btn search-icon" :disabled="searching" @click="handleSearch">
                                <i class="el-icon-search" v-if="!searching"></i>
                                <i class="el-icon-loading" v-else></i>
                            </button>
                        </div>
                        <div class="search-status">
                            <span class="result-indicator">{{ resultDisplay }}</span>
                            <button class="icon-btn" :disabled="!searchResults.length" @click="goToPrevious">
                                <i class="el-icon-arrow-up"></i>
                            </button>
                            <button class="icon-btn" :disabled="!searchResults.length" @click="goToNext">
                                <i class="el-icon-arrow-down"></i>
                            </button>
                            <button class="icon-btn" @click="handleCloseSearch">
                                <i class="el-icon-close"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </transition>
            <button v-if="!showSearchBar" class="floating-search-btn" @click="toggleSearchBar">
                <i class="el-icon-search"></i>
            </button>
            <div ref="pdfWrapper" class="pdf-wrapper"></div>

            <transition name="fade">
                <div v-if="overlayType === 'loading'" key="loading" class="state-panel overlay">
                    <i class="el-icon-loading state-icon"></i>
                    <p>正在加载 PDF，请稍候...</p>
                </div>
                <div v-else-if="overlayType === 'error'" key="error" class="state-panel overlay">
                    <i class="el-icon-warning-outline state-icon"></i>
                    <p>{{ error }}</p>
                    <el-button type="primary" @click="reload">重新加载</el-button>
                </div>
                <div v-else-if="overlayType === 'no-url'" key="no-url" class="state-panel overlay">
                    <i class="el-icon-document state-icon"></i>
                    <p>暂未指定 PDF 文件，请通过路由参数 url 传入文件地址。</p>
                </div>
            </transition>
        </div>
    </div>
</template>

<script>
import debounce from 'lodash/debounce'
import 'pdfjs-dist/legacy/web/pdf_viewer.css'
import { EventBus, TextLayerBuilder } from 'pdfjs-dist/legacy/web/pdf_viewer'
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf'
import pdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry'

const resolvedWorkerSrc =
    typeof pdfWorker === 'string'
        ? pdfWorker
        : pdfWorker && pdfWorker.default
            ? pdfWorker.default
            : (pdfWorker && pdfWorker.workerSrc) || null

if (resolvedWorkerSrc) {
    pdfjsLib.GlobalWorkerOptions.workerSrc = resolvedWorkerSrc
} else {
    console.warn('未能解析 PDF.js Worker 地址，将在主线程解析 PDF')
    pdfjsLib.GlobalWorkerOptions.workerSrc = null
}

const DEFAULT_SCALE = 1.20
const DEFAULT_PDF_URL = 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/10/30/fe5b46ea37554516a71e7c0c486d3715.pdf'

export default {
    name: 'DocumentSearch',
    props: {
        fileUrl: {
            type: String,
            default: ''
        },
        showSearchBar: true
    },
    computed: {
        overlayType() {
            if (this.loading) {
                return 'loading'
            }
            if (this.error) {
                return 'error'
            }
            if (!this.pdfUrl) {
                return 'no-url'
            }
            return null
        },
        resultDisplay() {
            const total = this.searchResults.length
            if (!total) return '0/0'
            const current = this.currentResultIndex >= 0 ? this.currentResultIndex + 1 : 0
            return `${current}/${total}`
        }
    },
    data() {
        return {
            pdfUrl: '',
            pdfDoc: null,
            keyword: '',
            loading: false,
            error: null,
            scale: DEFAULT_SCALE,
            eventBus: new EventBus(),
            pageTextDivs: [],
            searchResults: [],
            currentResultIndex: -1,
            pageMatchRanges: [],
            renderedPages: new Map(),
            observer: null,
            pageContainers: [],
            totalPages: 0,
            isDocumentReady: false,
            renderQueue: [],
            isProcessingQueue: false,
            pageCache: new Map(),
            isPrefetching: false,
            prefetchHandle: null,
            prefetchScheduled: false,
            initialPreloadedCount: 0,
            cMapUrl: '',
            standardFontDataUrl: '',
            pdfAssetsBase: '',
            searching: false,
            scrollAnimationFrame: null,
        }
    },
    mounted() {
        const base = (process.env.BASE_URL || '/').replace(/\/+$/, '/')
        this.pdfAssetsBase = `${base}pdfjs/`
        this.cMapUrl = `${this.pdfAssetsBase}cmaps/`
        this.standardFontDataUrl = `${this.pdfAssetsBase}standard_fonts/`

        pdfjsLib.GlobalCMapOptions = {
            url: this.cMapUrl,
            packed: true,
        }
        if ('GlobalStandardFontDataUrl' in pdfjsLib) {
            pdfjsLib.GlobalStandardFontDataUrl = this.standardFontDataUrl
        }
        if (pdfjsLib.GlobalOptions) {
            pdfjsLib.GlobalOptions.disableFontFace = false
            if ('useSystemFonts' in pdfjsLib.GlobalOptions) {
                pdfjsLib.GlobalOptions.useSystemFonts = true
            }
        }

        if (!this.pdfUrl) {
            const initialUrl = this.fileUrl || this.$route?.query?.url || DEFAULT_PDF_URL
            this.applyPdfUrl(initialUrl)
        }
    },
    beforeDestroy() {
        this.disconnectObserver()
        this.cancelScrollAnimation()
    },
    watch: {
        fileUrl: {
            immediate: true,
            handler(newVal) {
                if (newVal) {
                    this.applyPdfUrl(newVal)
                }
            }
        },
        '$route.query.url'(newUrl, oldUrl) {
            if (this.fileUrl) return
            if (newUrl !== oldUrl) {
                const target = newUrl || DEFAULT_PDF_URL
                this.applyPdfUrl(target)
            }
        },
    },
    methods: {
        toggleSearchBar() {
            this.showSearchBar = true
            this.$nextTick(() => {
                if (this.$refs.keywordInput) {
                    this.$refs.keywordInput.focus()
                }
            })
        },
        handleCloseSearch() {
            this.keyword = ''
            this.resetSearch()
            this.showSearchBar = false
        },
        applyPdfUrl(url) {
            const resolved = url || ''
            if (resolved === this.pdfUrl) {
                if (resolved) {
                    this.loadDocument()
                }
                return
            }
            this.pdfUrl = resolved
            if (this.pdfUrl) {
                this.loadDocument()
            } else {
                this.resetViewerState()
            }
        },
        async loadDocument() {
            if (!this.pdfUrl) return
            this.loading = true
            this.error = null
            this.resetViewerState()
            await this.$nextTick()

            try {
                const headers = {}
                const token = this.$store?.getters?.token || window?.sessionStorage?.getItem('token')
                if (token) {
                    headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`
                }

                const loadingTask = pdfjsLib.getDocument({
                    url: this.pdfUrl,
                    withCredentials: true,
                    httpHeaders: headers,
                    disableWorker: !resolvedWorkerSrc,
                    useWorkerFetch: !!resolvedWorkerSrc,
                    cMapUrl: this.cMapUrl,
                    cMapPacked: true,
                    standardFontDataUrl: this.standardFontDataUrl,
                    useSystemFonts: true,
                    fontExtraProperties: true,
                })
                this.pdfDoc = await loadingTask.promise
                console.log('PDF 文档加载成功', this.pdfDoc)
                await this.renderAllPages()
                if (this.keyword) {
                    await this.highlightMatches()
                }
            } catch (err) {
                console.error('加载 PDF 失败:', err)
                this.error = err?.message || 'PDF 文件加载失败，请稍后再试'
            } finally {
                this.loading = false
            }
        },

        async renderAllPages() {
            if (!this.pdfDoc) return
            const container = this.$refs.pdfWrapper
            if (!container) return
            container.innerHTML = ''
            container.style.minWidth = ''
            this.pageTextDivs = []
            this.renderedPages.clear()
            this.pageContainers = []
            this.totalPages = this.pdfDoc.numPages
            this.renderQueue = []
            this.isProcessingQueue = false
            this.pageCache.clear()
            this.isPrefetching = false
            this.initialPreloadedCount = 0

            const fragment = document.createDocumentFragment()
            for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber += 1) {
                const placeholder = document.createElement('div')
                placeholder.className = 'pdf-page placeholder'
                placeholder.style.margin = '0px auto 10px'
                placeholder.style.position = 'relative';
                placeholder.dataset.page = pageNumber
                placeholder.dataset.status = 'placeholder'
                fragment.appendChild(placeholder)
                this.pageContainers.push(placeholder)
            }
            container.appendChild(fragment)
            container.scrollTop = 0

            this.setupIntersectionObserver()
            await this.preloadInitialPages()
            this.observeInitialPages()
            this.schedulePrefetch()
            this.isDocumentReady = true
        },

        ensureContainerDimensions(pageNumber, viewport) {
            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return

            container.style.width = `${viewport.width}px`
            container.style.height = `${viewport.height}px`
            container.style.margin = '0px auto 10px'
            container.style.backgroundColor = '#fff';
            if (!container.dataset.status || container.dataset.status === 'placeholder') {
                container.dataset.status = 'prefetched'
            }
            container.classList.remove('placeholder')
            if (container.dataset.status !== 'rendered') {
                container.classList.add('prefetched')
            }

            if (this.$refs.pdfWrapper && !this.$refs.pdfWrapper.style.minWidth) {
                this.$refs.pdfWrapper.style.minWidth = `${Math.ceil(viewport.width)}px`
            }
        },

        async ensurePageCached(pageNumber) {
            if (!this.pdfDoc) return null
            if (this.pageCache.has(pageNumber)) {
                return this.pageCache.get(pageNumber)
            }
            const page = await this.pdfDoc.getPage(pageNumber)
            this.pageCache.set(pageNumber, page)
            return page
        },

        async renderCanvas(pageNumber) {
            const page = await this.ensurePageCached(pageNumber)
            if (!page) return null

            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return null

            const viewport = page.getViewport({ scale: this.scale })
            this.ensureContainerDimensions(pageNumber, viewport)
            container.classList.add('is-loading')

            const oldCanvas = container.querySelector('.pdf-canvas')
            if (oldCanvas) {
                oldCanvas.remove()
            }

            const canvas = document.createElement('canvas')
            canvas.className = 'pdf-canvas'
            const deviceScale = window.devicePixelRatio || 1
            const outputScale = deviceScale > 1 ? deviceScale : 1
            canvas.width = viewport.width * outputScale
            canvas.height = viewport.height * outputScale
            canvas.style.width = `${viewport.width}px`
            canvas.style.height = `${viewport.height}px`
            container.appendChild(canvas)

            const canvasContext = canvas.getContext('2d')
            if (canvasContext && 'imageSmoothingEnabled' in canvasContext) {
                canvasContext.imageSmoothingEnabled = false
            }
            const renderContext = {
                canvasContext,
                viewport,
                transform: [outputScale, 0, 0, outputScale, 0, 0],
                intent: 'print',
                enableWebGL: true,
                background: 'rgba(255,255,255,0)',
            }
            canvasContext.setTransform(outputScale, 0, 0, outputScale, 0, 0)
            canvasContext.imageSmoothingEnabled = false
            await page.render(renderContext).promise

            container.dataset.status = 'rendered'
            container.classList.remove('prefetched')

            const textLayerDiv = container.querySelector('.textLayer')
            if (textLayerDiv) {
                textLayerDiv.style.display = ''
            }

            this.renderedPages.set(pageNumber, { container, viewport })
            container.classList.remove('is-loading')
            return { page, viewport, container }
        },

        async renderTextLayer(pageNumber, { visible = true, force = false } = {}) {
            const page = await this.ensurePageCached(pageNumber)
            if (!page) return

            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return

            const viewport = page.getViewport({ scale: this.scale })
            this.ensureContainerDimensions(pageNumber, viewport)
            container.classList.add('is-loading-text')

            const existing = container.querySelector('.textLayer')
            if (existing && !force && this.pageTextDivs[index]?.length) {
                existing.style.display = ''
                container.classList.remove('is-loading-text')
                return
            }
            if (existing) {
                existing.remove()
            }

            const textLayerDiv = document.createElement('div')
            textLayerDiv.className = 'textLayer'
            textLayerDiv.style.width = `${viewport.width}px`
            textLayerDiv.style.height = `${viewport.height}px`
            textLayerDiv.style.display = ''
            container.appendChild(textLayerDiv)
            const textLayer = new TextLayerBuilder({
                textLayerDiv,
                pageIndex: index,
                viewport,
                eventBus: this.eventBus,
            })

            try {
                const textContent = await page.getTextContent()
                textLayer.setTextContent(textContent)
                textLayer.render()

                const textDivs = [...textLayer.textDivs]
                textDivs.forEach((div) => {
                    div.dataset.originalText = div.textContent
                })
                this.pageTextDivs[index] = textDivs
            } catch (error) {
                console.warn(`加载第 ${pageNumber} 页文本失败`, error)
            } finally {
                container.classList.remove('is-loading-text')
            }

            if (this.keyword.trim() && this.pageMatchRanges && this.pageMatchRanges.length) {
                this.applyHighlightsToPage(index)
            }

            if (container.dataset.status !== 'rendered') {
                container.dataset.status = visible ? 'rendered' : 'text-ready'
                if (!visible) {
                    container.classList.add('prefetched')
                }
            }
        },

        async renderSinglePage(pageNumber) {
            const container = this.pageContainers[pageNumber - 1]
            if (container) {
                container.classList.add('is-loading', 'is-loading-text')
            }
            try {
                await this.renderTextLayer(pageNumber, { visible: true, force: true })
                await this.renderCanvas(pageNumber)
            } finally {
                if (container) {
                    container.classList.remove('is-loading', 'is-loading-text')
                }
            }
        },

        scheduleRender(pageNumber, { priority = false } = {}) {
            if (!pageNumber || pageNumber > this.totalPages || this.renderedPages.has(pageNumber)) return
            if (this.renderQueue.includes(pageNumber)) return
            if (priority) {
                this.renderQueue.unshift(pageNumber)
            } else {
                this.renderQueue.push(pageNumber)
            }
            this.processRenderQueue()
        },

        async processRenderQueue() {
            if (this.isProcessingQueue) return
            this.isProcessingQueue = true
            try {
                while (this.renderQueue.length) {
                    const pageNumber = this.renderQueue.shift()
                    await new Promise((resolve) => requestAnimationFrame(resolve))
                    await this.renderSinglePage(pageNumber)
                }
            } finally {
                this.isProcessingQueue = false
            }
        },

        schedulePrefetch() {
            this.cancelPrefetch()
            if (!this.totalPages) return
            const startPage = Math.min(this.totalPages, Math.max(1, this.initialPreloadedCount + 1))
            let pageNumber = startPage

            const fetchNext = async () => {
                this.prefetchScheduled = false
                this.prefetchHandle = null
                if (pageNumber > this.totalPages) {
                    this.isPrefetching = false
                    return
                }
                if (!this.pageCache.has(pageNumber)) {
                    try {
                        const page = await this.pdfDoc.getPage(pageNumber)
                        this.pageCache.set(pageNumber, page)
                        const viewport = page.getViewport({ scale: this.scale })
                        this.ensureContainerDimensions(pageNumber, viewport)
                    } catch (error) {
                        console.warn(`预取第 ${pageNumber} 页失败`, error)
                    }
                }
                pageNumber += 1
                if (pageNumber <= this.totalPages) {
                    const schedule = () => {
                        this.prefetchScheduled = true
                        if (typeof window.requestIdleCallback === 'function') {
                            this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 1000 })
                        } else {
                            this.prefetchHandle = window.setTimeout(fetchNext, 80)
                        }
                    }
                    schedule()
                } else {
                    this.isPrefetching = false
                }
            }

            this.isPrefetching = true
            if (typeof window.requestIdleCallback === 'function') {
                this.prefetchScheduled = true
                this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 800 })
            } else {
                this.prefetchScheduled = true
                this.prefetchHandle = window.setTimeout(fetchNext, 120)
            }
        },

        cancelPrefetch() {
            if (this.prefetchHandle) {
                if (typeof window.cancelIdleCallback === 'function' && this.prefetchScheduled) {
                    window.cancelIdleCallback(this.prefetchHandle)
                } else {
                    clearTimeout(this.prefetchHandle)
                }
            }
            this.prefetchHandle = null
            this.prefetchScheduled = false
            this.isPrefetching = false
        },

        async preloadInitialPages() {
            const preloadCount = Math.min(3, this.totalPages)
            if (!preloadCount) {
                this.initialPreloadedCount = 0
                return
            }

            for (let pageNumber = 1; pageNumber <= preloadCount; pageNumber += 1) {
                await this.renderTextLayer(pageNumber, { visible: pageNumber === 1 })
            }

            if (preloadCount >= 1) {
                await this.renderCanvas(1)
            }

            this.initialPreloadedCount = preloadCount
        },

        unloadPage(pageNumber) {
            if (!pageNumber) return
            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container || container.dataset.status !== 'rendered') return

            const canvas = container.querySelector('.pdf-canvas')
            if (canvas) {
                canvas.remove()
            }
            const textLayer = container.querySelector('.textLayer')
            if (textLayer) {
                textLayer.style.display = ''
            }

            container.classList.remove('is-loading')
            this.renderedPages.delete(pageNumber)
            container.dataset.status = this.pageTextDivs[index]?.length ? 'text-ready' : 'prefetched'
            container.classList.add('prefetched')
        },

        handleSearch: debounce(async function () {
            const keyword = this.keyword.trim()
            if (!keyword) {
                this.resetSearch()
                return
            }
            if (!this.pdfDoc) return

            // if (this.searching) return
            this.searching = true
            await this.$nextTick()
            try {
                const allPrepared = this.pageTextDivs.length === this.totalPages && this.pageTextDivs.every(items => items && items.length)
                if (!allPrepared) {
                    try {
                        await this.ensureAllTextLayersLoaded()
                    } catch (error) {
                        console.error('全文预处理失败', error)
                    }
                }


                await this.highlightMatches()
            } finally {
                this.searching = false
            }
        }, 200),

        async ensureAllTextLayersLoaded() {
            if (!this.pdfDoc || !this.totalPages) return
            let total = this.totalPages
            for (let pageNumber = 1; pageNumber <= total; pageNumber += 1) {
                if (this.totalPages !== total) break
                if (!this.pageTextDivs[pageNumber - 1] || !this.pageTextDivs[pageNumber - 1].length) {
                    await this.renderTextLayer(pageNumber, { visible: pageNumber === 1, force: true })
                    await this.$nextTick()
                }
            }
        },

        async highlightMatches(options = {}) {
            const { preserveIndex = false, skipNavigate = false } = options
            const keyword = this.keyword.trim()
            if (!keyword) {
                this.resetSearch()
                return
            }

            const previousIndex = preserveIndex ? this.currentResultIndex : 0

            this.clearHighlights()
            const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
            const pattern = new RegExp(`(${escapedKeyword})`, 'gi')
            const results = []
            const matchRanges = Array.from({ length: this.totalPages || 0 }, () => [])

            for (let pageIndex = 0; pageIndex < this.totalPages; pageIndex += 1) {
                let textDivs = this.pageTextDivs[pageIndex]
                if (!textDivs || !textDivs.length) {
                    const container = this.pageContainers[pageIndex]
                    const isRendered = container?.dataset.status === 'rendered'
                    await this.renderTextLayer(pageIndex + 1, { visible: !!isRendered, force: true })
                    await this.$nextTick()
                    textDivs = this.pageTextDivs[pageIndex]
                }
                if (!textDivs || !textDivs.length) {
                    continue
                }

                const segments = []
                let position = 0
                textDivs.forEach((div, divIndex) => {
                    const original = div.dataset.originalText || div.textContent || ''
                    segments.push({
                        div,
                        divIndex,
                        text: original,
                        start: position,
                        end: position + original.length,
                    })
                    position += original.length
                })

                if (!segments.length) {
                    continue
                }

                const pageText = segments.map((seg) => seg.text).join('')
                if (!pageText) {
                    continue
                }

                pattern.lastIndex = 0
                const perDivHighlights = new Map()
                const matchRecords = []

                let match
                while ((match = pattern.exec(pageText)) !== null) {
                    if (!match[0]) continue
                    const start = match.index
                    const end = start + match[0].length
                    const matchIndex = matchRecords.length
                    matchRecords.push({
                        pageIndex,
                        start,
                        end,
                        elements: [],
                        segments: [],
                    })

                    let segIndex = segments.findIndex((seg) => start < seg.end && end > seg.start)
                    if (segIndex === -1) continue

                    let currentStart = start
                    while (segIndex < segments.length && currentStart < end) {
                        const seg = segments[segIndex]
                        const highlightStart = Math.max(0, currentStart - seg.start)
                        const highlightEnd = Math.min(seg.text.length, end - seg.start)
                        if (highlightEnd > highlightStart) {
                            const ranges = perDivHighlights.get(seg.div) || []
                            ranges.push({
                                start: highlightStart,
                                end: highlightEnd,
                                matchIndex,
                            })
                            perDivHighlights.set(seg.div, ranges)
                            matchRecords[matchIndex].segments.push({
                                divIndex: seg.divIndex,
                                start: highlightStart,
                                end: highlightEnd,
                            })
                        }
                        if (end <= seg.end) break
                        currentStart = seg.end
                        segIndex += 1
                    }
                }

                if (!matchRecords.length) {
                    continue
                }

                perDivHighlights.forEach((ranges, div) => {
                    const original = div.dataset.originalText || div.textContent || ''
                    if (!original) return

                    const sorted = ranges
                        .slice()
                        .sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start))

                    let cursor = 0
                    let html = ''
                    sorted.forEach(({ start, end, matchIndex }) => {
                        if (start > cursor) {
                            html += this.escapeForHtml(original.slice(cursor, start))
                        }
                        const text = original.slice(start, end)
                        html += `<mark class="search-highlight" data-match-index="${matchIndex}">${this.escapeForHtml(text)}</mark>`
                        cursor = end
                    })
                    if (cursor < original.length) {
                        html += this.escapeForHtml(original.slice(cursor))
                    }
                    div.innerHTML = html
                })

                perDivHighlights.forEach((_ranges, div) => {
                    const marks = div.querySelectorAll('mark.search-highlight')
                    marks.forEach((mark) => {
                        const matchIndex = Number(mark.dataset.matchIndex)
                        if (!Number.isNaN(matchIndex) && matchRecords[matchIndex]) {
                            matchRecords[matchIndex].elements.push(mark)
                            mark.dataset.pageIndex = String(pageIndex)
                        }
                    })
                })

                const pageMatches = matchRanges[pageIndex]
                matchRecords.forEach((record) => {
                    if (!record.elements.length) return
                    const newIndex = results.length
                    record.elements.forEach((mark) => {
                        mark.dataset.matchIndex = String(newIndex)
                    })
                    results.push({
                        pageIndex,
                        element: record.elements[0],
                        elements: record.elements,
                    })
                    pageMatches.push({
                        matchIndex: newIndex,
                        segments: record.segments.map((item) => ({ ...item })),
                    })
                })
            }

            this.pageMatchRanges = matchRanges
            this.searchResults = results
            if (!results.length) {
                this.currentResultIndex = -1
                this.$message.info(`未找到“${keyword}”相关内容`)
                return
            }

            if (preserveIndex && previousIndex < results.length && previousIndex >= 0) {
                this.currentResultIndex = previousIndex
            } else {
                this.currentResultIndex = 0
            }

            if (!skipNavigate) {
                await this.navigateToResult(this.currentResultIndex, true, false)
            } else {
                this.scheduleActiveHighlightRefresh()
            }
        },

        focusCurrentResult() {
            this.navigateToResult(this.currentResultIndex)
        },

        async navigateToResult(index, ensureRendered = false, useSmoothScroll = true) {
            if (!this.searchResults.length || index < 0 || index >= this.searchResults.length) return
            const currentResult = this.searchResults[index]
            const pageNumber = currentResult.pageIndex + 1

            if (ensureRendered || !this.pageTextDivs[pageNumber - 1] || !this.pageTextDivs[pageNumber - 1].length) {
                await this.renderTextLayer(pageNumber, { visible: true, force: true })
                await this.renderCanvas(pageNumber)
                await this.$nextTick()
                this.applyHighlightsToPage(pageNumber - 1)
                this.scheduleActiveHighlightRefresh()
            }

            this.currentResultIndex = index
            this.scheduleActiveHighlightRefresh()

            const performScroll = () => {
                const target = this.searchResults[this.currentResultIndex]?.element
                if (!target) return
                const wrapper = this.$refs.pdfWrapper
                if (!wrapper) return

                if (!useSmoothScroll) {
                    // 直接跳转到匹配位置，不做平滑滚动动画
                    try {
                        target.scrollIntoView({
                            behavior: 'auto',
                            block: 'center',
                            inline: 'nearest',
                        })
                    } catch (e) {
                        // 兼容性兜底
                        const container = target.closest('.pdf-page') || target
                        if (!container) return
                        const wrapperOffsetTop = container.offsetTop
                        const containerHeight = container.offsetHeight || target.offsetHeight || 0
                        const desired = wrapperOffsetTop - Math.max((wrapper.clientHeight - containerHeight) / 2, 0)
                        this.cancelScrollAnimation()
                        wrapper.scrollTop = desired
                    }
                    return
                }

                const container = target.closest('.pdf-page') || target
                if (!container) return
                const wrapperOffsetTop = container.offsetTop
                const containerHeight = container.offsetHeight || target.offsetHeight || 0
                const desired = wrapperOffsetTop - Math.max((wrapper.clientHeight - containerHeight) / 2, 0)
                this.smoothScrollTo(wrapper, desired)
            }

            if (useSmoothScroll) {
                this.$nextTick(() => performScroll())
            } else {
                performScroll()
            }
        },

        applyHighlightsToPage(pageIndex) {
            if (!this.keyword || !this.pageMatchRanges || !this.pageMatchRanges.length) return
            const textDivs = this.pageTextDivs[pageIndex]
            if (!textDivs || !textDivs.length) return

            const pageMatches = this.pageMatchRanges[pageIndex]
            if (!pageMatches || !pageMatches.length) return

            textDivs.forEach((div) => {
                const original = div.dataset.originalText
                if (typeof original !== 'undefined') {
                    div.textContent = original
                }
            })

            const perDivRanges = new Map()
            pageMatches.forEach((match) => {
                if (!match || !Array.isArray(match.segments)) return
                match.segments.forEach((segment) => {
                    const { divIndex, start, end } = segment || {}
                    if (typeof divIndex !== 'number') return
                    const ranges = perDivRanges.get(divIndex) || []
                    ranges.push({
                        start,
                        end,
                        matchIndex: match.matchIndex,
                    })
                    perDivRanges.set(divIndex, ranges)
                })
            })

            const updatedElements = new Map()

            perDivRanges.forEach((ranges, divIndex) => {
                const div = textDivs[divIndex]
                if (!div) return
                const original = div.dataset.originalText || ''
                if (!original) return

                const sorted = ranges
                    .slice()
                    .filter((item) => typeof item.start === 'number' && typeof item.end === 'number')
                    .sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start))

                let cursor = 0
                let html = ''
                sorted.forEach(({ start, end, matchIndex }) => {
                    if (start > cursor) {
                        html += this.escapeForHtml(original.slice(cursor, start))
                    }
                    const text = original.slice(start, end)
                    html += `<mark class="search-highlight" data-match-index="${matchIndex}">${this.escapeForHtml(text)}</mark>`
                    cursor = end
                })
                if (cursor < original.length) {
                    html += this.escapeForHtml(original.slice(cursor))
                }
                div.innerHTML = html

                const marks = div.querySelectorAll('mark.search-highlight')
                marks.forEach((mark) => {
                    const matchIndex = Number(mark.dataset.matchIndex)
                    if (Number.isNaN(matchIndex)) return
                    const elements = updatedElements.get(matchIndex) || []
                    elements.push(mark)
                    updatedElements.set(matchIndex, elements)
                })
            })

            updatedElements.forEach((elements, matchIndex) => {
                if (!this.searchResults[matchIndex]) return
                this.searchResults[matchIndex].elements = elements
                this.searchResults[matchIndex].element = elements[0] || null
            })

            this.scheduleActiveHighlightRefresh()
        },

        updateCurrentResultActiveState() {
            const activePageContainers = new Set()

            this.searchResults.forEach((item, idx) => {
                if (!item) return
                const elements = item.elements && item.elements.length ? item.elements : item.element ? [item.element] : []
                elements.forEach((el) => {
                    if (!el) return
                    if (idx === this.currentResultIndex) {
                        el.classList.add('is-active', 'is-current')
                        const container = el.closest('.pdf-page')
                        if (container) {
                            activePageContainers.add(container)
                        }
                    } else {
                        el.classList.remove('is-active', 'is-current')
                    }
                })
            })

            this.pageContainers.forEach((container) => {
                if (!container) return
                container.classList.remove('has-active-match')
            })
            activePageContainers.forEach((container) => {
                container.classList.add('has-active-match')
            })
        },

        scheduleActiveHighlightRefresh() {
            this.$nextTick(() => {
                if (typeof window !== 'undefined' && window.requestAnimationFrame) {
                    window.requestAnimationFrame(() => {
                        this.updateCurrentResultActiveState()
                    })
                } else {
                    this.updateCurrentResultActiveState()
                }
            })
        },

        cancelScrollAnimation() {
            if (this.scrollAnimationFrame !== null && typeof window !== 'undefined') {
                cancelAnimationFrame(this.scrollAnimationFrame)
                this.scrollAnimationFrame = null
            }
        },

        smoothScrollTo(container, target, baseDuration = 240) {
            if (!container) return
            const maxScroll = container.scrollHeight - container.clientHeight
            const finalTarget = Math.min(Math.max(target, 0), Math.max(maxScroll, 0))
            const start = container.scrollTop
            const change = finalTarget - start
            if (Math.abs(change) < 1) {
                container.scrollTop = finalTarget
                return
            }

            const distanceFactor = Math.min(Math.abs(change) / Math.max(container.clientHeight, 1), 2.4)
            const duration = Math.min(460, baseDuration + distanceFactor * 110)

            const startTime = performance.now()
            const ease = (t) => 1 - Math.pow(1 - t, 4)
            const velocityBoost = Math.min(Math.max(Math.abs(change) / 2400, 0), 0.25)

            this.cancelScrollAnimation()

            const step = (now) => {
                const elapsed = now - startTime
                const progress = Math.min(elapsed / duration, 1)
                const eased = ease(progress)
                const basePosition = start + change * eased
                const overshoot = velocityBoost * Math.sin(eased * Math.PI)
                container.scrollTop = Math.min(Math.max(basePosition + overshoot * change, 0), maxScroll)

                if (progress < 1) {
                    this.scrollAnimationFrame = requestAnimationFrame(step)
                } else {
                    this.scrollAnimationFrame = null
                }
            }

            this.scrollAnimationFrame = requestAnimationFrame(step)
        },

        async goToPrevious() {
            if (!this.searchResults.length) return
            let targetIndex = this.currentResultIndex
            if (targetIndex === -1) {
                targetIndex = this.searchResults.length - 1
            } else {
                targetIndex -= 1
                if (targetIndex < 0) {
                    targetIndex = this.searchResults.length - 1
                }
            }
            await this.navigateToResult(targetIndex, false, false)
        },

        async goToNext() {
            if (!this.searchResults.length) return
            let targetIndex = this.currentResultIndex
            if (targetIndex === -1) {
                targetIndex = 0
            } else {
                targetIndex += 1
                if (targetIndex > this.searchResults.length - 1) {
                    targetIndex = 0
                }
            }
            await this.navigateToResult(targetIndex, false, false)
        },

        resetSearch() {
            this.keyword = ''
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
            this.pageMatchRanges = []
        },

        clearHighlights() {
            this.pageTextDivs.forEach((textDivs) => {
                if (!textDivs || !textDivs.length) return
                textDivs.forEach((div) => {
                    const original = div.dataset.originalText
                    if (typeof original !== 'undefined') {
                        div.textContent = original
                    }
                })
            })
        },

        resetViewerState() {
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
            this.pageMatchRanges = []
            this.pageTextDivs = []
            this.renderedPages.clear()
            this.pageContainers = []
            this.totalPages = 0
            this.isDocumentReady = false
            this.disconnectObserver()
            this.cancelPrefetch()
            this.pageCache.clear()
            this.cancelScrollAnimation()
            const wrapper = this.$refs.pdfWrapper
            if (wrapper) {
                wrapper.innerHTML = ''
                wrapper.scrollTop = 0
            }
        },

        escapeForHtml(text = '') {
            return text
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;')
        },

        reload() {
            if (!this.pdfUrl) return
            this.loadDocument()
        },

        setupIntersectionObserver() {
            this.disconnectObserver()
            if (!this.$refs.pdfWrapper) return

            const options = {
                root: this.$refs.pdfWrapper,
                rootMargin: '120px 0px',
                threshold: 0.01,
            }

            this.observer = new IntersectionObserver((entries) => {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        const page = Number(entry.target.dataset.page)
                        if (page) {
                            this.renderTextLayer(page, { visible: true, force: true })
                            this.scheduleRender(page, { priority: true })
                        }
                    } else {
                        const page = Number(entry.target.dataset.page)
                        if (page) {
                            this.unloadPage(page)
                        }
                    }
                })
            }, options)

            this.pageContainers.forEach((container) => this.observer.observe(container))
        },

        observeInitialPages() {
            if (!this.pageContainers.length) return
            const initial = this.pageContainers.slice(0, 3)
            initial.forEach((container) => {
                const page = Number(container.dataset.page)
                if (page) {
                    if (page === 1) {
                        // 已在预处理中完成
                        return
                    }
                    if (page <= this.initialPreloadedCount) {
                        this.scheduleRender(page, { priority: true })
                    } else {
                        this.scheduleRender(page)
                    }
                }
            })
        },

        disconnectObserver() {
            if (this.observer) {
                this.observer.disconnect()
                this.observer = null
            }
            this.renderQueue = []
            this.isProcessingQueue = false
            this.cancelPrefetch()
        },
    },
}
</script>

<style scoped lang="scss">
.document-search {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 84px);
    overflow: hidden;
    background: #f4f7ff;
    padding: 0;
    box-sizing: border-box;
    font-size: 14px;
    line-height: 1.6;
    color: #1f2430;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

.floating-search {
    position: absolute;
    top: 20px;
    right: 20px;
    width: auto;
    max-width: calc(100% - 40px);
    z-index: 6;
    pointer-events: none;
}

.floating-search-btn {
    position: absolute;
    top: 20px;
    right: 20px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: none;
    background: #2b68ff;
    color: #fff;
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 10px 26px rgba(31, 114, 234, 0.25);
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    z-index: 5;
}

.floating-search-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 12px 30px rgba(31, 114, 234, 0.3);
}

.search-toolbar {
    display: flex;
    align-items: center;
    gap: 14px;
    background: rgba(255, 255, 255, 0.98);
    border-radius: 999px;
    padding: 8px 12px;
    box-shadow: 0 8px 26px rgba(31, 114, 234, 0.18);
    border: 1px solid rgba(226, 231, 239, 0.8);
    pointer-events: auto;
    position: relative;
    overflow: hidden;
}

.search-toolbar::after {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 999px;
    border: 1px solid transparent;
    pointer-events: none;
}

.search-toolbar.is-searching::after {
    border-color: rgba(43, 104, 255, 0.4);
    animation: search-pulse 1.2s ease-in-out infinite;
}

@keyframes search-pulse {
    0% {
        opacity: 0.25;
        transform: scale(0.98);
    }
    50% {
        opacity: 1;
        transform: scale(1);
    }
    100% {
        opacity: 0.25;
        transform: scale(0.98);
    }
}

.slide-fade-enter-active,
.slide-fade-leave-active {
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.slide-fade-enter,
.slide-fade-leave-to {
    opacity: 0;
    transform: translateY(-6px);
}

.search-box {
    display: flex;
    align-items: center;
    gap: 4px;
    background: #fff;
    border: 1px solid #e4e7f0;
    border-radius: 999px;
    padding: 0 6px 0 10px;
}

.search-input {
    width: 190px;
}

.search-input ::v-deep .el-input__inner {
    border: none;
    box-shadow: none;
    background: transparent;
    padding: 0;
    height: 30px;
    font-size: 13px;
}

.search-input ::v-deep .el-input__suffix,
.search-input ::v-deep .el-input__prefix {
    display: none;
}

.icon-btn {
    width: 28px;
    height: 28px;
    border: none;
    background: transparent;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6c7388;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease;
}

.icon-btn:hover {
    background: rgba(64, 158, 255, 0.12);
    color: #2b68ff;
}

.icon-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.search-icon {
    background: #2b68ff;
    color: #fff;
}

.search-icon:hover {
    background: #1d4fd8;
    color: #fff;
}

.search-status {
    display: flex;
    align-items: center;
    gap: 6px;
    color: #4f5875;
    font-size: 12px;
    padding-left: 10px;
    border-left: 1px solid #e4e8f2;
}

.result-indicator {
    font-weight: 600;
    min-width: 48px;
    text-align: center;
}

.viewer-container {
    flex: 1;
    background: #ffffff;
    border-radius: 12px;
    overflow: hidden;
    position: relative;
    display: flex;
    min-height: 480px;
}

.pdf-wrapper {
    flex: 1;
    overflow: auto;
    padding: 24px;
    background: #eaeaea;
    position: relative;
    scroll-behavior: smooth;
    overscroll-behavior: contain;
    scrollbar-gutter: stable both-edges;
    -webkit-overflow-scrolling: touch;
    overflow-x: hidden;
}

.pdf-page {
    position: relative;
    margin: 0px auto 10px !important;
    box-shadow: 0 10px 30px rgba(25, 64, 158, 0.12);
    border-radius: 8px;
    overflow: hidden;
    background: #ffffff;
    display: block;
    will-change: transform, opacity;
    max-width: 100%;
}

.pdf-page:first-of-type::before,
.pdf-page:last-of-type::after {
    display: none;
}

.pdf-page::before,
.pdf-page::after {
    content: '';
    position: absolute;
    left: 5%;
    right: 5%;
    height: 12px;
    height: 8px;
    border-radius: 6px;
    background: linear-gradient(180deg, rgba(206, 216, 232, 0.65) 0%, rgba(208, 216, 230, 0.25) 100%);
    box-shadow: 0 2px 6px rgba(102, 125, 160, 0.25);
}

.pdf-page::before {
    top: -20px;
}

.pdf-page::after {
    bottom: -28px;
}

.pdf-page.placeholder {
    box-shadow: none;
    background: linear-gradient(135deg, rgba(233, 239, 255, 0.7), rgba(255, 255, 255, 0.7));
    border: 1px dashed rgba(112, 143, 255, 0.4);
    min-height: 240px;
    color: #7f8bff;
    font-size: 13px;
    letter-spacing: 0.6px;
}

.pdf-page.placeholder::after {
    content: '正在准备页面...';
}

.pdf-page.prefetched {
    background: #fdfdfd;
    box-shadow: inset 0 0 0 1px rgba(112, 143, 255, 0.2);
    position: relative;
}

.pdf-page.prefetched::after {
    content: '滚动到此处以加载内容';
    color: rgba(112, 143, 255, 0.7);
    font-size: 12px;
    letter-spacing: 0.4px;
}

.pdf-canvas {
    width: 100%;
    display: block;
    margin: 0 auto;
}

.textLayer {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    pointer-events: auto;
    user-select: text;
    -webkit-user-select: text;
}

.textLayer>span {
    cursor: text;
    user-select: text;
    -webkit-user-select: text;
}

.pdf-page.is-loading::after {
    opacity: 0.5;
    background: rgba(255, 255, 255, 0.7);
}

.pdf-page.is-loading .textLayer,
.pdf-page.is-loading .pdf-canvas {
    opacity: 0.35;
}

@keyframes pdf-page-spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}
@keyframes search-glow {
    0%,
    100% {
        box-shadow: 0 0 0 0 rgba(32, 109, 255, 0.45);
    }
    50% {
        box-shadow: 0 0 0 10px rgba(32, 109, 255, 0);
    }
}

@keyframes search-rotate {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

@keyframes search-outline {
    0%,
    100% {
        opacity: 0;
        transform: scale(0.9);
    }
    50% {
        opacity: 1;
        transform: scale(1.05);
    }
}

.pdf-page>canvas.pdf-canvas {
    position: relative;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
}

.pdf-page>.textLayer {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
}

.search-highlight {
    background: rgba(255, 241, 168, 0.9);
    padding: 0 2px;
    border-radius: 2px;
}

::v-deep .search-highlight.is-active,
::v-deep .search-highlight.is-current {
    background: #206dff !important;
    color: #ffffff;
    box-shadow: 0 0 0 2px rgba(32, 109, 255, 0.35);
    border-radius: 4px;
    transition: background 0.2s ease, box-shadow 0.2s ease;
}

.pdf-page.has-active-match {
    box-shadow: 0 0 0 3px rgba(32, 109, 255, 0.45), 0 12px 36px rgba(32, 109, 255, 0.18);
    transition: box-shadow 0.3s ease;
}

::v-deep .textLayer {
    user-select: text;
    pointer-events: auto;
    -webkit-user-select: text;
    -moz-user-select: text;
}

::v-deep .textLayer span,
::v-deep .textLayer mark {
    user-select: text;
    pointer-events: auto;
    -webkit-user-select: text;
    -moz-user-select: text;
}

.state-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: #6072a1;
    gap: 12px;
    font-size: 15px;
    text-align: center;
}

.state-icon {
    font-size: 28px;
    color: #1f72ea;
}

.overlay {
    position: absolute;
    inset: 0;
    z-index: 2;
    background: linear-gradient(180deg, rgba(238, 243, 255, 0.92) 0%, rgba(255, 255, 255, 0.96) 100%);
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.25s ease;
}

.fade-enter,
.fade-leave-to {
    opacity: 0;
}
</style>