"use strict";
/*!
 * 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 _CustomArrayNodeBase_instances, _CustomArrayNodeBase_mapTreesFromFieldData;
Object.defineProperty(exports, "__esModule", { value: true });
exports.arraySchema = exports.asIndex = exports.IterableTreeArrayContent = exports.TreeArrayNode = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../../core/index.js");
const schemaTypes_js_1 = require("../../schemaTypes.js");
const index_js_2 = require("../../core/index.js");
const unhydratedFlexTreeFromInsertable_js_1 = require("../../unhydratedFlexTreeFromInsertable.js");
const prepareForInsertion_js_1 = require("../../prepareForInsertion.js");
const treeNodeValid_js_1 = require("../../treeNodeValid.js");
const createContext_js_1 = require("../../createContext.js");
/**
 * A {@link TreeNode} which implements 'readonly T[]' and the array mutation APIs.
 * @public
 */
exports.TreeArrayNode = {
    /**
     * Wrap an iterable of items to inserted as consecutive items in a array.
     * @remarks
     * The object returned by this function can be inserted into a {@link (TreeArrayNode:interface)}.
     * Its contents will be inserted consecutively in the corresponding location in the array.
     * @example
     * ```ts
     * array.insertAtEnd(TreeArrayNode.spread(iterable))
     * ```
     */
    spread: (content) => create(content),
};
/**
 * Package internal construction API.
 * Use {@link (TreeArrayNode:variable).spread} to create an instance of this type instead.
 */
let create;
/**
 * Used to insert iterable content into a {@link (TreeArrayNode:interface)}.
 * Use {@link (TreeArrayNode:variable).spread} to create an instance of this type.
 * @sealed @public
 */
class IterableTreeArrayContent {
    constructor(content) {
        this.content = content;
    }
    /**
     * Iterates over content for nodes to insert.
     */
    [Symbol.iterator]() {
        return this.content[Symbol.iterator]();
    }
}
exports.IterableTreeArrayContent = IterableTreeArrayContent;
(() => {
    create = (content) => new IterableTreeArrayContent(content);
})();
/**
 * Given a array node proxy, returns its underlying LazySequence field.
 */
function getSequenceField(arrayNode) {
    return (0, index_js_2.getOrCreateInnerNode)(arrayNode).getBoxed(index_js_1.EmptyKey);
}
// For compatibility, we are initially implement 'readonly T[]' by applying the Array.prototype methods
// to the array node proxy.  Over time, we should replace these with efficient implementations on LazySequence
// to avoid re-entering the proxy as these methods access 'length' and the indexed properties.
//
// For brevity, the current implementation dynamically builds a property descriptor map from a list of
// Array functions we want to re-expose via the proxy.
const arrayPrototypeKeys = [
    "concat",
    "entries",
    "every",
    "filter",
    "find",
    "findIndex",
    "flat",
    "flatMap",
    "forEach",
    "includes",
    "indexOf",
    "join",
    "keys",
    "lastIndexOf",
    "map",
    "reduce",
    "reduceRight",
    "slice",
    "some",
    "toLocaleString",
    "toString",
    // "copyWithin",
    // "fill",
    // "length",
    // "pop",
    // "push",
    // "reverse",
    // "shift",
    // "sort",
    // "splice",
    // "unshift",
];
/**
 * {@link TreeNodeValid}, but modified to add members from Array.prototype named in {@link arrayPrototypeKeys}.
 * @privateRemarks
 * Since a lot of scratch types and values are involved with creating this,
 * it's generating using an immediately invoked function expression (IIFE).
 * This is a common JavaScript pattern for cases like this to avoid cluttering the scope.
 */
