/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { strict as assert } from "assert";
import { asChangeAtomId, makeAnonChange, makeChangeAtomId, } from "../../../core/index.js";
import { RegisterMap,
// eslint-disable-next-line import/no-internal-modules
 } from "../../../feature-libraries/optional-field/index.js";
import { brand } from "../../../util/index.js";
const dummyDetachId = { localId: brand(0) };
function asRegister(input) {
    if (typeof input === "string" || typeof input === "object") {
        return input;
    }
    return { localId: input };
}
export const Change = {
    /**
     * @returns An empty changeset
     */
    empty: () => ({ moves: [], childChanges: [] }),
    /**
     * @param src - The register to move a node from. The register must be full in the input context of the changeset.
     * @param dst - The register to move that node to.
     * The register must be empty in the input context of the changeset, or emptied as part of the changeset.
     * @returns A changeset that moves a node from src to dst.
     */
    move: (src, dst) => {
        if (dst === "self") {
            return {
                type: "write",
                content: asRegister(src),
            };
        }
        if (src === "self") {
            return Change.clear("self", dst);
        }
        return {
            moves: [[asChangeAtomId(src), asChangeAtomId(dst)]],
            childChanges: [],
        };
    },
    /**
     * @param target - The register remove a node from. The register must be full in the input context of the changeset.
     * @param dst - The register to move the contents of the target register to.
     * The register must be empty in the input context of the changeset, or emptied as part of the changeset.
     * @returns A changeset that clears a register and moves the contents to another register.
     */
    clear: (target, dst) => target === "self"
        ? {
            moves: [],
            childChanges: [],
            valueReplace: { isEmpty: false, dst: asChangeAtomId(dst) },
        }
        : {
            moves: [[asChangeAtomId(target), asChangeAtomId(dst)]],
            childChanges: [],
        },
    /**
     * @param target - The register to reserve. The register must NOT be full in the input context of the changeset.
     * @param dst - The register that the contents of the target register should be moved to should it become populated.
     * The register must be empty in the input context of the changeset, or emptied as part of the changeset.
     * @returns A changeset that reserves an register.
     */
    reserve: (target, dst) => {
        assert(target === "self", "Reserve cell only supports self as source");
        return {
            moves: [],
            childChanges: [],
            valueReplace: { isEmpty: true, dst: asChangeAtomId(dst) },
        };
    },
    /**
     * @param location - The register that contains the child node to be changed.
     * That register must be full in the input context of the changeset.
     * @param change - A change to apply to a child node.
     * @returns A changeset that applies the given change to the child node in the given register.
     */
    childAt: (location, change) => ({
        moves: [],
        childChanges: [[asRegister(location), change]],
    }),
    /**
     * @param change - A change to apply to a child node in the "self" register.
     * @returns A changeset that applies the given change to the child node in the "self" register.
     * The "self" register must be full in the input context of the changeset.
     */
    child: (change) => Change.childAt("self", change),
    /**
     * Combines multiple changesets for the same input context into a single changeset.
     * @param changes - The change to apply as part of the changeset. Interpreted as applying to the same input context.
     * @returns A single changeset that applies all of the given changes.
     */
    atOnce: (...changes) => {
        const moves = [];
        const childChanges = [];
        let replace;
        const changeset = { moves, childChanges };
        for (const changeLike of changes) {
            if ("type" in changeLike === false) {
                const change = changeLike;
                // Note: this will stack overflow if there are too many moves.
                moves.push(...change.moves);
                // Note: this will stack overflow if there are too many child changes.
                childChanges.push(...change.childChanges);
                if (change.valueReplace !== undefined) {
                    assert(replace === undefined, "Multiple reserved detach ids");
                    replace = change.valueReplace;
                }
            }
        }
        for (const change of changes) {
            if ("type" in change) {
                assert(replace !== undefined, "Invalid write without detach");
                replace.src = change.content;
            }
        }
        if (replace !== undefined) {
            changeset.valueReplace = replace;
        }
        return changeset;
    },
};
// Optional changesets may be equivalent but not evaluate to be deep-equal, as some ordering is irrelevant.
export function assertTaggedEqual(a, b) {
    if (a === undefined || b === undefined) {
        assert.equal(a, b);
        return;
    }
    const normalizeRegisterId = (registerId) => {
        if (typeof registerId === "string") {
            return `s${registerId}`;
        }
        return `r${registerId.revision}id${registerId.localId}`;
    };
    const compareRegisterIds = (c, d) => normalizeRegisterId(c).localeCompare(normalizeRegisterId(d));
    // The composed rebase implementation deep-freezes.
    const aCopy = { ...a, change: { ...a.change, moves: [...a.change.moves] } };
    const bCopy = { ...b, change: { ...b.change, moves: [...b.change.moves] } };
    aCopy.change.moves.sort(([c], [d]) => compareRegisterIds(c, d));
    bCopy.change.moves.sort(([c], [d]) => compareRegisterIds(c, d));
    if (aCopy.change.valueReplace !== undefined) {
        assert(bCopy.change.valueReplace !== undefined);
        if (aCopy.change.valueReplace.isEmpty) {
            assert(bCopy.change.valueReplace.isEmpty);
            // Detach IDs are only relevant if the field was not empty, so we tolerate compose
            // assigning them arbitrarily in this case.
            aCopy.change.valueReplace = { ...aCopy.change.valueReplace, dst: dummyDetachId };
            bCopy.change.valueReplace = { ...bCopy.change.valueReplace, dst: dummyDetachId };
        }
    }
    delete aCopy.change.valueReplace;
    delete bCopy.change.valueReplace;
    assert.deepEqual(aCopy, bCopy);
}
export function assertEqual(a, b) {
    if (a === undefined || b === undefined) {
        assert.equal(a, b);
        return;
    }
    assertTaggedEqual(makeAnonChange(a), makeAnonChange(b));
}
export function taggedRegister(id, revision) {
    if (id === "self") {
        return id;
    }
    return taggedAtomId(id, revision);
}
export function taggedAtomId(id, revision) {
    return makeChangeAtomId(id.localId, id.revision ?? revision);
}
function getTouchedRegisters({ change, revision }) {
    const src = new RegisterMap();
    const dst = new RegisterMap();
    for (const leg1 of change.moves) {
        if (!isNoopMove(leg1)) {
            src.set(taggedRegister(leg1[0], revision), true);
            dst.set(taggedRegister(leg1[1], revision), true);
        }
    }
    if (change.valueReplace !== undefined && change.valueReplace.src !== "self") {
        if (change.valueReplace.isEmpty === false) {
            src.set(taggedRegister("self", revision), true);
            dst.set(taggedRegister(change.valueReplace.dst, revision), true);
        }
        if (change.valueReplace.src !== undefined) {
            src.set(taggedRegister(change.valueReplace.src, revision), true);
            dst.set(taggedRegister("self", revision), true);
        }
    }
    return { src, dst };
}
function getOutputRegisterStatues(a) {
    const { src, dst } = getTouchedRegisters(a);
    const statuses = new RegisterMap();
    for (const detach of src.keys()) {
        statuses.set(detach, "empty");
    }
    for (const attach of dst.keys()) {
        statuses.set(attach, "full");
    }
    return statuses;
}
export function verifyContextChain(a, b) {
    const statuses = getOutputRegisterStatues(a);
    const { src, dst } = getTouchedRegisters(b);
    for (const detach of src.keys()) {
        const status = statuses.get(detach);
        if (status !== undefined) {
            assert.equal(status, "full", "Invalid detach on empty register");
        }
    }
    for (const attach of dst.keys()) {
        if (!src.has(attach)) {
            const status = statuses.get(attach);
            if (status !== undefined) {
                assert.equal(status, "empty", "Invalid attach on full register");
            }
        }
    }
}
function isNoopMove(move) {
    return registerEqual(move[0], move[1]);
}
function registerEqual(a, b) {
    if (typeof a === "string" || typeof b === "string") {
        return a === b;
    }
    return a.revision === b.revision && a.localId === b.localId;
}
//# sourceMappingURL=optionalFieldUtils.js.map