/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { EmptyKey } from "../core/index.js";
import { FlexFieldNodeSchema, isFlexTreeNode, } from "../feature-libraries/index.js";
import { getOrCreateNodeProxy, markContentType, prepareContentForInsert, } from "./proxies.js";
import { getFlexNode, setFlexNode } from "./proxyBinding.js";
import { getSimpleFieldSchema, getSimpleNodeSchema } from "./schemaCaching.js";
import { NodeKind, } from "./schemaTypes.js";
import { cursorFromFieldData } from "./toMapTree.js";
import { fail } from "../util/index.js";
import { getFlexSchema } from "./toFlexSchema.js";
import { RawTreeNode, rawError } from "./rawNode.js";
/**
 * A {@link TreeNode} which implements 'readonly T[]' and the array mutation APIs.
 * @public
 */
export const 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.
 * @public
 */
export class IterableTreeArrayContent {
    constructor(content) {
        this.content = content;
    }
    /**
     * Iterates over content for nodes to insert.
     */
    [Symbol.iterator]() {
        return this.content[Symbol.iterator]();
    }
}
(() => {
    create = (content) => new IterableTreeArrayContent(content);
})();
/**
 * Given a array node proxy, returns its underlying LazySequence field.
 */
function getSequenceField(arrayNode) {
    return getFlexNode(arrayNode).getBoxed(EmptyKey);
}
// Used by 'insert*()' APIs to converts new content (expressed as a proxy union) to contextually
// typed data prior to forwarding to 'LazySequence.insert*()'.
function contextualizeInsertedArrayContent(content, sequenceField) {
    return prepareContentForInsert(content.flatMap((c) => c instanceof IterableTreeArrayContent ? Array.from(c) : [c]), sequenceField.context.forest);
}
// #region Create dispatch map for array nodes
// TODO: Experiment with alternative dispatch methods to see if we can improve performance.
/**
 * PropertyDescriptorMap used to build the prototype for our array node dispatch object.
 */
