/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var _a, _b;
import { assert } from "@fluidframework/core-utils/internal";
import { EmptyKey, } from "../../core/index.js";
import { brand, fail, getOrCreate, mapIterable } from "../../util/index.js";
import { FlexTreeEntityKind, TreeStatus, flexTreeMarker, indexForAt, } from "../flex-tree/index.js";
import { FlexFieldSchema, isLazy, schemaIsFieldNode, schemaIsLeaf, schemaIsMap, schemaIsObjectNode, } from "../typed-schema/index.js";
import { normalizeAllowedTypes } from "../schemaBuilderBase.js";
import { FieldKinds } from "../default-schema/index.js";
import { ComposableEventEmitter } from "../../events/index.js";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
/**
 * Checks if the given {@link FlexTreeNode} is a {@link MapTreeNode}.
 */
export function isMapTreeNode(flexNode) {
    return flexNode instanceof EagerMapTreeNode;
}
/**
 * Allows events to be forwarded to another event emitter.
 * @remarks TODO: After the eventing library is simplified, find a way to support this pattern elegantly in the library.
 */
class ForwardingEventEmitter extends ComposableEventEmitter {
    constructor() {
        super(...arguments);
        // A map from deregistration functions produced by this class to deregistration functions of Listenables that have been forwarded to
        this.forwardedOffs = new Map();
    }
    on(eventName, listener) {
        const off = super.on(eventName, listener);
        // Return a deregister function which...
        return () => {
            off(); // ...deregisters the event in this emitter,
            // and also deregisters the event in any Listenable that it gets forwarded to
            (this.forwardedOffs.get(off) ?? []).forEach((f) => f());
        };
    }
    forwardEvents(to) {
        for (const [eventName, listeners] of this.listeners) {
            for (const [off, listener] of listeners) {
                // For every one of our listeners, make the same subscription in the Listenable that we're forwarding to,
                // and then create a mapping from our deregistration function to theirs, so we can call it later if need be.
                getOrCreate(this.forwardedOffs, off, () => []).push(to.on(eventName, listener));
            }
        }
    }
}
/**
 * A readonly implementation of {@link FlexTreeNode} which wraps a {@link MapTree}.
 * @remarks Any methods that would mutate the node will fail,
 * as will the querying of data specific to the {@link LazyTreeNode} implementation (e.g. {@link FlexTreeNode.context}).
 * MapTreeNodes are unconditionally cached -
 * when retrieved via {@link getOrCreateNode}, the same {@link MapTree} object will always produce the same `MapTreeNode` object.
 */