const TreeNodeWithArrayFeatures = (() => {
    /**
     * {@link TreeNodeValid}, but modified to add members from Array.prototype named in {@link arrayPrototypeKeys}.
     */
    class TreeNodeWithArrayFeaturesUntyped extends treeNodeValid_js_1.TreeNodeValid {
    }
    // Modify TreeNodeWithArrayFeaturesUntyped to add the members from Array.prototype
    arrayPrototypeKeys.forEach((key) => {
        Object.defineProperty(TreeNodeWithArrayFeaturesUntyped.prototype, key, {
            value: Array.prototype[key],
        });
    });
    return TreeNodeWithArrayFeaturesUntyped;
})();
/* eslint-enable @typescript-eslint/explicit-member-accessibility, @typescript-eslint/no-explicit-any */
/**
 * Attempts to coerce the given property key to an integer index property.
 * @param key - The property key to coerce.
 * @param exclusiveMax - This restricts the range in which the resulting index is allowed to be.
 * The coerced index of `key` must be less than `exclusiveMax` or else this function will return `undefined`.
 * This is useful for reading an array within the bounds of its length, e.g. `asIndex(key, array.length)`.
 */
function asIndex(key, exclusiveMax) {
    if (typeof key !== "string") {
        return undefined;
    }
    // TODO: It may be worth a '0' <= ch <= '9' check before calling 'Number' to quickly
    // reject 'length' as an index, or even parsing integers ourselves.
    const asNumber = Number(key);
    if (!Number.isInteger(asNumber)) {
        return undefined;
    }
    // Check that the original string is the same after converting to a number and back again.
    // This prevents keys like "5.0", "0x5", " 5" from coercing to 5, and keys like " " or "" from coercing to 0.
    const asString = String(asNumber);
    if (asString !== key) {
        return undefined;
    }
    // TODO: See 'matrix/range.ts' for fast integer coercing + range check.
    return 0 <= asNumber && asNumber < exclusiveMax ? asNumber : undefined;
}
exports.asIndex = asIndex;
/**
 * Create a proxy which implements the {@link TreeArrayNode} API.
 * @param allowAdditionalProperties - If true, setting of unexpected properties will be forwarded to the target object.
 * Otherwise setting of unexpected properties will error.
 * @param proxyTarget - Target object of the proxy. Must provide an own `length` value property
 * (which is not used but must exist for getOwnPropertyDescriptor invariants) and the array functionality from {@link arrayNodePrototype}.
 * Controls the prototype exposed by the produced proxy.
 * @param dispatchTarget - provides the functionally of the node, implementing all fields.
 */
