"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEditableField = exports.unwrappedField = exports.FieldProxyTarget = exports.makeField = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const modular_schema_1 = require("../modular-schema");
const contextuallyTyped_1 = require("../contextuallyTyped");
const default_field_kinds_1 = require("../default-field-kinds");
const util_1 = require("../../util");
const utilities_1 = require("./utilities");
const editableTreeTypes_1 = require("./editableTreeTypes");
const editableTree_1 = require("./editableTree");
const ProxyTarget_1 = require("./ProxyTarget");
function makeField(context, fieldSchema, cursor) {
    const fieldAnchor = cursor.buildFieldAnchor();
    const targetSequence = new FieldProxyTarget(context, fieldSchema, cursor, fieldAnchor);
    const output = (0, utilities_1.adaptWithProxy)(targetSequence, fieldProxyHandler);
    // 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, util_1.fail)("parent anchor node should always exist since field is under a node");
        anchorNode.on("afterDelete", () => {
            targetSequence.free();
        });
    }
    return output;
}
exports.makeField = makeField;
function isFieldProxyTarget(target) {
    return target instanceof FieldProxyTarget;
}
/**
 * @returns the key, if any, of the primary array field.
 */
function getPrimaryArrayKey(type) {
    const primary = (0, contextuallyTyped_1.getPrimaryField)(type);
    if (primary === undefined) {
        return undefined;
    }
    const kind = (0, contextuallyTyped_1.getFieldKind)(primary.schema);
    if (kind.multiplicity === modular_schema_1.Multiplicity.Sequence) {
        // TODO: this could have issues if there are non-primary keys
        // that can collide with the array APIs (length or integers).
        return primary;
    }
    return undefined;
}
/**
 * A Proxy target, which together with a `fieldProxyHandler` implements a basic access to
 * the nodes of {@link EditableField} by means of the cursors.
 */
