"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var _a, _b;
Object.defineProperty(exports, "__esModule", { value: true });
exports.unsupportedUsageError = exports.tryUnhydratedFlexTreeNode = exports.UnhydratedTreeSequenceField = exports.UnhydratedContext = exports.UnhydratedFlexTreeNode = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
const index_js_3 = require("../../feature-libraries/index.js");
/**
 * An unhydrated implementation of {@link FlexTreeNode} which wraps a {@link MapTree}.
 * @remarks
 * MapTreeNodes are unconditionally cached -
 * when retrieved via {@link getOrCreateNode}, the same {@link MapTree} object will always produce the same `UnhydratedFlexTreeNode` object.
 *
 * Create a `UnhydratedFlexTreeNode` by calling {@link getOrCreate}.
 */
class UnhydratedFlexTreeNode {
    get schema() {
        return this.mapTree.type;
    }
    get storedSchema() {
        return this.context.schema.nodeSchema.get(this.mapTree.type) ?? (0, index_js_2.fail)("missing schema");
    }
    get events() {
        return this._events;
    }
    /**
     * Create a {@link UnhydratedFlexTreeNode} that wraps the given {@link MapTree}, or get the node that already exists for that {@link MapTree} if there is one.
     * @param nodeSchema - the {@link FlexTreeNodeSchema | schema} that the node conforms to
     * @param mapTree - the {@link MapTree} containing the data for this node.
     * @remarks It must conform to the `nodeSchema`.
     */
    static getOrCreate(context, mapTree) {
        return nodeCache.get(mapTree) ?? new UnhydratedFlexTreeNode(context, mapTree, undefined);
    }
    get context() {
        return this.simpleContext.flexContext;
    }
    /**
     * Create a new UnhydratedFlexTreeNode.
     * @param location - the parentage of this node, if it is being created underneath an existing node and field, or undefined if not
     * @remarks This class (and its subclasses) should not be directly constructed outside of this module.
     * Instead, use {@link getOrCreateNode} to create a UnhydratedFlexTreeNode from a {@link MapTree}.
     * A `UnhydratedFlexTreeNode` may never be constructed more than once for the same {@link MapTree} object.
     * Instead, it should always be acquired via {@link getOrCreateNode}.
     */
    constructor(simpleContext, 
    /** The underlying {@link MapTree} that this `UnhydratedFlexTreeNode` reads its data from */
    mapTree, location = unparentedLocation) {
        this.simpleContext = simpleContext;
        this.mapTree = mapTree;
        this.location = location;
        this[_a] = index_js_3.FlexTreeEntityKind.Node;
        this._events = (0, client_utils_1.createEmitter)();
        (0, internal_1.assert)(!nodeCache.has(mapTree), 0x98b /* A node already exists for the given MapTree */);
        nodeCache.set(mapTree, this);
        // Fully demand the tree to ensure that parent pointers are present and accurate on all nodes.
        // When a UnhydratedFlexTreeNode is constructed, its MapTree may contain nodes (anywhere below) that map (via the `nodeCache`) to pre-existing UnhydratedFlexTreeNodes.
        // Put another way, for a given MapTree, some ancestor UnhydratedFlexTreeNode can be created after any number of its descendant UnhydratedFlexTreeNodes already exist.
        // In such a case, the spine of nodes between the descendant and ancestor need to exist in order for the ancestor to be able to walk upwards via the `parentField` property.
        // This needs to happen for all UnhydratedFlexTreeNodes that are descendants of the ancestor UnhydratedFlexTreeNode.
        // Demanding the entire tree is overkill to solve this problem since not all descendant MapTree nodes will have corresponding UnhydratedFlexTreeNodes.
        // However, demanding the full tree also lets us eagerly validate that there are no duplicate MapTrees (i.e. same MapTree object) anywhere in the tree.
        this.walkTree();
    }
    get type() {
        return this.mapTree.type;
    }
    adoptBy(parent, index) {
        if (parent !== undefined) {
            (0, internal_1.assert)(this.location === unparentedLocation, 0x98c /* Node may not be adopted if it already has a parent */);
            (0, internal_1.assert)(index !== undefined, 0xa08 /* Expected index */);
            this.location = { parent, index };
        }
        else {
            (0, internal_1.assert)(this.location !== unparentedLocation, 0xa09 /* Node may not be un-adopted if it does not have a parent */);
            this.location = unparentedLocation;
        }
    }
    /**
     * The field this tree is in, and the index within that field.
     * @remarks If this node is unparented, this method will return the special {@link unparentedLocation} as the parent.
     */
    get parentField() {
        return this.location;
    }
    borrowCursor() {
        return (0, index_js_3.cursorForMapTreeNode)(this.mapTree);
    }
    tryGetField(key) {
        const field = this.mapTree.fields.get(key);
        // Only return the field if it is not empty, in order to fulfill the contract of `tryGetField`.
        if (field !== undefined && field.length > 0) {
            return getOrCreateField(this, key, this.storedSchema.getFieldSchema(key).kind, () => this.emitChangedEvent(key));
        }
    }
    getBoxed(key) {
        const fieldKey = (0, index_js_2.brand)(key);
        return getOrCreateField(this, fieldKey, this.storedSchema.getFieldSchema(fieldKey).kind, () => this.emitChangedEvent(fieldKey));
    }
    boxedIterator() {
        return (0, index_js_2.mapIterable)(this.mapTree.fields.entries(), ([key]) => getOrCreateField(this, key, this.storedSchema.getFieldSchema(key).kind, () => this.emitChangedEvent(key)));
    }
    keys() {
        // TODO: how this should handle missing defaults (and empty keys if they end up being allowed) needs to be determined.
        return this.mapTree.fields.keys();
    }
    get value() {
        return this.mapTree.value;
    }
    get anchorNode() {
        // This API is relevant to `LazyTreeNode`s, but not `UnhydratedFlexTreeNode`s.
        // TODO: Refactor the FlexTreeNode interface so that stubbing this out isn't necessary.
        return (0, index_js_2.fail)("UnhydratedFlexTreeNode does not implement anchorNode");
    }
    walkTree() {
        for (const [key, mapTrees] of this.mapTree.fields) {
            const field = getOrCreateField(this, key, this.storedSchema.getFieldSchema(key).kind, () => this.emitChangedEvent(key));
            for (let index = 0; index < field.length; index++) {
                const child = getOrCreateChild(this.simpleContext, mapTrees[index] ?? (0, internal_1.oob)(), {
                    parent: field,
                    index,
                });
                // These next asserts detect the case where `getOrCreateChild` gets a cache hit of a different node than the one we're trying to create
                (0, internal_1.assert)(child.location !== undefined, 0x98d /* Expected node to have parent */);
                (0, internal_1.assert)(child.location.parent.parent === this, 0x98e /* Node may not be multi-parented */);
                (0, internal_1.assert)(child.location.index === index, 0x98f /* Node may not be multi-parented */);
                child.walkTree();
            }
        }
    }
    emitChangedEvent(key) {
        this._events.emit("childrenChangedAfterBatch", { changedFields: new Set([key]) });
    }
}
exports.UnhydratedFlexTreeNode = UnhydratedFlexTreeNode;
_a = index_js_3.flexTreeMarker;
/**
 * Implementation of `FlexTreeContext`.
 *
 * @remarks An editor is required to edit the FlexTree.
 */
