"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createListProxy = exports.createObjectProxy = exports.getProxyForNode = exports.getProxyForField = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../../../util");
const typed_schema_1 = require("../../typed-schema");
const default_field_kinds_1 = require("../../default-field-kinds");
const lazyTree_1 = require("../lazyTree");
const types_1 = require("./types");
const objectFactory_1 = require("./objectFactory");
const proxyCacheSym = Symbol("ProxyCache");
/** Cache the proxy that wraps the given tree node so that the proxy can be re-used in future reads */
function cacheProxy(target, proxy) {
    Object.defineProperty(target, proxyCacheSym, {
        value: proxy,
        writable: false,
        enumerable: false,
        configurable: false,
    });
}
/** If there has already been a proxy created to wrap the given tree node, return it */
function getCachedProxy(treeNode) {
    return treeNode[proxyCacheSym];
}
/** Retrieve the associated proxy for the given field. */
function getProxyForField(field) {
    switch (field.schema.kind) {
        case default_field_kinds_1.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 getProxyForNode(asValue.boxedContent);
        }
        case default_field_kinds_1.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 : getProxyForNode(maybeContent));
        }
        // TODO: Remove if/when 'FieldNode' is removed.
        case default_field_kinds_1.FieldKinds.sequence: {
            // 'getProxyForNode' handles FieldNodes by unconditionally creating a list proxy, making
            // this case unreachable as long as users follow the 'list recipe'.
            (0, util_1.fail)("'sequence' field is unexpected.");
        }
        default:
            (0, util_1.fail)("invalid field kind");
    }
}
exports.getProxyForField = getProxyForField;
function getProxyForNode(treeNode) {
    const schema = treeNode.schema;
    if ((0, typed_schema_1.schemaIsMap)(schema)) {
        (0, util_1.fail)("Map not implemented");
    }
    if ((0, typed_schema_1.schemaIsLeaf)(schema)) {
        return treeNode.value;
    }
    const isFieldNode = (0, typed_schema_1.schemaIsFieldNode)(schema);
    if (isFieldNode || (0, typed_schema_1.schemaIsObjectNode)(schema)) {
        const cachedProxy = getCachedProxy(treeNode);
        if (cachedProxy !== undefined) {
            return cachedProxy;
        }
        const proxy = isFieldNode ? createListProxy(treeNode) : createObjectProxy(treeNode, schema);
        cacheProxy(treeNode, proxy);
        return proxy;
    }
    (0, util_1.fail)("unrecognized node kind");
}
exports.getProxyForNode = getProxyForNode;
function createObjectProxy(content, schema) {
    // To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an object with the same
    // prototype as an object literal '{}'.  This is because 'deepEquals' uses 'Object.getPrototypeOf'
    // as a way to quickly reject objects with different prototype chains.
    //
    // (Note that the prototype of an object literal appears as '[Object: null prototype] {}', not because
    // the prototype is null, but because the prototype object itself has a null prototype.)
    // TODO: Although the target is an object literal, it's still worthwhile to try experimenting with
    // a dispatch object to see if it improves performance.
    const proxy = new Proxy({}, {
        get(target, key) {
            const field = content.tryGetField(key);
            if (field !== undefined) {
                return getProxyForField(field);
            }
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            const fieldSchema = content.schema.objectNodeFields.get(key);
            if (fieldSchema === undefined) {
                return false;
            }
            // TODO: Is it safe to assume 'content' is a LazyObjectNode?
            (0, core_utils_1.assert)(content instanceof lazyTree_1.LazyObjectNode, 0x7e0 /* invalid content */);
            (0, core_utils_1.assert)(typeof key === "string", 0x7e1 /* invalid key */);
            const field = (0, lazyTree_1.getBoxedField)(content, (0, util_1.brand)(key), fieldSchema);
            switch (field.schema.kind) {
                case default_field_kinds_1.FieldKinds.required: {
                    field.content =
                        (0, objectFactory_1.getFactoryContent)(value) ?? value;
                    break;
                }
                case default_field_kinds_1.FieldKinds.optional: {
                    field.content =
                        (0, objectFactory_1.getFactoryContent)(value) ?? value;
                    break;
                }
                default:
                    (0, util_1.fail)("invalid FieldKind");
            }
            return true;
        },
        has: (target, key) => {
            return schema.objectNodeFields.has(key);
        },
        ownKeys: (target) => {
            return [...schema.objectNodeFields.keys()];
        },
        getOwnPropertyDescriptor: (target, key) => {
            const field = content.tryGetField(key);
            if (field === undefined) {
                return undefined;
            }
            const p = {
                value: getProxyForField(field),
                writable: true,
                enumerable: true,
                configurable: true, // Must be 'configurable' if property is absent from proxy target.
            };
            return p;
        },
    });
    (0, types_1.setTreeNode)(proxy, content);
    return proxy;
}
exports.createObjectProxy = createObjectProxy;
/**
 * Given the a list proxy, returns its underlying LazySequence field.
 */
