import Foundation /// 页图接管决策 enum RDEPUBPageMapTakeoverDecision { /// 保持当前窗口不变 case keepCurrentWindow /// 扩窗:将候选段合并到当前窗口 case expandWindow(RDEPUBBackgroundCoverageSegment) /// 分段替换:用候选段替换当前窗口的部分内容 case segmentReplace(RDEPUBBackgroundCoverageSegment) /// 全量替换:用完整页图替换当前窗口 case fullReplace(RDEPUBBookPageMap) } /// 页图协调器:负责判断后台解析结果是否可以接管前台窗口 final class RDEPUBPageMapReconciliationCoordinator { private unowned let context: RDEPUBReaderContext init(context: RDEPUBReaderContext) { self.context = context } /// 判断是否可以接管前台窗口 func evaluateTakeover( candidatePageMap: RDEPUBBookPageMap?, candidateSegment: RDEPUBBackgroundCoverageSegment?, currentWindow: RDEPUBBookPageMap?, jumpSession: RDEPUBJumpSession? ) -> RDEPUBPageMapTakeoverDecision { // 如果没有当前窗口,允许接管 guard let currentWindow else { if let candidatePageMap { return .fullReplace(candidatePageMap) } return .keepCurrentWindow } // 获取当前阅读位置 let currentSpineIndex = context.runtime?.locationCoordinator.currentVisibleLocation() .flatMap { context.normalizedSpineIndex(for: $0) } let lastBuildableSpineIndex = context.publication?.spine.indices .reversed() .first(where: { index in guard let item = context.publication?.spine[index] else { return false } return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml")) }) ?? 0 // 检查 JumpSession 保护 if let jumpSession { let protectedIndices = jumpSession.protectedSpineIndices if let currentSpineIndex, protectedIndices.contains(currentSpineIndex) { // 在保护区内,检查候选是否覆盖保护区 if let candidateSegment { let candidateIndices = candidateSegment.resolvedSpineIndices let coverageRatio = Double(protectedIndices.intersection(candidateIndices).count) / Double(protectedIndices.count) if coverageRatio < 0.8 { RDEPUBBackgroundTrace.log( "Reconciliation", "blocked: candidate doesn't cover protected area (coverage=\(coverageRatio))" ) return .keepCurrentWindow } } } } // 检查边界章节覆盖 if let currentSpineIndex { let requiresAdjacentCoverage = currentSpineIndex > 0 && currentSpineIndex < lastBuildableSpineIndex if requiresAdjacentCoverage { if let candidateSegment { let hasPrev = candidateSegment.contains(spineIndex: currentSpineIndex - 1) let hasNext = candidateSegment.contains(spineIndex: currentSpineIndex + 1) if !hasPrev || !hasNext { RDEPUBBackgroundTrace.log( "Reconciliation", "blocked: missing adjacent chapter coverage" ) return .keepCurrentWindow } } } } // 检查渲染签名一致性 if let candidateSegment { let currentRenderSignature = context.currentRenderSignature() if candidateSegment.renderSignature != currentRenderSignature { RDEPUBBackgroundTrace.log( "Reconciliation", "blocked: render signature mismatch" ) return .keepCurrentWindow } } // 评估接管类型 if let candidateSegment { return evaluateSegmentTakeover( candidateSegment: candidateSegment, currentWindow: currentWindow, currentSpineIndex: currentSpineIndex, lastBuildableSpineIndex: lastBuildableSpineIndex ) } if let candidatePageMap { return evaluateFullPageMapTakeover( candidatePageMap: candidatePageMap, currentWindow: currentWindow, currentSpineIndex: currentSpineIndex, lastBuildableSpineIndex: lastBuildableSpineIndex ) } return .keepCurrentWindow } /// 评估分段接管 private func evaluateSegmentTakeover( candidateSegment: RDEPUBBackgroundCoverageSegment, currentWindow: RDEPUBBookPageMap, currentSpineIndex: Int?, lastBuildableSpineIndex: Int ) -> RDEPUBPageMapTakeoverDecision { let currentIndices = Set(currentWindow.entries.map { $0.spineIndex }) let candidateIndices = candidateSegment.resolvedSpineIndices // 检查是否覆盖当前阅读位置 if let currentSpineIndex { if !candidateIndices.contains(currentSpineIndex) { return .keepCurrentWindow } } // 检查是否覆盖相邻章节 if let currentSpineIndex { let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0 let hasNext = candidateIndices.contains(currentSpineIndex + 1) || currentSpineIndex == lastBuildableSpineIndex if !hasPrev || !hasNext { return .keepCurrentWindow } } // 检查是否与当前窗口连续 let isContinuous = currentIndices.contains(candidateSegment.lowerSpineIndex - 1) || currentIndices.contains(candidateSegment.upperSpineIndex + 1) || candidateIndices.contains(currentWindow.entries.first?.spineIndex ?? Int.max) || candidateIndices.contains(currentWindow.entries.last?.spineIndex ?? Int.min) if isContinuous { // 连续,可以扩窗 return .expandWindow(candidateSegment) } else { // 不连续,检查是否覆盖当前窗口的大部分 let overlap = currentIndices.intersection(candidateIndices) let overlapRatio = Double(overlap.count) / Double(currentIndices.count) if overlapRatio > 0.5 { // 覆盖大部分,可以替换 return .segmentReplace(candidateSegment) } } return .keepCurrentWindow } /// 评估全量页图接管 private func evaluateFullPageMapTakeover( candidatePageMap: RDEPUBBookPageMap, currentWindow: RDEPUBBookPageMap, currentSpineIndex: Int?, lastBuildableSpineIndex: Int ) -> RDEPUBPageMapTakeoverDecision { let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex }) // 检查是否覆盖当前阅读位置 if let currentSpineIndex { if !candidateIndices.contains(currentSpineIndex) { return .keepCurrentWindow } } // 检查是否覆盖相邻章节 if let currentSpineIndex { let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0 let hasNext = candidateIndices.contains(currentSpineIndex + 1) || currentSpineIndex == lastBuildableSpineIndex if !hasPrev || !hasNext { return .keepCurrentWindow } } // 检查是否完整覆盖 let isComplete = candidateIndices.count >= currentWindow.entries.count if isComplete { return .fullReplace(candidatePageMap) } return .keepCurrentWindow } /// 生成保护区域的 spineIndex 集合 func protectedSpineIndices( currentSpineIndex: Int?, jumpSession: RDEPUBJumpSession? ) -> Set { var indices: Set = [] if let currentSpineIndex { indices.insert(currentSpineIndex) // 添加相邻章节 if currentSpineIndex > 0 { indices.insert(currentSpineIndex - 1) } indices.insert(currentSpineIndex + 1) } if let jumpSession { indices.formUnion(jumpSession.protectedSpineIndices) } return indices } }