/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { strict as assert } from "assert";
import { combineReducers } from "@fluid-private/stochastic-test-utils";
import { cursorForJsonableTreeNode, cursorForJsonableTreeField, } from "../../../feature-libraries/index.js";
import { fail } from "../../../util/index.js";
import { validateTreeConsistency } from "../../utils.js";
import { isRevertibleSharedTreeView } from "./fuzzUtils.js";
import { viewFromState, } from "./fuzzEditGenerators.js";
const syncFuzzReducer = combineReducers({
    edit: (state, operation) => {
        const { contents } = operation;
        switch (contents.type) {
            case "fieldEdit": {
                applyFieldEdit(viewFromState(state), contents);
                break;
            }
            default:
                break;
        }
    },
    transaction: (state, operation) => {
        applyTransactionEdit(state, operation.contents);
    },
    undoRedo: (state, operation) => {
        const view = viewFromState(state).checkout;
        assert(isRevertibleSharedTreeView(view));
        applyUndoRedoEdit(view.undoStack, view.redoStack, operation.contents);
    },
    synchronizeTrees: (state) => {
        applySynchronizationOp(state);
    },
});
export const fuzzReducer = async (state, operation) => syncFuzzReducer(state, operation);
export function checkTreesAreSynchronized(trees) {
    for (const tree of trees) {
        validateTreeConsistency(trees[0], tree);
    }
}
export function applySynchronizationOp(state) {
    state.containerRuntimeFactory.processAllMessages();
    const connectedClients = state.clients.filter((client) => client.containerRuntime.connected);
    if (connectedClients.length > 0) {
        const readonlyChannel = state.summarizerClient.channel;
        for (const { channel } of connectedClients) {
            validateTreeConsistency(channel, readonlyChannel);
        }
    }
}
/**
 * Assumes tree is using the fuzzSchema.
 * TODO: Maybe take in a schema aware strongly typed Tree node or field.
 */
