"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyMessages = exports.generateClientNames = exports.generateOperationMessagesForClients = exports.runMergeTreeOperationRunner = exports.replayResultsPath = exports.doOverRanges = exports.resolveRanges = exports.resolveRange = exports.doOverRange = exports.insert = exports.insertAtRefPos = exports.annotateRange = exports.obliterateRangeSided = exports.obliterateRange = exports.removeRange = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const node_assert_1 = require("node:assert");
const fs = __importStar(require("node:fs"));
const mergeTreeNodeWalk_js_1 = require("../mergeTreeNodeWalk.js");
const ops_js_1 = require("../ops.js");
const segmentInfos_js_1 = require("../segmentInfos.js");
const sequencePlace_js_1 = require("../sequencePlace.js");
const textSegment_js_1 = require("../textSegment.js");
const dirname_cjs_1 = require("./dirname.cjs");
const testClientLogger_js_1 = require("./testClientLogger.js");
const removeRange = (client, opStart, opEnd) => client.removeRangeLocal(opStart, opEnd);
exports.removeRange = removeRange;
const obliterateRange = (client, opStart, opEnd) => client.obliterateRangeLocal(opStart, opEnd);
exports.obliterateRange = obliterateRange;
const obliterateRangeSided = (client, opStart, opEnd, random) => {
    let startSide;
    let endSide;
    const oblEnd = random.integer(opStart, client.getLength() - 1);
    // TODO: to create zero length obliterate ops, change '<=' to '<'.
    // Doing so may cause different failures than those without zero length.
    // AB#19930
    if (oblEnd - opStart <= 1) {
        startSide = sequencePlace_js_1.Side.Before;
        endSide = sequencePlace_js_1.Side.After;
    }
    else {
        startSide = random.pick([sequencePlace_js_1.Side.Before, sequencePlace_js_1.Side.After]);
        endSide = random.pick([sequencePlace_js_1.Side.Before, sequencePlace_js_1.Side.After]);
    }
    const start = { pos: opStart, side: startSide };
    const end = { pos: oblEnd, side: endSide };
    return client.obliterateRangeLocal(start, end);
};
exports.obliterateRangeSided = obliterateRangeSided;
const annotateRange = (client, opStart, opEnd, random) => {
    if (random.bool()) {
        return client.annotateRangeLocal(opStart, opEnd, {
            [random.integer(1, 5)]: client.longClientId,
        });
    }
    else {
        const max = random.pick([undefined, random.integer(-10, 100)]);
        const min = random.pick([undefined, random.integer(-100, 10)]);
        return client.annotateAdjustRangeLocal(opStart, opEnd, {
            [random.integer(0, 2).toString()]: {
                delta: random.integer(-5, 5),
                min: (min ?? max ?? 0) > (max ?? 0) ? undefined : min,
                max,
            },
        });
    }
};
exports.annotateRange = annotateRange;
const insertAtRefPos = (client, opStart, opEnd, random) => {
    const segs = [];
    // gather all the segments at the pos, including removed segments
    (0, mergeTreeNodeWalk_js_1.walkAllChildSegments)(client.mergeTree.root, (seg) => {
        const pos = client.getPosition(seg);
        if (pos >= opStart) {
            if (pos <= opStart) {
                segs.push(seg);
                return true;
            }
            return false;
        }
        return true;
    });
    if (segs.length > 0) {
        const text = client.longClientId.repeat(random.integer(1, 3));
        const seg = random.pick(segs);
        const removed = (0, segmentInfos_js_1.toRemovalInfo)(seg);
        const lref = client.createLocalReferencePosition(seg, removed ? 0 : random.integer(0, seg.cachedLength - 1), removed
            ? ops_js_1.ReferenceType.SlideOnRemove
            : random.pick([
                ops_js_1.ReferenceType.Simple,
                ops_js_1.ReferenceType.SlideOnRemove,
                ops_js_1.ReferenceType.Transient,
            ]), undefined);
        return client.insertAtReferencePositionLocal(lref, textSegment_js_1.TextSegment.make(text));
    }
};
exports.insertAtRefPos = insertAtRefPos;
const insert = (client, _start, _end, random) => {
    // Note: the _start param is generated using exclusive range. This provides more coverage by allowing
    // insertion at the end.
    const start = random.integer(0, client.getLength());
    const text = client.longClientId.repeat(random.integer(1, 3));
    return client.insertTextLocal(start, text);
};
exports.insert = insert;
const generateInsert = (client, random) => {
    const len = client.getLength();
    const text = client.longClientId.repeat(random.integer(1, 3));
    return client.insertTextLocal(random.integer(0, len), text);
};
function doOverRange(range, defaultGrowthFunc, doAction) {
    let lastCurrent = Number.NaN;
    for (let current = range.min; current <= range.max; current = (range.growthFunc ?? defaultGrowthFunc)(current)) {
        // let growth funcs be simple
        // especially around 0 and 1
        // if the value didn't change,
        // just increment it
        if (current === lastCurrent) {
            current++;
        }
        if (current <= range.max) {
            lastCurrent = current;
            doAction(current);
        }
    }
}
exports.doOverRange = doOverRange;
function resolveRange(range, defaultGrowthFunc) {
    const results = [];
    doOverRange(range, range.growthFunc ?? defaultGrowthFunc, (num) => {
        results.push(num);
    });
    return results;
}
exports.resolveRange = resolveRange;
function resolveRanges(ranges, defaultGrowthFunc) {
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const resolvedRanges = {};
    for (const [key, value] of Object.entries(ranges)) {
        if (isConfigRange(value)) {
            resolvedRanges[key] = resolveRange(value, defaultGrowthFunc);
        }
    }
    return resolvedRanges;
}
exports.resolveRanges = resolveRanges;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isConfigRange(t) {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    return typeof t === "object" && typeof t.min === "number" && typeof t.max === "number";
}
function doOverRanges(ranges, doAction) {
    const rangeEntries = [];
    for (const [key, value] of Object.entries(ranges)) {
        if (isConfigRange(value)) {
            rangeEntries.push([key, value]);
        }
    }
    const doOverRangesHelper = (selections) => {
        if (selections.length === rangeEntries.length) {
            const selectionsObj = {};
            for (const [key, value] of selections) {
                selectionsObj[key] = value;
            }
            const description = selections.map(([key, value]) => `${key}:${value}`).join("_");
            doAction(selectionsObj, description);
        }
        else {
            const [key, value] = rangeEntries[selections.length];
            doOverRange(value, value.growthFunc ?? ranges.growthFunc, (selection) => {
                selections.push([key, selection]);
                doOverRangesHelper(selections);
                selections.pop();
            });
        }
    };
    doOverRangesHelper([]);
}
exports.doOverRanges = doOverRanges;
exports.replayResultsPath = `${dirname_cjs_1._dirname}/../../src/test/results`;
function runMergeTreeOperationRunner(random, startingSeq, clients, minLength, config, apply = applyMessages) {
    let seq = startingSeq;
    const results = [];
    let fakeTime = 1725916319097;
    doOverRange(config.opsPerRoundRange, config.growthFunc, (opsPerRound) => {
        if (config.incrementalLog) {
            console.log(`MinLength: ${minLength} Clients: ${clients.length} Ops: ${opsPerRound} Seq: ${seq}`);
        }
        for (let round = 0; round < config.rounds; round++) {
            const initialText = clients[0].getText();
            const logger = new testClientLogger_js_1.TestClientLogger(clients, `Clients: ${clients.length} Ops: ${opsPerRound} Round: ${round}`);
            const messageData = generateOperationMessagesForClients(random, seq, clients, logger, opsPerRound, minLength, config.operations, config.applyOpDuringGeneration, config.insertText);
            seq = apply(messageData[0][0].sequenceNumber - 1, messageData, clients, logger, random);
            const resultText = logger.validate();
            results.push({
                initialText,
                resultText,
                msgs: messageData.map((md) => ({ ...md[0], timestamp: fakeTime++ })),
                seq,
            });
            logger.dispose();
        }
    });
    if (config.resultsFilePostfix !== undefined) {
        const resultsFilePath = `${exports.replayResultsPath}/len_${minLength}-clients_${clients.length}-${config.resultsFilePostfix}`;
        fs.writeFileSync(resultsFilePath, JSON.stringify(results, undefined, "\t"));
    }
    return seq;
}
exports.runMergeTreeOperationRunner = runMergeTreeOperationRunner;
function generateOperationMessagesForClients(random, startingSeq, clients, logger, opsPerRound, minLength, operations, applyOpDuringGeneration, insertText) {
    const minimumSequenceNumber = startingSeq;
    let runningSeq = startingSeq;
    const messages = [];
    for (let i = 0; i < opsPerRound; i++) {
        // pick a client greater than 0, client 0 only applies remote ops
        // and is our baseline
        const client = clients[random.integer(1, clients.length - 1)];
        if (applyOpDuringGeneration === true && messages.length > 0 && random.bool()) {
            const toApply = messages
                .filter(([msg]) => msg.sequenceNumber > client.getCollabWindow().currentSeq)
                .slice(0, random.integer(1, 3));
            applyMessages(toApply[0][0].sequenceNumber - 1, toApply, [client], logger);
        }
        const len = client.getLength();
        const sg = client.peekPendingSegmentGroups();
        let opOrOps;
        if (len === 0 || len < minLength) {
            opOrOps =
                insertText === undefined ? generateInsert(client, random) : insertText(client, random);
        }
        else {
            let opIndex = random.integer(0, operations.length - 1);
            const start = random.integer(0, len - 1);
            const end = random.integer(start + 1, len);
            for (let y = 0; y < operations.length && opOrOps === undefined; y++) {
                opOrOps = operations[opIndex](client, start, end, random);
                opIndex++;
                opIndex %= operations.length;
            }
        }
        if (opOrOps !== undefined) {
            // Pre-check to avoid logger.toString() in the string template
            if (sg === client.peekPendingSegmentGroups()) {
                node_assert_1.strict.notEqual(sg, client.peekPendingSegmentGroups(), `op created but segment group not enqueued.${logger}`);
            }
            const ops = Array.isArray(opOrOps) ? opOrOps : [opOrOps];
            const totalIndividualOps = ops
                .map((o) => (o.type === ops_js_1.MergeTreeDeltaType.GROUP ? o.ops.length : 1))
                .reduce((a, b) => a + b, 0);
            let allSegmentGroups = client.peekPendingSegmentGroups(totalIndividualOps);
            if (!Array.isArray(allSegmentGroups)) {
                allSegmentGroups = [allSegmentGroups];
            }
            (0, node_assert_1.strict)(Array.isArray(allSegmentGroups), "Expected array of segment groups");
            for (const op of ops) {
                const message = client.makeOpMessage(op, ++runningSeq);
                message.minimumSequenceNumber = minimumSequenceNumber;
                const segmentGroups = allSegmentGroups.splice(0, op.type === ops_js_1.MergeTreeDeltaType.GROUP ? op.ops.length : 1);
                messages.push([
                    message,
                    op.type === ops_js_1.MergeTreeDeltaType.GROUP ? segmentGroups : segmentGroups[0],
                ]);
            }
        }
    }
    const maxProcessedSeq = Math.max(...clients.map((c) => c.getCollabWindow().currentSeq));
    if (messages.length > 0) {
        const index = messages.findIndex(([msg]) => msg.sequenceNumber === maxProcessedSeq);
        if (index !== -1) {
            const apply = messages.splice(0, index + 1);
            applyMessages(apply[0][0].sequenceNumber - 1, apply, clients, logger);
        }
    }
    return messages;
}
exports.generateOperationMessagesForClients = generateOperationMessagesForClients;
function generateClientNames() {
    const clientNames = [];
    function addClientNames(startChar, count) {
        const startCode = startChar.codePointAt(0);
        if (startCode === undefined) {
            throw new Error("startCode must be a single character");
        }
        for (let i = 0; i < count; i++) {
            clientNames.push(String.fromCodePoint(startCode + i));
        }
    }
    addClientNames("A", 26);
    addClientNames("a", 26);
    addClientNames("0", 17);
    return clientNames;
}
exports.generateClientNames = generateClientNames;
function applyMessages(startingSeq, messageData, clients, logger) {
    let seq = startingSeq;
    try {
        // log and apply all the ops created in the round
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < messageData.length; i++) {
            const [message] = messageData[i];
            message.sequenceNumber = ++seq;
            for (const c of clients) {
                if (c.getCollabWindow().currentSeq < message.sequenceNumber) {
                    c.applyMsg(message);
                }
            }
        }
    }
    catch (error) {
        throw logger.addLogsToError(error);
    }
    return seq;
}
exports.applyMessages = applyMessages;
//# sourceMappingURL=mergeTreeOperationRunner.js.map