class FieldProxyTarget extends ProxyTarget_1.ProxyTarget {
    constructor(context, 
    // TODO: use view schema typed in editableTree
    fieldSchema, cursor, fieldAnchor) {
        super(context, cursor, fieldAnchor);
        this.fieldSchema = fieldSchema;
        (0, core_utils_1.assert)(cursor.mode === 1 /* Fields */, 0x453 /* must be in fields mode */);
        this.fieldKey = cursor.getFieldKey();
        this[contextuallyTyped_1.arrayLikeMarkerSymbol] = true;
        this.kind = (0, contextuallyTyped_1.getFieldKind)(this.fieldSchema);
    }
    /**
     * Check if this field is the same as a different field.
     * This is defined to mean that both are in the same editable tree, and are the same field on the same node.
     * This is more than just a reference comparison because unlike EditableTree nodes, fields are not cached on anchors and can be duplicated.
     */
    isSameAs(other) {
        (0, core_utils_1.assert)(other.context === this.context, 0x6b6 /* Content from different editable trees should not be used together */);
        return this.fieldKey === other.fieldKey && this.parent === other.parent;
    }
    normalizeNewContent(content) {
        return (0, contextuallyTyped_1.normalizeNewFieldContent)(this.context, this.fieldSchema, content);
    }
    get [editableTreeTypes_1.proxyTargetSymbol]() {
        return this;
    }
    get parent() {
        if (this.getAnchor().parent === undefined) {
            return undefined;
        }
        const cursor = this.cursor;
        cursor.exitField();
        const output = (0, editableTree_1.makeTree)(this.context, cursor);
        cursor.enterField(this.fieldKey);
        return output;
    }
    tryMoveCursorToAnchor(anchor, cursor) {
        return this.context.forest.tryMoveCursorToField(anchor, cursor);
    }
    forgetAnchor(anchor) {
        if (anchor.parent === undefined)
            return;
        this.context.forest.anchors.forget(anchor.parent);
    }
    get length() {
        return this.cursor.getFieldLength();
    }
    /**
     * Returns a node (unwrapped by default, see {@link UnwrappedEditableTree}) by its index.
     */
    unwrappedTree(index) {
        return (0, core_1.inCursorNode)(this.cursor, index, (cursor) => unwrappedTree(this.context, cursor));
    }
    /**
     * Gets a node by its index without unwrapping.
     */
    getNode(index) {
        (0, core_utils_1.assert)((0, utilities_1.keyIsValidIndex)(index, this.length), 0x454 /* A child node must exist at index to get it without unwrapping. */);
        return (0, core_1.inCursorNode)(this.cursor, index, (cursor) => (0, editableTree_1.makeTree)(this.context, cursor));
    }
    /**
     * Gets array of unwrapped nodes.
     */
    asArray() {
        return (0, core_1.mapCursorField)(this.cursor, (cursor) => unwrappedTree(this.context, cursor));
    }
    [Symbol.iterator]() {
        return this.asArray().values();
    }
    /**
     * Asserts this field is a sequence, and returns an editor for it.
     */
    sequenceEditor() {
        (0, core_utils_1.assert)(this.kind === default_field_kinds_1.FieldKinds.sequence, 0x6b9 /* Field kind must be a sequence to edit as a sequence. */);
        const fieldPath = this.cursor.getFieldPath();
        const fieldEditor = this.context.editor.sequenceField(fieldPath);
        return fieldEditor;
    }
    /**
     * Asserts this field is a sequence, and returns an editor for it.
     */
    optionalEditor() {
        (0, core_utils_1.assert)(this.kind === default_field_kinds_1.FieldKinds.optional, 0x6ba /* Field kind must be a optional to edit as optional. */);
        const fieldPath = this.cursor.getFieldPath();
        const fieldEditor = this.context.editor.optionalField(fieldPath);
        return fieldEditor;
    }
    /**
     * Asserts this field is a sequence, and returns an editor for it.
     */
    valueFieldEditor() {
        (0, core_utils_1.assert)(this.kind === default_field_kinds_1.FieldKinds.value, 0x6bb /* Field kind must be a value to edit as a value. */);
        const fieldPath = this.cursor.getFieldPath();
        const fieldEditor = this.context.editor.valueField(fieldPath);
        return fieldEditor;
    }
    get content() {
        switch (this.kind.multiplicity) {
            case modular_schema_1.Multiplicity.Optional: {
                if (this.length === 0) {
                    return undefined;
                }
                return this.getNode(0);
            }
            case modular_schema_1.Multiplicity.Value: {
                return this.getNode(0);
            }
            case modular_schema_1.Multiplicity.Forbidden: {
                return undefined;
            }
            case modular_schema_1.Multiplicity.Sequence: {
                return this;
            }
            default:
                (0, core_utils_1.unreachableCase)(this.kind.multiplicity);
        }
    }
    setContent(newContent) {
        const content = this.normalizeNewContent(newContent);
        switch (this.kind) {
            case default_field_kinds_1.FieldKinds.optional: {
                const fieldEditor = this.optionalEditor();
                (0, core_utils_1.assert)(content.length <= 1, 0x6bc /* optional field content should normalize at most one item */);
                fieldEditor.set(content.length === 0 ? undefined : content[0], this.length === 0);
                break;
            }
            case default_field_kinds_1.FieldKinds.value: {
                const fieldEditor = this.valueFieldEditor();
                (0, core_utils_1.assert)(content.length === 1, 0x6bd /* value field content should normalize to one item */);
                fieldEditor.set(content[0]);
                break;
            }
            case default_field_kinds_1.FieldKinds.sequence: {
                const fieldEditor = this.sequenceEditor();
                // TODO: this does not have the atomicity or merge semantics that are likely desired.
                // It should probably either be last write wins OR conflict if concurrently edited.
                // Current behavior results in concurrent sets concatenating.
                fieldEditor.delete(0, this.length);
                fieldEditor.insert(0, content);
                break;
            }
            case default_field_kinds_1.FieldKinds.nodeKey: {
                (0, util_1.fail)("Cannot set node key field: node keys are immutable.");
            }
            default:
                (0, util_1.fail)(`Cannot set content of fields of "${this.kind.identifier}" kind.`);
        }
    }
    remove() {
        switch (this.kind.multiplicity) {
            case modular_schema_1.Multiplicity.Optional: {
                const fieldEditor = this.optionalEditor();
                fieldEditor.set(undefined, false);
                break;
            }
            case modular_schema_1.Multiplicity.Sequence: {
                const fieldEditor = this.sequenceEditor();
                fieldEditor.delete(0, this.length);
                break;
            }
            default:
                (0, util_1.fail)(`Cannot delete fields of "${this.kind.identifier}" kind.`);
        }
    }
    insertNodes(index, newContent) {
        const fieldEditor = this.sequenceEditor();
        const content = this.normalizeNewContent(newContent);
        const fieldKind = (0, contextuallyTyped_1.getFieldKind)(this.fieldSchema);
        // TODO: currently for all field kinds the nodes can be created by editor using `sequenceField.insert()`.
        // Uncomment the next line and remove non-sequence related code when the editor will become more schema-aware.
        // assert(fieldKind.multiplicity === Multiplicity.Sequence, "The field must be of a sequence kind.");
        if (fieldKind.multiplicity !== modular_schema_1.Multiplicity.Sequence) {
            (0, core_utils_1.assert)(this.length === 0 && (!Array.isArray(content) || content.length <= 1), 0x455 /* A non-sequence field cannot have more than one node. */);
        }
        (0, core_utils_1.assert)((0, utilities_1.keyIsValidIndex)(index, this.length + 1), 0x456 /* Index must be less than or equal to length. */);
        fieldEditor.insert(index, content);
    }
    moveNodes(sourceIndex, count, destinationIndex, destinationField) {
        const sourceFieldPath = this.cursor.getFieldPath();
        const destination = destinationField === undefined
            ? this
            : destinationField?.[editableTreeTypes_1.proxyTargetSymbol];
        (0, core_utils_1.assert)(isFieldProxyTarget(destination), 0x684 /* destination must be a field proxy target */);
        (0, core_utils_1.assert)(this.kind === default_field_kinds_1.FieldKinds.sequence, 0x6be /* Move source must be a sequence. */);
        (0, core_utils_1.assert)(destination.kind === default_field_kinds_1.FieldKinds.sequence, 0x6bf /* Move destination must be a sequence. */);
        (0, util_1.assertNonNegativeSafeInteger)(count);
        // This permits a move of 0 nodes starting at this.length, which does seem like it should be allowed.
        (0, util_1.assertValidIndex)(sourceIndex + count, this, true);
        let destinationLength = destination.length;
        if (this.isSameAs(destination)) {
            destinationLength -= count;
        }
        (0, util_1.assertValidIndex)(destinationIndex, { length: destinationLength }, true);
        const destinationFieldPath = destination.cursor.getFieldPath();
        this.context.editor.move(sourceFieldPath, sourceIndex, count, destinationFieldPath, destinationIndex);
    }
    treeStatus() {
        if (this.isFreed()) {
            return editableTreeTypes_1.TreeStatus.Deleted;
        }
        const fieldAnchor = this.getAnchor();
        const parentAnchor = fieldAnchor.parent;
        // If the parentAnchor is undefined it is a detached field.
        if (parentAnchor === undefined) {
            return (0, core_1.keyAsDetachedField)(fieldAnchor.fieldKey) === core_1.rootField
                ? editableTreeTypes_1.TreeStatus.InDocument
                : editableTreeTypes_1.TreeStatus.Removed;
        }
        const parentAnchorNode = this.context.forest.anchors.locate(parentAnchor);
        // As the "parentAnchor === undefined" case is handled above, parentAnchorNode should exist.
        (0, core_utils_1.assert)(parentAnchorNode !== undefined, 0x748 /* parentAnchorNode must exist. */);
        return (0, utilities_1.treeStatusFromPath)(parentAnchorNode);
    }
    getfieldPath() {
        return this.cursor.getFieldPath();
    }
    removeNodes(index, count) {
        const fieldEditor = this.sequenceEditor();
        (0, core_utils_1.assert)(this.length === 0 || (0, utilities_1.keyIsValidIndex)(index, this.length), 0x457 /* Index must be less than length. */);
        if (count !== undefined)
            (0, core_utils_1.assert)(count >= 0, 0x458 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const adjustedCount = count === undefined || count > maxCount ? maxCount : count;
        fieldEditor.delete(index, adjustedCount);
    }
    replaceNodes(index, newContent, count) {
        const fieldEditor = this.sequenceEditor();
        const content = this.normalizeNewContent(newContent);
        (0, core_utils_1.assert)((this.length === 0 && index === 0) || (0, utilities_1.keyIsValidIndex)(index, this.length), 0x4d1 /* Index must be less than length or, if the field is empty, be 0. */);
        if (count !== undefined)
            (0, core_utils_1.assert)(count >= 0, 0x4d2 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const adjustedCount = count === undefined || count > maxCount ? maxCount : count;
        fieldEditor.delete(index, adjustedCount);
        if (content.length > 0) {
            fieldEditor.insert(index, content);
        }
    }
}
exports.FieldProxyTarget = FieldProxyTarget;
const editableFieldPropertySetWithoutLength = new Set([
    "fieldKey",
    "fieldSchema",
    "primaryType",
    "parent",
    "context",
    "content",
]);
/**
 * The set of `EditableField` properties exposed by `fieldProxyHandler`.
 * Any other properties are considered to be non-existing.
 */
const editableFieldPropertySet = new Set([
    "length",
    ...editableFieldPropertySetWithoutLength,
]);
/**
 * Returns a Proxy handler, which together with a {@link FieldProxyTarget} implements a basic read/write access to
 * the sequence fields by means of the cursors.
 */
const fieldProxyHandler = {
    get: (target, key, receiver) => {
        if (typeof key === "string") {
            if (editableFieldPropertySet.has(key)) {
                return Reflect.get(target, key);
            }
            // This maps the methods of the `EditableField` to their implementation in the `FieldProxyTarget`.
            // Expected are only the methods declared in the `EditableField` interface,
            // as only those are visible for the users of the public API.
            // Such implicit delegation is chosen for a future array implementation in case it will be needed.
            const reflected = Reflect.get(target, key);
            if (typeof reflected === "function") {
                return function (...args) {
                    return Reflect.apply(reflected, target, args);
                };
            }
            if ((0, utilities_1.keyIsValidIndex)(key, target.length)) {
                return target.unwrappedTree(Number(key));
            }
            return undefined;
        }
        switch (key) {
            case editableTreeTypes_1.proxyTargetSymbol:
                return target;
            case Symbol.iterator:
                return target[Symbol.iterator].bind(target);
            case contextuallyTyped_1.arrayLikeMarkerSymbol:
                return true;
            default:
        }
        return undefined;
    },
    set: (target, key, value, receiver) => {
        switch (key) {
            case "content": {
                target.setContent(value);
                break;
            }
            default: {
                (0, core_utils_1.assert)((0, utilities_1.keyIsValidIndex)(key, target.length + 1), 0x6c0 /* cannot assign to unexpected member of field. */);
                const cursor = (0, contextuallyTyped_1.cursorFromContextualData)(target.context, target.fieldSchema.types, value);
                const index = Number(key);
                if (target.kind.multiplicity === modular_schema_1.Multiplicity.Sequence) {
                    if (index < target.length) {
                        target.replaceNodes(index, [cursor], 1);
                    }
                    else {
                        target.insertNodes(index, [cursor]);
                    }
                }
                else {
                    (0, core_utils_1.assert)(index === 0, 0x6c1 /* Assignments to non-sequence field content by index must use index 0. */);
                    target.setContent(cursor);
                }
            }
        }
        return true;
    },
    deleteProperty: (target, key) => {
        throw new Error("Not supported. Use `deleteNodes()` instead");
    },
    // Include documented symbols and all non-empty fields.
    has: (target, key) => {
        if (typeof key === "symbol") {
            switch (key) {
                case Symbol.iterator:
                case editableTreeTypes_1.proxyTargetSymbol:
                case contextuallyTyped_1.arrayLikeMarkerSymbol:
                    return true;
                default:
            }
        }
        else {
            if ((0, utilities_1.keyIsValidIndex)(key, target.length) || editableFieldPropertySet.has(key)) {
                return true;
            }
        }
        return false;
    },
    ownKeys: (target) => {
        // This includes 'length' property.
        const keys = (0, utilities_1.getOwnArrayKeys)(target.length);
        keys.push(...editableFieldPropertySetWithoutLength);
        return keys;
    },
    getOwnPropertyDescriptor: (target, key) => {
        // We generally don't want to allow users of the proxy to reconfigure all the properties,
        // but it is a TypeError to return non-configurable for properties that do not exist on target,
        // so they must return true.
        if (typeof key === "symbol") {
            switch (key) {
                case editableTreeTypes_1.proxyTargetSymbol:
                    return {
                        configurable: true,
                        enumerable: false,
                        value: target,
                        writable: false,
                    };
                case Symbol.iterator:
                    return {
                        configurable: true,
                        enumerable: false,
                        value: target[Symbol.iterator].bind(target),
                        writable: false,
                    };
                default:
            }
        }
        else {
            if (editableFieldPropertySet.has(key)) {
                return {
                    configurable: true,
                    enumerable: false,
                    value: Reflect.get(target, key),
                    writable: false,
                };
            }
            else if ((0, utilities_1.keyIsValidIndex)(key, target.length)) {
                return {
                    configurable: true,
                    enumerable: true,
                    value: target.unwrappedTree(Number(key)),
                    writable: true,
                };
            }
        }
        return undefined;
    },
};
/**
 * See {@link UnwrappedEditableTree} for documentation on what unwrapping this performs.
 */
function unwrappedTree(context, cursor) {
    const nodeTypeName = cursor.type;
    const nodeType = context.schema.treeSchema.get(nodeTypeName) ??
        (0, util_1.fail)("requested type does not exist in schema");
    // Unwrap primitives or nodes having a primary field. Sequences unwrap nodes on their own.
    if ((0, utilities_1.isPrimitive)(nodeType)) {
        const nodeValue = cursor.value;
        if ((0, contextuallyTyped_1.isPrimitiveValue)(nodeValue)) {
            return nodeValue;
        }
        (0, util_1.fail)("`undefined` values not allowed for primitive fields");
    }
    const primary = getPrimaryArrayKey(nodeType);
    if (primary !== undefined) {
        cursor.enterField(primary.key);
        const primaryField = makeField(context, primary.schema, cursor);
        cursor.exitField();
        return primaryField;
    }
    return (0, editableTree_1.makeTree)(context, cursor);
}
/**
 * @param context - the common context of the field.
 * @param fieldSchema - the FieldStoredSchema of the field.
 * @param cursor - the cursor, which must point to the field being proxified.
 */
function unwrappedField(context, fieldSchema, cursor) {
    const fieldKind = (0, contextuallyTyped_1.getFieldKind)(fieldSchema);
    if (fieldKind.multiplicity === modular_schema_1.Multiplicity.Sequence) {
        return makeField(context, fieldSchema, cursor);
    }
    const length = cursor.getFieldLength();
    (0, core_utils_1.assert)(length <= 1, 0x3c8 /* invalid non sequence */);
    if (length === 1) {
        return (0, core_1.inCursorNode)(cursor, 0, (innerCursor) => unwrappedTree(context, innerCursor));
    }
    (0, core_utils_1.assert)(fieldKind.multiplicity === modular_schema_1.Multiplicity.Optional ||
        fieldKind.multiplicity === modular_schema_1.Multiplicity.Forbidden, 0x59a /* invalid empty field */);
    return undefined;
}
exports.unwrappedField = unwrappedField;
/**
 * Checks the type of an UnwrappedEditableField.
 * @alpha
 */
function isEditableField(field) {
    return (typeof field === "object" &&
        isFieldProxyTarget(field[editableTreeTypes_1.proxyTargetSymbol]));
}
exports.isEditableField = isEditableField;
//# sourceMappingURL=editableField.js.map