/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var _a, _b;
import { createEmitter } from "@fluid-internal/client-utils";
import { assert, oob, fail } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { dummyRoot, } from "../../core/index.js";
import { FlexTreeEntityKind, flexTreeMarker, indexForAt, FieldKinds, cursorForMapTreeNode, cursorForMapTreeField, } from "../../feature-libraries/index.js";
import { brand, filterIterable, getOrCreate, mapIterable } from "../../util/index.js";
/**
 * The {@link Unhydrated} implementation of {@link FlexTreeNode}.
 */
export class UnhydratedFlexTreeNode {
    isHydrated() {
        return false;
    }
    get storedSchema() {
        return (this.context.schema.nodeSchema.get(this.data.type) ?? fail(0xb46 /* missing schema */));
    }
    get events() {
        return this._events;
    }
    get context() {
        return this.simpleContext.flexContext;
    }
    /**
     * Create a new UnhydratedFlexTreeNode.
     */
    constructor(
    /**
     * The {@link NodeData} for this node.
     */
    data, 
    /**
     * All {@link UnhydratedFlexTreeField} for this node that have been created so far.
     * @remarks
     * This includes all non-empty fields, but also any empty fields which have been previously requested.
     */
    fieldsAll, 
    /**
     * The {@link Context} for this node.
     * @remarks
     * Provides access to all schema reachable from this node.
     * See {@link getUnhydratedContext}.
     */
    simpleContext) {
        this.data = data;
        this.fieldsAll = fieldsAll;
        this.simpleContext = simpleContext;
        this.location = unparentedLocation;
        this[_a] = FlexTreeEntityKind.Node;
        this._events = createEmitter();
        /**
         * The non-empty fields on this node.
         * @remarks
         * This is needed to implement {@link MapTreeNodeViewGeneric.fields}, which must omit empty fields.
         * Due to having to detect if a field is empty, this forces the evaluation of any pending defaults in the fields.
         * Use {@link allFieldsLazy} to avoid evaluating pending defaults.
         */
        this.fields = {
            get: (key) => this.tryGetField(key),
            [Symbol.iterator]: () => filterIterable(this.fieldsAll, ([, field]) => field.length > 0),
        };
        for (const [_key, field] of this.fieldsAll) {
            field.parent = this;
        }
    }
    [(_a = flexTreeMarker, Symbol.iterator)]() {
        return mapIterable(this.fields, ([, field]) => field)[Symbol.iterator]();
    }
    /**
     * Gets all fields, without filtering out empty ones.
     * @remarks
     * This avoids forcing the evaluating of pending defaults in the fields, and also saves a copy on access.
     */
    get allFieldsLazy() {
        return this.fieldsAll;
    }
    get type() {
        return this.data.type;
    }
    getOrCreateField(key) {
        return getOrCreate(this.fieldsAll, key, () => {
            const stored = this.storedSchema.getFieldSchema(key).kind;
            const field = createField(this.context, stored, key, []);
            field.parent = this;
            return field;
        });
    }
    adoptBy(parent, index) {
        if (parent !== undefined) {
            assert(index !== undefined, 0xa08 /* Expected index */);
            if (this.location !== unparentedLocation) {
                throw new UsageError("A node may not be in more than one place in the tree");
            }
            let unhydratedNode = parent.parent;
            while (unhydratedNode !== undefined) {
                if (unhydratedNode === this) {
                    throw new UsageError("A node may not be inserted into a location that is under itself");
                }
                const parentNode = unhydratedNode.parentField.parent.parent;
                assert(parentNode === undefined || parentNode instanceof UnhydratedFlexTreeNode, 0xb77 /* Unhydrated node's parent should be an unhydrated node */);
                unhydratedNode = parentNode;
            }
            this.location = { parent, index };
        }
        else {
            assert(this.location !== unparentedLocation, 0xa09 /* Node may not be un-adopted if it does not have a parent */);
            this.location = unparentedLocation;
        }
    }
    /**
     * The field this tree is in, and the index within that field.
     * @remarks If this node is unparented, this method will return the special {@link unparentedLocation} as the parent.
     */
    get parentField() {
        return this.location;
    }
    borrowCursor() {
        return cursorForMapTreeNode(this);
    }
    tryGetField(key) {
        const field = this.fieldsAll.get(key);
        // Only return the field if it is not empty, in order to fulfill the contract of `tryGetField`.
        if (field !== undefined && field.length > 0) {
            return field;
        }
    }
    getBoxed(key) {
        const fieldKey = brand(key);
        return this.getOrCreateField(fieldKey);
    }
    keys() {
        return Array.from(this.fields, ([key]) => key)[Symbol.iterator]();
    }
    get value() {
        return this.data.value;
    }
    emitChangedEvent(key) {
        this._events.emit("childrenChangedAfterBatch", { changedFields: new Set([key]) });
    }
}
/**
 * Implementation of `FlexTreeContext`.
 *
 * @remarks An editor is required to edit the FlexTree.
 */
