/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _SharedTreeBranch_events;
import { assert } from "@fluidframework/core-utils/internal";
import { measure } from "@fluidframework/telemetry-utils/internal";
import { CommitKind, findAncestor, makeAnonChange, mintCommit, rebaseBranch, tagRollbackInverse, } from "../core/index.js";
import { createEmitter } from "@fluid-internal/client-utils";
import { hasSome, defineLazyCachedProperty } from "../util/index.js";
/**
 * A branch of changes that can be applied to a SharedTree.
 */
export class SharedTreeBranch {
    /**
     * Construct a new branch.
     * @param head - the head of the branch
     * @param changeFamily - determines the set of changes that this branch can commit
     * @param branchTrimmer - an optional event emitter that informs the branch it has been trimmed. If this is not supplied, then the branch must
     * never be trimmed. See {@link BranchTrimmingEvents} for details on trimming.
     */
    constructor(head, changeFamily, mintRevisionTag, branchTrimmer, telemetryEventBatcher) {
        this.head = head;
        this.changeFamily = changeFamily;
        this.mintRevisionTag = mintRevisionTag;
        this.branchTrimmer = branchTrimmer;
        this.telemetryEventBatcher = telemetryEventBatcher;
        _SharedTreeBranch_events.set(this, createEmitter());
        this.events = __classPrivateFieldGet(this, _SharedTreeBranch_events, "f");
        this.disposed = false;
        this.editor = this.changeFamily.buildEditor(mintRevisionTag, (change) => this.apply(change));
        this.unsubscribeBranchTrimmer = branchTrimmer?.on("ancestryTrimmed", (commit) => {
            __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("ancestryTrimmed", commit);
        });
    }
    /**
     * Sets the head of this branch.
     * @remarks This is a "manual override" of sorts, for when the branch needs to be set to a certain state without going through the usual flow of edits.
     * This might be necessary as a performance optimization, or to prevent parts of the system updating incorrectly (this method emits no change events!).
     */
    setHead(head) {
        this.assertNotDisposed();
        this.head = head;
    }
    /**
     * Apply a change to this branch.
     * @param change - the change to apply
     * @param kind - the kind of change to apply
     * @returns the change that was applied and the new head commit of the branch
     */
    apply(change, kind = CommitKind.Default) {
        this.assertNotDisposed();
        const revisionTag = change.revision;
        assert(revisionTag !== undefined, 0xa49 /* Revision tag must be provided */);
        const newHead = mintCommit(this.head, {
            revision: revisionTag,
            change: change.change,
        });
        const changeEvent = {
            type: "append",
            kind,
            change,
            newCommits: [newHead],
        };
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("beforeChange", changeEvent);
        this.head = newHead;
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("afterChange", changeEvent);
    }
    /**
     * @returns the commit at the head of this branch.
     */
    getHead() {
        return this.head;
    }
    /**
     * Spawn a new branch that is based off of the current state of this branch.
     * @param commit - The commit to base the new branch off of. Defaults to the head of this branch.
     * @remarks Changes made to the new branch will not be applied to this branch until the new branch is {@link SharedTreeBranch.merge | merged} back in.
     * Forks created during a transaction will be disposed when the transaction ends.
     */
    fork(commit = this.head) {
        this.assertNotDisposed();
        const fork = new SharedTreeBranch(commit, this.changeFamily, this.mintRevisionTag, this.branchTrimmer);
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("fork", fork);
        return fork;
    }
    /**
     * Rebase the changes that have been applied to this branch over divergent changes in the given branch.
     * After this operation completes, this branch will be based off of `branch`.
     *
     * @param branch - the branch to rebase onto
     * @param upTo - the furthest commit on `branch` over which to rebase (inclusive). Defaults to the head commit of `branch`.
     * @returns the result of the rebase or undefined if nothing changed
     */
    rebaseOnto(branch, upTo = branch.getHead()) {
        this.assertNotDisposed();
        // Rebase this branch onto the given branch
        const rebaseResult = this.rebaseBranch(this, branch, upTo);
        if (rebaseResult === undefined) {
            return;
        }
        // The net change to this branch is provided by the `rebaseBranch` API
        const { newSourceHead, commits } = rebaseResult;
        const { deletedSourceCommits, targetCommits, sourceCommits } = commits;
        assert(hasSome(targetCommits), 0xa83 /* Expected commit(s) for a non no-op rebase */);
        const newCommits = targetCommits.concat(sourceCommits);
        const changeEvent = {
            type: "rebase",
            get change() {
                const change = rebaseResult.sourceChange;
                return change === undefined ? undefined : makeAnonChange(change);
            },
            removedCommits: deletedSourceCommits,
            newCommits,
        };
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("beforeChange", changeEvent);
        this.head = newSourceHead;
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("afterChange", changeEvent);
    }
    /**
     * Remove a range of commits from this branch.
     * @param commit - All commits after (but not including) this commit will be removed.
     * @returns The net change to this branch and the commits that were removed from this branch.
     */
    removeAfter(commit) {
        if (commit === this.head) {
            return;
        }
        const removedCommits = [];
        const inverses = [];
        findAncestor([this.head, removedCommits], (c) => {
            // TODO: Pull this side effect out if/when more diverse ancestry walking helpers are available
            if (c !== commit) {
                const revision = this.mintRevisionTag();
                const inverse = this.changeFamily.rebaser.changeRevision(this.changeFamily.rebaser.invert(c, true, revision), revision, c.revision);
                inverses.push(tagRollbackInverse(inverse, revision, c.revision));
                return false;
            }
            return true;
        });
        assert(hasSome(removedCommits), 0xa84 /* Commit must be in the branch's ancestry */);
        const change = makeAnonChange(this.changeFamily.rebaser.compose(inverses));
        const changeEvent = {
            type: "remove",
            change,
            removedCommits,
        };
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("beforeChange", changeEvent);
        this.head = commit;
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("afterChange", changeEvent);
    }
    /**
     * Apply all the divergent changes on the given branch to this branch.
     *
     * @param branch - the branch to merge into this branch
     * @returns the net change to this branch and the commits that were added to this branch by the merge,
     * or undefined if nothing changed
     */
    merge(branch) {
        this.assertNotDisposed();
        branch.assertNotDisposed();
        if (branch === this) {
            return undefined;
        }
        // Rebase the given branch onto this branch
        const rebaseResult = this.rebaseBranch(branch, this);
        if (rebaseResult === undefined) {
            return undefined;
        }
        // Compute the net change to this branch
        const sourceCommits = rebaseResult.commits.sourceCommits;
        assert(hasSome(sourceCommits), 0xa86 /* Expected source commits in non no-op merge */);
        const { rebaser } = this.changeFamily;
        const changeEvent = defineLazyCachedProperty({
            type: "append",
            kind: CommitKind.Default,
            newCommits: sourceCommits,
        }, "change", () => makeAnonChange(rebaser.compose(sourceCommits)));
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("beforeChange", changeEvent);
        this.head = rebaseResult.newSourceHead;
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("afterChange", changeEvent);
    }
    /** Rebase `branchHead` onto `onto`, but return undefined if nothing changed */
    rebaseBranch(branch, onto, upTo = onto.getHead()) {
        const { head } = branch;
        if (head === upTo) {
            return undefined;
        }
        const { duration, output } = measure(() => rebaseBranch(this.mintRevisionTag, this.changeFamily.rebaser, head, upTo, onto.getHead()));
        this.telemetryEventBatcher?.accumulateAndLog({ duration, ...output.telemetryProperties });
        if (this.head === output.newSourceHead) {
            return undefined;
        }
        return output;
    }
    /**
     * Dispose this branch, freezing its state.
     *
     * @remarks
     * Attempts to further mutate the branch will error.
     * Any transactions in progress will be aborted.
     * Calling dispose more than once has no effect.
     */
    dispose() {
        if (this.disposed) {
            return;
        }
        this.unsubscribeBranchTrimmer?.();
        this.disposed = true;
        __classPrivateFieldGet(this, _SharedTreeBranch_events, "f").emit("dispose");
    }
    assertNotDisposed() {
        assert(!this.disposed, 0x66e /* Branch is disposed */);
    }
}
_SharedTreeBranch_events = new WeakMap();
/**
 * Registers an event listener that fires when the given branch forks.
 * The listener will also fire when any of those forks fork, and when those forks of forks fork, and so on.
 * @param branch - the branch that will be listened to for forks
 * @param onFork - the fork event listener
 * @returns a function which when called will deregister all registrations (including transitive) created by this function.
 * The deregister function has undefined behavior if called more than once.
 */
// Branches are invariant over TChange
export function onForkTransitive(branch, onFork) {
    const offs = [];
    offs.push(branch.events.on("fork", (fork) => {
        offs.push(onForkTransitive(fork, onFork));
        onFork(fork);
    }));
    return () => offs.forEach((off) => off());
}
//# sourceMappingURL=branch.js.map