/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/common-utils";
import { AnchorSet, castCursorToSynchronous, Delta, EmptyKey, getDescendant, keyAsDetachedField, moveToDetachedField, SparseNode, } from "../core";
import { chunkTree, defaultChunkPolicy } from "./chunked-forest";
const repairDataFactory = () => ({});
const undefinedFactory = () => undefined;
export class ForestRepairDataStore {
    constructor(forestProvider) {
        this.forestProvider = forestProvider;
        this.root = new SparseNode(EmptyKey, 0, undefined, undefined);
    }
    capture(change, revision) {
        const forest = this.forestProvider(revision);
        const cursor = forest.allocateCursor();
        const visitFieldMarks = (fields, parent) => {
            for (const [key, field] of fields) {
                if (parent !== this.root) {
                    cursor.enterField(key);
                }
                else {
                    moveToDetachedField(forest, cursor, keyAsDetachedField(key));
                }
                visitField(field, parent, key);
                if (parent !== this.root) {
                    cursor.exitField();
                }
            }
        };
        function visitField(delta, parent, key) {
            let index = 0;
            for (const mark of delta) {
                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: {
                            const child = parent.getOrCreateChild(key, index, repairDataFactory);
                            visitModify(mark, child);
                            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);
                    }
                }
            }
        }
        function visitModify(modify, node) {
            // Note that the check below returns true for properties that are present on the object even if they
            // are set to `undefined`. This is leveraged here to represent the fact that the value should be set to
            // `undefined` as opposed to leaving the value unchanged.
            if (Object.prototype.hasOwnProperty.call(modify, "setValue")) {
                if (node.data === undefined) {
                    node.data = repairDataFactory();
                }
                const value = cursor.value;
                if (node.data.value === undefined) {
                    node.data.value = new Map();
                }
                node.data.value.set(revision, value);
            }
            if (modify.fields !== undefined) {
                visitFieldMarks(modify.fields, node);
            }
        }
        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(change, this.root);
        cursor.free();
    }
    getNodes(revision, path, field, index, count) {
        var _a;
        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(((_a = sparseField[sparseIndex + count - 1]) === null || _a === void 0 ? void 0 : _a.parentIndex) === index + count - 1, 0x47c /* No repair data found */);
        return sparseField.slice(sparseIndex, sparseIndex + count).map((node) => {
            var _a, _b;
            const repair = (_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.node) === null || _b === void 0 ? void 0 : _b.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;
        });
    }
    getValue(revision, path) {
        const data = getDescendant(this.root, path).data;
        const valueMap = data === null || data === void 0 ? void 0 : data.value;
        assert((valueMap === null || valueMap === void 0 ? void 0 : valueMap.has(revision)) === true, 0x47e /* No repair data found */);
        return valueMap.get(revision);
    }
}
export function repairDataStoreFromForest(forest) {
    return new ForestRepairDataStore(() => forest);
}
export class ForestRepairDataStoreProvider {
    constructor(forest, storedSchema) {
        this.forest = forest;
        this.storedSchema = storedSchema;
    }
    freeze() {
        this.frozenForest = this.forest.clone(this.storedSchema.clone(), new AnchorSet());
    }
    applyDelta(change) {
        if (this.frozenForest === undefined) {
            this.forest.applyDelta(change);
        }
    }
    createRepairData() {
        const repairDataStore = this.frozenForest !== undefined
            ? repairDataStoreFromForest(this.frozenForest)
            : repairDataStoreFromForest(this.forest);
        this.frozenForest = undefined;
        return repairDataStore;
    }
    clone(forest) {
        const storedSchema = this.storedSchema.clone();
        return new ForestRepairDataStoreProvider(forest !== null && forest !== void 0 ? forest : this.forest.clone(storedSchema, new AnchorSet()), storedSchema);
    }
}
//# sourceMappingURL=forestRepairDataStore.js.map