import * as monaco from 'monaco-editor';
import { DisposableStore } from '@codingame/monaco-vscode-api/monaco';
import { getRangesFromDecorations, excludeRanges } from './tools/utils/rangeUtils.js';
import { tryIgnoreLockedCode, LockedCodeError } from './tools/utils/editorOperationUtils.js';

/**
 * Exctract ranges between startToken and endToken
 */
function extractRangesFromTokens(editor, startToken, endToken, isRegex = false) {
    const editorModel = editor.getModel();
    const ranges = [];
    if (editorModel != null) {
        let currentPosition = editorModel.getFullModelRange().getStartPosition();
        let match;
        while ((match = editorModel.findNextMatch(startToken, 
        /* searchStart */ currentPosition, 
        /* isRegex */ isRegex, 
        /* matchCase */ true, 
        /* wordSeparators */ null, 
        /* captureMatches */ false)) != null) {
            if (match.range.getStartPosition().isBefore(currentPosition)) {
                break;
            }
            const matchEnd = editorModel.findNextMatch(endToken, 
            /* searchStart */ match.range.getEndPosition(), 
            /* isRegex */ isRegex, 
            /* matchCase */ true, 
            /* wordSeparators */ null, 
            /* captureMatches */ false);
            if (matchEnd != null &&
                matchEnd.range.getStartPosition().isBefore(match.range.getStartPosition())) {
                break;
            }
            currentPosition =
                matchEnd?.range.getEndPosition() ?? editorModel.getFullModelRange().getEndPosition();
            ranges.push(monaco.Range.fromPositions(match.range.getStartPosition(), currentPosition));
        }
        return ranges;
    }
    return [];
}
function generateErrorMessageErrorHandler(errorMessage) {
    return (editor, operation) => {
        const messageContribution = editor.getContribution('editor.contrib.messageController');
        messageContribution.showMessage(errorMessage, operation.range.getStartPosition());
    };
}
function lockCodeRanges(editor, { onError, allowChangeFromSources = [], getLockedRanges = () => [], transactionMode = true, allowUndoRedo = true }) {
    const disposableStore = new DisposableStore();
    function canEditRange(range) {
        const model = editor.getModel();
        if (model == null) {
            return false;
        }
        const ranges = getLockedRanges();
        if (ranges.length === 0) {
            return true;
        }
        return ranges.every((uneditableRange) => !monaco.Range.areIntersectingOrTouching(uneditableRange, range));
    }
    let currentEditSource;
    const originalExecuteEdit = editor.executeEdits;
    editor.executeEdits = (source, edits, endCursorState) => {
        currentEditSource = source;
        try {
            return originalExecuteEdit.call(editor, source, edits, endCursorState);
        }
        finally {
            currentEditSource = null;
        }
    };
    let restoreModel;
    function lockModel() {
        restoreModel?.();
        const model = editor.getModel();
        if (model == null) {
            return;
        }
        const original = model._validateEditOperations;
        model._validateEditOperations = function (rawOperations) {
            let editorOperations = original.call(this, rawOperations);
            if (currentEditSource != null && allowChangeFromSources.includes(currentEditSource)) {
                return editorOperations;
            }
            if (allowUndoRedo && (this._isUndoing || this._isRedoing)) {
                return editorOperations;
            }
            try {
                editorOperations = tryIgnoreLockedCode(model, getLockedRanges(), editorOperations);
            }
            catch (e) {
                if (e instanceof LockedCodeError) {
                    console.info(e);
                }
                else {
                    console.error(e);
                }
            }
            if (transactionMode) {
                const firstForbiddenOperation = editorOperations.find((operation) => !canEditRange(operation.range));
                if (firstForbiddenOperation != null) {
                    onError?.(editor, firstForbiddenOperation);
                    return [];
                }
                else {
                    return editorOperations;
                }
            }
            else {
                return editorOperations.filter((operation) => {
                    if (!canEditRange(operation.range)) {
                        onError?.(editor, operation);
                        return false;
                    }
                    return true;
                });
            }
        };
        restoreModel = () => {
            model._validateEditOperations = original;
        };
    }
    disposableStore.add(editor.onDidChangeModel(lockModel));
    lockModel();
    disposableStore.add(editor.onDidDispose(() => restoreModel?.()));
    // Handle selection of the last line of an editable range
    disposableStore.add(editor.onDidChangeCursorSelection((e) => {
        if (canEditRange(e.selection) || e.selection.isEmpty()) {
            return;
        }
        const model = editor.getModel();
        if (model == null) {
            return;
        }
        const shiftedRange = monaco.Range.fromPositions(model.getPositionAt(model.getOffsetAt(e.selection.getStartPosition()) - 1), model.getPositionAt(model.getOffsetAt(e.selection.getEndPosition()) - 1));
        if (canEditRange(shiftedRange)) {
            editor.setSelection(shiftedRange);
        }
    }));
    disposableStore.add({
        dispose() {
            restoreModel?.();
            editor.executeEdits = originalExecuteEdit;
        }
    });
    return disposableStore;
}
function lockCodeWithDecoration(editor, lockOptions) {
    return lockCodeRanges(editor, {
        onError: lockOptions.errorMessage != null
            ? generateErrorMessageErrorHandler(lockOptions.errorMessage)
            : undefined,
        allowChangeFromSources: lockOptions.allowChangeFromSources,
        getLockedRanges() {
            const model = editor.getModel();
            if (model == null) {
                return [];
            }
            return getRangesFromDecorations(model, lockOptions.decorationFilter);
        },
        transactionMode: lockOptions.transactionMode,
        allowUndoRedo: lockOptions.allowUndoRedo
    });
}
function lockCodeWithoutDecoration(editor, lockOptions) {
    return lockCodeRanges(editor, {
        onError: lockOptions.errorMessage != null
            ? generateErrorMessageErrorHandler(lockOptions.errorMessage)
            : undefined,
        allowChangeFromSources: lockOptions.allowChangeFromSources,
        getLockedRanges() {
            const model = editor.getModel();
            if (model == null) {
                return [];
            }
            return excludeRanges(model, model.getFullModelRange(), getRangesFromDecorations(model, lockOptions.decorationFilter)).filteredRanges;
        },
        transactionMode: lockOptions.transactionMode,
        allowUndoRedo: lockOptions.allowUndoRedo
    });
}
let hideCodeWithoutDecorationCounter = 0;
function hideCodeWithoutDecoration(editor, decorationFilter) {
    const hideId = `hideCodeWithoutDecoration:${hideCodeWithoutDecorationCounter++}`;
    function updateHiddenAreas() {
        const model = editor.getModel();
        if (model == null) {
            return;
        }
        const decorations = model.getAllDecorations().filter(decorationFilter);
        if (decorations.length === 0) {
            return;
        }
        const ranges = decorations
            .map((decoration) => decoration.range)
            .sort((a, b) => a.startLineNumber - b.startLineNumber)
            // merge ranges
            .reduce((acc, range) => {
            if (acc.length === 0) {
                return [range];
            }
            const lastRange = acc[acc.length - 1];
            if (range.getStartPosition().isBeforeOrEqual(lastRange.getEndPosition())) {
                return [
                    ...acc.slice(0, -1),
                    monaco.Range.fromPositions(lastRange.getStartPosition(), range.getEndPosition())
                ];
            }
            else {
                return [...acc, range];
            }
        }, []);
        const { filteredRanges: hiddenAreas, removedRanges: visibleRanges } = excludeRanges(model, model.getFullModelRange(), ranges);
        editor.setHiddenAreas(hiddenAreas, hideId);
        // Make sure only visible code is selected
        const selections = editor.getSelections();
        if (selections != null) {
            let newSelections = selections.flatMap((selection) => visibleRanges
                .map((visibleRange) => selection.intersectRanges(visibleRange))
                .filter((range) => range != null)
                .map((range) => monaco.Selection.fromRange(range, selection.getDirection())));
            if (newSelections.length === 0 && visibleRanges.length > 0) {
                newSelections = [monaco.Selection.fromPositions(visibleRanges[0].getStartPosition())];
            }
            if (newSelections.length > 0) {
                editor.setSelections(newSelections);
            }
        }
    }
    const disposableStore = new DisposableStore();
    disposableStore.add(editor.onDidChangeModel(() => {
        updateHiddenAreas();
    }));
    disposableStore.add(editor.onDidChangeModelDecorations(() => {
        updateHiddenAreas();
    }));
    updateHiddenAreas();
    disposableStore.add({
        dispose() {
            editor.setHiddenAreas([], hideId);
        }
    });
    return disposableStore;
}
/**
 * Collapse everything from ranges
 */
