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 { createInlinePanelController, } from './inlinePanel';
import { createDiffExtension } from './diffView';
import { createEditorActionsController, } from './editorActions';
import { formatQuery, CypherLanguageService, } from '@neo4j-cypher/language-support';
import debounce from 'lodash.debounce';
import { Component, createRef } from 'react';
import { createPortal } from 'react-dom';
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';
import { richClipboardCopier } from './richClipboardCopier';
import workerpool from 'workerpool';
/**
 * Normalize CRLF line endings to LF, which is what CodeMirror expects.
 * CodeMirror collapses `\r\n` into a single line break, so a raw `value`
 * string with CRLF endings is longer than the resulting document. Normalizing
 * up front keeps the document length in sync with the value length used for
 * cursor placement, and avoids "Selection points outside of document" errors.
 * https://codemirror.net/docs/ref/#state.EditorState^lineSeparator
 */
function normalizeLineEndings(value) {
    return value.replace(/\r\n/g, '\n');
}
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 {
        // 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 diffCompartment = new Compartment();
const formatLineNumber = (prompt) => (a, state) => {
    if (state.doc.lines === 1 && prompt !== undefined) {
        return prompt;
    }
    return a.toString();
};
const ExternalEdit = Annotation.define();
const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
    .pathname;
class CodemirrorSymbolFetcher {
    constructor(languageService) {
        this.languageService = languageService;
    }
    languageService;
    processing = false;
    nextJob;
    symbolTablePool = workerpool.pool(WorkerURL, {
        minWorkers: 1,
        workerOpts: { type: 'module' },
        workerTerminateTimeout: 2000,
    });
    queueSymbolJob(query, schema) {
        this.nextJob = { query, schema };
        if (!this.processing) {
            void this.processJobQueue();
        }
    }
    terminate() {
        this.nextJob = undefined;
        void this.symbolTablePool.terminate();
    }
    async processJobQueue() {
        this.processing = true;
        while (this.nextJob) {
            try {
                const query = this.nextJob.query;
                const dbSchema = this.nextJob.schema;
                this.nextJob = undefined;
                const proxyWorker = (await this.symbolTablePool.proxy());
                const result = await proxyWorker.lintCypherQuery(query, dbSchema);
                if (result.symbolTables) {
                    this.languageService.setSymbolsInfo({
                        query,
                        symbolTables: result.symbolTables,
                    });
                }
            }
            catch (err) {
                //eslint-disable-next-line
                console.log('Symbol table calculation failed ' + String(err));
            }
        }
        this.processing = false;
    }
}
export class CypherEditor extends Component {
    /**
     * The symbol fetcher object used to fetch the current symbol table on document changes
     */
    symbolFetcher;
    /**
     * The codemirror editor container.
     */
    editorContainer = createRef();
    /**
     * The codemirror editor state.
     */
    editorState = createRef();
    /**
     * The codemirror editor view.
     */
    editorView = createRef();
    schemaRef = createRef();
    inlinePanelController = null;
    editorActionsController = null;
    state = {
        editorActionsContainer: null,
    };
    editorActionsCallbacks = {
        onMount: (container) => this.setState({ editorActionsContainer: container }),
        onUnmount: () => this.setState({ editorActionsContainer: null }),
    };
    /**
     * 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) {
        const view = this.editorView.current;
        if (!view) {
            return;
        }
        // Clamp to the doc length; `position` may come from a raw value longer than
        // the doc (e.g. CRLF endings), which would throw "Selection points outside
        // of document".
        const clamped = Math.max(0, Math.min(position, view.state.doc.length));
        view.dispatch({
            selection: { anchor: clamped, head: clamped },
        });
    }
    /**
     * Externally set the editor value and focus the editor.
     */
    setValueAndFocus(value = '') {
        const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
        const normalizedValue = normalizeLineEndings(value);
        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 = {
            languageService: new CypherLanguageService(),
            schema,
            lint,
            showSignatureTooltipBelow,
            featureFlags: {
                consoleCommands: true,
                ...featureFlags,
            },
            useLightVersion: false,
            setUseLightVersion: (newVal) => {
                if (this.schemaRef.current !== undefined) {
                    this.schemaRef.current.useLightVersion = newVal;
                }
            },
        };
        this.symbolFetcher = new CodemirrorSymbolFetcher(this.schemaRef.current.languageService);
        const themeExtension = getThemeExtension(theme, overrideThemeBackgroundColor);
        this.inlinePanelController = createInlinePanelController();
        this.editorActionsController = createEditorActionsController();
        const changeListener = this.debouncedOnChange
            ? [
                EditorView.updateListener.of((upt) => {
                    if (upt.docChanged) {
                        this.symbolFetcher.queueSymbolJob(upt.state.doc.toString(), this.schemaRef.current.schema);
                    }
                    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,
                ])),
                richClipboardCopier,
                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,
                    })
                    : [],
                !this.props.moveFocusOnTab
                    ? EditorView.contentAttributes.of({
                        'aria-description': 'Press Escape to leave the editor and continue tabbing through the page',
                    })
                    : [],
                this.inlinePanelController.extension,
                diffCompartment.of(this.props.diff ? createDiffExtension(this.props.diff) : []),
                this.editorActionsController.extension,
            ],
            doc: normalizeLineEndings(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);
        }
        if (this.props.inlinePanel) {
            this.openInlinePanel(this.props.inlinePanel);
        }
        if (this.props.editorActions != null) {
            this.setEditorActions(true);
        }
    }
    setEditorActions(active) {
        const view = this.editorView.current;
        const controller = this.editorActionsController;
        if (!view || !controller) {
            return;
        }
        view.dispatch({
            effects: controller.set(active ? this.editorActionsCallbacks : null),
        });
    }
    openInlinePanel(props) {
        const view = this.editorView.current;
        const controller = this.inlinePanelController;
        if (!view || !controller) {
            return;
        }
        const pos = Math.max(0, Math.min(props.pos, view.state.doc.length));
        const line = view.state.doc.lineAt(pos);
        controller.updateCallbacks({
            onMount: props.onMount,
            onUnmount: props.onUnmount,
        });
        view.dispatch({
            effects: controller.show({
                pos: props.placement === 'below' ? line.to : line.from,
                placement: props.placement,
                onMount: props.onMount,
                onUnmount: props.onUnmount,
            }),
        });
    }
    updateInlinePanel(props) {
        this.inlinePanelController?.updateCallbacks({
            onMount: props.onMount,
            onUnmount: props.onUnmount,
        });
    }
    closeInlinePanel() {
        const view = this.editorView.current;
        const controller = this.inlinePanelController;
        if (!view || !controller) {
            return;
        }
        view.dispatch({ effects: controller.hide() });
    }
    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: normalizeLineEndings(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,
                ])),
            });
        }
        const prevPanel = prevProps.inlinePanel;
        const nextPanel = this.props.inlinePanel;
        if (prevPanel !== nextPanel) {
            if (!nextPanel) {
                this.closeInlinePanel();
            }
            else if (!prevPanel ||
                prevPanel.pos !== nextPanel.pos ||
                prevPanel.placement !== nextPanel.placement) {
                this.openInlinePanel(nextPanel);
            }
            else {
                this.updateInlinePanel(nextPanel);
            }
        }
        if (prevProps.diff?.original !== this.props.diff?.original) {
            this.editorView.current.dispatch({
                effects: diffCompartment.reconfigure(this.props.diff ? createDiffExtension(this.props.diff) : []),
            });
        }
        const hadActions = prevProps.editorActions != null;
        const hasActions = this.props.editorActions != null;
        if (hadActions !== hasActions) {
            this.setEditorActions(hasActions);
        }
        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();
        this.symbolFetcher?.terminate();
        cleanupWorkers();
    }
    render() {
        const { className, theme } = this.props;
        const themeClass = typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
        const { editorActionsContainer } = this.state;
        return (_jsx("div", { ref: this.editorContainer, className: `${themeClass}${className ? ` ${className}` : ''}`, children: editorActionsContainer &&
                createPortal(this.props.editorActions, editorActionsContainer) }));
    }
}
//# sourceMappingURL=CypherEditor.js.map