"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeModularChangeCodecV1 = exports.getFieldChangesetCodecs = exports.decodeChange = exports.encodeChange = exports.decodeRevisionInfos = exports.encodeRevisionInfos = exports.decodeDetachedNodes = exports.encodeDetachedNodes = exports.decodeNodeChangesetFromJson = exports.decodeFieldChangesFromJson = exports.encodeNodeChangesForJson = exports.encodeFieldChangesForJsonI = exports.encodeFieldChangesForJson = exports.getFieldChangesetCodec = 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 changeAtomIdBTree_js_1 = require("../changeAtomIdBTree.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 modularChangeFormatV1_js_1 = require("./modularChangeFormatV1.js");
const modularChangeTypes_js_1 = require("./modularChangeTypes.js");
function getFieldChangesetCodec(fieldKind, fieldChangesetCodecs) {
    const entry = fieldChangesetCodecs.get(fieldKind);
    (0, internal_1.assert)(entry !== undefined, 0x5ea /* Tried to encode unsupported fieldKind */);
    return entry;
}
exports.getFieldChangesetCodec = getFieldChangesetCodec;
function encodeFieldChangesForJson(change, context, nodeChanges, fieldChangesetCodecs) {
    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, fieldChangesetCodecs);
        },
        decodeNode: () => (0, internal_1.fail)(0xb1e /* Should not decode nodes during field encoding */),
    };
    return encodeFieldChangesForJsonI(change, fieldContext, fieldChangesetCodecs);
}
exports.encodeFieldChangesForJson = encodeFieldChangesForJson;
function encodeFieldChangesForJsonI(change, context, fieldChangesetCodecs) {
    const encodedFields = [];
    for (const [field, fieldChange] of change) {
        const { codec, compiledSchema } = getFieldChangesetCodec(fieldChange.fieldKind, fieldChangesetCodecs);
        const encodedChange = codec.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;
}
exports.encodeFieldChangesForJsonI = encodeFieldChangesForJsonI;
function encodeNodeChangesForJson(change, context, fieldChangesetCodecs) {
    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, fieldChangesetCodecs);
    }
    if (nodeExistsConstraint !== undefined) {
        encodedChange.nodeExistsConstraint = nodeExistsConstraint;
    }
    return encodedChange;
}
exports.encodeNodeChangesForJson = encodeNodeChangesForJson;
function decodeFieldChangesFromJson(encodedChange, parentId, decoded, context, idAllocator, fieldKinds, fieldChangesetCodecs) {
    const decodedFields = new Map();
    for (const field of encodedChange) {
        const { codec, compiledSchema } = getFieldChangesetCodec(field.fieldKind, fieldChangesetCodecs);
        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, fieldKinds, fieldChangesetCodecs);
                decoded.nodeChanges.set([nodeId.revision, nodeId.localId], node);
                decoded.nodeToParent.set([nodeId.revision, nodeId.localId], fieldId);
                return nodeId;
            },
        };
        const fieldChangeset = codec.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;
}
exports.decodeFieldChangesFromJson = decodeFieldChangesFromJson;
function decodeNodeChangesetFromJson(encodedChange, id, decoded, context, idAllocator, fieldKinds, fieldChangesetCodecs) {
    const decodedChange = {};
    const { fieldChanges, nodeExistsConstraint } = encodedChange;
    if (fieldChanges !== undefined) {
        decodedChange.fieldChanges = decodeFieldChangesFromJson(fieldChanges, id, decoded, context, idAllocator, fieldKinds, fieldChangesetCodecs);
    }
    if (nodeExistsConstraint !== undefined) {
        decodedChange.nodeExistsConstraint = nodeExistsConstraint;
    }
    return decodedChange;
}
exports.decodeNodeChangesetFromJson = decodeNodeChangesetFromJson;
function encodeDetachedNodes(detachedNodes, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy) {
    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,
            }),
        };
}
exports.encodeDetachedNodes = encodeDetachedNodes;
function decodeDetachedNodes(encoded, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy) {
    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, changeAtomIdBTree_js_1.newChangeAtomIdBTree)();
    // eslint-disable-next-line unicorn/no-array-for-each -- Codec internals: minimizing changes to serialization logic
    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;
}
exports.decodeDetachedNodes = decodeDetachedNodes;
function encodeRevisionInfos(revisions, context, revisionTagCodec) {
    if (context.revision !== undefined) {
        (0, internal_1.assert)(revisions.length === 1 &&
            // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- Using optional chaining here would change behavior: `revisions[0]?.rollbackOf === undefined` is true when revisions[0] is undefined, but this check requires revisions[0] to be defined. As currently written, such a change would be safe because context.revision is included in the check and from a couple lines above is confirmed not undefined. But this more verbose form is clearer.
            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;
}
exports.encodeRevisionInfos = encodeRevisionInfos;
function decodeRevisionInfos(revisions, context, revisionTagCodec) {
    if (revisions === undefined) {
        return context.revision === undefined ? undefined : [{ revision: context.revision }];
    }
    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;
}
exports.decodeRevisionInfos = decodeRevisionInfos;
function encodeChange(change, context, fieldChangesetCodecs, revisionTagCodec, fieldsCodec, chunkCompressionStrategy) {
    // 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, revisionTagCodec),
        changes: encodeFieldChangesForJson(change.fieldChanges, context, change.nodeChanges, fieldChangesetCodecs),
        builds: encodeDetachedNodes(change.builds, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy),
        refreshers: encodeDetachedNodes(change.refreshers, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy),
        violations: change.constraintViolationCount,
    };
}
exports.encodeChange = encodeChange;
function decodeChange(encodedChange, context, fieldKinds, fieldChangesetCodecs, revisionTagCodec, fieldsCodec, chunkCompressionStrategy) {
    const decoded = {
        fieldChanges: new Map(),
        nodeChanges: (0, changeAtomIdBTree_js_1.newChangeAtomIdBTree)(),
        nodeToParent: (0, changeAtomIdBTree_js_1.newChangeAtomIdBTree)(),
        nodeAliases: (0, changeAtomIdBTree_js_1.newChangeAtomIdBTree)(),
        crossFieldKeys: (0, modularChangeTypes_js_1.newCrossFieldKeyTable)(),
    };
    decoded.fieldChanges = decodeFieldChangesFromJson(encodedChange.changes, undefined, decoded, context, (0, index_js_2.idAllocatorFromMaxId)(encodedChange.maxId), fieldKinds, fieldChangesetCodecs);
    if (encodedChange.builds !== undefined) {
        decoded.builds = decodeDetachedNodes(encodedChange.builds, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy);
    }
    if (encodedChange.refreshers !== undefined) {
        decoded.refreshers = decodeDetachedNodes(encodedChange.refreshers, context, revisionTagCodec, fieldsCodec, chunkCompressionStrategy);
    }
    if (encodedChange.violations !== undefined) {
        decoded.constraintViolationCount = encodedChange.violations;
    }
    const decodedRevInfos = decodeRevisionInfos(encodedChange.revisions, context, revisionTagCodec);
    if (decodedRevInfos !== undefined) {
        decoded.revisions = decodedRevInfos;
    }
    if (encodedChange.maxId !== undefined) {
        decoded.maxId = encodedChange.maxId;
    }
    return decoded;
}
exports.decodeChange = decodeChange;
function getFieldChangesetCodecs(fieldKinds, revisionTagCodec, codecOptions) {
    // 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.encodedSchema
                ? (0, index_js_1.extractJsonValidator)(codecOptions.jsonValidator).compile(codec.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 }),
        ],
    ]);
    // eslint-disable-next-line unicorn/no-array-for-each -- Map.forEach with (value, key) signature; codec initialization
    fieldKinds.forEach((entry, identifier) => {
        fieldChangesetCodecs.set(identifier, getMapEntry(entry));
    });
    return fieldChangesetCodecs;
}
exports.getFieldChangesetCodecs = getFieldChangesetCodecs;
function makeModularChangeCodecV1(fieldKinds, revisionTagCodec, fieldsCodec, codecOptions, chunkCompressionStrategy = treeCompressionUtils_js_1.TreeCompressionStrategy.Compressed) {
    const fieldChangesetCodecs = getFieldChangesetCodecs(fieldKinds, revisionTagCodec, codecOptions);
    const modularChangeCodec = {
        encode: (change, context) => encodeChange(change, context, fieldChangesetCodecs, revisionTagCodec, fieldsCodec, chunkCompressionStrategy),
        decode: (encodedChange, context) => decodeChange(encodedChange, context, fieldKinds, fieldChangesetCodecs, revisionTagCodec, fieldsCodec, chunkCompressionStrategy),
    };
    return (0, index_js_1.withSchemaValidation)(modularChangeFormatV1_js_1.EncodedModularChangesetV1, modularChangeCodec, codecOptions.jsonValidator);
}
exports.makeModularChangeCodecV1 = makeModularChangeCodecV1;
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=modularChangeCodecV1.js.map