/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _UnhydratedFlexTreeNode_instances, _a, _UnhydratedFlexTreeNode_emitDeepChangesToAncestorArrays, _UnhydratedFlexTreeNode_emitSubtreeChangedEvents, _b, _c;
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, EmptyKey, } from "../../core/index.js";
import { FlexTreeEntityKind, flexTreeMarker, indexForAt, FieldKinds, cursorForMapTreeNode, cursorForMapTreeField, currentObserver, } from "../../feature-libraries/index.js";
import { brand, filterIterable, getOrCreate, mapIterable } from "../../util/index.js";
/**
 * Placeholder `DeltaDetachedNodeId` used as the attach/detach id in synthetic delta marks produced
 * by the unhydrated sequence-field editor. Only the *presence* of the id is checked by
 * {@link deltaMarksToArrayOps}, so the value itself is arbitrary.
 */
const syntheticDetachedNodeId = { minor: 0 };
/**
 * 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) {
        _UnhydratedFlexTreeNode_instances.add(this);
        this.data = data;
        this.fieldsAll = fieldsAll;
        this.simpleContext = simpleContext;
        this.location = unparentedLocation;
        this[_b] = 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]: () => {
                currentObserver?.observeNodeFields(this);
                return filterIterable(this.fieldsAll, ([, field]) => field.length > 0);
            },
        };
        for (const [_key, field] of this.fieldsAll) {
            field.parent = this;
        }
    }
    [(_UnhydratedFlexTreeNode_instances = new WeakSet(), _b = 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(this.location !== unparentedLocation, 0xa09 /* Node may not be un-adopted if it does not have a parent */);
            this.location = unparentedLocation;
        }
        else {
            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 _a, 0xb77 /* Unhydrated node's parent should be an unhydrated node */);
                unhydratedNode = parentNode;
            }
            this.location = { parent, index };
        }
    }
    /**
     * 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() {
        currentObserver?.observeParentOf(this);
        return this.location;
    }
    borrowCursor() {
        return cursorForMapTreeNode(this);
    }
    tryGetField(key) {
        currentObserver?.observeNodeField(this, 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);
        currentObserver?.observeNodeField(this, fieldKey);
        return this.getOrCreateField(fieldKey);
    }
    keys() {
        return Array.from(this.fields, ([key]) => key)[Symbol.iterator]();
    }
    get value() {
        return this.data.value;
    }
    /**
     * Emit a `childrenChangedAfterBatch` event for this node, then propagate deep-change
     * signals to ancestor array nodes and subtree-changed signals up the entire ancestor chain.
     * @param key - The field key that changed.
     * @param marks - Optional delta marks describing the change to the field. When provided, they
     * are included in the `fieldMarks` payload so that array-node listeners can build a delta.
     * When omitted (e.g. for non-sequence fields), `fieldMarks` is empty.
     */
    emitChangedEvent(key, marks) {
        this._events.emit("childrenChangedAfterBatch", {
            changedFields: new Set([key]),
            fieldMarks: marks === undefined ? new Map() : new Map([[key, marks]]),
        });
        // Emit subtree-changed events for this node and its non-array ancestors first,
        // so that node.treeChanged fires before any ancestor array.treeChanged.
        // Array ancestors and the nodes above them are handled by
        // #emitDeepChangesToAncestorArrays, which propagates subtree events above
        // each array boundary in the correct order.
        __classPrivateFieldGet(this, _UnhydratedFlexTreeNode_instances, "m", _UnhydratedFlexTreeNode_emitSubtreeChangedEvents).call(this);
        // Mirrors the onlyDeepChanges block in anchorSet.ts for unhydrated nodes.
        __classPrivateFieldGet(this, _UnhydratedFlexTreeNode_instances, "m", _UnhydratedFlexTreeNode_emitDeepChangesToAncestorArrays).call(this);
    }
}
_a = UnhydratedFlexTreeNode, _UnhydratedFlexTreeNode_emitDeepChangesToAncestorArrays = function _UnhydratedFlexTreeNode_emitDeepChangesToAncestorArrays() {
    const location = this.parentField;
    const parentField = location.parent;
    const parentNode = parentField.parent;
    if (parentNode === undefined || !(parentNode instanceof _a)) {
        return;
    }
    // Only emit on array ancestors (EmptyKey); object/map ancestors don't carry delta payloads.
    if (parentField.key === EmptyKey) {
        const index = location.index;
        const syntheticMarks = [];
        if (index > 0) {
            syntheticMarks.push({ count: index });
        }
        // `fields` presence (not content) signals a deep change to deltaMarksToArrayOps.
        syntheticMarks.push({ count: 1, fields: new Map([[EmptyKey, { marks: [] }]]) });
        parentNode._events.emit("childrenChangedAfterBatch", {
            changedFields: new Set([EmptyKey]),
            fieldMarks: new Map([[EmptyKey, syntheticMarks]]),
        });
        // Propagate subtree-changed events from the array upward so that
        // ancestors above this array receive treeChanged after the array itself.
        __classPrivateFieldGet(parentNode, _UnhydratedFlexTreeNode_instances, "m", _UnhydratedFlexTreeNode_emitSubtreeChangedEvents).call(parentNode);
    }
    __classPrivateFieldGet(parentNode, _UnhydratedFlexTreeNode_instances, "m", _UnhydratedFlexTreeNode_emitDeepChangesToAncestorArrays).call(parentNode);
}, _UnhydratedFlexTreeNode_emitSubtreeChangedEvents = function _UnhydratedFlexTreeNode_emitSubtreeChangedEvents() {
    this._events.emit("subtreeChangedAfterBatch");
    const parentField = this.parentField.parent;
    if (parentField.key === EmptyKey) {
        // This node is an array element; stop here so that array ancestor
        // events fire in the correct order relative to this node's treeChanged.
        return;
    }
    const parent = parentField.parent;
    assert(parent === undefined || parent instanceof _a, 0xb76 /* Unhydrated node's parent should be an unhydrated node */);
    if (parent !== undefined) {
        __classPrivateFieldGet(parent, _UnhydratedFlexTreeNode_instances, "m", _UnhydratedFlexTreeNode_emitSubtreeChangedEvents).call(parent);
    }
};
/**
 * 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[_c] = 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) ? undefined : this.lazyChildren;
    }
    /**
     * 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;
    }
    [(_c = 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, 
    /**
     * Delta marks describing this edit, forwarded to {@link UnhydratedFlexTreeNode.emitChangedEvent}.
     * Sequence-field subclasses pass pre-computed marks so that array-node listeners receive a
     * meaningful delta; other field kinds omit this parameter.
     */
    marks) {
        // 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, marks);
    }
    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.length = 0;
                    }
                    else {
                        mapTrees[0] = newContent;
                    }
                });
            },
        };
    }
    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;
                const insertCount = newContentChecked.length;
                const marks = [];
                if (index > 0)
                    marks.push({ count: index });
                marks.push({ count: insertCount, attach: syntheticDetachedNodeId });
                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), ...newContentChecked, ...mapTrees.slice(index)];
                    }
                }, marks);
            },
            remove: (index, count) => {
                for (let i = index; i < index + count; i++) {
                    const c = this.children[i];
                    assert(c !== undefined, 0xa0b /* Unexpected sparse array */);
                }
                const marks = [];
                if (index > 0)
                    marks.push({ count: index });
                marks.push({ count, detach: syntheticDetachedNodeId });
                let removed;
                this.edit((mapTrees) => {
                    removed = mapTrees.splice(index, count);
                }, marks);
                return removed ?? fail(0xb4a /* Expected removed to be set by edit */);
            },
            move: (sourceIndex, count, destIndex, source) => {
                const sourceField = source ?? this;
                if (sourceField === this) {
                    // Within-field move: do both operations in a single edit to emit only one event
                    const marks = buildUnhydratedMoveMarks(sourceIndex, count, destIndex);
                    this.edit((mapTrees) => {
                        const removed = mapTrees.splice(sourceIndex, count);
                        // Adjust destination index if it comes after the source
                        const adjustedDest = destIndex > sourceIndex ? destIndex - count : destIndex;
                        if (removed.length < 1000) {
                            // For "smallish arrays" (`1000` is not empirically derived), the `splice` function is appropriate...
                            mapTrees.splice(adjustedDest, 0, ...removed);
                        }
                        else {
                            // ...but we avoid using `splice` + spread for very large arrays since there is a limit on how many elements can be spread (too many will overflow the stack).
                            return [
                                ...mapTrees.slice(0, adjustedDest),
                                ...removed,
                                ...mapTrees.slice(adjustedDest),
                            ];
                        }
                    }, marks);
                }
                else {
                    // Cross-field move: remove from source, insert into destination
                    // Each field emits one event (correct behavior for different fields)
                    const removed = sourceField.editor.remove(sourceIndex, count);
                    this.editor.insert(destIndex, removed);
                }
            },
        };
    }
    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
