import UIKit final class RDEPUBPresentationRuntime { private unowned let context: RDEPUBReaderContext private unowned let locationCoordinator: RDEPUBReaderLocationCoordinator private unowned let jumpSessionManager: RDEPUBJumpSessionManager private unowned let reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator let navigationStateMachine = RDEPUBNavigationStateMachine() private(set) var paginationState = RDEPUBPaginationState() init( context: RDEPUBReaderContext, locationCoordinator: RDEPUBReaderLocationCoordinator, jumpSessionManager: RDEPUBJumpSessionManager, reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator ) { self.context = context self.locationCoordinator = locationCoordinator self.jumpSessionManager = jumpSessionManager self.reconciliationCoordinator = reconciliationCoordinator } func applyBookPageMap( _ bookPageMap: RDEPUBBookPageMap, restoreLocation: RDEPUBLocation?, finishPagination: (RDEPUBLocation?) -> Void ) { navigationStateMachine.transition(to: .presentingWindow) context.textBook = nil context.bookPageMap = bookPageMap context.pendingFullPageMap = nil context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap)) paginationState.activePageMap = bookPageMap paginationState.pendingFullPageMap = nil paginationState.source = .initialPartial finishPagination(restoreLocation) } func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) { if let pendingMap = context.pendingFullPageMap { let shouldKeepExisting = pendingMap.totalChapters > bookPageMap.totalChapters || (pendingMap.totalChapters == bookPageMap.totalChapters && pendingMap.totalPages >= bookPageMap.totalPages) if shouldKeepExisting { return } } navigationStateMachine.transition(to: .reconcilingFullMap) context.pendingFullPageMap = bookPageMap paginationState.pendingFullPageMap = bookPageMap } func applyPendingFullPageMapIfNeeded() { guard let pendingMap = context.pendingFullPageMap, let readerView = context.readerView, let controller = context.controller else { return } guard !controller.isRepaginating else { return } navigationStateMachine.transition(to: .reconcilingFullMap) let decision = reconciliationCoordinator.evaluateTakeover( candidatePageMap: pendingMap, candidateSegment: nil, currentWindow: context.bookPageMap, jumpSession: jumpSessionManager.activeSession ) switch decision { case .keepCurrentWindow: RDEPUBBackgroundTrace.log("Reconciliation", "decision: keepCurrentWindow") case .fullReplace(let newPageMap): RDEPUBBackgroundTrace.log("Reconciliation", "decision: fullReplace") applyFullPageMapReplacement(newPageMap, readerView: readerView, controller: controller) case .expandWindow, .segmentReplace: RDEPUBBackgroundTrace.log("Reconciliation", "decision: unexpected segment decision") } } func applyExtendedPartialPageMap( _ bookPageMap: RDEPUBBookPageMap, currentPageNumber: Int, currentLocation: RDEPUBLocation? ) { guard let readerView = context.readerView else { return } navigationStateMachine.transition(to: .presentingWindow) context.bookPageMap = bookPageMap context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap)) paginationState.activePageMap = bookPageMap paginationState.source = .asyncExtension readerView.reloadPageCountOnly() if let currentLocation, locationCoordinator.restoreReadingLocation(currentLocation, animated: false) { return } readerView.transitionToPage(pageNum: max(currentPageNumber - 1, 0), animated: false) } func applySettingsPreviewPageMap(_ bookPageMap: RDEPUBBookPageMap) { navigationStateMachine.transition(to: .presentingWindow) context.bookPageMap = bookPageMap context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap)) paginationState.activePageMap = bookPageMap paginationState.source = .settingsPreview } func clear() { paginationState = RDEPUBPaginationState() navigationStateMachine.transition(to: .idle) } func makeSnapshot(from bookPageMap: RDEPUBBookPageMap) -> RDEPUBReadingSession.PaginationSnapshot { let pages = bookPageMap.entries.flatMap { entry in (0..