/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
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 { assertNever } from '../../../../base/common/assert.js';
import { DeferredPromise } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { SetMap } from '../../../../base/common/map.js';
import { onUnexpectedExternalError } from '../../../../base/common/errors.js';
import { Range } from '../../../common/core/range.js';
import { fixBracketsInLine } from '../../../common/model/bracketPairsTextModelPart/fixBrackets.js';
import { getReadonlyEmptyArray } from './utils.js';
import { SnippetParser, Text } from '../../snippet/browser/snippetParser.js';
export function provideInlineCompletions(registry, position, model, context, token = CancellationToken.None, languageConfigurationService) {
    return __awaiter(this, void 0, void 0, function* () {
        // Important: Don't use position after the await calls, as the model could have been changed in the meantime!
        const defaultReplaceRange = getDefaultRange(position, model);
        const providers = registry.all(model);
        const multiMap = new SetMap();
        for (const provider of providers) {
            if (provider.groupId) {
                multiMap.add(provider.groupId, provider);
            }
        }
        function getPreferredProviders(provider) {
            if (!provider.yieldsToGroupIds) {
                return [];
            }
            const result = [];
            for (const groupId of provider.yieldsToGroupIds || []) {
                const providers = multiMap.get(groupId);
                for (const p of providers) {
                    result.push(p);
                }
            }
            return result;
        }
        const states = new Map();
        const seen = new Set();
        function findPreferredProviderCircle(provider, stack) {
            stack = [...stack, provider];
            if (seen.has(provider)) {
                return stack;
            }
            seen.add(provider);
            try {
                const preferred = getPreferredProviders(provider);
                for (const p of preferred) {
                    const c = findPreferredProviderCircle(p, stack);
                    if (c) {
                        return c;
                    }
                }
            }
            finally {
                seen.delete(provider);
            }
            return undefined;
        }
        function processProvider(provider) {
            const state = states.get(provider);
            if (state) {
                return state;
            }
            const circle = findPreferredProviderCircle(provider, []);
            if (circle) {
                onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected. Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`));
            }
            const deferredPromise = new DeferredPromise();
            states.set(provider, deferredPromise.p);
            (() => __awaiter(this, void 0, void 0, function* () {
                if (!circle) {
                    const preferred = getPreferredProviders(provider);
                    for (const p of preferred) {
                        const result = yield processProvider(p);
                        if (result && result.items.length > 0) {
                            // Skip provider
                            return undefined;
                        }
                    }
                }
                try {
                    const completions = yield provider.provideInlineCompletions(model, position, context, token);
                    return completions;
                }
                catch (e) {
                    onUnexpectedExternalError(e);
                    return undefined;
                }
            }))().then(c => deferredPromise.complete(c), e => deferredPromise.error(e));
            return deferredPromise.p;
        }
        const providerResults = yield Promise.all(providers.map((provider) => __awaiter(this, void 0, void 0, function* () { return ({ provider, completions: yield processProvider(provider) }); })));
        const itemsByHash = new Map();
        const lists = [];
        for (const result of providerResults) {
            const completions = result.completions;
            if (!completions) {
                continue;
            }
            const list = new InlineCompletionList(completions, result.provider);
            lists.push(list);
            for (const item of completions.items) {
                const inlineCompletionItem = InlineCompletionItem.from(item, list, defaultReplaceRange, model, languageConfigurationService);
                itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem);
            }
        }
        return new InlineCompletionProviderResult(Array.from(itemsByHash.values()), new Set(itemsByHash.keys()), lists);
    });
}
export class InlineCompletionProviderResult {
    constructor(
    /**
     * Free of duplicates.
     */
    completions, hashs, providerResults) {
        this.completions = completions;
        this.hashs = hashs;
        this.providerResults = providerResults;
    }
    has(item) {
        return this.hashs.has(item.hash());
    }
    dispose() {
        for (const result of this.providerResults) {
            result.removeRef();
        }
    }
}
/**
 * A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that
 * computed them.
 */
export class InlineCompletionList {
    constructor(inlineCompletions, provider) {
        this.inlineCompletions = inlineCompletions;
        this.provider = provider;
        this.refCount = 1;
    }
    addRef() {
        this.refCount++;
    }
    removeRef() {
        this.refCount--;
        if (this.refCount === 0) {
            this.provider.freeInlineCompletions(this.inlineCompletions);
        }
    }
}
export class InlineCompletionItem {
    static from(inlineCompletion, source, defaultReplaceRange, textModel, languageConfigurationService) {
        let insertText;
        let snippetInfo;
        let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange;
        if (typeof inlineCompletion.insertText === 'string') {
            insertText = inlineCompletion.insertText;
            if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
                insertText = closeBrackets(insertText, range.getStartPosition(), textModel, languageConfigurationService);
                // Modify range depending on if brackets are added or removed
                const diff = insertText.length - inlineCompletion.insertText.length;
                if (diff !== 0) {
                    range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
                }
            }
            snippetInfo = undefined;
        }
        else if ('snippet' in inlineCompletion.insertText) {
            const preBracketCompletionLength = inlineCompletion.insertText.snippet.length;
            if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
                inlineCompletion.insertText.snippet = closeBrackets(inlineCompletion.insertText.snippet, range.getStartPosition(), textModel, languageConfigurationService);
                // Modify range depending on if brackets are added or removed
                const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength;
                if (diff !== 0) {
                    range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
                }
            }
            const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet);
            if (snippet.children.length === 1 && snippet.children[0] instanceof Text) {
                insertText = snippet.children[0].value;
                snippetInfo = undefined;
            }
            else {
                insertText = snippet.toString();
                snippetInfo = {
                    snippet: inlineCompletion.insertText.snippet,
                    range: range
                };
            }
        }
        else {
            assertNever(inlineCompletion.insertText);
        }
        return new InlineCompletionItem(insertText, inlineCompletion.command, range, insertText, snippetInfo, inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), inlineCompletion, source);
    }
    constructor(filterText, command, range, insertText, snippetInfo, additionalTextEdits, 
    /**
     * A reference to the original inline completion this inline completion has been constructed from.
     * Used for event data to ensure referential equality.
    */
    sourceInlineCompletion, 
    /**
     * A reference to the original inline completion list this inline completion has been constructed from.
     * Used for event data to ensure referential equality.
    */
    source) {
        this.filterText = filterText;
        this.command = command;
        this.range = range;
        this.insertText = insertText;
        this.snippetInfo = snippetInfo;
        this.additionalTextEdits = additionalTextEdits;
        this.sourceInlineCompletion = sourceInlineCompletion;
        this.source = source;
        filterText = filterText.replace(/\r\n|\r/g, '\n');
        insertText = filterText.replace(/\r\n|\r/g, '\n');
    }
    withRange(updatedRange) {
        return new InlineCompletionItem(this.filterText, this.command, updatedRange, this.insertText, this.snippetInfo, this.additionalTextEdits, this.sourceInlineCompletion, this.source);
    }
    hash() {
        return JSON.stringify({ insertText: this.insertText, range: this.range.toString() });
    }
}
function getDefaultRange(position, model) {
    const word = model.getWordAtPosition(position);
    const maxColumn = model.getLineMaxColumn(position.lineNumber);
    // By default, always replace up until the end of the current line.
    // This default might be subject to change!
    return word
        ? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn)
        : Range.fromPositions(position, position.with(undefined, maxColumn));
}
function closeBrackets(text, position, model, languageConfigurationService) {
    const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1);
    const newLine = lineStart + text;
    const newTokens = model.tokenization.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text);
    const slicedTokens = newTokens === null || newTokens === void 0 ? void 0 : newTokens.sliceAndInflate(position.column - 1, newLine.length, 0);
    if (!slicedTokens) {
        return text;
    }
    const newText = fixBracketsInLine(slicedTokens, languageConfigurationService);
    return newText;
}
