"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.unboxedFlexNode = exports.LazyForbiddenField = exports.LazyOptionalField = exports.LazyValueField = exports.LazySequence = exports.LazyField = exports.makeField = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
const index_js_3 = require("../default-schema/index.js");
const mapTreeCursor_js_1 = require("../mapTreeCursor.js");
const flexTreeTypes_js_1 = require("./flexTreeTypes.js");
const lazyEntity_js_1 = require("./lazyEntity.js");
const lazyNode_js_1 = require("./lazyNode.js");
const utilities_js_1 = require("./utilities.js");
/**
 * Reuse fields.
 * Since field currently own cursors and register themselves for disposal when the node hit end of life,
 * not reusing them results in memory leaks every time the field is accessed.
 * Since the fields stay alive until the node is end of life reusing them this way is safe.
 *
 * This ins't a perfect solution:
 *
 * - This can cause leaks, like map nodes will keep all accessed field objects around. Since other things cause this same leak already, its not too bad.
 * - This does not cache the root.
 * - Finding the parent anchor to do the caching on has significant cost.
 *
 * Despite these limitations, this cache provides a large performance win in some common cases (over 10x), especially with how simple tree requests far more field objects than necessary currently.
 */
const fieldCache = new WeakMap();
function makeField(context, schema, cursor) {
    const fieldAnchor = cursor.buildFieldAnchor();
    let usedAnchor = false;
    const makeFlexTreeField = () => {
        usedAnchor = true;
        const field = new (kindToClass.get(schema) ?? (0, internal_1.fail)(0xb0f /* missing field implementation */))(context, schema, cursor, fieldAnchor);
        return field;
    };
    if (fieldAnchor.parent === undefined) {
        return makeFlexTreeField();
    }
    // For the common case (all but roots), cache field associated with its node's anchor and field key.
    const anchorNode = context.checkout.forest.anchors.locate(fieldAnchor.parent) ??
        (0, internal_1.fail)(0xb10 /* missing anchor */);
    // Since anchor-set could be reused across a flex tree context getting disposed, key off the flex tree node not the anchor.
    const cacheKey = anchorNode.slots.get(flexTreeTypes_js_1.flexTreeSlot);
    // If there is no flex tree parent node, skip caching: this is not expected to be a hot path, but should probably be fixed at some point.
    if (cacheKey === undefined) {
        return makeFlexTreeField();
    }
    const innerCache = (0, index_js_2.getOrCreate)(fieldCache, cacheKey, () => new Map());
    const result = (0, index_js_2.getOrCreate)(innerCache, fieldAnchor.fieldKey, makeFlexTreeField);
    if (!usedAnchor) {
        // The anchor must be disposed to avoid leaking. In the case of a cache hit,
        // we are not transferring ownership to a new FlexTreeField, so it must be disposed of here to avoid the leak.
        context.checkout.forest.anchors.forget(fieldAnchor.parent);
    }
    return result;
}
exports.makeField = makeField;
/**
 * Base type for fields implementing {@link FlexTreeField} using cursors.
 */
