// preview.js — file-preview rendering for the browse tool's right pane.
//
// Default flow: showFilePreview(node) renders into the inline preview
// pane (#previewBody). Popup flow: showFilePreview(node, {popup:true})
// opens a separate window — kept for users who want previews on a
// second monitor.
//
// Rendering uses shared/preview-lib.js for content types it handles
// (TIFF, ZIP listing, image-mime detection). PDF / HTML go in iframes;
// text into a <pre>; markdown into the dedicated markdown plugin
// (preview-markdown.js); unknown extensions show a download button.
(function () {
    'use strict';

    var state = window.app.state;
    var loader = window.app.modules.loader;
    var preview = window.zddc && window.zddc.preview;
    if (!preview) {
        console.error('[browse] zddc.preview not loaded — preview disabled.');
    }

    var util = window.app.modules.util;
    var escapeHtml = util.escapeHtml;

    var MIME = {
        'pdf': 'application/pdf',
        'html': 'text/html', 'htm': 'text/html',
        'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
        'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
        'tif': 'image/tiff', 'tiff': 'image/tiff',
        'zip': 'application/zip',
        'txt': 'text/plain', 'md': 'text/markdown', 'json': 'application/json',
        'xml': 'application/xml', 'csv': 'text/csv', 'log': 'text/plain',
        'js': 'text/javascript', 'css': 'text/css',
        'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'xls': 'application/vnd.ms-excel'
    };

    function getMime(ext) { return MIME[ext] || 'application/octet-stream'; }

    var fmtSize = util.fmtSize;

    async function getArrayBuffer(node) {
        // A zip member node carries a ZipFileHandle in node.handle, so
        // it falls through the same getFile() path as any local file.
        if (state.source === 'server' && node.url) {
            var resp = await fetch(node.url);
            if (!resp.ok) throw new Error('HTTP ' + resp.status);
            return await resp.arrayBuffer();
        }
        if (node.handle) {
            var f = await node.handle.getFile();
            return await f.arrayBuffer();
        }
        throw new Error('no source for file');
    }

    // Like getArrayBuffer, but also returns the server version token
    // ({etag, lastModified}) captured from the content GET. The editors use
    // it to send an If-Match precondition on save so a concurrent edit is
    // rejected (412) instead of silently clobbered. FS-Access mode has no
    // server version — etag/lastModified are null and the precondition is a
    // clean no-op (a single locally-picked file has no concurrency).
    async function getContentWithVersion(node) {
        if (state.source === 'server' && node.url) {
            var resp = await fetch(node.url, { credentials: 'same-origin' });
            if (!resp.ok) throw new Error('HTTP ' + resp.status);
            var buf = await resp.arrayBuffer();
            return {
                buf: buf,
                etag: resp.headers.get('ETag') || null,
                lastModified: resp.headers.get('Last-Modified') || null
            };
        }
        if (node.handle) {
            var f = await node.handle.getFile();
            return { buf: await f.arrayBuffer(), etag: null, lastModified: null };
        }
        throw new Error('no source for file');
    }

    async function getBlobUrl(node) {
        // Server-served files (including zip members at "<…>.zip/<member>"
        // URLs) load straight from the server — preserves Content-Type
        // and lets relative links inside HTML resolve back to the server.
        if (state.source === 'server' && node.url) {
            return { url: node.url, fromServer: true };
        }
        var buf = await getArrayBuffer(node);
        var blob = new Blob([buf], { type: getMime(node.ext) });
        return { url: URL.createObjectURL(blob), fromServer: false };
    }

    // ── Editor lifecycle helpers ─────────────────────────────────────────────
    // The markdown and YAML plugins each mount a long-lived editor into the
    // preview pane. Switching files (or clearing the pane) must dispose the
    // live editor first — otherwise the Toast UI instance, its DOM, and its
    // document-level resizer listeners leak when we overwrite the container.

    function editorModules() {
        var m = window.app.modules;
        return [m.markdown, m.yamledit].filter(Boolean);
    }

    function disposeEditors() {
        editorModules().forEach(function (mod) {
            if (mod.dispose) { try { mod.dispose(); } catch (_) { /* ignore */ } }
        });
    }

    // The editor module (if any) holding unsaved edits, else null.
    function dirtyEditor() {
        var mods = editorModules();
        for (var i = 0; i < mods.length; i++) {
            if (mods[i].isDirty && mods[i].isDirty()) return mods[i];
        }
        return null;
    }

    function samePreviewNode(a, b) {
        if (!a || !b) return false;
        if (a === b) return true;
        if (a.url && b.url) return a.url === b.url;
        return a.name === b.name && a.parentId === b.parentId;
    }

    // Tear down any live editor and blank the pane. Used by callers that
    // reset the preview directly (rescope, popstate) so they don't leak the
    // editor or strand its dirty state.
    function clearPreview() {
        disposeEditors();
        var container = document.getElementById('previewBody');
        if (container) container.innerHTML = '';
        toggleTargetNode = null;
        var tb = document.getElementById('previewViewToggle');
        if (tb) tb.classList.add('hidden');
    }

    // Warn before a full page unload (reload / close / external nav) drops
    // unsaved editor changes. SPA-internal switches are guarded in
    // renderInline; this catches the browser-level exit.
    window.addEventListener('beforeunload', function (e) {
        if (dirtyEditor()) { e.preventDefault(); e.returnValue = ''; }
    });

    // ── Rendered ⇄ Source toggle ─────────────────────────────────────────────
    // Some types we can RENDER, not just edit (.html). Those show rendered by
    // default (sandboxed — no scripts, no same-origin) with a toggle to the
    // CodeMirror source view. Markdown has its own rendered/source toggle, so
    // it's not here. Extend RENDERABLE to add more (svg already previews as an
    // image; csv could render as a table later).
    var RENDERABLE = { html: 1, htm: 1 };
    function isRenderable(ext) { return !!RENDERABLE[(ext || '').toLowerCase()]; }
    function nodeKey(node) { return (node && (node.url || node.name)) || ''; }
    // Per-node mode; 'rendered' is the default. Only the node the user last
    // toggled is remembered, so switching files resets to rendered.
    var viewToggle = { key: null, mode: 'rendered' };
    var toggleTargetNode = null;
    function effectiveMode(node) {
        return (viewToggle.key && viewToggle.key === nodeKey(node)) ? viewToggle.mode : 'rendered';
    }
    function ensureViewToggleBtn() {
        var btn = document.getElementById('previewViewToggle');
        if (btn) return btn;
        var popout = document.getElementById('previewPopout');
        if (!popout || !popout.parentNode) return null;
        btn = document.createElement('button');
        btn.id = 'previewViewToggle';
        btn.type = 'button';
        btn.className = 'btn btn-sm btn-secondary hidden';
        popout.parentNode.insertBefore(btn, popout);
        btn.addEventListener('click', function () {
            if (!toggleTargetNode) return;
            var next = effectiveMode(toggleTargetNode) === 'rendered' ? 'source' : 'rendered';
            viewToggle = { key: nodeKey(toggleTargetNode), mode: next };
            renderInline(toggleTargetNode, { toggle: true });
        });
        return btn;
    }

    // ── Inline rendering ────────────────────────────────────────────────────

    // Bumped on every renderInline entry; a render that loses the race
    // (a newer selection started while its bytes were in flight) bails
    // before writing stale content into the shared pane.
    var renderSeq = 0;

    function renderEmpty(container, msg) {
        container.innerHTML = '<div class="preview-empty">' + escapeHtml(msg) + '</div>';
    }

    function renderError(container, msg) {
        container.innerHTML = '<div class="preview-empty" style="color:var(--danger)">'
            + escapeHtml(msg) + '</div>';
    }

    async function renderInline(node, opts) {
        opts = opts || {};
        var container = document.getElementById('previewBody');
        var titleEl = document.getElementById('previewTitle');
        var metaEl = document.getElementById('previewMeta');
        var popoutBtn = document.getElementById('previewPopout');
        if (!container) return;

        // Guard unsaved editor edits before we tear the editor down.
        var dm = dirtyEditor();
        if (dm) {
            var cur = dm.currentNode ? dm.currentNode() : null;
            if (samePreviewNode(cur, node) && !opts.toggle) {
                // Re-selecting the file we're already editing — don't reload
                // and clobber the in-progress edits. (A deliberate view toggle
                // falls through to the discard prompt below.)
                return;
            }
            if (opts.auto) {
                // Keyboard/auto preview (cursor walking the tree): leave the
                // dirty editor in place rather than prompting on every key.
                return;
            }
            var label = cur ? cur.name : 'this file';
            if (!window.confirm('Discard unsaved changes to ' + label + '?')) return;
        }
        // Safe to replace the pane now: dispose any live editor so its
        // instance + document-level listeners don't leak.
        disposeEditors();

        var seq = ++renderSeq;

        if (titleEl) titleEl.textContent = node.name;
        if (metaEl) {
            var meta = [];
            if (!node.isDir && !node.isZip) meta.push(fmtSize(node.size));
            if (node.ext) meta.push(node.ext.toUpperCase());
            metaEl.textContent = meta.join(' · ');
        }
        if (popoutBtn) popoutBtn.classList.remove('hidden');

        var ext = (node.ext || '').toLowerCase();

        // Rendered ⇄ Source toggle button — shown only for renderable types.
        var toggleBtn = ensureViewToggleBtn();
        if (toggleBtn) {
            if (isRenderable(ext)) {
                toggleTargetNode = node;
                toggleBtn.classList.remove('hidden');
                toggleBtn.textContent = effectiveMode(node) === 'rendered' ? '⟨⟩ Source' : '◱ Preview';
            } else {
                toggleBtn.classList.add('hidden');
            }
        }

        // Renderable types (.html) — show rendered by default, sandboxed for
        // safety (no scripts, no same-origin). The toggle flips to source.
        if (isRenderable(ext) && effectiveMode(node) === 'rendered') {
            try {
                var rinfo = await getBlobUrl(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '<iframe class="preview-iframe" sandbox src="'
                    + escapeHtml(rinfo.url) + '"></iframe>';
            } catch (e) {
                renderError(container, e.message || String(e));
            }
            return;
        }

        // Markdown plugin (if loaded) takes over for .md / .markdown.
        if ((ext === 'md' || ext === 'markdown') &&
            window.app.modules.markdown &&
            typeof window.app.modules.markdown.render === 'function') {
            try {
                await window.app.modules.markdown.render(node, container, { getArrayBuffer: getArrayBuffer, getContentWithVersion: getContentWithVersion });
            } catch (e) {
                renderError(container, 'Markdown render failed: ' + (e.message || e));
            }
            return;
        }

        // CodeMirror editor: the general editor for editable text files that
        // aren't markdown — yaml/.zddc (schema lint + completion + hover) plus
        // txt/csv/tsv/json/xml/html/css/js/… as a plaintext code editor.
        // Guided dialogs (Manage access, …) are the front door for the common
        // .zddc tasks; this is the full/raw edit surface.
        var yamlMod = window.app.modules.yamledit;
        if (yamlMod && yamlMod.handles(node)) {
            try {
                await yamlMod.render(node, container, { getArrayBuffer: getArrayBuffer, getContentWithVersion: getContentWithVersion });
            } catch (e) {
                renderError(container, 'Editor failed: ' + (e.message || e));
            }
            return;
        }

        // PDF → iframe (HTML now routes to the editor above).
        if (ext === 'pdf') {
            try {
                var info = await getBlobUrl(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '<iframe class="preview-iframe" src="' + escapeHtml(info.url) + '"></iframe>';
            } catch (e) {
                renderError(container, e.message || String(e));
            }
            return;
        }

        // Plain images (jpg/png/gif/webp/svg) → <img>. TIFF goes through preview-lib.
        if (preview && preview.isImage(ext) && !preview.isTiff(ext)) {
            try {
                var imgInfo = await getBlobUrl(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '<img class="preview-image" alt="' + escapeHtml(node.name)
                    + '" src="' + escapeHtml(imgInfo.url) + '">';
            } catch (e) {
                renderError(container, e.message || String(e));
            }
            return;
        }

        if (preview && preview.isTiff(ext)) {
            try {
                var tiffBuf = await getArrayBuffer(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '';
                await preview.renderTiff(document, container, tiffBuf, { fileName: node.name });
            } catch (e) {
                renderError(container, 'Failed to render TIFF: ' + (e.message || e));
            }
            return;
        }

        if (preview && preview.isZip(ext)) {
            try {
                var zipBuf = await getArrayBuffer(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '';
                await preview.renderZipListing(document, container, zipBuf, { fileName: node.name });
            } catch (e) {
                renderError(container, 'Failed to read ZIP: ' + (e.message || e));
            }
            return;
        }

        // Office docs (.docx via docx-preview, .xlsx/.xls via SheetJS) →
        // shared/preview-lib renderers. .doc/.ppt etc. fall through to the
        // download fallback below.
        if (preview && preview.isOffice(ext)) {
            try {
                var officeBuf = await getArrayBuffer(node);
                if (seq !== renderSeq) return;
                container.innerHTML = '';
                if (ext === 'docx') {
                    await preview.renderDocx(document, container, officeBuf, { fileName: node.name });
                } else {
                    await preview.renderXlsx(document, container, officeBuf, { fileName: node.name });
                }
            } catch (e) {
                renderError(container, 'Failed to render ' + ext.toUpperCase() + ': ' + (e.message || e));
            }
            return;
        }

        if (preview && preview.isText(ext)) {
            try {
                var txtBuf = await getArrayBuffer(node);
                if (seq !== renderSeq) return;
                var text = new TextDecoder('utf-8', { fatal: false }).decode(txtBuf);
                var MAX = 200000;
                if (text.length > MAX) {
                    text = text.substring(0, MAX) + '\n\n... (truncated, '
                        + (text.length - MAX) + ' more chars)';
                }
                container.innerHTML = '';
                var pre = document.createElement('pre');
                pre.className = 'preview-text';
                pre.textContent = text;
                container.appendChild(pre);
            } catch (e) {
                renderError(container, e.message || String(e));
            }
            return;
        }

        // Unknown type — offer a download link.
        try {
            var fallbackInfo = await getBlobUrl(node);
            if (seq !== renderSeq) return;
            container.innerHTML =
                '<div class="preview-empty">'
                + 'No inline preview for <code>.' + escapeHtml(ext) + '</code>. '
                + '<br><a class="btn btn-primary btn-sm" download="' + escapeHtml(node.name)
                + '" href="' + escapeHtml(fallbackInfo.url) + '" style="margin-top:1rem">'
                + 'Download ' + escapeHtml(node.name) + '</a>'
                + '</div>';
        } catch (e) {
            renderError(container, 'No source for ' + node.name);
        }
    }

    // ── Popup window (kept for "Pop out" button) ────────────────────────────

    function popupShell(node, primaryUrl) {
        var safeName = escapeHtml(node.name);
        var safeHref = escapeHtml(primaryUrl);
        var ext = (node.ext || '').toLowerCase();
        var contentHtml;
        if (ext === 'pdf') {
            contentHtml = '<iframe src="' + safeHref + '"></iframe>';
        } else if (ext === 'html' || ext === 'htm') {
            contentHtml = '<iframe src="' + safeHref + '" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>';
        } else if (preview && preview.isImage(ext) && !preview.isTiff(ext)) {
            contentHtml = '<img class="preview-image" src="' + safeHref + '" alt="' + safeName + '">';
        } else {
            contentHtml = '<div id="previewContent"><div class="loading">Loading preview…</div></div>';
        }
        return '<!DOCTYPE html><html><head><meta charset="UTF-8">'
            + '<title>' + safeName + ' — preview</title><style>'
            + '*{margin:0;padding:0;box-sizing:border-box;}'
            + 'body{display:flex;flex-direction:column;height:100vh;'
            + 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}'
            + '.toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;'
            + 'background:#f5f5f5;border-bottom:1px solid #ddd;}'
            + '.toolbar h1{flex:1;font-size:.95rem;font-weight:500;overflow:hidden;'
            + 'text-overflow:ellipsis;white-space:nowrap;}'
            + '.btn{padding:.4rem .8rem;font-size:.85rem;border:1px solid #ccc;'
            + 'border-radius:4px;background:white;cursor:pointer;}'
            + '.btn:hover{background:#e8e8e8;}'
            + 'iframe{flex:1;width:100%;border:none;}'
            + '#previewContent{flex:1;overflow:auto;display:flex;flex-direction:column;}'
            + '.loading{display:flex;align-items:center;justify-content:center;height:100%;'
            + 'color:#666;font-size:1.1rem;}'
            + 'img.preview-image{max-width:100%;max-height:100%;object-fit:contain;'
            + 'margin:auto;display:block;}'
            + 'pre.preview-text{padding:1rem;font-family:Consolas,Monaco,monospace;'
            + 'font-size:.85rem;white-space:pre-wrap;word-wrap:break-word;}'
            + '</style></head><body>'
            + '<div class="toolbar"><h1>' + safeName + '</h1>'
            + '<button class="btn" onclick="downloadFile()">Download</button></div>'
            + contentHtml
            + '<script>'
            + 'var blobUrl=' + JSON.stringify(primaryUrl) + ';'
            + 'var fileName=' + JSON.stringify(node.name) + ';'
            + 'function downloadFile(){var a=document.createElement("a");'
            + 'a.href=blobUrl;a.download=fileName;document.body.appendChild(a);'
            + 'a.click();document.body.removeChild(a);}'
            + '</' + 'script></body></html>';
    }

    async function renderInPopupWindow(node, win, info) {
        var ext = (node.ext || '').toLowerCase();
        if (ext === 'pdf' || ext === 'html' || ext === 'htm') return;
        if (preview && preview.isImage(ext) && !preview.isTiff(ext)) return;
        var c = win.document.getElementById('previewContent');
        if (!c) return;
        try {
            if (preview && preview.isTiff(ext)) {
                var tb = await getArrayBuffer(node);
                await preview.renderTiff(win.document, c, tb, { fileName: node.name });
            } else if (preview && preview.isZip(ext)) {
                var zb = await getArrayBuffer(node);
                await preview.renderZipListing(win.document, c, zb, { fileName: node.name });
            } else if (preview && preview.isOffice(ext)) {
                var ob = await getArrayBuffer(node);
                if (ext === 'docx') {
                    await preview.renderDocx(win.document, c, ob, { fileName: node.name });
                } else {
                    await preview.renderXlsx(win.document, c, ob, { fileName: node.name });
                }
            } else if (preview && preview.isText(ext)) {
                var txb = await getArrayBuffer(node);
                var text = new TextDecoder('utf-8', { fatal: false }).decode(txb);
                var MAX = 200000;
                if (text.length > MAX) text = text.substring(0, MAX) + '\n\n... (truncated)';
                var pre = win.document.createElement('pre');
                pre.className = 'preview-text';
                pre.textContent = text;
                c.innerHTML = '';
                c.appendChild(pre);
            } else {
                c.innerHTML = '<div class="loading">No inline preview for .'
                    + escapeHtml(ext) + ' — click Download.</div>';
            }
        } catch (e) {
            c.innerHTML = '<div class="loading">Error: ' + escapeHtml(e.message || e) + '</div>';
        }
    }

    async function renderInPopup(node) {
        // Editor-type files (markdown, yaml/.zddc, code text) can't be hosted
        // in the lightweight popup window — they need the bundled editor. Pop
        // them out as the FULL browse app deep-linked to the file, which loads
        // the real editor in a new window. Server mode only; HTML keeps its
        // rendered popup. Falls through to the lightweight popup otherwise.
        var pext = (node.ext || '').toLowerCase();
        var ym = window.app.modules.yamledit;
        var isEditorType = pext === 'md' || pext === 'markdown'
            || (ym && ym.handles && ym.handles(node) && pext !== 'html' && pext !== 'htm');
        if (isEditorType && window.app.state.source === 'server' && node.url) {
            var slash = node.url.lastIndexOf('/');
            var pdir = slash >= 0 ? node.url.slice(0, slash + 1) : '/';
            var pbase = slash >= 0 ? node.url.slice(slash + 1) : node.url;
            var pp = new URLSearchParams();
            try { pp.set('file', decodeURIComponent(pbase)); } catch (_e) { pp.set('file', pbase); }
            if (window.app.state.showHidden) pp.set('hidden', '1');
            window.open(pdir + '?' + pp.toString(), '_blank', 'noopener');
            return;
        }
        var info;
        try {
            info = await getBlobUrl(node);
        } catch (e) {
            window.app.modules.events.statusError('Pop-out failed: ' + e.message);
            return;
        }
        var html = popupShell(node, info.url);
        var win = state.previewWindow;
        if (win && !win.closed) {
            win.document.open();
            win.document.write(html);
            win.document.close();
            win.focus();
        } else {
            var w = Math.round(screen.width * 0.6);
            var h = Math.round(screen.height * 0.8);
            var left = Math.round((screen.width - w) / 2);
            var top = Math.round((screen.height - h) / 2);
            win = window.open('', 'browseFilePreview',
                'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top
                + ',resizable=yes,scrollbars=yes');
            if (!win) {
                window.open(info.url, '_blank', 'noopener');
                return;
            }
            win.document.write(html);
            win.document.close();
            win.focus();
            state.previewWindow = win;
        }
        await renderInPopupWindow(node, win, info);
    }

    // ── Public entry ────────────────────────────────────────────────────────

    async function showFilePreview(node, opts) {
        opts = opts || {};
        // Table-leaf dirs (mdl/rsk/ssr, default_tool=tables) open the tables
        // tool inline in the preview pane instead of expanding/navigating.
        if (window.app.modules.util.isTableLeaf(node)) return renderTableLeaf(node);
        if (node.isDir) return;
        if (opts.popup) return renderInPopup(node);
        return renderInline(node, opts);
    }

    // renderTableLeaf embeds the tables tool for a default_tool=tables
    // directory as an iframe scoped to that dir — the same in-pane tool
    // embed pattern grid.js uses for classifier. Server mode only (the
    // default_tool listing hint that flags a table-leaf is absent offline,
    // so this never fires on file:// — the dir stays an ordinary folder).
    function renderTableLeaf(node) {
        disposeEditors();
        var container = document.getElementById('previewBody');
        var titleEl = document.getElementById('previewTitle');
        var metaEl = document.getElementById('previewMeta');
        var popoutBtn = document.getElementById('previewPopout');
        if (!container) return;
        if (titleEl) titleEl.textContent = node.displayName || node.name;
        if (metaEl) metaEl.textContent = 'table';
        if (popoutBtn) popoutBtn.classList.add('hidden');
        if (window.app.state.source !== 'server' || !node.url) {
            renderEmpty(container, 'Table view is available in server mode.');
            return;
        }
        // The tables tool is served at the dir's NO-SLASH URL (the cascade's
        // default_tool routing). The trailing-slash form would serve the
        // browse listing instead, and <dir>/tables.html 404s for a virtual
        // dir (mdl/rsk/ssr have no on-disk folder). So strip the slash.
        var src = node.url.replace(/\/+$/, '');
        container.innerHTML = '';
        var frame = document.createElement('iframe');
        frame.className = 'preview-iframe';
        frame.src = src;
        frame.setAttribute('title', 'Table: ' + (node.displayName || node.name));
        container.appendChild(frame);
    }

    window.app.modules.preview = {
        showFilePreview: showFilePreview,
        // Tear down any live editor + blank the pane (rescope / popstate).
        clearPreview: clearPreview,
        // Expose for the markdown plugin so it can read file bytes.
        getArrayBuffer: getArrayBuffer,
        // Like getArrayBuffer but also returns the {etag, lastModified}
        // version token — the editors use it for optimistic-concurrency saves.
        getContentWithVersion: getContentWithVersion
    };
})();
