"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.markContentType = exports.prepareContentForHydration = exports.getOrCreateNodeProxy = exports.getProxyForField = exports.isTreeNode = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../feature-libraries/index.js");
const index_js_3 = require("../util/index.js");
const proxyBinding_js_1 = require("./proxyBinding.js");
const schemaCaching_js_1 = require("./schemaCaching.js");
/**
 * Detects if the given 'candidate' is a TreeNode.
 *
 * @remarks
 * Supports both Hydrated and {@link Unhydrated} TreeNodes, both of which return true.
 *
 * Because the common usage is to check if a value being inserted/set is a TreeNode,
 * this function permits calling with primitives as well as objects.
 *
 * Primitives will always return false (as they are copies of data, not references to nodes).
 *
 * @param candidate - Value which may be a TreeNode
 * @returns true if the given 'candidate' is a hydrated TreeNode.
 */
function isTreeNode(candidate) {
    return (0, proxyBinding_js_1.tryGetFlexNode)(candidate) !== undefined;
}
exports.isTreeNode = isTreeNode;
/**
 * Retrieve the associated proxy for the given field.
 * */
function getProxyForField(field) {
    switch (field.schema.kind) {
        case index_js_2.FieldKinds.required: {
            const asValue = field;
            // TODO: Ideally, we would return leaves without first boxing them.  However, this is not
            //       as simple as calling '.content' since this skips the node and returns the FieldNode's
            //       inner field.
            return getOrCreateNodeProxy(asValue.boxedContent);
        }
        case index_js_2.FieldKinds.optional: {
            const asValue = field;
            // TODO: Ideally, we would return leaves without first boxing them.  However, this is not
            //       as simple as calling '.content' since this skips the node and returns the FieldNode's
            //       inner field.
            const maybeContent = asValue.boxedContent;
            // Normally, empty fields are unreachable due to the behavior of 'tryGetField'.  However, the
            // root field is a special case where the field is always present (even if empty).
            return maybeContent === undefined ? undefined : getOrCreateNodeProxy(maybeContent);
        }
        // TODO: Remove if/when 'FieldNode' is removed.
        case index_js_2.FieldKinds.sequence: {
            // 'getProxyForNode' handles FieldNodes by unconditionally creating a array node proxy, making
            // this case unreachable as long as users follow the 'array recipe'.
            (0, index_js_3.fail)("'sequence' field is unexpected.");
        }
        case index_js_2.FieldKinds.identifier: {
            const identifier = field.boxedAt(0);
            (0, internal_1.assert)(identifier !== undefined, 0x91a /* identifier must exist */);
            return getOrCreateNodeProxy(identifier);
        }
        default:
            (0, index_js_3.fail)("invalid field kind");
    }
}
exports.getProxyForField = getProxyForField;
function getOrCreateNodeProxy(flexNode) {
    const cachedProxy = (0, proxyBinding_js_1.tryGetProxy)(flexNode);
    if (cachedProxy !== undefined) {
        return cachedProxy;
    }
    const schema = flexNode.schema;
    const classSchema = (0, schemaCaching_js_1.tryGetSimpleNodeSchema)(schema);
    (0, internal_1.assert)(classSchema !== undefined, 0x91b /* node without schema */);
    if (typeof classSchema === "function") {
        const simpleSchema = classSchema;
        return new simpleSchema(flexNode);
    }
    else {
        return classSchema.create(flexNode);
    }
}
exports.getOrCreateNodeProxy = getOrCreateNodeProxy;
/**
 * Records any proxies in the given content tree and does the necessary bookkeeping to ensure they are synchronized with subsequent reads of the tree.
 * @remarks If the content tree contains any proxies, this function must be called just prior to inserting the content into the tree.
 * Specifically, no other content may be inserted into the tree between the invocation of this function and the insertion of `content`.
 * The insertion of `content` must occur or else this function will cause memory leaks.
 * @param content - the tree of content to be inserted, of which any of its object/map/array nodes might be a proxy
 * @param anchors - the {@link AnchorSet} for the tree
 * @returns The content after having all proxies replaced inline with plain javascript objects.
 * See {@link extractFactoryContent} for more details.
 */
function prepareContentForHydration(content, forest) {
    if ((0, index_js_3.isReadonlyArray)(content)) {
        return prepareArrayContentForHydration(content, forest);
    }
    if (content !== undefined) {
        const proxies = {
            rootPath: { parent: undefined, parentField: index_js_1.EmptyKey, parentIndex: 0 },
            proxyPaths: [],
        };
        walkMapTree(content, proxies.rootPath, (p, mapTreeNode, proxy) => {
            proxies.proxyPaths.push({ path: p, mapTreeNode, proxy });
        });
        bindProxies([proxies], forest);
    }
}
exports.prepareContentForHydration = prepareContentForHydration;
function prepareArrayContentForHydration(content, forest) {
    const proxies = [];
    for (let i = 0; i < content.length; i++) {
        proxies.push({
            rootPath: {
                parent: undefined,
                parentField: index_js_1.EmptyKey,
                parentIndex: 0,
            },
            proxyPaths: [],
        });
        walkMapTree(content[i], proxies[i].rootPath, (p, mapTreeNode, proxy) => {
            proxies[i].proxyPaths.push({ path: p, mapTreeNode, proxy });
        });
    }
    bindProxies(proxies, forest);
}
function walkMapTree(mapTree, path, onVisitTreeNode) {
    const mapTreeNode = (0, index_js_2.tryGetMapTreeNode)(mapTree);
    if (mapTreeNode !== undefined) {
        const treeNode = (0, proxyBinding_js_1.tryGetProxy)(mapTreeNode);
        if (treeNode !== undefined) {
            onVisitTreeNode(path, mapTreeNode, treeNode);
        }
    }
    for (const [key, field] of mapTree.fields) {
        for (let i = 0; i < field.length; i++) {
            walkMapTree(field[i], {
                parent: path,
                parentField: key,
                parentIndex: i,
            }, onVisitTreeNode);
        }
    }
}
function bindProxies(proxies, forest) {
    // Only subscribe to the event if there is at least one proxy tree to hydrate - this is not the case when inserting an empty array [].
    if (proxies.length > 0) {
        // Creating a new array emits one event per element in the array, so listen to the event once for each element
        let i = 0;
        const off = forest.on("afterRootFieldCreated", (fieldKey) => {
            proxies[i].rootPath.parentField = fieldKey;
            for (const { path, mapTreeNode, proxy } of proxies[i].proxyPaths) {
                const anchorNode = (0, proxyBinding_js_1.anchorProxy)(forest.anchors, path, proxy);
                mapTreeNode.forwardEvents({
                    on(eventName, listener) {
                        switch (eventName) {
                            case "nodeChanged": {
                                return anchorNode.on("childrenChangedAfterBatch", listener);
                            }
                            case "treeChanged": {
                                return anchorNode.on("subtreeChangedAfterBatch", listener);
                            }
                            default:
                                (0, index_js_3.fail)("Unexpected event subscription");
                        }
                    },
                });
            }
            if (++i === proxies.length) {
                off();
            }
        });
    }
}
/**
 * Brand `copy` with the type (under {@link typeNameSymbol}) to avoid ambiguity when inferring types from this data.
 */
function markContentType(typeName, copy) {
    Object.defineProperty(copy, index_js_2.typeNameSymbol, { value: typeName });
}
exports.markContentType = markContentType;
//# sourceMappingURL=proxies.js.map