/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/common-utils";
import { ValueSchema, lookupTreeSchema, mapCursorField, inCursorNode, } from "../../core";
import { Multiplicity } from "../modular-schema";
import { getFieldKind, getPrimaryField, isPrimitiveValue, arrayLikeMarkerSymbol, cursorFromContextualData, } from "../contextuallyTyped";
import { sequence } from "../defaultFieldKinds";
import { assertValidIndex } from "../../util";
import { adaptWithProxy, isPrimitive, keyIsValidIndex, getOwnArrayKeys, } from "./utilities";
import { proxyTargetSymbol, } from "./editableTreeTypes";
import { makeTree } from "./editableTree";
import { ProxyTarget } from "./ProxyTarget";
export function makeField(context, fieldSchema, cursor) {
    const targetSequence = new FieldProxyTarget(context, fieldSchema, cursor);
    return adaptWithProxy(targetSequence, fieldProxyHandler);
}
function isFieldProxyTarget(target) {
    return target instanceof FieldProxyTarget;
}
/**
 * @returns the key, if any, of the primary array field.
 */
function getPrimaryArrayKey(type) {
    const primary = getPrimaryField(type);
    if (primary === undefined) {
        return undefined;
    }
    const kind = getFieldKind(primary.schema);
    if (kind.multiplicity === Multiplicity.Sequence) {
        // TODO: this could have issues if there are non-primary keys
        // that can collide with the array APIs (length or integers).
        return primary;
    }
    return undefined;
}
/**
 * A Proxy target, which together with a `fieldProxyHandler` implements a basic access to
 * the nodes of {@link EditableField} by means of the cursors.
 */
