"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.DetachedFieldIndex = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../codec/index.js");
const index_js_2 = require("../../util/index.js");
const detachedFieldIndexCodecs_js_1 = require("./detachedFieldIndexCodecs.js");
/**
 * The tree index records detached field IDs and associates them with a change atom ID.
 */
class DetachedFieldIndex {
    /**
     * @param name - A name for the index, used as a prefix for the generated field keys.
     * @param rootIdAllocator - An ID allocator used to generate unique field keys.
     */
    constructor(name, rootIdAllocator, revisionTagCodec, idCompressor, options) {
        this.name = name;
        this.rootIdAllocator = rootIdAllocator;
        this.revisionTagCodec = revisionTagCodec;
        this.idCompressor = idCompressor;
        /**
         * A mapping from detached node ids to detached fields.
         */
        this.detachedNodeToField = new Map();
        /**
         * A map from revisions to all detached fields for which the revision is the latest relevant revision.
         * See {@link DetachedField.latestRelevantRevision}.
         *
         * @remarks
         * undefined revisions are tolerated but any roots not associated with a revision must be disposed manually
         */
        this.latestRelevantRevisionToFields = new Map();
        /**
         * The process for loading `DetachedFieldIndex` data from a summary is split into two steps:
         * 1. Call {@link loadData}
         * 2. Call {@link setRevisionsForLoadedData}
         *
         * This flag is only set to `false` after calling `loadData` and is set back to `true` after calling `setRevisionsForLoadedData`.
         * This helps ensure that `setRevisionsForLoadedData` is only called after `loadData` and only called once.
         */
        this.isFullyLoaded = true;
        this.options = options ?? {
            jsonValidator: index_js_1.FormatValidatorNoOp,
            minVersionForCollab: index_js_1.FluidClientVersion.v2_0,
        };
        this.codec = detachedFieldIndexCodecs_js_1.detachedFieldIndexCodecBuilder.build({
            ...this.options,
            revisionTagCodec,
            idCompressor,
        });
    }
    clone() {
        const clone = new DetachedFieldIndex(this.name, (0, index_js_2.idAllocatorFromMaxId)(this.rootIdAllocator.getMaxId()), this.revisionTagCodec, this.idCompressor, this.options);
        (0, index_js_2.populateNestedMap)(this.detachedNodeToField, clone.detachedNodeToField, true);
        (0, index_js_2.populateNestedMap)(this.latestRelevantRevisionToFields, clone.latestRelevantRevisionToFields, true);
        return clone;
    }
    /**
     * Creates a restorable checkpoint of the current state of the DetachedFieldIndex.
     */
    createCheckpoint() {
        const clone = this.clone();
        return () => {
            this.purge();
            (0, index_js_2.populateNestedMap)(clone.detachedNodeToField, this.detachedNodeToField, true);
            (0, index_js_2.populateNestedMap)(clone.latestRelevantRevisionToFields, this.latestRelevantRevisionToFields, true);
            this.rootIdAllocator = (0, index_js_2.idAllocatorFromMaxId)(clone.rootIdAllocator.getMaxId());
        };
    }
    *entries() {
        for (const [major, innerMap] of this.detachedNodeToField) {
            if (major === undefined) {
                for (const [minor, { root, latestRelevantRevision }] of innerMap) {
                    yield latestRelevantRevision === undefined
                        ? { id: { minor }, root }
                        : { id: { minor }, root, latestRelevantRevision };
                }
            }
            else {
                for (const [minor, { root, latestRelevantRevision }] of innerMap) {
                    yield latestRelevantRevision === undefined
                        ? { id: { major, minor }, root }
                        : { id: { major, minor }, root, latestRelevantRevision };
                }
            }
        }
    }
    /**
     * Removes all entries from the index.
     */
    purge() {
        this.detachedNodeToField.clear();
        this.latestRelevantRevisionToFields.clear();
    }
    toFieldKey(id) {
        return (0, index_js_2.brand)(`${this.name}-${id}`);
    }
    tryGetEntry(id) {
        return (0, index_js_2.tryGetFromNestedMap)(this.detachedNodeToField, id.major, id.minor)?.root;
    }
    getEntry(id) {
        const key = this.tryGetEntry(id);
        (0, internal_1.assert)(key !== undefined, 0x7aa /* Unknown removed node ID */);
        return key;
    }
    /**
     * Returns the detached root IDs for all the trees that were detached or last modified by the given revision.
     */
    *getRootsLastTouchedByRevision(revision) {
        const roots = this.latestRelevantRevisionToFields.get(revision);
        if (roots !== undefined) {
            yield* roots.keys();
        }
    }
    /**
     * Removes the detached roots for all the trees that were detached or last modified by the given revision.
     */
    deleteRootsLastTouchedByRevision(revision) {
        const entries = this.latestRelevantRevisionToFields.get(revision);
        if (entries === undefined) {
            return;
        }
        this.latestRelevantRevisionToFields.delete(revision);
        for (const detachedNodeId of entries.values()) {
            const found = (0, index_js_2.deleteFromNestedMap)(this.detachedNodeToField, detachedNodeId.major, detachedNodeId.minor);
            (0, internal_1.assert)(found, 0x9ba /* Unable to delete unknown entry */);
        }
    }
    deleteEntry(nodeId) {
        const entry = (0, index_js_2.tryGetFromNestedMap)(this.detachedNodeToField, nodeId.major, nodeId.minor);
        (0, internal_1.assert)(entry !== undefined, 0x9bb /* Unable to delete unknown entry */);
        (0, index_js_2.deleteFromNestedMap)(this.detachedNodeToField, nodeId.major, nodeId.minor);
        (0, index_js_2.deleteFromNestedMap)(this.latestRelevantRevisionToFields, entry.latestRelevantRevision, entry.root);
    }
    /**
     * Associates the DetachedNodeId with a field key and creates an entry for it in the index.
     * @param nodeId - The ID of the detached node.
     * @param revision - The revision that last detached the root.
     * See {@link DetachedField.latestRelevantRevision} for details.
     * @param count - The number of entries to create. These entries will have consecutive minor IDs.
     * @returns The atomic ID that the `DetachedFieldIndex` uses to uniquely identify the first root.
     */
    createEntry(nodeId, revision, count = 1) {
        const root = this.rootIdAllocator.allocate(count);
        if (nodeId !== undefined) {
            for (let i = 0; i < count; i++) {
                (0, internal_1.assert)((0, index_js_2.tryGetFromNestedMap)(this.detachedNodeToField, nodeId.major, nodeId.minor + i) ===
                    undefined, 0x7ce /* Detached node ID already exists in index */);
                (0, index_js_2.setInNestedMap)(this.detachedNodeToField, nodeId.major, nodeId.minor + i, {
                    root: (0, index_js_2.brand)(root + i),
                    latestRelevantRevision: revision,
                });
                (0, index_js_2.setInNestedMap)(this.latestRelevantRevisionToFields, revision, root + i, {
                    major: nodeId.major,
                    minor: nodeId.minor + i,
                });
            }
        }
        return root;
    }
    /**
     * Updates the latest revision that is relevant to the provided root
     */
    updateLatestRevision(id, revision) {
        const fieldEntry = (0, index_js_2.tryGetFromNestedMap)(this.detachedNodeToField, id.major, id.minor);
        (0, internal_1.assert)(fieldEntry !== undefined, 0x9bc /* detached node id does not exist in the detached field index */);
        const { root, latestRelevantRevision: previousRevision } = fieldEntry;
        // remove this root from the set of roots for the previous latest revision
        (0, index_js_2.deleteFromNestedMap)(this.latestRelevantRevisionToFields, previousRevision, root);
        // add this root to the set of roots for the new latest revision
        (0, index_js_2.setInNestedMap)(this.latestRelevantRevisionToFields, revision, root, id);
        (0, index_js_2.setInNestedMap)(this.detachedNodeToField, id.major, id.minor, {
            root,
            latestRelevantRevision: revision,
        });
    }
    encode() {
        return this.codec.encode({
            data: this.detachedNodeToField,
            maxId: this.rootIdAllocator.getMaxId(),
        });
    }
    /**
     * Loads the tree index from the given string, this overrides any existing data.
     */
    loadData(data) {
        const detachedFieldIndex = this.codec.decode(data);
        this.rootIdAllocator = (0, index_js_2.idAllocatorFromMaxId)(detachedFieldIndex.maxId);
        this.detachedNodeToField = new Map();
        this.latestRelevantRevisionToFields = new Map();
        this.isFullyLoaded = false;
        const rootMap = new Map();
        (0, index_js_2.forEachInNestedMap)(detachedFieldIndex.data, ({ root }, major, minor) => {
            (0, index_js_2.setInNestedMap)(this.detachedNodeToField, major, minor, { root });
            rootMap.set(root, { major, minor });
        });
        this.latestRelevantRevisionToFields.set(undefined, rootMap);
    }
    /**
     * Sets the latest relevant revision for any roots that have an undefined latest relevant revision.
     * This occurs when the detached field index is loaded from a summary and can only be called once after
     * the summary has been loaded.
     */
    setRevisionsForLoadedData(latestRevision) {
        (0, internal_1.assert)(!this.isFullyLoaded, 0x9bd /* revisions should only be set once using this function after loading data from a summary */);
        const newDetachedNodeToField = new Map();
        const rootMap = new Map();
        (0, index_js_2.forEachInNestedMap)(this.detachedNodeToField, ({ root }, major, minor) => {
            (0, index_js_2.setInNestedMap)(newDetachedNodeToField, major, minor, {
                root,
                latestRelevantRevision: latestRevision,
            });
            rootMap.set(root, { major, minor });
        });
        this.detachedNodeToField = newDetachedNodeToField;
        this.latestRelevantRevisionToFields.delete(undefined);
        this.latestRelevantRevisionToFields.set(latestRevision, rootMap);
        this.isFullyLoaded = true;
    }
}
exports.DetachedFieldIndex = DetachedFieldIndex;
//# sourceMappingURL=detachedFieldIndex.js.map