export class EagerMapTreeNode {
    forwardEvents(to) {
        this.events.forwardEvents(to);
    }
    /**
     * Create a new MapTreeNode.
     * @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 MapTreeNode from a {@link MapTree}.
     * A `MapTreeNode` may never be constructed more than once for the same {@link MapTree} object.
     * Instead, it should always be acquired via {@link getOrCreateNode}.
     */
    constructor(schema, 
    /** The underlying {@link MapTree} that this `MapTreeNode` reads its data from */
    mapTree, location) {
        this.schema = schema;
        this.mapTree = mapTree;
        this.location = location;
        this[_a] = FlexTreeEntityKind.Node;
        this.events = new ForwardingEventEmitter();
        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 MapTreeNode is constructed, its MapTree may contain nodes (anywhere below) that map (via the `nodeCache`) to pre-existing MapTreeNodes.
        // Put another way, for a given MapTree, some ancestor MapTreeNode can be created after any number of its descendant MapTreeNodes 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 MapTreeNodes that are descendants of the ancestor MapTreeNode.
        // Demanding the entire tree is overkill to solve this problem since not all descendant MapTree nodes will have corresponding MapTreeNodes.
        // 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.schema.name;
    }
    /**
     * Set this node's parentage (see {@link FlexTreeNode.parentField}).
     * @remarks A node may only be adopted to a new parent one time, and only if it was not constructed with a parent.
     */
    adopt(parent, index) {
        assert(this.location === undefined, 0x98c /* Node may not be adopted if it already has a parent */);
        this.location = { parent, index };
    }
    /**
     * 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 rootMapTreeField} as the parent.
     */
    get parentField() {
        if (this.location === undefined) {
            return {
                parent: rootMapTreeField,
                index: -1,
            };
        }
        return this.location;
    }
    is(schema) {
        return schema === this.schema;
    }
    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, field, this.schema.getFieldSchema(key));
        }
    }
    getBoxed(key) {
        const fieldKey = brand(key);
        const field = this.mapTree.fields.get(fieldKey) ?? [];
        return getOrCreateField(this, fieldKey, field, this.schema.getFieldSchema(fieldKey));
    }
    boxedIterator() {
        return mapIterable(this.mapTree.fields.entries(), ([key, field]) => getOrCreateField(this, key, field, this.schema.getFieldSchema(key)));
    }
    treeStatus() {
        return TreeStatus.New;
    }
    get value() {
        return this.mapTree.value;
    }
    on(eventName, listener) {
        switch (eventName) {
            case "nodeChanged":
            case "treeChanged":
                return this.events.on(eventName, listener);
            default:
                throw unsupportedUsageError(`Subscribing to ${eventName}`);
        }
    }
    get context() {
        // This API is relevant to `LazyTreeNode`s, but not `MapTreeNode`s.
        // TODO: Refactor the FlexTreeNode interface so that stubbing this out isn't necessary.
        return fail("MapTreeNode does not implement context");
    }
    get anchorNode() {
        // This API is relevant to `LazyTreeNode`s, but not `MapTreeNode`s.
        // TODO: Refactor the FlexTreeNode interface so that stubbing this out isn't necessary.
        return fail("MapTreeNode does not implement anchorNode");
    }
    walkTree() {
        for (const [key, mapTrees] of this.mapTree.fields) {
            const field = getOrCreateField(this, key, mapTrees, this.schema.getFieldSchema(key));
            for (let index = 0; index < field.length; index++) {
                const child = getOrCreateChild(mapTrees[index], this.schema.getFieldSchema(key).allowedTypes, { 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();
            }
        }
    }
}
_a = flexTreeMarker;
/**
 * The implementation of a field node created by {@link getOrCreateNode}.
 */
export class EagerMapTreeFieldNode extends EagerMapTreeNode {
    get content() {
        const field = this.tryGetField(EmptyKey);
        if (field === undefined) {
            return undefined;
        }
        return unboxedField(field, EmptyKey, this.mapTree, this);
    }
    get boxedContent() {
        const field = this.mapTree.fields.get(EmptyKey) ?? [];
        return getOrCreateField(this, EmptyKey, field, this.schema.info);
    }
    getBoxed(key) {
        return super.getBoxed(key);
    }
}
/**
 * The implementation of a map node created by {@link getOrCreateNode}.
 */
export class EagerMapTreeMapNode extends EagerMapTreeNode {
    get size() {
        return this.mapTree.fields.size;
    }
    has(key) {
        return this.tryGetField(brand(key)) !== undefined;
    }
    get(key) {
        const field = this.tryGetField(brand(key));
        if (field === undefined) {
            return undefined;
        }
        return unboxedField(field, brand(key), this.mapTree, this);
    }
    keys() {
        return this.mapTree.fields.keys();
    }
    values() {
        return mapIterable(this.mapTree.fields.keys(), (key) => unboxedField(this.tryGetField(key) ?? fail("Unexpected empty map field"), key, this.mapTree, this));
    }
    entries() {
        return mapIterable(this.mapTree.fields.keys(), (key) => [
            key,
            unboxedField(this.tryGetField(key) ?? fail("Unexpected empty map field"), key, this.mapTree, this),
        ]);
    }
    forEach(callbackFn, thisArg) {
        const fn = thisArg !== undefined ? callbackFn.bind(thisArg) : callbackFn;
        for (const [key, value] of this.entries()) {
            fn(value, key, this);
        }
    }
    getBoxed(key) {
        return super.getBoxed(key);
    }
    set(key, value) {
        // `MapTreeNode`s cannot be mutated
        throw unsupportedUsageError("Setting a map entry");
    }
    delete(key) {
        // `MapTreeNode`s cannot be mutated
        throw unsupportedUsageError("Deleting a map entry");
    }
    [Symbol.iterator]() {
        return this.entries();
    }
    boxedIterator() {
        return super.boxedIterator();
    }
}
class EagerMapTreeLeafNode extends EagerMapTreeNode {
    get value() {
        return super.value;
    }
}
// #endregion Nodes
// #region Fields
/**
 * A special singleton that is the implicit parent field of all un-parented {@link EagerMapTreeNode}s.
 * @remarks This exists because {@link EagerMapTreeNode.parentField} must return a field.
 * If a {@link EagerMapTreeNode} is created without a parent, its {@link EagerMapTreeNode.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.
 */
export const rootMapTreeField = {
    [flexTreeMarker]: FlexTreeEntityKind.Field,
    length: 0,
    key: EmptyKey,
    parent: undefined,
    is(schema) {
        return schema === FlexFieldSchema.empty;
    },
    isSameAs(other) {
        return other === this;
    },
    boxedIterator() {
        return [].values();
    },
    boxedAt(index) {
        return undefined;
    },
    schema: FlexFieldSchema.empty,
    get context() {
        return fail("MapTreeField does not implement context");
    },
    treeStatus() {
        return TreeStatus.New;
    },
    mapTrees: [],
};
class MapTreeField {
    constructor(schema, key, parent, mapTrees) {
        this.schema = schema;
        this.key = key;
        this.parent = parent;
        this.mapTrees = mapTrees;
        this[_b] = FlexTreeEntityKind.Field;
        assert(!fieldCache.has(mapTrees), 0x990 /* A field already exists for the given MapTrees */);
        fieldCache.set(mapTrees, 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 (let i = 0; i < mapTrees.length; i++) {
            const mapTreeNodeChild = nodeCache.get(mapTrees[i]);
            if (mapTreeNodeChild !== undefined) {
                assert(mapTreeNodeChild.parentField.parent === rootMapTreeField, 0x991 /* Node is already parented under a different field */);
                mapTreeNodeChild.adopt(this, i);
            }
        }
    }
    get length() {
        return this.mapTrees.length;
    }
    is(schema) {
        return this.schema.equals(schema);
    }
    isSameAs(other) {
        if (other.parent === this.parent && other.key === this.key) {
            assert(other === this, 0x992 /* Expected field to be cached */);
            return true;
        }
        return false;
    }
    boxedIterator() {
        return this.mapTrees
            .map((m, index) => getOrCreateChild(m, this.schema.allowedTypes, {
            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(m, this.schema.allowedTypes, {
                parent: this,
                index: i,
            });
        }
    }
    get context() {
        return fail("MapTreeField does not implement context");
    }
    treeStatus() {
        return TreeStatus.New;
    }
}
_b = flexTreeMarker;
class MapTreeRequiredField extends MapTreeField {
    get content() {
        return unboxedUnion(this.schema, this.mapTrees[0], { parent: this, index: 0 });
    }
    set content(_) {
        throw unsupportedUsageError("Setting an optional field");
    }
    get boxedContent() {
        return this.boxedAt(0) ?? fail("Required field must have exactly one node");
    }
}
class MapTreeOptionalField extends MapTreeField {
    get content() {
        return this.mapTrees.length > 0
            ? unboxedUnion(this.schema, this.mapTrees[0], { parent: this, index: 0 })
            : undefined;
    }
    set content(_) {
        throw unsupportedUsageError("Setting an optional field");
    }
    get boxedContent() {
        return this.boxedAt(0);
    }
}
class MapTreeSequenceField extends MapTreeField {
    at(index) {
        const i = indexForAt(index, this.length);
        if (i === undefined) {
            return undefined;
        }
        return unboxedUnion(this.schema, this.mapTrees[i], { parent: this, index: i });
    }
    map(callbackfn) {
        return Array.from(this, callbackfn);
    }
    mapBoxed(callbackfn) {
        return Array.from(this.boxedIterator(), callbackfn);
    }
    *[Symbol.iterator]() {
        for (let i = 0; i < this.mapTrees.length; i++) {
            yield unboxedUnion(this.schema, this.mapTrees[i], { parent: this, index: i });
        }
    }
    sequenceEditor() {
        throw unsupportedUsageError("Editing a sequence");
    }
    insertAt(index, value) {
        throw unsupportedUsageError("Editing a sequence");
    }
    insertAtStart(value) {
        throw unsupportedUsageError("Editing a sequence");
    }
    insertAtEnd(value) {
        throw unsupportedUsageError("Editing a sequence");
    }
    removeAt(index) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveToStart(sourceIndex, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveToEnd(sourceIndex, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveToIndex(index, sourceIndex, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveRangeToStart(sourceStart, sourceEnd, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveRangeToEnd(sourceStart, sourceEnd, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    moveRangeToIndex(index, sourceStart, sourceEnd, source) {
        throw unsupportedUsageError("Editing a sequence");
    }
    getFieldPath() {
        throw unsupportedUsageError("Editing a sequence");
    }
}
// #endregion Fields
// #region Caching and unboxing utilities
const nodeCache = new WeakMap();
const fieldCache = new WeakMap();
/**
 * If there exists a {@link EagerMapTreeNode} for the given {@link MapTree}, returns it, otherwise returns `undefined`.
 * @remarks {@link EagerMapTreeNode | MapTreeNodes} are created via {@link getOrCreateNode}.
 */
export function tryGetMapTreeNode(mapTree) {
    return nodeCache.get(mapTree);
}
/**
 * Create a {@link MapTreeNode} 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`.
 */
export function getOrCreateMapTreeNode(nodeSchema, mapTree) {
    return getOrCreateNode(nodeSchema, mapTree);
}
export function getOrCreateNode(nodeSchema, mapTree) {
    const cached = tryGetMapTreeNode(mapTree);
    if (cached !== undefined) {
        return cached;
    }
    return createNode(nodeSchema, mapTree, undefined);
}
/** Helper for creating a `MapTreeNode` given the parent field (e.g. when "walking down") */
function getOrCreateChild(mapTree, implicitAllowedTypes, parent) {
    const cached = nodeCache.get(mapTree);
    if (cached !== undefined) {
        return cached;
    }
    const allowedTypes = normalizeAllowedTypes(implicitAllowedTypes);
    const nodeSchema = allowedTypes
        .map((t) => (isLazy(t) ? t() : t))
        .find((t) => {
        assert(t !== "Any", 0x993 /* 'Any' type is not supported */);
        return t.name === mapTree.type;
    }) ?? fail("Unsupported node schema");
    return createNode(nodeSchema, mapTree, parent);
}
function createNode(nodeSchema, mapTree, parentField) {
    if (schemaIsLeaf(nodeSchema)) {
        return new EagerMapTreeLeafNode(nodeSchema, mapTree, parentField);
    }
    if (schemaIsMap(nodeSchema)) {
        return new EagerMapTreeMapNode(nodeSchema, mapTree, parentField);
    }
    if (schemaIsFieldNode(nodeSchema)) {
        return new EagerMapTreeFieldNode(nodeSchema, mapTree, parentField);
    }
    if (schemaIsObjectNode(nodeSchema)) {
        return new EagerMapTreeNode(nodeSchema, mapTree, parentField);
    }
    assert(false, 0x994 /* Unrecognized node kind */);
}
/** Creates a field with the given attributes, or returns a cached field if there is one */
function getOrCreateField(parent, key, mapTrees, schema) {
    const cached = fieldCache.get(mapTrees);
    if (cached !== undefined) {
        return cached;
    }
    if (schema.kind.identifier === FieldKinds.required.identifier ||
        schema.kind.identifier === FieldKinds.identifier.identifier) {
        return new MapTreeRequiredField(schema, key, parent, mapTrees);
    }
    if (schema.kind.identifier === FieldKinds.optional.identifier) {
        return new MapTreeOptionalField(schema, key, parent, mapTrees);
    }
    if (schema.kind.identifier === FieldKinds.sequence.identifier) {
        return new MapTreeSequenceField(schema, key, parent, mapTrees);
    }
    return new MapTreeField(schema, key, parent, mapTrees);
}
/** Unboxes non-polymorphic leaf nodes to their values, if applicable */
function unboxedUnion(schema, mapTree, parent) {
    const type = schema.monomorphicChildType;
    if (type !== undefined) {
        if (schemaIsLeaf(type)) {
            return mapTree.value;
        }
        return getOrCreateChild(mapTree, type, parent);
    }
    return getOrCreateChild(mapTree, schema.allowedTypes, parent);
}
/** Unboxes non-polymorphic required and optional fields holding leaf nodes to their values, if applicable */
function unboxedField(field, key, mapTree, parentNode) {
    const fieldSchema = field.schema;
    const mapTrees = mapTree.fields.get(key) ?? fail("Key does not exist in unhydrated map tree");
    if (fieldSchema.kind === FieldKinds.required) {
        return unboxedUnion(fieldSchema, mapTrees[0], {
            parent: field,
            index: 0,
        });
    }
    if (fieldSchema.kind === FieldKinds.optional) {
        return (mapTrees.length > 0
            ? unboxedUnion(fieldSchema, mapTrees[0], { parent: field, index: 0 })
            : undefined);
    }
    return getOrCreateField(parentNode, key, mapTrees, fieldSchema);
}
// #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=mapTreeNode.js.map