"use strict";
/*---------------------------------------------------------------------------------------------
 *  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 NesXtabHistoryTracker_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NesXtabHistoryTracker = exports.XtabEditMergeStrategy = void 0;
const assert_1 = require("../../../../util/vs/base/common/assert");
const lifecycle_1 = require("../../../../util/vs/base/common/lifecycle");
const linkedList_1 = require("../../../../util/vs/base/common/linkedList");
const observable_1 = require("../../../../util/vs/base/common/observable");
const observableInternal_1 = require("../../../../util/vs/base/common/observableInternal");
const configurationService_1 = require("../../../configuration/common/configurationService");
const nullExperimentationService_1 = require("../../../telemetry/common/nullExperimentationService");
const edit_1 = require("../dataTypes/edit");
const xtabHistoryOptions_1 = require("../dataTypes/xtabHistoryOptions");
const observable_2 = require("../utils/observable");
const utils_1 = require("../utils/utils");
var XtabEditMergeStrategy;
(function (XtabEditMergeStrategy) {
    XtabEditMergeStrategy.sameStartLine = { kind: xtabHistoryOptions_1.DiffHistoryMergeStrategy.SameStartLine };
    function proximity(lineGap) {
        return { kind: xtabHistoryOptions_1.DiffHistoryMergeStrategy.Proximity, lineGap };
    }
    XtabEditMergeStrategy.proximity = proximity;
    function hybrid(lineGap, splitAfterMs) {
        return { kind: xtabHistoryOptions_1.DiffHistoryMergeStrategy.Hybrid, lineGap, splitAfterMs };
    }
    XtabEditMergeStrategy.hybrid = hybrid;
    /**
     * Constructs a {@link XtabEditMergeStrategy} from config values.
     */
    function fromConfig(strategyKind, lineGap, splitAfterMs) {
        switch (strategyKind) {
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.Proximity:
                return proximity(lineGap);
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.Hybrid:
                return hybrid(lineGap, splitAfterMs);
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.SameStartLine:
                return XtabEditMergeStrategy.sameStartLine;
            default:
                (0, assert_1.assertNever)(strategyKind);
        }
    }
    XtabEditMergeStrategy.fromConfig = fromConfig;
})(XtabEditMergeStrategy || (exports.XtabEditMergeStrategy = XtabEditMergeStrategy = {}));
/**
 * Returns whether all replacements in `a` are within `maxLineGap` lines of some replacement in `b` (and vice-versa).
 */