export class UnhydratedContext {
    /**
     * @param flexSchema - Schema to use when working with the tree.
     */
    constructor(schemaPolicy, schema) {
        this.schemaPolicy = schemaPolicy;
        this.schema = schema;
    }
    isDisposed() {
        return false;
    }
    isHydrated() {
        return false;
    }
}
// #region Fields
/**
 * A special singleton that is the implicit {@link LocationInField} of all un-parented {@link UnhydratedFlexTreeNode}s.
 * @remarks This exists because {@link UnhydratedFlexTreeNode.parentField} must return a field.
 * If a {@link UnhydratedFlexTreeNode} is created without a parent, its {@link UnhydratedFlexTreeNode.parentField} property will point to this object.
 * However, this field cannot be used in any practical way because it is empty, i.e. it does not actually contain the children that claim to be parented under it.
 * It has the "empty" schema and it will always contain zero children if queried.
 * Any nodes with this location will have a dummy parent index of `-1`.
 *
 * TODO: make this make sense.
 */
const unparentedLocation = {
    parent: {
        key: dummyRoot,
        parent: undefined,
        schema: brand(FieldKinds.optional.identifier),
    },
    index: 0,
};
/**
 * The {@link Unhydrated} implementation of {@link FlexTreeField}.
 */
export class UnhydratedFlexTreeField {
    constructor(context, schema, key, 
    /**
     * The children of this field.
     * @remarks
     * This is either an array of {@link UnhydratedFlexTreeNode}s or a {@link ContextualFieldProvider} that will be used to populate the children lazily (after which it will become an array).
     * See {@link fillPendingDefaults}.
     * Note that any fields using a {@link ConstantFieldProvider} should be evaluated before constructing the UnhydratedFlexTreeField.
     */
    lazyChildren) {
        this.context = context;
        this.schema = schema;
        this.key = key;
        this.lazyChildren = lazyChildren;
        this[_b] = FlexTreeEntityKind.Field;
        this.parent = undefined;
        // When this field is created (which only happens one time, because it is cached), all the children become parented for the first time.
        // "Adopt" each child by updating its parent information to point to this field.
        if (Array.isArray(lazyChildren)) {
            for (const [i, child] of lazyChildren.entries()) {
                child.adoptBy(this, i);
            }
        }
    }
    borrowCursor() {
        return cursorForMapTreeField(this.children);
    }
    getPendingDefault() {
        return !Array.isArray(this.lazyChildren) ? this.lazyChildren : undefined;
    }
    /**
     * Populate pending default (if present) using the provided context.
     * @remarks
     * This apply to just this field: caller will likely want to recursively walk the tree.
     * @see {@link pendingDefault}.
     */
    fillPendingDefaults(context) {
        const provider = this.getPendingDefault();
        if (provider) {
            const content = provider(context);
            this.lazyChildren = content;
        }
    }
    /**
     * Returns true if this field has a pending default due to defined defined using a {@link ContextualFieldProvider}.
     */
    get pendingDefault() {
        return this.getPendingDefault() !== undefined;
    }
    get children() {
        const provider = this.getPendingDefault();
        if (provider) {
            const content = provider("UseGlobalContext");
            this.lazyChildren = content;
        }
        return this.lazyChildren;
    }
    get length() {
        return this.children.length;
    }
    is(kind) {
        return this.schema === kind.identifier;
    }
    boxedAt(index) {
        const i = indexForAt(index, this.length);
        if (i === undefined) {
            return undefined;
        }
        const m = this.children[i];
        return m;
    }
    [(_b = flexTreeMarker, Symbol.iterator)]() {
        return this.children[Symbol.iterator]();
    }
    /**
     * Mutate this field.
     * @param edit - A function which receives the current `MapTree`s that comprise the contents of the field so that it may be mutated.
     * The function may mutate the array in place or return a new array.
     * If a new array is returned then it will be used as the new contents of the field, otherwise the original array will be continue to be used.
     * @remarks All edits to the field (i.e. mutations of the field's MapTrees) should be directed through this function.
     * This function ensures that the parent MapTree has no empty fields (which is an invariant of `MapTree`) after the mutation.
     */
    edit(edit) {
        // Clear parents for all old map trees.
        for (const tree of this.children) {
            tree.adoptBy(undefined);
        }
        this.lazyChildren = edit(this.children) ?? this.children;
        // Set parents for all new map trees.
        for (const [index, tree] of this.children.entries()) {
            tree.adoptBy(this, index);
        }
        this.parent?.emitChangedEvent(this.key);
    }
    getFieldPath() {
        throw unsupportedUsageError("Editing an array");
    }
    /** Unboxes leaf nodes to their values */
    unboxed(index) {
        const child = this.children[index] ?? oob();
        const value = child.value;
        if (value !== undefined) {
            return value;
        }
        return child;
    }
}
/**
 * The {@link Unhydrated} implementation of {@link FlexTreeOptionalField}.
 */
