"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.recordSchema = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../../util/index.js");
const index_js_2 = require("../../core/index.js");
const createContext_js_1 = require("../../createContext.js");
const getTreeNodeForField_js_1 = require("../../getTreeNodeForField.js");
const fieldSchema_js_1 = require("../../fieldSchema.js");
const unhydratedFlexTreeFromInsertable_js_1 = require("../../unhydratedFlexTreeFromInsertable.js");
const index_js_3 = require("../../../feature-libraries/index.js");
const prepareForInsertion_js_1 = require("../../prepareForInsertion.js");
const common_js_1 = require("../common.js");
const index_js_4 = require("../../../core/index.js");
/**
 * Create a proxy which implements the {@link TreeRecordNode} API.
 * @param proxyTarget - Target object of the proxy.
 * @param customizable - See {@link RecordNodeSchemaOptions.customizable}.
 * @param schema - The schema of the record node.
 */
function createRecordNodeProxy(proxyTarget, customizable, schema) {
    const proxy = new Proxy(proxyTarget, {
        get: (target, key, receiver) => {
            if (typeof key === "symbol") {
                switch (key) {
                    // POJO mode records don't have TreeNode's build in members on their targets, so special case them:
                    case index_js_2.typeSchemaSymbol: {
                        return schema;
                    }
                    // eslint-disable-next-line import/no-deprecated
                    case index_js_2.typeNameSymbol: {
                        return schema.identifier;
                    }
                    case Symbol.iterator: {
                        return () => recordIterator(proxy);
                    }
                    case Symbol.toPrimitive: {
                        // Handle string interpolation and coercion to string
                        return () => Object.prototype.toString.call(proxy);
                    }
                    case Symbol.toStringTag: {
                        // In order to satisfy deep equality checks in POJO (non-customizable) mode,
                        // we cannot override the behavior of this.
                        if (customizable) {
                            // Generates nicer toString behavior for customizable records.
                            // E.g. `[object My.Record]` instead of `[object Object]`.
                            return schema.identifier;
                        }
                        break;
                    }
                    default: {
                        // No-op
                    }
                }
            }
            if (typeof key === "string") {
                const innerNode = (0, index_js_2.getOrCreateInnerNode)(receiver);
                const field = innerNode.tryGetField((0, index_js_1.brand)(key));
                if (field !== undefined) {
                    return (0, getTreeNodeForField_js_1.tryGetTreeNodeForField)(field);
                }
            }
            return undefined;
        },
        set: (target, key, value, receiver) => {
            if (typeof key === "symbol") {
                return false;
            }
            const innerNode = (0, index_js_2.getOrCreateInnerNode)(receiver);
            const field = innerNode.getBoxed((0, index_js_1.brand)(key));
            const kernel = (0, index_js_2.getKernel)(receiver);
            const innerSchema = innerNode.context.schema.nodeSchema.get((0, index_js_1.brand)(schema.identifier));
            (0, internal_1.assert)(innerSchema instanceof index_js_4.MapNodeStoredSchema, 0xc1a /* Expected MapNodeStoredSchema */);
            const mapTree = (0, prepareForInsertion_js_1.prepareForInsertion)(value, (0, fieldSchema_js_1.createFieldSchema)(fieldSchema_js_1.FieldKind.Optional, kernel.schema.info), innerNode.context, innerSchema.mapFields);
            field.editor.set(mapTree, field.length === 0);
            return true;
        },
        has: (target, key) => {
            if (typeof key === "symbol") {
                return false;
            }
            const innerNode = (0, index_js_2.getOrCreateInnerNode)(proxy);
            const childField = innerNode.tryGetField((0, index_js_1.brand)(key));
            return childField !== undefined;
        },
        ownKeys: (target) => {
            const innerNode = (0, index_js_2.getOrCreateInnerNode)(proxy);
            return [...innerNode.keys()];
        },
        getOwnPropertyDescriptor: (target, key) => {
            if (typeof key === "symbol") {
                return undefined;
            }
            const innerNode = (0, index_js_2.getOrCreateInnerNode)(proxy);
            const field = innerNode.tryGetField((0, index_js_1.brand)(key));
            if (field === undefined) {
                return undefined;
            }
            return {
                value: (0, getTreeNodeForField_js_1.tryGetTreeNodeForField)(field),
                writable: true,
                enumerable: true,
                configurable: true, // Must be 'configurable' if property is absent from proxy target.
            };
        },
        defineProperty(target, key, attributes) {
            throw new internal_2.UsageError("Shadowing properties of record nodes is not permitted.");
        },
        deleteProperty(target, key) {
            if (typeof key === "symbol") {
                return false;
            }
            const innerNode = (0, index_js_2.getOrCreateInnerNode)(proxy);
            const field = innerNode.tryGetField((0, index_js_1.brand)(key));
            if (field === undefined) {
                return false;
            }
            field.editor.set(undefined, field.length === 0);
            return true;
        },
    });
    return proxy;
}
class CustomRecordNodeBase extends index_js_2.TreeNodeValid {
    constructor(input) {
        super(input ?? {});
    }
}
CustomRecordNodeBase.kind = index_js_2.NodeKind.Record;
/**
 * Define a {@link TreeNodeSchema} for a {@link TreeRecordNode}.
 *
 * @param fields - Schema for fields of the object node's schema. Defines what children can be placed under each key.
 * @param persistedMetadata -
 */
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function recordSchema(options) {
    const { identifier, info, customizable, implicitlyConstructable, metadata, persistedMetadata, } = options;
    const lazyChildTypes = new internal_1.Lazy(() => (0, index_js_2.normalizeAllowedTypes)((0, index_js_2.unannotateImplicitAllowedTypes)(info)));
    const lazyAllowedTypesIdentifiers = new internal_1.Lazy(() => new Set([...lazyChildTypes.value].map((type) => type.identifier)));
    let privateData;
    class Schema extends CustomRecordNodeBase {
        static prepareInstance(instance, flexNode) {
            // Differentiate between the following cases:
            //
            // Case 1: Direct construction (POJO emulation)
            //
            //     const Foo = schemaFactory.record("Foo", schemaFactory.number);
            //
            //     assert.deepEqual(new Foo({ bar: 42 }), { bar: 42 },
            //		   "Prototype chain equivalent to POJO.");
            //
            // Case 2: Subclass construction (Customizable Record)
            //
            // 	   class Foo extends schemaFactory.record("Foo", 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 proxyTarget = customizable ? instance : {};
            return createRecordNodeProxy(proxyTarget, customizable, this);
        }
        static buildRawNode(instance, input) {
            return (0, unhydratedFlexTreeFromInsertable_js_1.unhydratedFlexTreeFromInsertable)(input, this);
        }
        static oneTimeSetup() {
            // First run, do extra validation.
            // TODO: provide a way for TreeConfiguration to trigger this same validation to ensure it gets run early.
            // Scan for shadowing inherited members which won't work, but stop scan early to allow shadowing built in (which seems to work ok).
            {
                let prototype = this.prototype;
                // There isn't a clear cleaner way to author this loop.
                while (prototype !== Schema.prototype) {
                    for (const key of Object.getOwnPropertyNames(prototype)) {
                        if (
                        // constructor is a special case, since one is built in on the derived type, and shadowing it works fine since we only use it before fields are applied.
                        key !== "constructor" &&
                            Reflect.getOwnPropertyDescriptor(prototype, key) !== undefined) {
                            throw new internal_2.UsageError(`Schema ${identifier} defines an inherited property "${key.toString()}" which could shadow a legal entry. Since child fields are exposed as own properties, shadowing properties of record nodes is not permitted.`);
                        }
                    }
                    // Since this stops at CustomRecordNode, it should never see a null prototype, so this case is safe.
                    // Additionally, if the prototype chain is ever messed up such that CustomRecordNode is not in it,
                    // the null that would show up here does at least ensure this code throws instead of hanging.
                    prototype = Reflect.getPrototypeOf(prototype);
                }
            }
            const schema = this;
            return (0, createContext_js_1.getTreeNodeSchemaInitializedData)(this, {
                shallowCompatibilityTest,
                toFlexContent: (data) => recordToFlexContent(data, schema),
            });
        }
        static get allowedTypesIdentifiers() {
            return lazyAllowedTypesIdentifiers.value;
        }
        static get childTypes() {
            return lazyChildTypes.value;
        }
        // eslint-disable-next-line import/no-deprecated
        get [index_js_2.typeNameSymbol]() {
            return identifier;
        }
        get [index_js_2.typeSchemaSymbol]() {
            return Schema.constructorCached?.constructor;
        }
        [Symbol.iterator]() {
            return recordIterator(this);
        }
        get [Symbol.toStringTag]() {
            return identifier;
        }
        static get [index_js_2.privateDataSymbol]() {
            return (privateData ??= (0, index_js_2.createTreeNodeSchemaPrivateData)(this, [info], (storedOptions) => new index_js_4.MapNodeStoredSchema({
                kind: index_js_3.FieldKinds.optional.identifier,
                types: (0, index_js_2.convertAllowedTypes)(info, storedOptions),
                persistedMetadata,
            }, persistedMetadata)));
        }
    }
    Schema.constructorCached = undefined;
    Schema.identifier = identifier;
    Schema.info = info;
    Schema.implicitlyConstructable = implicitlyConstructable;
    Schema.metadata = metadata ?? {};
    Schema.persistedMetadata = persistedMetadata;
    const output = Schema;
    return output;
}
exports.recordSchema = recordSchema;
function* recordIterator(record) {
    for (const [key, value] of Object.entries(record)) {
        yield [key, value];
    }
}
/**
 * {@link TreeNodeSchemaInitializedData.shallowCompatibilityTest} for Record nodes.
 */
function shallowCompatibilityTest(data) {
    if ((0, index_js_3.isTreeValue)(data)) {
        return index_js_2.CompatibilityLevel.None;
    }
    if (Symbol.iterator in data) {
        return index_js_2.CompatibilityLevel.None;
    }
    return index_js_2.CompatibilityLevel.Normal;
}
/**
 * {@link TreeNodeSchemaInitializedData.toFlexContent} for Record nodes.
 *
 * Transforms data under a Record schema.
 * @param data - The tree data to be transformed. Must be a Record-like object.
 * @param schema - The schema to comply with.
 */
function recordToFlexContent(data, schema) {
    if (!(typeof data === "object" && data !== null)) {
        throw new internal_2.UsageError(`Input data is incompatible with Record schema: ${data}`);
    }
    const fieldsIterator = Object.entries(data);
    return (0, common_js_1.recordLikeDataToFlexContent)(fieldsIterator, schema);
}
//# sourceMappingURL=recordNode.js.map