function areLineEditsWithinProximity(a, b, maxLineGap) {
    if (a.isEmpty() || b.isEmpty()) {
        return false;
    }
    // Every replacement in `a` must be close to at least one replacement in `b`.
    for (const repA of a.replacements) {
        const closeToSomeInB = b.replacements.some(repB => repA.lineRange.distanceToRange(repB.lineRange) <= maxLineGap);
        if (!closeToSomeInB) {
            return false;
        }
    }
    return true;
}
let NesXtabHistoryTracker = class NesXtabHistoryTracker extends lifecycle_1.Disposable {
    static { NesXtabHistoryTracker_1 = this; }
    /** Max # of entries in history */
    static { this.MAX_HISTORY_SIZE = 50; }
    constructor(workspace, maxHistorySize, _configurationService, _expService) {
        super();
        this._configurationService = _configurationService;
        this._expService = _expService;
        this.idToEntry = new Map();
        this.history = new linkedList_1.LinkedList();
        this.maxHistorySize = maxHistorySize ?? NesXtabHistoryTracker_1.MAX_HISTORY_SIZE;
        this.mergeStrategy = (0, observableInternal_1.derived)(reader => XtabEditMergeStrategy.fromConfig(this._configurationService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabDiffMergeStrategy, this._expService).read(reader), this._configurationService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabDiffMergeLineGap, this._expService).read(reader), this._configurationService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabDiffMergeSplitAfterMs, this._expService).read(reader)));
        (0, observable_1.mapObservableArrayCached)(this, workspace.openDocuments, (doc, store) => {
            // add .value to all observables
            store.add((0, observable_2.autorunWithChanges)(this, {
                rootedEdits: doc.value,
                visibleRanges: doc.visibleRanges,
            }, (data) => {
                if (data.rootedEdits.changes.length > 0 && data.rootedEdits.previous !== undefined) {
                    this.handleEdits(doc, data.rootedEdits);
                }
                else {
                    this.handleVisibleRangesChange(doc, data.visibleRanges);
                }
            }));
        }, d => d.id).recomputeInitiallyAndOnChange(this._store);
    }
    getHistory() {
        return [...this.history];
    }
    /**
     * If the document isn't already in history, add it to the history.
     * If the document is in history either with an edit or selection entry, do not include it again.
     */
    handleVisibleRangesChange(doc, visibleRangesChange) {
        if (visibleRangesChange.value.length === 0) {
            return;
        }
        const previousRecord = this.idToEntry.get(doc.id);
        // if this's an already known file
        if (previousRecord !== undefined) {
            // if it's an edit entry, do not change anything
            if (previousRecord.entry.kind === 'edit') {
                return;
            }
            // else remove from history to update the visible ranges
            previousRecord.removeFromHistory();
        }
        const entry = { docId: doc.id, kind: 'visibleRanges', visibleRanges: visibleRangesChange.value, documentContent: doc.value.get() };
        const removeFromHistory = this.history.push(entry);
        this.idToEntry.set(doc.id, { entry, removeFromHistory, lastEditTimestamp: (0, utils_1.now)() });
        this.compactHistory();
    }
    shouldMerge(lastLineEdit, currentLineEdit, lastEditTimestamp) {
        const strategy = this.mergeStrategy.get();
        switch (strategy.kind) {
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.SameStartLine:
                return !currentLineEdit.isEmpty()
                    && !lastLineEdit.isEmpty()
                    && lastLineEdit.replacements[0].lineRange.startLineNumber === currentLineEdit.replacements[0].lineRange.startLineNumber;
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.Proximity:
                return areLineEditsWithinProximity(currentLineEdit, lastLineEdit, strategy.lineGap);
            case xtabHistoryOptions_1.DiffHistoryMergeStrategy.Hybrid: {
                const withinTimeWindow = ((0, utils_1.now)() - lastEditTimestamp) <= strategy.splitAfterMs;
                return withinTimeWindow && areLineEditsWithinProximity(currentLineEdit, lastLineEdit, strategy.lineGap);
            }
        }
    }
    handleEdits(doc, rootedEdits) {
        (0, assert_1.assert)(rootedEdits.previous !== undefined, `Document has previous version`);
        (0, assert_1.assert)(rootedEdits.changes.length === 1, `Expected 1 edit change but got ${rootedEdits.changes.length}`);
        const currentEdit = rootedEdits.changes[0];
        if (currentEdit.replacements.length === 0) {
            return;
        }
        const previousRecord = this.idToEntry.get(doc.id);
        // const currentBase = rootedEdits.value.apply(currentEdit.inverseOnString(rootedEdits.previous.value));
        const currentBase = rootedEdits.previous;
        const currentRootedEdit = new edit_1.RootedEdit(currentBase, currentEdit);
        if (previousRecord === undefined) {
            this.pushToHistory(doc.id, currentRootedEdit);
            return;
        }
        if (previousRecord.entry.kind === 'visibleRanges') {
            previousRecord.removeFromHistory();
            this.pushToHistory(doc.id, currentRootedEdit);
            return;
        }
        const lastRootedEdit = previousRecord.entry.edit;
        const lastLineEdit = edit_1.RootedEdit.toLineEdit(lastRootedEdit);
        const currentLineEdit = edit_1.RootedEdit.toLineEdit(currentRootedEdit);
        if (this.shouldMerge(lastLineEdit, currentLineEdit, previousRecord.lastEditTimestamp)) {
            // merge edits
            previousRecord.removeFromHistory();
            const composedEdit = lastRootedEdit.edit.compose(currentEdit);
            const edit = new edit_1.RootedEdit(lastRootedEdit.base, composedEdit);
            this.pushToHistory(doc.id, edit);
        }
        else {
            this.pushToHistory(doc.id, currentRootedEdit);
        }
    }
    pushToHistory(docId, edit) {
        const entry = { docId, kind: 'edit', edit };
        const removeFromHistory = this.history.push(entry);
        this.idToEntry.set(docId, { entry, removeFromHistory, lastEditTimestamp: (0, utils_1.now)() });
        this.compactHistory();
    }
    compactHistory() {
        if (this.history.size > this.maxHistorySize) {
            const removedEntry = this.history.shift();
            if (removedEntry !== undefined) {
                const lastRecord = this.idToEntry.get(removedEntry.docId);
                if (lastRecord !== undefined && removedEntry === lastRecord.entry) {
                    this.idToEntry.delete(removedEntry.docId);
                }
            }
        }
    }
};
exports.NesXtabHistoryTracker = NesXtabHistoryTracker;
exports.NesXtabHistoryTracker = NesXtabHistoryTracker = NesXtabHistoryTracker_1 = __decorate([
    __param(2, configurationService_1.IConfigurationService),
    __param(3, nullExperimentationService_1.IExperimentationService)
], NesXtabHistoryTracker);
//# sourceMappingURL=nesXtabHistoryTracker.js.map