function createArrayNodeProxy(allowAdditionalProperties, proxyTarget, dispatchTarget) {
    // To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an array literal in order
    // to pass 'Object.getPrototypeOf'.  It also satisfies 'Array.isArray' and 'Object.prototype.toString'
    // requirements without use of Array[Symbol.species], which is potentially on a path ot deprecation.
    const proxy = new Proxy(proxyTarget, {
        get: (target, key, receiver) => {
            const field = getSequenceField(receiver);
            const maybeIndex = asIndex(key, field.length);
            if (maybeIndex === undefined) {
                if (key === "length") {
                    return field.length;
                }
                // Pass the proxy as the receiver here, so that any methods on
                // the prototype receive `proxy` as `this`.
                return Reflect.get(dispatchTarget, key, receiver);
            }
            const maybeContent = field.at(maybeIndex);
            return maybeContent === undefined
                ? undefined
                : (0, index_js_2.getOrCreateNodeFromInnerUnboxedNode)(maybeContent);
        },
        set: (target, key, newValue, receiver) => {
            if (key === "length") {
                // To allow "length" to look like "length" on an array, getOwnPropertyDescriptor has to report it as a writable value.
                // This means the proxy target must provide a length value, but since it can't use getters and setters, it can't be correct.
                // Therefor length has to be handled in this proxy.
                // Since it's not actually mutable, return false so setting it will produce a type error.
                return false;
            }
            // 'Symbol.isConcatSpreadable' may be set on an Array instance to modify the behavior of
            // the concat method.  We allow this property to be added to the dispatch object.
            if (key === Symbol.isConcatSpreadable) {
                return Reflect.set(dispatchTarget, key, newValue, receiver);
            }
            // Array nodes treat all non-negative integer indexes as array access.
            // Using Infinity here (rather than length) ensures that indexing past the end doesn't create additional session local properties.
            const maybeIndex = asIndex(key, Number.POSITIVE_INFINITY);
            if (maybeIndex !== undefined) {
                // For MVP, we otherwise disallow setting properties (mutation is only available via the array node mutation APIs).
                // To ensure a clear and actionable error experience, we will throw explicitly here, rather than just returning false.
                throw new internal_2.UsageError("Cannot set indexed properties on array nodes. Use array node mutation APIs to alter the array.");
            }
            return allowAdditionalProperties ? Reflect.set(target, key, newValue, receiver) : false;
        },
        has: (target, key) => {
            const field = getSequenceField(proxy);
            const maybeIndex = asIndex(key, field.length);
            return maybeIndex !== undefined || Reflect.has(dispatchTarget, key);
        },
        ownKeys: (target) => {
            const field = getSequenceField(proxy);
            // TODO: Would a lazy iterator to produce the indexes work / be more efficient?
            // TODO: Need to surface 'Symbol.isConcatSpreadable' as an own key.
            const keys = Array.from({ length: field.length }, (_, index) => `${index}`);
            if (allowAdditionalProperties) {
                keys.push(...Reflect.ownKeys(target));
            }
            else {
                keys.push("length");
            }
            return keys;
        },
        getOwnPropertyDescriptor: (target, key) => {
            const field = getSequenceField(proxy);
            const maybeIndex = asIndex(key, field.length);
            if (maybeIndex !== undefined) {
                const val = field.at(maybeIndex);
                // To satisfy 'deepEquals' level scrutiny, the property descriptor for indexed properties must
                // be a simple value property (as opposed to using getter) and declared writable/enumerable/configurable.
                return {
                    value: val === undefined ? undefined : (0, index_js_2.getOrCreateNodeFromInnerUnboxedNode)(val),
                    writable: true, // For MVP, setting indexed properties is reported as allowed here (for deep equals compatibility noted above), but not actually supported.
                    enumerable: true,
                    configurable: true,
                };
            }
            else if (key === "length") {
                // To satisfy 'deepEquals' level scrutiny, the property descriptor for 'length' must be a simple
                // value property (as opposed to using getter) and be declared writable / non-configurable.
                return {
                    value: field.length,
                    writable: true,
                    enumerable: false,
                    configurable: false,
                };
            }
            return Reflect.getOwnPropertyDescriptor(dispatchTarget, key);
        },
        defineProperty(target, key, attributes) {
            const maybeIndex = asIndex(key, Number.POSITIVE_INFINITY);
            if (maybeIndex !== undefined) {
                throw new internal_2.UsageError("Shadowing of array indices is not permitted.");
            }
            return Reflect.defineProperty(dispatchTarget, key, attributes);
        },
    });
    return proxy;
}
class CustomArrayNodeBase extends TreeNodeWithArrayFeatures {
    constructor(input) {
        super(input ?? []);
        _CustomArrayNodeBase_instances.add(this);
    }
    toJSON() {
        // This override causes the class instance to `JSON.stringify` as `[a, b]` rather than `{0: a, 1: b}`.
        return Array.from(this);
    }
    // Instances of this class are used as the dispatch object for the proxy,
    // and thus its set of keys is used to implement `has` (for the `in` operator) for the non-numeric cases.
    // Therefore it must include `length`,
    // even though this "length" is never invoked (due to being shadowed by the proxy provided own property).
    get length() {
        return (0, internal_1.fail)(0xadb /* Proxy should intercept length */);
    }
    [(_CustomArrayNodeBase_instances = new WeakSet(), _CustomArrayNodeBase_mapTreesFromFieldData = function _CustomArrayNodeBase_mapTreesFromFieldData(value) {
        const sequenceField = getSequenceField(this);
        const content = value;
        const contentArray = content.flatMap((c) => c instanceof IterableTreeArrayContent ? Array.from(c) : [c]);
        const mapTrees = (0, prepareForInsertion_js_1.prepareArrayContentForInsertion)(contentArray, this.simpleSchema, sequenceField.context);
        return mapTrees;
    }, Symbol.iterator)]() {
        return this.values();
    }
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    get [Symbol.unscopables]() {
        // This might not be the exact right set of values, but it only matters for `with` clauses which are deprecated and are banned in strict mode, so it shouldn't matter much.
        // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with for details.
        return Array.prototype[Symbol.unscopables];
    }
    at(index) {
        const field = getSequenceField(this);
        const val = field.boxedAt(index);
        if (val === undefined) {
            return val;
        }
        return (0, index_js_2.getOrCreateNodeFromInnerNode)(val);
    }
    insertAt(index, ...value) {
        const field = getSequenceField(this);
        validateIndex(index, field, "insertAt", true);
        const content = __classPrivateFieldGet(this, _CustomArrayNodeBase_instances, "m", _CustomArrayNodeBase_mapTreesFromFieldData).call(this, value);
        field.editor.insert(index, content);
    }
    insertAtStart(...value) {
        this.insertAt(0, ...value);
    }
    insertAtEnd(...value) {
        this.insertAt(this.length, ...value);
    }
    removeAt(index) {
        const field = getSequenceField(this);
        validateIndex(index, field, "removeAt");
        field.editor.remove(index, 1);
    }
    removeRange(start, end) {
        const field = getSequenceField(this);
        const { length } = field;
        const removeStart = start ?? 0;
        const removeEnd = Math.min(length, end ?? length);
        validatePositiveIndex(removeStart);
        validatePositiveIndex(removeEnd);
        if (removeEnd < removeStart) {
            // This catches both the case where start is > array.length and when start is > end.
            throw new internal_2.UsageError('Too large of "start" value passed to TreeArrayNode.removeRange.');
        }
        field.editor.remove(removeStart, removeEnd - removeStart);
    }
    moveToStart(sourceIndex, source) {
        const sourceArray = source ?? this;
        const sourceField = getSequenceField(sourceArray);
        validateIndex(sourceIndex, sourceField, "moveToStart");
        this.moveRangeToIndex(0, sourceIndex, sourceIndex + 1, source);
    }
    moveToEnd(sourceIndex, source) {
        const sourceArray = source ?? this;
        const sourceField = getSequenceField(sourceArray);
        validateIndex(sourceIndex, sourceField, "moveToEnd");
        this.moveRangeToIndex(this.length, sourceIndex, sourceIndex + 1, source);
    }
    moveToIndex(destinationGap, sourceIndex, source) {
        const sourceArray = source ?? this;
        const sourceField = getSequenceField(sourceArray);
        const destinationField = getSequenceField(this);
        validateIndex(destinationGap, destinationField, "moveToIndex", true);
        validateIndex(sourceIndex, sourceField, "moveToIndex");
        this.moveRangeToIndex(destinationGap, sourceIndex, sourceIndex + 1, source);
    }
    moveRangeToStart(sourceStart, sourceEnd, source) {
        validateIndexRange(sourceStart, sourceEnd, source ?? getSequenceField(this), "moveRangeToStart");
        this.moveRangeToIndex(0, sourceStart, sourceEnd, source);
    }
    moveRangeToEnd(sourceStart, sourceEnd, source) {
        validateIndexRange(sourceStart, sourceEnd, source ?? getSequenceField(this), "moveRangeToEnd");
        this.moveRangeToIndex(this.length, sourceStart, sourceEnd, source);
    }
    moveRangeToIndex(destinationGap, sourceStart, sourceEnd, source) {
        const destinationField = getSequenceField(this);
        const destinationSchema = this.allowedTypes;
        const sourceField = source !== undefined ? getSequenceField(source) : destinationField;
        validateIndex(destinationGap, destinationField, "moveRangeToIndex", true);
        validateIndexRange(sourceStart, sourceEnd, source ?? destinationField, "moveRangeToIndex");
        // TODO: determine support for move across different sequence types
        if (sourceField !== destinationField) {
            for (let i = sourceStart; i < sourceEnd; i++) {
                const sourceNode = sourceField.boxedAt(i) ?? (0, internal_1.oob)();
                const sourceSchema = (0, index_js_2.getSimpleNodeSchemaFromInnerNode)(sourceNode);
                if (!destinationSchema.has(sourceSchema)) {
                    throw new internal_2.UsageError("Type in source sequence is not allowed in destination.");
                }
            }
        }
        const movedCount = sourceEnd - sourceStart;
        if (!destinationField.context.isHydrated()) {
            if (!(sourceField instanceof index_js_2.UnhydratedSequenceField)) {
                throw new internal_2.UsageError("Cannot move elements from a hydrated array to an unhydrated array.");
            }
            if (sourceField.context.isHydrated()) {
                throw new internal_2.UsageError("Cannot move elements from an unhydrated array to a hydrated array.");
            }
            if (sourceField !== destinationField || destinationGap < sourceStart) {
                destinationField.editor.insert(destinationGap, sourceField.editor.remove(sourceStart, movedCount));
            }
            else if (destinationGap > sourceStart + movedCount) {
                destinationField.editor.insert(destinationGap - movedCount, sourceField.editor.remove(sourceStart, movedCount));
            }
        }
        else {
            if (!sourceField.context.isHydrated()) {
                throw new internal_2.UsageError("Cannot move elements from an unhydrated array to a hydrated array.");
            }
            if (sourceField.context !== destinationField.context) {
                throw new internal_2.UsageError("Cannot move elements between two different TreeViews.");
            }
            destinationField.context.checkout.editor.move(sourceField.getFieldPath(), sourceStart, movedCount, destinationField.getFieldPath(), destinationGap);
        }
    }
    values() {
        return this.generateValues((0, index_js_2.getKernel)(this).generationNumber);
    }
    *generateValues(initialLastUpdatedStamp) {
        const kernel = (0, index_js_2.getKernel)(this);
        if (initialLastUpdatedStamp !== kernel.generationNumber) {
            throw new internal_2.UsageError(`Concurrent editing and iteration is not allowed.`);
        }
        for (let i = 0; i < this.length; i++) {
            yield this.at(i) ?? (0, internal_1.fail)(0xadc /* Index is out of bounds */);
            if (initialLastUpdatedStamp !== kernel.generationNumber) {
                throw new internal_2.UsageError(`Concurrent editing and iteration is not allowed.`);
            }
        }
    }
}
CustomArrayNodeBase.kind = index_js_2.NodeKind.Array;
/**
 * Define a {@link TreeNodeSchema} for a {@link (TreeArrayNode:interface)}.
 *
 * @param name - Unique identifier for this schema including the factory's scope.
 * @param persistedMetadata - Optional persisted metadata for the object node schema.
 */
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function arraySchema(identifier, info, implicitlyConstructable, customizable, metadata, persistedMetadata) {
    const unannotatedTypes = (0, schemaTypes_js_1.unannotateImplicitAllowedTypes)(info);
    const lazyChildTypes = new internal_1.Lazy(() => (0, schemaTypes_js_1.normalizeAllowedTypes)(unannotatedTypes));
    const lazyAnnotatedTypes = new internal_1.Lazy(() => [(0, schemaTypes_js_1.normalizeAnnotatedAllowedTypes)(info)]);
    const lazyAllowedTypesIdentifiers = new internal_1.Lazy(() => new Set([...lazyChildTypes.value].map((type) => type.identifier)));
    let unhydratedContext;
    // This class returns a proxy from its constructor to handle numeric indexing.
    // Alternatively it could extend a normal class which gets tons of numeric properties added.
    class Schema extends CustomArrayNodeBase {
        static prepareInstance(instance, flexNode) {
            const proxyTarget = customizable ? instance : [];
            if (customizable) {
                // Since proxy reports this as a "non-configurable" property, it must exist on the underlying object used as the proxy target, not as an inherited property.
                // This should not get used as the proxy should intercept all use.
                Object.defineProperty(instance, "length", {
                    value: Number.NaN,
                    writable: true,
                    enumerable: false,
                    configurable: false,
                });
            }
            return createArrayNodeProxy(customizable, proxyTarget, instance);
        }
        static buildRawNode(instance, input) {
            return (0, unhydratedFlexTreeFromInsertable_js_1.unhydratedFlexTreeFromInsertable)(input, this);
        }
        static get allowedTypesIdentifiers() {
            return lazyAllowedTypesIdentifiers.value;
        }
        static oneTimeSetup() {
            const schema = this;
            unhydratedContext = (0, createContext_js_1.getUnhydratedContext)(schema);
            // First run, do extra validation.
            // TODO: provide a way for TreeConfiguration to trigger this same validation to ensure it gets run early.
            // Scan for shadowing inherited members which won't work, but stop scan early to allow shadowing built in (which seems to work ok).
            {
                let prototype = this.prototype;
                // There isn't a clear cleaner way to author this loop.
                while (prototype !== Schema.prototype) {
                    // Search prototype keys and check for positive integers. Throw if any are found.
                    // Shadowing of index properties on array nodes is not supported.
                    for (const key of Object.getOwnPropertyNames(prototype)) {
                        const maybeIndex = asIndex(key, Number.POSITIVE_INFINITY);
                        if (maybeIndex !== undefined) {
                            throw new internal_2.UsageError(`Schema ${identifier} defines an inherited index property "${key.toString()}" which shadows a possible array index. Shadowing of array indices is not permitted.`);
                        }
                    }
                    // Since this stops at the array node base schema, it should never see a null prototype, so this case is safe.
                    // Additionally, if the prototype chain is ever messed up such that the array base schema is not in it,
                    // the null that would show up here does at least ensure this code throws instead of hanging.
                    prototype = Reflect.getPrototypeOf(prototype);
                }
            }
            return unhydratedContext;
        }
        static get childTypes() {
            return lazyChildTypes.value;
        }
        static get childAnnotatedAllowedTypes() {
            return lazyAnnotatedTypes.value;
        }
        // eslint-disable-next-line import/no-deprecated
        get [index_js_2.typeNameSymbol]() {
            return identifier;
        }
        get [index_js_2.typeSchemaSymbol]() {
            return Schema.constructorCached?.constructor;
        }
        get simpleSchema() {
            return unannotatedTypes;
        }
        get allowedTypes() {
            return lazyChildTypes.value;
        }
    }
    Schema.constructorCached = undefined;
    Schema.identifier = identifier;
    Schema.info = info;
    Schema.implicitlyConstructable = implicitlyConstructable;
    Schema.metadata = metadata ?? {};
    Schema.persistedMetadata = persistedMetadata;
    const output = Schema;
    return output;
}
exports.arraySchema = arraySchema;
function validateSafeInteger(index) {
    if (!Number.isSafeInteger(index)) {
        throw new internal_2.UsageError(`Expected a safe integer, got ${index}.`);
    }
}
function validatePositiveIndex(index) {
    validateSafeInteger(index);
    if (index < 0) {
        throw new internal_2.UsageError(`Expected non-negative index, got ${index}.`);
    }
}
function validateIndex(index, array, methodName, allowOnePastEnd = false) {
    validatePositiveIndex(index);
    if (allowOnePastEnd) {
        if (index > array.length) {
            throw new internal_2.UsageError(`Index value passed to TreeArrayNode.${methodName} is out of bounds.`);
        }
    }
    else {
        if (index >= array.length) {
            throw new internal_2.UsageError(`Index value passed to TreeArrayNode.${methodName} is out of bounds.`);
        }
    }
}
function validateIndexRange(startIndex, endIndex, array, methodName) {
    validateIndex(startIndex, array, methodName, true);
    validateIndex(endIndex, array, methodName, true);
    if (startIndex > endIndex || array.length < endIndex) {
        throw new internal_2.UsageError(`Index value passed to TreeArrayNode.${methodName} is out of bounds.`);
    }
}
//# sourceMappingURL=arrayNode.js.map