"use strict";
/*!
 * 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;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFieldSchema = exports.getBoxedField = exports.buildLazyObjectNode = exports.LazyObjectNode = exports.LazyFieldNode = exports.LazyLeaf = exports.LazyMap = exports.LazyTreeNode = exports.makeTree = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const typed_schema_1 = require("../typed-schema");
const default_field_kinds_1 = require("../default-field-kinds");
const editableTreeTypes_1 = require("./editableTreeTypes");
const lazyField_1 = require("./lazyField");
const lazyEntity_1 = require("./lazyEntity");
const unboxed_1 = require("./unboxed");
const utilities_1 = require("./utilities");
const lazyTreeSlot = (0, core_1.anchorSlot)();
function makeTree(context, cursor) {
    const anchor = cursor.buildAnchor();
    const anchorNode = context.forest.anchors.locate(anchor) ??
        (0, util_1.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);
        (0, core_utils_1.assert)(cached.context === context, 0x782 /* contexts must match */);
        return cached;
    }
    const schema = context.schema.treeSchema.get(cursor.type) ?? (0, util_1.fail)("missing schema");
    const output = buildSubclass(context, schema, cursor, anchorNode, anchor);
    anchorNode.slots.set(lazyTreeSlot, output);
    anchorNode.on("afterDestroy", cleanupTree);
    return output;
}
exports.makeTree = makeTree;
function cleanupTree(anchor) {
    const cached = anchor.slots.get(lazyTreeSlot) ?? (0, util_1.fail)("tree should only be cleaned up once");
    cached[util_1.disposeSymbol]();
}
function buildSubclass(context, schema, cursor, anchorNode, anchor) {
    if ((0, typed_schema_1.schemaIsMap)(schema)) {
        return new LazyMap(context, schema, cursor, anchorNode, anchor);
    }
    if ((0, typed_schema_1.schemaIsLeaf)(schema)) {
        return new LazyLeaf(context, schema, cursor, anchorNode, anchor);
    }
    if ((0, typed_schema_1.schemaIsFieldNode)(schema)) {
        return new LazyFieldNode(context, schema, cursor, anchorNode, anchor);
    }
    if ((0, typed_schema_1.schemaIsObjectNode)(schema)) {
        return buildLazyObjectNode(context, schema, cursor, anchorNode, anchor);
    }
    (0, util_1.fail)("unrecognized node kind");
}
/**
 * Lazy implementation of {@link TreeNode}.
 */
