"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 common_utils_1 = require("@fluidframework/common-utils");
const core_1 = require("../../core");
const modular_schema_1 = require("../modular-schema");
const contextuallyTyped_1 = require("../contextuallyTyped");
const defaultFieldKinds_1 = require("../defaultFieldKinds");
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 targetSequence = new FieldProxyTarget(context, fieldSchema, cursor);
    return (0, utilities_1.adaptWithProxy)(targetSequence, fieldProxyHandler);
}
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, fieldSchema, cursor) {
        super(context, cursor);
        this.fieldSchema = fieldSchema;
        (0, common_utils_1.assert)(cursor.mode === 1 /* Fields */, 0x453 /* must be in fields mode */);
        this.fieldKey = cursor.getFieldKey();
        this[contextuallyTyped_1.arrayLikeMarkerSymbol] = true;
    }
    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;
    }
    buildAnchor() {
        return this.cursor.buildFieldAnchor();
    }
    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, common_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();
    }
    insertNodes(index, 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, common_utils_1.assert)(this.length === 0 && (!Array.isArray(newContent) || newContent.length <= 1), 0x455 /* A non-sequence field cannot have more than one node. */);
        }
        (0, common_utils_1.assert)((0, utilities_1.keyIsValidIndex)(index, this.length + 1), 0x456 /* Index must be less than or equal to length. */);
        const fieldPath = this.cursor.getFieldPath();
        this.context.insertNodes(fieldPath, index, newContent);
    }
    moveNodes(sourceIndex, count, destinationIndex, destinationField) {
        const sourceFieldPath = this.cursor.getFieldPath();
        const destinationFieldKindIdentifier = destinationField !== undefined
            ? destinationField.fieldSchema.kind.identifier
            : this.fieldSchema.kind.identifier;
        (0, common_utils_1.assert)(this.fieldSchema.kind.identifier === defaultFieldKinds_1.sequence.identifier &&
            destinationFieldKindIdentifier === defaultFieldKinds_1.sequence.identifier, 0x683 /* Both source and destination fields must be sequence fields. */);
        const destinationFieldProxy = destinationField !== undefined
            ? destinationField[editableTreeTypes_1.proxyTargetSymbol]
            : this;
        (0, common_utils_1.assert)(isFieldProxyTarget(destinationFieldProxy), 0x684 /* destination field proxy must be a field proxy target */);
        (0, util_1.assertValidIndex)(destinationIndex, destinationFieldProxy, true);
        const destinationFieldPath = destinationFieldProxy.cursor.getFieldPath();
        this.context.moveNodes(sourceFieldPath, sourceIndex, count, destinationFieldPath, destinationIndex);
    }
    getfieldPath() {
        return this.cursor.getFieldPath();
    }
    deleteNodes(index, count) {
        // TODO: currently for all field kinds the nodes can be deleted by editor using `sequenceField.delete()`.
        // Uncomment when the editor will become more schema-aware.
        // const fieldKind = getFieldKind(this.fieldSchema);
        // assert(fieldKind.multiplicity === Multiplicity.Sequence, "The field must be of a sequence kind.");
        (0, common_utils_1.assert)(this.length === 0 || (0, utilities_1.keyIsValidIndex)(index, this.length), 0x457 /* Index must be less than length. */);
        if (count !== undefined)
            (0, common_utils_1.assert)(count >= 0, 0x458 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const _count = count === undefined || count > maxCount ? maxCount : count;
        const fieldPath = this.cursor.getFieldPath();
        this.context.deleteNodes(fieldPath, index, _count);
    }
    replaceNodes(index, newContent, count) {
        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, common_utils_1.assert)(this.length <= 1 && (!Array.isArray(newContent) || newContent.length <= 1), 0x4d0 /* A non-sequence field cannot have more than one node. */);
        }
        (0, common_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, common_utils_1.assert)(count >= 0, 0x4d2 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const _count = count === undefined || count > maxCount ? maxCount : count;
        const fieldPath = this.cursor.getFieldPath();
        this.context.replaceNodes(fieldPath, index, _count, newContent);
    }
}
exports.FieldProxyTarget = FieldProxyTarget;
const editableFieldPropertySetWithoutLength = new Set([
    "fieldKey",
    "fieldSchema",
    "primaryType",
    "parent",
    "context",
]);
/**
 * 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);
            }
            else if ((0, utilities_1.keyIsValidIndex)(key, target.length)) {
                return target.unwrappedTree(Number(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);
                };
            }
            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) => {
        const cursor = (0, contextuallyTyped_1.cursorFromContextualData)(target.context.schema, target.fieldSchema.types, value);
        // This is just a cheap way to check if there might be a node at the given index.
        // An implementation of the target methods holds all relevant key assertions.
        // TODO: maybe refactor this to add a real node existence check if desired,
        // but it might be costly regarding performance.
        if ((0, utilities_1.keyIsValidIndex)(key, target.length)) {
            target.replaceNodes(Number(key), cursor, 1);
        }
        else {
            target.insertNodes(Number(key), 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 = (0, core_1.lookupTreeSchema)(context.schema, nodeTypeName);
    // 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, common_utils_1.assert)(nodeType.value === core_1.ValueSchema.Serializable, 0x3c7 /* `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 FieldSchema 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, common_utils_1.assert)(length <= 1, 0x3c8 /* invalid non sequence */);
    if (length === 1) {
        return (0, core_1.inCursorNode)(cursor, 0, (innerCursor) => unwrappedTree(context, innerCursor));
    }
    (0, common_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