async function collapseCodeSectionsFromRanges(editor, ranges) {
    if (ranges.length > 0) {
        const selections = editor.getSelections();
        editor.setSelections(ranges.map((r) => ({
            selectionStartLineNumber: r.startLineNumber,
            selectionStartColumn: r.startColumn,
            positionLineNumber: r.endLineNumber,
            positionColumn: r.endColumn
        })));
        await editor.getAction('editor.createFoldingRangeFromSelection').run();
        if (selections != null) {
            editor.setSelections(selections);
        }
    }
}
/**
 * Collapse everything between startToken and endToken
 */
async function collapseCodeSections(editor, startToken, endToken, isRegex = false) {
    const ranges = extractRangesFromTokens(editor, startToken, endToken, isRegex);
    await collapseCodeSectionsFromRanges(editor, ranges);
}
function registerTextDecorationProvider(provider) {
    const disposableStore = new DisposableStore();
    const watchEditor = (editor) => {
        const disposableStore = new DisposableStore();
        const decorationCollection = editor.createDecorationsCollection();
        const checkEditor = () => {
            const model = editor.getModel();
            if (model != null) {
                decorationCollection.set(provider.provideDecorations(model));
            }
            else {
                decorationCollection.clear();
            }
        };
        disposableStore.add(editor.onDidChangeModel(checkEditor));
        disposableStore.add(editor.onDidChangeModelContent(checkEditor));
        disposableStore.add({
            dispose() {
                decorationCollection.clear();
            }
        });
        checkEditor();
        return disposableStore;
    };
    monaco.editor.getEditors().forEach((editor) => disposableStore.add(watchEditor(editor)));
    disposableStore.add(monaco.editor.onDidCreateEditor((editor) => disposableStore.add(watchEditor(editor))));
    return disposableStore;
}
function runOnAllEditors(cb) {
    const disposableStore = new DisposableStore();
    const handleEditor = (editor) => {
        const disposable = cb(editor);
        disposableStore.add(disposable);
        const disposeEventDisposable = editor.onDidDispose(() => {
            disposableStore.delete(disposable);
            disposableStore.delete(disposeEventDisposable);
        });
        disposableStore.add(disposeEventDisposable);
    };
    monaco.editor.getEditors().forEach(handleEditor);
    disposableStore.add(monaco.editor.onDidCreateEditor(handleEditor));
    return disposableStore;
}
function preventAlwaysConsumeTouchEvent(editor) {
    let firstX = 0;
    let firstY = 0;
    let atTop = false;
    let atBottom = false;
    let atLeft = false;
    let atRight = false;
    let useBrowserBehavior = null;
    editor.onDidChangeModel(() => {
        const domNode = editor.getDomNode();
        if (domNode == null) {
            return;
        }
        domNode.addEventListener('touchstart', (e) => {
            const firstTouch = e.targetTouches.item(0);
            if (firstTouch == null) {
                return;
            }
            // Prevent monaco-editor from trying to call preventDefault on the touchstart event
            // so we'll be able to use the default behavior of the touchmove event
            e.preventDefault = () => { };
            firstX = firstTouch.clientX;
            firstY = firstTouch.clientY;
            const layoutInfo = editor.getLayoutInfo();
            atTop = editor.getScrollTop() <= 0;
            atBottom = editor.getScrollTop() >= editor.getContentHeight() - layoutInfo.height;
            atLeft = editor.getScrollLeft() <= 0;
            atRight = editor.getScrollLeft() >= editor.getContentWidth() - layoutInfo.width;
            useBrowserBehavior = null;
        });
        domNode.addEventListener('touchmove', (e) => {
            const firstTouch = e.changedTouches.item(0);
            if (firstTouch == null) {
                return;
            }
            if (useBrowserBehavior == null) {
                const dx = firstTouch.clientX - firstX;
                const dy = firstTouch.clientY - firstY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    // It's an horizontal scroll
                    useBrowserBehavior = (dx < 0 && atRight) || (dx > 0 && atLeft);
                }
                else {
                    // It's a vertical scroll
                    useBrowserBehavior = (dy < 0 && atBottom) || (dy > 0 && atTop);
                }
            }
            if (useBrowserBehavior) {
                // Stop the event before monaco tries to preventDefault on it
                e.stopPropagation();
            }
        });
        domNode.addEventListener('touchend', (e) => {
            if (useBrowserBehavior ?? false) {
                // Prevent monaco from trying to open its context menu
                // It thinks it's a long press because it didn't receive the move events
                e.stopPropagation();
            }
        });
    });
}
function mapClipboard(editor, { toClipboard, fromClipboard }) {
    const disposableStore = new DisposableStore();
    let copiedText = '';
    const originalTrigger = editor.trigger;
    editor.trigger = function (source, handlerId, payload) {
        if (handlerId === 'editor.action.clipboardCopyAction') {
            copiedText = editor.getModel().getValueInRange(editor.getSelection());
        }
        else if (handlerId === 'editor.action.clipboardCutAction') {
            copiedText = editor.getModel().getValueInRange(editor.getSelection());
        }
        else if (((handlerId, payload) => handlerId === 'paste')(handlerId)) {
            const newText = fromClipboard(payload.text);
            if (newText !== payload.text) {
                payload = {
                    ...payload,
                    text: newText
                };
            }
        }
        originalTrigger.call(this, source, handlerId, payload);
    };
    disposableStore.add({
        dispose() {
            editor.trigger = originalTrigger;
        }
    });
    function mapCopy(event) {
        const clipdata = event.clipboardData ?? window.clipboardData;
        let content = clipdata.getData('Text');
        if (content.length === 0) {
            content = copiedText;
        }
        const transformed = toClipboard(content);
        if (transformed !== content) {
            if (clipdata.types != null) {
                clipdata.types.forEach((type) => clipdata.setData(type, toClipboard(content)));
            }
            else {
                clipdata.setData('text/plain', toClipboard(content));
            }
        }
    }
    const editorDomNode = editor.getContainerDomNode();
    editorDomNode.addEventListener('copy', mapCopy);
    editorDomNode.addEventListener('cut', mapCopy);
    disposableStore.add({
        dispose() {
            editorDomNode.removeEventListener('copy', mapCopy);
            editorDomNode.removeEventListener('cut', mapCopy);
        }
    });
    return disposableStore;
}

export { collapseCodeSections, collapseCodeSectionsFromRanges, extractRangesFromTokens, hideCodeWithoutDecoration, lockCodeRanges, lockCodeWithDecoration, lockCodeWithoutDecoration, mapClipboard, preventAlwaysConsumeTouchEvent, registerTextDecorationProvider, runOnAllEditors };