const arrayNodePrototypeProperties = {
    // We manually add [Symbol.iterator] to the dispatch map rather than use '[fn.name] = fn' as
    // below when adding 'Array.prototype.*' properties to this map because 'Array.prototype[Symbol.iterator].name'
    // returns "values" (i.e., Symbol.iterator is an alias for the '.values()' function.)
    [Symbol.iterator]: {
        value: Array.prototype[Symbol.iterator],
    },
    at: {
        value(index) {
            const field = getSequenceField(this);
            const val = field.boxedAt(index);
            if (val === undefined) {
                return val;
            }
            return getOrCreateNodeProxy(val);
        },
    },
    insertAt: {
        value(index, ...value) {
            const sequenceNode = getFlexNode(this);
            const sequenceField = getSequenceField(this);
            const content = contextualizeInsertedArrayContent(value, sequenceField);
            const simpleNodeSchema = getSimpleNodeSchema(sequenceNode.schema);
            assert(simpleNodeSchema.kind === NodeKind.Array, 0x912 /* Expected array schema */);
            const simpleFieldSchema = getSimpleFieldSchema(sequenceField.schema, simpleNodeSchema.info);
            sequenceField.insertAt(index, cursorFromFieldData(content, simpleFieldSchema));
        },
    },
    insertAtStart: {
        value(...value) {
            const sequenceNode = getFlexNode(this);
            const sequenceField = getSequenceField(this);
            const content = contextualizeInsertedArrayContent(value, sequenceField);
            const simpleNodeSchema = getSimpleNodeSchema(sequenceNode.schema);
            assert(simpleNodeSchema.kind === NodeKind.Array, 0x913 /* Expected array schema */);
            const simpleFieldSchema = getSimpleFieldSchema(sequenceField.schema, simpleNodeSchema.info);
            sequenceField.insertAtStart(cursorFromFieldData(content, simpleFieldSchema));
        },
    },
    insertAtEnd: {
        value(...value) {
            const sequenceNode = getFlexNode(this);
            const sequenceField = getSequenceField(this);
            const content = contextualizeInsertedArrayContent(value, sequenceField);
            const simpleNodeSchema = getSimpleNodeSchema(sequenceNode.schema);
            assert(simpleNodeSchema.kind === NodeKind.Array, 0x914 /* Expected array schema */);
            const simpleFieldSchema = getSimpleFieldSchema(sequenceField.schema, simpleNodeSchema.info);
            sequenceField.insertAtEnd(cursorFromFieldData(content, simpleFieldSchema));
        },
    },
    removeAt: {
        value(index) {
            getSequenceField(this).removeAt(index);
        },
    },
    removeRange: {
        value(start, end) {
            getSequenceField(this).removeRange(start, end);
        },
    },
    moveToStart: {
        value(sourceIndex, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToStart(sourceIndex, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToStart(sourceIndex);
            }
        },
    },
    moveToEnd: {
        value(sourceIndex, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToEnd(sourceIndex, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToEnd(sourceIndex);
            }
        },
    },
    moveToIndex: {
        value(index, sourceIndex, source) {
            if (source !== undefined) {
                getSequenceField(this).moveToIndex(index, sourceIndex, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveToIndex(index, sourceIndex);
            }
        },
    },
    moveRangeToStart: {
        value(sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveRangeToStart(sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveRangeToStart(sourceStart, sourceEnd);
            }
        },
    },
    moveRangeToEnd: {
        value(sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveRangeToEnd(sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveRangeToEnd(sourceStart, sourceEnd);
            }
        },
    },
    moveRangeToIndex: {
        value(index, sourceStart, sourceEnd, source) {
            if (source !== undefined) {
                getSequenceField(this).moveRangeToIndex(index, sourceStart, sourceEnd, getSequenceField(source));
            }
            else {
                getSequenceField(this).moveRangeToIndex(index, sourceStart, sourceEnd);
            }
        },
    },
};
/* eslint-disable @typescript-eslint/unbound-method */
// 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.
// TODO: This assumes 'Function.name' matches the property name on 'Array.prototype', which may be
// dubious across JS engines.
[
    Array.prototype.concat,
    // Array.prototype.copyWithin,
    Array.prototype.entries,
    Array.prototype.every,
    // Array.prototype.fill,
    Array.prototype.filter,
    Array.prototype.find,
    Array.prototype.findIndex,
    Array.prototype.flat,
    Array.prototype.flatMap,
    Array.prototype.forEach,
    Array.prototype.includes,
    Array.prototype.indexOf,
    Array.prototype.join,
    Array.prototype.keys,
    Array.prototype.lastIndexOf,
    // Array.prototype.length,
    Array.prototype.map,
    // Array.prototype.pop,
    // Array.prototype.push,
    Array.prototype.reduce,
    Array.prototype.reduceRight,
    // Array.prototype.reverse,
    // Array.prototype.shift,
    Array.prototype.slice,
    Array.prototype.some,
    // Array.prototype.sort,
    // Array.prototype.splice,
    Array.prototype.toLocaleString,
    Array.prototype.toString,
    // Array.prototype.unshift,
    Array.prototype.values,
].forEach((fn) => {
    arrayNodePrototypeProperties[fn.name] = { value: fn };
});
/* eslint-enable @typescript-eslint/unbound-method */
const arrayNodePrototype = Object.create(Object.prototype, arrayNodePrototypeProperties);
// #endregion
/**
 * Helper to coerce property keys to integer indexes (or undefined if not an in-range integer).
 */
function asIndex(key, length) {
    if (typeof key === "string") {
        // 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);
        // TODO: See 'matrix/range.ts' for fast integer coercing + range check.
        if (Number.isInteger(asNumber)) {
            return 0 <= asNumber && asNumber < length ? asNumber : undefined;
        }
    }
}
/**
 * @param allowAdditionalProperties - If true, setting of unexpected properties will be forwarded to the target object.
 * Otherwise setting of unexpected properties will error.
 * @param customTargetObject - Target object of the proxy.
 * If not provided `[]` is used for the target and a separate object created to dispatch array methods.
 * If provided, the customTargetObject will be used as both the dispatch object and the proxy target, and therefor must provide an own `length` value property
 * (which is not used but must exist for getOwnPropertyDescriptor invariants) and the array functionality from {@link arrayNodePrototype}.
 */
function createArrayNodeProxy(allowAdditionalProperties, customTargetObject) {
    const targetObject = customTargetObject ?? [];
    // Create a 'dispatch' object that this Proxy forwards to instead of the proxy target, because we need
    // the proxy target to be a plain JS array (see comments below when we instantiate the Proxy).
    // Own properties on the dispatch object are surfaced as own properties of the proxy.
    // (e.g., 'length', which is defined below).
    //
    // Properties normally inherited from 'Array.prototype' are surfaced via the prototype chain.
    const dispatch = customTargetObject ??
        Object.create(arrayNodePrototype, {
            // This dispatch object's set of keys is used to implement `has` (for the `in` operator) for the non-numeric cases, and therefor must include `length`.
            length: {
                get() {
                    fail("Proxy should intercept length");
                },
                set() {
                    fail("Proxy should intercept length");
                },
                enumerable: false,
                configurable: false,
            },
        });
    // 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(targetObject, {
        get: (target, key) => {
            const field = getSequenceField(proxy);
            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(dispatch, key, proxy);
            }
            const value = field.boxedAt(maybeIndex);
            if (value === undefined) {
                return undefined;
            }
            // TODO: Ideally, we would return leaves without first boxing them.  However, this is not
            //       as simple as calling '.content' since this skips the node and returns the FieldNode's
            //       inner field.
            return getOrCreateNodeProxy(value);
        },
        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 its 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(dispatch, key, newValue, proxy);
            }
            const field = getSequenceField(proxy);
            const maybeIndex = asIndex(key, field.length);
            if (maybeIndex !== undefined) {
                // For MVP, we otherwise disallow setting properties (mutation is only available via the array node mutation APIs).
                return false;
            }
            return allowAdditionalProperties ? Reflect.set(target, key, newValue) : false;
        },
        has: (target, key) => {
            const field = getSequenceField(proxy);
            const maybeIndex = asIndex(key, field.length);
            return maybeIndex !== undefined || Reflect.has(dispatch, 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.boxedAt(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 {
                    // TODO: Ideally, we would return leaves without first boxing them.  However, this is not
                    //       as simple as calling '.at' since this skips the node and returns the FieldNode's
                    //       inner field.
                    value: val === undefined ? val : getOrCreateNodeProxy(val),
                    writable: true,
                    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(dispatch, key);
        },
    });
    return proxy;
}
/**
 * Define a {@link TreeNodeSchema} for a {@link (TreeArrayNode:interface)}.
 *
 * @param name - Unique identifier for this schema including the factory's scope.
 */
