/*---------------------------------------------------------------------------------------------
 *  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 { RunOnceScheduler } from '../../../../base/common/async.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { derived, observableSignal, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
import { autorunWithStore2 } from '../../../../base/common/observableImpl/autorun.js';
import { LineRange } from '../../../common/core/lineRange.js';
import { Range } from '../../../common/core/range.js';
import { LineRangeMapping, RangeMapping } from '../../../common/diff/linesDiffComputer.js';
import { lineRangeMappingFromRangeMappings } from '../../../common/diff/standardLinesDiffComputer.js';
import { TextEditInfo } from '../../../common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.js';
import { combineTextEditInfos } from '../../../common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos.js';
import { lengthAdd, lengthDiffNonNegative, lengthOfRange, lengthToPosition, lengthZero, positionToLength } from '../../../common/model/bracketPairsTextModelPart/bracketPairsTree/length.js';
export class DiffModel extends Disposable {
    constructor(model, ignoreTrimWhitespace, maxComputationTimeMs, hideUnchangedRegions, documentDiffProvider) {
        super();
        this.hideUnchangedRegions = hideUnchangedRegions;
        this._isDiffUpToDate = observableValue('isDiffUpToDate', false);
        this._diff = observableValue('diff', undefined);
        this.diff = this._diff;
        this._unchangedRegions = observableValue('unchangedRegion', { regions: [], originalDecorationIds: [], modifiedDecorationIds: [] });
        this.unchangedRegions = derived('unchangedRegions', r => this.hideUnchangedRegions.read(r) ? this._unchangedRegions.read(r).regions : []);
        const contentChangedSignal = observableSignal('contentChangedSignal');
        const debouncer = this._register(new RunOnceScheduler(() => contentChangedSignal.trigger(undefined), 200));
        this._register(model.modified.onDidChangeContent((e) => {
            const diff = this._diff.get();
            if (!diff) {
                return;
            }
            const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
            this._diff.set(applyModifiedEdits(diff, textEdits, model.original, model.modified), undefined);
            debouncer.schedule();
        }));
        this._register(model.original.onDidChangeContent((e) => {
            const diff = this._diff.get();
            if (!diff) {
                return;
            }
            const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
            this._diff.set(applyOriginalEdits(diff, textEdits, model.original, model.modified), undefined);
            debouncer.schedule();
        }));
        const documentDiffProviderOptionChanged = observableSignalFromEvent('documentDiffProviderOptionChanged', documentDiffProvider.onDidChange);
        this._register(autorunWithStore2('compute diff', (reader, store) => __awaiter(this, void 0, void 0, function* () {
            debouncer.cancel();
            contentChangedSignal.read(reader);
            documentDiffProviderOptionChanged.read(reader);
            const ignoreTrimWhitespaceVal = ignoreTrimWhitespace.read(reader);
            const maxComputationTimeMsVal = maxComputationTimeMs.read(reader);
            this._isDiffUpToDate.set(false, undefined);
            let originalTextEditInfos = [];
            store.add(model.original.onDidChangeContent((e) => {
                const edits = TextEditInfo.fromModelContentChanges(e.changes);
                originalTextEditInfos = combineTextEditInfos(originalTextEditInfos, edits);
            }));
            let modifiedTextEditInfos = [];
            store.add(model.modified.onDidChangeContent((e) => {
                const edits = TextEditInfo.fromModelContentChanges(e.changes);
                modifiedTextEditInfos = combineTextEditInfos(modifiedTextEditInfos, edits);
            }));
            let result = yield documentDiffProvider.computeDiff(model.original, model.modified, {
                ignoreTrimWhitespace: ignoreTrimWhitespaceVal,
                maxComputationTimeMs: maxComputationTimeMsVal,
            });
            result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified);
            result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified);
            const newUnchangedRegions = UnchangedRegion.fromDiffs(result.changes, model.original.getLineCount(), model.modified.getLineCount());
            // Transfer state from cur state
            const lastUnchangedRegions = this._unchangedRegions.get();
            const lastUnchangedRegionsOrigRanges = lastUnchangedRegions.originalDecorationIds
                .map(id => model.original.getDecorationRange(id))
                .filter(r => !!r)
                .map(r => LineRange.fromRange(r));
            const lastUnchangedRegionsModRanges = lastUnchangedRegions.modifiedDecorationIds
                .map(id => model.modified.getDecorationRange(id))
                .filter(r => !!r)
                .map(r => LineRange.fromRange(r));
            for (const r of newUnchangedRegions) {
                for (let i = 0; i < lastUnchangedRegions.regions.length; i++) {
                    if (r.originalRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
                        && r.modifiedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
                        r.setState(lastUnchangedRegions.regions[i].visibleLineCountTop.get(), lastUnchangedRegions.regions[i].visibleLineCountBottom.get(), undefined);
                        break;
                    }
                }
            }
            const originalDecorationIds = model.original.deltaDecorations(lastUnchangedRegions.originalDecorationIds, newUnchangedRegions.map(r => ({ range: r.originalRange.toInclusiveRange(), options: { description: 'unchanged' } })));
            const modifiedDecorationIds = model.modified.deltaDecorations(lastUnchangedRegions.modifiedDecorationIds, newUnchangedRegions.map(r => ({ range: r.modifiedRange.toInclusiveRange(), options: { description: 'unchanged' } })));
            transaction(tx => {
                this._diff.set(result, tx);
                this._isDiffUpToDate.set(true, tx);
                this._unchangedRegions.set({
                    regions: newUnchangedRegions,
                    originalDecorationIds,
                    modifiedDecorationIds
                }, tx);
            });
        })));
    }
    revealModifiedLine(lineNumber, tx) {
        const unchangedRegions = this._unchangedRegions.get().regions;
        for (const r of unchangedRegions) {
            if (r.getHiddenModifiedRange(undefined).contains(lineNumber)) {
                r.showAll(tx); // TODO only unhide what is needed
                return;
            }
        }
    }
    revealOriginalLine(lineNumber, tx) {
        const unchangedRegions = this._unchangedRegions.get().regions;
        for (const r of unchangedRegions) {
            if (r.getHiddenOriginalRange(undefined).contains(lineNumber)) {
                r.showAll(tx); // TODO only unhide what is needed
                return;
            }
        }
    }
}
export class UnchangedRegion {
    static fromDiffs(changes, originalLineCount, modifiedLineCount) {
        const inversedMappings = LineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
        const result = [];
        const minHiddenLineCount = 3;
        const minContext = 3;
        for (const mapping of inversedMappings) {
            let origStart = mapping.originalRange.startLineNumber;
            let modStart = mapping.modifiedRange.startLineNumber;
            let length = mapping.originalRange.length;
            if (origStart === 1 && length > minContext + minHiddenLineCount) {
                length -= minContext;
                result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
            }
            else if (origStart + length === originalLineCount + 1 && length > minContext + minHiddenLineCount) {
                origStart += minContext;
                modStart += minContext;
                length -= minContext;
                result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
            }
            else if (length > minContext * 2 + minHiddenLineCount) {
                origStart += minContext;
                modStart += minContext;
                length -= minContext * 2;
                result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
            }
        }
        return result;
    }
    get originalRange() {
        return LineRange.ofLength(this.originalLineNumber, this.lineCount);
    }
    get modifiedRange() {
        return LineRange.ofLength(this.modifiedLineNumber, this.lineCount);
    }
    constructor(originalLineNumber, modifiedLineNumber, lineCount, visibleLineCountTop, visibleLineCountBottom) {
        this.originalLineNumber = originalLineNumber;
        this.modifiedLineNumber = modifiedLineNumber;
        this.lineCount = lineCount;
        this._visibleLineCountTop = observableValue('visibleLineCountTop', 0);
        this.visibleLineCountTop = this._visibleLineCountTop;
        this._visibleLineCountBottom = observableValue('visibleLineCountBottom', 0);
        this.visibleLineCountBottom = this._visibleLineCountBottom;
        this._visibleLineCountTop.set(visibleLineCountTop, undefined);
        this._visibleLineCountBottom.set(visibleLineCountBottom, undefined);
    }
    getHiddenOriginalRange(reader) {
        return LineRange.ofLength(this.originalLineNumber + this._visibleLineCountTop.read(reader), this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader));
    }
    getHiddenModifiedRange(reader) {
        return LineRange.ofLength(this.modifiedLineNumber + this._visibleLineCountTop.read(reader), this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader));
    }
    showMoreAbove(tx) {
        const maxVisibleLineCountTop = this.lineCount - this._visibleLineCountBottom.get();
        this._visibleLineCountTop.set(Math.min(this._visibleLineCountTop.get() + 10, maxVisibleLineCountTop), tx);
    }
    showMoreBelow(tx) {
        const maxVisibleLineCountBottom = this.lineCount - this._visibleLineCountTop.get();
        this._visibleLineCountBottom.set(Math.min(this._visibleLineCountBottom.get() + 10, maxVisibleLineCountBottom), tx);
    }
    showAll(tx) {
        this._visibleLineCountBottom.set(this.lineCount - this._visibleLineCountTop.get(), tx);
    }
    setState(visibleLineCountTop, visibleLineCountBottom, tx) {
        visibleLineCountTop = Math.min(visibleLineCountTop, this.lineCount);
        visibleLineCountBottom = Math.min(visibleLineCountBottom, this.lineCount - visibleLineCountTop);
        this._visibleLineCountTop.set(visibleLineCountTop, tx);
        this._visibleLineCountBottom.set(visibleLineCountBottom, tx);
    }
}
function applyOriginalEdits(diff, textEdits, originalTextModel, modifiedTextModel) {
    if (textEdits.length === 0) {
        return diff;
    }
    const diffTextEdits = diff.changes.flatMap(c => c.innerChanges.map(c => new TextEditInfo(positionToLength(c.modifiedRange.getStartPosition()), positionToLength(c.modifiedRange.getEndPosition()), lengthOfRange(c.originalRange).toLength())));
    const combined = combineTextEditInfos(diffTextEdits, textEdits);
    let lastModifiedEndOffset = lengthZero;
    let lastOriginalEndOffset = lengthZero;
    const rangeMappings = combined.map(c => {
        const originalStartOffset = lengthAdd(lastOriginalEndOffset, lengthDiffNonNegative(lastModifiedEndOffset, c.startOffset));
        lastModifiedEndOffset = c.endOffset;
        lastOriginalEndOffset = lengthAdd(originalStartOffset, c.newLength);
        return new RangeMapping(Range.fromPositions(lengthToPosition(originalStartOffset), lengthToPosition(lastOriginalEndOffset)), Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)));
    });
    const changes = lineRangeMappingFromRangeMappings(rangeMappings, originalTextModel.getLinesContent(), modifiedTextModel.getLinesContent());
    return {
        identical: false,
        quitEarly: false,
        changes,
    };
}
function applyModifiedEdits(diff, textEdits, originalTextModel, modifiedTextModel) {
    if (textEdits.length === 0) {
        return diff;
    }
    const diffTextEdits = diff.changes.flatMap(c => c.innerChanges.map(c => new TextEditInfo(positionToLength(c.originalRange.getStartPosition()), positionToLength(c.originalRange.getEndPosition()), lengthOfRange(c.modifiedRange).toLength())));
    const combined = combineTextEditInfos(diffTextEdits, textEdits);
    let lastOriginalEndOffset = lengthZero;
    let lastModifiedEndOffset = lengthZero;
    const rangeMappings = combined.map(c => {
        const modifiedStartOffset = lengthAdd(lastModifiedEndOffset, lengthDiffNonNegative(lastOriginalEndOffset, c.startOffset));
        lastOriginalEndOffset = c.endOffset;
        lastModifiedEndOffset = lengthAdd(modifiedStartOffset, c.newLength);
        return new RangeMapping(Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)), Range.fromPositions(lengthToPosition(modifiedStartOffset), lengthToPosition(lastModifiedEndOffset)));
    });
    const changes = lineRangeMappingFromRangeMappings(rangeMappings, originalTextModel.getLinesContent(), modifiedTextModel.getLinesContent());
    return {
        identical: false,
        quitEarly: false,
        changes,
    };
}