class LazyField extends lazyEntity_js_1.LazyEntity {
    get [flexTreeTypes_js_1.flexTreeMarker]() {
        return flexTreeTypes_js_1.FlexTreeEntityKind.Field;
    }
    constructor(context, schema, cursor, fieldAnchor) {
        super(context, cursor, fieldAnchor);
        this.schema = schema;
        (0, internal_1.assert)(cursor.mode === 1 /* CursorLocationType.Fields */, 0x77b /* must be in fields mode */);
        this.key = cursor.getFieldKey();
        // Fields currently live as long as their parent does.
        // For root fields, this means forever, but other cases can be cleaned up when their parent anchor is deleted.
        if (fieldAnchor.parent !== undefined) {
            const anchorNode = context.checkout.forest.anchors.locate(fieldAnchor.parent) ??
                (0, internal_1.fail)(0xb11 /* parent anchor node should always exist since field is under a node */);
            this.offAfterDestroy = anchorNode.events.on("afterDestroy", () => {
                this[index_js_2.disposeSymbol]();
            });
        }
    }
    is(kind) {
        (0, internal_1.assert)(this.context.schemaPolicy.fieldKinds.get(kind.identifier) === kind, 0xa26 /* Narrowing must be done to a kind that exists in this context */);
        return this.schema === kind.identifier;
    }
    get parent() {
        if (this.anchor.parent === undefined) {
            return undefined;
        }
        const cursor = this.cursor;
        cursor.exitField();
        const output = (0, lazyNode_js_1.getOrCreateHydratedFlexTreeNode)(this.context, cursor);
        cursor.enterField(this.key);
        return output;
    }
    tryMoveCursorToAnchor(cursor) {
        return this.context.checkout.forest.tryMoveCursorToField(this.anchor, cursor);
    }
    forgetAnchor() {
        this.offAfterDestroy?.();
        if (this.anchor.parent === undefined)
            return;
        this.context.checkout.forest.anchors.forget(this.anchor.parent);
    }
    get length() {
        return this.cursor.getFieldLength();
    }
    atIndex(index) {
        return (0, index_js_1.inCursorNode)(this.cursor, index, (cursor) => unboxedFlexNode(this.context, cursor, this.anchor));
    }
    boxedAt(index) {
        const finalIndex = (0, utilities_js_1.indexForAt)(index, this.length);
        if (finalIndex === undefined) {
            return undefined;
        }
        return (0, index_js_1.inCursorNode)(this.cursor, finalIndex, (cursor) => (0, lazyNode_js_1.getOrCreateHydratedFlexTreeNode)(this.context, cursor));
    }
    map(callbackfn) {
        return (0, index_js_1.mapCursorField)(this.cursor, (cursor) => callbackfn(unboxedFlexNode(this.context, cursor, this.anchor), cursor.fieldIndex));
    }
    [Symbol.iterator]() {
        return (0, index_js_1.iterateCursorField)(this.cursor, (cursor) => (0, lazyNode_js_1.getOrCreateHydratedFlexTreeNode)(this.context, cursor));
    }
    getFieldPath() {
        return { parent: this.parent?.anchorNode, field: this.key };
    }
    /**
     * Returns the path to this field to use for editing. Throws iff this path is not {@link TreeStatus#InDocument}.
     * This path is not valid to hold onto across edits: this must be recalled for each edit.
     */
    getFieldPathForEditing() {
        if (!this.isFreed()) {
            if (
            // Only allow editing if we are the root document field...
            (this.parent === undefined && this.anchor.fieldKey === index_js_1.rootFieldKey) ||
                // ...or are under a node in the document
                (this.parent !== undefined &&
                    (0, utilities_js_1.treeStatusFromAnchorCache)(this.parent.anchorNode) === flexTreeTypes_js_1.TreeStatus.InDocument)) {
                return this.getFieldPath();
            }
        }
        throw new internal_2.UsageError("Editing only allowed on fields with TreeStatus.InDocument status");
    }
    getEditor() {
        return new index_js_3.MappedEditBuilder(this.context.checkout.editor, (cursor) => this.context.checkout.forest.chunkField(cursor));
    }
}
exports.LazyField = LazyField;
class LazySequence extends LazyField {
    constructor() {
        super(...arguments);
        this.editor = {
            insert: (index, newContent) => {
                this.sequenceEditor().insert(index, (0, mapTreeCursor_js_1.cursorForMapTreeField)(newContent));
            },
            remove: (index, count) => {
                this.sequenceEditor().remove(index, count);
            },
        };
    }
    at(index) {
        const finalIndex = (0, utilities_js_1.indexForAt)(index, this.length);
        if (finalIndex === undefined) {
            return undefined;
        }
        return this.atIndex(finalIndex);
    }
    get asArray() {
        return this.map((x) => x);
    }
    sequenceEditor() {
        const fieldPath = this.getFieldPathForEditing();
        return this.getEditor().sequenceField(fieldPath);
    }
}
exports.LazySequence = LazySequence;
class LazyValueField extends LazyField {
    constructor() {
        super(...arguments);
        this.editor = {
            set: (newContent) => {
                this.valueFieldEditor().set((0, mapTreeCursor_js_1.cursorForMapTreeField)([newContent]));
            },
        };
    }
    valueFieldEditor() {
        const fieldPath = this.getFieldPathForEditing();
        const fieldEditor = this.getEditor().valueField(fieldPath);
        return fieldEditor;
    }
    get content() {
        return this.atIndex(0);
    }
}
exports.LazyValueField = LazyValueField;
class LazyOptionalField extends LazyField {
    constructor() {
        super(...arguments);
        this.editor = {
            set: (newContent, wasEmpty) => {
                this.optionalEditor().set(newContent !== undefined ? (0, mapTreeCursor_js_1.cursorForMapTreeField)([newContent]) : newContent, wasEmpty);
            },
        };
    }
    optionalEditor() {
        const fieldPath = this.getFieldPathForEditing();
        const fieldEditor = this.getEditor().optionalField(fieldPath);
        return fieldEditor;
    }
    get content() {
        return this.length === 0 ? undefined : this.atIndex(0);
    }
}
exports.LazyOptionalField = LazyOptionalField;
class LazyForbiddenField extends LazyField {
}
exports.LazyForbiddenField = LazyForbiddenField;
const builderList = [
    [index_js_3.FieldKinds.forbidden.identifier, LazyForbiddenField],
    [index_js_3.FieldKinds.optional.identifier, LazyOptionalField],
    [index_js_3.FieldKinds.sequence.identifier, LazySequence],
    [index_js_3.FieldKinds.required.identifier, LazyValueField],
    [index_js_3.FieldKinds.identifier.identifier, LazyValueField],
];
const kindToClass = new Map(builderList);
/**
 * Returns the flex tree node, or the value if it has one.
 */
function unboxedFlexNode(context, cursor, fieldAnchor) {
    const value = cursor.value;
    if (value !== undefined) {
        return value;
    }
    // Try accessing cached child node via anchors.
    // This avoids O(depth) related costs from getOrCreateHydratedFlexTreeNode in the cached case.
    const anchor = fieldAnchor.parent;
    let child;
    if (anchor !== undefined) {
        const anchorNode = context.checkout.forest.anchors.locate(anchor);
        (0, internal_1.assert)(anchorNode !== undefined, 0xa4c /* missing anchor */);
        child = anchorNode.childIfAnchored(fieldAnchor.fieldKey, cursor.fieldIndex);
    }
    else {
        child = context.checkout.forest.anchors.find({
            parent: undefined,
            parentField: fieldAnchor.fieldKey,
            parentIndex: cursor.fieldIndex,
        });
    }
    if (child !== undefined) {
        const cached = child.slots.get(flexTreeTypes_js_1.flexTreeSlot);
        if (cached !== undefined) {
            return cached;
        }
    }
    return (0, lazyNode_js_1.getOrCreateHydratedFlexTreeNode)(context, cursor);
}
exports.unboxedFlexNode = unboxedFlexNode;
//# sourceMappingURL=lazyField.js.map