export function arraySchema(base, customizable) {
    // 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 base {
        constructor(input) {
            super(input);
            const proxyTarget = customizable ? this : undefined;
            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(this, "length", {
                    value: NaN,
                    writable: true,
                    enumerable: false,
                    configurable: false,
                });
            }
            const flexSchema = getFlexSchema(this.constructor);
            assert(flexSchema instanceof FlexFieldNodeSchema, 0x915 /* invalid flex schema */);
            const flexNode = isFlexTreeNode(input)
                ? input
                : new RawFieldNode(flexSchema, copyContent(flexSchema.name, input));
            const proxy = createArrayNodeProxy(customizable, proxyTarget);
            setFlexNode(proxy, flexNode);
            return proxy;
        }
    }
    // Setup array functionality
    Object.defineProperties(schema.prototype, arrayNodePrototypeProperties);
    return schema;
}
/**
 * The implementation of a field node created by {@link createRawNode}.
 */
class RawFieldNode extends RawTreeNode {
    get content() {
        throw rawError("Reading content of an array node");
    }
    get boxedContent() {
        throw rawError("Reading boxed content of an array node");
    }
}
function copyContent(typeName, content) {
    const copy = Array.from(content);
    markContentType(typeName, copy);
    return copy;
}
//# sourceMappingURL=arrayNode.js.map