const getSequenceField = (list) => {
    const treeNode = (0, types_1.getTreeNode)(list);
    const field = treeNode.content;
    return field;
};
// Converts a proxy union to contextually typed data, extracting factory content if necessary.
const asContextuallyTypedData = (value) => (value === null || typeof value !== "object"
    ? value // Return primitives as-is
    : (0, objectFactory_1.getFactoryContent)(value) ?? value); // Otherwise extract factory content (if necessary).
// Used by 'insert*()' APIs to converts new content (expressed as a proxy union) to contextually
// typed data prior to forwarding to 'LazySequence.insert*()'.
function itemsAsContextuallyTyped(iterable) {
    // If the iterable is not already an array, copy it into an array to use '.map()' below.
    return Array.isArray(iterable)
        ? iterable.map(asContextuallyTypedData)
        : Array.from(iterable, asContextuallyTypedData);
}
// TODO: Experiment with alternative dispatch methods to see if we can improve performance.
/**
 * PropertyDescriptorMap used to build the prototype for our SharedListNode dispatch object.
 */
const listPrototypeProperties = {
    // We manually add [Symbol.iterator] to the dispatch map rather than use '[fn.name] = fn' as
    // above because 'Array.prototype[Symbol.iterator].name' returns "values" (i.e., Symbol.iterator
    // is an alias for the '.values()' function.)
    [Symbol.iterator]: {
        value: Array.prototype[Symbol.iterator],
    },
    insertAt: {
        value(index, value) {
            getSequenceField(this).insertAt(index, itemsAsContextuallyTyped(value));
        },
    },
    insertAtStart: {
        value(value) {
            getSequenceField(this).insertAtStart(itemsAsContextuallyTyped(value));
        },
    },
    insertAtEnd: {
        value(value) {
            getSequenceField(this).insertAtEnd(itemsAsContextuallyTyped(value));
        },
    },
    removeAt: {
        value(index) {
            getSequenceField(this).removeAt(index);
        },
    },
    removeRange: {
        value(start, end) {
            getSequenceField(this).removeRange(start, end);
        },
    },
    moveToStart: {
        value(sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToStart(sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToStart(sourceStart, sourceEnd);
            }
        },
    },
    moveToEnd: {
        value(sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToEnd(sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToEnd(sourceStart, sourceEnd);
            }
        },
    },
    moveToIndex: {
        value(index, sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToIndex(index, sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToIndex(index, sourceStart, sourceEnd);
            }
        },
    },
};
/* eslint-disable @typescript-eslint/unbound-method */
// For compatibility, we are initially implement 'readonly T[]' by applying the Array.prototype methods
// to the list proxy.  Over time, we should replace these with efficient implementations on LazySequence
// to avoid re-entering the proxy as these methods access 'length' and the indexed properties.
//
// For brevity, the current implementation dynamically builds a property descriptor map from a list of
// Array functions we want to re-expose via the proxy.
// TODO: This assumes 'Function.name' matches the property name on 'Array.prototype', which may be
// dubious across JS engines.
[
    // TODO: Remove cast to any once targeting a more recent ES version.
    Array.prototype.at,
    Array.prototype.concat,
    // Array.prototype.copyWithin,
    Array.prototype.entries,
    Array.prototype.every,
    // Array.prototype.fill,
    Array.prototype.filter,
    Array.prototype.find,
    Array.prototype.findIndex,
    Array.prototype.flat,
    Array.prototype.flatMap,
    Array.prototype.forEach,
    Array.prototype.includes,
    Array.prototype.indexOf,
    Array.prototype.join,
    Array.prototype.keys,
    Array.prototype.lastIndexOf,
    // Array.prototype.length,
    Array.prototype.map,
    // Array.prototype.pop,
    // Array.prototype.push,
    Array.prototype.reduce,
    Array.prototype.reduceRight,
    // Array.prototype.reverse,
    // Array.prototype.shift,
    Array.prototype.slice,
    Array.prototype.some,
    // Array.prototype.sort,
    // Array.prototype.splice,
    Array.prototype.toLocaleString,
    Array.prototype.toString,
    // Array.prototype.unshift,
    Array.prototype.values,
].forEach((fn) => {
    listPrototypeProperties[fn.name] = { value: fn };
});
/* eslint-enable @typescript-eslint/unbound-method */
const prototype = Object.create(Object.prototype, listPrototypeProperties);
/**
 * Helper to coerce property keys to integer indexes (or undefined if not an in-range integer).
 */
