/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, fail, oob } from "@fluidframework/core-utils/internal";
import { extractJsonValidator, withSchemaValidation, } from "../../codec/index.js";
import { newChangeAtomIdTransform, } from "../../core/index.js";
import { brand, idAllocatorFromMaxId, newTupleBTree, } from "../../util/index.js";
import { chunkFieldSingle, defaultChunkPolicy, } from "../chunked-forest/index.js";
import { TreeCompressionStrategy } from "../treeCompressionUtils.js";
import { addNodeRename, getFirstAttachField, getFirstDetachField, newRootTable, } from "./modularChangeFamily.js";
import { newCrossFieldRangeTable, setInChangeAtomIdMap, } from "./modularChangeTypes.js";
import { genericFieldKind } from "./genericFieldKind.js";
import { EncodedModularChangeset, } from "./modularChangeFormatV2.js";
import { makeChangeAtomIdCodec } from "../changeAtomIdCodec.js";
export function makeModularChangeCodecV2(fieldKinds, revisionTagCodec, fieldsCodec, codecOptions, chunkCompressionStrategy = 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
                ? extractJsonValidator(codecOptions.jsonValidator).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;
    };
    const changeAtomIdCodec = makeChangeAtomIdCodec(revisionTagCodec);
    function encodeFieldChangesForJson(change, parentId, fieldToRoots, context, encodeNode, getInputDetachId, isAttachId, isDetachId) {
        const encodedFields = [];
        for (const [field, fieldChange] of change) {
            const { codec, compiledSchema } = getFieldChangesetCodec(fieldChange.fieldKind);
            const rootChanges = fieldToRoots.get([parentId?.revision, parentId?.localId, field]);
            const fieldContext = {
                baseContext: context,
                rootNodeChanges: rootChanges?.nodeChanges ?? newTupleBTree(),
                rootRenames: rootChanges?.renames ?? newChangeAtomIdTransform(),
                encodeNode,
                getInputRootId: getInputDetachId,
                isAttachId,
                isDetachId,
                decodeNode: () => fail(0xb1e /* Should not decode nodes during field encoding */),
                decodeRootNodeChange: () => fail("Should not be called during encoding"),
                decodeRootRename: () => fail("Should not be called during encoding"),
                decodeMoveAndDetach: () => fail("Should not be called during encoding"),
                generateId: () => fail("Should not be called during encoding"),
            };
            const encodedChange = codec.json.encode(fieldChange.change, fieldContext);
            if (compiledSchema !== undefined && !compiledSchema.check(encodedChange)) {
                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, id, fieldToRoots, context, encodeNode, getInputDetachId, isAttachId, isDetachId) {
        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 = encodeFieldChangesForJson(fieldChanges, id, fieldToRoots, context, encodeNode, getInputDetachId, isAttachId, isDetachId);
        }
        if (nodeExistsConstraint !== undefined) {
            encodedChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return encodedChange;
    }
    function encodeRootNodesForJson(roots, context, encodeNode) {
        const encoded = [];
        for (const [[revision, localId], nodeId] of roots.entries()) {
            encoded.push({
                detachId: changeAtomIdCodec.encode({ revision, localId }, context),
                nodeChangeset: encodeNode(nodeId),
            });
        }
        return encoded;
    }
    function encodeRenamesForJson(renames, context) {
        const encoded = [];
        for (const entry of renames.entries()) {
            encoded.push({
                oldId: changeAtomIdCodec.encode(entry.start, context),
                newId: changeAtomIdCodec.encode(entry.value, context),
                count: entry.length,
            });
        }
        return encoded;
    }
    function decodeFieldChangesFromJson(encodedChange, parentId, decodedCrossFieldKeys, context, decodeNode, idAllocator) {
        const decodedFields = new Map();
        for (const field of encodedChange) {
            const { codec, compiledSchema } = getFieldChangesetCodec(field.fieldKind);
            if (compiledSchema !== undefined && !compiledSchema.check(field.change)) {
                fail(0xb20 /* Encoded change didn't pass schema validation. */);
            }
            const fieldId = {
                nodeId: parentId,
                field: field.fieldKey,
            };
            const fieldContext = {
                baseContext: context,
                rootNodeChanges: newTupleBTree(),
                rootRenames: newChangeAtomIdTransform(),
                encodeNode: () => fail(0xb21 /* Should not encode nodes during field decoding */),
                getInputRootId: () => fail("Should not query during decoding"),
                isAttachId: () => fail("Should not query during decoding"),
                isDetachId: () => fail("Should not query during decoding"),
                decodeNode: (encodedNode) => decodeNode(encodedNode, { field: fieldId }),
                decodeRootNodeChange: (detachId, encodedNode) => { },
                decodeRootRename: (oldId, newId, count) => { },
                decodeMoveAndDetach: (detachId, count) => { },
                generateId: () => ({
                    revision: context.revision,
                    localId: brand(idAllocator.allocate()),
                }),
            };
            const fieldChangeset = codec.json.decode(field.change, fieldContext);
            const crossFieldKeys = getChangeHandler(fieldKinds, field.fieldKind).getCrossFieldKeys(fieldChangeset);
            for (const { key, count } of crossFieldKeys) {
                decodedCrossFieldKeys.set(key, count, fieldId);
            }
            const fieldKey = brand(field.fieldKey);
            decodedFields.set(fieldKey, {
                fieldKind: field.fieldKind,
                change: brand(fieldChangeset),
            });
        }
        return decodedFields;
    }
    function decodeNodeChangesetFromJson(encodedChange, id, decodedCrossFieldKeys, decodedRootTable, context, decodeNode, idAllocator) {
        const decodedChange = {};
        const { fieldChanges, nodeExistsConstraint } = encodedChange;
        if (fieldChanges !== undefined) {
            decodedChange.fieldChanges = decodeFieldChangesFromJson(fieldChanges, id, decodedCrossFieldKeys, context, decodeNode, 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) => {
            assert(index < chunks.length, 0x898 /* out of bounds index for build chunk */);
            return chunkFieldSingle(chunks[index] ?? oob(), {
                policy: defaultChunkPolicy,
                idCompressor: context.idCompressor,
            });
        };
        const map = 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) {
            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;
    }
    function decodeRootTable(encodedRoots, encodedRenames, context, decodeNode) {
        const roots = newRootTable();
        if (encodedRoots !== undefined) {
            for (const { detachId, nodeChangeset } of encodedRoots) {
                const decodedId = changeAtomIdCodec.decode(detachId, context);
                setInChangeAtomIdMap(roots.nodeChanges, decodedId, decodeNode(nodeChangeset, { root: decodedId }));
            }
        }
        if (encodedRenames !== undefined) {
            for (const { oldId, newId, count } of encodedRenames) {
                addNodeRename(roots, changeAtomIdCodec.decode(oldId, context), changeAtomIdCodec.decode(newId, context), count, undefined);
            }
        }
        return roots;
    }
    const modularChangeCodec = {
        encode: (change, context) => {
            const fieldToRoots = getFieldToRoots(change.rootNodes);
            const isAttachId = (id, count) => {
                const attachEntry = getFirstAttachField(change.crossFieldKeys, id, count);
                return { ...attachEntry, value: attachEntry.value !== undefined };
            };
            const isDetachId = (id, count) => {
                const detachEntry = getFirstDetachField(change.crossFieldKeys, id, count);
                const renameEntry = change.rootNodes.oldToNewId.getFirst(id, detachEntry.length);
                const isDetach = (detachEntry.value ?? renameEntry.value) !== undefined;
                return { start: id, value: isDetach, length: renameEntry.length };
            };
            const getInputDetachId = (id, count) => {
                return change.rootNodes.newToOldId.getFirst(id, count);
            };
            const encodeNode = (nodeId) => {
                // TODO: Handle node aliasing.
                const node = change.nodeChanges.get([nodeId.revision, nodeId.localId]);
                assert(node !== undefined, 0x92e /* Unknown node ID */);
                return encodeNodeChangesForJson(node, nodeId, fieldToRoots, context, encodeNode, getInputDetachId, isAttachId, isDetachId);
            };
            // Destroys only exist in rollback changesets, which are never sent.
            assert(change.destroys === undefined, 0x899 /* Unexpected changeset with destroys */);
            const encoded = {
                maxId: change.maxId,
                revisions: change.revisions === undefined
                    ? undefined
                    : encodeRevisionInfos(change.revisions, context),
                fieldChanges: encodeFieldChangesForJson(change.fieldChanges, undefined, fieldToRoots, context, encodeNode, getInputDetachId, isAttachId, isDetachId),
                rootNodes: encodeRootNodesForJson(change.rootNodes.nodeChanges, context, encodeNode),
                nodeRenames: encodeRenamesForJson(change.rootNodes.oldToNewId, context),
                builds: encodeDetachedNodes(change.builds, context),
                refreshers: encodeDetachedNodes(change.refreshers, context),
                violations: change.constraintViolationCount,
            };
            return encoded;
        },
        decode: (encodedChange, context) => {
            const idAllocator = idAllocatorFromMaxId(encodedChange.maxId);
            const nodeChanges = newTupleBTree();
            const nodeToParent = newTupleBTree();
            const crossFieldKeys = newCrossFieldRangeTable();
            const rootNodes = newRootTable();
            const decodeNode = (encodedNode, fieldId) => {
                const nodeId = {
                    revision: context.revision,
                    localId: brand(idAllocator.allocate()),
                };
                const node = decodeNodeChangesetFromJson(encodedNode, nodeId, crossFieldKeys, rootNodes, context, decodeNode, idAllocator);
                nodeChanges.set([nodeId.revision, nodeId.localId], node);
                if (fieldId !== undefined) {
                    nodeToParent.set([nodeId.revision, nodeId.localId], fieldId);
                }
                return nodeId;
            };
            const decoded = {
                rebaseVersion: 2,
                fieldChanges: decodeFieldChangesFromJson(encodedChange.fieldChanges, undefined, crossFieldKeys, context, decodeNode, idAllocator),
                nodeChanges,
                rootNodes: decodeRootTable(encodedChange.rootNodes, encodedChange.nodeRenames, context, decodeNode),
                nodeToParent,
                nodeAliases: newTupleBTree(),
                crossFieldKeys,
            };
            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 withSchemaValidation(EncodedModularChangeset, modularChangeCodec, codecOptions.jsonValidator);
}
function getChangeHandler(fieldKinds, fieldKind) {
    if (fieldKind === genericFieldKind.identifier) {
        return genericFieldKind.changeHandler;
    }
    const handler = fieldKinds.get(fieldKind)?.kind.changeHandler;
    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);
}
function getFieldToRoots(rootTable) {
    const fieldToRoots = newTupleBTree();
    for (const [[revision, localId], nodeId] of rootTable.nodeChanges.entries()) {
        const detachId = { revision, localId };
        const fieldId = rootTable.detachLocations.getFirst(detachId, 1).value;
        if (fieldId !== undefined) {
            setInChangeAtomIdMap(getOrAddInFieldRootMap(fieldToRoots, fieldId).nodeChanges, detachId, nodeId);
        }
    }
    for (const entry of rootTable.oldToNewId.entries()) {
        const fieldId = rootTable.detachLocations.getFirst(entry.start, 1).value;
        if (fieldId !== undefined) {
            getOrAddInFieldRootMap(fieldToRoots, fieldId).renames.set(entry.start, entry.length, entry.value);
        }
    }
    return fieldToRoots;
}
function getOrAddInFieldRootMap(map, fieldId) {
    const key = [fieldId.nodeId?.revision, fieldId.nodeId?.localId, fieldId.field];
    const rootChanges = map.get(key);
    if (rootChanges !== undefined) {
        return rootChanges;
    }
    const newRootChanges = {
        nodeChanges: newTupleBTree(),
        renames: newChangeAtomIdTransform(),
    };
    map.set(key, newRootChanges);
    return newRootChanges;
}
//# sourceMappingURL=modularChangeCodecV2.js.map