/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/common-utils";
import { isStableId } from "@fluidframework/container-runtime";
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
import { SharedObject, } from "@fluidframework/shared-object-base";
import { v4 as uuid } from "uuid";
import { AnchorSet, mintRevisionTag, findAncestor, UndoRedoManager, UndoRedoManagerCommitType, markCommits, rebaseBranch, } from "../core";
import { brand, isJsonObject, TransactionResult } from "../util";
import { createEmitter } from "../events";
import { TransactionStack } from "./transactionStack";
import { SharedTreeBranch } from "./branch";
import { EditManagerSummarizer } from "./editManagerSummarizer";
import { EditManager, minimumPossibleSequenceNumber } from "./editManager";
// TODO: How should the format version be determined?
const formatVersion = 0;
// TODO: Organize this to be adjacent to persisted types.
const summarizablesTreeKey = "indexes";
/**
 * Generic shared tree, which needs to be configured with indexes, field kinds and a history policy to be used.
 *
 * TODO: actually implement
 * TODO: is history policy a detail of what indexes are used, or is there something else to it?
 */
export class SharedTreeCore extends SharedObject {
    /**
     * @param summarizables - Summarizers for all indexes used by this tree
     * @param changeFamily - The change family
     * @param editManager - The edit manager
     * @param anchors - The anchor set
     * @param id - The id of the shared object
     * @param runtime - The IFluidDataStoreRuntime which contains the shared object
     * @param attributes - Attributes of the shared object
     * @param telemetryContextPrefix - the context for any telemetry logs/errors emitted
     */
    constructor(summarizables, changeFamily, anchors, repairDataStoreProvider, 
    // Base class arguments
    id, runtime, attributes, telemetryContextPrefix) {
        super(id, runtime, attributes, telemetryContextPrefix);
        this.changeFamily = changeFamily;
        this.anchors = anchors;
        /**
         * The sequence number that this instance is at.
         * This is number is artificial in that it is made up by this instance as opposed to being provided by the runtime.
         * Is `undefined` after (and only after) this instance is attached.
         */
        this.detachedRevision = minimumPossibleSequenceNumber;
        /**
         * Provides internal events that result from changes to the tree
         */
        this.changeEvents = createEmitter();
        this.transactions = new TransactionStack();
        /**
         * A random ID that uniquely identifies this client in the collab session.
         * This is sent alongside every op to identify which client the op originated from.
         * This is used rather than the Fluid client ID because the Fluid client ID is not stable across reconnections.
         */
        const localSessionId = uuid();
        const undoRedoManager = new UndoRedoManager(repairDataStoreProvider, changeFamily, () => this.editManager.getLocalBranchHead());
        this.editManager = new EditManager(changeFamily, localSessionId, undoRedoManager, undoRedoManager.clone(() => this.editManager.getTrunkHead()), anchors);
        this.summarizables = [
            new EditManagerSummarizer(runtime, this.editManager),
            ...summarizables,
        ];
        assert(new Set(this.summarizables.map((e) => e.key)).size === this.summarizables.length, 0x350 /* Index summary element keys must be unique */);
        this.changeCodec = changeFamily.codecs.resolve(formatVersion);
        this.editor = this.changeFamily.buildEditor((change) => this.applyChange(change, mintRevisionTag(), UndoRedoManagerCommitType.Undoable), new AnchorSet());
    }
    // TODO: SharedObject's merging of the two summary methods into summarizeCore is not what we want here:
    // We might want to not subclass it, or override/reimplement most of its functionality.
    summarizeCore(serializer, telemetryContext) {
        const builder = new SummaryTreeBuilder();
        const summarizableBuilder = new SummaryTreeBuilder();
        // Merge the summaries of all summarizables together under a single ISummaryTree
        for (const s of this.summarizables) {
            summarizableBuilder.addWithStats(s.key, s.getAttachSummary((contents) => serializer.stringify(contents, this.handle), undefined, undefined, telemetryContext));
        }
        builder.addWithStats(summarizablesTreeKey, summarizableBuilder.getSummaryTree());
        return builder.getSummaryTree();
    }
    async loadCore(services) {
        const loadSummaries = this.summarizables.map(async (summaryElement) => summaryElement.load(scopeStorageService(services, summarizablesTreeKey, summaryElement.key), (contents) => this.serializer.parse(contents)));
        await Promise.all(loadSummaries);
    }
    /**
     * Submits an op to the Fluid runtime containing the given commit
     * @param commit - the commit to submit
     * @param undoRedoType - if provided, `commit` will be tracked for undo/redo
     */
    submitCommit(commit, undoRedoType) {
        // Edits should not be submitted until all transactions finish
        assert(!this.isTransacting(), 0x673 /* Unexpected edit submitted during transaction */);
        // Nested transactions are tracked as part of the outermost transaction
        if (undoRedoType !== undefined) {
            this.editManager.localBranchUndoRedoManager.trackCommit(commit, undoRedoType);
        }
        // Edits submitted before the first attach are treated as sequenced because they will be included
        // in the attach summary that is uploaded to the service.
        // Until this attach workflow happens, this instance essentially behaves as a centralized data structure.
        if (this.detachedRevision !== undefined) {
            const newRevision = brand(this.detachedRevision + 1);
            this.detachedRevision = newRevision;
            this.editManager.addSequencedChange(commit, newRevision, this.detachedRevision);
        }
        const message = {
            revision: commit.revision,
            originatorId: this.editManager.localSessionId,
            changeset: this.changeCodec.json.encode(commit.change),
        };
        this.submitLocalMessage(message);
    }
    /**
     * Update the state of the tree (including all indexes) according to the given change by creating a new commit and
     * appending it the root local branch. If there is not currently a transaction open, the change will be submitted to Fluid.
     * @param change - The change to apply.
     * @param revision - The revision to associate with the change.
     * @param undoRedoType - if provided, the new commit will be tracked for undo/redo
     * @returns the new commit that was appended to the root local branch
     */
    applyChange(change, revision, undoRedoType) {
        const [commit, delta] = this.addLocalChange(change, revision, undoRedoType);
        // submitCommit should not be called for stashed ops so this is kept separate from
        // addLocalChange
        if (!this.isTransacting()) {
            this.submitCommit(commit, undoRedoType);
        }
        this.changeFamily.rebaser.rebaseAnchors(this.anchors, change);
        this.emitLocalChange(change, delta);
        return commit;
    }
    /**
     * Creates a new commit with the given change and appends it the root local branch.
     * @param change - The change to apply
     * @param revision - The revision to associate with the change.
     * @returns the commit and the delta resulting from applying `change`
     */
    addLocalChange(change, revision, undoRedoType) {
        var _a;
        const commit = {
            change,
            revision,
            sessionId: this.editManager.localSessionId,
        };
        const delta = this.editManager.addLocalChange(revision, change, false, undoRedoType);
        (_a = this.transactions.repairStore) === null || _a === void 0 ? void 0 : _a.capture(this.changeFamily.intoDelta(change), revision);
        return [commit, delta];
    }
    emitLocalChange(change, delta) {
        this.changeEvents.emit("newLocalChange", change);
        this.changeEvents.emit("newLocalState", delta);
    }
    processCore(message, local, localOpMetadata) {
        const commit = parseCommit(message.contents, this.changeCodec);
        const delta = this.editManager.addSequencedChange(commit, brand(message.sequenceNumber), brand(message.referenceSequenceNumber));
        const sequencedChange = this.editManager.getLastSequencedChange();
        this.changeEvents.emit("newSequencedChange", sequencedChange);
        this.changeEvents.emit("newLocalState", delta);
        this.editManager.advanceMinimumSequenceNumber(brand(message.minimumSequenceNumber));
    }
    startTransaction(repairStore) {
        if (!this.isTransacting()) {
            // If this is the start of a transaction stack, freeze the undo redo manager's
            // repair data store provider so that repair data can be captured based on the
            // state of the branch at the start of the transaction.
            this.editManager.localBranchUndoRedoManager.repairDataStoreProvider.freeze();
        }
        this.transactions.push(this.editManager.getLocalBranchHead().revision, repairStore);
        this.editor.enterTransaction();
    }
    commitTransaction() {
        const { startRevision } = this.transactions.pop();
        this.editor.exitTransaction();
        const squashCommit = this.editManager.squashLocalChanges(startRevision);
        if (!this.isTransacting()) {
            this.submitCommit(squashCommit, UndoRedoManagerCommitType.Undoable);
        }
        return TransactionResult.Commit;
    }
    abortTransaction() {
        const { startRevision, repairStore } = this.transactions.pop();
        this.editor.exitTransaction();
        const delta = this.editManager.rollbackLocalChanges(startRevision, repairStore);
        this.changeEvents.emit("newLocalState", delta);
        return TransactionResult.Abort;
    }
    isTransacting() {
        return this.transactions.size !== 0;
    }
    get localBranchUndoRedoManager() {
        return this.editManager.localBranchUndoRedoManager;
    }
    /**
     * Undoes the last completed transaction made by the client.
     * It is invalid to call it while a transaction is open (this will be supported in the future).
     */
    undo() {
        // TODO: allow this once it becomes possible to compose the changesets created by edits made
        // within transactions and edits that represent completed transactions.
        assert(!this.isTransacting(), 0x66b /* Undo is not yet supported during transactions */);
        const undoChange = this.editManager.localBranchUndoRedoManager.undo();
        if (undoChange !== undefined) {
            this.applyChange(undoChange, mintRevisionTag(), UndoRedoManagerCommitType.Undo);
        }
    }
    /**
     * Redoes the last completed undo made by the client.
     * It is invalid to call it while a transaction is open (this will be supported in the future).
     */
    redo() {
        // TODO: allow this once it becomes possible to compose the changesets created by edits made
        // within transactions and edits that represent completed transactions.
        assert(!this.isTransacting(), 0x67f /* Redo is not yet supported during transactions */);
        const redoChange = this.editManager.localBranchUndoRedoManager.redo();
        if (redoChange !== undefined) {
            this.applyChange(redoChange, mintRevisionTag(), UndoRedoManagerCommitType.Redo);
        }
    }
    /**
     * Spawns a `SharedTreeBranch` that is based on the current state of the tree.
     * This can be used to support asynchronous checkouts of the tree.
     * @remarks
     * Branches are valid until they are disposed. Branches should be disposed when
     * they are no longer needed because it allows `SharedTreeCore` to free memory.
     * Branches are no longer guaranteed to be based off of the trunk once disposed.
     */
    createBranch(repairDataStoreProvider, anchors) {
        const branch = new SharedTreeBranch(this.editManager.getLocalBranchHead(), this.editManager.localSessionId, this.changeFamily, this.editManager.localBranchUndoRedoManager.clone(() => branch.getHead(), repairDataStoreProvider), anchors);
        this.editManager.registerBranch(branch);
        return branch;
    }
    /**
     * Merges the commits of the given branch into the root local branch.
     * This behaves as if all divergent commits on the branch were applied to the root local branch one at a time.
     * @param branch - the branch to merge
     */
    mergeBranch(branch) {
        assert(!branch.isTransacting(), 0x5cb /* Branch may not be merged while transaction is in progress */);
        const commits = [];
        const localBranchHead = this.editManager.getLocalBranchHead();
        const ancestor = findAncestor([branch.getHead(), commits], (c) => c === localBranchHead);
        if (ancestor === localBranchHead) {
            const markedCommits = markCommits(commits, branch.undoRedoManager.headUndoable, branch.undoRedoManager.headRedoable);
            for (const { commit: { change, revision }, undoRedoManagerCommitType, } of markedCommits) {
                // Only track commits that are undoable.
                this.applyChange(change, revision, undoRedoManagerCommitType);
            }
        }
        else {
            const [newHead] = rebaseBranch(this.changeFamily.rebaser, branch.getHead(), this.getLocalBranchHead());
            const changes = [];
            findAncestor([newHead, changes], (c) => c === localBranchHead);
            this.editManager.localBranchUndoRedoManager.updateAfterMerge(newHead, branch.undoRedoManager);
            // Apply the changes without tracking them in the undo redo manager because
            // `updateAfterRebase` takes care of tracking any applicable commits in the rebased branch.
            changes.forEach(({ change, revision }) => {
                this.applyChange(change, revision, undefined);
            });
        }
    }
    /**
     * @returns the head commit of the root local branch
     */
    getLocalBranchHead() {
        return this.editManager.getLocalBranchHead();
    }
    onDisconnect() { }
    didAttach() {
        if (this.detachedRevision !== undefined) {
            this.detachedRevision = undefined;
        }
    }
    reSubmitCore(content, localOpMetadata) {
        const { revision } = parseCommit(content, this.changeCodec);
        const [commit] = this.editManager.findLocalCommit(revision);
        // Skip tracking commits as undoable during resubmit.
        this.submitCommit(commit, undefined);
    }
    applyStashedOp(content) {
        assert(!this.isTransacting(), 0x674 /* Unexpected transaction is open while applying stashed ops */);
        const { revision, change } = parseCommit(content, this.changeCodec);
        const [commit, delta] = this.addLocalChange(change, revision, UndoRedoManagerCommitType.Undoable);
        this.editManager.localBranchUndoRedoManager.trackCommit(commit, UndoRedoManagerCommitType.Undoable);
        this.emitLocalChange(change, delta);
        return;
    }
    getGCData(fullGC) {
        var _a;
        const gcNodes = {};
        for (const s of this.summarizables) {
            for (const [id, routes] of Object.entries(s.getGCData(fullGC).gcNodes)) {
                (_a = gcNodes[id]) !== null && _a !== void 0 ? _a : (gcNodes[id] = []);
                for (const route of routes) {
                    gcNodes[id].push(route);
                }
            }
        }
        return {
            gcNodes,
        };
    }
}
/**
 * Compose an {@link IChannelStorageService} which prefixes all paths before forwarding them to the original service
 */
function scopeStorageService(service, ...pathElements) {
    const scope = `${pathElements.join("/")}/`;
    return {
        async readBlob(path) {
            return service.readBlob(`${scope}${path}`);
        },
        async contains(path) {
            return service.contains(`${scope}${path}`);
        },
        async list(path) {
            return service.list(`${scope}${path}`);
        },
    };
}
/**
 * validates that the message contents is an object which contains valid revisionId, sessionId, and changeset and returns a Commit
 * @param content - message contents
 * @returns a Commit object
 */
function parseCommit(content, codec) {
    assert(isJsonObject(content), 0x5e4 /* expected content to be an object */);
    assert(typeof content.revision === "string" && isStableId(content.revision), 0x5e5 /* expected revision id to be valid stable id */);
    assert(content.changeset !== undefined, 0x5e7 /* expected changeset to be defined */);
    assert(typeof content.originatorId === "string", 0x5e8 /* expected changeset to be defined */);
    const change = codec.json.decode(content.changeset);
    return { revision: content.revision, sessionId: content.originatorId, change };
}
//# sourceMappingURL=sharedTreeCore.js.map