/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { FieldKinds, FlexObjectNodeSchema, isFlexTreeNode, } from "../feature-libraries/index.js";
import { getProxyForField, markContentType, prepareContentForInsert, } from "./proxies.js";
import { getFlexNode, setFlexNode } from "./proxyBinding.js";
import { getSimpleFieldSchema, getSimpleNodeSchema } from "./schemaCaching.js";
import { getStoredKey, getExplicitStoredKey, } from "./schemaTypes.js";
import { cursorFromNodeData } from "./toMapTree.js";
import { brand, fail } from "../util/index.js";
import { getFlexSchema } from "./toFlexSchema.js";
import { RawTreeNode, rawError } from "./rawNode.js";
// TODO: decide how to deal with dependencies on flex-tree implementation.
// eslint-disable-next-line import/no-internal-modules
import { LazyObjectNode, getBoxedField } from "../feature-libraries/flex-tree/lazyNode.js";
/**
 * Caches a {@link SimpleKeyToFlexKeyMap} for a given {@link TreeNodeSchema}.
 */
const simpleKeyToFlexKeyCache = new WeakMap();
/**
 * Caches the mappings from view keys to stored keys for the provided object field schemas in {@link simpleKeyToFlexKeyCache}.
 */
function getOrCreateFlexKeyMapping(nodeSchema, fields) {
    let keyMap = simpleKeyToFlexKeyCache.get(nodeSchema);
    if (keyMap === undefined) {
        keyMap = new Map();
        for (const [viewKey, fieldSchema] of Object.entries(fields)) {
            // Only specify mapping if the stored key differs from the view key.
            // No entry in this map will indicate that the two keys are the same.
            const storedKey = getStoredKey(viewKey, fieldSchema);
            if (viewKey !== storedKey) {
                keyMap.set(brand(viewKey), brand(storedKey));
            }
        }
        simpleKeyToFlexKeyCache.set(nodeSchema, keyMap);
    }
    return keyMap;
}
/**
 * @param allowAdditionalProperties - If true, setting of unexpected properties will be forwarded to the target object.
 * Otherwise setting of unexpected properties will error.
 * @param customTargetObject - Target object of the proxy.
 * If not provided `{}` is used for the target.
 */
function createObjectProxy(schema, allowAdditionalProperties, targetObject = {}) {
    // Performance optimization: cache view key => stored key mapping.
    const flexKeyMap = getOrCreateFlexKeyMapping(schema, schema.info);
    function getFlexKey(viewKey) {
        return flexKeyMap.get(viewKey) ?? viewKey;
    }
    // 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(targetObject, {
        get(target, viewKey) {
            const flexKey = getFlexKey(viewKey);
            const field = getFlexNode(proxy).tryGetField(flexKey);
            if (field !== undefined) {
                return getProxyForField(field);
            }
            // Pass the proxy as the receiver here, so that any methods on the prototype receive `proxy` as `this`.
            return Reflect.get(target, viewKey, proxy);
        },
        set(target, viewKey, value) {
            const flexNode = getFlexNode(proxy);
            const flexNodeSchema = flexNode.schema;
            assert(flexNodeSchema instanceof FlexObjectNodeSchema, 0x888 /* invalid schema */);
            const flexKey = getFlexKey(viewKey);
            const flexFieldSchema = flexNodeSchema.objectNodeFields.get(flexKey);
            if (flexFieldSchema === undefined) {
                return allowAdditionalProperties ? Reflect.set(target, viewKey, value) : false;
            }
            // TODO: Is it safe to assume 'content' is a LazyObjectNode?
            assert(flexNode instanceof LazyObjectNode, 0x7e0 /* invalid content */);
            assert(typeof viewKey === "string", 0x7e1 /* invalid key */);
            const field = getBoxedField(flexNode, flexKey, flexFieldSchema);
            const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema);
            const simpleNodeFields = simpleNodeSchema.info;
            if (simpleNodeFields[viewKey] === undefined) {
                fail(`Field key '${viewKey}' not found in schema.`);
            }
            const simpleFieldSchema = getSimpleFieldSchema(flexFieldSchema, simpleNodeFields[viewKey]);
            switch (field.schema.kind) {
                case FieldKinds.required:
                case FieldKinds.optional: {
                    const typedField = field;
                    const content = prepareContentForInsert(value, flexNode.context.forest);
                    const cursor = cursorFromNodeData(content, simpleFieldSchema.allowedTypes);
                    typedField.content = cursor;
                    break;
                }
                default:
                    fail("invalid FieldKind");
            }
            return true;
        },
        has: (target, viewKey) => {
            const fields = schema.info;
            return (fields[viewKey] !== undefined ||
                (allowAdditionalProperties ? Reflect.has(target, viewKey) : false));
        },
        ownKeys: (target) => {
            const fields = schema.info;
            return [
                ...Object.keys(fields),
                ...(allowAdditionalProperties ? Reflect.ownKeys(target) : []),
            ];
        },
        getOwnPropertyDescriptor: (target, viewKey) => {
            const flexKey = getFlexKey(viewKey);
            const field = getFlexNode(proxy).tryGetField(flexKey);
            if (field === undefined) {
                return allowAdditionalProperties
                    ? Reflect.getOwnPropertyDescriptor(target, viewKey)
                    : undefined;
            }
            const p = {
                value: getProxyForField(field),
                writable: true,
                enumerable: true,
                configurable: true, // Must be 'configurable' if property is absent from proxy target.
            };
            return p;
        },
    });
    return proxy;
}
/**
 * Define a {@link TreeNodeSchema} for a {@link TreeObjectNode}.
 *
 * @param name - Unique identifier for this schema within this factory's scope.
 * @param fields - Schema for fields of the object node's schema. Defines what children can be placed under each key.
 */