export function applyFieldEdit(tree, fieldEdit) {
    switch (fieldEdit.change.type) {
        case "sequence":
            applySequenceFieldEdit(tree, fieldEdit.change.edit);
            break;
        case "required":
            applyValueFieldEdit(tree, fieldEdit.change.edit);
            break;
        case "optional":
            applyOptionalFieldEdit(tree, fieldEdit.change.edit);
            break;
        default:
            break;
    }
}
function applySequenceFieldEdit(tree, change) {
    const nodeSchema = tree.currentSchema;
    switch (change.type) {
        case "insert": {
            assert(change.parent !== undefined, "Sequence change should not occur at the root.");
            const parent = navigateToNode(tree, change.parent);
            assert(parent?.is(nodeSchema), "Defined down-path should point to a valid parent");
            const field = parent.boxedSequenceChildren;
            assert(field !== undefined);
            field.insertAt(change.index, cursorForJsonableTreeField([change.value]));
            break;
        }
        case "remove": {
            const firstNode = navigateToNode(tree, change.firstNode);
            assert(firstNode !== undefined, "Down-path should point to a valid firstNode");
            const { parent: field, index } = firstNode.parentField;
            assert(field?.is(nodeSchema.objectNodeFieldsObject.sequenceChildren), "Defined down-path should point to a valid parent");
            field.removeRange(index, index + change.count);
            break;
        }
        case "move": {
            const firstNode = navigateToNode(tree, change.firstNode);
            assert(firstNode !== undefined, "Down-path should point to a valid firstNode");
            const { parent: field, index } = firstNode.parentField;
            assert(field?.is(nodeSchema.objectNodeFieldsObject.sequenceChildren), "Defined down-path should point to a valid parent");
            field.moveRangeToIndex(change.dstIndex, index, index + change.count);
            break;
        }
        default:
            fail("Invalid edit.");
    }
}
function applyValueFieldEdit(tree, change) {
    const nodeSchema = tree.currentSchema;
    assert(change.parent !== undefined, "Value change should not occur at the root.");
    const parent = navigateToNode(tree, change.parent);
    assert(parent?.is(nodeSchema), "Defined down-path should point to a valid parent");
    const field = parent.tryGetField(change.key);
    assert(field?.is(nodeSchema.objectNodeFieldsObject.requiredChild), "Parent of Value change should have an optional field to modify");
    field.content = cursorForJsonableTreeNode(change.value);
}
function navigateToNode(tree, path) {
    const nodeSchema = tree.currentSchema;
    const rootField = tree.flexTree;
    if (path === undefined) {
        return undefined;
    }
    const finalLocation = path.reduce(({ containedNode }, nextStep) => {
        const childField = containedNode?.tryGetField(nextStep.field);
        // Checking "=== true" causes tsc to fail to typecheck, as it is no longer able to narrow according
        // to the .is typeguard.
        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
        if (childField?.is(nodeSchema.objectNodeFieldsObject.sequenceChildren)) {
            assert(nextStep.index !== undefined);
            return {
                field: childField,
                containedNode: childField.at(nextStep.index),
            };
        }
        else if (
        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
        childField?.is(nodeSchema.objectNodeFieldsObject.optionalChild) ||
            childField?.is(nodeSchema.objectNodeFieldsObject.requiredChild)) {
            return { field: childField, containedNode: childField.content };
        }
        /* eslint-enable @typescript-eslint/strict-boolean-expressions */
        fail(`Unexpected field type: ${childField?.key}`);
    }, { field: rootField, containedNode: rootField.content });
    return finalLocation.containedNode;
}
function applyOptionalFieldEdit(tree, change) {
    const nodeSchema = tree.currentSchema;
    switch (change.type) {
        case "set": {
            const rootField = tree.flexTree;
            if (change.parent === undefined) {
                rootField.content = cursorForJsonableTreeNode(change.value);
            }
            else {
                const parent = navigateToNode(tree, change.parent);
                assert(parent?.is(nodeSchema), "Defined down-path should point to a valid parent");
                parent.boxedOptionalChild.content = cursorForJsonableTreeNode(change.value);
            }
            break;
        }
        case "remove": {
            const field = navigateToNode(tree, change.firstNode)?.parentField.parent;
            assert(field?.is(nodeSchema.objectNodeFieldsObject.optionalChild));
            field.content = undefined;
            break;
        }
        default:
            fail("Invalid edit.");
    }
}
export function applyTransactionEdit(state, contents) {
    state.transactionViews ?? (state.transactionViews = new Map());
    let view = state.transactionViews.get(state.client.channel);
    if (view === undefined) {
        assert(contents.fuzzType === "transactionStart", "Forked view should be present in the fuzz state unless a (non-nested) transaction is being started.");
        const treeView = viewFromState(state);
        view = treeView.fork();
        view.currentSchema = treeView.currentSchema;
        state.transactionViews.set(state.client.channel, view);
    }
    const { checkout } = view;
    switch (contents.fuzzType) {
        case "transactionStart": {
            checkout.transaction.start();
            break;
        }
        case "transactionCommit": {
            checkout.transaction.commit();
            break;
        }
        case "transactionAbort": {
            checkout.transaction.abort();
            break;
        }
        default:
            fail("Invalid edit.");
    }
    if (!checkout.transaction.inProgress()) {
        // Transaction is complete, so merge the changes into the root view and clean up the fork from the state.
        state.transactionViews.delete(state.client.channel);
        const rootView = viewFromState(state);
        rootView.checkout.merge(checkout);
    }
}
export function applyUndoRedoEdit(undoStack, redoStack, contents) {
    switch (contents.type) {
        case "undo": {
            undoStack.pop()?.revert();
            break;
        }
        case "redo": {
            redoStack.pop()?.revert();
            break;
        }
        default:
            fail("Invalid edit.");
    }
}
//# sourceMappingURL=fuzzEditReducers.js.map