"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.findCommonAncestor = exports.findAncestor = exports.rebaseChange = exports.rebaseBranch = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../../util");
const changeRebaser_1 = require("./changeRebaser");
const types_1 = require("./types");
function rebaseBranch(changeRebaser, sourceRepairDataStoreProvider, sourceHead, targetCommit, targetHead = targetCommit) {
    // Get both source and target as path arrays
    const sourcePath = [];
    const targetPath = [];
    const ancestor = findCommonAncestor([sourceHead, sourcePath], [targetHead, targetPath]);
    (0, core_utils_1.assert)(ancestor !== undefined, 0x675 /* branches must be related */);
    // Find where `targetCommit` is in the target branch
    const targetCommitIndex = targetPath.findIndex((r) => r === targetCommit);
    if (targetCommitIndex === -1) {
        // If the targetCommit is not in the target path, then it is either disjoint from `target` or it is behind/at
        // the commit where source and target diverge (ancestor), in which case there is nothing more to rebase
        // TODO: Ideally, this would be an "assertExpensive"
        (0, core_utils_1.assert)(findCommonAncestor(targetCommit, targetHead) !== undefined, 0x676 /* target commit is not in target branch */);
        return [
            sourceHead,
            undefined,
            { deletedSourceCommits: [], targetCommits: [], sourceCommits: sourcePath },
        ];
    }
    // Iterate through the target path and look for commits that are also present on the source branch (i.e. they
    // have matching tags). Each commit found in the target branch can be skipped when processing the source branch
    // because it has already been rebased onto the target. In the case that one or more of these commits are present
    // directly after `targetCommit`, then the new base can be advanced further without having to do any work.
    const sourceSet = new Set(sourcePath.map((r) => r.revision));
    let newBaseIndex = targetCommitIndex;
    for (let i = 0; i < targetPath.length; i += 1) {
        const { revision } = targetPath[i];
        if (sourceSet.has(revision)) {
            sourceSet.delete(revision);
            newBaseIndex = Math.max(newBaseIndex, i);
        }
        else if (i >= targetCommitIndex) {
            break;
        }
    }
    /** The commit on the target branch that the new source branch branches off of (i.e. the new common ancestor) */
    const newBase = targetPath[newBaseIndex];
    // Figure out how much of the trunk to start rebasing over.
    const targetCommits = targetPath.slice(0, newBaseIndex + 1);
    const deletedSourceCommits = [...sourcePath];
    // If the source and target rebase path begin with a range that has all the same revisions, remove it; it is
    // equivalent on both branches and doesn't need to be rebased.
    const targetRebasePath = [...targetCommits];
    const minLength = Math.min(sourcePath.length, targetRebasePath.length);
    for (let i = 0; i < minLength; i++) {
        if (sourcePath[0].revision === targetRebasePath[0].revision) {
            sourcePath.shift();
            targetRebasePath.shift();
        }
    }
    const sourceCommits = [];
    // If all commits that are about to be rebased over on the target branch already comprise the start of the source branch,
    // are in the same order, and have no other commits interleaving them, then no rebasing needs to occur. Those commits can
    // simply be removed from the source branch, and the remaining commits on the source branch are reparented off of the new
    // base commit.
    if (targetRebasePath.length === 0) {
        for (const c of sourcePath) {
            sourceCommits.push((0, types_1.mintCommit)(sourceCommits[sourceCommits.length - 1] ?? newBase, c));
        }
        return [
            sourceCommits[sourceCommits.length - 1] ?? newBase,
            undefined,
            {
                deletedSourceCommits,
                targetCommits,
                sourceCommits,
            },
        ];
    }
    let newHead = newBase;
    const inverses = [];
    if (sourcePath.length !== 0) {
        // Clone the original repair data store provider so that it can be modified without affecting the original.
        const repairDataStoreProviderClone = sourceRepairDataStoreProvider?.clone();
        const nonTaggedInverses = [];
        // Revert changes from the source path to get to the new base
        for (let i = sourcePath.length - 1; i >= 0; i--) {
            const c = sourcePath[i];
            const inverse = changeRebaser.invert(c, true, c.repairData);
            nonTaggedInverses.push(inverse);
            repairDataStoreProviderClone?.applyChange(inverse);
        }
        if (repairDataStoreProviderClone !== undefined) {
            // Apply the changes in the target rebase path
            for (const c of targetRebasePath) {
                repairDataStoreProviderClone.applyChange(c.change);
            }
        }
        // For each source commit, rebase backwards over the inverses of any commits already rebased, and then
        // rebase forwards over the rest of the commits up to the new base before advancing the new base.
        for (const c of sourcePath) {
            if (sourceSet.has(c.revision)) {
                const change = rebaseChangeOverChanges(changeRebaser, c.change, [
                    ...inverses,
                    ...targetRebasePath,
                ]);
                const repairData = repairDataStoreProviderClone?.createRepairData();
                repairData?.capture(change, c.revision);
                newHead = {
                    revision: c.revision,
                    change,
                    parent: newHead,
                    repairData,
                };
                sourceCommits.push(newHead);
                targetRebasePath.push({ ...c, change });
                repairDataStoreProviderClone?.applyChange(change);
            }
            inverses.unshift((0, changeRebaser_1.tagRollbackInverse)(nonTaggedInverses.pop() ??
                (0, util_1.fail)("The commits in source path should not be modified."), (0, types_1.mintRevisionTag)(), c.revision));
        }
    }
    return [
        newHead,
        changeRebaser.compose([...inverses, ...targetRebasePath]),
        {
            deletedSourceCommits,
            targetCommits,
            sourceCommits,
        },
    ];
}
exports.rebaseBranch = rebaseBranch;
/**
 * "Sandwich/Horseshoe Rebase" a change over the given source and target branches
 * @param changeRebaser - the change rebaser responsible for rebasing the change over the commits in each branch
 * @param change - the change to rebase
 * @param sourceHead - the head of the branch that `change` is based on
 * @param targetHead - the branch to rebase `change` onto
 * @returns the rebased change
 *
 * @remarks inverses will be cached.
 */
