import { Compartment, EditorState, Facet, StateEffect, StateField } from "@codemirror/state";
import { EditorView, ViewPlugin, keymap, showPanel, tooltips, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from "@codemirror/view";
import { debounce } from "lodash";
import { codeblockTheme } from "./themes/index";
import { vscodeLightDark, vscodeStyleMod } from "./themes/vscode";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import { detectIndentationUnit } from "./utils";
import { completionKeymap, closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
import { extOrLanguageToLanguageId, getLanguageSupport } from "./lsps";
import { documentUri, languageId } from '@marimo-team/codemirror-languageserver';
import { lintKeymap } from "@codemirror/lint";
import { highlightCode } from "@lezer/highlight";
import { LSP } from "./utils/lsp";
import { toolbarPanel, searchResultsField } from "./panels/toolbar";
import { StyleModule } from "style-mod";
import { dirname } from "path-browserify";
export const CodeblockFacet = Facet.define({
    combine: (values) => values[0]
});
// Compartments for dynamically reconfiguring extensions
export const configCompartment = new Compartment();
export const languageSupportCompartment = new Compartment();
export const languageServerCompartment = new Compartment();
export const indentationCompartment = new Compartment();
export const readOnlyCompartment = new Compartment();
// Effects + Fields for async file handling
export const openFileEffect = StateEffect.define();
export const fileLoadedEffect = StateEffect.define();
// Light mode/dark mode theme toggle
export const setThemeEffect = StateEffect.define();
// Holds the current file lifecycle
export const currentFileField = StateField.define({
    create(state) {
        const cfg = state.facet(CodeblockFacet);
        if (cfg.filepath) {
            // Seed an initial load; the plugin will react after init without dispatching during construction
            return { path: cfg.filepath, content: "", language: null, loading: true };
        }
        // No initial file; start with provided content
        return { path: null, content: cfg.content || "", language: cfg.language || null, loading: false };
    },
    update(value, tr) {
        for (let e of tr.effects) {
            if (e.is(openFileEffect)) {
                return { path: e.value.path, content: "", language: null, loading: true };
            }
            if (e.is(fileLoadedEffect)) {
                return { path: e.value.path, content: e.value.content, language: e.value.language, loading: false };
            }
        }
        return value;
    }
});
// A safe dispatcher to avoid nested-update errors from UI events during CM updates
function safeDispatch(view, spec) {
    // Always queue to a microtask so we never dispatch within an ongoing update cycle
    queueMicrotask(() => {
        try {
            view.dispatch(spec);
        }
        catch (e) {
            console.error(e);
        }
    });
}
const navigationKeymap = [{
        key: "ArrowUp",
        run: (view) => {
            const cursor = view.state.selection.main;
            const line = view.state.doc.lineAt(cursor.head);
            const toolbarInput = view.dom.querySelector('.cm-toolbar-input');
            if (line.number === 1 && toolbarInput) {
                toolbarInput.focus();
                return true;
            }
            return false;
        }
    }];
export const renderMarkdownCode = (code, parser, highlighter) => {
    let result = document.createElement("pre");
    function emit(text, classes) {
        let node = document.createTextNode(text);
        if (classes) {
            let span = document.createElement("span");
            span.appendChild(node);
            span.className = classes;
            node = span;
        }
        result.appendChild(node);
    }
    function emitBreak() { result.appendChild(document.createTextNode("\n")); }
    highlightCode(code, parser.parse(code), highlighter, emit, emitBreak);
    return result.getHTML();
};
// Main codeblock factory
export const codeblock = ({ content, fs, cwd, filepath, language, toolbar = true, index }) => [
    configCompartment.of(CodeblockFacet.of({ content, fs, filepath, cwd, language, toolbar, index })),
    currentFileField,
    languageSupportCompartment.of([]),
    languageServerCompartment.of([]),
    indentationCompartment.of(indentUnit.of("    ")),
    readOnlyCompartment.of(EditorState.readOnly.of(false)),
    tooltips({ position: "fixed" }),
    showPanel.of(toolbar ? toolbarPanel : null),
    codeblockTheme,
    codeblockView,
    keymap.of(navigationKeymap.concat([indentWithTab])),
    vscodeLightDark,
    searchResultsField,
];
// ViewPlugin reacts to field state & effects, with microtask scheduling to avoid nested updates
const codeblockView = ViewPlugin.define((view) => {
    StyleModule.mount(document, vscodeStyleMod);
    let { fs } = view.state.facet(CodeblockFacet);
    // Debounced save
    const save = debounce(async () => {
        const fileState = view.state.field(currentFileField);
        if (fileState.path) {
            // confirm parent exists
            const parent = dirname(fileState.path);
            if (parent) {
                await fs.mkdir(parent, { recursive: true }).catch(console.error);
            }
            await fs.writeFile(fileState.path, view.state.doc.toString()).catch(console.error);
        }
    }, 500);
    // Guard to prevent duplicate opens for same path while loading
    let opening = null;
    async function setLanguageSupport(language) {
        if (!language)
            return;
        const langSupport = await getLanguageSupport(extOrLanguageToLanguageId[language]);
        safeDispatch(view, {
            effects: [
                languageSupportCompartment.reconfigure(langSupport || []),
            ]
        });
    }
    async function handleOpen(path) {
        if (!path)
            return;
        if (opening === path)
            return;
        opening = path;
        try {
            const ext = path.split('.').pop()?.toLowerCase();
            const lang = (ext ? (extOrLanguageToLanguageId)[ext] ?? null : language) || 'markdown';
            let langSupport = lang ? await getLanguageSupport(lang) : null;
            safeDispatch(view, {
                effects: [
                    languageSupportCompartment.reconfigure(langSupport || []),
                ]
            });
            const exists = await fs.exists(path);
            const content = exists ? await fs.readFile(path) : "";
            const unit = detectIndentationUnit(content) || "    ";
            let lsp = lang ? await LSP.client({ view, language: lang, path, fs }) : null;
            safeDispatch(view, {
                changes: { from: 0, to: view.state.doc.length, insert: content },
                effects: [
                    indentationCompartment.reconfigure(indentUnit.of(unit)),
                    fileLoadedEffect.of({ path, content, language: lang }),
                    languageServerCompartment.reconfigure([
                        documentUri.of(`file:///${path}`),
                        languageId.of(lang || ""),
                        ...(lsp ? [lsp] : [])
                    ]),
                ]
            });
        }
        catch (e) {
            console.error("Failed to open file", e);
        }
        finally {
            opening = null;
        }
    }
    // On initial mount, if field indicates a pending load, kick it off *after* construction
    const { path, loading, language } = view.state.field(currentFileField);
    if (!path && language) {
        setLanguageSupport(language);
    }
    if (path && loading) {
        handleOpen(path);
    }
    return {
        update(u) {
            // React to explicit openFileEffect requests
            for (let e of u.transactions.flatMap(t => t.effects)) {
                if (e.is(openFileEffect))
                    queueMicrotask(() => handleOpen(e.value.path));
                if (e.is(setThemeEffect)) {
                    const dark = e.value.dark;
                    u.view.dom.setAttribute('data-theme', dark ? 'dark' : 'light');
                }
            }
            // Keep read-only in sync with loading state without dispatching new transactions
            const prev = u.startState.field(currentFileField);
            const next = u.state.field(currentFileField);
            if (prev.loading !== next.loading) {
                // Reconfigure readOnly via compartment inside the same update when possible
                safeDispatch(view, { effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(next.loading)) });
            }
            if (u.docChanged)
                save();
            // If fs changed via facet reconfig, refresh handle references
            const newFs = u.state.facet(CodeblockFacet).fs;
            if (fs !== newFs)
                fs = newFs;
        }
    };
});
export const basicSetup = (() => [
    lineNumbers(),
    highlightActiveLineGutter(),
    highlightSpecialChars(),
    history(),
    foldGutter(),
    drawSelection(),
    dropCursor(),
    EditorState.allowMultipleSelections.of(true),
    indentOnInput(),
    syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
    bracketMatching(),
    closeBrackets(),
    rectangularSelection(),
    crosshairCursor(),
    highlightActiveLine(),
    highlightSelectionMatches(),
    keymap.of([
        ...closeBracketsKeymap,
        ...defaultKeymap,
        ...searchKeymap,
        ...historyKeymap,
        ...foldKeymap,
        ...completionKeymap,
        ...lintKeymap
    ])
])();
export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark }) {
    const state = EditorState.create({
        doc: content,
        extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark })]
    });
    return new EditorView({ state, parent });
}
