/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { matchesSubString } from '../../../../base/common/filters.js';
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { disposableObservableValue, transaction } from '../../../../base/common/observableImpl/base.js';
import { Position } from '../../../common/core/position.js';
import { InlineCompletionTriggerKind } from '../../../common/languages.js';
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { SingleTextEdit } from './singleTextEdit.js';
import { provideInlineCompletions } from './provideInlineCompletions.js';
let InlineCompletionsSource = class InlineCompletionsSource extends Disposable {
    constructor(textModel, _debounceValue, languageFeaturesService, languageConfigurationService) {
        super();
        this.textModel = textModel;
        this._debounceValue = _debounceValue;
        this.languageFeaturesService = languageFeaturesService;
        this.languageConfigurationService = languageConfigurationService;
        this.updateOperation = this._register(new MutableDisposable());
        this.inlineCompletions = disposableObservableValue('inlineCompletions', undefined);
        this.suggestWidgetInlineCompletions = disposableObservableValue('suggestWidgetInlineCompletions', undefined);
        this._register(this.textModel.onDidChangeContent(() => {
            this.updateOperation.clear();
        }));
    }
    clear(tx) {
        this.updateOperation.clear();
        this.inlineCompletions.set(undefined, tx);
        this.suggestWidgetInlineCompletions.set(undefined, tx);
    }
    clearSuggestWidgetInlineCompletions() {
        var _a;
        if ((_a = this.updateOperation.value) === null || _a === void 0 ? void 0 : _a.request.context.selectedSuggestionInfo) {
            this.updateOperation.clear();
        }
        this.suggestWidgetInlineCompletions.set(undefined, undefined);
    }
    update(position, context, activeInlineCompletion) {
        var _a, _b;
        const request = new UpdateRequest(position, context, this.textModel.getVersionId());
        const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions : this.inlineCompletions;
        if ((_a = this.updateOperation.value) === null || _a === void 0 ? void 0 : _a.request.satisfies(request)) {
            return this.updateOperation.value.promise;
        }
        else if ((_b = target.get()) === null || _b === void 0 ? void 0 : _b.request.satisfies(request)) {
            return Promise.resolve(true);
        }
        const updateOngoing = !!this.updateOperation.value;
        this.updateOperation.clear();
        const source = new CancellationTokenSource();
        const promise = (() => __awaiter(this, void 0, void 0, function* () {
            const shouldDebounce = updateOngoing || context.triggerKind === InlineCompletionTriggerKind.Automatic;
            if (shouldDebounce) {
                // This debounces the operation
                yield wait(this._debounceValue.get(this.textModel));
            }
            if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
                return false;
            }
            const startTime = new Date();
            const updatedCompletions = yield provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, position, this.textModel, context, source.token, this.languageConfigurationService);
            if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
                return false;
            }
            const endTime = new Date();
            this._debounceValue.update(this.textModel, endTime.getTime() - startTime.getTime());
            const completions = new UpToDateInlineCompletions(updatedCompletions, request, this.textModel);
            if (activeInlineCompletion && activeInlineCompletion.updatedRange.containsPosition(position)
                && activeInlineCompletion.isVisible(this.textModel, position)
                && !activeInlineCompletion.isSmallerThanOriginal()
                && !updatedCompletions.has(activeInlineCompletion.toInlineCompletion())) {
                completions.prepend(activeInlineCompletion.inlineCompletion, activeInlineCompletion.updatedRange, true);
            }
            transaction(tx => {
                target.set(completions, tx);
            });
            this.updateOperation.clear();
            return true;
        }))();
        const updateOperation = new UpdateOperation(request, source, promise);
        this.updateOperation.value = updateOperation;
        return promise;
    }
};
InlineCompletionsSource = __decorate([
    __param(2, ILanguageFeaturesService),
    __param(3, ILanguageConfigurationService)
], InlineCompletionsSource);
export { InlineCompletionsSource };
function wait(ms, cancellationToken) {
    return new Promise(resolve => {
        let d = undefined;
        const handle = setTimeout(() => {
            if (d) {
                d.dispose();
            }
            resolve();
        }, ms);
        if (cancellationToken) {
            d = cancellationToken.onCancellationRequested(() => {
                clearTimeout(handle);
                if (d) {
                    d.dispose();
                }
                resolve();
            });
        }
    });
}
class UpdateRequest {
    constructor(position, context, versionId) {
        this.position = position;
        this.context = context;
        this.versionId = versionId;
    }
    satisfies(other) {
        return this.position.equals(other.position)
            && equals(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, (v1, v2) => v1.equals(v2))
            && (other.context.triggerKind === InlineCompletionTriggerKind.Automatic
                || this.context.triggerKind === InlineCompletionTriggerKind.Explicit)
            && this.versionId === other.versionId;
    }
}
function equals(v1, v2, equals) {
    if (!v1 || !v2) {
        return v1 === v2;
    }
    return equals(v1, v2);
}
class UpdateOperation {
    constructor(request, cancellationTokenSource, promise) {
        this.request = request;
        this.cancellationTokenSource = cancellationTokenSource;
        this.promise = promise;
    }
    dispose() {
        this.cancellationTokenSource.cancel();
    }
}
export class UpToDateInlineCompletions {
    constructor(inlineCompletionProviderResult, request, textModel) {
        this.inlineCompletionProviderResult = inlineCompletionProviderResult;
        this.request = request;
        this.textModel = textModel;
        this.lastVersionId = -1;
        this.refCount = 1;
        this.prependedInlineCompletionItems = [];
        const ids = textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({
            range: i.range,
            options: {
                description: 'inline-completion-tracking-range'
            },
        })));
        this.inlineCompletions = inlineCompletionProviderResult.completions.map((i, index) => new InlineCompletionWithUpdatedRange(i, ids[index]));
    }
    prepend(inlineCompletion, range, addRefToSource) {
        if (addRefToSource) {
            inlineCompletion.source.addRef();
        }
        const id = this.textModel.deltaDecorations([], [{
                range,
                options: {
                    description: 'inline-completion-tracking-range'
                },
            }])[0];
        this.inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, range));
        this.prependedInlineCompletionItems.push(inlineCompletion);
    }
    clone() {
        this.refCount++;
        return this;
    }
    dispose() {
        this.refCount--;
        if (this.refCount === 0) {
            this.textModel.deltaDecorations(this.inlineCompletions.map(i => i.decorationId), []);
            this.inlineCompletionProviderResult.dispose();
            for (const i of this.prependedInlineCompletionItems) {
                i.source.removeRef();
            }
        }
    }
    /**
     * The ranges of the inline completions are extended as the user typed.
     */
    getInlineCompletions(versionId) {
        if (versionId !== this.textModel.getVersionId()) {
            throw new BugIndicatingError();
        }
        if (this.textModel.getVersionId() !== this.lastVersionId) {
            this.inlineCompletions.forEach(i => i.updateRange(this.textModel));
            this.lastVersionId = this.textModel.getVersionId();
        }
        return this.inlineCompletions;
    }
}
export class InlineCompletionWithUpdatedRange {
    get updatedRange() { return this._updatedRange; }
    constructor(inlineCompletion, decorationId, initialRange) {
        this.inlineCompletion = inlineCompletion;
        this.decorationId = decorationId;
        this.semanticId = JSON.stringify([this.inlineCompletion.filterText, this.inlineCompletion.insertText, this.inlineCompletion.range.getStartPosition().toString()]);
        this._updatedRange = initialRange !== null && initialRange !== void 0 ? initialRange : inlineCompletion.range;
    }
    updateRange(textModel) {
        const range = textModel.getDecorationRange(this.decorationId);
        if (!range) {
            throw new BugIndicatingError();
        }
        this._updatedRange = range;
    }
    toInlineCompletion() {
        return this.inlineCompletion.withRange(this.updatedRange);
    }
    toSingleTextEdit() {
        return new SingleTextEdit(this.updatedRange, this.inlineCompletion.insertText);
    }
    toFilterTextReplacement() {
        return new SingleTextEdit(this.updatedRange, this.inlineCompletion.filterText);
    }
    isVisible(model, cursorPosition) {
        const minimizedReplacement = this.toFilterTextReplacement().removeCommonPrefix(model);
        if (!this.inlineCompletion.range.getStartPosition().equals(this.updatedRange.getStartPosition())) {
            return false;
        }
        if (cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber) {
            return false;
        }
        const originalValue = model.getValueInRange(minimizedReplacement.range, 1 /* EndOfLinePreference.LF */).toLowerCase();
        const filterText = minimizedReplacement.text.toLowerCase();
        const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn);
        let filterTextBefore = filterText.substring(0, cursorPosIndex);
        let filterTextAfter = filterText.substring(cursorPosIndex);
        let originalValueBefore = originalValue.substring(0, cursorPosIndex);
        let originalValueAfter = originalValue.substring(cursorPosIndex);
        const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber);
        if (minimizedReplacement.range.startColumn <= originalValueIndent) {
            // Remove indentation
            originalValueBefore = originalValueBefore.trimStart();
            if (originalValueBefore.length === 0) {
                originalValueAfter = originalValueAfter.trimStart();
            }
            filterTextBefore = filterTextBefore.trimStart();
            if (filterTextBefore.length === 0) {
                filterTextAfter = filterTextAfter.trimStart();
            }
        }
        return filterTextBefore.startsWith(originalValueBefore)
            && !!matchesSubString(originalValueAfter, filterTextAfter);
    }
    isSmallerThanOriginal() {
        return length(this.updatedRange).isBefore(length(this.inlineCompletion.range));
    }
}
function length(range) {
    if (range.startLineNumber === range.endLineNumber) {
        return new Position(1, 1 + range.endColumn - range.startColumn);
    }
    else {
        return new Position(1 + range.endLineNumber - range.startLineNumber, range.endColumn);
    }
}
