import { jsx as _jsx } from "react/jsx-runtime";
import { insertNewline } from '@codemirror/commands';
import { Annotation, Compartment, EditorState, } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, placeholder, } from '@codemirror/view';
import { formatQuery, } from '@neo4j-cypher/language-support';
import debounce from 'lodash.debounce';
import { Component, createRef } from 'react';
import { DEBOUNCE_TIME } from './constants';
import { replaceHistory, replMode as historyNavigation, } from './historyNavigation';
import { cypher } from './lang-cypher/langCypher';
import { cleanupWorkers } from './lang-cypher/syntaxValidation';
import { basicNeo4jSetup } from './neo4jSetup';
import { getThemeExtension } from './themes';
const format = (view) => {
    try {
        const doc = view.state.doc.toString();
        const { formattedQuery, newCursorPos } = formatQuery(doc, { cursorPosition: view.state.selection.main.anchor });
        view.dispatch({
            changes: {
                from: 0,
                to: doc.length,
                insert: formattedQuery,
            },
            selection: { anchor: newCursorPos },
        });
    }
    catch (error) {
        // Formatting failed, likely because of a syntax error
    }
};
const executeKeybinding = (onExecute, newLineOnEnter, flush) => {
    const keybindings = {
        'Shift-Enter': {
            key: 'Shift-Enter',
            run: insertNewline,
        },
        'Ctrl-Enter': {
            key: 'Ctrl-Enter',
            run: () => true,
        },
        Enter: {
            key: 'Enter',
            run: insertNewline,
        },
    };
    if (onExecute) {
        keybindings['Ctrl-Enter'] = {
            key: 'Ctrl-Enter',
            mac: 'Mod-Enter',
            preventDefault: true,
            run: (view) => {
                const doc = view.state.doc.toString();
                if (doc.trim() !== '') {
                    flush?.();
                    onExecute(doc);
                }
                return true;
            },
        };
        if (!newLineOnEnter) {
            keybindings['Enter'] = {
                key: 'Enter',
                preventDefault: true,
                run: (view) => {
                    const doc = view.state.doc.toString();
                    if (doc.includes('\n')) {
                        // Returning false means the event will mark the event
                        // as not handled and the default behavior will be executed
                        return false;
                    }
                    if (doc.trim() !== '') {
                        flush?.();
                        onExecute(doc);
                    }
                    return true;
                },
            };
        }
    }
    return Object.values(keybindings);
};
const themeCompartment = new Compartment();
const keyBindingCompartment = new Compartment();
const lineNumbersCompartment = new Compartment();
const readOnlyCompartment = new Compartment();
const placeholderCompartment = new Compartment();
const domEventHandlerCompartment = new Compartment();
const formatLineNumber = (prompt) => (a, state) => {
    if (state.doc.lines === 1 && prompt !== undefined) {
        return prompt;
    }
    return a.toString();
};
const ExternalEdit = Annotation.define();
export class CypherEditor extends Component {
    /**
     * The codemirror editor container.
     */
    editorContainer = createRef();
    /**
     * The codemirror editor state.
     */
    editorState = createRef();
    /**
     * The codemirror editor view.
     */
    editorView = createRef();
    schemaRef = createRef();
    /**
     * Format Cypher query
     */
    format() {
        format(this.editorView.current);
    }
    /**
     * Focus the editor
     */
    focus() {
        this.editorView.current?.focus();
    }
    /**
     * Move the cursor to the supplied position.
     * For example, to move the cursor to the end of the editor, use `value.length`
     */
    updateCursorPosition(position) {
        this.editorView.current?.dispatch({
            selection: { anchor: position, head: position },
        });
    }
    /**
     * Externally set the editor value and focus the editor.
     */
    setValueAndFocus(value = '') {
        const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
        // Normalize line endings to LF that CM expects.
        // Prevents issues with inserted values that contain CRLF line endings.
        // https://codemirror.net/docs/ref/?utm_source=chatgpt.com#state.EditorState^lineSeparator
        const normalizedValue = value.replace(/\r\n/g, '\n');
        this.editorView.current.dispatch({
            changes: {
                from: 0,
                to: currentCmValue.length,
                insert: normalizedValue,
            },
            selection: { anchor: normalizedValue.length, head: normalizedValue.length },
        });
        this.editorView.current?.focus();
    }
    static defaultProps = {
        lint: true,
        schema: {},
        overrideThemeBackgroundColor: false,
        lineWrap: false,
        showSignatureTooltipBelow: true,
        extraKeybindings: [],
        history: [],
        theme: 'light',
        lineNumbers: true,
        newLineOnEnter: false,
        moveFocusOnTab: false,
    };
    debouncedOnChange = this.props.onChange
        ? debounce(((value, viewUpdate) => {
            this.props.onChange(value, viewUpdate);
        }), DEBOUNCE_TIME)
        : undefined;
    componentDidMount() {
        const { theme, extraKeybindings, lineWrap, overrideThemeBackgroundColor, schema, lint, showSignatureTooltipBelow, featureFlags, onExecute, newLineOnEnter, } = this.props;
        this.schemaRef.current = {
            schema,
            lint,
            showSignatureTooltipBelow,
            featureFlags: {
                consoleCommands: true,
                ...featureFlags,
            },
            useLightVersion: false,
            setUseLightVersion: (newVal) => {
                if (this.schemaRef.current !== undefined) {
                    this.schemaRef.current.useLightVersion = newVal;
                }
            },
        };
        const themeExtension = getThemeExtension(theme, overrideThemeBackgroundColor);
        const changeListener = this.debouncedOnChange
            ? [
                EditorView.updateListener.of((upt) => {
                    const wasUserEdit = !upt.transactions.some((tr) => tr.annotation(ExternalEdit));
                    if (upt.docChanged && wasUserEdit) {
                        const doc = upt.state.doc;
                        const value = doc.toString();
                        this.debouncedOnChange(value, upt);
                    }
                }),
            ]
            : [];
        this.editorState.current = EditorState.create({
            extensions: [
                keyBindingCompartment.of(keymap.of([
                    ...executeKeybinding(onExecute, newLineOnEnter, () => this.debouncedOnChange?.flush()),
                    ...extraKeybindings,
                ])),
                historyNavigation(this.props),
                basicNeo4jSetup(this.props),
                themeCompartment.of(themeExtension),
                changeListener,
                cypher(this.schemaRef.current),
                lineWrap ? EditorView.lineWrapping : [],
                lineNumbersCompartment.of(this.props.lineNumbers
                    ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
                    : []),
                readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
                placeholderCompartment.of(this.props.placeholder ? placeholder(this.props.placeholder) : []),
                domEventHandlerCompartment.of(this.props.domEventHandlers
                    ? EditorView.domEventHandlers(this.props.domEventHandlers)
                    : []),
                this.props.ariaLabel
                    ? EditorView.contentAttributes.of({
                        'aria-label': this.props.ariaLabel,
                    })
                    : [],
            ],
            doc: this.props.value,
        });
        this.editorView.current = new EditorView({
            state: this.editorState.current,
            parent: this.editorContainer.current,
        });
        if (this.props.autofocus) {
            this.focus();
            if (this.props.value) {
                this.updateCursorPosition(this.props.value.length);
            }
        }
        else if (this.props.offset) {
            this.updateCursorPosition(this.props.offset);
        }
    }
    componentDidUpdate(prevProps) {
        if (!this.editorView.current) {
            return;
        }
        // Handle externally set value
        const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
        if (this.props.value !== undefined && // If the component becomes uncontolled, we just leave the value as is
            this.props.value !== prevProps.value && // The value prop has changed, we need to update the editor
            this.props.value !== currentCmValue // No need to dispatch an update if the value is the same
        ) {
            this.editorView.current.dispatch({
                changes: {
                    from: 0,
                    to: currentCmValue.length,
                    insert: this.props.value ?? '',
                },
                annotations: [ExternalEdit.of(true)],
            });
        }
        // Handle theme change
        const didChangeTheme = prevProps.theme !== this.props.theme ||
            prevProps.overrideThemeBackgroundColor !==
                this.props.overrideThemeBackgroundColor;
        if (didChangeTheme) {
            this.editorView.current.dispatch({
                effects: themeCompartment.reconfigure(getThemeExtension(this.props.theme, this.props.overrideThemeBackgroundColor)),
            });
        }
        if (prevProps.lineNumbers !== this.props.lineNumbers ||
            prevProps.prompt !== this.props.prompt) {
            this.editorView.current.dispatch({
                effects: lineNumbersCompartment.reconfigure(this.props.lineNumbers
                    ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
                    : []),
            });
        }
        if (prevProps.readonly !== this.props.readonly) {
            this.editorView.current.dispatch({
                effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(this.props.readonly)),
            });
        }
        if (prevProps.placeholder !== this.props.placeholder) {
            this.editorView.current.dispatch({
                effects: placeholderCompartment.reconfigure(this.props.placeholder ? placeholder(this.props.placeholder) : []),
            });
        }
        if (prevProps.extraKeybindings !== this.props.extraKeybindings ||
            prevProps.onExecute !== this.props.onExecute) {
            this.editorView.current.dispatch({
                effects: keyBindingCompartment.reconfigure(keymap.of([
                    ...executeKeybinding(this.props.onExecute, this.props.newLineOnEnter, () => this.debouncedOnChange?.flush()),
                    ...this.props.extraKeybindings,
                ])),
            });
        }
        if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
            this.editorView.current.dispatch({
                effects: domEventHandlerCompartment.reconfigure(this.props.domEventHandlers
                    ? EditorView.domEventHandlers(this.props.domEventHandlers)
                    : []),
            });
        }
        // This component rerenders on every keystroke and comparing the
        // full lists of editor strings on every render could be expensive.
        const didChangeHistoryEstimate = prevProps.history?.length !== this.props.history?.length ||
            prevProps.history?.[0] !== this.props.history?.[0];
        if (didChangeHistoryEstimate) {
            this.editorView.current.dispatch({
                effects: replaceHistory.of(this.props.history ?? []),
            });
        }
        /*
        The cypher configuration is a mutable object that is passed to the cypher language extension.
        Much like how the schema based completions work in the official sql language extension.
        https://github.com/codemirror/lang-sql/blob/4b7b2564dff7cdb1a15f8ccd08142f2cc8a0006f/src/sql.ts#L178C17-L178C18
        */
        this.schemaRef.current.schema = this.props.schema;
        this.schemaRef.current.lint = this.props.lint;
        this.schemaRef.current.featureFlags = this.props.featureFlags;
    }
    componentWillUnmount() {
        this.editorView.current?.destroy();
        cleanupWorkers();
    }
    render() {
        const { className, theme } = this.props;
        const themeClass = typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
        return (_jsx("div", { ref: this.editorContainer, className: `${themeClass}${className ? ` ${className}` : ''}` }));
    }
}
//# sourceMappingURL=CypherEditor.js.map