"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NextEditCache = void 0;
const configurationService_1 = require("../../../platform/configuration/common/configurationService");
const observable_1 = require("../../../platform/inlineEdits/common/utils/observable");
const cache_1 = require("../../../util/common/cache");
const lifecycle_1 = require("../../../util/vs/base/common/lifecycle");
const observableInternal_1 = require("../../../util/vs/base/common/observableInternal");
const editRebase_1 = require("../common/editRebase");
const rebaseResult_1 = require("./rebaseResult");
class NextEditCache extends lifecycle_1.Disposable {
    constructor(workspace, _logService, _configService, _expService) {
        super();
        this.workspace = workspace;
        this._logService = _logService;
        this._configService = _configService;
        this._expService = _expService;
        this._documentCaches = new Map();
        this._sharedCache = new cache_1.LRUCache(50);
        (0, observableInternal_1.mapObservableArrayCached)(this, workspace.openDocuments, (doc, store) => {
            const state = new DocumentEditCache(this, doc.id, doc, this._sharedCache, this._logService);
            this._documentCaches.set(state.docId, state);
            store.add((0, observable_1.autorunWithChanges)(this, {
                value: doc.value,
            }, (data) => {
                for (const edit of data.value.changes) {
                    if (!edit.isEmpty()) {
                        state.handleEdit(edit);
                    }
                }
                // if editor-change triggering is allowed,
                // 	it means an edit in file A can result in a cached edit for file B to be less relevant than with the edits in file A included
                if (this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, this._expService) !== undefined) {
                    for (const [k, v] of this._sharedCache.entries()) {
                        if (v.docId !== doc.id) {
                            this._sharedCache.deleteKey(k);
                        }
                    }
                }
            }));
            store.add((0, lifecycle_1.toDisposable)(() => {
                this._documentCaches.delete(doc.id);
            }));
        }).recomputeInitiallyAndOnChange(this._store);
    }
    setKthNextEdit(docId, documentContents, editWindow, nextEdit, subsequentN, nextEdits, userEditSince, source, opts) {
        const docCache = this._documentCaches.get(docId);
        if (!docCache) {
            return;
        }
        return docCache.setKthNextEdit(documentContents, editWindow, nextEdit, nextEdits, userEditSince, subsequentN, source, opts);
    }
    setNoNextEdit(docId, documentContents, editWindow, source) {
        const docCache = this._documentCaches.get(docId);
        if (!docCache) {
            return;
        }
        docCache.setNoNextEdit(documentContents, editWindow, source);
    }
    _getNesRebaseConfigs() {
        const maxImperfectAgreementLength = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsMaxImperfectAgreementLength, this._expService);
        return {
            absorbSubsequenceTyping: this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAbsorbSubsequenceTyping, this._expService),
            reverseAgreement: this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsReverseAgreement, this._expService),
            maxImperfectAgreementLength: typeof maxImperfectAgreementLength === 'number' ? Math.max(0, maxImperfectAgreementLength) : maxImperfectAgreementLength,
        };
    }
    lookupNextEdit(docId, currentDocumentContents, currentSelection) {
        const docCache = this._documentCaches.get(docId);
        if (!docCache) {
            return undefined;
        }
        const cacheCursorDistanceCheck = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsCacheCursorDistanceCheck, this._expService) ?? false;
        return docCache.lookupNextEdit(currentDocumentContents, currentSelection, this._getNesRebaseConfigs(), cacheCursorDistanceCheck);
    }
    tryRebaseCacheEntry(cachedEdit, currentDocumentContents, currentSelection) {
        const docCache = this._documentCaches.get(cachedEdit.docId);
        if (!docCache) {
            return { edit: undefined };
        }
        return docCache.tryRebaseCacheEntry(cachedEdit, currentDocumentContents, currentSelection, this._getNesRebaseConfigs());
    }
    rejectedNextEdit(requestId) {
        this._sharedCache.getValues()
            .filter(v => v.source.headerRequestId === requestId)
            .forEach(v => v.rejected = true);
    }
    isRejectedNextEdit(docId, currentDocumentContents, edit) {
        const docCache = this._documentCaches.get(docId);
        if (!docCache) {
            return false;
        }
        return docCache.isRejectedNextEdit(currentDocumentContents, edit);
    }
    evictedCachedEdit(cachedEdit) {
        const docCache = this._documentCaches.get(cachedEdit.docId);
        if (docCache) {
            docCache.evictedCachedEdit(cachedEdit);
        }
    }
    clear() {
        this._documentCaches.forEach(cache => cache.clear());
        this._sharedCache.clear();
    }
}
exports.NextEditCache = NextEditCache;
class DocumentEditCache {
    constructor(_nextEditCache, docId, _doc, _sharedCache, _logService) {
        this._nextEditCache = _nextEditCache;
        this.docId = docId;
        this._doc = _doc;
        this._sharedCache = _sharedCache;
        this._trackedCachedEdits = [];
        this._logger = _logService.createSubLogger(['NES', 'DocumentEditCache']);
    }
    handleEdit(edit) {
        const logger = this._logger.createSubLogger('handleEdit');
        for (const cachedEdit of this._trackedCachedEdits) {
            if (cachedEdit.userEditSince) {
                cachedEdit.userEditSince = cachedEdit.userEditSince.compose(edit);
                cachedEdit.rebaseFailed = false;
                if (!(0, editRebase_1.checkEditConsistency)(cachedEdit.documentBeforeEdit.value, cachedEdit.userEditSince, this._doc.value.get().value, logger)) {
                    cachedEdit.userEditSince = undefined;
                }
            }
        }
    }
    evictedCachedEdit(cachedEdit) {
        const index = this._trackedCachedEdits.indexOf(cachedEdit);
        if (index !== -1) {
            this._trackedCachedEdits.splice(index, 1);
        }
    }
    clear() {
        this._trackedCachedEdits.length = 0;
    }
    setKthNextEdit(documentContents, editWindow, nextEdit, nextEdits, userEditSince, subsequentN, source, opts) {
        const key = this._getKey(documentContents.value);
        const cachedEdit = { docId: this.docId, edit: nextEdit, edits: nextEdits, detailedEdits: [], userEditSince, subsequentN, source, documentBeforeEdit: documentContents, editWindow, originalEditWindow: opts.originalEditWindow, cacheTime: Date.now(), isFromCursorJump: opts.isFromCursorJump, cursorOffsetAtCacheTime: opts.cursorOffset };
        if (userEditSince) {
            if (!(0, editRebase_1.checkEditConsistency)(cachedEdit.documentBeforeEdit.value, userEditSince, this._doc.value.get().value, this._logger.createSubLogger('setKthNextEdit'))) {
                cachedEdit.userEditSince = undefined;
            }
            else {
                this._trackedCachedEdits.unshift(cachedEdit);
            }
        }
        const existing = this._sharedCache.get(key);
        if (existing) {
            this.evictedCachedEdit(existing);
        }
        const evicted = this._sharedCache.put(key, cachedEdit);
        if (evicted) {
            this._nextEditCache.evictedCachedEdit(evicted[1]);
        }
        return cachedEdit;
    }
    setNoNextEdit(documentContents, editWindow, source) {
        const key = this._getKey(documentContents.value);
        const cachedEdit = { docId: this.docId, edit: undefined, edits: [], detailedEdits: [], source, documentBeforeEdit: documentContents, editWindow, cacheTime: Date.now(), isFromCursorJump: false };
        const existing = this._sharedCache.get(key);
        if (existing) {
            this.evictedCachedEdit(existing);
        }
        const evicted = this._sharedCache.put(key, cachedEdit);
        if (evicted) {
            this._nextEditCache.evictedCachedEdit(evicted[1]);
        }
    }
    lookupNextEdit(currentDocumentContents, currentSelection, nesRebaseConfigs, cacheCursorDistanceCheck = false) {
        // TODO@chrmarti: Update entries i > 1 with user edits and edit window and start tracking.
        const key = this._getKey(currentDocumentContents.value);
        const cachedEdit = this._sharedCache.get(key);
        if (cachedEdit) {
            const editWindow = cachedEdit.editWindow;
            const originalEditWindow = cachedEdit.originalEditWindow;
            const cursorRange = currentSelection[0];
            // For cursor jump edits, allow cache hits when cursor is in either the jump target window
            // (editWindow) or the original cursor location window (originalEditWindow)
            const inEditWindow = editWindow?.containsRange(cursorRange);
            const inOriginalWindow = originalEditWindow?.containsRange(cursorRange);
            if (editWindow && !inEditWindow && !inOriginalWindow) {
                return undefined;
            }
            // If the cursor moved farther from the edit's start line than it was at cache time,
            // reject the cached edit so the same suggestion is not shown again.
            // Only applies to non-rebased, non-subsequent edits.
            if (cacheCursorDistanceCheck
                && cachedEdit.edit
                && (cachedEdit.subsequentN === undefined || cachedEdit.subsequentN === 0)
                && cachedEdit.cursorOffsetAtCacheTime !== undefined
                && cursorRange) {
                const transformer = currentDocumentContents.getTransformer();
                const editStartLine = transformer.getPosition(cachedEdit.edit.replaceRange.start).lineNumber;
                const originalCursorLine = transformer.getPosition(cachedEdit.cursorOffsetAtCacheTime).lineNumber;
                const currentCursorLine = transformer.getPosition(cursorRange.start).lineNumber;
                if (Math.abs(currentCursorLine - editStartLine) > Math.abs(originalCursorLine - editStartLine)) {
                    cachedEdit.rejected = true;
                    return cachedEdit;
                }
            }
            return cachedEdit;
        }
        for (const cachedEdit of this._trackedCachedEdits) {
            const result = this.tryRebaseCacheEntry(cachedEdit, currentDocumentContents, currentSelection, nesRebaseConfigs);
            if (result.edit) {
                return result.edit;
            }
        }
        return undefined;
    }
    tryRebaseCacheEntry(cachedEdit, currentDocumentContents, currentSelection, nesRebaseConfigs) {
        const logger = this._logger.createSubLogger('tryRebaseCacheEntry');
        if (cachedEdit.userEditSince && !cachedEdit.rebaseFailed) {
            const originalEdits = cachedEdit.edits || (cachedEdit.edit ? [cachedEdit.edit] : []);
            // For cursor jump edits, try rebasing with the primary edit window first.
            // If that fails due to cursor being outside, try with the original edit window
            // (the window around the cursor's original position before the jump).
            const windowsToTry = cachedEdit.originalEditWindow
                ? [cachedEdit.editWindow, cachedEdit.originalEditWindow]
                : [cachedEdit.editWindow];
            for (const window of windowsToTry) {
                const res = (0, editRebase_1.tryRebase)(cachedEdit.documentBeforeEdit.value, window, originalEdits, cachedEdit.detailedEdits, cachedEdit.userEditSince, currentDocumentContents.value, currentSelection, 'strict', logger, nesRebaseConfigs);
                if (res === 'rebaseFailed') {
                    cachedEdit.rebaseFailed = true;
                    return {
                        edit: undefined,
                        failureInfo: new rebaseResult_1.RebaseFailureInfo(cachedEdit.documentBeforeEdit.value, window, originalEdits, cachedEdit.userEditSince, currentDocumentContents.value, currentSelection, nesRebaseConfigs),
                    };
                }
                else if (res === 'inconsistentEdits' || res === 'error') {
                    cachedEdit.userEditSince = undefined;
                    return { edit: undefined };
                }
                else if (res === 'outsideEditWindow') {
                    // Try the next window (if available)
                    continue;
                }
                else if (res.length) {
                    if (!cachedEdit.rejected && this.isRejectedNextEdit(currentDocumentContents, res[0].rebasedEdit)) {
                        cachedEdit.rejected = true;
                    }
                    return { edit: { ...cachedEdit, ...res[0], baseCacheEntry: cachedEdit } };
                }
                else if (!originalEdits.length) {
                    return { edit: cachedEdit }; // cached 'no edits'
                }
            }
        }
        return { edit: undefined };
    }
    isRejectedNextEdit(currentDocumentContents, edit) {
        const logger = this._logger.createSubLogger('isRejectedNextEdit');
        const resultEdit = edit.removeCommonSuffixAndPrefix(currentDocumentContents.value);
        for (const rejectedEdit of this._trackedCachedEdits.filter(edit => edit.rejected)) {
            if (!rejectedEdit.userEditSince) {
                continue;
            }
            const edits = rejectedEdit.edits || (rejectedEdit.edit ? [rejectedEdit.edit] : []);
            if (!edits.length) {
                continue; // cached 'no edits'
            }
            const rejectedEdits = (0, editRebase_1.tryRebase)(rejectedEdit.documentBeforeEdit.value, undefined, edits, rejectedEdit.detailedEdits, rejectedEdit.userEditSince, currentDocumentContents.value, [], 'lenient', logger);
            if (typeof rejectedEdits === 'string') {
                continue;
            }
            const rejected = rejectedEdits.some(rejected => rejected.rebasedEdit.removeCommonSuffixAndPrefix(currentDocumentContents.value).equals(resultEdit));
            if (rejected) {
                logger.trace('Found rejected edit that matches current edit');
                return true;
            }
        }
        return false;
    }
    _getKey(val) {
        return JSON.stringify([this.docId.uri, val]);
    }
}
//# sourceMappingURL=nextEditCache.js.map