function asIndex(key, length) {
    if (typeof key === "string") {
        // TODO: It may be worth a '0' <= ch <= '9' check before calling 'Number' to quickly
        // reject 'length' as an index, or even parsing integers ourselves.
        const asNumber = Number(key);
        // TODO: See 'matrix/range.ts' for fast integer coercing + range check.
        if (Number.isInteger(asNumber)) {
            return 0 <= asNumber && asNumber < length ? asNumber : undefined;
        }
    }
}
function createListProxy(treeNode) {
    // Create a 'dispatch' object that this Proxy forwards to instead of the proxy target.
    // Own properties on the dispatch object are surfaced as own properties of the proxy.
    // (e.g., 'length', which is defined below).
    //
    // Properties normally inherited from 'Array.prototype' are surfaced via the prototype chain.
    const dispatch = Object.create(prototype, {
        length: {
            get() {
                return getSequenceField(this).length;
            },
            set() { },
            enumerable: false,
            configurable: false,
        },
    });
    (0, types_1.setTreeNode)(dispatch, treeNode);
    // To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an array literal in order
    // to pass 'Object.getPrototypeOf'.  It also satisfies 'Array.isArray' and 'Object.prototype.toString'
    // requirements without use of Array[Symbol.species], which is potentially on a path ot deprecation.
    return new Proxy([], {
        get: (target, key) => {
            const field = getSequenceField(dispatch);
            const maybeIndex = asIndex(key, field.length);
            // 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 maybeIndex !== undefined
                ? getProxyForNode(field.boxedAt(maybeIndex))
                : Reflect.get(dispatch, key);
        },
        set: (target, key, newValue, receiver) => {
            // 'Symbol.isConcatSpreadable' may be set on an Array instance to modify the behavior of
            // the concat method.  We allow this property to be added to the dispatch object.
            if (key === Symbol.isConcatSpreadable) {
                return Reflect.set(dispatch, key, newValue);
            }
            // For MVP, we otherwise disallow setting properties (mutation is only available via the list mutation APIs).
            return false;
        },
        has: (target, key) => {
            const field = getSequenceField(dispatch);
            const maybeIndex = asIndex(key, field.length);
            return maybeIndex !== undefined || Reflect.has(dispatch, key);
        },
        ownKeys: (target) => {
            const field = getSequenceField(dispatch);
            // TODO: Would a lazy iterator to produce the indexes work / be more efficient?
            // TODO: Need to surface 'Symbol.isConcatSpreadable' as an own key.
            return Array.from({ length: field.length }, (_, index) => `${index}`).concat("length");
        },
        getOwnPropertyDescriptor: (target, key) => {
            const field = getSequenceField(dispatch);
            const maybeIndex = asIndex(key, field.length);
            if (maybeIndex !== undefined) {
                // To satisfy 'deepEquals' level scrutiny, the property descriptor for indexed properties must
                // be a simple value property (as opposed to using getter) and declared writable/enumerable/configurable.
                return {
                    // TODO: Ideally, we would return leaves without first boxing them.  However, this is not
                    //       as simple as calling '.at' since this skips the node and returns the FieldNode's
                    //       inner field.
                    value: getProxyForNode(field.boxedAt(maybeIndex)),
                    writable: true,
                    enumerable: true,
                    configurable: true,
                };
            }
            else if (key === "length") {
                // To satisfy 'deepEquals' level scrutiny, the property descriptor for 'length' must be a simple
                // value property (as opposed to using getter) and be declared writable / non-configurable.
                return {
                    value: getSequenceField(dispatch).length,
                    writable: true,
                    enumerable: false,
                    configurable: false,
                };
            }
            return Reflect.getOwnPropertyDescriptor(dispatch, key);
        },
    });
}
exports.createListProxy = createListProxy;
//# sourceMappingURL=proxies.js.map