"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.isUnwrappedNode = exports.NodeProxyTarget = exports.makeTree = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const mapTreeCursor_1 = require("../mapTreeCursor");
const contextuallyTyped_1 = require("../contextuallyTyped");
const utilities_1 = require("./utilities");
const editableTreeTypes_1 = require("./editableTreeTypes");
const editableField_1 = require("./editableField");
const ProxyTarget_1 = require("./ProxyTarget");
const editableTreeSlot = (0, core_1.anchorSlot)();
function makeTree(context, cursor) {
    var _a;
    const anchor = cursor.buildAnchor();
    const anchorNode = (_a = context.forest.anchors.locate(anchor)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("cursor should point to a node that is not the root of the AnchorSet");
    const cached = anchorNode.slots.get(editableTreeSlot);
    if (cached !== undefined) {
        context.forest.anchors.forget(anchor);
        return cached;
    }
    const newTarget = new NodeProxyTarget(context, cursor, anchorNode, anchor);
    const output = (0, utilities_1.adaptWithProxy)(newTarget, nodeProxyHandler);
    anchorNode.slots.set(editableTreeSlot, output);
    anchorNode.on("afterDelete", cleanupTree);
    return output;
}
exports.makeTree = makeTree;
function cleanupTree(anchor) {
    var _a;
    const cached = (_a = anchor.slots.get(editableTreeSlot)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("tree should only be cleaned up once");
    cached[editableTreeTypes_1.proxyTargetSymbol].free();
}
function isNodeProxyTarget(target) {
    return target instanceof NodeProxyTarget;
}
/**
 * A Proxy target, which together with a `nodeProxyHandler` implements a basic access to
 * the fields of {@link EditableTree} by means of the cursors.
 */
class NodeProxyTarget extends ProxyTarget_1.ProxyTarget {
    constructor(context, cursor, anchorNode, anchor) {
        super(context, cursor, anchor);
        this.anchorNode = anchorNode;
        (0, common_utils_1.assert)(cursor.mode === 0 /* Nodes */, 0x44c /* must be in nodes mode */);
        this.proxy = (0, utilities_1.adaptWithProxy)(this, nodeProxyHandler);
        anchorNode.slots.set(editableTreeSlot, this.proxy);
        this.removeDeleteCallback = anchorNode.on("afterDelete", cleanupTree);
        (0, common_utils_1.assert)(this.context.schema.treeSchema.get(this.typeName) !== undefined, 0x5b1 /* There is no explicit schema for this node type. Ensure that the type is correct and the schema for it was added to the SchemaData */);
    }
    buildAnchor() {
        return this.context.forest.anchors.track(this.anchorNode);
    }
    tryMoveCursorToAnchor(anchor, cursor) {
        return this.context.forest.tryMoveCursorToNode(anchor, cursor);
    }
    forgetAnchor(anchor) {
        // 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(editableTreeSlot);
        this.removeDeleteCallback();
        this.context.forest.anchors.forget(anchor);
    }
    get typeName() {
        return this.cursor.type;
    }
    get type() {
        return (0, core_1.lookupTreeSchema)(this.context.schema, this.typeName);
    }
    get value() {
        return this.cursor.value;
    }
    set value(value) {
        (0, common_utils_1.assert)((0, contextuallyTyped_1.allowsValue)(this.type.value, value), 0x5b2 /* Out of schema value can not be set on tree */);
        this.context.setNodeValue(this.anchorNode, value);
    }
    get currentIndex() {
        return this.cursor.fieldIndex;
    }
    lookupFieldKind(field) {
        return (0, contextuallyTyped_1.getFieldKind)(this.getFieldSchema(field));
    }
    getFieldSchema(field) {
        return (0, contextuallyTyped_1.getFieldSchema)(field, this.context.schema, this.type);
    }
    getFieldKeys() {
        return (0, core_1.mapCursorFields)(this.cursor, (c) => c.getFieldKey());
    }
    has(field) {
        // Make fields present only if non-empty.
        return this.fieldLength(field) !== 0;
    }
    unwrappedField(field) {
        const schema = this.getFieldSchema(field);
        return (0, core_1.inCursorField)(this.cursor, field, (cursor) => (0, editableField_1.unwrappedField)(this.context, schema, cursor));
    }
    getField(fieldKey) {
        const schema = this.getFieldSchema(fieldKey);
        return (0, core_1.inCursorField)(this.cursor, fieldKey, (cursor) => (0, editableField_1.makeField)(this.context, schema, cursor));
    }
    [Symbol.iterator]() {
        const type = this.type;
        return (0, core_1.mapCursorFields)(this.cursor, (cursor) => (0, editableField_1.makeField)(this.context, (0, contextuallyTyped_1.getFieldSchema)(cursor.getFieldKey(), this.context.schema, type), cursor)).values();
    }
    createField(fieldKey, newContent) {
        (0, common_utils_1.assert)(!this.has(fieldKey), 0x44f /* The field already exists. */);
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case modular_schema_1.Multiplicity.Optional: {
                (0, common_utils_1.assert)(!Array.isArray(newContent), 0x450 /* Use single cursor to create the optional field */);
                this.context.setOptionalField(path, newContent, true);
                break;
            }
            case modular_schema_1.Multiplicity.Sequence: {
                this.context.insertNodes(path, 0, newContent);
                break;
            }
            case modular_schema_1.Multiplicity.Value:
                (0, util_1.fail)("It is invalid to create fields of kind `value` as they should always exist.");
            default:
                (0, util_1.fail)("`Forbidden` fields may not be created.");
        }
    }
    fieldLength(field) {
        return (0, core_1.inCursorField)(this.cursor, field, (cursor) => cursor.getFieldLength());
    }
    deleteField(fieldKey) {
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case modular_schema_1.Multiplicity.Optional: {
                this.context.setOptionalField(path, undefined, false);
                break;
            }
            case modular_schema_1.Multiplicity.Sequence: {
                const length = this.fieldLength(fieldKey);
                this.context.deleteNodes(path, 0, length);
                break;
            }
            case modular_schema_1.Multiplicity.Value:
                (0, util_1.fail)("Fields of kind `value` may not be deleted.");
            default:
                (0, util_1.fail)("`Forbidden` fields may not be deleted.");
        }
    }
    replaceField(fieldKey, newContent) {
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case modular_schema_1.Multiplicity.Optional: {
                (0, common_utils_1.assert)(!Array.isArray(newContent), 0x4cd /* It is invalid to replace the optional field using the array data. */);
                this.context.setOptionalField(path, newContent, !this.has(fieldKey));
                break;
            }
            case modular_schema_1.Multiplicity.Sequence: {
                (0, common_utils_1.assert)(Array.isArray(newContent), 0x5cc /* It is invalid to replace the sequence field with a non array value. */);
                const length = this.fieldLength(fieldKey);
                /**
                 * `replaceNodes` has different merge semantics than the `replaceField` would ideally offer:
                 * `replaceNodes` should not overwrite concurrently inserted content while `replaceField` should.
                 * We currently use `replaceNodes` here because the low-level editing API
                 * for the desired `replaceField` semantics is not yet avaialble.
                 */
                // TODO: update implementation once the low-level editing API is available.
                this.context.replaceNodes(path, 0, length, newContent);
                break;
            }
            case modular_schema_1.Multiplicity.Value: {
                (0, common_utils_1.assert)(!Array.isArray(newContent), 0x4ce /* It is invalid to replace the value field using the array data. */);
                (0, common_utils_1.assert)(newContent !== undefined, 0x5cd /* It is invalid to replace a value field with undefined */);
                this.context.setValueField(path, newContent);
                break;
            }
            default:
                (0, util_1.fail)("`Forbidden` fields may not be replaced as they never exist.");
        }
    }
    get parentField() {
        const cursor = this.cursor;
        const index = cursor.fieldIndex;
        cursor.exitNode();
        const key = cursor.getFieldKey();
        cursor.exitField();
        const parentType = cursor.type;
        cursor.enterField(key);
        const fieldSchema = (0, contextuallyTyped_1.getFieldSchema)(key, this.context.schema, (0, core_1.lookupTreeSchema)(this.context.schema, parentType));
        const proxifiedField = (0, editableField_1.makeField)(this.context, fieldSchema, this.cursor);
        this.cursor.enterNode(index);
        return { parent: proxifiedField, index };
    }
    on(eventName, listener) {
        switch (eventName) {
            case "changing": {
                const unsubscribeFromValueChange = this.anchorNode.on("valueChanging", listener);
                const unsubscribeFromChildrenChange = this.anchorNode.on("childrenChanging", (anchorNode) => listener(anchorNode, undefined));
                return () => {
                    unsubscribeFromValueChange();
                    unsubscribeFromChildrenChange();
                };
            }
            case "subtreeChanging": {
                const unsubscribeFromSubtreeChange = this.anchorNode.on("subtreeChanging", (anchorNode) => listener(anchorNode, undefined));
                return unsubscribeFromSubtreeChange;
            }
            default:
                (0, common_utils_1.unreachableCase)(eventName);
        }
    }
}
exports.NodeProxyTarget = NodeProxyTarget;
/**
 * A Proxy handler together with a {@link NodeProxyTarget} implements a basic read/write access to the Forest
 * by means of the cursors.
 */
