import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter, codeFolding } from "@codemirror/language";
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
import { highlightSelectionMatches } from "@codemirror/search";
import { vim } from "@replit/codemirror-vim";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import byMimeType from "./syntax_highlighting.js";
import smartIndentWithTab from "./extensions/custom_tab.js";
import type { ThemeDefinition } from "./color_themes.js";
import { createSearchHighlighter, SearchHighlighter, searchMatchHighlightTheme } from "./find_replace.js";

export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";

type ContentChangedListener = () => void;

export interface EditorConfig {
    parent: HTMLElement;
    placeholder?: string;
    lineWrapping?: boolean;
    vimKeybindings?: boolean;
    readOnly?: boolean;
    /** Disables some of the nice-to-have features (bracket matching, syntax highlighting, indentation markers) in order to improve performance. */
    preferPerformance?: boolean;
    tabIndex?: number;
    onContentChanged?: ContentChangedListener;
}

export default class CodeMirror extends EditorView {

    private config: EditorConfig;
    private languageCompartment: Compartment;
    private historyCompartment: Compartment;
    private themeCompartment: Compartment;
    private lineWrappingCompartment: Compartment;
    private searchHighlightCompartment: Compartment;
    private searchPlugin?: SearchHighlighter | null;

    constructor(config: EditorConfig) {
        const languageCompartment = new Compartment();
        const historyCompartment = new Compartment();
        const themeCompartment = new Compartment();
        const lineWrappingCompartment = new Compartment();
        const searchHighlightCompartment = new Compartment();

        let extensions: Extension[] = [];

        if (config.vimKeybindings) {
            extensions.push(vim());
        }

        extensions = [
            ...extensions,
            languageCompartment.of([]),
            lineWrappingCompartment.of(config.lineWrapping ? EditorView.lineWrapping : []),
            searchMatchHighlightTheme,
            searchHighlightCompartment.of([]),
            highlightActiveLine(),
            lineNumbers(),
            indentUnit.of(" ".repeat(4)),
            keymap.of([
                ...defaultKeymap,
                ...historyKeymap,
                ...smartIndentWithTab
            ])
        ]

        if (!config.preferPerformance) {
            extensions = [
                ...extensions,
                themeCompartment.of([
                    syntaxHighlighting(defaultHighlightStyle, { fallback: true })
                ]),
                highlightSelectionMatches(),
                bracketMatching(),
                codeFolding(),
                foldGutter(),
                indentationMarkers(),
            ];
        }

        if (!config.readOnly) {
            // Logic specific to editable notes
            if (config.placeholder) {
                extensions.push(placeholder(config.placeholder));
            }

            if (config.onContentChanged) {
                extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v)));
            }

            extensions.push(historyCompartment.of(history()));
        } else {
            // Logic specific to read-only notes
            extensions.push(EditorState.readOnly.of(true));
        }

        super({
            parent: config.parent,
            extensions
        });

        if (config.tabIndex) {
            this.dom.tabIndex = config.tabIndex;
        }

        this.config = config;
        this.languageCompartment = languageCompartment;
        this.historyCompartment = historyCompartment;
        this.themeCompartment = themeCompartment;
        this.lineWrappingCompartment = lineWrappingCompartment;
        this.searchHighlightCompartment = searchHighlightCompartment;
    }

    #onDocumentUpdated(v: ViewUpdate) {
        if (v.docChanged) {
            this.config.onContentChanged?.();
        }
    }

    getText() {
        return this.state.doc.toString();
    }

    /**
     * Returns the currently selected text.
     *
     * If there are multiple selections, all of them will be concatenated.
     */
    getSelectedText() {
        return this.state.selection.ranges
            .map((range) => this.state.sliceDoc(range.from, range.to))
            .join("");
    }

    setText(content: string) {
        this.dispatch({
            changes: {
                from: 0,
                to: this.state.doc.length,
                insert: content || "",
            }
        })
    }

    async setTheme(theme: ThemeDefinition) {
        const extension = await theme.load();
        this.dispatch({
            effects: [ this.themeCompartment.reconfigure([ extension ]) ]
        });
    }

    setLineWrapping(wrapping: boolean) {
        this.dispatch({
            effects: [ this.lineWrappingCompartment.reconfigure(wrapping ? EditorView.lineWrapping : []) ]
        });
    }

    /**
     * Clears the history of undo/redo. Generally useful when changing to a new document.
     */
    clearHistory() {
        if (this.config.readOnly) {
            return;
        }

        this.dispatch({
            effects: [ this.historyCompartment.reconfigure([]) ]
        });
        this.dispatch({
            effects: [ this.historyCompartment.reconfigure(history())]
        });
    }

    scrollToEnd() {
        const endPos = this.state.doc.length;
        this.dispatch({
            selection: EditorSelection.cursor(endPos),
            effects: EditorView.scrollIntoView(endPos, { y: "end" }),
            scrollIntoView: true
        });
    }

    async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
        const plugin = createSearchHighlighter();
        this.dispatch({
            effects: this.searchHighlightCompartment.reconfigure(plugin)
        });

        // Wait for the plugin to activate in the next render cycle
        await new Promise(requestAnimationFrame);
        const instance = this.plugin(plugin);
        instance?.searchFor(searchTerm, matchCase, wholeWord);
        this.searchPlugin = instance;

        return {
            totalFound: instance?.totalFound ?? 0,
            currentFound: instance?.currentFound ?? 0
        }
    }

    async findNext(direction: number, currentFound: number, nextFound: number) {
        this.searchPlugin?.scrollToMatch(nextFound);
    }

    async replace(replaceText: string) {
        this.searchPlugin?.replaceActiveMatch(replaceText);
    }

    async replaceAll(replaceText: string) {
        this.searchPlugin?.replaceAll(replaceText);
    }

    cleanSearch() {
        if (this.searchPlugin) {
            this.dispatch({
                effects: this.searchHighlightCompartment.reconfigure([])
            });
            this.searchPlugin = null;
        }
    }

    async setMimeType(mime: string) {
        let newExtension: Extension[] = [];

        const correspondingSyntax = byMimeType[mime];
        if (correspondingSyntax) {
            const resolvedSyntax = await correspondingSyntax();

            if ("token" in resolvedSyntax) {
                const extension = StreamLanguage.define(resolvedSyntax);
                newExtension.push(extension);
            } else if (Array.isArray(resolvedSyntax)) {
                newExtension = [ ...newExtension, ...resolvedSyntax ];
            } else {
                newExtension.push(resolvedSyntax);
            }
        }

        this.dispatch({
            effects: this.languageCompartment.reconfigure(newExtension)
        });
    }
}
