/*!
 * 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 { brand, isJsonObject } from "../util";
import { createEmitter } from "../events";
import { isTransactionCommitChange } 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;
        /** Iff false, calls to `submitOp` will have no effect */
        this.submitOps = true;
        /**
         * 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();
        /**
         * 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.
         */
        // TODO: Change this type to be the Session ID type provided by the IdCompressor when available.
        const localSessionId = uuid();
        this.editManager = new EditManager(changeFamily, localSessionId, repairDataStoreProvider, anchors);
        this.editManager.on("newTrunkHead", (head) => {
            this.changeEvents.emit("newSequencedChange", head.change);
        });
        this.editManager.localBranch.on("change", (args) => {
            const { type, change } = args;
            switch (type) {
                case "append":
                    for (const c of args.newCommits) {
                        if (!this.getLocalBranch().isTransacting()) {
                            this.submitCommit(c);
                        }
                        this.changeEvents.emit("newLocalChange", c.change);
                    }
                    break;
                case "replace":
                    if (isTransactionCommitChange(args)) {
                        if (!this.getLocalBranch().isTransacting()) {
                            this.submitCommit(args.newCommits[0]);
                        }
                    }
                    break;
                default:
                    break;
            }
            if (change !== undefined) {
                this.changeEvents.emit("newLocalState", this.changeFamily.intoDelta(change));
            }
        });
        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);
    }
    /**
     * Used to edit the state of the tree. Edits will be immediately applied locally to the tree.
     * If there is no transaction currently ongoing, then the edits will be submitted to Fluid immediately as well.
     */
    get editor() {
        return this.getLocalBranch().editor;
    }
    // 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
     */
    submitCommit(commit) {
        if (!this.submitOps) {
            return;
        }
        // Edits should not be submitted until all transactions finish
        assert(!this.getLocalBranch().isTransacting(), 0x68b /* Unexpected edit submitted during transaction */);
        // 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(Object.assign(Object.assign({}, commit), { sessionId: this.editManager.localSessionId }), newRevision, this.detachedRevision);
        }
        const message = {
            revision: commit.revision,
            originatorId: this.editManager.localSessionId,
            changeset: this.changeCodec.json.encode(commit.change),
        };
        this.submitLocalMessage(message);
    }
    processCore(message, local, localOpMetadata) {
        const commit = parseCommit(message.contents, this.changeCodec);
        this.editManager.addSequencedChange(commit, brand(message.sequenceNumber), brand(message.referenceSequenceNumber));
        this.editManager.advanceMinimumSequenceNumber(brand(message.minimumSequenceNumber));
    }
    /**
     * 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() {
        this.editManager.localBranch.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() {
        this.editManager.localBranch.redo();
    }
    /**
     * @returns the head commit of the root local branch
     */
    getLocalBranch() {
        return this.editManager.localBranch;
    }
    onDisconnect() { }
    didAttach() {
        if (this.detachedRevision !== undefined) {
            this.detachedRevision = undefined;
        }
    }
    reSubmitCore(content, localOpMetadata) {
        const { revision } = parseCommit(content, this.changeCodec);
        const [commit] = this.editManager.findLocalCommit(revision);
        this.submitCommit(commit);
    }
    applyStashedOp(content) {
        assert(!this.getLocalBranch().isTransacting(), 0x674 /* Unexpected transaction is open while applying stashed ops */);
        const { revision, change } = parseCommit(content, this.changeCodec);
        this.submitOps = false;
        this.editManager.localBranch.apply(change, revision);
        this.submitOps = true;
        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