"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEditableTree = 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 contextuallyTyped_1 = require("../contextuallyTyped");
const default_field_kinds_1 = require("../default-field-kinds");
const untypedTree_1 = require("../untypedTree");
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) {
    const anchor = cursor.buildAnchor();
    const anchorNode = context.forest.anchors.locate(anchor) ??
        (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) {
    const cached = anchor.slots.get(editableTreeSlot) ?? (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 (this.context.schema.treeSchema.get(this.typeName) ??
            (0, util_1.fail)("requested type does not exist in schema"));
    }
    get value() {
        return this.cursor.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.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));
    }
    setField(fieldKey, content) {
        this.getField(fieldKey).setContent(content);
    }
    [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(), type), cursor)).values();
    }
    fieldLength(field) {
        return (0, core_1.inCursorField)(this.cursor, field, (cursor) => cursor.getFieldLength());
    }
    get parentField() {
        const cursor = this.cursor;
        const index = this.anchorNode.parentIndex;
        (0, common_utils_1.assert)(this.cursor.fieldIndex === index, 0x714 /* mismatched indexes */);
        const key = this.anchorNode.parentField;
        cursor.exitNode();
        (0, common_utils_1.assert)(key === cursor.getFieldKey(), 0x715 /* mismatched keys */);
        let fieldSchema;
        // Check if the current node is in a detached sequence.
        if (this.anchorNode.parent === undefined) {
            // Parent field is a detached sequence, and thus needs special handling for its schema.
            // eslint-disable-next-line unicorn/prefer-ternary
            if (key === core_1.rootFieldKey) {
                fieldSchema = this.context.schema.rootFieldSchema;
            }
            else {
                // All fields (in the editable tree API) have a schema.
                // Since currently there is no known schema for detached sequences other than the special default root:
                // give all other detached fields a schema of sequence of any.
                // That schema is the only one that is safe since its the only field schema that allows any possible field content.
                //
                // TODO:
                // if any of the following are done this schema will need to be more specific:
                // 1. Editing APIs start exposing user created detached sequences.
                // 2. Remove (and its inverse) start working on subsequences or fields contents (like everything in a sequence or optional field) and not just single nodes.
                // 3. Possibly other unknown cases.
                // Additionally this approach makes it possible for a user to take an EditableTree node, get its parent, check its schema, down cast based on that, then edit that detached field (ex: removing the node in it).
                // This MIGHT work properly with existing merge resolution logic (it must keep client in sync and be unable to violate schema), but this either needs robust testing or to be explicitly banned (error before s3ending the op).
                // Issues like replacing a node in the a removed sequenced then undoing the remove could easily violate schema if not everything works exactly right!
                fieldSchema = { kind: default_field_kinds_1.FieldKinds.sequence };
            }
        }
        else {
            cursor.exitField();
            const parentType = cursor.type;
            cursor.enterField(key);
            fieldSchema = (0, contextuallyTyped_1.getFieldSchema)(key, this.context.schema.treeSchema.get(parentType) ??
                (0, util_1.fail)("requested schema that does not exist"));
        }
        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 unsubscribeFromChildrenChange = this.anchorNode.on("childrenChanging", (anchorNode) => listener(anchorNode));
                return unsubscribeFromChildrenChange;
            }
            case "subtreeChanging": {
                const unsubscribeFromSubtreeChange = this.anchorNode.on("subtreeChanging", (anchorNode) => listener(anchorNode));
                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") {
            // All string keys are fields
            return target.unwrappedField((0, util_1.brand)(key));
        }
        // utility symbols
        switch (key) {
            case untypedTree_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 untypedTree_1.getField:
                return target.getField.bind(target);
            case editableTreeTypes_1.setField:
                return target.setField.bind(target);
            case untypedTree_1.parentField:
                return target.parentField;
            case untypedTree_1.contextSymbol:
                return target.context;
            case untypedTree_1.on:
                return target.on.bind(target);
            case editableTreeTypes_1.localNodeKeySymbol:
                return getLocalNodeKey(target);
            default:
                return undefined;
        }
    },
    set: (target, key, value, receiver) => {
        (0, common_utils_1.assert)(key !== contextuallyTyped_1.valueSymbol, 0x703 /* The value of a node can only be changed by replacing the node */);
        if (typeof key === "string") {
            const fieldKey = (0, util_1.brand)(key);
            target.setField(fieldKey, value);
            return true;
        }
        return false;
    },
    deleteProperty: (target, key) => {
        if (typeof key === "string") {
            const fieldKey = (0, util_1.brand)(key);
            target.getField(fieldKey).delete();
            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") {
            return target.has((0, util_1.brand)(key));
        }
        // utility symbols
        switch (key) {
            case editableTreeTypes_1.proxyTargetSymbol:
            case untypedTree_1.typeSymbol:
            case contextuallyTyped_1.typeNameSymbol:
            case Symbol.iterator:
            case untypedTree_1.getField:
            case untypedTree_1.parentField:
            case untypedTree_1.on:
            case untypedTree_1.contextSymbol:
                return true;
            case editableTreeTypes_1.localNodeKeySymbol:
                return target.context.nodeKeyFieldKey !== undefined;
            case contextuallyTyped_1.valueSymbol:
                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" && 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 untypedTree_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 untypedTree_1.getField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.getField.bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.setField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.setField.bind(target),
                    writable: false,
                };
            case untypedTree_1.parentField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.parentField,
                    writable: false,
                };
            case untypedTree_1.contextSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.context,
                    writable: false,
                };
            case untypedTree_1.on:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.on.bind(target),
                    writable: false,
                };
            case editableTreeTypes_1.localNodeKeySymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: getLocalNodeKey(target),
                    writable: false,
                };
            default:
                return undefined;
        }
    },
};
/**
 * Checks the type of an UnwrappedEditableField.
 * @alpha
 */
function isEditableTree(field) {
    return (typeof field === "object" &&
        isNodeProxyTarget(field[editableTreeTypes_1.proxyTargetSymbol]));
}
exports.isEditableTree = isEditableTree;
/**
 * Retrieves the {@link LocalNodeKey} for the given node.
 * @remarks TODO: Optimize this to be a fast path that gets a {@link LocalNodeKey} directly from the
 * forest rather than getting the {@link StableNodeKey} and the compressing it.
 */
function getLocalNodeKey(target) {
    if (target.context.nodeKeyFieldKey === undefined) {
        return undefined;
    }
    const stableNodeKey = (0, utilities_1.getStableNodeKey)(target.context.nodeKeyFieldKey, target.proxy);
    if (stableNodeKey === undefined) {
        return undefined;
    }
    return target.context.nodeKeys.localizeNodeKey(stableNodeKey);
}
//# sourceMappingURL=editableTree.js.map