/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _LazyTreeNode_removeDeleteCallback, _LazyTreeNode_anchorNode;
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { mapCursorFields, anchorSlot, inCursorField, rootFieldKey, EmptyKey, forEachField, } from "../../core";
import { capitalize, disposeSymbol, fail, getOrCreate } from "../../util";
import { TreeFieldSchema, schemaIsFieldNode, schemaIsLeaf, schemaIsMap, schemaIsObjectNode as schemaIsObjectNode, Any, } from "../typed-schema";
import { FieldKinds } from "../default-field-kinds";
import { boxedIterator, TreeStatus, } from "./editableTreeTypes";
import { LazyNodeKeyField, makeField } from "./lazyField";
import { LazyEntity, cursorSymbol, forgetAnchorSymbol, isFreedSymbol, makePropertyEnumerableOwn, tryMoveCursorToAnchorSymbol, } from "./lazyEntity";
import { unboxedField } from "./unboxed";
import { treeStatusFromAnchorCache } from "./utilities";
const lazyTreeSlot = anchorSlot();
export function makeTree(context, cursor) {
    const anchor = cursor.buildAnchor();
    const anchorNode = context.forest.anchors.locate(anchor) ??
        fail("cursor should point to a node that is not the root of the AnchorSet");
    const cached = anchorNode.slots.get(lazyTreeSlot);
    if (cached !== undefined) {
        context.forest.anchors.forget(anchor);
        assert(cached.context === context, 0x782 /* contexts must match */);
        return cached;
    }
    const schema = context.schema.treeSchema.get(cursor.type) ?? fail("missing schema");
    const output = buildSubclass(context, schema, cursor, anchorNode, anchor);
    anchorNode.slots.set(lazyTreeSlot, output);
    anchorNode.on("afterDestroy", cleanupTree);
    return output;
}
function cleanupTree(anchor) {
    const cached = anchor.slots.get(lazyTreeSlot) ?? fail("tree should only be cleaned up once");
    cached[disposeSymbol]();
}
function buildSubclass(context, schema, cursor, anchorNode, anchor) {
    if (schemaIsMap(schema)) {
        return new LazyMap(context, schema, cursor, anchorNode, anchor);
    }
    if (schemaIsLeaf(schema)) {
        return new LazyLeaf(context, schema, cursor, anchorNode, anchor);
    }
    if (schemaIsFieldNode(schema)) {
        return new LazyFieldNode(context, schema, cursor, anchorNode, anchor);
    }
    if (schemaIsObjectNode(schema)) {
        return buildLazyObjectNode(context, schema, cursor, anchorNode, anchor);
    }
    fail("unrecognized node kind");
}
/**
 * Lazy implementation of {@link TreeNode}.
 */
