"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LazyForbiddenField = exports.LazyNodeKeyField = exports.LazyOptionalField = exports.LazyValueField = exports.LazySequence = exports.LazyField = exports.makeField = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
// TODO: stop depending on contextuallyTyped
const contextuallyTyped_js_1 = require("../contextuallyTyped.js");
const index_js_3 = require("../default-schema/index.js");
const mapTreeCursor_js_1 = require("../mapTreeCursor.js");
const index_js_4 = require("../node-key/index.js");
const flexTreeTypes_js_1 = require("./flexTreeTypes.js");
const lazyEntity_js_1 = require("./lazyEntity.js");
const lazyNode_js_1 = require("./lazyNode.js");
const unboxed_js_1 = require("./unboxed.js");
const utilities_js_1 = require("./utilities.js");
/**
 * Indexing for {@link LazyField.at} and {@link LazyField.boxedAt} supports the
 * usage of negative indices, which regular indexing using `[` and `]` does not.
 *
 * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at
 * for additional context on the semantics.
 *
 * @returns A positive index that can be used in regular indexing. Returns
 * undefined if that index would be out-of-bounds.
 */
function indexForAt(index, length) {
    let finalIndex = Math.trunc(+index);
    if (isNaN(finalIndex)) {
        finalIndex = 0;
    }
    if (finalIndex < -length || finalIndex >= length) {
        return undefined;
    }
    if (finalIndex < 0) {
        finalIndex = finalIndex + length;
    }
    return finalIndex;
}
function makeField(context, schema, cursor) {
    const fieldAnchor = cursor.buildFieldAnchor();
    const field = new (kindToClass.get(schema.kind) ?? (0, index_js_2.fail)("missing field implementation"))(context, schema, cursor, fieldAnchor);
    return field;
}
exports.makeField = makeField;
/**
 * A Proxy target, which together with a `fieldProxyHandler` implements a basic access to
 * the nodes of {@link EditableField} by means of the 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, schema, cursor, fieldAnchor);
        (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.forest.anchors.locate(fieldAnchor.parent) ??
                (0, index_js_2.fail)("parent anchor node should always exist since field is under a node");
            this.offAfterDestroy = anchorNode.on("afterDestroy", () => {
                this[index_js_2.disposeSymbol]();
            });
        }
    }
    is(schema) {
        (0, internal_1.assert)(this.context.schema.policy.fieldKinds.get(schema.kind.identifier) === schema.kind, 0x77c /* Narrowing must be done to a kind that exists in this context */);
        return this.schema.equals(schema);
    }
    isSameAs(other) {
        (0, internal_1.assert)(other.context === this.context, 0x77d /* Content from different flex trees should not be used together */);
        return this.key === other.key && this.parent === other.parent;
    }
    get parent() {
        if (this[lazyEntity_js_1.anchorSymbol].parent === undefined) {
            return undefined;
        }
        const cursor = this[lazyEntity_js_1.cursorSymbol];
        cursor.exitField();
        const output = (0, lazyNode_js_1.makeTree)(this.context, cursor);
        cursor.enterField(this.key);
        return output;
    }
    [lazyEntity_js_1.tryMoveCursorToAnchorSymbol](cursor) {
        return this.context.forest.tryMoveCursorToField(this[lazyEntity_js_1.anchorSymbol], cursor);
    }
    [lazyEntity_js_1.forgetAnchorSymbol]() {
        this.offAfterDestroy?.();
        if (this[lazyEntity_js_1.anchorSymbol].parent === undefined)
            return;
        this.context.forest.anchors.forget(this[lazyEntity_js_1.anchorSymbol].parent);
    }
    get length() {
        return this[lazyEntity_js_1.cursorSymbol].getFieldLength();
    }
    atIndex(index) {
        return (0, index_js_1.inCursorNode)(this[lazyEntity_js_1.cursorSymbol], index, (cursor) => (0, unboxed_js_1.unboxedUnion)(this.context, this.schema, cursor));
    }
    boxedAt(index) {
        const finalIndex = indexForAt(index, this.length);
        if (finalIndex === undefined) {
            return undefined;
        }
        return (0, index_js_1.inCursorNode)(this[lazyEntity_js_1.cursorSymbol], finalIndex, (cursor) => (0, lazyNode_js_1.makeTree)(this.context, cursor));
    }
    map(callbackfn) {
        return Array.from(this, callbackfn);
    }
    mapBoxed(callbackfn) {
        return Array.from(this.boxedIterator(), callbackfn);
    }
    boxedIterator() {
        return (0, index_js_1.iterateCursorField)(this[lazyEntity_js_1.cursorSymbol], (cursor) => (0, lazyNode_js_1.makeTree)(this.context, cursor));
    }
    [Symbol.iterator]() {
        return (0, index_js_1.iterateCursorField)(this[lazyEntity_js_1.cursorSymbol], (cursor) => (0, unboxed_js_1.unboxedUnion)(this.context, this.schema, cursor));
    }
    treeStatus() {
        if (this[lazyEntity_js_1.isFreedSymbol]()) {
            return flexTreeTypes_js_1.TreeStatus.Deleted;
        }
        const fieldAnchor = this[lazyEntity_js_1.anchorSymbol];
        const parentAnchor = fieldAnchor.parent;
        // If the parentAnchor is undefined it is a detached field.
        if (parentAnchor === undefined) {
            return (0, utilities_js_1.treeStatusFromDetachedField)((0, index_js_1.keyAsDetachedField)(fieldAnchor.fieldKey));
        }
        const parentAnchorNode = this.context.forest.anchors.locate(parentAnchor);
        // As the "parentAnchor === undefined" case is handled above, parentAnchorNode should exist.
        (0, internal_1.assert)(parentAnchorNode !== undefined, 0x77e /* parentAnchorNode must exist. */);
        return (0, utilities_js_1.treeStatusFromAnchorCache)(this.context.forest.anchors, parentAnchorNode);
    }
    getFieldPath() {
        return this[lazyEntity_js_1.cursorSymbol].getFieldPath();
    }
    /**
     * 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() {
        (0, internal_1.assert)(this.treeStatus() === flexTreeTypes_js_1.TreeStatus.InDocument, 0x77f /* Editing only allowed on fields with TreeStatus.InDocument status */);
        return this.getFieldPath();
    }
}
exports.LazyField = LazyField;
class LazySequence extends LazyField {
    constructor(context, schema, cursor, fieldAnchor) {
        super(context, schema, cursor, fieldAnchor);
    }
    at(index) {
        const finalIndex = indexForAt(index, this.length);
        if (finalIndex === undefined) {
            return undefined;
        }
        return (0, index_js_1.inCursorNode)(this[lazyEntity_js_1.cursorSymbol], finalIndex, (cursor) => (0, unboxed_js_1.unboxedUnion)(this.context, this.schema, cursor));
    }
    get asArray() {
        return this.map((x) => x);
    }
    sequenceEditor() {
        const fieldPath = this.getFieldPathForEditing();
        const fieldEditor = this.context.editor.sequenceField(fieldPath);
        return fieldEditor;
    }
    insertAt(index, value) {
        (0, index_js_2.assertValidIndex)(index, this, true);
        const content = (0, index_js_1.isCursor)(value)
            ? prepareFieldCursorForInsert(value)
            : (0, mapTreeCursor_js_1.cursorForMapTreeField)(Array.from(value, (item) => (0, contextuallyTyped_js_1.applyTypesFromContext)(this.context, this.schema.allowedTypeSet, item)));
        const fieldEditor = this.sequenceEditor();
        fieldEditor.insert(index, content);
    }
    insertAtStart(value) {
        this.insertAt(0, value);
    }
    insertAtEnd(value) {
        this.insertAt(this.length, value);
    }
    removeAt(index) {
        const fieldEditor = this.sequenceEditor();
        fieldEditor.remove(index, 1);
    }
    removeRange(start, end) {
        const fieldEditor = this.sequenceEditor();
        const { length } = this;
        const removeStart = start ?? 0;
        const removeEnd = Math.min(length, end ?? length);
        (0, index_js_2.assertValidRangeIndices)(removeStart, removeEnd, this);
        fieldEditor.remove(removeStart, removeEnd - removeStart);
    }
    moveToStart(sourceIndex, source) {
        this._moveRangeToIndex(0, sourceIndex, sourceIndex + 1, source);
    }
    moveToEnd(sourceIndex, source) {
        this._moveRangeToIndex(this.length, sourceIndex, sourceIndex + 1, source);
    }
    moveToIndex(index, sourceIndex, source) {
        this._moveRangeToIndex(index, sourceIndex, sourceIndex + 1, source);
    }
    moveRangeToStart(sourceStart, sourceEnd, source) {
        this._moveRangeToIndex(0, sourceStart, sourceEnd, source);
    }
    moveRangeToEnd(sourceStart, sourceEnd, source) {
        this._moveRangeToIndex(this.length, sourceStart, sourceEnd, source);
    }
    moveRangeToIndex(index, sourceStart, sourceEnd, source) {
        this._moveRangeToIndex(index, sourceStart, sourceEnd, source);
    }
    _moveRangeToIndex(index, sourceStart, sourceEnd, source) {
        const sourceField = source !== undefined ? (this.isSameAs(source) ? this : source) : this;
        // TODO: determine support for move across different sequence types
        (0, internal_1.assert)(sourceField instanceof LazySequence, 0x7b1 /* Unsupported sequence implementation. */);
        (0, index_js_2.assertValidRangeIndices)(sourceStart, sourceEnd, sourceField);
        if (this.schema.types !== undefined && sourceField !== this) {
            for (let i = sourceStart; i < sourceEnd; i++) {
                const sourceNode = sourceField.boxedAt(sourceStart) ?? (0, index_js_2.fail)("impossible out of bounds index");
                if (!this.schema.types.has(sourceNode.schema.name)) {
                    throw new Error("Type in source sequence is not allowed in destination.");
                }
            }
        }
        const movedCount = sourceEnd - sourceStart;
        (0, index_js_2.assertValidIndex)(index, this, true);
        const sourceFieldPath = sourceField.getFieldPath();
        const destinationFieldPath = this.getFieldPath();
        this.context.editor.move(sourceFieldPath, sourceStart, movedCount, destinationFieldPath, index);
    }
}
exports.LazySequence = LazySequence;
class LazyValueField extends LazyField {
    constructor(context, schema, cursor, fieldAnchor) {
        super(context, schema, cursor, fieldAnchor);
    }
    valueFieldEditor() {
        const fieldPath = this.getFieldPathForEditing();
        const fieldEditor = this.context.editor.valueField(fieldPath);
        return fieldEditor;
    }
    get content() {
        return this.atIndex(0);
    }
    set content(newContent) {
        const content = (0, index_js_1.isCursor)(newContent)
            ? prepareNodeCursorForInsert(newContent)
            : [(0, contextuallyTyped_js_1.cursorFromContextualData)(this.context, this.schema.allowedTypeSet, newContent)];
        const fieldEditor = this.valueFieldEditor();
        (0, internal_1.assert)(content.length === 1, 0x780 /* value field content should normalize to one item */);
        fieldEditor.set(content[0]);
    }
    get boxedContent() {
        return this.boxedAt(0) ?? (0, index_js_2.fail)("value node must have 1 item");
    }
}
exports.LazyValueField = LazyValueField;
class LazyOptionalField extends LazyField {
    constructor(context, schema, cursor, fieldAnchor) {
        super(context, schema, cursor, fieldAnchor);
    }
    optionalEditor() {
        const fieldPath = this.getFieldPathForEditing();
        const fieldEditor = this.context.editor.optionalField(fieldPath);
        return fieldEditor;
    }
    get content() {
        return this.length === 0 ? undefined : this.atIndex(0);
    }
    set content(newContent) {
        const content = newContent === undefined
            ? []
            : (0, index_js_1.isCursor)(newContent)
                ? prepareNodeCursorForInsert(newContent)
                : [(0, contextuallyTyped_js_1.cursorFromContextualData)(this.context, this.schema.allowedTypeSet, newContent)];
        const fieldEditor = this.optionalEditor();
        (0, internal_1.assert)(content.length <= 1, 0x781 /* optional field content should normalize at most one item */);
        fieldEditor.set(content.length === 0 ? undefined : content[0], this.length === 0);
    }
    get boxedContent() {
        return this.length === 0 ? undefined : this.boxedAt(0);
    }
}
exports.LazyOptionalField = LazyOptionalField;
class LazyNodeKeyField extends LazyField {
    constructor(context, schema, cursor, fieldAnchor) {
        super(context, schema, cursor, fieldAnchor);
    }
    get localNodeKey() {
        // TODO: Optimize this to be a fast path that gets a LocalNodeKey directly from the
        // forest rather than getting the StableNodeKey and the compressing it.
        return this.context.nodeKeys.localize(this.stableNodeKey);
    }
    get stableNodeKey() {
        const cursor = this[lazyEntity_js_1.cursorSymbol];
        cursor.enterNode(0);
        (0, internal_1.assert)(cursor.type === index_js_4.nodeKeyTreeIdentifier, 0x7b2 /* invalid node key type */);
        const stableKey = cursor.value;
        (0, internal_1.assert)(typeof stableKey === "string", 0x7b3 /* invalid node key type */);
        cursor.exitNode();
        return (0, index_js_2.brand)(stableKey);
    }
}
exports.LazyNodeKeyField = LazyNodeKeyField;
class LazyForbiddenField extends LazyField {
}
exports.LazyForbiddenField = LazyForbiddenField;
const builderList = [
    [index_js_3.FieldKinds.forbidden, LazyForbiddenField],
    [index_js_3.FieldKinds.nodeKey, LazyNodeKeyField],
    [index_js_3.FieldKinds.optional, LazyOptionalField],
    [index_js_3.FieldKinds.sequence, LazySequence],
    [index_js_3.FieldKinds.required, LazyValueField],
];
const kindToClass = new Map(builderList);
/**
 * Prepare a fields cursor (holding a sequence of nodes) for inserting.
 */
function prepareFieldCursorForInsert(cursor) {
    // TODO: optionally validate content against schema.
    (0, internal_1.assert)(cursor.mode === 1 /* CursorLocationType.Fields */, 0x8cb /* should be in fields mode */);
    return cursor;
}
/**
 * Prepare a node cursor (holding a single node) for inserting.
 */
function prepareNodeCursorForInsert(cursor) {
    // TODO: optionally validate content against schema.
    (0, internal_1.assert)(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x805 /* should be in nodes mode */);
    return [cursor];
}
//# sourceMappingURL=lazyField.js.map