/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { fail } from "@fluidframework/core-utils/internal";
import { DiscriminatedUnionDispatcher } from "../../codec/index.js";
import { brand, invertMap, } from "../../util/index.js";
import { PersistedValueSchema, } from "./formatV1.js";
/**
 * The format version for the schema.
 */
export var SchemaVersion;
(function (SchemaVersion) {
    SchemaVersion[SchemaVersion["v1"] = 1] = "v1";
    /**
     * Adds persisted metadata to the node schema and field schema.
     */
    SchemaVersion[SchemaVersion["v2"] = 2] = "v2";
})(SchemaVersion || (SchemaVersion = {}));
/**
 * Schema for what {@link TreeLeafValue} is allowed on a Leaf node.
 * @privateRemarks
 * See also {@link TreeValue}.
 * If further stabilizing this,
 * consider the implications of how this might prevent adding of new leaf types in the future.
 * Maybe add a disclaimer that it might be extended like on {@link NodeKind}?
 * @alpha
 */
export var ValueSchema;
(function (ValueSchema) {
    ValueSchema[ValueSchema["Number"] = 0] = "Number";
    ValueSchema[ValueSchema["String"] = 1] = "String";
    ValueSchema[ValueSchema["Boolean"] = 2] = "Boolean";
    ValueSchema[ValueSchema["FluidHandle"] = 3] = "FluidHandle";
    ValueSchema[ValueSchema["Null"] = 4] = "Null";
})(ValueSchema || (ValueSchema = {}));
/**
 * Identifier used for the FieldKind for fields which must be empty.
 *
 * @remarks
 * This mainly show up in:
 *
 * 1. The root default field for documents.
 *
 * 2. The schema used for out of schema fields (which thus must be empty/not exist) on object and leaf nodes.
 */
export const forbiddenFieldKindIdentifier = "Forbidden";
/**
 * A schema for empty fields (fields which must always be empty).
 * There are multiple ways this could be encoded, but this is the most explicit.
 */
export const storedEmptyFieldSchema = {
    // This kind requires the field to be empty.
    kind: brand(forbiddenFieldKindIdentifier),
    // This type set also forces the field to be empty not not allowing any types as all.
    types: new Set(),
    persistedMetadata: undefined,
};
/**
 * Identifier used for the FieldKind for fields of type identifier.
 */
export const identifierFieldKindIdentifier = "Identifier";
/**
 */
export class TreeNodeStoredSchema {
    /**
     * Constructor for a TreeNodeStoredSchema.
     * @param metadata - Persisted metadata for this node schema.
     */
    constructor(metadata) {
        this.metadata = metadata;
    }
}
/**
 */
export class ObjectNodeStoredSchema extends TreeNodeStoredSchema {
    /**
     * @param objectNodeFields -
     * Schema for fields with keys scoped to this TreeNodeStoredSchema.
     * This refers to the TreeFieldStoredSchema directly
     * (as opposed to just supporting FieldSchemaIdentifier and having a central FieldKey -\> TreeFieldStoredSchema map).
     * This allows us short friendly field keys which can be ergonomically used as field names in code.
     * It also interoperates well with mapFields being used as a map with arbitrary data as keys.
     */
    constructor(objectNodeFields, metadata) {
        super(metadata);
        this.objectNodeFields = objectNodeFields;
    }
    encodeV1() {
        const fieldsObject = this.encodeFieldsObject(encodeFieldSchemaV1);
        return {
            object: fieldsObject,
        };
    }
    encodeV2() {
        const fieldsObject = this.encodeFieldsObject(encodeFieldSchemaV2);
        const kind = { object: fieldsObject };
        return { kind, metadata: this.metadata };
    }
    getFieldSchema(field) {
        return this.objectNodeFields.get(field) ?? storedEmptyFieldSchema;
    }
    encodeFieldsObject(encodeFieldSchema) {
        const fieldsObject = Object.create(null);
        // Sort fields to ensure output is identical for for equivalent schema (since field order is not considered significant).
        // This makes comparing schema easier, and ensures chunk reuse for schema summaries isn't needlessly broken.
        for (const key of [...this.objectNodeFields.keys()].sort()) {
            Object.defineProperty(fieldsObject, key, {
                enumerable: true,
                configurable: true,
                writable: true,
                value: encodeFieldSchema(this.objectNodeFields.get(key) ?? fail(0xae7 /* missing field */)),
            });
        }
        return fieldsObject;
    }
}
/**
 */
