"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeModularChangeCodecFamily = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../codec/index.js");
const index_js_2 = require("../../util/index.js");
const index_js_3 = require("../chunked-forest/index.js");
const treeCompressionUtils_js_1 = require("../treeCompressionUtils.js");
const genericFieldKind_js_1 = require("./genericFieldKind.js");
const modularChangeFormat_js_1 = require("./modularChangeFormat.js");
const modularChangeTypes_js_1 = require("./modularChangeTypes.js");
function makeModularChangeCodecFamily(fieldKindConfigurations, revisionTagCodec, fieldsCodec, codecOptions, chunkCompressionStrategy = treeCompressionUtils_js_1.TreeCompressionStrategy.Compressed) {
    return (0, index_js_1.makeCodecFamily)(Array.from(fieldKindConfigurations.entries(), ([version, fieldKinds]) => [
        version,
        makeModularChangeCodec(fieldKinds, revisionTagCodec, fieldsCodec, codecOptions, chunkCompressionStrategy),
    ]));
}
exports.makeModularChangeCodecFamily = makeModularChangeCodecFamily;
function makeModularChangeCodec(fieldKinds, revisionTagCodec, fieldsCodec, codecOptions, chunkCompressionStrategy = treeCompressionUtils_js_1.TreeCompressionStrategy.Compressed) {
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    const getMapEntry = ({ kind, formatVersion }) => {
        const codec = kind.changeHandler.codecsFactory(revisionTagCodec).resolve(formatVersion);
        return {
            codec,
            compiledSchema: codec.json.encodedSchema
                ? codecOptions.jsonValidator.compile(codec.json.encodedSchema)
                : undefined,
        };
    };
    /**
     * The codec version for the generic field kind.
     */
    const genericFieldKindFormatVersion = 1;
    const fieldChangesetCodecs = new Map([
        [
            genericFieldKind_js_1.genericFieldKind.identifier,
            getMapEntry({ kind: genericFieldKind_js_1.genericFieldKind, formatVersion: genericFieldKindFormatVersion }),
        ],
    ]);
    fieldKinds.forEach((entry, identifier) => {
        fieldChangesetCodecs.set(identifier, getMapEntry(entry));
    });
    const getFieldChangesetCodec = (fieldKind) => {
        const entry = fieldChangesetCodecs.get(fieldKind);
        (0, internal_1.assert)(entry !== undefined, 0x5ea /* Tried to encode unsupported fieldKind */);
        return entry;
    };
    function encodeFieldChangesForJson(change, context, nodeChanges) {
        const fieldContext = {
            baseContext: context,
            encodeNode: (nodeId) => {
                const node = nodeChanges.get([nodeId.revision, nodeId.localId]);
                (0, internal_1.assert)(node !== undefined, 0x92e /* Unknown node ID */);
                return encodeNodeChangesForJson(node, fieldContext);
            },
            decodeNode: () => (0, internal_1.fail)(0xb1e /* 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)) {
                (0, internal_1.fail)(0xb1f /* 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 = {};
        // Note: revert constraints are ignored for now because they would only be needed if we supported reverting changes made by peers.
        const { fieldChanges, nodeExistsConstraint } = change;
        if (fieldChanges !== undefined) {
            encodedChange.fieldChanges = encodeFieldChangesForJsonI(fieldChanges, context);
        }
        if (nodeExistsConstraint !== undefined) {
            encodedChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return encodedChange;
    }
    function decodeFieldChangesFromJson(encodedChange, parentId, decoded, context, idAllocator) {
        const decodedFields = new Map();
        for (const field of encodedChange) {
            const { codec, compiledSchema } = getFieldChangesetCodec(field.fieldKind);
            if (compiledSchema !== undefined && !compiledSchema.check(field.change)) {
                (0, internal_1.fail)(0xb20 /* Encoded change didn't pass schema validation. */);
            }
            const fieldId = {
                nodeId: parentId,
                field: field.fieldKey,
            };
            const fieldContext = {
                baseContext: context,
                encodeNode: () => (0, internal_1.fail)(0xb21 /* Should not encode nodes during field decoding */),
                decodeNode: (encodedNode) => {
                    const nodeId = {
                        revision: context.revision,
                        localId: (0, index_js_2.brand)(idAllocator.allocate()),
                    };
                    const node = decodeNodeChangesetFromJson(encodedNode, nodeId, decoded, context, idAllocator);
                    decoded.nodeChanges.set([nodeId.revision, nodeId.localId], node);
                    decoded.nodeToParent.set([nodeId.revision, nodeId.localId], fieldId);
                    return nodeId;
                },
            };
            const fieldChangeset = codec.json.decode(field.change, fieldContext);
            const crossFieldKeys = getChangeHandler(fieldKinds, field.fieldKind).getCrossFieldKeys(fieldChangeset);
            for (const { key, count } of crossFieldKeys) {
                decoded.crossFieldKeys.set(key, count, fieldId);
            }
            const fieldKey = (0, index_js_2.brand)(field.fieldKey);
            decodedFields.set(fieldKey, {
                fieldKind: field.fieldKind,
                change: (0, index_js_2.brand)(fieldChangeset),
            });
        }
        return decodedFields;
    }
    function decodeNodeChangesetFromJson(encodedChange, id, decoded, context, idAllocator) {
        const decodedChange = {};
        const { fieldChanges, nodeExistsConstraint } = encodedChange;
        if (fieldChanges !== undefined) {
            decodedChange.fieldChanges = decodeFieldChangesFromJson(fieldChanges, id, decoded, context, idAllocator);
        }
        if (nodeExistsConstraint !== undefined) {
            decodedChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return decodedChange;
    }
    function encodeDetachedNodes(detachedNodes, context) {
        if (detachedNodes === undefined) {
            return undefined;
        }
        const treesToEncode = [];
        const buildsArray = [];
        let buildsForRevision;
        for (const [[revision, id], chunk] of detachedNodes.entries()) {
            const encodedRevision = encodeRevisionOpt(revisionTagCodec, revision, context);
            if (buildsForRevision === undefined || buildsForRevision[1] !== encodedRevision) {
                if (buildsForRevision !== undefined) {
                    buildsArray.push(buildsForRevision);
                }
                buildsForRevision = encodedRevision !== undefined ? [[], encodedRevision] : [[]];
            }
            treesToEncode.push(chunk.cursor());
            const treeIndexInBatch = treesToEncode.length - 1;
            buildsForRevision?.[0].push([id, treeIndexInBatch]);
        }
        if (buildsForRevision !== undefined) {
            buildsArray.push(buildsForRevision);
        }
        return buildsArray.length === 0
            ? undefined
            : {
                builds: buildsArray,
                trees: fieldsCodec.encode(treesToEncode, {
                    encodeType: chunkCompressionStrategy,
                    schema: context.schema,
                    originatorId: context.originatorId,
                    idCompressor: context.idCompressor,
                }),
            };
    }
    function decodeDetachedNodes(encoded, context) {
        if (encoded === undefined || encoded.builds.length === 0) {
            return undefined;
        }
        const chunks = fieldsCodec.decode(encoded.trees, {
            encodeType: chunkCompressionStrategy,
            originatorId: context.originatorId,
            idCompressor: context.idCompressor,
        });
        const getChunk = (index) => {
            (0, internal_1.assert)(index < chunks.length, 0x898 /* out of bounds index for build chunk */);
            return (0, index_js_3.chunkFieldSingle)(chunks[index] ?? (0, internal_1.oob)(), {
                policy: index_js_3.defaultChunkPolicy,
                idCompressor: context.idCompressor,
            });
        };
        const map = (0, index_js_2.newTupleBTree)();
        encoded.builds.forEach((build) => {
            // EncodedRevisionTag cannot be an array so this ensures that we can isolate the tuple
            const revision = build[1] === undefined ? context.revision : revisionTagCodec.decode(build[1], context);
            const decodedChunks = build[0].map(([i, n]) => [
                i,
                getChunk(n),
            ]);
            for (const [id, chunk] of decodedChunks) {
                map.set([revision, id], chunk);
            }
        });
        return map;
    }
    function encodeRevisionInfos(revisions, context) {
        if (context.revision !== undefined) {
            (0, internal_1.assert)(revisions.length === 1 &&
                revisions[0] !== undefined &&
                revisions[0].revision === context.revision &&
                revisions[0].rollbackOf === undefined, 0x964 /* A tagged change should only contain the tagged revision */);
            return undefined;
        }
        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) {
        if (revisions === undefined) {
            return context.revision !== undefined ? [{ revision: context.revision }] : undefined;
        }
        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.
            (0, internal_1.assert)(change.destroys === undefined, 0x899 /* Unexpected changeset with destroys */);
            return {
                maxId: change.maxId,
                revisions: change.revisions === undefined
                    ? undefined
                    : encodeRevisionInfos(change.revisions, context),
                changes: encodeFieldChangesForJson(change.fieldChanges, context, change.nodeChanges),
                builds: encodeDetachedNodes(change.builds, context),
                refreshers: encodeDetachedNodes(change.refreshers, context),
                violations: change.constraintViolationCount,
            };
        },
        decode: (encodedChange, context) => {
            const decoded = {
                fieldChanges: new Map(),
                nodeChanges: (0, index_js_2.newTupleBTree)(),
                nodeToParent: (0, index_js_2.newTupleBTree)(),
                nodeAliases: (0, index_js_2.newTupleBTree)(),
                crossFieldKeys: (0, modularChangeTypes_js_1.newCrossFieldKeyTable)(),
            };
            decoded.fieldChanges = decodeFieldChangesFromJson(encodedChange.changes, undefined, decoded, context, (0, index_js_2.idAllocatorFromMaxId)(encodedChange.maxId));
            if (encodedChange.builds !== undefined) {
                decoded.builds = decodeDetachedNodes(encodedChange.builds, context);
            }
            if (encodedChange.refreshers !== undefined) {
                decoded.refreshers = decodeDetachedNodes(encodedChange.refreshers, context);
            }
            if (encodedChange.violations !== undefined) {
                decoded.constraintViolationCount = encodedChange.violations;
            }
            const decodedRevInfos = decodeRevisionInfos(encodedChange.revisions, context);
            if (decodedRevInfos !== undefined) {
                decoded.revisions = decodedRevInfos;
            }
            if (encodedChange.maxId !== undefined) {
                decoded.maxId = encodedChange.maxId;
            }
            return decoded;
        },
    };
    return (0, index_js_1.withSchemaValidation)(modularChangeFormat_js_1.EncodedModularChangeset, modularChangeCodec, codecOptions.jsonValidator);
}
function getChangeHandler(fieldKinds, fieldKind) {
    if (fieldKind === genericFieldKind_js_1.genericFieldKind.identifier) {
        return genericFieldKind_js_1.genericFieldKind.changeHandler;
    }
    const handler = fieldKinds.get(fieldKind)?.kind.changeHandler;
    (0, internal_1.assert)(handler !== undefined, 0x9c1 /* Unknown field kind */);
    return handler;
}
function encodeRevisionOpt(revisionCodec, revision, context) {
    if (revision === undefined) {
        return undefined;
    }
    return revision === context.revision ? undefined : revisionCodec.encode(revision, context);
}
//# sourceMappingURL=modularChangeCodecs.js.map