(function () {
    var mount = document.getElementById('editor');
    var hidden = document.getElementById('editor-content');
    if (!mount || !hidden || !window.CM) return;

    var form = hidden.closest('form');

    var T = EditorTables;
    var D = EditorDates;
    var M = EditorMovie;

    // --- CodeMirror setup ---

    // Shift+Enter (new table row below) / Shift+Delete (delete table row) run at
    // highest precedence so they win over CM's default newline/forward-delete.
    // Returning false (no table at cursor) lets CM fall back to its default.
    function tableKey(fn) {
        return function (view) {
            var result = fn(view.state.doc.toString(), view.state.selection.main.head);
            if (!result) return false;
            dispatchFullReplace(result);
            return true;
        };
    }

    var tableKeymap = [
        { key: 'Shift-Enter', run: tableKey(T.insertRowBelow) },
        { key: 'Shift-Delete', run: tableKey(T.deleteRow) },
    ];

    var state = CM.EditorState.create({
        doc: hidden.value,
        extensions: [
            CM.history(),
            CM.drawSelection(),
            CM.indentOnInput(),
            CM.EditorView.lineWrapping,
            // Enable native browser spellcheck on the contenteditable surface
            // (CM6 leaves it off by default). autocapitalize helps prose entry
            // on the Android/mobile path; CM's DOM observer absorbs corrections.
            CM.EditorView.contentAttributes.of({ spellcheck: 'true', autocapitalize: 'sentences' }),
            CM.markdown({ base: CM.markdownLanguage }),
            CM.syntaxHighlighting(CM.highlightStyle),
            CM.closeBrackets(),
            CM.autocompletion({ override: [WikiComplete.source] }),
            CM.theme,
            CM.Prec.highest(CM.keymap.of(tableKeymap)),
            CM.keymap.of([].concat(
                CM.closeBracketsKeymap,
                CM.completionKeymap,
                CM.markdownKeymap,
                CM.defaultKeymap,
                CM.historyKeymap,
                [CM.indentWithTab]
            )),
        ],
    });

    var view = new CM.EditorView({ state: state, parent: mount });
    view.focus();

    // --- CM document helpers ---

    function dispatchFullReplace(result) {
        view.dispatch({
            changes: { from: 0, to: view.state.doc.length, insert: result.text },
            selection: { anchor: result.cursor },
            scrollIntoView: true,
        });
        view.focus();
    }

    function insertAtCursor(s) {
        var sel = view.state.selection.main;
        view.dispatch({
            changes: { from: sel.from, to: sel.to, insert: s },
            selection: { anchor: sel.from + s.length },
            scrollIntoView: true,
        });
        view.focus();
    }

    function wrap(before, after, placeholder) {
        var sel = view.state.selection.main;
        var hadSelection = sel.to > sel.from;
        var selected = hadSelection ? view.state.sliceDoc(sel.from, sel.to) : placeholder;
        var insert = before + selected + after;
        var anchor, head;
        if (hadSelection) {
            anchor = head = sel.from + insert.length;
        } else {
            anchor = sel.from + before.length;
            head = anchor + placeholder.length;
        }
        view.dispatch({
            changes: { from: sel.from, to: sel.to, insert: insert },
            selection: { anchor: anchor, head: head },
            scrollIntoView: true,
        });
        view.focus();
    }

    function linePrefix(prefix) {
        var sel = view.state.selection.main;
        var line = view.state.doc.lineAt(sel.from);
        view.dispatch({
            changes: { from: line.from, to: line.from, insert: prefix },
            selection: { anchor: sel.from + prefix.length },
            scrollIntoView: true,
        });
        view.focus();
    }

    function applyTableOp(fn, arg) {
        var text = view.state.doc.toString();
        var pos = view.state.selection.main.head;
        var result = arg !== undefined ? fn(text, pos, arg) : fn(text, pos);
        if (result) dispatchFullReplace(result);
    }

    // Adapter passed to movie.js so it reads/writes the CM document instead of a
    // textarea (replace() dispatches a transaction; cursor lands after the block).
    var movieCtx = {
        getValue: function () { return view.state.doc.toString(); },
        replace: function (start, end, text) {
            view.dispatch({
                changes: { from: start, to: end, insert: text },
                selection: { anchor: start + text.length },
                scrollIntoView: true,
            });
            view.focus();
        },
    };

    // --- Content sync ---

    // Serialize the CM document into the hidden textarea on submit only — Save
    // must not depend on CM focus or async state (decision 8). requestSubmit
    // fires this listener even for the ALT+SHIFT+S path.
    function syncContent() {
        hidden.value = view.state.doc.toString();
    }

    // Save POSTs via fetch and then rewrites the *editor's* history entry with
    // the resulting page, so the edit session and its result share a single
    // entry (see history-nav.js). A plain form submit would push a second one
    // and leave the editor sitting in history behind the saved page.
    //
    // The server answers 204 + X-Target instead of a 303 because the target may
    // carry a #section anchor the client cannot compute, and fetch drops the
    // fragment from a followed redirect.
    function postSave() {
        syncContent();
        var body = new URLSearchParams(new FormData(form)).toString();
        fetch(form.action, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-Save-Mode': 'replace',
            },
            body: body,
        }).then(function (res) {
            if (!res.ok) {
                return res.text().then(function (msg) {
                    alert(msg || ('Save failed (' + res.status + ')'));
                });
            }
            var target = res.headers.get('X-Target') || form.action;
            // replaceState + reload rather than location.replace: if target
            // differs from the current URL only by fragment the browser would
            // skip the re-fetch and show pre-save content.
            window.history.replaceState(null, '', target);
            window.location.reload();
        }).catch(function () {
            alert('Network error — the page was not saved');
        });
    }

    form.addEventListener('submit', function (e) {
        e.preventDefault();
        postSave();
    });

    // --- Actions ---

    var actions = {
        save:           function () { form.requestSubmit(); },
        undo:           function () { CM.undo(view); view.focus(); },
        redo:           function () { CM.redo(view); view.focus(); },
        deleteline:     function () { CM.deleteLine(view); view.focus(); },
        bold:           function () { wrap('**', '**', 'bold text'); },
        italic:         function () { wrap('*', '*', 'italic text'); },
        h1:             function () { linePrefix('# '); },
        h2:             function () { linePrefix('## '); },
        h3:             function () { linePrefix('### '); },
        code:           function () { wrap('`', '`', 'code'); },
        codeblock:      function () { wrap('```\n', '\n```', 'code'); },
        quote:          function () { linePrefix('> '); },
        link:           function () { wrap('[', '](url)', 'link text'); },
        wikilink:       insertWikilink,
        ul:             function () { linePrefix('- '); },
        ol:             function () { linePrefix('1. '); },
        task:           function () { linePrefix('- [ ] '); },
        hr:             function () { wrap('\n\n---\n\n', '', ''); },
        fmttable:       function () { applyTableOp(T.formatTableText); },
        tblalignleft:   function () { applyTableOp(T.setColumnAlignment, 'left'); },
        tblaligncenter: function () { applyTableOp(T.setColumnAlignment, 'center'); },
        tblalignright:  function () { applyTableOp(T.setColumnAlignment, 'right'); },
        tblinsertcol:   function () { applyTableOp(T.insertColumn); },
        tbldeletecol:   function () { applyTableOp(T.deleteColumn); },
        tblinsertrow:   function () { applyTableOp(T.insertRow); },
        tbldeleterow:   function () { applyTableOp(T.deleteRow); },
        dateiso:        function () { insertAtCursor(D.isoDate()); },
        datelong:       function () { insertAtCursor(D.longDate()); },
        movie:          function () { M.run(movieCtx); },
    };

    // isValidWikiTarget mirrors the Go validator in wikilinks.go — absolute
    // path, no empty/dot segments. Used to gate the modal's INSERT button.
    function isValidWikiTarget(p) {
        if (!p || p[0] !== '/') return false;
        var trimmed = p.replace(/^\/+|\/+$/g, '');
        if (trimmed === '') return true;
        var segs = trimmed.split('/');
        for (var i = 0; i < segs.length; i++) {
            if (segs[i] === '' || segs[i] === '.' || segs[i] === '..') return false;
        }
        return true;
    }

    // Wiki link button (ALT+SHIFT+P): open a modal with a target field backed by
    // full /_search typeahead plus an optional display-text field, then insert
    // [[target]] or [[target::display]] at the cursor. (Inline `[[` typing uses
    // the folder-scoped completion in wikicomplete.js instead.)
    function insertWikilink() {
        var sel = view.state.selection.main;
        var selectedText = view.state.sliceDoc(sel.from, sel.to);

        var container = document.createElement('div');

        var targetWrap = document.createElement('div');
        var targetInput = document.createElement('input');
        targetInput.type = 'text';
        targetInput.className = 'input';
        targetInput.placeholder = 'Page path or search…';
        targetWrap.appendChild(targetInput);

        var displayInput = document.createElement('input');
        displayInput.type = 'text';
        displayInput.className = 'input';
        displayInput.placeholder = 'Display text (optional)';
        if (selectedText) displayInput.value = selectedText;

        container.appendChild(targetWrap);
        container.appendChild(displayInput);

        var handle = openModal({
            title: 'Insert link',
            body: container,
            confirm: {
                label: 'INSERT',
                initiallyDisabled: true,
                onConfirm: function () {
                    var target = targetInput.value.trim();
                    if (!isValidWikiTarget(target)) return;
                    var display = displayInput.value.trim();
                    handle.close();
                    insertAtCursor(display ? '[[' + target + '::' + display + ']]' : '[[' + target + ']]');
                }
            }
        });

        function updateConfirm() {
            handle.setConfirmDisabled(!isValidWikiTarget(targetInput.value.trim()));
        }
        targetInput.addEventListener('input', updateConfirm);

        window.attachSuggestions(targetInput, {
            showFooter: false,
            container: targetWrap,
            onPick: function (r) {
                targetInput.value = '/' + r.path;
                updateConfirm();
                displayInput.focus();
                displayInput.select();
            }
        });
    }

    // --- Keyboard shortcut registration ---

    var keyMap = {};
    document.querySelectorAll('[data-action]').forEach(function (btn) {
        btn.addEventListener('click', function () {
            var action = actions[btn.dataset.action];
            if (action) action();
        });
        if (btn.dataset.key) {
            keyMap[btn.dataset.key] = actions[btn.dataset.action];
        }
    });

    // Keep the editor focused when a toolbar button is tapped. Without this the
    // button steals focus on mousedown, which dismisses the mobile soft keyboard
    // before the action runs (and view.focus() can't reopen it without a direct
    // gesture). preventDefault on mousedown blocks the focus shift; click still
    // fires. Scoped to the toolbar so header SAVE/CANCEL are unaffected. Includes
    // dropdown toggles, which also must not pull focus off the editor.
    var toolbar = document.querySelector('.editor-toolbar');
    if (toolbar) {
        toolbar.addEventListener('mousedown', function (e) {
            if (e.target.closest('.btn')) e.preventDefault();
        });
    }

    document.addEventListener('keydown', function (e) {
        if (!e.altKey || !e.shiftKey) return;
        // Shift+digit produces a layout-dependent character in e.key (e.g. "!"
        // on US, "!" on DE), so fall back to e.code for digit rows.
        var key = /^Digit[0-9]$/.test(e.code) ? e.code.slice(5) : e.key;
        var action = keyMap[key];
        if (action) {
            e.preventDefault();
            action();
        }
    });

    // --- Dropdowns ---

    // The toolbar scrolls horizontally (so it clips its absolutely-positioned
    // menus) and on mobile is fixed to the bottom of the viewport. Pin an open
    // menu to the viewport so it escapes the clip, opening upward when there
    // isn't room below it (the bottom-toolbar case).
    function pinMenu(toggle, menu) {
        if (!menu.classList.contains('is-open')) return;
        var r = toggle.getBoundingClientRect();
        var vh = window.innerHeight;
        menu.style.position = 'fixed';
        menu.style.overflowY = 'auto';
        var spaceBelow = vh - r.bottom;
        var spaceAbove = r.top;
        if (spaceBelow < menu.offsetHeight + 8 && spaceAbove > spaceBelow) {
            menu.style.top = 'auto';
            menu.style.bottom = (vh - r.top) + 'px';
            menu.style.maxHeight = (spaceAbove - 8) + 'px';
        } else {
            menu.style.bottom = 'auto';
            menu.style.top = r.bottom + 'px';
            menu.style.maxHeight = (spaceBelow - 8) + 'px';
        }
        var left = Math.min(r.left, document.documentElement.clientWidth - menu.offsetWidth - 4);
        menu.style.left = Math.max(4, left) + 'px';
    }

    document.querySelectorAll('.dropdown-toggle').forEach(function (toggle) {
        wireDropdown(toggle);
        var menu = toggle.parentElement.querySelector('.dropdown-menu');
        if (!menu || !toolbar || !toolbar.contains(toggle)) return;
        // Runs after wireDropdown's own click handler has toggled is-open.
        toggle.addEventListener('click', function () { pinMenu(toggle, menu); });
    });
})();