export function objectSchema(base) {
    // Ensure no collisions between final set of view keys, and final set of stored keys (including those
    // implicitly derived from view keys)
    assertUniqueKeys(base.identifier, base.info);
    class schema extends base {
        constructor(input) {
            super(input);
            // Differentiate between the following cases:
            //
            // Case 1: Direct construction (POJO emulation)
            //
            //     const Foo = schemaFactory.object("Foo", {bar: schemaFactory.number});
            //
            //     assert.deepEqual(new Foo({ bar: 42 }), { bar: 42 },
            //		   "Prototype chain equivalent to POJO.");
            //
            // Case 2: Subclass construction (Customizable Object)
            //
            // 	   class Foo extends schemaFactory.object("Foo", {bar: schemaFactory.number}) {}
            //
            // 	   assert.notDeepEqual(new Foo({ bar: 42 }), { bar: 42 },
            // 	       "Subclass prototype chain differs from POJO.");
            //
            // In Case 1 (POJO emulation), the prototype chain match '{}' (proxyTarget = undefined)
            // In Case 2 (Customizable Object), the prototype chain include the user's subclass (proxyTarget = this)
            const customizable = this.constructor !== schema;
            const proxyTarget = customizable ? this : undefined;
            const flexSchema = getFlexSchema(this.constructor);
            assert(flexSchema instanceof FlexObjectNodeSchema, "invalid flex schema");
            const flexNode = isFlexTreeNode(input)
                ? input
                : new RawObjectNode(flexSchema, copyContent(flexSchema.name, input));
            const proxy = createObjectProxy(this.constructor, customizable, proxyTarget);
            setFlexNode(proxy, flexNode);
            return proxy;
        }
    }
    return schema;
}
/**
 * The implementation of an object node created by {@link createRawNode}.
 */
export class RawObjectNode extends RawTreeNode {
    get localNodeKey() {
        throw rawError("Reading local node keys");
    }
}
/**
 * Ensures that the set of view keys in the schema is unique.
 * Also ensure that the final set of stored keys (including those implicitly derived from view keys) is unique.
 * @throws Throws a `UsageError` if either of the key uniqueness invariants is violated.
 */
function assertUniqueKeys(schemaName, fields) {
    // Verify that there are no duplicates among the explicitly specified stored keys.
    const explicitStoredKeys = new Set();
    for (const schema of Object.values(fields)) {
        const storedKey = getExplicitStoredKey(schema);
        if (storedKey === undefined) {
            continue;
        }
        if (explicitStoredKeys.has(storedKey)) {
            throw new UsageError(`Duplicate stored key "${storedKey}" in schema "${schemaName}". Stored keys must be unique within an object schema.`);
        }
        explicitStoredKeys.add(storedKey);
    }
    // Verify that there are no duplicates among the derived
    // (including those implicitly derived from view keys) stored keys.
    const derivedStoredKeys = new Set();
    for (const [viewKey, schema] of Object.entries(fields)) {
        const storedKey = getStoredKey(viewKey, schema);
        if (derivedStoredKeys.has(storedKey)) {
            throw new UsageError(`Stored key "${storedKey}" in schema "${schemaName}" conflicts with a property key of the same name, which is not overridden by a stored key. The final set of stored keys in an object schema must be unique.`);
        }
        derivedStoredKeys.add(storedKey);
    }
}
function copyContent(typeName, content) {
    const copy = { ...content };
    markContentType(typeName, copy);
    return copy;
}
//# sourceMappingURL=objectNode.js.map