class UnhydratedContext {
    /**
     * @param flexSchema - Schema to use when working with the tree.
     */
    constructor(schemaPolicy, schema) {
        this.schemaPolicy = schemaPolicy;
        this.schema = schema;
    }
    isHydrated() {
        return false;
    }
}
exports.UnhydratedContext = UnhydratedContext;
// #region Fields
/**
 * A special singleton that is the implicit {@link LocationInField} of all un-parented {@link UnhydratedFlexTreeNode}s.
 * @remarks This exists because {@link UnhydratedFlexTreeNode.parentField} must return a field.
 * If a {@link UnhydratedFlexTreeNode} is created without a parent, its {@link UnhydratedFlexTreeNode.parentField} property will point to this object.
 * However, this field cannot be used in any practical way because it is empty, i.e. it does not actually contain the children that claim to be parented under it.
 * It has the "empty" schema and it will always contain zero children if queried.
 * Any nodes with this location will have a dummy parent index of `-1`.
 */
const unparentedLocation = {
    parent: {
        [index_js_3.flexTreeMarker]: index_js_3.FlexTreeEntityKind.Field,
        length: 0,
        key: index_js_1.EmptyKey,
        parent: undefined,
        is(kind) {
            return this.schema === kind.identifier;
        },
        boxedIterator() {
            return [].values();
        },
        boxedAt(index) {
            return undefined;
        },
        schema: (0, index_js_2.brand)(index_js_1.forbiddenFieldKindIdentifier),
        get context() {
            return (0, index_js_2.fail)("unsupported");
        },
        getFieldPath() {
            (0, index_js_2.fail)("unsupported");
        },
    },
    index: -1,
};
class UnhydratedFlexTreeField {
    get context() {
        return this.simpleContext.flexContext;
    }
    constructor(simpleContext, schema, key, parent, onEdit) {
        this.simpleContext = simpleContext;
        this.schema = schema;
        this.key = key;
        this.parent = parent;
        this.onEdit = onEdit;
        this[_b] = index_js_3.FlexTreeEntityKind.Field;
        const fieldKeyCache = getFieldKeyCache(parent);
        (0, internal_1.assert)(!fieldKeyCache.has(key), 0x990 /* A field already exists for the given MapTrees */);
        fieldKeyCache.set(key, this);
        // When this field is created (which only happens one time, because it is cached), all the children become parented for the first time.
        // "Adopt" each child by updating its parent information to point to this field.
        for (const [i, mapTree] of this.mapTrees.entries()) {
            const mapTreeNodeChild = nodeCache.get(mapTree);
            if (mapTreeNodeChild !== undefined) {
                if (mapTreeNodeChild.parentField !== unparentedLocation) {
                    throw new internal_2.UsageError("A node may not be in more than one place in the tree");
                }
                mapTreeNodeChild.adoptBy(this, i);
            }
        }
    }
    get mapTrees() {
        return this.parent.mapTree.fields.get(this.key) ?? [];
    }
    get length() {
        return this.mapTrees.length;
    }
    is(kind) {
        return this.schema === kind.identifier;
    }
    boxedIterator() {
        return this.mapTrees
            .map((m, index) => getOrCreateChild(this.simpleContext, m, {
            parent: this,
            index,
        }))
            .values();
    }
    boxedAt(index) {
        const i = (0, index_js_3.indexForAt)(index, this.length);
        if (i === undefined) {
            return undefined;
        }
        const m = this.mapTrees[i];
        if (m !== undefined) {
            return getOrCreateChild(this.simpleContext, m, {
                parent: this,
                index: i,
            });
        }
    }
    /**
     * Mutate this field.
     * @param edit - A function which receives the current `MapTree`s that comprise the contents of the field so that it may be mutated.
     * The function may mutate the array in place or return a new array.
     * If a new array is returned then it will be used as the new contents of the field, otherwise the original array will be continue to be used.
     * @remarks All edits to the field (i.e. mutations of the field's MapTrees) should be directed through this function.
     * This function ensures that the parent MapTree has no empty fields (which is an invariant of `MapTree`) after the mutation.
     */
    edit(edit) {
        const oldMapTrees = this.parent.mapTree.fields.get(this.key) ?? [];
        const newMapTrees = edit(oldMapTrees) ?? oldMapTrees;
        if (newMapTrees.length > 0) {
            this.parent.mapTree.fields.set(this.key, newMapTrees);
        }
        else {
            this.parent.mapTree.fields.delete(this.key);
        }
        this.onEdit?.();
    }
    getFieldPath() {
        throw unsupportedUsageError("Editing an array");
    }
    /** Unboxes leaf nodes to their values */
    unboxed(index) {
        const mapTree = this.mapTrees[index] ?? (0, internal_1.oob)();
        const value = mapTree.value;
        if (value !== undefined) {
            return value;
        }
        return getOrCreateChild(this.simpleContext, mapTree, { parent: this, index });
    }
}
_b = index_js_3.flexTreeMarker;
class EagerMapTreeOptionalField extends UnhydratedFlexTreeField {
    constructor() {
        super(...arguments);
        this.editor = {
            set: (newContent) => {
                // If the new content is a UnhydratedFlexTreeNode, it needs to have its parent pointer updated
                if (newContent !== undefined) {
                    nodeCache.get(newContent)?.adoptBy(this, 0);
                }
                // If the old content is a UnhydratedFlexTreeNode, it needs to have its parent pointer unset
                const oldContent = this.mapTrees[0];
                if (oldContent !== undefined) {
                    nodeCache.get(oldContent)?.adoptBy(undefined);
                }
                this.edit((mapTrees) => {
                    if (newContent !== undefined) {
                        mapTrees[0] = newContent;
                    }
                    else {
                        mapTrees.length = 0;
                    }
                });
            },
        };
    }
    get content() {
        const value = this.mapTrees[0];
        if (value !== undefined) {
            return this.unboxed(0);
        }
        return undefined;
    }
}
class EagerMapTreeRequiredField extends EagerMapTreeOptionalField {
    get content() {
        // This cannot use ?? since null is a legal value here.
        (0, internal_1.assert)(super.content !== undefined, 0xa57 /* Expected EagerMapTree required field to have a value */);
        return super.content;
    }
}
class UnhydratedTreeSequenceField extends UnhydratedFlexTreeField {
    constructor() {
        super(...arguments);
        this.editor = {
            insert: (index, newContent) => {
                for (let i = 0; i < newContent.length; i++) {
                    const c = newContent[i];
                    (0, internal_1.assert)(c !== undefined, 0xa0a /* Unexpected sparse array content */);
                    nodeCache.get(c)?.adoptBy(this, index + i);
                }
                this.edit((mapTrees) => {
                    if (newContent.length < 1000) {
                        // For "smallish arrays" (`1000` is not empirically derived), the `splice` function is appropriate...
                        mapTrees.splice(index, 0, ...newContent);
                    }
                    else {
                        // ...but we avoid using `splice` + spread for very large input arrays since there is a limit on how many elements can be spread (too many will overflow the stack).
                        return mapTrees.slice(0, index).concat(newContent, mapTrees.slice(index));
                    }
                });
            },
            remove: (index, count) => {
                for (let i = index; i < index + count; i++) {
                    const c = this.mapTrees[i];
                    (0, internal_1.assert)(c !== undefined, 0xa0b /* Unexpected sparse array */);
                    nodeCache.get(c)?.adoptBy(undefined);
                }
                let removed;
                this.edit((mapTrees) => {
                    removed = mapTrees.splice(index, count);
                });
                return removed ?? (0, index_js_2.fail)("Expected removed to be set by edit");
            },
        };
    }
    at(index) {
        const i = (0, index_js_3.indexForAt)(index, this.length);
        if (i === undefined) {
            return undefined;
        }
        return this.unboxed(i);
    }
    map(callbackfn) {
        return Array.from(this, callbackfn);
    }
    *[Symbol.iterator]() {
        for (const [i] of this.mapTrees.entries()) {
            yield this.unboxed(i);
        }
    }
}
exports.UnhydratedTreeSequenceField = UnhydratedTreeSequenceField;
// #endregion Fields
// #region Caching and unboxing utilities
const nodeCache = new WeakMap();
/** Node Parent -\> Field Key -\> Field */
const fieldCache = new WeakMap();
function getFieldKeyCache(parent) {
    return (0, index_js_2.getOrCreate)(fieldCache, parent, () => new Map());
}
/**
 * If there exists a {@link UnhydratedFlexTreeNode} for the given {@link MapTree}, returns it, otherwise returns `undefined`.
 * @remarks {@link UnhydratedFlexTreeNode | UnhydratedFlexTreeNodes} are created via {@link getOrCreateNode}.
 */
