// preview-markdown.js — markdown plugin for the browse preview pane.
//
// Layout (CSS Grid):
//   ┌─────────────────────────────────────────────────────────────────┐
//   │  toolbar: Save | ● modified | status | source                   │
//   ├────────────────────────────────────────┬────────────────────────┤
//   │                                        │  Outline               │
//   │                                        │  • Heading 1           │
//   │   Toast UI Editor                      │    • Subheading        │
//   │   (md / wysiwyg / preview)             │  • Heading 2           │
//   │                                        ├────────────────────────┤
//   │                                        │  Front matter          │
//   │                                        │  title:    Foo         │
//   │                                        │  revision: A           │
//   └────────────────────────────────────────┴────────────────────────┘
// Grid keeps every cell's size definite, which is what Toast UI needs
// to compute its inner scroll regions correctly. The previous nested-
// flexbox layout produced indeterminate heights and a fragile TOC
// pane width — grid fixes both.
//
// Save (Ctrl+S) writes back via PUT (server mode) or
// FileSystemWritableFileStream (FS-API). Zip-virtual files are
// read-only — Save stays disabled. Toast UI is vendored
// (shared/vendor/toastui-editor-all.min.js); window.toastui is
// available synchronously before this module runs.
(function () {
    'use strict';

    if (!window.app || !window.app.modules) return;

    var SIDEBAR_MIN_WIDTH = 180;
    var SIDEBAR_MAX_WIDTH = 480;
    var SIDEBAR_DEFAULT_WIDTH = 280;
    var FM_DEFAULT_HEIGHT = 180; // px — front-matter pane height inside sidebar

    function escapeHtml(s) {
        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
            .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    }

    var currentInstance = null; // { editor, container, dirty, node, hash, tocEl, fmEl }
    var lastSidebarWidth = SIDEBAR_DEFAULT_WIDTH;  // remember across mounts
    var lastFmHeight = FM_DEFAULT_HEIGHT;

    async function hashContent(text) {
        if (!window.crypto || !window.crypto.subtle) return null;
        var enc = new TextEncoder().encode(text);
        var buf = await window.crypto.subtle.digest('SHA-256', enc);
        var bytes = new Uint8Array(buf);
        var hex = '';
        for (var i = 0; i < bytes.length; i++) {
            hex += bytes[i].toString(16).padStart(2, '0');
        }
        return hex;
    }

    function dispose() {
        if (currentInstance && currentInstance.editor) {
            try { currentInstance.editor.destroy(); } catch (_) { /* ignore */ }
        }
        currentInstance = null;
    }

    // ── Front matter ────────────────────────────────────────────────────────
    // Lightweight YAML front-matter parser. Same envelope as mdedit's:
    // `---\n…\n---\n`, key:value lines, simple `[a, b, c]` arrays.

    function parseFrontMatter(content) {
        if (!content || content.indexOf('---\n') !== 0) {
            return { data: {}, body: content || '' };
        }
        var endIdx = content.indexOf('\n---\n', 4);
        if (endIdx === -1) return { data: {}, body: content };
        var fmText = content.substring(4, endIdx);
        var body = content.substring(endIdx + 5);
        var data = {};
        var lines = fmText.split('\n');
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (!line || line.charAt(0) === '#') continue;
            var colon = line.indexOf(':');
            if (colon <= 0) continue;
            var key = line.substring(0, colon).trim();
            var val = line.substring(colon + 1).trim();
            val = val.replace(/^["']|["']$/g, '');
            if (val.startsWith('[') && val.endsWith(']')) {
                val = val.slice(1, -1).split(',').map(function (s) {
                    return s.trim().replace(/^["']|["']$/g, '');
                });
            }
            data[key] = val;
        }
        return { data: data, body: body };
    }

    function renderFrontMatter(fmEl, content) {
        if (!fmEl) return;
        var parsed = parseFrontMatter(content);
        var keys = Object.keys(parsed.data);
        if (keys.length === 0) {
            fmEl.innerHTML = '<p class="md-fm__empty">No front matter.</p>';
            return;
        }
        var html = '<dl class="md-fm__list">';
        for (var i = 0; i < keys.length; i++) {
            var k = keys[i];
            var v = parsed.data[k];
            var displayV = Array.isArray(v)
                ? v.map(escapeHtml).join(', ')
                : escapeHtml(String(v));
            html += '<dt>' + escapeHtml(k) + '</dt><dd>' + displayV + '</dd>';
        }
        html += '</dl>';
        fmEl.innerHTML = html;
    }

    // ── TOC (table of contents) ────────────────────────────────────────────
    // ATX headings only; the body markdown drives the outline. Clicking
    // a heading routes to whichever Toast UI pane is currently active
    // (WYSIWYG or markdown preview).

    function parseHeadings(content) {
        var headings = [];
        // Strip front matter so headings inside the envelope (e.g. comments)
        // don't appear in the outline.
        var parsed = parseFrontMatter(content);
        var body = parsed.body;
        var lines = body.split('\n');
        var inFence = false;
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];
            // Skip fenced code blocks — headings inside them aren't real.
            if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
            if (inFence) continue;
            var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
            if (!m) continue;
            var text = m[2]
                .replace(/\\(.)/g, '$1')
                .replace(/\*\*(.*?)\*\*/g, '$1')
                .replace(/\*(.*?)\*/g, '$1')
                .replace(/`(.*?)`/g, '$1')
                .replace(/\[(.*?)\]\(.*?\)/g, '$1')
                .replace(/~~(.*?)~~/g, '$1')
                .trim();
            headings.push({ level: m[1].length, text: text, lineIndex: i });
        }
        return headings;
    }

    function scrollEditorToHeading(editor, heading) {
        try {
            var els = editor.getEditorElements();
            if (editor.isWysiwygMode && editor.isWysiwygMode()) {
                var ww = els.wwEditor;
                if (!ww) return;
                var hs = ww.querySelectorAll('h1, h2, h3, h4, h5, h6');
                for (var i = 0; i < hs.length; i++) {
                    if (hs[i].textContent.trim() === heading.text) {
                        var scroller = findScrollParent(hs[i]) || ww;
                        scroller.scrollTo({
                            top: hs[i].offsetTop - 12,
                            behavior: 'smooth'
                        });
                        flashHeading(hs[i]);
                        return;
                    }
                }
            } else {
                var line = heading.lineIndex + 1;
                try { editor.setSelection([line, 1], [line, 1]); } catch (_) { /* ignore */ }
                // Find the matching heading in the live markdown preview
                // (right column of split view). If preview is collapsed
                // (markdown-only) this is a no-op.
                var preview = els.mdPreview;
                if (preview) {
                    var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
                    for (var j = 0; j < phs.length; j++) {
                        if (phs[j].textContent.trim() === heading.text) {
                            var pscroller = findScrollParent(phs[j]) || preview;
                            pscroller.scrollTo({
                                top: phs[j].offsetTop - 12,
                                behavior: 'smooth'
                            });
                            flashHeading(phs[j]);
                            return;
                        }
                    }
                }
            }
        } catch (_e) { /* swallow; click was best-effort */ }
    }

    function findScrollParent(el) {
        var cur = el.parentElement;
        while (cur) {
            var s = getComputedStyle(cur);
            if (/(auto|scroll)/.test(s.overflowY)) return cur;
            cur = cur.parentElement;
        }
        return null;
    }

    function flashHeading(el) {
        if (!el) return;
        el.classList.add('md-toc__flash');
        setTimeout(function () { el.classList.remove('md-toc__flash'); }, 900);
    }

    function renderToc(tocEl, content, editor) {
        if (!tocEl) return;
        if (!content || !content.trim()) {
            tocEl.innerHTML = '<p class="md-toc__empty">Empty file.</p>';
            return;
        }
        var headings = parseHeadings(content);
        if (headings.length === 0) {
            tocEl.innerHTML = '<p class="md-toc__empty">No headings yet.</p>';
            return;
        }
        // Build a flat list; CSS handles indentation. Using a flat list
        // (rather than nested <ul>s) keeps the click target a clean,
        // full-width row regardless of heading depth.
        var html = '<ul class="md-toc__list">';
        for (var i = 0; i < headings.length; i++) {
            var h = headings[i];
            html += '<li class="md-toc__item md-toc__item--l' + h.level + '"'
                + ' data-line="' + h.lineIndex + '"'
                + ' data-text="' + escapeHtml(h.text) + '"'
                + ' title="' + escapeHtml(h.text) + '">'
                + escapeHtml(h.text)
                + '</li>';
        }
        html += '</ul>';
        tocEl.innerHTML = html;
        tocEl.querySelectorAll('.md-toc__item').forEach(function (li) {
            li.addEventListener('click', function () {
                var idx = parseInt(li.dataset.line, 10);
                var text = li.dataset.text;
                scrollEditorToHeading(editor, { text: text, lineIndex: idx });
            });
        });
    }

    function debounce(fn, ms) {
        var t;
        return function () {
            clearTimeout(t);
            var args = arguments, self = this;
            t = setTimeout(function () { fn.apply(self, args); }, ms);
        };
    }

    // ── Save ────────────────────────────────────────────────────────────────

    async function saveContent(node, content) {
        if (node.handle && typeof node.handle.createWritable === 'function') {
            var writable = await node.handle.createWritable();
            await writable.write(content);
            await writable.close();
            return;
        }
        if (node.url && window.app.state.source === 'server') {
            var resp = await fetch(node.url, {
                method: 'PUT',
                headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
                body: content,
                credentials: 'same-origin'
            });
            if (!resp.ok) throw new Error('HTTP ' + resp.status);
            return;
        }
        throw new Error('No write target for this file (read-only source).');
    }

    function canSave(node) {
        if (node.zipParentId != null) return false;
        if (node.handle && typeof node.handle.createWritable === 'function') return true;
        if (node.url && window.app.state.source === 'server') return true;
        return false;
    }

    // ── Mount ───────────────────────────────────────────────────────────────

    async function render(node, container, ctx) {
        if (typeof window.toastui === 'undefined') {
            container.innerHTML =
                '<div class="preview-empty" style="color:var(--danger)">'
                + 'Toast UI Editor isn\'t bundled in this build.</div>';
            return;
        }
        dispose();

        // Read content.
        var text;
        try {
            var buf = await ctx.getArrayBuffer(node);
            text = new TextDecoder('utf-8', { fatal: false }).decode(buf);
        } catch (e) {
            container.innerHTML =
                '<div class="preview-empty" style="color:var(--danger)">'
                + 'Could not read ' + escapeHtml(node.name) + ': '
                + escapeHtml(e.message || String(e)) + '</div>';
            return;
        }

        // Wipe the container and install a single shell child. The
        // shell mirrors mdedit's layout: sidebar on the LEFT (front
        // matter top, TOC bottom), content on the RIGHT (informational
        // header above the Toast UI editor). CSS Grid keeps every
        // cell sized definitely so Toast UI's scroll regions resolve
        // correctly.
        container.innerHTML = '';
        var shell = document.createElement('div');
        shell.className = 'md-shell';
        shell.style.gridTemplateColumns = lastSidebarWidth + 'px 1fr';
        container.appendChild(shell);

        // ── Sidebar (col 1): front matter (top) + TOC (bottom) ──────────────
        var sidebar = document.createElement('div');
        sidebar.className = 'md-shell__sidebar';
        sidebar.style.gridTemplateRows = lastFmHeight + 'px 1fr';
        shell.appendChild(sidebar);

        var fmSection = document.createElement('section');
        fmSection.className = 'md-side md-side--fm';
        var fmHeader = document.createElement('div');
        fmHeader.className = 'md-side__header';
        fmHeader.textContent = 'YAML front matter';
        var fmBody = document.createElement('div');
        fmBody.className = 'md-side__body md-fm__body';
        fmSection.appendChild(fmHeader);
        fmSection.appendChild(fmBody);
        sidebar.appendChild(fmSection);

        // Horizontal resizer between front-matter and TOC.
        var fmResizer = document.createElement('div');
        fmResizer.className = 'md-shell__fmresizer';
        fmResizer.setAttribute('role', 'separator');
        fmResizer.setAttribute('aria-orientation', 'horizontal');
        fmResizer.setAttribute('aria-label', 'Resize front-matter pane');
        fmResizer.tabIndex = 0;
        sidebar.appendChild(fmResizer);

        var tocSection = document.createElement('section');
        tocSection.className = 'md-side md-side--toc';
        var tocHeader = document.createElement('div');
        tocHeader.className = 'md-side__header';
        tocHeader.textContent = 'Outline';
        var tocBody = document.createElement('div');
        tocBody.className = 'md-side__body md-toc__body';
        tocSection.appendChild(tocHeader);
        tocSection.appendChild(tocBody);
        sidebar.appendChild(tocSection);

        // Vertical resizer between sidebar and content.
        var resizer = document.createElement('div');
        resizer.className = 'md-shell__resizer';
        resizer.setAttribute('role', 'separator');
        resizer.setAttribute('aria-orientation', 'vertical');
        resizer.setAttribute('aria-label', 'Resize sidebar');
        resizer.tabIndex = 0;
        shell.appendChild(resizer);

        // ── Content (col 2): informational header + editor ──────────────────
        var content = document.createElement('div');
        content.className = 'md-shell__content';
        shell.appendChild(content);

        // Informational header above the editor: file name + save +
        // dirty indicator + status + source hint. Renamed from
        // "toolbar" to read as a header, since it titles the content.
        var infohdr = document.createElement('div');
        infohdr.className = 'md-shell__infohdr';

        var titleEl = document.createElement('span');
        titleEl.className = 'md-shell__title';
        titleEl.textContent = node.name;
        titleEl.title = node.name;

        var saveBtn = document.createElement('button');
        saveBtn.className = 'btn btn-sm btn-primary md-shell__save';
        saveBtn.type = 'button';
        saveBtn.textContent = 'Save';
        saveBtn.disabled = true;

        var dirtyEl = document.createElement('span');
        dirtyEl.className = 'md-shell__dirty';

        var statusEl = document.createElement('span');
        statusEl.className = 'md-shell__status';

        var sourceEl = document.createElement('span');
        sourceEl.className = 'md-shell__source';
        if (node.zipParentId != null) {
            sourceEl.textContent = 'read-only (zip)';
        } else if (node.handle) {
            sourceEl.textContent = 'local';
        } else if (node.url) {
            sourceEl.textContent = 'server';
        }

        infohdr.appendChild(titleEl);
        infohdr.appendChild(dirtyEl);
        infohdr.appendChild(statusEl);
        infohdr.appendChild(sourceEl);
        infohdr.appendChild(saveBtn);
        content.appendChild(infohdr);

        // Editor host.
        var editorHost = document.createElement('div');
        editorHost.className = 'md-shell__editor';
        content.appendChild(editorHost);

        // Construct the editor. height: 100% works because editorHost
        // is a grid cell with a definite size.
        var initialHash = await hashContent(text);
        var editor = new window.toastui.Editor({
            el: editorHost,
            height: '100%',
            initialEditType: 'markdown',
            previewStyle: 'vertical',
            initialValue: text,
            usageStatistics: false,
            toolbarItems: [
                ['heading', 'bold', 'italic', 'strike'],
                ['hr', 'quote'],
                ['ul', 'ol', 'task', 'indent', 'outdent'],
                ['table', 'image', 'link'],
                ['code', 'codeblock']
            ]
        });

        currentInstance = {
            editor: editor,
            container: container,
            dirty: false,
            node: node,
            hash: initialHash,
            tocEl: tocBody,
            fmEl: fmBody
        };

        var writable = canSave(node);
        if (!writable) {
            saveBtn.disabled = true;
            saveBtn.title = 'Save not available — read-only source.';
        }

        renderToc(tocBody, text, editor);
        renderFrontMatter(fmBody, text);

        // ── Sidebar/content resizer ─────────────────────────────────────────
        // Sidebar is on the LEFT now. Dragging right grows the
        // sidebar; left shrinks it.
        (function () {
            var dragging = false;
            var startX = 0;
            var startW = 0;
            function onMove(e) {
                if (!dragging) return;
                var dx = e.clientX - startX;
                var w = startW + dx;
                w = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, w));
                lastSidebarWidth = w;
                shell.style.gridTemplateColumns = w + 'px 1fr';
                e.preventDefault();
            }
            function onUp() {
                dragging = false;
                resizer.classList.remove('is-dragging');
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            }
            resizer.addEventListener('mousedown', function (e) {
                dragging = true;
                resizer.classList.add('is-dragging');
                startX = e.clientX;
                startW = sidebar.getBoundingClientRect().width;
                document.addEventListener('mousemove', onMove);
                document.addEventListener('mouseup', onUp);
                e.preventDefault();
            });
            resizer.addEventListener('keydown', function (e) {
                if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
                e.preventDefault();
                var step = e.key === 'ArrowLeft' ? -24 : 24;
                var w = Math.max(SIDEBAR_MIN_WIDTH,
                    Math.min(SIDEBAR_MAX_WIDTH, lastSidebarWidth + step));
                lastSidebarWidth = w;
                shell.style.gridTemplateColumns = w + 'px 1fr';
            });
        })();

        // ── Front-matter / TOC vertical resizer ─────────────────────────────
        (function () {
            var FM_MIN = 60;
            var dragging = false;
            var startY = 0;
            var startH = 0;
            function maxFmHeight() {
                var sidebarRect = sidebar.getBoundingClientRect();
                // Leave at least 120 px for the TOC body + headers.
                return Math.max(FM_MIN, sidebarRect.height - 160);
            }
            function onMove(e) {
                if (!dragging) return;
                var dy = e.clientY - startY;
                var h = Math.max(FM_MIN, Math.min(maxFmHeight(), startH + dy));
                lastFmHeight = h;
                sidebar.style.gridTemplateRows = h + 'px 1fr';
                e.preventDefault();
            }
            function onUp() {
                dragging = false;
                fmResizer.classList.remove('is-dragging');
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            }
            fmResizer.addEventListener('mousedown', function (e) {
                dragging = true;
                fmResizer.classList.add('is-dragging');
                startY = e.clientY;
                startH = fmSection.getBoundingClientRect().height;
                document.addEventListener('mousemove', onMove);
                document.addEventListener('mouseup', onUp);
                e.preventDefault();
            });
            fmResizer.addEventListener('keydown', function (e) {
                if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
                e.preventDefault();
                var step = e.key === 'ArrowUp' ? -24 : 24;
                var h = Math.max(FM_MIN, Math.min(maxFmHeight(), lastFmHeight + step));
                lastFmHeight = h;
                sidebar.style.gridTemplateRows = h + 'px 1fr';
            });
        })();

        // ── Change tracking + auto-rerender ────────────────────────────────
        function markDirty(isDirty) {
            currentInstance.dirty = isDirty;
            saveBtn.disabled = !isDirty || !writable;
            dirtyEl.textContent = isDirty ? '● modified' : '';
        }

        var onChange = debounce(async function () {
            var current = editor.getMarkdown();
            var h = await hashContent(current);
            markDirty(h !== currentInstance.hash);
            renderToc(tocBody, current, editor);
            renderFrontMatter(fmBody, current);
        }, 250);
        editor.on('change', onChange);

        // ── Save ───────────────────────────────────────────────────────────
        async function save() {
            if (!currentInstance.dirty || !writable) return;
            var content = editor.getMarkdown();
            try {
                statusEl.textContent = 'Saving…';
                await saveContent(node, content);
                currentInstance.hash = await hashContent(content);
                markDirty(false);
                statusEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
                if (window.zddc && window.zddc.toast) {
                    window.zddc.toast('Saved ' + node.name, 'success');
                }
            } catch (e) {
                statusEl.textContent = 'Save failed: ' + (e.message || e);
                if (window.zddc && window.zddc.toast) {
                    window.zddc.toast('Save failed: ' + (e.message || e), 'error');
                }
            }
        }
        saveBtn.addEventListener('click', save);
        container.addEventListener('keydown', function (e) {
            if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
                e.preventDefault();
                save();
            }
        });
    }

    window.app.modules.markdown = {
        render: render,
        dispose: dispose
    };
})();