export class MapNodeStoredSchema extends TreeNodeStoredSchema {
    /**
     * @param mapFields -
     * Allows using the fields as a map, with the keys being
     * FieldKeys and the values being constrained by this TreeFieldStoredSchema.
     * Usually `FieldKind.Value` should NOT be used here
     * since no nodes can ever be in schema if you use `FieldKind.Value` here
     * (that would require infinite children).
     */
    constructor(mapFields, metadata) {
        super(metadata);
        this.mapFields = mapFields;
    }
    encodeV1() {
        return {
            map: encodeFieldSchemaV1(this.mapFields),
        };
    }
    encodeV2() {
        const kind = { map: encodeFieldSchemaV2(this.mapFields) };
        return { kind, metadata: this.metadata };
    }
    getFieldSchema(field) {
        return this.mapFields;
    }
}
/**
 */
export class LeafNodeStoredSchema extends TreeNodeStoredSchema {
    /**
     * @param leafValue -
     * There are several approaches for how to store actual data in the tree
     * (special node types, special field contents, data on nodes etc.)
     * as well as several options about how the data should be modeled at this level
     * (byte sequence? javascript type? json?),
     * as well as options for how much of this would be exposed in the schema language
     * (ex: would all nodes with values be special built-ins, or could any schema add them?)
     * A simple easy to do in javascript approach is taken here:
     * this is not intended to be a suggestion of what approach to take, or what to expose in the schema language.
     * This is simply one approach that can work for modeling them in the internal schema representation.
     */
    constructor(leafValue) {
        // No metadata for leaf nodes.
        super(undefined);
        this.leafValue = leafValue;
    }
    encodeV1() {
        return {
            leaf: encodeValueSchema(this.leafValue),
        };
    }
    encodeV2() {
        return {
            // No metadata for leaf nodes, so don't emit a metadata field.
            kind: {
                leaf: encodeValueSchema(this.leafValue),
            },
        };
    }
    getFieldSchema(field) {
        return storedEmptyFieldSchema;
    }
}
export const storedSchemaDecodeDispatcher = new DiscriminatedUnionDispatcher({
    leaf: (data) => (metadata) => new LeafNodeStoredSchema(decodeValueSchema(data)),
    object: (data) => (metadata) => {
        const map = new Map();
        for (const [key, value] of Object.entries(data)) {
            map.set(key, decodeFieldSchema(value));
        }
        return new ObjectNodeStoredSchema(map, metadata);
    },
    map: (data) => (metadata) => new MapNodeStoredSchema(decodeFieldSchema(data), metadata),
});
const valueSchemaEncode = new Map([
    [ValueSchema.Number, PersistedValueSchema.Number],
    [ValueSchema.String, PersistedValueSchema.String],
    [ValueSchema.Boolean, PersistedValueSchema.Boolean],
    [ValueSchema.FluidHandle, PersistedValueSchema.FluidHandle],
    [ValueSchema.Null, PersistedValueSchema.Null],
]);
const valueSchemaDecode = invertMap(valueSchemaEncode);
function encodeValueSchema(inMemory) {
    return valueSchemaEncode.get(inMemory) ?? fail(0xae8 /* missing PersistedValueSchema */);
}
function decodeValueSchema(inMemory) {
    return valueSchemaDecode.get(inMemory) ?? fail(0xae9 /* missing ValueSchema */);
}
export function encodeFieldSchemaV1(schema) {
    return {
        kind: schema.kind,
        // Types are sorted by identifier to improve stability of persisted data to increase chance of schema blob reuse.
        types: [...schema.types].sort(),
    };
}
export function encodeFieldSchemaV2(schema) {
    const fieldSchema = encodeFieldSchemaV1(schema);
    // Omit metadata from the output if it is undefined
    return schema.persistedMetadata !== undefined
        ? { ...fieldSchema, metadata: schema.persistedMetadata }
        : { ...fieldSchema };
}
export function decodeFieldSchema(schema) {
    const out = {
        // TODO: maybe provide actual FieldKind objects here, error on unrecognized kinds.
        kind: schema.kind,
        types: new Set(schema.types),
        persistedMetadata: schema.metadata,
    };
    return out;
}
//# sourceMappingURL=schema.js.map