const nodeProxyHandler = {
    get: (target, key) => {
        if (typeof key === "string" || (0, core_1.symbolIsFieldKey)(key)) {
            // All string keys are fields
            return target.unwrappedField((0, util_1.brand)(key));
        }
        // utility symbols
        switch (key) {
            case editableTreeTypes_1.typeSymbol:
                return target.type;
            case contextuallyTyped_1.typeNameSymbol:
                return target.typeName;
            case contextuallyTyped_1.valueSymbol:
                return target.value;
            case editableTreeTypes_1.proxyTargetSymbol:
                return target;
            case Symbol.iterator:
                return target[Symbol.iterator].bind(target);
            case editableTreeTypes_1.getField:
                return target.getField.bind(target);
            case editableTreeTypes_1.createField:
                return target.createField.bind(target);
            case editableTreeTypes_1.replaceField:
                return target.replaceField.bind(target);
            case editableTreeTypes_1.parentField:
                return target.parentField;
            case editableTreeTypes_1.contextSymbol:
                return target.context;
            case editableTreeTypes_1.on:
                return target.on.bind(target);
            default:
                return undefined;
        }
    },
    set: (target, key, value, receiver) => {
        if (typeof key === "string" || (0, core_1.symbolIsFieldKey)(key)) {
            const fieldKey = (0, util_1.brand)(key);
            const fieldSchema = target.getFieldSchema(fieldKey);
            const multiplicity = target.lookupFieldKind(fieldKey).multiplicity;
            const content = (0, contextuallyTyped_1.applyFieldTypesFromContext)(target.context.schema, fieldSchema, value);
            const cursors = content.map(mapTreeCursor_1.singleMapTreeCursor);
            // This unconditionally uses `replaceField`, which differs from `createField`
            // only for sequence fields while using `insertNodes` instead of `replaceNodes`
            // (plus some difference in assertions, which is ignored here for a sake of a better
            // consistency with the low-level editing API).
            // Since `insertNodes` and `replaceNodes` have same merge semantics with `replaceNodes`
            // being a bit more general purpose function, it's ok to just use that.
            if (multiplicity !== modular_schema_1.Multiplicity.Sequence) {
                (0, common_utils_1.assert)(cursors.length <= 1, 0x5ce /* more than one top level node in non-sequence filed */);
                target.replaceField(fieldKey, cursors.length === 0 ? undefined : cursors[0]);
            }
            else {
                target.replaceField(fieldKey, cursors);
            }
            return true;
        }
        else if (key === contextuallyTyped_1.valueSymbol) {
            target.value = value;
            return true;
        }
        return false;
    },
    deleteProperty: (target, key) => {
        if (typeof key === "string" || (0, core_1.symbolIsFieldKey)(key)) {
            const fieldKey = (0, util_1.brand)(key);
            target.deleteField(fieldKey);
            return true;
        }
        return false;
    },
    // Include documented symbols (except value when value is undefined) and all non-empty fields.
    has: (target, key) => {
        if (typeof key === "string" || (0, core_1.symbolIsFieldKey)(key)) {
            return target.has((0, util_1.brand)(key));
        }
        // utility symbols
        switch (key) {
            case editableTreeTypes_1.proxyTargetSymbol:
            case editableTreeTypes_1.typeSymbol:
            case contextuallyTyped_1.typeNameSymbol:
            case Symbol.iterator:
            case editableTreeTypes_1.getField:
            case editableTreeTypes_1.createField:
            case editableTreeTypes_1.replaceField:
            case editableTreeTypes_1.parentField:
            case editableTreeTypes_1.on:
            case editableTreeTypes_1.contextSymbol:
                return true;
            case contextuallyTyped_1.valueSymbol:
                // Could do `target.value !== ValueSchema.Nothing`
                // instead if values which could be modified should report as existing.
                return target.value !== undefined;
            default:
                return false;
        }
    },
    // Includes all non-empty fields, which are the enumerable fields.
    ownKeys: (target) => {
        return target.getFieldKeys();
    },
    getOwnPropertyDescriptor: (target, key) => {
        // We generally don't want to allow users of the proxy to reconfigure all the properties,
        // but it is an TypeError to return non-configurable for properties that do not exist on target,
        // so they must return true.
        if ((typeof key === "string" || (0, core_1.symbolIsFieldKey)(key)) && target.has((0, util_1.brand)(key))) {
            const field = target.unwrappedField((0, util_1.brand)(key));
            return {
                configurable: true,
                enumerable: true,
                value: field,
                writable: true,
            };
        }
        // utility symbols
        switch (key) {
            case editableTreeTypes_1.proxyTargetSymbol:
                return { configurable: true, enumerable: false, value: target, writable: false };
            case editableTreeTypes_1.typeSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.type,
                    writable: false,
                };
            case contextuallyTyped_1.typeNameSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.typeName,
                    writable: false,
                };
            case contextuallyTyped_1.valueSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.value,
                    writable: false,
                };
            case Symbol.iterator:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target[Symbol.iterator].bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.getField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.getField.bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.createField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.createField.bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.replaceField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.replaceField.bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.parentField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.parentField,
                    writable: false,
                };
            case editableTreeTypes_1.contextSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.context,
                    writable: false,
                };
            case editableTreeTypes_1.on:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.on.bind(target),
                    writable: false,
                };
            default:
                return undefined;
        }
    },
};
/**
 * Checks the type of an UnwrappedEditableField.
 * @alpha
 */
function isUnwrappedNode(field) {
    return (typeof field === "object" &&
        isNodeProxyTarget(field[editableTreeTypes_1.proxyTargetSymbol]));
}
exports.isUnwrappedNode = isUnwrappedNode;
//# sourceMappingURL=editableTree.js.map