/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { AnchorSet, applyDelta, castCursorToSynchronous, Delta, EmptyKey, getDescendant, InMemoryStoredSchemaRepository, keyAsDetachedField, moveToDetachedField, SparseNode, } from "../core";
import { chunkTree, defaultChunkPolicy } from "./chunked-forest";
const repairDataFactory = () => ({});
const undefinedFactory = () => undefined;
export class ForestRepairDataStore {
    constructor(forest, intoDelta) {
        this.forest = forest;
        this.intoDelta = intoDelta;
        this.root = new SparseNode(EmptyKey, 0, undefined, undefined);
    }
    capture(change, revision) {
        /**
         * Cursor used to traverse the forest and build fetch the repair data.
         * Note that the cursor is implicitly captured by the functions below, which have requirements for the cursor.
         * Calling those functions requires that the cursor be in the appropriate state.
         */
        const cursor = this.forest.allocateCursor();
        const delta = this.intoDelta(change);
        /**
         * Visits the node `cursor` is positioned at.
         * Restores the `cursor` to that same position before exiting.
         */
        const visitFieldMarks = (fields, parent) => {
            for (const [key, field] of fields) {
                if (parent !== this.root) {
                    cursor.enterField(key);
                }
                else {
                    moveToDetachedField(this.forest, cursor, keyAsDetachedField(key));
                }
                visitField(field, parent, key);
                if (parent !== this.root) {
                    cursor.exitField();
                }
            }
        };
        /**
         * Visits the field `cursor` is positioned at.
         * Restores the `cursor` to that same position before exiting.
         */
        function visitField(field, parent, key) {
            let index = 0;
            for (const mark of field) {
                if (typeof mark === "number") {
                    // Untouched nodes
                    index += mark;
                }
                else {
                    // Inline into `switch(mark.type)` once we upgrade to TS 4.7
                    const type = mark.type;
                    switch (type) {
                        case Delta.MarkType.MoveOut:
                        case Delta.MarkType.Delete: {
                            cursor.enterNode(index);
                            const child = parent.getOrCreateChild(key, index, repairDataFactory);
                            visitModify(mark, child);
                            cursor.exitNode();
                            onDelete(parent, key, index, mark.count);
                            index += mark.count;
                            break;
                        }
                        case Delta.MarkType.Modify: {
                            cursor.enterNode(index);
                            const child = parent.getOrCreateChild(key, index, undefinedFactory);
                            visitModify(mark, child);
                            cursor.exitNode();
                            index += 1;
                            break;
                        }
                        case Delta.MarkType.Insert:
                        case Delta.MarkType.MoveIn:
                            break;
                        default:
                            unreachableCase(type);
                    }
                }
            }
        }
        /**
         * Requires that `cursor` be positioned on a node.
         * Restores the `cursor` to that same position before exiting.
         */
        function visitModify(modify, node) {
            if (modify.fields !== undefined) {
                visitFieldMarks(modify.fields, node);
            }
        }
        /**
         * Requires that `cursor` be positioned in the field where the deletion takes place.
         * Does not move `cursor`.
         */
        function onDelete(parent, key, startIndex, count) {
            for (let i = 0; i < count; ++i) {
                const fork = cursor.fork();
                const index = startIndex + i;
                fork.enterNode(index);
                const nodeData = chunkTree(castCursorToSynchronous(fork), defaultChunkPolicy);
                fork.free();
                const child = parent.getOrCreateChild(key, index, repairDataFactory);
                if (child.data === undefined) {
                    child.data = repairDataFactory();
                }
                if (child.data.node === undefined) {
                    child.data.node = new Map();
                }
                child.data.node.set(revision, nodeData);
            }
        }
        visitFieldMarks(delta, this.root);
        cursor.free();
    }
    getNodes(revision, path, field, index, count) {
        const parent = getDescendant(this.root, path);
        const sparseField = parent.children.get(field);
        assert(sparseField !== undefined, 0x47a /* No repair data found */);
        // TODO: should do more optimized search (ex: binary search).
        const sparseIndex = sparseField.findIndex((child) => child.parentIndex === index);
        assert(sparseIndex !== -1, 0x47b /* No repair data found */);
        assert(sparseField[sparseIndex + count - 1]?.parentIndex === index + count - 1, 0x47c /* No repair data found */);
        return sparseField.slice(sparseIndex, sparseIndex + count).map((node) => {
            const repair = node.data?.node?.get(revision);
            assert(repair !== undefined, 0x47d /* No repair data found */);
            const cursor = repair.cursor();
            // TODO: leverage TreeChunk's ability to represent a range of contiguous nodes
            assert(cursor.getFieldLength() === 1, 0x55b /* only one node should have been chunked */);
            cursor.firstNode();
            return cursor;
        });
    }
}
export class ForestRepairDataStoreProvider {
    constructor(forest, 
    // TODO: Its unclear what it means if this schema is modified, and why its separate from the one in the forest. Maybe this should be removed or replaced with SchemaData?
    storedSchema, intoDelta) {
        this.forest = forest;
        this.storedSchema = storedSchema;
        this.intoDelta = intoDelta;
    }
    freeze() {
        // TODO: ensure editing this clone of the schema does the right thing (probably error?).
        this.frozenForest = this.forest.clone(new InMemoryStoredSchemaRepository(this.storedSchema), new AnchorSet());
    }
    applyChange(change) {
        if (this.frozenForest === undefined) {
            applyDelta(this.intoDelta(change), this.forest);
        }
    }
    createRepairData() {
        const repairDataStore = this.frozenForest !== undefined
            ? new ForestRepairDataStore(this.frozenForest, this.intoDelta)
            : new ForestRepairDataStore(this.forest, this.intoDelta);
        this.frozenForest = undefined;
        return repairDataStore;
    }
    clone(forest) {
        // TODO: ensure editing this clone of the schema does the right thing (probably error?).
        const storedSchema = new InMemoryStoredSchemaRepository(this.storedSchema);
        return new ForestRepairDataStoreProvider(forest ?? this.forest.clone(storedSchema, new AnchorSet()), storedSchema, this.intoDelta);
    }
}
//# sourceMappingURL=forestRepairDataStore.js.map