/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { fail, strict as assert } from "assert";
import { AnchorSet, emptyDelta, } from "../core/index.js";
import { makeCodecFamily } from "../codec/index.js";
import { brand } from "../util/index.js";
import { deepFreeze } from "./utils.js";
function isNonEmptyChange(change) {
    return "inputContext" in change;
}
function mint(inputContext, intention) {
    const intentions = Array.isArray(intention) ? intention : [intention];
    return {
        inputContext: composeIntentions([], inputContext),
        intentions,
        outputContext: composeIntentions(inputContext, intentions),
    };
}
function composeIntentions(base, extras) {
    const composed = [...base];
    let last = composed[composed.length - 1];
    for (const extra of extras) {
        // Check wether we are composing intentions that cancel each other out.
        // This helps us ensure that we always represent sequences of intentions
        // in the same canonical form.
        if (last === -extra) {
            composed.pop();
            last = composed[composed.length - 1];
        }
        else {
            composed.push(extra);
            last = extra;
        }
    }
    return composed;
}
function compose(change1, change2, verify = true) {
    return composeList(getArrayWithoutUndefined([change1, change2]), verify);
}
function composeList(changes, verify = true) {
    let inputContext;
    let outputContext;
    let intentions = [];
    for (const change of changes) {
        if (isNonEmptyChange(change)) {
            inputContext ?? (inputContext = change.inputContext);
            if (verify && outputContext !== undefined) {
                // The input context should match the output context of the previous change.
                assert.deepEqual(change.inputContext, outputContext);
            }
            outputContext = composeIntentions(outputContext ?? inputContext, change.intentions);
            intentions = composeIntentions(intentions, change.intentions);
        }
    }
    if (inputContext !== undefined) {
        return {
            inputContext,
            intentions,
            outputContext: outputContext ?? fail(),
        };
    }
    return emptyChange;
}
function getArrayWithoutUndefined(array) {
    const result = [];
    for (const item of array) {
        if (item !== undefined) {
            result.push(item);
        }
    }
    return result;
}
function invert(change) {
    if (isNonEmptyChange(change)) {
        return {
            inputContext: change.outputContext,
            outputContext: change.inputContext,
            intentions: change.intentions.map((i) => -i).reverse(),
        };
    }
    return emptyChange;
}
function rebase(change, over) {
    if (change === undefined) {
        return undefined;
    }
    if (over === undefined) {
        return change;
    }
    if (isNonEmptyChange(change)) {
        if (isNonEmptyChange(over)) {
            // Rebasing should only occur between two changes with the same input context
            assert.deepEqual(change.inputContext, over.inputContext);
            return {
                inputContext: over.outputContext,
                outputContext: composeIntentions(over.outputContext, change.intentions),
                intentions: change.intentions,
            };
        }
        return change;
    }
    return TestChange.emptyChange;
}
function checkChangeList(changes, intentions) {
    const filtered = changes.filter(isNonEmptyChange);
    let intentionsSeen = [];
    let index = 0;
    for (const change of filtered) {
        intentionsSeen = composeIntentions(intentionsSeen, change.intentions);
        if (index > 0) {
            const prev = filtered[index - 1];
            // The current change should apply to the context brought about by the previous change
            assert.deepEqual(change.inputContext, prev.outputContext);
        }
        ++index;
    }
    // All expected intentions were present
    assert.deepEqual(intentionsSeen, intentions);
}
function toDelta({ change }) {
    if (change.intentions.length > 0) {
        return new Map([
            [
                // We represent the intentions as a list if node offsets in some imaginary field "testIntentions".
                // This is purely for the sake of testing.
                brand("testIntentions"),
                { local: change.intentions.map((i) => ({ count: i })) },
            ],
        ]);
    }
    return new Map();
}
const emptyChange = { intentions: [] };
const codec = {
    encode: (x) => x,
    decode: (x) => x,
};
export const TestChange = {
    emptyChange,
    mint,
    compose,
    composeList,
    invert,
    rebase,
    checkChangeList,
    toDelta,
    isEmpty,
    codec,
};
deepFreeze(TestChange);
export class TestChangeRebaser {
    compose(changes) {
        return composeList(changes.map((c) => c.change));
    }
    invert(change) {
        return invert(change.change);
    }
    rebase(change, over) {
        return rebase(change, over.change) ?? { intentions: [] };
    }
}
export class UnrebasableTestChangeRebaser extends TestChangeRebaser {
    rebase(change, over) {
        assert.fail("Unexpected call to rebase");
    }
}
export class NoOpChangeRebaser extends TestChangeRebaser {
    constructor() {
        super(...arguments);
        this.rebasedCount = 0;
        this.invertedCount = 0;
        this.composedCount = 0;
    }
    rebase(change, over) {
        this.rebasedCount += 1;
        return change;
    }
    invert(change) {
        this.invertedCount += 1;
        return change.change;
    }
    compose(changes) {
        this.composedCount += changes.length;
        return changes.length === 0 ? emptyChange : changes[0].change;
    }
}
export class ConstrainedTestChangeRebaser extends TestChangeRebaser {
    constructor(constraint) {
        super();
        this.constraint = constraint;
    }
    rebase(change, over) {
        assert(this.constraint(change, over));
        return super.rebase(change, over);
    }
}
export class TestAnchorSet extends AnchorSet {
    constructor() {
        super(...arguments);
        this.rebases = [];
        this.intentions = [];
    }
}
const rootKey = brand("root");
/**
 * This is a hack to encode arbitrary information (the intentions) into a Delta
 * The resulting Delta does not represent a concrete change to a document tree.
 * It is instead used as composite value in deep comparisons that verify that `EditManager` calls
 * `ChangeFamily.intoDelta` with the expected change.
 */
export function asDelta(intentions) {
    return intentions.length === 0
        ? emptyDelta
        : {
            fields: new Map([[rootKey, { local: intentions.map((i) => ({ count: i })) }]]),
        };
}
export function testChangeFamilyFactory(rebaser) {
    const family = {
        rebaser: rebaser ?? new TestChangeRebaser(),
        codecs: makeCodecFamily([[0, TestChange.codec]]),
        buildEditor: () => ({
            enterTransaction: () => assert.fail("Unexpected edit"),
            exitTransaction: () => assert.fail("Unexpected edit"),
        }),
    };
    return family;
}
export function isEmpty(change) {
    return change.intentions.length === 0;
}
//# sourceMappingURL=testChange.js.map