function tryUnhydratedFlexTreeNode(mapTree) {
    return nodeCache.get(mapTree);
}
exports.tryUnhydratedFlexTreeNode = tryUnhydratedFlexTreeNode;
/** Helper for creating a `UnhydratedFlexTreeNode` given the parent field (e.g. when "walking down") */
function getOrCreateChild(context, mapTree, parent) {
    const cached = nodeCache.get(mapTree);
    if (cached !== undefined) {
        return cached;
    }
    return new UnhydratedFlexTreeNode(context, mapTree, parent);
}
/** Creates a field with the given attributes, or returns a cached field if there is one */
function getOrCreateField(parent, key, schema, onEdit) {
    const cached = getFieldKeyCache(parent).get(key);
    if (cached !== undefined) {
        return cached;
    }
    if (schema === index_js_3.FieldKinds.required.identifier ||
        schema === index_js_3.FieldKinds.identifier.identifier) {
        return new EagerMapTreeRequiredField(parent.simpleContext, schema, key, parent, onEdit);
    }
    if (schema === index_js_3.FieldKinds.optional.identifier) {
        return new EagerMapTreeOptionalField(parent.simpleContext, schema, key, parent, onEdit);
    }
    if (schema === index_js_3.FieldKinds.sequence.identifier) {
        return new UnhydratedTreeSequenceField(parent.simpleContext, schema, key, parent, onEdit);
    }
    return new UnhydratedFlexTreeField(parent.simpleContext, schema, key, parent, onEdit);
}
// #endregion Caching and unboxing utilities
function unsupportedUsageError(message) {
    return new internal_2.UsageError(`${message ?? "Operation"} is not supported for content that has not yet been inserted into the tree`);
}
exports.unsupportedUsageError = unsupportedUsageError;
//# sourceMappingURL=unhydratedFlexTree.js.map