/**
 * Builds {@link DeltaMark}s describing a within-field move for use in
 * {@link UnhydratedFlexTreeNode.emitChangedEvent}.
 *
 * @remarks
 * Forward move (`sourceIndex < destIndex`):
 * `[retain(src), detach(n), retain(mid), attach(n)]`
 *
 * Backward move (`destIndex < sourceIndex`):
 * `[retain(dst), attach(n), retain(mid), detach(n)]`
 *
 * A no-op move (`sourceIndex === destIndex`) returns an empty array; the event
 * should not fire in that case, but the empty marks are harmless if it does.
 */
function buildUnhydratedMoveMarks(sourceIndex, count, destIndex) {
    const marks = [];
    if (sourceIndex < destIndex) {
        if (sourceIndex > 0)
            marks.push({ count: sourceIndex });
        marks.push({ count, detach: syntheticDetachedNodeId });
        const between = destIndex - sourceIndex - count;
        if (between > 0)
            marks.push({ count: between });
        marks.push({ count, attach: syntheticDetachedNodeId });
    }
    else if (destIndex < sourceIndex) {
        if (destIndex > 0)
            marks.push({ count: destIndex });
        marks.push({ count, attach: syntheticDetachedNodeId });
        const between = sourceIndex - destIndex;
        if (between > 0)
            marks.push({ count: between });
        marks.push({ count, detach: syntheticDetachedNodeId });
    }
    return marks;
}
/** 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