/**
 * page-manager-enhanced.ts — Enhanced PageManager with performance optimizations
 *
 * Extends the original PageManager with:
 * - Better integration with render system
 * - Adaptive quality support
 * - Memory-efficient canvas management
 * - Smooth transitions during quality changes
 */
const MAX_CANVAS_PIXELS = 16777216; // 4096 × 4096
/**
 * Clamp render scale so the canvas pixel count stays within the budget.
 * CSS dimensions stay correct — pages appear at the right size, just at
 * reduced effective DPI when zoomed very high.
 */
function clampedRenderScale(vpWidth, vpHeight, desiredScale, dpr, quality = 1.0) {
    const effectiveScale = desiredScale * quality;
    const w = vpWidth * effectiveScale * dpr;
    const h = vpHeight * effectiveScale * dpr;
    const pixels = w * h;
    if (pixels <= MAX_CANVAS_PIXELS)
        return effectiveScale * dpr;
    return effectiveScale * dpr * Math.sqrt(MAX_CANVAS_PIXELS / pixels);
}
const State = Object.freeze({
    PLACEHOLDER: 0,
    RENDERING: 1,
    RENDERED: 2,
    QUALITY_ADJUSTING: 3 // New state for quality transitions
});
export class EnhancedPageManager {
    /**
     * @param wrapper       - #pages-wrapper container (already in DOM)
     * @param viewports     - scale=1 viewports (0-indexed)
     * @param initialScale  - initial display scale
     * @param dpr           - devicePixelRatio
     * @param dispatchRender - (pageNum, scale, vpWidth, vpHeight, gen, quality) => void
     */
    constructor(wrapper, viewports, initialScale, dpr, dispatchRender) {
        this._wrapper = wrapper;
        this._viewports = viewports;
        this._scale = initialScale;
        this._dpr = dpr;
        this._dispatchRender = dispatchRender;
        this._renderGen = 0;
        this._inFlight = 0;
        this._zooming = false;
        this._states = new Array(viewports.length).fill(State.PLACEHOLDER);
        this._canvases = new Array(viewports.length).fill(null);
        this._wrappers = [];
        this._currentQualities = new Array(viewports.length).fill(1.0); // Track quality per page
        this._targetQualities = new Array(viewports.length).fill(1.0); // Target quality for transitions
        this._needsQualityUpdate = false;
        this._buildPlaceholders();
    }
    get pageWrappers() { return this._wrappers; }
    get numPages() { return this._viewports.length; }
    get renderGen() { return this._renderGen; }
    /** Called by ZoomController to suppress canvas teardown during CSS zoom. */
    setZooming(z) { this._zooming = z; }
    setScale(scale) {
        this._scale = scale;
    }
    setQualityForPage(pageNum, quality) {
        const i = pageNum - 1;
        if (i >= 0 && i < this._viewports.length) {
            this._targetQualities[i] = quality;
            // If page is rendered and quality changed significantly, trigger re-render
            if (this._states[i] === State.RENDERED &&
                Math.abs(quality - this._currentQualities[i]) > 0.1) {
                this._startQualityAdjustment(i);
            }
        }
    }
    setGlobalQuality(quality) {
        for (let i = 0; i < this._targetQualities.length; i++) {
            this._targetQualities[i] = quality;
        }
        // Re-render visible pages with new quality
        this._needsQualityUpdate = true;
    }
    _buildPlaceholders() {
        for (let i = 0; i < this._viewports.length; i++) {
            const vp = this._viewports[i];
            const wrap = document.createElement("div");
            wrap.className = "page-wrapper";
            wrap.dataset.page = String(i + 1);
            wrap.style.width = vp.width * this._scale + "px";
            wrap.style.height = vp.height * this._scale + "px";
            this._wrapper.appendChild(wrap);
            this._wrappers.push(wrap);
        }
    }
    /**
     * Called on each IntersectionObserver tick.
     * Renders pages entering the buffer; cleans up pages leaving it.
     * Visible pages are prioritized in the render queue.
     *
     * @param bufferSet  - 1-based page numbers in the buffer zone
     * @param visibleSet - 1-based page numbers currently on screen
     */
    reconcile(bufferSet, visibleSet) {
        const gen = this._renderGen;
        // Prioritize visible pages, then the rest of the buffer
        const toRender = [
            ...[...visibleSet].sort((a, b) => a - b),
            ...[...bufferSet].filter(p => !visibleSet.has(p)).sort((a, b) => a - b),
        ];
        for (const pageNum of toRender) {
            const i = pageNum - 1;
            if (i < 0 || i >= this._viewports.length)
                continue;
            if (this._states[i] === State.PLACEHOLDER) {
                this._startRender(i, gen);
            }
            else if (this._states[i] === State.QUALITY_ADJUSTING) {
                // Already handling quality transition
            }
            else if (this._needsQualityUpdate && this._states[i] === State.RENDERED) {
                // Apply quality updates if needed
                if (Math.abs(this._targetQualities[i] - this._currentQualities[i]) > 0.05) {
                    this._startQualityAdjustment(i);
                }
            }
        }
        // Clean up pages no longer in the buffer
        for (let i = 0; i < this._viewports.length; i++) {
            const pageNum = i + 1;
            if (!this._zooming && !bufferSet.has(pageNum) && this._states[i] === State.RENDERED) {
                this._cleanup(i);
            }
            // Cancel stale RENDERING pages outside the buffer
            if (!bufferSet.has(pageNum) && (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING)) {
                if (this._states[i] === State.RENDERING)
                    this._inFlight--;
                this._states[i] = State.PLACEHOLDER;
            }
        }
        this._needsQualityUpdate = false;
    }
    get allRendered() { return this._inFlight === 0; }
    getPageElement(pageNum) {
        const i = pageNum - 1;
        if (i >= 0 && i < this._wrappers.length) {
            return this._wrappers[i];
        }
        return null;
    }
    _startRender(i, gen) {
        const vp = this._viewports[i];
        const quality = this._targetQualities[i];
        const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
        this._states[i] = State.RENDERING;
        this._inFlight++;
        this._dispatchRender(i + 1, this._scale, vp.width, vp.height, gen, quality);
    }
    _startQualityAdjustment(i) {
        // Transition to quality adjustment state
        this._states[i] = State.QUALITY_ADJUSTING;
        const pageNum = i + 1;
        const gen = ++this._renderGen; // Increment gen to cancel any pending renders
        const vp = this._viewports[i];
        const quality = this._targetQualities[i];
        const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
        this._inFlight++;
        this._dispatchRender(pageNum, this._scale, vp.width, vp.height, gen, quality);
    }
    _cleanup(i) {
        if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
            this._inFlight--;
        }
        this._states[i] = State.PLACEHOLDER;
        const canvas = this._canvases[i];
        if (canvas) {
            canvas.remove();
            this._canvases[i] = null;
        }
        // Also remove the text and annotation layers
        const wrap = this._wrappers[i];
        wrap.querySelector(".text-layer")?.remove();
        wrap.querySelector(".annot-layer")?.remove();
    }
    /**
     * Called when the worker returns a rendered bitmap.
     *
     * @param pageNum
     * @param gen       - render generation the bitmap was rendered for
     * @param bitmap
     * @param quality   - quality at which this bitmap was rendered
     */
    onRendered(pageNum, gen, bitmap, quality) {
        if (gen !== this._renderGen) {
            bitmap.close();
            return;
        }
        const i = pageNum - 1;
        if (i < 0 || i >= this._viewports.length ||
            (this._states[i] !== State.RENDERING && this._states[i] !== State.QUALITY_ADJUSTING)) {
            bitmap.close();
            return;
        }
        this._inFlight--;
        this._currentQualities[i] = quality;
        const vp = this._viewports[i];
        const wrap = this._wrappers[i];
        const cssW = vp.width * this._scale;
        const cssH = vp.height * this._scale;
        // For quality adjustments, we want smooth transitions
        const wasQualityAdjustment = this._states[i] === State.QUALITY_ADJUSTING;
        this._states[i] = State.RENDERED;
        // Remove the old canvas
        const old = wrap.querySelector("canvas");
        if (old) {
            if (wasQualityAdjustment) {
                // For quality adjustments, fade out the old canvas for smooth transition
                old.style.transition = "opacity 0.15s ease-out";
                old.style.opacity = "0";
                setTimeout(() => old.remove(), 150);
            }
            else {
                old.remove();
            }
        }
        const canvas = document.createElement("canvas");
        canvas.width = bitmap.width;
        canvas.height = bitmap.height;
        canvas.style.width = cssW + "px";
        canvas.style.height = cssH + "px";
        // Zero-copy display — fall back to drawImage if bitmaprenderer unavailable
        const ctx = canvas.getContext("bitmaprenderer");
        if (ctx) {
            ctx.transferFromImageBitmap(bitmap);
        }
        else {
            const ctx2d = canvas.getContext("2d");
            if (ctx2d) {
                ctx2d.drawImage(bitmap, 0, 0);
                bitmap.close();
            }
        }
        // For quality adjustments, fade in the new canvas
        if (wasQualityAdjustment) {
            canvas.style.opacity = "0";
            canvas.style.transition = "opacity 0.15s ease-in";
            wrap.appendChild(canvas);
            requestAnimationFrame(() => {
                canvas.style.opacity = "1";
            });
        }
        else {
            wrap.appendChild(canvas);
        }
        this._canvases[i] = canvas;
    }
    /**
     * Get the canvas element for a specific page
     */
    getPageCanvas(pageNum) {
        const i = pageNum - 1;
        if (i >= 0 && i < this._canvases.length) {
            return this._canvases[i];
        }
        return null;
    }
    /**
     * Called after the zoom debounce fires.
     * Increments renderGen (cancels stale work), resizes all placeholders,
     * then re-renders the buffer pages at the new resolution.
     *
     * @param newScale
     * @param bufferSet
     * @param visibleSet
     */
    onScaleChange(newScale, bufferSet, visibleSet) {
        this._scale = newScale;
        this._renderGen++;
        for (let i = 0; i < this._viewports.length; i++) {
            const pageNum = i + 1;
            const vp = this._viewports[i];
            const wrap = this._wrappers[i];
            const cssW = vp.width * newScale;
            const cssH = vp.height * newScale;
            wrap.style.width = cssW + "px";
            wrap.style.height = cssH + "px";
            wrap.style.zoom = "1";
            if (bufferSet.has(pageNum)) {
                // Keep the old canvas visible (stretched to the new CSS size) so there
                // is no blank flash while the new render is in flight.
                const canvas = this._canvases[i];
                if (canvas) {
                    canvas.style.width = cssW + "px";
                    canvas.style.height = cssH + "px";
                }
                if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
                    this._inFlight--;
                }
                this._states[i] = State.PLACEHOLDER;
            }
            else {
                // Off-screen: clean up immediately — not visible, so no flash.
                const canvas = this._canvases[i];
                if (canvas) {
                    canvas.remove();
                    this._canvases[i] = null;
                }
                this._states[i] = State.PLACEHOLDER;
            }
        }
        this.reconcile(bufferSet, visibleSet);
    }
    /**
     * Returns the 1-based number of the topmost visible page.
     *
     * @param visibleSet
     */
    getCurrentPage(visibleSet) {
        // Defensive programming: handle null/undefined visibleSet
        if (!visibleSet || visibleSet.size === 0) {
            // Fall back to scroll position
            const top = this._wrapper.parentElement?.getBoundingClientRect().top ?? 0;
            for (const wrap of this._wrappers) {
                if (wrap.getBoundingClientRect().bottom > top + 4) {
                    return parseInt(wrap.dataset.page || '1', 10);
                }
            }
            return 1;
        }
        return Math.min(...visibleSet);
    }
    /**
     * Get the current quality for a specific page
     */
    getPageQuality(pageNum) {
        const i = pageNum - 1;
        if (i >= 0 && i < this._currentQualities.length) {
            return this._currentQualities[i];
        }
        return 1.0;
    }
    /**
     * Get the current render state for a page
     */
    getPageState(pageNum) {
        const i = pageNum - 1;
        if (i >= 0 && i < this._states.length) {
            return this._states[i];
        }
        return State.PLACEHOLDER;
    }
    /**
     * Force cleanup of all resources
     */
    cleanup() {
        for (let i = 0; i < this._viewports.length; i++) {
            this._cleanup(i);
        }
        this._inFlight = 0;
        this._renderGen = 0;
    }
}
//# sourceMappingURL=page-manager-enhanced.js.map