function rebaseChange(changeRebaser, change, sourceHead, targetHead) {
    const sourcePath = [];
    const targetPath = [];
    (0, core_utils_1.assert)(findCommonAncestor([sourceHead, sourcePath], [targetHead, targetPath]) !== undefined, 0x576 /* branch A and branch B must be related */);
    const changeRebasedToRef = sourcePath.reduceRight((newChange, branchCommit) => changeRebaser.rebase(newChange, inverseFromCommit(changeRebaser, branchCommit, branchCommit.repairData, true)), change);
    return targetPath.reduce((a, b) => changeRebaser.rebase(a, b), changeRebasedToRef);
}
exports.rebaseChange = rebaseChange;
function rebaseChangeOverChanges(changeRebaser, changeToRebase, changesToRebaseOver) {
    return changesToRebaseOver.reduce((a, b) => changeRebaser.rebase(a, b), changeToRebase);
}
function inverseFromCommit(changeRebaser, commit, repairData, cache) {
    const inverse = commit.inverse ?? changeRebaser.invert(commit, true, repairData);
    if (cache === true && commit.inverse === undefined) {
        commit.inverse = inverse;
    }
    return (0, changeRebaser_1.tagRollbackInverse)(inverse, (0, types_1.mintRevisionTag)(), commit.revision);
}
function findAncestor(descendant, predicate = (t) => t.parent === undefined) {
    let d;
    let path;
    if (Array.isArray(descendant)) {
        [d, path] = descendant;
    }
    else {
        d = descendant;
    }
    for (let cur = d; cur !== undefined; cur = cur.parent) {
        if (predicate(cur)) {
            path?.reverse();
            return cur;
        }
        path?.push(cur);
    }
    if (path !== undefined) {
        path.length = 0;
    }
    return undefined;
}
exports.findAncestor = findAncestor;
/**
 * Find a common ancestor between two descendants that are linked by parent pointers.
 * @param descendantA - a descendant. If an empty `path` array is included, it will be populated
 * with the chain of commits from the ancestor to `descendantA` (not including the ancestor).
 * @param descendantB - another descendant. If an empty `path` array is included, it will be populated
 * with the chain of commits from the ancestor to `descendantB` (not including the ancestor).
 * @returns the common ancestor of `descendantA` and `descendantB`, or `undefined` if no such ancestor exists.
 *
 * @example
 *
 * ```typescript
 * interface Parented {
 *   parent?: Parented;
 * }
 * const shared = {};
 * const a = { parent: shared };
 * const b1 = { parent: shared };
 * const b2 = { parent: b1 };
 * const pathB: Parented[] = []
 * const ancestor = findCommonAncestor<Parented>(a, [b2, pathB]);
 * // ancestor === shared
 * // pathB === [b1, b2]
 * ```
 */
function findCommonAncestor(descendantA, descendantB) {
    let a;
    let b;
    let pathA;
    let pathB;
    if (Array.isArray(descendantA)) {
        [a, pathA] = descendantA;
        (0, core_utils_1.assert)(pathA === undefined || pathA.length === 0, 0x578 /* Path A must be empty */);
    }
    else {
        a = descendantA;
    }
    if (Array.isArray(descendantB)) {
        [b, pathB] = descendantB;
        (0, core_utils_1.assert)(pathB === undefined || pathB.length === 0, 0x579 /* Path B must be empty */);
    }
    else {
        b = descendantB;
    }
    if (a === b) {
        return a;
    }
    const reversePaths = () => {
        pathA?.reverse();
        pathB?.reverse();
    };
    const visited = new Set();
    while (a !== undefined || b !== undefined) {
        if (a !== undefined) {
            if (visited.has(a)) {
                if (pathB !== undefined) {
                    pathB.length = pathB.findIndex((r) => Object.is(r, a));
                }
                reversePaths();
                return a;
            }
            visited.add(a);
            pathA?.push(a);
            a = a.parent;
        }
        if (b !== undefined) {
            if (visited.has(b)) {
                if (pathA !== undefined) {
                    pathA.length = pathA.findIndex((r) => Object.is(r, b));
                }
                reversePaths();
                return b;
            }
            visited.add(b);
            pathB?.push(b);
            b = b.parent;
        }
    }
    if (pathA !== undefined) {
        pathA.length = 0;
    }
    if (pathB !== undefined) {
        pathB.length = 0;
    }
    return undefined;
}
exports.findCommonAncestor = findCommonAncestor;
//# sourceMappingURL=utils.js.map