/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, Lazy } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { MapNodeStoredSchema } from "../../../core/index.js";
import { isTreeValue, } from "../../../feature-libraries/index.js";
import { brand } from "../../../util/index.js";
import { NodeKind, 
// eslint-disable-next-line import-x/no-deprecated
typeNameSymbol, typeSchemaSymbol, getInnerNode, getKernel, normalizeAllowedTypes, TreeNodeValid, privateDataSymbol, createTreeNodeSchemaPrivateData, CompatibilityLevel, AnnotatedAllowedTypesInternal, } from "../../core/index.js";
import { getTreeNodeSchemaInitializedData } from "../../createContext.js";
import { createFieldSchema, FieldKind } from "../../fieldSchema.js";
import { tryGetTreeNodeForField } from "../../getTreeNodeForField.js";
import { prepareForInsertion } from "../../prepareForInsertion.js";
import { unhydratedFlexTreeFromInsertable, } from "../../unhydratedFlexTreeFromInsertable.js";
import { recordLikeDataToFlexContent } from "../common.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 typeSchemaSymbol: {
                        return schema;
                    }
                    // eslint-disable-next-line import-x/no-deprecated
                    case 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 = getInnerNode(receiver);
                const field = innerNode.tryGetField(brand(key));
                if (field !== undefined) {
                    return tryGetTreeNodeForField(field);
                }
            }
            return undefined;
        },
        set: (target, key, value, receiver) => {
            if (typeof key === "symbol") {
                return false;
            }
            const innerNode = getInnerNode(receiver);
            const field = innerNode.getBoxed(brand(key));
            const kernel = getKernel(receiver);
            const innerSchema = innerNode.context.schema.nodeSchema.get(brand(schema.identifier));
            assert(innerSchema instanceof MapNodeStoredSchema, 0xc1a /* Expected MapNodeStoredSchema */);
            const mapTree = prepareForInsertion(value, createFieldSchema(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 = getInnerNode(proxy);
            const childField = innerNode.tryGetField(brand(key));
            return childField !== undefined;
        },
        ownKeys: (target) => {
            const innerNode = getInnerNode(proxy);
            return [...innerNode.keys()];
        },
        getOwnPropertyDescriptor: (target, key) => {
            if (typeof key === "symbol") {
                return undefined;
            }
            const innerNode = getInnerNode(proxy);
            const field = innerNode.tryGetField(brand(key));
            if (field === undefined) {
                return undefined;
            }
            return {
                value: tryGetTreeNodeForField(field),
                writable: true,
                enumerable: true,
                configurable: true, // Must be 'configurable' if property is absent from proxy target.
            };
        },
        defineProperty(target, key, attributes) {
            throw new UsageError("Shadowing properties of record nodes is not permitted.");
        },
        deleteProperty(target, key) {
            if (typeof key === "symbol") {
                return false;
            }
            const innerNode = getInnerNode(proxy);
            const field = innerNode.tryGetField(brand(key));
            if (field !== undefined) {
                field.editor.set(undefined, field.length === 0);
            }
            return true;
        },
    });
    return proxy;
}
class CustomRecordNodeBase extends TreeNodeValid {
    constructor(input) {
        super(input ?? {});
    }
}
CustomRecordNodeBase.kind = 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
export function recordSchema(options) {
    const { identifier, info, customizable, implicitlyConstructable, nodeOptions } = options;
    const persistedMetadata = nodeOptions?.persistedMetadata;
    const normalizedTypes = normalizeAllowedTypes(info);
    const lazyAllowedTypesIdentifiers = new Lazy(() => new Set(normalizedTypes.evaluate().map((type) => type.identifier)));
    const lazySimpleAllowedTypes = new Lazy(() => {
        return AnnotatedAllowedTypesInternal.evaluateSimpleAllowedTypes(normalizedTypes);
    });
    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 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 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 getTreeNodeSchemaInitializedData(this, {
                shallowCompatibilityTest,
                toFlexContent: (data) => recordToFlexContent(data, schema),
            });
        }
        static get allowedTypesIdentifiers() {
            return lazyAllowedTypesIdentifiers.value;
        }
        static get simpleAllowedTypes() {
            return lazySimpleAllowedTypes.value;
        }
        static get childTypes() {
            return normalizedTypes.evaluateSet();
        }
        // eslint-disable-next-line import-x/no-deprecated
        get [typeNameSymbol]() {
            return identifier;
        }
        get [typeSchemaSymbol]() {
            return Schema.constructorCached?.constructor;
        }
        [Symbol.iterator]() {
            return recordIterator(this);
        }
        get [Symbol.toStringTag]() {
            return identifier;
        }
        static get [privateDataSymbol]() {
            return (privateData ??= createTreeNodeSchemaPrivateData(this, [normalizedTypes]));
        }
    }
    Schema.constructorCached = undefined;
    Schema.identifier = identifier;
    Schema.info = info;
    Schema.implicitlyConstructable = implicitlyConstructable;
    Schema.metadata = nodeOptions?.metadata ?? {};
    Schema.persistedMetadata = persistedMetadata;
    const output = Schema;
    return output;
}
function* recordIterator(record) {
    for (const [key, value] of Object.entries(record)) {
        yield [key, value];
    }
}
/**
 * {@link TreeNodeSchemaInitializedData.shallowCompatibilityTest} for Record nodes.
 */
function shallowCompatibilityTest(data) {
    if (isTreeValue(data)) {
        return CompatibilityLevel.None;
    }
    if (Symbol.iterator in data) {
        return CompatibilityLevel.None;
    }
    return 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 UsageError(`Input data is incompatible with Record schema: ${data}`);
    }
    const fieldsIterator = Object.entries(data);
    return recordLikeDataToFlexContent(fieldsIterator, schema);
}
//# sourceMappingURL=recordNode.js.map