/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { makeAnonChange, tagChange, tagRollbackInverse, } from "./changeRebaser.js";
import { mintCommit } from "./types.js";
export function rebaseBranch(mintRevisionTag, changeRebaser, sourceHead, targetCommit, targetHead = targetCommit) {
    // Get both source and target as path arrays
    const sourcePath = [];
    const targetPath = [];
    const ancestor = findCommonAncestor([sourceHead, sourcePath], [targetHead, targetPath]);
    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"
        assert(findCommonAncestor(targetCommit, targetHead) !== undefined, 0x676 /* target commit is not in target branch */);
        return {
            newSourceHead: sourceHead,
            sourceChange: undefined,
            commits: { 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(mintCommit(sourceCommits[sourceCommits.length - 1] ?? newBase, c));
        }
        return {
            newSourceHead: sourceCommits[sourceCommits.length - 1] ?? newBase,
            sourceChange: undefined,
            commits: {
                deletedSourceCommits,
                targetCommits,
                sourceCommits,
            },
        };
    }
    // 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.
    let newHead = newBase;
    const revInfos = getRevInfoFromTaggedChanges(targetRebasePath);
    // Note that the `revisionMetadata` gets updated as `revInfos` gets updated.
    const revisionMetadata = revisionMetadataSourceFromInfo(revInfos);
    let currentComposedEdit = makeAnonChange(changeRebaser.compose(targetRebasePath));
    for (const c of sourcePath) {
        const inverseTag = mintRevisionTag();
        const inverse = tagRollbackInverse(changeRebaser.invert(c, true), inverseTag, c.revision);
        const editsToCompose = [inverse, currentComposedEdit];
        if (sourceSet.has(c.revision)) {
            const change = changeRebaser.rebase(c.change, currentComposedEdit, revisionMetadata);
            newHead = {
                revision: c.revision,
                change,
                parent: newHead,
            };
            sourceCommits.push(newHead);
            editsToCompose.push(tagChange(change, c.revision));
        }
        currentComposedEdit = makeAnonChange(changeRebaser.compose(editsToCompose));
        revInfos.unshift({ revision: inverseTag, rollbackOf: inverse.rollbackOf });
        revInfos.push({ revision: c.revision });
    }
    return {
        newSourceHead: newHead,
        sourceChange: currentComposedEdit.change,
        commits: {
            deletedSourceCommits,
            targetCommits,
            sourceCommits,
        },
    };
}
/**
 * "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.
 */
export function rebaseChange(changeRebaser, change, sourceHead, targetHead, mintRevisionTag) {
    const sourcePath = [];
    const targetPath = [];
    assert(findCommonAncestor([sourceHead, sourcePath], [targetHead, targetPath]) !== undefined, 0x576 /* branch A and branch B must be related */);
    const inverses = sourcePath.map((commit) => inverseFromCommit(changeRebaser, commit, mintRevisionTag, true));
    inverses.reverse();
    return rebaseChangeOverChanges(changeRebaser, change, [...inverses, ...targetPath]);
}
/**
 * @internal
 */
export function revisionMetadataSourceFromInfo(revInfos) {
    const getIndex = (revision) => {
        const index = revInfos.findIndex((revInfo) => revInfo.revision === revision);
        return index >= 0 ? index : undefined;
    };
    const tryGetInfo = (revision) => {
        if (revision === undefined) {
            return undefined;
        }
        const index = getIndex(revision);
        return index === undefined ? undefined : revInfos[index];
    };
    const hasRollback = (revision) => {
        return revInfos.find((info) => info.rollbackOf === revision) !== undefined;
    };
    return { getIndex, tryGetInfo, hasRollback };
}
export function rebaseChangeOverChanges(changeRebaser, changeToRebase, changesToRebaseOver) {
    const revisionMetadata = revisionMetadataSourceFromInfo(getRevInfoFromTaggedChanges(changesToRebaseOver));
    return changesToRebaseOver.reduce((a, b) => changeRebaser.rebase(a, b, revisionMetadata), changeToRebase);
}
// TODO: Deduplicate
function getRevInfoFromTaggedChanges(changes) {
    const revInfos = [];
    for (const taggedChange of changes) {
        revInfos.push(...revisionInfoFromTaggedChange(taggedChange));
    }
    return revInfos;
}
// TODO: Deduplicate
function revisionInfoFromTaggedChange(taggedChange) {
    const revInfos = [];
    if (taggedChange.revision !== undefined) {
        const info = { revision: taggedChange.revision };
        if (taggedChange.rollbackOf !== undefined) {
            info.rollbackOf = taggedChange.rollbackOf;
        }
        revInfos.push(info);
    }
    return revInfos;
}
function inverseFromCommit(changeRebaser, commit, mintRevisionTag, cache) {
    const inverse = commit.inverse ?? changeRebaser.invert(commit, true);
    if (cache === true && commit.inverse === undefined) {
        commit.inverse = inverse;
    }
    return tagRollbackInverse(inverse, mintRevisionTag(), commit.revision);
}
export 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;
}
/**
 * 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]
 * ```
 */
export function findCommonAncestor(descendantA, descendantB) {
    let a;
    let b;
    let pathA;
    let pathB;
    if (Array.isArray(descendantA)) {
        [a, pathA] = descendantA;
        assert(pathA === undefined || pathA.length === 0, 0x578 /* Path A must be empty */);
    }
    else {
        a = descendantA;
    }
    if (Array.isArray(descendantB)) {
        [b, pathB] = descendantB;
        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;
}
//# sourceMappingURL=utils.js.map