// ==========================================
// renderer.js: CORE PDF & CANVAS RENDERING
// ==========================================

window.sortTextLayerDOM = function(layer) {
    const spans = Array.from(layer.querySelectorAll("span"));
    const spanData = spans.map(span => {
        const rect = span.getBoundingClientRect();
        // ✨ FIX 1: Calculate the vertical center of the word for perfect line grouping
        return { el: span, y: rect.top, x: rect.left, centerY: rect.top + (rect.height / 2) };
    });

    spanData.sort((a, b) => {
        // ✨ FIX 2: Sort by center, with a generous 12px threshold for wavy/sloppy OCR lines
        if (Math.abs(a.centerY - b.centerY) < 12) return a.x - b.x;
        return a.centerY - b.centerY;
    });
    spanData.forEach(data => layer.appendChild(data.el));
};

/**
 * @param {import('pdfjs-dist').PDFDocumentProxy} pdf
 * @param {number} pageNum
 * @param {HTMLElement} [targetViewer] pin destination pane (multi-tab safe)
 */
window.renderPage = async function(pdf, pageNum, targetViewer) {
    const viewer = targetViewer
        || (APP.DOM && APP.DOM.viewer)
        || document.getElementById("viewer");
    if (!viewer) {
        console.error("[renderPage] no viewer pane");
        return null;
    }

    const page = await pdf.getPage(pageNum);
    const viewport2x = page.getViewport({ scale: 2 });
    const viewport1x = page.getViewport({ scale: 1 }); 
    const baseWidth = viewport1x.width;
    const baseHeight = viewport1x.height;

    // Canvas — do not burn widget appearances into pixels (interactive AnnotationLayer handles forms)
    const canvas = document.createElement("canvas");
    canvas.className = "pageCanvas" + (APP.isPixelated ? " render-pixelated" : "");
    canvas.width = viewport2x.width; canvas.height = viewport2x.height;
    canvas.style.width = "100%"; canvas.style.height = "100%";
    const ctx = canvas.getContext("2d");
    const annMode = (pdfjsLib.AnnotationMode && pdfjsLib.AnnotationMode.DISABLE != null)
        ? pdfjsLib.AnnotationMode.DISABLE
        : 0;
    await page.render({
        canvasContext: ctx,
        viewport: viewport2x,
        intent: "display",
        annotationMode: annMode
    }).promise;

    // Text Layer
    const textLayerDiv = document.createElement("div");
    textLayerDiv.className = "textLayer";
    textLayerDiv.style.setProperty('--scale-factor', "1");
    textLayerDiv.style.width = baseWidth + "px"; textLayerDiv.style.height = baseHeight + "px";

    // Containers
    const container = document.createElement("div");
    container.className = "pageContainer";
    container.style.width = baseWidth + "px"; container.style.height = baseHeight + "px";
    container.style.transform = `scale(${window.currentZoom})`;
    container.append(canvas, textLayerDiv); 

    const wrapper = document.createElement("div");
    wrapper.className = "pageWrapper";
    if (window.CcsProcessor) window.CcsProcessor.ensureId(wrapper, "page");
	wrapper.dataset.pdfFingerprint = pdf.fingerprints[0];
	console.log(wrapper.dataset.pdfFingerprint);
	wrapper.dataset.originalPageNum = pageNum;
    wrapper.dataset.pageId = `${pdf.fingerprints[0]}_page_${pageNum}`;
    wrapper.dataset.baseWidth = baseWidth; wrapper.dataset.baseHeight = baseHeight;
    // PDF /Rotate (0|90|180|270) — needed so Save PDF can re-orient flattened overlays
    try {
        wrapper.dataset.pageRotate = String(
            (page.rotate != null ? page.rotate : (page.getViewport && page.getViewport({ scale: 1 }).rotation)) || 0
        );
    } catch (_) {
        wrapper.dataset.pageRotate = "0";
    }
    if (window.CcsProcessor) {
        window.CcsProcessor.ensureId(viewer, "viewer");
    }
    wrapper.style.width = (baseWidth * window.currentZoom) + "px";
    wrapper.style.height = (baseHeight * window.currentZoom) + "px";
    wrapper.appendChild(container);
    viewer.appendChild(wrapper);

    // Native PDF.js Rendering
    try {
        if (pdfjsLib.TextLayer) {
            const textLayer = new pdfjsLib.TextLayer({
                textContentSource: page.streamTextContent(),
                container: textLayerDiv,
                viewport: viewport1x
            });
            await textLayer.render();
            window.sortTextLayerDOM(textLayerDiv);
        } else {
            const renderTask = pdfjsLib.renderTextLayer({
                textContentSource: page.streamTextContent(),
                container: textLayerDiv,
                viewport: viewport1x,
                textDivs: []
            });
            if (renderTask.promise) {
                renderTask.promise.then(() => window.sortTextLayerDOM(textLayerDiv));
            } else {
                setTimeout(() => window.sortTextLayerDOM(textLayerDiv), 500);
            }
        }
    } catch (e) {
        console.warn("Text layer rendering failed:", e);
    }

    // Interactive AcroForm / links (Editor: fill; Form: dimmed via CSS)
    try {
        if (typeof window.renderPageAnnotationLayer === "function") {
            await window.renderPageAnnotationLayer(pdf, page, pageNum, container, viewport1x);
        }
    } catch (e) {
        console.warn("Annotation layer failed:", e);
    }

    // Ensure container stays under wrapper (annotation path may re-parent)
    if (wrapper.parentNode !== viewer) {
        viewer.appendChild(wrapper);
    }
    if (container.parentNode !== wrapper) {
        wrapper.appendChild(container);
    }

    // ✨ THE UPGRADE: Return the wrapper (the top-level page element)
    return wrapper;
};

