/*!
 * 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;
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { EmptyKey, forEachField, inCursorField, mapCursorFields, rootFieldKey, } from "../../core/index.js";
import { brand, capitalize, disposeSymbol, fail, getOrCreate } from "../../util/index.js";
import { FieldKinds } from "../default-schema/index.js";
import { Any, FlexFieldSchema, schemaIsFieldNode, schemaIsLeaf, schemaIsMap, schemaIsObjectNode, } from "../typed-schema/index.js";
import { FlexTreeEntityKind, TreeStatus, flexTreeMarker, flexTreeSlot, reservedObjectNodeFieldPropertyNamePrefixes, reservedObjectNodeFieldPropertyNames, } from "./flexTreeTypes.js";
import { LazyEntity, anchorSymbol, cursorSymbol, forgetAnchorSymbol, isFreedSymbol, tryMoveCursorToAnchorSymbol, } from "./lazyEntity.js";
import { LazyNodeKeyField, makeField } from "./lazyField.js";
import { unboxedField } from "./unboxed.js";
import { treeStatusFromAnchorCache } from "./utilities.js";
/**
 * @param cursor - This does not take ownership of this cursor: Node will fork it as needed.
 */
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(flexTreeSlot);
    if (cached !== undefined) {
        context.forest.anchors.forget(anchor);
        assert(cached.context === context, 0x782 /* contexts must match */);
        assert(cached instanceof LazyTreeNode, "Expected LazyTreeNode");
        return cached;
    }
    const schema = context.schema.nodeSchema.get(cursor.type) ?? fail("missing schema");
    const output = buildSubclass(context, schema, cursor, anchorNode, anchor);
    anchorNode.slots.set(flexTreeSlot, output);
    anchorNode.on("afterDestroy", cleanupTree);
    return output;
}
function cleanupTree(anchor) {
    const cached = anchor.slots.get(flexTreeSlot) ?? fail("tree should only be cleaned up once");
    assert(cached instanceof LazyTreeNode, "Expected LazyTreeNode");
    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);
    }
    // TODO: there should be a common fallback that works for cases without a specialized implementation.
    fail("unrecognized node kind");
}
/**
 * Lazy implementation of {@link FlexTreeNode}.
 */