export class FieldProxyTarget extends ProxyTarget {
    constructor(context, fieldSchema, cursor) {
        super(context, cursor);
        this.fieldSchema = fieldSchema;
        assert(cursor.mode === 1 /* Fields */, 0x453 /* must be in fields mode */);
        this.fieldKey = cursor.getFieldKey();
        this[arrayLikeMarkerSymbol] = true;
    }
    get [proxyTargetSymbol]() {
        return this;
    }
    get parent() {
        if (this.getAnchor().parent === undefined) {
            return undefined;
        }
        const cursor = this.cursor;
        cursor.exitField();
        const output = makeTree(this.context, cursor);
        cursor.enterField(this.fieldKey);
        return output;
    }
    buildAnchor() {
        return this.cursor.buildFieldAnchor();
    }
    tryMoveCursorToAnchor(anchor, cursor) {
        return this.context.forest.tryMoveCursorToField(anchor, cursor);
    }
    forgetAnchor(anchor) {
        if (anchor.parent === undefined)
            return;
        this.context.forest.anchors.forget(anchor.parent);
    }
    get length() {
        return this.cursor.getFieldLength();
    }
    /**
     * Returns a node (unwrapped by default, see {@link UnwrappedEditableTree}) by its index.
     */
    unwrappedTree(index) {
        return inCursorNode(this.cursor, index, (cursor) => unwrappedTree(this.context, cursor));
    }
    /**
     * Gets a node by its index without unwrapping.
     */
    getNode(index) {
        assert(keyIsValidIndex(index, this.length), 0x454 /* A child node must exist at index to get it without unwrapping. */);
        return inCursorNode(this.cursor, index, (cursor) => makeTree(this.context, cursor));
    }
    /**
     * Gets array of unwrapped nodes.
     */
    asArray() {
        return mapCursorField(this.cursor, (cursor) => unwrappedTree(this.context, cursor));
    }
    [Symbol.iterator]() {
        return this.asArray().values();
    }
    insertNodes(index, newContent) {
        const fieldKind = getFieldKind(this.fieldSchema);
        // TODO: currently for all field kinds the nodes can be created by editor using `sequenceField.insert()`.
        // Uncomment the next line and remove non-sequence related code when the editor will become more schema-aware.
        // assert(fieldKind.multiplicity === Multiplicity.Sequence, "The field must be of a sequence kind.");
        if (fieldKind.multiplicity !== Multiplicity.Sequence) {
            assert(this.length === 0 && (!Array.isArray(newContent) || newContent.length <= 1), 0x455 /* A non-sequence field cannot have more than one node. */);
        }
        assert(keyIsValidIndex(index, this.length + 1), 0x456 /* Index must be less than or equal to length. */);
        const fieldPath = this.cursor.getFieldPath();
        this.context.insertNodes(fieldPath, index, newContent);
    }
    moveNodes(sourceIndex, count, destinationIndex, destinationField) {
        const sourceFieldPath = this.cursor.getFieldPath();
        const destinationFieldKindIdentifier = destinationField !== undefined
            ? destinationField.fieldSchema.kind.identifier
            : this.fieldSchema.kind.identifier;
        assert(this.fieldSchema.kind.identifier === sequence.identifier &&
            destinationFieldKindIdentifier === sequence.identifier, 0x683 /* Both source and destination fields must be sequence fields. */);
        const destinationFieldProxy = destinationField !== undefined
            ? destinationField[proxyTargetSymbol]
            : this;
        assert(isFieldProxyTarget(destinationFieldProxy), 0x684 /* destination field proxy must be a field proxy target */);
        assertValidIndex(destinationIndex, destinationFieldProxy, true);
        const destinationFieldPath = destinationFieldProxy.cursor.getFieldPath();
        this.context.moveNodes(sourceFieldPath, sourceIndex, count, destinationFieldPath, destinationIndex);
    }
    getfieldPath() {
        return this.cursor.getFieldPath();
    }
    deleteNodes(index, count) {
        // TODO: currently for all field kinds the nodes can be deleted by editor using `sequenceField.delete()`.
        // Uncomment when the editor will become more schema-aware.
        // const fieldKind = getFieldKind(this.fieldSchema);
        // assert(fieldKind.multiplicity === Multiplicity.Sequence, "The field must be of a sequence kind.");
        assert(this.length === 0 || keyIsValidIndex(index, this.length), 0x457 /* Index must be less than length. */);
        if (count !== undefined)
            assert(count >= 0, 0x458 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const _count = count === undefined || count > maxCount ? maxCount : count;
        const fieldPath = this.cursor.getFieldPath();
        this.context.deleteNodes(fieldPath, index, _count);
    }
    replaceNodes(index, newContent, count) {
        const fieldKind = getFieldKind(this.fieldSchema);
        // TODO: currently for all field kinds the nodes can be created by editor using `sequenceField.insert()`.
        // Uncomment the next line and remove non-sequence related code when the editor will become more schema-aware.
        // assert(fieldKind.multiplicity === Multiplicity.Sequence, "The field must be of a sequence kind.");
        if (fieldKind.multiplicity !== Multiplicity.Sequence) {
            assert(this.length <= 1 && (!Array.isArray(newContent) || newContent.length <= 1), 0x4d0 /* A non-sequence field cannot have more than one node. */);
        }
        assert((this.length === 0 && index === 0) || keyIsValidIndex(index, this.length), 0x4d1 /* Index must be less than length or, if the field is empty, be 0. */);
        if (count !== undefined)
            assert(count >= 0, 0x4d2 /* Count must be non-negative. */);
        const maxCount = this.length - index;
        const _count = count === undefined || count > maxCount ? maxCount : count;
        const fieldPath = this.cursor.getFieldPath();
        this.context.replaceNodes(fieldPath, index, _count, newContent);
    }
}
const editableFieldPropertySetWithoutLength = new Set([
    "fieldKey",
    "fieldSchema",
    "primaryType",
    "parent",
    "context",
]);
/**
 * The set of `EditableField` properties exposed by `fieldProxyHandler`.
 * Any other properties are considered to be non-existing.
 */
const editableFieldPropertySet = new Set([
    "length",
    ...editableFieldPropertySetWithoutLength,
]);
/**
 * Returns a Proxy handler, which together with a {@link FieldProxyTarget} implements a basic read/write access to
 * the sequence fields by means of the cursors.
 */
const fieldProxyHandler = {
    get: (target, key, receiver) => {
        if (typeof key === "string") {
            if (editableFieldPropertySet.has(key)) {
                return Reflect.get(target, key);
            }
            else if (keyIsValidIndex(key, target.length)) {
                return target.unwrappedTree(Number(key));
            }
            // This maps the methods of the `EditableField` to their implementation in the `FieldProxyTarget`.
            // Expected are only the methods declared in the `EditableField` interface,
            // as only those are visible for the users of the public API.
            // Such implicit delegation is chosen for a future array implementation in case it will be needed.
            const reflected = Reflect.get(target, key);
            if (typeof reflected === "function") {
                return function (...args) {
                    return Reflect.apply(reflected, target, args);
                };
            }
            return undefined;
        }
        switch (key) {
            case proxyTargetSymbol:
                return target;
            case Symbol.iterator:
                return target[Symbol.iterator].bind(target);
            case arrayLikeMarkerSymbol:
                return true;
            default:
        }
        return undefined;
    },
    set: (target, key, value, receiver) => {
        const cursor = cursorFromContextualData(target.context.schema, target.fieldSchema.types, value);
        // This is just a cheap way to check if there might be a node at the given index.
        // An implementation of the target methods holds all relevant key assertions.
        // TODO: maybe refactor this to add a real node existence check if desired,
        // but it might be costly regarding performance.
        if (keyIsValidIndex(key, target.length)) {
            target.replaceNodes(Number(key), cursor, 1);
        }
        else {
            target.insertNodes(Number(key), cursor);
        }
        return true;
    },
    deleteProperty: (target, key) => {
        throw new Error("Not supported. Use `deleteNodes()` instead");
    },
    // Include documented symbols and all non-empty fields.
    has: (target, key) => {
        if (typeof key === "symbol") {
            switch (key) {
                case Symbol.iterator:
                case proxyTargetSymbol:
                case arrayLikeMarkerSymbol:
                    return true;
                default:
            }
        }
        else {
            if (keyIsValidIndex(key, target.length) || editableFieldPropertySet.has(key)) {
                return true;
            }
        }
        return false;
    },
    ownKeys: (target) => {
        // This includes 'length' property.
        const keys = getOwnArrayKeys(target.length);
        keys.push(...editableFieldPropertySetWithoutLength);
        return keys;
    },
    getOwnPropertyDescriptor: (target, key) => {
        // We generally don't want to allow users of the proxy to reconfigure all the properties,
        // but it is a TypeError to return non-configurable for properties that do not exist on target,
        // so they must return true.
        if (typeof key === "symbol") {
            switch (key) {
                case proxyTargetSymbol:
                    return {
                        configurable: true,
                        enumerable: false,
                        value: target,
                        writable: false,
                    };
                case Symbol.iterator:
                    return {
                        configurable: true,
                        enumerable: false,
                        value: target[Symbol.iterator].bind(target),
                        writable: false,
                    };
                default:
            }
        }
        else {
            if (editableFieldPropertySet.has(key)) {
                return {
                    configurable: true,
                    enumerable: false,
                    value: Reflect.get(target, key),
                    writable: false,
                };
            }
            else if (keyIsValidIndex(key, target.length)) {
                return {
                    configurable: true,
                    enumerable: true,
                    value: target.unwrappedTree(Number(key)),
                    writable: true,
                };
            }
        }
        return undefined;
    },
};
/**
 * See {@link UnwrappedEditableTree} for documentation on what unwrapping this performs.
 */
function unwrappedTree(context, cursor) {
    const nodeTypeName = cursor.type;
    const nodeType = lookupTreeSchema(context.schema, nodeTypeName);
    // Unwrap primitives or nodes having a primary field. Sequences unwrap nodes on their own.
    if (isPrimitive(nodeType)) {
        const nodeValue = cursor.value;
        if (isPrimitiveValue(nodeValue)) {
            return nodeValue;
        }
        assert(nodeType.value === ValueSchema.Serializable, 0x3c7 /* `undefined` values not allowed for primitive fields */);
    }
    const primary = getPrimaryArrayKey(nodeType);
    if (primary !== undefined) {
        cursor.enterField(primary.key);
        const primaryField = makeField(context, primary.schema, cursor);
        cursor.exitField();
        return primaryField;
    }
    return makeTree(context, cursor);
}
/**
 * @param context - the common context of the field.
 * @param fieldSchema - the FieldSchema of the field.
 * @param cursor - the cursor, which must point to the field being proxified.
 */
export function unwrappedField(context, fieldSchema, cursor) {
    const fieldKind = getFieldKind(fieldSchema);
    if (fieldKind.multiplicity === Multiplicity.Sequence) {
        return makeField(context, fieldSchema, cursor);
    }
    const length = cursor.getFieldLength();
    assert(length <= 1, 0x3c8 /* invalid non sequence */);
    if (length === 1) {
        return inCursorNode(cursor, 0, (innerCursor) => unwrappedTree(context, innerCursor));
    }
    assert(fieldKind.multiplicity === Multiplicity.Optional ||
        fieldKind.multiplicity === Multiplicity.Forbidden, 0x59a /* invalid empty field */);
    return undefined;
}
/**
 * Checks the type of an UnwrappedEditableField.
 * @alpha
 */
export function isEditableField(field) {
    return (typeof field === "object" &&
        isFieldProxyTarget(field[proxyTargetSymbol]));
}
//# sourceMappingURL=editableField.js.map