/** Drop source PDF bytes that no open tab still references (multi-tab safe). */
window.gcSourcePdfBuffers = function() {
    window.sourcePdfBuffers = window.sourcePdfBuffers || {};
    const used = new Set();
    document.querySelectorAll(".pageWrapper[data-pdf-fingerprint]").forEach((w) => {
        if (w.dataset.pdfFingerprint) used.add(w.dataset.pdfFingerprint);
    });
    Object.keys(window.sourcePdfBuffers).forEach((key) => {
        if (!used.has(key)) delete window.sourcePdfBuffers[key];
    });
};

/**
 * Load a PDF into a viewer pane.
 * @param {ArrayBuffer} arrayBuffer
 * @param {boolean} [append=false]
 * @param {{ viewer?: HTMLElement }} [opts] pin destination pane so tab switches
 *   mid-load cannot dump pages into the wrong document (session restore race).
 */
window.renderPDF = async function(arrayBuffer, append = false, opts) {
    const options = (opts && typeof opts === "object") ? opts : {};
    // Pin once — never re-read APP.DOM.viewer after awaits (restore/open race).
    const targetViewer = options.viewer
        || (APP.DOM && APP.DOM.viewer)
        || document.getElementById("viewer");
    if (!targetViewer) {
        console.error("[renderPDF] no viewer pane");
        return [];
    }
    const isActiveViewer = !!(APP.DOM && APP.DOM.viewer === targetViewer);

    if (!append) {
        targetViewer.innerHTML = "";
        // Only reset live history when replacing the *active* document
        if (isActiveViewer && window.historyEngine) {
            window.historyEngine.undoStack = [];
            window.historyEngine.redoStack = [];
            window.historyEngine.savePoint = 0;
            if (typeof window.updateHistoryUI === "function") window.updateHistoryUI();
        }
        // Do NOT wipe sourcePdfBuffers globally — other tabs still need their bytes.
        // Orphaned entries are removed after pages are (re)built.
        if (isActiveViewer && typeof window.clearProjectAudit === "function") {
            window.clearProjectAudit();
        }
    }
	window.sourcePdfBuffers = window.sourcePdfBuffers || {};

    // Keep the exact input for session restore. For .ccspdf this MUST include the
    // ---CCS-SAVE-STATE--- trailer; caching only the stripped PDF half loses form
    // designer schema on hard refresh (shells live in the trailer, not only AcroForm).
    let sessionCacheBytes = null;
    try {
        if (arrayBuffer instanceof ArrayBuffer) {
            sessionCacheBytes = arrayBuffer.slice(0);
        } else if (arrayBuffer && arrayBuffer.buffer) {
            sessionCacheBytes = arrayBuffer.buffer.slice(
                arrayBuffer.byteOffset || 0,
                (arrayBuffer.byteOffset || 0) + arrayBuffer.byteLength
            );
        }
    } catch (_) {
        sessionCacheBytes = null;
    }

    let schemaToLoad = null;

    // Project state: Catalog /CCSProject (gzip+hex) and/or legacy ---CCS-SAVE-STATE--- trailer
    try {
        if (window.CcsProjectState && typeof window.CcsProjectState.extractProjectStateFromBytes === "function") {
            const extracted = await window.CcsProjectState.extractProjectStateFromBytes(arrayBuffer);
            if (extracted && extracted.abort) {
                return; // integrity failure already reported
            }
            if (extracted && extracted.schema) {
                schemaToLoad = extracted.schema;
                arrayBuffer = extracted.pdfBytes;
                console.log("[renderPDF] project state source=", extracted.source,
                    "version=", schemaToLoad.version);
                if (schemaToLoad.seal) {
                    console.log(
                        "[renderPDF] seal purpose=", schemaToLoad.seal.purpose,
                        "flattened=", !!schemaToLoad.seal.flattened,
                        "contentHash=", schemaToLoad.seal.contentHash
                            && schemaToLoad.seal.contentHash.slice(0, 16) + "…",
                        "originHash=", schemaToLoad.seal.originHash
                            && schemaToLoad.seal.originHash.slice(0, 16) + "…"
                    );
                    if (typeof window.CcsProjectState.verifyProjectSeal === "function") {
                        window.CcsProjectState.verifyProjectSeal(schemaToLoad).then((v) => {
                            console.log("[renderPDF] seal verify:", v.message, v.checks);
                        }).catch(() => { /* ignore */ });
                    }
                }
            }
        } else {
            // Minimal legacy-only fallback if project-state.js failed to load
            const bytes = new Uint8Array(arrayBuffer);
            const markerBytes = new TextEncoder().encode("\n---CCS-SAVE-STATE---");
            let markerIndex = -1;
            for (let i = bytes.length - markerBytes.length; i >= 0; i--) {
                let match = true;
                for (let j = 0; j < markerBytes.length; j++) {
                    if (bytes[i + j] !== markerBytes[j]) { match = false; break; }
                }
                if (match) { markerIndex = i; break; }
            }
            if (markerIndex !== -1) {
                const payloadBytes = bytes.slice(markerIndex + markerBytes.length);
                const delimBytes = new TextEncoder().encode("---HASH-DELIM---");
                let delimIndex = -1;
                for (let i = 0; i < payloadBytes.length - delimBytes.length; i++) {
                    if (payloadBytes.slice(i, i + delimBytes.length).every((v, j) => v === delimBytes[j])) {
                        delimIndex = i; break;
                    }
                }
                let jsonStr;
                if (delimIndex !== -1) {
                    const savedHash = new TextDecoder().decode(payloadBytes.slice(0, delimIndex));
                    const compressedData = payloadBytes.slice(delimIndex + delimBytes.length);
                    const actualHash = await window.computeHash(compressedData);
                    if (actualHash !== savedHash) {
                        window.customAlert("Project file integrity check failed. The file may be corrupted or tampered with.", "❌ Security Warning");
                        return;
                    }
                    jsonStr = await window.decompressData(compressedData);
                } else {
                    jsonStr = new TextDecoder("utf-8").decode(payloadBytes);
                }
                schemaToLoad = JSON.parse(jsonStr);
                arrayBuffer = arrayBuffer.slice(0, markerIndex);
            }
        }
    } catch (e) {
        console.warn("No project state found or parse error:", e);
    }
    
    if (!pdfjsLib) { window.customAlert("PDF.js failed to load!", "❌ Engine Error"); return; }
	const newWrappers = [];

	const safeCleanBytes = arrayBuffer.slice(0);

    try {
        const loadingTask = pdfjsLib.getDocument({
            data: arrayBuffer,
            enableXfa: true,
            cMapUrl: './lib/cmaps/',
            cMapPacked: true,
            standardFontDataUrl: './lib/standard_fonts/',
        });

        // Only decrypt THIS load if PDF.js actually asked for a password.
        // A sticky password from a previous encrypted file must never run
        // qpdf --decrypt on a cleartext PDF (that corrupts source buffers → blank
        // pages / missing forms on every subsequent save).
        let passwordUsedThisLoad = null;
        loadingTask.onPassword = (updatePassword, reason) => {
            const PR = pdfjsLib.PasswordResponses || { NEED_PASSWORD: 1, INCORRECT_PASSWORD: 2 };
            const incorrect = reason === PR.INCORRECT_PASSWORD || reason === 2;
            const msg = incorrect
                ? "That password was incorrect. Enter the password again to open this PDF."
                : "This PDF is password-protected. Enter the password to open it.";

            const promptFn = typeof window.customPasswordPrompt === "function"
                ? window.customPasswordPrompt
                : async (m) => {
                    const p = window.prompt(m);
                    return p === null ? null : p;
                };

            Promise.resolve(promptFn(msg, "🔒 Password required", { incorrect }))
                .then((pw) => {
                    if (pw === null || pw === undefined) {
                        updatePassword(new Error("Password entry cancelled."));
                    } else {
                        passwordUsedThisLoad = String(pw);
                        window._ccsLastPdfPassword = passwordUsedThisLoad;
                        updatePassword(passwordUsedThisLoad);
                    }
                })
                .catch((e) => {
                    updatePassword(e instanceof Error ? e : new Error(String(e)));
                });
        };

        const pdf = await loadingTask.promise;
        const fpKey = pdf.fingerprints[0];

        // Cleartext by default. Decrypt ONLY when this document required a password.
		window.sourcePdfBuffers[fpKey] = safeCleanBytes;
        if (passwordUsedThisLoad) {
            window._ccsPdfPasswordByFingerprint = window._ccsPdfPasswordByFingerprint || {};
            window._ccsPdfPasswordByFingerprint[fpKey] = passwordUsedThisLoad;
            if (typeof window.decryptPdfBytes === "function") {
                try {
                    const dec = await window.decryptPdfBytes(safeCleanBytes, passwordUsedThisLoad);
                    const ab = dec.buffer.slice(dec.byteOffset, dec.byteOffset + dec.byteLength);
                    window.sourcePdfBuffers[fpKey] = ab;
                    console.log("[renderPDF] decrypted source buffer for export (", ab.byteLength, "bytes)");
                } catch (decErr) {
                    console.warn("[renderPDF] decrypt for export buffer failed; export may be blank", decErr);
                }
            }
        } else if (window._ccsPdfPasswordByFingerprint) {
            // Drop stale password for this fingerprint if reopened without encryption
            try { delete window._ccsPdfPasswordByFingerprint[fpKey]; } catch (_) { /* ignore */ }
        }

        // Large single-PDF: do NOT full-screen dim (user can watch pages stream in).
        // Soft gate blocks save/print/export until done; chip shows progress.
        // Multi-file combine still uses showLoading (dim) at a higher layer.
        const totalPages = pdf.numPages || 0;
        const useSoftGate = totalPages >= 2
            && typeof window.beginDocGate === "function"
            && typeof window.endDocGate === "function";
        // If a full dim overlay is already up (multi-file), just update that message.
        const outerDim = window.isAppBusy && window.isAppBusy()
            && window._ccsOverlayBusy;
        let ownsGate = false;

        if (useSoftGate && !outerDim) {
            window.beginDocGate("renderPDF");
            ownsGate = true;
        }

        try {
            for (let i = 1; i <= totalPages; i++) {
                if (totalPages >= 2) {
                    const verb = append ? "Appending" : "Loading";
                    const msg = `${verb} page ${i} of ${totalPages}…`;
                    if (outerDim && typeof window.showLoading === "function") {
                        window.showLoading(msg);
                    } else if (typeof window.setDocLoadStatus === "function") {
                        window.setDocLoadStatus(msg + "  ·  Save/print wait until done");
                    }
                }
                const wrapper = await window.renderPage(pdf, i, targetViewer);
                if (wrapper) newWrappers.push(wrapper);
            }
        } finally {
            // Always release our gate — never leave save/print stuck
            if (ownsGate) {
                try { window.endDocGate(); } catch (_) { /* ignore */ }
            }
            if (!outerDim && typeof window.setDocLoadStatus === "function") {
                // endDocGate already clears; belt-and-suspenders if gate wasn't used
                if (!ownsGate) window.setDocLoadStatus(null);
            }
        }
        
        // Thumbnails only for the visible active pane
        if (isActiveViewer && typeof window.syncPageThumbnails === "function") {
            window.syncPageThumbnails();
        }
        if (typeof window.gcSourcePdfBuffers === "function") window.gcSourcePdfBuffers();

        // Origin hash + provenance (always). Logs clearly when no CCS project state.
        if (!append && isActiveViewer && window.CcsProjectState
            && typeof window.CcsProjectState.recordDocumentOrigin === "function") {
            try {
                await window.CcsProjectState.recordDocumentOrigin(
                    sessionCacheBytes || safeCleanBytes,
                    {
                        source: schemaToLoad ? "open-with-state" : "open-plain",
                        hasProjectState: !!schemaToLoad
                    }
                );
            } catch (e) {
                console.warn("[renderPDF] recordDocumentOrigin", e);
            }
        }

        // Session: cache full input so hard refresh reloads form schema
        // Prefer the pinned viewer’s doc bag when workspace is available.
        if (!append && window.CcsWorkspace && typeof window.CcsWorkspace.rememberDocBytes === "function") {
            try {
                let docForBytes = null;
                if (typeof window.CcsWorkspace.getDocs === "function") {
                    const map = window.CcsWorkspace.getDocs();
                    if (map && typeof map.forEach === "function") {
                        map.forEach((d) => {
                            if (d && d.viewer === targetViewer) docForBytes = d;
                        });
                    }
                }
                window.CcsWorkspace.rememberDocBytes(sessionCacheBytes || safeCleanBytes, docForBytes || undefined);
            } catch (e) {
                console.warn("rememberDocBytes", e);
            }
        }

        // Always use the pinned pane for project/form import (never re-read APP.DOM.viewer)
        const importRoot = targetViewer;

        // Pages + annotation layers are already fully awaited above — apply immediately.
        // A fire-and-forget setTimeout(100) raced with tab switch / hard-refresh restore
        // and intermittently left form shells (or natives) missing.
		if (schemaToLoad) {
            try {
                if (importRoot) {
                    importRoot._gaProjectSchema = schemaToLoad;
                    const expectedForms = (Array.isArray(schemaToLoad.pages) ? schemaToLoad.pages : [])
                        .reduce((n, p) => n + ((p.overlays || []).filter((o) => o && o.type === "formField").length), 0);
                    if (expectedForms > 0) {
                        importRoot.dataset.expectedFormFields = String(expectedForms);
                    }
                    if (schemaToLoad.formFieldsManaged) {
                        importRoot.dataset.formFieldsManaged = "1";
                    }
                }
                // Temporarily point APP.DOM.viewer at the import root so applyProjectData
                // (and helpers that still read the global) hit the correct pane.
                const prevViewer = APP.DOM && APP.DOM.viewer;
                if (APP.DOM) APP.DOM.viewer = importRoot;
                try {
                    window.applyProjectData(schemaToLoad, importRoot);
                } finally {
                    if (APP.DOM && prevViewer && prevViewer.isConnected) {
                        APP.DOM.viewer = prevViewer;
                    } else if (APP.DOM) {
                        APP.DOM.viewer = importRoot;
                    }
                }
                const got = importRoot
                    ? importRoot.querySelectorAll(".formFieldOverlay").length
                    : 0;
                console.log(
                    "[renderPDF] project applied; form shells=", got,
                    "expected=", importRoot && importRoot.dataset.expectedFormFields
                );
            } catch (e) {
                console.warn("[renderPDF] applyProjectData failed", e);
            }
            if (typeof window.scheduleSuppressNativesForDesignerTwins === "function") {
                window.scheduleSuppressNativesForDesignerTwins(importRoot || undefined);
            }
            if (typeof window.scheduleEnsureFormsReady === "function") {
                window.scheduleEnsureFormsReady(importRoot, { schema: schemaToLoad, pdf });
            }
		} else if (!append && typeof window.maybeOfferNativeFormImport === "function") {
            // Await + explicit root: multi-tab restore must not import into the wrong pane
            try {
                const n = await window.maybeOfferNativeFormImport(pdf, { root: importRoot });
                if (importRoot && n > 0) {
                    importRoot.dataset.expectedFormFields = String(n);
                }
                console.log("[renderPDF] native form import count=", n);
                if (typeof window.scheduleSuppressNativesForDesignerTwins === "function") {
                    window.scheduleSuppressNativesForDesignerTwins(importRoot || undefined);
                }
                if (typeof window.scheduleEnsureFormsReady === "function") {
                    window.scheduleEnsureFormsReady(importRoot, { pdf });
                }
            } catch (e) {
                console.warn("maybeOfferNativeFormImport", e);
                if (typeof window.scheduleEnsureFormsReady === "function") {
                    window.scheduleEnsureFormsReady(importRoot, { pdf });
                }
            }
        }
        return newWrappers;
    } catch (err) {
        console.error("PDF Parsing Error:", err);
        const name = err && err.name;
        const msg = (err && err.message) ? String(err.message) : "";
        if (name === "PasswordException" || /password/i.test(msg) || /cancelled/i.test(msg)) {
            if (/cancelled/i.test(msg)) {
                // Quiet cancel — user backed out of password dialog
                return;
            }
            await window.customAlert(
                "Could not unlock this PDF. The password may be wrong, or the file uses an unsupported encryption scheme.",
                "🔒 Password required"
            );
            return;
        }
        await window.customAlert(
            "This PDF could not be opened. It may be damaged or use an unsupported format. " +
            "If it is encrypted with a special DRM scheme, try flattening via Print to PDF first.",
            "❌ Open failed"
        );
    }
};
