/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { makeCodecFamily, withSchemaValidation, } from "../../codec/index.js";
import { brand, fail, idAllocatorFromMaxId, setInNestedMap, tryGetFromNestedMap, } from "../../util/index.js";
import { chunkFieldSingle, defaultChunkPolicy, } from "../chunked-forest/index.js";
import { TreeCompressionStrategy } from "../treeCompressionUtils.js";
import { genericFieldKind } from "./genericFieldKind.js";
import { EncodedModularChangeset, } from "./modularChangeFormat.js";
export function makeModularChangeCodecFamily(fieldKindConfigurations, revisionTagCodec, fieldsCodec, { jsonValidator: validator }, chunkCompressionStrategy = TreeCompressionStrategy.Compressed) {
    return makeCodecFamily(Array.from(fieldKindConfigurations.entries(), ([version, fieldKinds]) => [
        version,
        makeModularChangeCodec(fieldKinds, revisionTagCodec, fieldsCodec, { jsonValidator: validator }, chunkCompressionStrategy),
    ]));
}
function makeModularChangeCodec(fieldKinds, revisionTagCodec, fieldsCodec, { jsonValidator: validator }, chunkCompressionStrategy = TreeCompressionStrategy.Compressed) {
    const getMapEntry = ({ kind, formatVersion }) => {
        const codec = kind.changeHandler.codecsFactory(revisionTagCodec).resolve(formatVersion);
        return {
            codec,
            compiledSchema: codec.json.encodedSchema
                ? validator.compile(codec.json.encodedSchema)
                : undefined,
        };
    };
    /**
     * The codec version for the generic field kind.
     */
    const genericFieldKindFormatVersion = 1;
    const fieldChangesetCodecs = new Map([
        [
            genericFieldKind.identifier,
            getMapEntry({ kind: genericFieldKind, formatVersion: genericFieldKindFormatVersion }),
        ],
    ]);
    fieldKinds.forEach((entry, identifier) => {
        fieldChangesetCodecs.set(identifier, getMapEntry(entry));
    });
    const getFieldChangesetCodec = (fieldKind) => {
        const entry = fieldChangesetCodecs.get(fieldKind);
        assert(entry !== undefined, 0x5ea /* Tried to encode unsupported fieldKind */);
        return entry;
    };
    function encodeFieldChangesForJson(change, context, nodeChanges) {
        const fieldContext = {
            baseContext: context,
            encodeNode: (nodeId) => {
                const node = tryGetFromNestedMap(nodeChanges, nodeId.revision, nodeId.localId);
                assert(node !== undefined, 0x92e /* Unknown node ID */);
                return encodeNodeChangesForJson(node, fieldContext);
            },
            decodeNode: () => fail("Should not decode nodes during field encoding"),
        };
        return encodeFieldChangesForJsonI(change, fieldContext);
    }
    function encodeFieldChangesForJsonI(change, context) {
        const encodedFields = [];
        for (const [field, fieldChange] of change) {
            const { codec, compiledSchema } = getFieldChangesetCodec(fieldChange.fieldKind);
            const encodedChange = codec.json.encode(fieldChange.change, context);
            if (compiledSchema !== undefined && !compiledSchema.check(encodedChange)) {
                fail("Encoded change didn't pass schema validation.");
            }
            const fieldKey = field;
            const encodedField = {
                fieldKey,
                fieldKind: fieldChange.fieldKind,
                change: encodedChange,
            };
            encodedFields.push(encodedField);
        }
        return encodedFields;
    }
    function encodeNodeChangesForJson(change, context) {
        const encodedChange = {};
        const { fieldChanges, nodeExistsConstraint } = change;
        if (fieldChanges !== undefined) {
            encodedChange.fieldChanges = encodeFieldChangesForJsonI(fieldChanges, context);
        }
        if (nodeExistsConstraint !== undefined) {
            encodedChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return encodedChange;
    }
    function decodeFieldChangesFromJson(encodedChange, context, idAllocator) {
        const decodedNodes = new Map();
        const fieldContext = {
            baseContext: context,
            encodeNode: () => fail("Should not encode nodes during field decoding"),
            decodeNode: (encodedNode) => {
                const node = decodeNodeChangesetFromJson(encodedNode, fieldContext);
                const nodeId = { localId: brand(idAllocator.allocate()) };
                setInNestedMap(decodedNodes, nodeId.revision, nodeId.localId, node);
                return nodeId;
            },
        };
        const decodedFields = decodeFieldChangesFromJsonI(encodedChange, fieldContext);
        return [decodedFields, decodedNodes];
    }
    function decodeFieldChangesFromJsonI(encodedChange, context) {
        const decodedFields = new Map();
        for (const field of encodedChange) {
            const { codec, compiledSchema } = getFieldChangesetCodec(field.fieldKind);
            if (compiledSchema !== undefined && !compiledSchema.check(field.change)) {
                fail("Encoded change didn't pass schema validation.");
            }
            const fieldChangeset = codec.json.decode(field.change, context);
            const fieldKey = brand(field.fieldKey);
            decodedFields.set(fieldKey, {
                fieldKind: field.fieldKind,
                change: brand(fieldChangeset),
            });
        }
        return decodedFields;
    }
    function decodeNodeChangesetFromJson(encodedChange, context) {
        const decodedChange = {};
        const { fieldChanges, nodeExistsConstraint } = encodedChange;
        if (fieldChanges !== undefined) {
            decodedChange.fieldChanges = decodeFieldChangesFromJsonI(fieldChanges, context);
        }
        if (nodeExistsConstraint !== undefined) {
            decodedChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return decodedChange;
    }
    function encodeDetachedNodes(detachedNodes, context) {
        if (detachedNodes === undefined) {
            return undefined;
        }
        const treesToEncode = [];
        const buildsArray = Array.from(detachedNodes.entries()).map(([r, commitBuilds]) => {
            const commitBuildsEncoded = Array.from(commitBuilds.entries()).map(([i, t]) => {
                treesToEncode.push(t.cursor());
                const treeIndexInBatch = treesToEncode.length - 1;
                return [i, treeIndexInBatch];
            });
            // `undefined` does not round-trip through JSON strings, so it needs special handling.
            // Most entries will have an undefined revision due to the revision information being inherited from the `ModularChangeset`.
            // We therefore optimize for the common case by omitting the revision when it is undefined.
            return r !== undefined
                ? [commitBuildsEncoded, revisionTagCodec.encode(r, context)]
                : [commitBuildsEncoded];
        });
        return buildsArray.length === 0
            ? undefined
            : {
                builds: buildsArray,
                trees: fieldsCodec.encode(treesToEncode, {
                    encodeType: chunkCompressionStrategy,
                    schema: context.schema,
                }),
            };
    }
    function decodeDetachedNodes(encoded, context) {
        if (encoded === undefined || encoded.builds.length === 0) {
            return undefined;
        }
        const chunks = fieldsCodec.decode(encoded.trees, {
            encodeType: chunkCompressionStrategy,
        });
        const getChunk = (index) => {
            assert(index < chunks.length, 0x898 /* out of bounds index for build chunk */);
            return chunkFieldSingle(chunks[index], defaultChunkPolicy);
        };
        const map = new Map();
        encoded.builds.forEach((build) => {
            // EncodedRevisionTag cannot be an array so this ensures that we can isolate the tuple
            const revision = build[1] === undefined ? undefined : revisionTagCodec.decode(build[1], context);
            map.set(revision, new Map(build[0].map(([i, n]) => [i, getChunk(n)])));
        });
        return map;
    }
    function encodeRevisionInfos(revisions, context) {
        const encodedRevisions = [];
        for (const revision of revisions) {
            const encodedRevision = {
                revision: revisionTagCodec.encode(revision.revision, context),
            };
            if (revision.rollbackOf !== undefined) {
                encodedRevision.rollbackOf = revisionTagCodec.encode(revision.rollbackOf, context);
            }
            encodedRevisions.push(encodedRevision);
        }
        return encodedRevisions;
    }
    function decodeRevisionInfos(revisions, context) {
        const decodedRevisions = [];
        for (const revision of revisions) {
            const decodedRevision = {
                revision: revisionTagCodec.decode(revision.revision, context),
            };
            if (revision.rollbackOf !== undefined) {
                decodedRevision.rollbackOf = revisionTagCodec.decode(revision.rollbackOf, context);
            }
            decodedRevisions.push(decodedRevision);
        }
        return decodedRevisions;
    }
    const modularChangeCodec = {
        encode: (change, context) => {
            // Destroys only exist in rollback changesets, which are never sent.
            assert(change.destroys === undefined, 0x899 /* Unexpected changeset with destroys */);
            return {
                maxId: change.maxId,
                revisions: change.revisions === undefined
                    ? change.revisions
                    : encodeRevisionInfos(change.revisions, context),
                changes: encodeFieldChangesForJson(change.fieldChanges, context, change.nodeChanges),
                builds: encodeDetachedNodes(change.builds, context),
                refreshers: encodeDetachedNodes(change.refreshers, context),
            };
        },
        decode: (encodedChange, context) => {
            const [fieldChanges, nodeChanges] = decodeFieldChangesFromJson(encodedChange.changes, context, idAllocatorFromMaxId(encodedChange.maxId));
            const decoded = {
                fieldChanges,
                nodeChanges,
            };
            if (encodedChange.builds !== undefined) {
                decoded.builds = decodeDetachedNodes(encodedChange.builds, context);
            }
            if (encodedChange.refreshers !== undefined) {
                decoded.refreshers = decodeDetachedNodes(encodedChange.builds, context);
            }
            if (encodedChange.revisions !== undefined) {
                decoded.revisions = decodeRevisionInfos(encodedChange.revisions, context);
            }
            if (encodedChange.maxId !== undefined) {
                decoded.maxId = encodedChange.maxId;
            }
            return decoded;
        },
    };
    return withSchemaValidation(EncodedModularChangeset, modularChangeCodec, validator);
}
//# sourceMappingURL=modularChangeCodecs.js.map