/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var _a, _b;
import { assert, oob } from "@fluidframework/core-utils/internal";
import { createEmitter } from "@fluid-internal/client-utils";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EmptyKey, forbiddenFieldKindIdentifier, } from "../../core/index.js";
import { brand, fail, getOrCreate, mapIterable } from "../../util/index.js";
import { FlexTreeEntityKind, flexTreeMarker, indexForAt, FieldKinds, cursorForMapTreeNode, } from "../../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}.
 */
export class UnhydratedFlexTreeNode {
    get schema() {
        return this.mapTree.type;
    }
    get storedSchema() {
        return this.context.schema.nodeSchema.get(this.mapTree.type) ?? 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] = FlexTreeEntityKind.Node;
        this._events = createEmitter();
        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) {
            assert(this.location === unparentedLocation, 0x98c /* Node may not be adopted if it already has a parent */);
            assert(index !== undefined, 0xa08 /* Expected index */);
            this.location = { parent, index };
        }
        else {
            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 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 = brand(key);
        return getOrCreateField(this, fieldKey, this.storedSchema.getFieldSchema(fieldKey).kind, () => this.emitChangedEvent(fieldKey));
    }
    boxedIterator() {
        return 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 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] ?? 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
                assert(child.location !== undefined, 0x98d /* Expected node to have parent */);
                assert(child.location.parent.parent === this, 0x98e /* Node may not be multi-parented */);
                assert(child.location.index === index, 0x98f /* Node may not be multi-parented */);
                child.walkTree();
            }
        }
    }
    emitChangedEvent(key) {
        this._events.emit("childrenChangedAfterBatch", { changedFields: new Set([key]) });
    }
}
_a = flexTreeMarker;
/**
 * Implementation of `FlexTreeContext`.
 *
 * @remarks An editor is required to edit the FlexTree.
 */
export class UnhydratedContext {
    /**
     * @param flexSchema - Schema to use when working with the tree.
     */
    constructor(schemaPolicy, schema) {
        this.schemaPolicy = schemaPolicy;
        this.schema = schema;
    }
    isHydrated() {
        return false;
    }
}
// #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: {
        [flexTreeMarker]: FlexTreeEntityKind.Field,
        length: 0,
        key: EmptyKey,
        parent: undefined,
        is(kind) {
            return this.schema === kind.identifier;
        },
        boxedIterator() {
            return [].values();
        },
        boxedAt(index) {
            return undefined;
        },
        schema: brand(forbiddenFieldKindIdentifier),
        get context() {
            return fail("unsupported");
        },
        getFieldPath() {
            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] = FlexTreeEntityKind.Field;
        const fieldKeyCache = getFieldKeyCache(parent);
        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 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 = 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] ?? oob();
        const value = mapTree.value;
        if (value !== undefined) {
            return value;
        }
        return getOrCreateChild(this.simpleContext, mapTree, { parent: this, index });
    }
}
_b = 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.
        assert(super.content !== undefined, 0xa57 /* Expected EagerMapTree required field to have a value */);
        return super.content;
    }
}
export class UnhydratedTreeSequenceField extends UnhydratedFlexTreeField {
    constructor() {
        super(...arguments);
        this.editor = {
            insert: (index, newContent) => {
                for (let i = 0; i < newContent.length; i++) {
                    const c = newContent[i];
                    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];
                    assert(c !== undefined, 0xa0b /* Unexpected sparse array */);
                    nodeCache.get(c)?.adoptBy(undefined);
                }
                let removed;
                this.edit((mapTrees) => {
                    removed = mapTrees.splice(index, count);
                });
                return removed ?? fail("Expected removed to be set by edit");
            },
        };
    }
    at(index) {
        const i = 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);
        }
    }
}
// #endregion Fields
// #region Caching and unboxing utilities
const nodeCache = new WeakMap();
/** Node Parent -\> Field Key -\> Field */
const fieldCache = new WeakMap();
function getFieldKeyCache(parent) {
    return 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}.
 */
export function tryUnhydratedFlexTreeNode(mapTree) {
    return nodeCache.get(mapTree);
}
/** 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 === FieldKinds.required.identifier ||
        schema === FieldKinds.identifier.identifier) {
        return new EagerMapTreeRequiredField(parent.simpleContext, schema, key, parent, onEdit);
    }
    if (schema === FieldKinds.optional.identifier) {
        return new EagerMapTreeOptionalField(parent.simpleContext, schema, key, parent, onEdit);
    }
    if (schema === 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
export function unsupportedUsageError(message) {
    return new UsageError(`${message ?? "Operation"} is not supported for content that has not yet been inserted into the tree`);
}
//# sourceMappingURL=unhydratedFlexTree.js.map