export class LazyTreeNode extends LazyEntity {
    get [(_LazyTreeNode_removeDeleteCallback = new WeakMap(), flexTreeMarker)]() {
        return FlexTreeEntityKind.Node;
    }
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchor);
        this.anchorNode = anchorNode;
        // 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);
        assert(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x783 /* must be in nodes mode */);
        anchorNode.slots.set(flexTreeSlot, this);
        __classPrivateFieldSet(this, _LazyTreeNode_removeDeleteCallback, anchorNode.on("afterDestroy", cleanupTree), "f");
        assert(this.context.schema.nodeSchema.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 */);
        this.type = schema.name;
    }
    is(schema) {
        assert(this.context.schema.nodeSchema.get(schema.name) === schema, 0x785 /* Narrowing must be done to a schema that exists in this context */);
        return this.schema === schema;
    }
    [tryMoveCursorToAnchorSymbol](cursor) {
        return this.context.forest.tryMoveCursorToNode(this[anchorSymbol], cursor);
    }
    [forgetAnchorSymbol]() {
        // 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.
        this.anchorNode.slots.delete(flexTreeSlot);
        __classPrivateFieldGet(this, _LazyTreeNode_removeDeleteCallback, "f").call(this);
        this.context.forest.anchors.forget(this[anchorSymbol]);
    }
    get value() {
        return this[cursorSymbol].value;
    }
    tryGetField(fieldKey) {
        const schema = this.schema.getFieldSchema(fieldKey);
        return inCursorField(this[cursorSymbol], fieldKey, (cursor) => {
            if (cursor.getFieldLength() === 0) {
                return undefined;
            }
            return makeField(this.context, schema, cursor);
        });
    }
    getBoxed(key) {
        return inCursorField(this[cursorSymbol], brand(key), (cursor) => makeField(this.context, this.schema.getFieldSchema(key), cursor));
    }
    boxedIterator() {
        return mapCursorFields(this[cursorSymbol], (cursor) => makeField(this.context, this.schema.getFieldSchema(cursor.getFieldKey()), cursor)).values();
    }
    get parentField() {
        const cursor = this[cursorSymbol];
        const index = this.anchorNode.parentIndex;
        assert(cursor.fieldIndex === index, 0x786 /* mismatched indexes */);
        const key = this.anchorNode.parentField;
        cursor.exitNode();
        assert(key === cursor.getFieldKey(), 0x787 /* mismatched keys */);
        let fieldSchema;
        // Check if the current node is in a detached sequence.
        if (this.anchorNode.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 flex 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 a FlexTree 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 = FlexFieldSchema.create(FieldKinds.sequence, [Any]);
            }
        }
        else {
            cursor.exitField();
            const parentType = cursor.type;
            cursor.enterField(key);
            const nodeSchema = this.context.schema.nodeSchema.get(parentType) ??
                fail("requested schema that does not exist");
            fieldSchema = nodeSchema.getFieldSchema(key);
        }
        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, this.anchorNode);
    }
    on(eventName, listener) {
        switch (eventName) {
            case "changing": {
                const unsubscribeFromChildrenChange = this.anchorNode.on("childrenChanging", (anchorNode) => listener(anchorNode));
                return unsubscribeFromChildrenChange;
            }
            case "subtreeChanging": {
                const unsubscribeFromSubtreeChange = this.anchorNode.on("subtreeChanging", (anchorNode) => listener(anchorNode));
                return unsubscribeFromSubtreeChange;
            }
            default:
                unreachableCase(eventName);
        }
    }
}
export class LazyMap extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
    }
    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.info, cursor)).values();
    }
    entries() {
        return mapCursorFields(this[cursorSymbol], (cursor) => {
            const entry = [
                cursor.getFieldKey(),
                unboxedField(this.context, this.schema.info, 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(brand(key)) !== undefined;
    }
    get(key) {
        return inCursorField(this[cursorSymbol], brand(key), (cursor) => unboxedField(this.context, this.schema.info, cursor));
    }
    getBoxed(key) {
        return super.getBoxed(brand(key));
    }
    set(key, content) {
        const field = this.getBoxed(key);
        const fieldSchema = this.schema.info;
        if (fieldSchema.kind === FieldKinds.optional) {
            const optionalField = field;
            optionalField.content = content;
        }
        else {
            assert(fieldSchema.kind === FieldKinds.sequence, 0x807 /* Unexpected map field kind */);
            // TODO: implement setting of sequence fields once we have defined clear merged semantics for doing so.
            // For now, we will throw an error, since the public API does not currently expose a way to do this anyways.
            throw new Error("Setting of sequence values in maps is not yet supported.");
        }
    }
    delete(key) {
        // Since all keys implicitly exist under a Map node, and we represent "no value" with `undefined`,
        // "deleting" a key/value pair is the same as setting the value to `undefined`.
        this.set(key, undefined);
    }
    boxedIterator() {
        return super.boxedIterator();
    }
    [Symbol.iterator]() {
        return this.entries();
    }
}
export class LazyLeaf extends LazyTreeNode {
    constructor(context, schema, cursor, anchorNode, anchor) {
        super(context, schema, cursor, anchorNode, anchor);
    }
    get value() {
        return super.value;
    }
}
export class LazyFieldNode extends LazyTreeNode {
    get content() {
        return inCursorField(this[cursorSymbol], EmptyKey, (cursor) => unboxedField(this.context, this.schema.info, cursor));
    }
    get boxedContent() {
        return inCursorField(this[cursorSymbol], EmptyKey, (cursor) => makeField(this.context, this.schema.info, cursor));
    }
}
/**
 * {@link reservedObjectNodeFieldPropertyNames} but as a set.
 */
export const reservedObjectNodeFieldPropertyNameSet = new Set(reservedObjectNodeFieldPropertyNames);
export function propertyNameFromFieldKey(key) {
    if (reservedObjectNodeFieldPropertyNameSet.has(key)) {
        return `field${capitalize(key)}`;
    }
    for (const prefix of reservedObjectNodeFieldPropertyNamePrefixes) {
        if (key.startsWith(prefix)) {
            const afterPrefix = key.slice(prefix.length);
            if (afterPrefix === capitalize(afterPrefix)) {
                return `field${capitalize(key)}`;
            }
        }
    }
    return key;
}
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(FlexFieldSchema.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 = {};
    for (const [key, fieldSchema] of schema.objectNodeFields) {
        const escapedKey = propertyNameFromFieldKey(key);
        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
        propertyDescriptorMap[escapedKey] = {
            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(escapedKey)}`] = {
                enumerable: false,
                get() {
                    return setter;
                },
            };
        }
        propertyDescriptorMap[`boxed${capitalize(escapedKey)}`] = {
            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(CustomStruct.prototype, propertyDescriptorMap);
    return CustomStruct;
}
//# sourceMappingURL=lazyNode.js.map