class LazyTreeNode extends lazyEntity_1.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");
        (0, core_utils_1.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");
        (0, core_utils_1.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) {
        (0, core_utils_1.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(), lazyEntity_1.tryMoveCursorToAnchorSymbol)](anchor, cursor) {
        return this.context.forest.tryMoveCursorToNode(anchor, cursor);
    }
    [lazyEntity_1.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[lazyEntity_1.cursorSymbol].value;
    }
    tryGetField(fieldKey) {
        const schema = getFieldSchema(fieldKey, this.schema);
        return (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], fieldKey, (cursor) => {
            if (cursor.getFieldLength() === 0) {
                return undefined;
            }
            return (0, lazyField_1.makeField)(this.context, schema, cursor);
        });
    }
    [editableTreeTypes_1.boxedIterator]() {
        return (0, core_1.mapCursorFields)(this[lazyEntity_1.cursorSymbol], (cursor) => (0, lazyField_1.makeField)(this.context, getFieldSchema(cursor.getFieldKey(), this.schema), cursor)).values();
    }
    get parentField() {
        const cursor = this[lazyEntity_1.cursorSymbol];
        const index = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").parentIndex;
        (0, core_utils_1.assert)(cursor.fieldIndex === index, 0x786 /* mismatched indexes */);
        const key = __classPrivateFieldGet(this, _LazyTreeNode_anchorNode, "f").parentField;
        cursor.exitNode();
        (0, core_utils_1.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 === core_1.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 = typed_schema_1.TreeFieldSchema.create(default_field_kinds_1.FieldKinds.sequence, [typed_schema_1.Any]);
            }
        }
        else {
            cursor.exitField();
            const parentType = cursor.type;
            cursor.enterField(key);
            fieldSchema = getFieldSchema(key, this.context.schema.treeSchema.get(parentType) ??
                (0, util_1.fail)("requested schema that does not exist"));
        }
        const proxifiedField = (0, lazyField_1.makeField)(this.context, fieldSchema, cursor);
        cursor.enterNode(index);
        return { parent: proxifiedField, index };
    }
    treeStatus() {
        if (this[lazyEntity_1.isFreedSymbol]()) {
            return editableTreeTypes_1.TreeStatus.Deleted;
        }
        return (0, utilities_1.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);
                    (0, core_utils_1.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);
                    (0, core_utils_1.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:
                (0, core_utils_1.unreachableCase)(eventName);
        }
    }
}
exports.LazyTreeNode = LazyTreeNode;
class LazyMap extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
        // Setup JS Object API:
        (0, lazyEntity_1.makePropertyEnumerableOwn)(this, "asObject", LazyMap.prototype);
    }
    get size() {
        let fieldCount = 0;
        (0, core_1.forEachField)(this[lazyEntity_1.cursorSymbol], () => (fieldCount += 1));
        return fieldCount;
    }
    keys() {
        return (0, core_1.mapCursorFields)(this[lazyEntity_1.cursorSymbol], (cursor) => cursor.getFieldKey()).values();
    }
    values() {
        return (0, core_1.mapCursorFields)(this[lazyEntity_1.cursorSymbol], (cursor) => (0, unboxed_1.unboxedField)(this.context, this.schema.mapFields, cursor)).values();
    }
    entries() {
        return (0, core_1.mapCursorFields)(this[lazyEntity_1.cursorSymbol], (cursor) => {
            const entry = [
                cursor.getFieldKey(),
                (0, unboxed_1.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 (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], key, (cursor) => (0, unboxed_1.unboxedField)(this.context, this.schema.mapFields, cursor));
    }
    getBoxed(key) {
        return (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], key, (cursor) => (0, lazyField_1.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>);
    // 	}
    // }
    [editableTreeTypes_1.boxedIterator]() {
        return super[editableTreeTypes_1.boxedIterator]();
    }
    [Symbol.iterator]() {
        return this.entries();
    }
    get asObject() {
        const record = Object.create(null);
        (0, core_1.forEachField)(this[lazyEntity_1.cursorSymbol], (cursor) => {
            Object.defineProperty(record, cursor.getFieldKey(), {
                value: (0, unboxed_1.unboxedField)(this.context, this.schema.mapFields, cursor),
                configurable: true,
                enumerable: true,
            });
        });
        return record;
    }
}
exports.LazyMap = LazyMap;
class LazyLeaf extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
        // Setup JS Object API:
        (0, lazyEntity_1.makePropertyEnumerableOwn)(this, "value", LazyTreeNode.prototype);
    }
    get value() {
        return super.value;
    }
}
exports.LazyLeaf = LazyLeaf;
class LazyFieldNode extends LazyTreeNode {
    get content() {
        return (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], core_1.EmptyKey, (cursor) => (0, unboxed_1.unboxedField)(this.context, this.schema.objectNodeFields.get(core_1.EmptyKey) ?? (0, util_1.fail)("missing field schema"), cursor));
    }
    get boxedContent() {
        return (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], core_1.EmptyKey, (cursor) => (0, lazyField_1.makeField)(this.context, this.schema.objectNodeFields.get(core_1.EmptyKey) ?? (0, util_1.fail)("missing field schema"), cursor));
    }
}
exports.LazyFieldNode = LazyFieldNode;
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);
        (0, core_utils_1.assert)(field instanceof lazyField_1.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;
    }
}
exports.LazyObjectNode = LazyObjectNode;
function buildLazyObjectNode(context, schema, cursor, anchorNode, anchor) {
    const objectNodeClass = (0, util_1.getOrCreate)(cachedStructClasses, schema, () => buildStructClass(schema));
    return new objectNodeClass(context, cursor, anchorNode, anchor);
}
exports.buildLazyObjectNode = buildLazyObjectNode;
const cachedStructClasses = new WeakMap();
function getBoxedField(objectNode, key, fieldSchema) {
    return (0, core_1.inCursorField)(objectNode[lazyEntity_1.cursorSymbol], key, (cursor) => {
        return (0, lazyField_1.makeField)(objectNode.context, fieldSchema, cursor);
    });
}
exports.getBoxedField = getBoxedField;
function buildStructClass(schema) {
    const propertyDescriptorMap = {};
    const ownPropertyMap = {};
    for (const [key, fieldSchema] of schema.objectNodeFields) {
        let setter;
        switch (fieldSchema.kind) {
            case default_field_kinds_1.FieldKinds.optional: {
                setter = function (newContent) {
                    const field = getBoxedField(this, key, fieldSchema);
                    field.content = newContent;
                };
                break;
            }
            case default_field_kinds_1.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 (0, core_1.inCursorField)(this[lazyEntity_1.cursorSymbol], key, (cursor) => (0, unboxed_1.unboxedField)(this.context, fieldSchema, cursor));
            },
            set: setter,
        };
        // Create set method for property (when appropriate)
        if (setter !== undefined) {
            propertyDescriptorMap[`set${(0, util_1.capitalize)(key)}`] = {
                enumerable: false,
                get() {
                    return setter;
                },
            };
        }
        propertyDescriptorMap[`boxed${(0, util_1.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;
}
function getFieldSchema(field, schema) {
    return schema.objectNodeFields.get(field) ?? schema.mapFields ?? typed_schema_1.TreeFieldSchema.empty;
}
exports.getFieldSchema = getFieldSchema;
//# sourceMappingURL=lazyTree.js.map