export class LazyTreeNode extends LazyEntity {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchor);
        // Using JS private here prevents it from showing up as a enumerable own property, or conflicting with struct fields.
        _LazyTreeNode_removeDeleteCallback.set(this, void 0);
        _LazyTreeNode_anchorNode.set(this, void 0);
        __classPrivateFieldSet(this, _LazyTreeNode_anchorNode, anchorNode, "f");
        assert(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x783 /* must be in nodes mode */);
        anchorNode.slots.set(lazyTreeSlot, this);
        __classPrivateFieldSet(this, _LazyTreeNode_removeDeleteCallback, anchorNode.on("afterDestroy", cleanupTree), "f");
        assert(this.context.schema.treeSchema.get(this.schema.name) !== undefined, 0x784 /* There is no explicit schema for this node type. Ensure that the type is correct and the schema for it was added to the TreeStoredSchema */);
        // Setup JS Object API:
        // makePrivatePropertyNotEnumerable(this, "removeDeleteCallback");
        // makePrivatePropertyNotEnumerable(this, "anchorNode");
        this.type = schema.name;
    }
    is(schema) {
        assert(this.context.schema.treeSchema.get(schema.name) === schema, 0x785 /* Narrowing must be done to a schema that exists in this context */);
        return this.schema === schema;
    }
    [(_LazyTreeNode_removeDeleteCallback = new WeakMap(), _LazyTreeNode_anchorNode = new WeakMap(), tryMoveCursorToAnchorSymbol)](anchor, cursor) {
        return this.context.forest.tryMoveCursorToNode(anchor, cursor);
    }
    [forgetAnchorSymbol](anchor) {
        // This type unconditionally has an anchor, so `forgetAnchor` is always called and cleanup can be done here:
        // After this point this node will not be usable,
        // so remove it from the anchor incase a different context (or the same context later) uses this AnchorSet.
        __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").slots.delete(lazyTreeSlot);
        __classPrivateFieldGet(this, _LazyTreeNode_removeDeleteCallback, "f").call(this);
        this.context.forest.anchors.forget(anchor);
    }
    get value() {
        return this[cursorSymbol].value;
    }
    tryGetField(fieldKey) {
        const schema = getFieldSchema(fieldKey, this.schema);
        return inCursorField(this[cursorSymbol], fieldKey, (cursor) => {
            if (cursor.getFieldLength() === 0) {
                return undefined;
            }
            return makeField(this.context, schema, cursor);
        });
    }
    [boxedIterator]() {
        return mapCursorFields(this[cursorSymbol], (cursor) => makeField(this.context, getFieldSchema(cursor.getFieldKey(), this.schema), cursor)).values();
    }
    get parentField() {
        const cursor = this[cursorSymbol];
        const index = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").parentIndex;
        assert(cursor.fieldIndex === index, 0x786 /* mismatched indexes */);
        const key = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").parentField;
        cursor.exitNode();
        assert(key === cursor.getFieldKey(), 0x787 /* mismatched keys */);
        let fieldSchema;
        // Check if the current node is in a detached sequence.
        if (__classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").parent === undefined) {
            // Parent field is a detached sequence, and thus needs special handling for its schema.
            // eslint-disable-next-line unicorn/prefer-ternary
            if (key === rootFieldKey) {
                fieldSchema = this.context.schema.rootFieldSchema;
            }
            else {
                // All fields (in the editable tree API) have a schema.
                // Since currently there is no known schema for detached field other than the special default root:
                // give all other detached fields a schema of sequence of any.
                // That schema is the only one that is safe since its the only field schema that allows any possible field content.
                //
                // TODO:
                // if any of the following are done this schema will need to be more specific:
                // 1. Editing APIs start exposing user created detached sequences.
                // 2. Remove (and its inverse) start working on subsequences or fields contents (like everything in a sequence or optional field) and not just single nodes.
                // 3. Possibly other unknown cases.
                // Additionally this approach makes it possible for a user to take an EditableTree node, get its parent, check its schema, down cast based on that, then edit that detached field (ex: removing the node in it).
                // This MIGHT work properly with existing merge resolution logic (it must keep client in sync and be unable to violate schema), but this either needs robust testing or to be explicitly banned (error before s3ending the op).
                // Issues like replacing a node in the a removed sequenced then undoing the remove could easily violate schema if not everything works exactly right!
                fieldSchema = TreeFieldSchema.create(FieldKinds.sequence, [Any]);
            }
        }
        else {
            cursor.exitField();
            const parentType = cursor.type;
            cursor.enterField(key);
            fieldSchema = getFieldSchema(key, this.context.schema.treeSchema.get(parentType) ??
                fail("requested schema that does not exist"));
        }
        const proxifiedField = makeField(this.context, fieldSchema, cursor);
        cursor.enterNode(index);
        return { parent: proxifiedField, index };
    }
    treeStatus() {
        if (this[isFreedSymbol]()) {
            return TreeStatus.Deleted;
        }
        return treeStatusFromAnchorCache(this.context.forest.anchors, __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f"));
    }
    on(eventName, listener) {
        switch (eventName) {
            case "changing": {
                const unsubscribeFromChildrenChange = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").on("childrenChanging", (anchorNode) => 
                // Ugly casting workaround because I can't figure out how to make TS understand that in this case block
                // the listener argument only needs to be an AnchorNode. Should go away if/when we make the listener signature
                // for changing and subtreeChanging match the one for beforeChange and afterChange.
                listener(anchorNode));
                return unsubscribeFromChildrenChange;
            }
            case "subtreeChanging": {
                const unsubscribeFromSubtreeChange = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").on("subtreeChanging", (anchorNode) => 
                // Ugly casting workaround because I can't figure out how to make TS understand that in this case block
                // the listener argument only needs to be an AnchorNode. Should go away if/when we make the listener signature
                // for changing and subtreeChanging match the one for beforeChange and afterChange.
                listener(anchorNode));
                return unsubscribeFromSubtreeChange;
            }
            case "beforeChange": {
                const unsubscribeFromChildrenBeforeChange = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").on("beforeChange", (anchorNode) => {
                    const treeNode = anchorNode.slots.get(lazyTreeSlot);
                    assert(treeNode !== undefined, 0x7d3 /* tree node not found in anchor node slots */);
                    // Ugly casting workaround because I can't figure out how to make TS understand that in this case block
                    // the listener argument only needs to be a TreeEvent. Should go away if/when we make the listener signature
                    // for changing and subtreeChanging match the one for beforeChange and afterChange.
                    listener({ target: treeNode });
                });
                return unsubscribeFromChildrenBeforeChange;
            }
            case "afterChange": {
                const unsubscribeFromChildrenAfterChange = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").on("afterChange", (anchorNode) => {
                    const treeNode = anchorNode.slots.get(lazyTreeSlot);
                    assert(treeNode !== undefined, 0x7d4 /* tree node not found in anchor node slots */);
                    // Ugly casting workaround because I can't figure out how to make TS understand that in this case block
                    // the listener argument only needs to be a TreeEvent. Should go away if/when we make the listener signature
                    // for changing and subtreeChanging match the one for beforeChange and afterChange.
                    listener({ target: treeNode });
                });
                return unsubscribeFromChildrenAfterChange;
            }
            default:
                unreachableCase(eventName);
        }
    }
}
export class LazyMap extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
        // Setup JS Object API:
        makePropertyEnumerableOwn(this, "asObject", LazyMap.prototype);
    }
    get size() {
        let fieldCount = 0;
        forEachField(this[cursorSymbol], () => (fieldCount += 1));
        return fieldCount;
    }
    keys() {
        return mapCursorFields(this[cursorSymbol], (cursor) => cursor.getFieldKey()).values();
    }
    values() {
        return mapCursorFields(this[cursorSymbol], (cursor) => unboxedField(this.context, this.schema.mapFields, cursor)).values();
    }
    entries() {
        return mapCursorFields(this[cursorSymbol], (cursor) => {
            const entry = [
                cursor.getFieldKey(),
                unboxedField(this.context, this.schema.mapFields, cursor),
            ];
            return entry;
        }).values();
    }
    forEach(callbackFn, thisArg) {
        const fn = thisArg !== undefined ? callbackFn.bind(thisArg) : callbackFn;
        for (const [key, value] of this.entries()) {
            fn(value, key, this);
        }
    }
    has(key) {
        return this.tryGetField(key) !== undefined;
    }
    get(key) {
        return inCursorField(this[cursorSymbol], key, (cursor) => unboxedField(this.context, this.schema.mapFields, cursor));
    }
    getBoxed(key) {
        return inCursorField(this[cursorSymbol], key, (cursor) => makeField(this.context, this.schema.mapFields, cursor));
    }
    // TODO: when appropriate add setter that delegates to field kind specific setter.
    // public set(key: FieldKey, content: FlexibleFieldContent<TSchema["mapFields"]>): void {
    // 	const field = this.get(key);
    // 	if (field.is(SchemaBuilder.optional(this.schema.mapFields.allowedTypes))) {
    // 		field.setContent(content);
    // 	} else {
    // 		assert(
    // 			field.is(SchemaBuilder.sequence(this.schema.mapFields.allowedTypes)),
    // 			"unexpected map field kind",
    // 		);
    // 		// TODO: fix merge semantics.
    // 		field.replaceRange(0, field.length, content as Iterable<ContextuallyTypedNodeData>);
    // 	}
    // }
    [boxedIterator]() {
        return super[boxedIterator]();
    }
    [Symbol.iterator]() {
        return this.entries();
    }
    get asObject() {
        const record = Object.create(null);
        forEachField(this[cursorSymbol], (cursor) => {
            Object.defineProperty(record, cursor.getFieldKey(), {
                value: unboxedField(this.context, this.schema.mapFields, cursor),
                configurable: true,
                enumerable: true,
            });
        });
        return record;
    }
}
export class LazyLeaf extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
        // Setup JS Object API:
        makePropertyEnumerableOwn(this, "value", LazyTreeNode.prototype);
    }
    get value() {
        return super.value;
    }
}
export class LazyFieldNode extends LazyTreeNode {
    get content() {
        return inCursorField(this[cursorSymbol], EmptyKey, (cursor) => unboxedField(this.context, this.schema.objectNodeFields.get(EmptyKey) ?? fail("missing field schema"), cursor));
    }
    get boxedContent() {
        return inCursorField(this[cursorSymbol], EmptyKey, (cursor) => makeField(this.context, this.schema.objectNodeFields.get(EmptyKey) ?? fail("missing field schema"), cursor));
    }
}
export class LazyObjectNode extends LazyTreeNode {
    get localNodeKey() {
        // TODO: Optimize this to be in the derived class so it can cache schema lookup.
        // TODO: Optimize this to avoid allocating the field object.
        const key = this.context.nodeKeyFieldKey;
        const fieldSchema = this.schema.objectNodeFields.get(key);
        if (fieldSchema === undefined) {
            return undefined;
        }
        const field = this.tryGetField(key);
        assert(field instanceof LazyNodeKeyField, 0x7b4 /* unexpected node key field */);
        // TODO: ideally we would do something like this, but that adds dependencies we can't have here:
        // assert(
        // 	field.is(TreeFieldSchema.create(FieldKinds.nodeKey, [nodeKeyTreeSchema])),
        // 	"invalid node key field",
        // );
        if (this.context.nodeKeyFieldKey === undefined) {
            return undefined;
        }
        return field.localNodeKey;
    }
}
export function buildLazyObjectNode(context, schema, cursor, anchorNode, anchor) {
    const objectNodeClass = getOrCreate(cachedStructClasses, schema, () => buildStructClass(schema));
    return new objectNodeClass(context, cursor, anchorNode, anchor);
}
const cachedStructClasses = new WeakMap();
export function getBoxedField(objectNode, key, fieldSchema) {
    return inCursorField(objectNode[cursorSymbol], key, (cursor) => {
        return makeField(objectNode.context, fieldSchema, cursor);
    });
}
function buildStructClass(schema) {
    const propertyDescriptorMap = {};
    const ownPropertyMap = {};
    for (const [key, fieldSchema] of schema.objectNodeFields) {
        let setter;
        switch (fieldSchema.kind) {
            case FieldKinds.optional: {
                setter = function (newContent) {
                    const field = getBoxedField(this, key, fieldSchema);
                    field.content = newContent;
                };
                break;
            }
            case FieldKinds.required: {
                setter = function (newContent) {
                    const field = getBoxedField(this, key, fieldSchema);
                    field.content = newContent;
                };
                break;
            }
            default:
                setter = undefined;
                break;
        }
        // Create getter and setter (when appropriate) for property
        ownPropertyMap[key] = {
            enumerable: true,
            get() {
                return inCursorField(this[cursorSymbol], key, (cursor) => unboxedField(this.context, fieldSchema, cursor));
            },
            set: setter,
        };
        // Create set method for property (when appropriate)
        if (setter !== undefined) {
            propertyDescriptorMap[`set${capitalize(key)}`] = {
                enumerable: false,
                get() {
                    return setter;
                },
            };
        }
        propertyDescriptorMap[`boxed${capitalize(key)}`] = {
            enumerable: false,
            get() {
                return getBoxedField(this, key, fieldSchema);
            },
        };
    }
    // This must implement `StructTyped<TSchema>`, but TypeScript can't constrain it to do so.
    class CustomStruct extends LazyObjectNode {
        constructor(context, cursor, anchorNode, anchor) {
            super(context, schema, cursor, anchorNode, anchor);
            Object.defineProperties(this, ownPropertyMap);
        }
    }
    Object.defineProperties(CustomStruct.prototype, propertyDescriptorMap);
    return CustomStruct;
}
export function getFieldSchema(field, schema) {
    return schema.objectNodeFields.get(field) ?? schema.mapFields ?? TreeFieldSchema.empty;
}
//# sourceMappingURL=lazyTree.js.map