export class UnhydratedOptionalField extends UnhydratedFlexTreeField {
    constructor() {
        super(...arguments);
        this.editor = {
            set: (newContent) => {
                if (newContent !== undefined) {
                    assert(newContent instanceof UnhydratedFlexTreeNode, 0xbb7 /* Expected unhydrated node */);
                }
                this.edit((mapTrees) => {
                    if (newContent !== undefined) {
                        mapTrees[0] = newContent;
                    }
                    else {
                        mapTrees.length = 0;
                    }
                });
            },
        };
    }
    get content() {
        const value = this.children[0];
        if (value !== undefined) {
            return this.unboxed(0);
        }
        return undefined;
    }
}
class UnhydratedRequiredField extends UnhydratedOptionalField {
    get content() {
        // This cannot use ?? since null is a legal value here.
        assert(super.content !== undefined, 0xa57 /* Expected EagerMapTree required field to have a value */);
        return super.content;
    }
}
/**
 * The {@link Unhydrated} implementation of {@link FlexTreeSequenceField}.
 */
export class UnhydratedSequenceField extends UnhydratedFlexTreeField {
    constructor() {
        super(...arguments);
        this.editor = {
            insert: (index, newContent) => {
                for (const c of newContent) {
                    assert(c !== undefined, 0xa0a /* Unexpected sparse array content */);
                    assert(c instanceof UnhydratedFlexTreeNode, 0xbb8 /* Expected unhydrated node */);
                }
                const newContentChecked = newContent;
                this.edit((mapTrees) => {
                    if (newContent.length < 1000) {
                        // For "smallish arrays" (`1000` is not empirically derived), the `splice` function is appropriate...
                        mapTrees.splice(index, 0, ...newContentChecked);
                    }
                    else {
                        // ...but we avoid using `splice` + spread for very large input arrays since there is a limit on how many elements can be spread (too many will overflow the stack).
                        return mapTrees.slice(0, index).concat(newContentChecked, mapTrees.slice(index));
                    }
                });
            },
            remove: (index, count) => {
                for (let i = index; i < index + count; i++) {
                    const c = this.children[i];
                    assert(c !== undefined, 0xa0b /* Unexpected sparse array */);
                }
                let removed;
                this.edit((mapTrees) => {
                    removed = mapTrees.splice(index, count);
                });
                return removed ?? fail(0xb4a /* Expected removed to be set by edit */);
            },
        };
    }
    at(index) {
        const i = indexForAt(index, this.length);
        if (i === undefined) {
            return undefined;
        }
        return this.unboxed(i);
    }
    map(callbackfn) {
        return Array.from(this, callbackfn);
    }
}
// #endregion Fields
/** Creates a field with the given attributes */
export function createField(...args) {
    switch (args[1]) {
        case FieldKinds.required.identifier:
        case FieldKinds.identifier.identifier:
            return new UnhydratedRequiredField(...args);
        case FieldKinds.optional.identifier:
            return new UnhydratedOptionalField(...args);
        case FieldKinds.sequence.identifier:
            return new UnhydratedSequenceField(...args);
        case FieldKinds.forbidden.identifier:
            // TODO: this seems to used by unknown optional fields. They should probably use "optional" not "Forbidden" schema.
            return new UnhydratedFlexTreeField(...args);
        default:
            return fail(0xb9d /* unsupported field kind */);
    }
}
export function unsupportedUsageError(message) {
    return new UsageError(`${message ?? "Operation"} is not supported for content that has not yet been inserted into the tree`);
}
//# sourceMappingURL=unhydratedFlexTree.js.map