"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateRefreshers = exports.hasSchemaChange = exports.SharedTreeChangeFamily = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../feature-libraries/index.js");
const index_js_3 = require("../util/index.js");
const sharedTreeChangeCodecs_js_1 = require("./sharedTreeChangeCodecs.js");
const sharedTreeEditBuilder_js_1 = require("./sharedTreeEditBuilder.js");
/**
 * Implementation of {@link ChangeFamily} that combines edits to fields and schema changes.
 *
 * @sealed
 */
class SharedTreeChangeFamily {
    constructor(revisionTagCodec, fieldBatchCodec, codecOptions, chunkCompressionStrategy) {
        const modularChangeCodec = (0, index_js_2.makeModularChangeCodecFamily)(index_js_2.fieldKindConfigurations, revisionTagCodec, fieldBatchCodec, codecOptions, chunkCompressionStrategy);
        this.modularChangeFamily = new index_js_2.ModularChangeFamily(index_js_2.fieldKinds, modularChangeCodec);
        this.codecs = (0, sharedTreeChangeCodecs_js_1.makeSharedTreeChangeCodecFamily)(this.modularChangeFamily.codecs, codecOptions);
    }
    buildEditor(changeReceiver) {
        return new sharedTreeEditBuilder_js_1.SharedTreeEditBuilder(this.modularChangeFamily, changeReceiver);
    }
    compose(changes) {
        const newChanges = [];
        const dataChangeRun = [];
        const flushDataChangeRun = () => {
            if (dataChangeRun.length > 0) {
                newChanges.push({
                    type: "data",
                    innerChange: this.modularChangeFamily.compose(dataChangeRun),
                });
                dataChangeRun.length = 0;
            }
        };
        for (const topChange of changes) {
            for (const change of topChange.change.changes) {
                if (change.type === "schema") {
                    flushDataChangeRun();
                    newChanges.push(change);
                }
                else {
                    dataChangeRun.push((0, index_js_1.mapTaggedChange)(topChange, change.innerChange));
                }
            }
        }
        flushDataChangeRun();
        return { changes: newChanges };
    }
    invert(change, isRollback) {
        const invertInnerChange = (innerChange) => {
            switch (innerChange.type) {
                case "data":
                    return {
                        type: "data",
                        innerChange: this.modularChangeFamily.invert((0, index_js_1.mapTaggedChange)(change, innerChange.innerChange), isRollback),
                    };
                case "schema": {
                    return {
                        type: "schema",
                        innerChange: {
                            schema: {
                                new: innerChange.innerChange.schema.old,
                                old: innerChange.innerChange.schema.new,
                            },
                            isInverse: true,
                        },
                    };
                }
                default:
                    (0, index_js_3.fail)("Unknown SharedTree change type.");
            }
        };
        return {
            changes: change.change.changes.map(invertInnerChange).reverse(),
        };
    }
    rebase(change, over, revisionMetadata) {
        if (change.change.changes.length === 0 || over.change.changes.length === 0) {
            return change.change;
        }
        if (hasSchemaChange(change.change) || hasSchemaChange(over.change)) {
            // Any SharedTreeChange (a list of sub-changes) that contains a schema change will cause ANY change that rebases over it to conflict.
            // Similarly, any SharedTreeChange containing a schema change will fail to rebase over ANY change.
            // Those two combine to mean: no concurrency with schema changes is supported.
            // This is fine because it's an open problem. Example: a tree with an A at the root and a schema that allows an A | B at the root will
            // become out of schema if a schema changes to restrict root types to just A is concurrent with a data change that sets it to a B.
            // We don't have an efficient way to detect this document-wide and there are varying opinions on restricting schema changes to prevent this.
            // A SharedTreeChange containing a schema change will NOT conflict in a non-concurrency case, as the "meatless sandwich" optimization
            // will result in rebase never being called.
            return SharedTreeChangeFamily.emptyChange;
        }
        (0, internal_1.assert)(change.change.changes.length === 1 && over.change.changes.length === 1, 0x884 /* SharedTreeChange should have exactly one inner change if no schema change is present. */);
        const dataChangeIntention = change.change.changes[0];
        const dataChangeOver = over.change.changes[0];
        (0, internal_1.assert)(dataChangeIntention.type === "data" && dataChangeOver.type === "data", 0x885 /* Data change should be present. */);
        return {
            changes: [
                {
                    type: "data",
                    innerChange: this.modularChangeFamily.rebase((0, index_js_1.mapTaggedChange)(change, dataChangeIntention.innerChange), (0, index_js_1.mapTaggedChange)(over, dataChangeOver.innerChange), revisionMetadata),
                },
            ],
        };
    }
    changeRevision(change, newRevision, rollbackOf) {
        return {
            changes: change.changes.map((inner) => {
                return inner.type === "data"
                    ? {
                        ...inner,
                        innerChange: this.modularChangeFamily.rebaser.changeRevision(inner.innerChange, newRevision, rollbackOf),
                    }
                    : inner;
            }),
        };
    }
    get rebaser() {
        return this;
    }
}
exports.SharedTreeChangeFamily = SharedTreeChangeFamily;
SharedTreeChangeFamily.emptyChange = {
    changes: [],
};
function hasSchemaChange(change) {
    return change.changes.some((innerChange) => innerChange.type === "schema");
}
exports.hasSchemaChange = hasSchemaChange;
function mapDataChanges(change, map) {
    return {
        changes: change.changes.map((dataOrSchemaChange) => {
            if (dataOrSchemaChange.type === "data") {
                return {
                    type: "data",
                    innerChange: map(dataOrSchemaChange.innerChange),
                };
            }
            return dataOrSchemaChange;
        }),
    };
}
/**
 * Produces an equivalent change with an updated set of appropriate refreshers.
 * @param change - The change to compute refreshers for. Not mutated.
 * @param getDetachedNode - retrieves a {@link TreeChunk} for the corresponding detached node id.
 * Is expected to read from a forest in a state that corresponds to the input context of the given change.
 * @returns An equivalent change with an updated set of appropriate refreshers.
 */
function updateRefreshers(change, getDetachedNode, relevantRemovedRootsFromDataChange, updateDataChangeRefreshers) {
    // Adding refreshers to a SharedTreeChange is not as simple as adding refreshers to each of its data changes.
    // This is because earlier data changes affect the state of the forest in ways that can influence the refreshers
    // needed for later data changes. This can happen in two ways:
    // 1. By removing a tree that is a relevant root to a later data change.
    // 2. By changing the contents of a tree that is a relevant root to a later data change.
    // (Note that these two cases can compound)
    // Thankfully, in both of these cases, refreshers can be omitted from the later data changes because the forest
    // applying those data changes is guaranteed to still have have the relevant trees in memory.
    // This means that for the first data change, all required refreshers should be added (and none should be missing).
    // While for later data changes, we should not include refreshers that either:
    // A) were already included in the earlier data changes
    // B) correspond to trees that were removed by earlier data changes
    // Set A is excluded by tracking which roots have already been included in the earlier data changes, and filtering
    // them out from the relevant removed roots.
    // Set B is excluded because the `getDetachedNode` is bound to return `undefined` for them, which tell
    // `defaultUpdateRefreshers` to ignore. One downside of this approach is that it prevents `defaultUpdateRefreshers`
    // from detecting cases where a detached node is missing for another reason (which would be a bug).
    // The roots that have been included as refreshers across all data changes so far.
    const includedRoots = new Map();
    function getAndRememberDetachedNode(id) {
        (0, index_js_3.addToNestedSet)(includedRoots, id.major, id.minor);
        return getDetachedNode(id);
    }
    function* filterIncludedRoots(toFilter) {
        for (const id of toFilter) {
            if (!(0, index_js_3.nestedSetContains)(includedRoots, id.major, id.minor)) {
                yield id;
            }
        }
    }
    let isFirstDataChange = true;
    return mapDataChanges(change, (dataChange) => {
        const removedRoots = relevantRemovedRootsFromDataChange(dataChange);
        if (isFirstDataChange) {
            isFirstDataChange = false;
            return updateDataChangeRefreshers(dataChange, getAndRememberDetachedNode, removedRoots, true);
        }
        else {
            return updateDataChangeRefreshers(dataChange, getAndRememberDetachedNode, filterIncludedRoots(removedRoots), false);
        }
    });
}
exports.updateRefreshers = updateRefreshers;
//# sourceMappingURL=sharedTreeChangeFamily.js.map