/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/common-utils";
import { symbolIsFieldKey, lookupTreeSchema, mapCursorFields, anchorSlot, inCursorField, } from "../../core";
import { brand, fail } from "../../util";
import { Multiplicity } from "../modular-schema";
import { singleMapTreeCursor } from "../mapTreeCursor";
import { getFieldKind, getFieldSchema, applyFieldTypesFromContext, typeNameSymbol, valueSymbol, allowsValue, } from "../contextuallyTyped";
import { adaptWithProxy } from "./utilities";
import { createField, getField, on, parentField, proxyTargetSymbol, replaceField, typeSymbol, contextSymbol, } from "./editableTreeTypes";
import { makeField, unwrappedField } from "./editableField";
import { ProxyTarget } from "./ProxyTarget";
const editableTreeSlot = anchorSlot();
export function makeTree(context, cursor) {
    var _a;
    const anchor = cursor.buildAnchor();
    const anchorNode = (_a = context.forest.anchors.locate(anchor)) !== null && _a !== void 0 ? _a : fail("cursor should point to a node that is not the root of the AnchorSet");
    const cached = anchorNode.slots.get(editableTreeSlot);
    if (cached !== undefined) {
        context.forest.anchors.forget(anchor);
        return cached;
    }
    const newTarget = new NodeProxyTarget(context, cursor, anchorNode, anchor);
    const output = adaptWithProxy(newTarget, nodeProxyHandler);
    anchorNode.slots.set(editableTreeSlot, output);
    anchorNode.on("afterDelete", cleanupTree);
    return output;
}
function cleanupTree(anchor) {
    var _a;
    const cached = (_a = anchor.slots.get(editableTreeSlot)) !== null && _a !== void 0 ? _a : fail("tree should only be cleaned up once");
    cached[proxyTargetSymbol].free();
}
function isNodeProxyTarget(target) {
    return target instanceof NodeProxyTarget;
}
/**
 * A Proxy target, which together with a `nodeProxyHandler` implements a basic access to
 * the fields of {@link EditableTree} by means of the cursors.
 */
export class NodeProxyTarget extends ProxyTarget {
    constructor(context, cursor, anchorNode, anchor) {
        super(context, cursor, anchor);
        this.anchorNode = anchorNode;
        assert(cursor.mode === 0 /* Nodes */, 0x44c /* must be in nodes mode */);
        this.proxy = adaptWithProxy(this, nodeProxyHandler);
        anchorNode.slots.set(editableTreeSlot, this.proxy);
        this.removeDeleteCallback = anchorNode.on("afterDelete", cleanupTree);
        assert(this.context.schema.treeSchema.get(this.typeName) !== undefined, 0x5b1 /* There is no explicit schema for this node type. Ensure that the type is correct and the schema for it was added to the SchemaData */);
    }
    buildAnchor() {
        return this.context.forest.anchors.track(this.anchorNode);
    }
    tryMoveCursorToAnchor(anchor, cursor) {
        return this.context.forest.tryMoveCursorToNode(anchor, cursor);
    }
    forgetAnchor(anchor) {
        // This type unconditionally has an anchor, so `forgetAnchor` is always called and cleanup can be done here:
        // After this point this node will not be usable,
        // so remove it from the anchor incase a different context (or the same context later) uses this AnchorSet.
        this.anchorNode.slots.delete(editableTreeSlot);
        this.removeDeleteCallback();
        this.context.forest.anchors.forget(anchor);
    }
    get typeName() {
        return this.cursor.type;
    }
    get type() {
        return lookupTreeSchema(this.context.schema, this.typeName);
    }
    get value() {
        return this.cursor.value;
    }
    set value(value) {
        assert(allowsValue(this.type.value, value), 0x5b2 /* Out of schema value can not be set on tree */);
        this.context.setNodeValue(this.anchorNode, value);
    }
    get currentIndex() {
        return this.cursor.fieldIndex;
    }
    lookupFieldKind(field) {
        return getFieldKind(this.getFieldSchema(field));
    }
    getFieldSchema(field) {
        return getFieldSchema(field, this.context.schema, this.type);
    }
    getFieldKeys() {
        return mapCursorFields(this.cursor, (c) => c.getFieldKey());
    }
    has(field) {
        // Make fields present only if non-empty.
        return this.fieldLength(field) !== 0;
    }
    unwrappedField(field) {
        const schema = this.getFieldSchema(field);
        return inCursorField(this.cursor, field, (cursor) => unwrappedField(this.context, schema, cursor));
    }
    getField(fieldKey) {
        const schema = this.getFieldSchema(fieldKey);
        return inCursorField(this.cursor, fieldKey, (cursor) => makeField(this.context, schema, cursor));
    }
    [Symbol.iterator]() {
        const type = this.type;
        return mapCursorFields(this.cursor, (cursor) => makeField(this.context, getFieldSchema(cursor.getFieldKey(), this.context.schema, type), cursor)).values();
    }
    createField(fieldKey, newContent) {
        assert(!this.has(fieldKey), 0x44f /* The field already exists. */);
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case Multiplicity.Optional: {
                assert(!Array.isArray(newContent), 0x450 /* Use single cursor to create the optional field */);
                this.context.setOptionalField(path, newContent, true);
                break;
            }
            case Multiplicity.Sequence: {
                this.context.insertNodes(path, 0, newContent);
                break;
            }
            case Multiplicity.Value:
                fail("It is invalid to create fields of kind `value` as they should always exist.");
            default:
                fail("`Forbidden` fields may not be created.");
        }
    }
    fieldLength(field) {
        return inCursorField(this.cursor, field, (cursor) => cursor.getFieldLength());
    }
    deleteField(fieldKey) {
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case Multiplicity.Optional: {
                this.context.setOptionalField(path, undefined, false);
                break;
            }
            case Multiplicity.Sequence: {
                const length = this.fieldLength(fieldKey);
                this.context.deleteNodes(path, 0, length);
                break;
            }
            case Multiplicity.Value:
                fail("Fields of kind `value` may not be deleted.");
            default:
                fail("`Forbidden` fields may not be deleted.");
        }
    }
    replaceField(fieldKey, newContent) {
        const fieldKind = this.lookupFieldKind(fieldKey);
        const path = { parent: this.anchorNode, field: fieldKey };
        switch (fieldKind.multiplicity) {
            case Multiplicity.Optional: {
                assert(!Array.isArray(newContent), 0x4cd /* It is invalid to replace the optional field using the array data. */);
                this.context.setOptionalField(path, newContent, !this.has(fieldKey));
                break;
            }
            case Multiplicity.Sequence: {
                assert(Array.isArray(newContent), 0x5cc /* It is invalid to replace the sequence field with a non array value. */);
                const length = this.fieldLength(fieldKey);
                /**
                 * `replaceNodes` has different merge semantics than the `replaceField` would ideally offer:
                 * `replaceNodes` should not overwrite concurrently inserted content while `replaceField` should.
                 * We currently use `replaceNodes` here because the low-level editing API
                 * for the desired `replaceField` semantics is not yet avaialble.
                 */
                // TODO: update implementation once the low-level editing API is available.
                this.context.replaceNodes(path, 0, length, newContent);
                break;
            }
            case Multiplicity.Value: {
                assert(!Array.isArray(newContent), 0x4ce /* It is invalid to replace the value field using the array data. */);
                assert(newContent !== undefined, 0x5cd /* It is invalid to replace a value field with undefined */);
                this.context.setValueField(path, newContent);
                break;
            }
            default:
                fail("`Forbidden` fields may not be replaced as they never exist.");
        }
    }
    get parentField() {
        const cursor = this.cursor;
        const index = cursor.fieldIndex;
        cursor.exitNode();
        const key = cursor.getFieldKey();
        cursor.exitField();
        const parentType = cursor.type;
        cursor.enterField(key);
        const fieldSchema = getFieldSchema(key, this.context.schema, lookupTreeSchema(this.context.schema, parentType));
        const proxifiedField = makeField(this.context, fieldSchema, this.cursor);
        this.cursor.enterNode(index);
        return { parent: proxifiedField, index };
    }
    on(eventName, listener) {
        switch (eventName) {
            case "changing": {
                const unsubscribeFromValueChange = this.anchorNode.on("valueChanging", listener);
                const unsubscribeFromChildrenChange = this.anchorNode.on("childrenChanging", (anchorNode) => listener(anchorNode, undefined));
                return () => {
                    unsubscribeFromValueChange();
                    unsubscribeFromChildrenChange();
                };
            }
            case "subtreeChanging": {
                const unsubscribeFromSubtreeChange = this.anchorNode.on("subtreeChanging", (anchorNode) => listener(anchorNode, undefined));
                return unsubscribeFromSubtreeChange;
            }
            default:
                unreachableCase(eventName);
        }
    }
}
/**
 * A Proxy handler together with a {@link NodeProxyTarget} implements a basic read/write access to the Forest
 * by means of the cursors.
 */
const nodeProxyHandler = {
    get: (target, key) => {
        if (typeof key === "string" || symbolIsFieldKey(key)) {
            // All string keys are fields
            return target.unwrappedField(brand(key));
        }
        // utility symbols
        switch (key) {
            case typeSymbol:
                return target.type;
            case typeNameSymbol:
                return target.typeName;
            case valueSymbol:
                return target.value;
            case proxyTargetSymbol:
                return target;
            case Symbol.iterator:
                return target[Symbol.iterator].bind(target);
            case getField:
                return target.getField.bind(target);
            case createField:
                return target.createField.bind(target);
            case replaceField:
                return target.replaceField.bind(target);
            case parentField:
                return target.parentField;
            case contextSymbol:
                return target.context;
            case on:
                return target.on.bind(target);
            default:
                return undefined;
        }
    },
    set: (target, key, value, receiver) => {
        if (typeof key === "string" || symbolIsFieldKey(key)) {
            const fieldKey = brand(key);
            const fieldSchema = target.getFieldSchema(fieldKey);
            const multiplicity = target.lookupFieldKind(fieldKey).multiplicity;
            const content = applyFieldTypesFromContext(target.context.schema, fieldSchema, value);
            const cursors = content.map(singleMapTreeCursor);
            // This unconditionally uses `replaceField`, which differs from `createField`
            // only for sequence fields while using `insertNodes` instead of `replaceNodes`
            // (plus some difference in assertions, which is ignored here for a sake of a better
            // consistency with the low-level editing API).
            // Since `insertNodes` and `replaceNodes` have same merge semantics with `replaceNodes`
            // being a bit more general purpose function, it's ok to just use that.
            if (multiplicity !== Multiplicity.Sequence) {
                assert(cursors.length <= 1, 0x5ce /* more than one top level node in non-sequence filed */);
                target.replaceField(fieldKey, cursors.length === 0 ? undefined : cursors[0]);
            }
            else {
                target.replaceField(fieldKey, cursors);
            }
            return true;
        }
        else if (key === valueSymbol) {
            target.value = value;
            return true;
        }
        return false;
    },
    deleteProperty: (target, key) => {
        if (typeof key === "string" || symbolIsFieldKey(key)) {
            const fieldKey = brand(key);
            target.deleteField(fieldKey);
            return true;
        }
        return false;
    },
    // Include documented symbols (except value when value is undefined) and all non-empty fields.
    has: (target, key) => {
        if (typeof key === "string" || symbolIsFieldKey(key)) {
            return target.has(brand(key));
        }
        // utility symbols
        switch (key) {
            case proxyTargetSymbol:
            case typeSymbol:
            case typeNameSymbol:
            case Symbol.iterator:
            case getField:
            case createField:
            case replaceField:
            case parentField:
            case on:
            case contextSymbol:
                return true;
            case valueSymbol:
                // Could do `target.value !== ValueSchema.Nothing`
                // instead if values which could be modified should report as existing.
                return target.value !== undefined;
            default:
                return false;
        }
    },
    // Includes all non-empty fields, which are the enumerable fields.
    ownKeys: (target) => {
        return target.getFieldKeys();
    },
    getOwnPropertyDescriptor: (target, key) => {
        // We generally don't want to allow users of the proxy to reconfigure all the properties,
        // but it is an TypeError to return non-configurable for properties that do not exist on target,
        // so they must return true.
        if ((typeof key === "string" || symbolIsFieldKey(key)) && target.has(brand(key))) {
            const field = target.unwrappedField(brand(key));
            return {
                configurable: true,
                enumerable: true,
                value: field,
                writable: true,
            };
        }
        // utility symbols
        switch (key) {
            case proxyTargetSymbol:
                return { configurable: true, enumerable: false, value: target, writable: false };
            case typeSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.type,
                    writable: false,
                };
            case typeNameSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.typeName,
                    writable: false,
                };
            case valueSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.value,
                    writable: false,
                };
            case Symbol.iterator:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target[Symbol.iterator].bind(target),
                    writable: false,
                };
            case getField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.getField.bind(target),
                    writable: false,
                };
            case createField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.createField.bind(target),
                    writable: false,
                };
            case replaceField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.replaceField.bind(target),
                    writable: false,
                };
            case parentField:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.parentField,
                    writable: false,
                };
            case contextSymbol:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.context,
                    writable: false,
                };
            case on:
                return {
                    configurable: true,
                    enumerable: false,
                    value: target.on.bind(target),
                    writable: false,
                };
            default:
                return undefined;
        }
    },
};
/**
 * Checks the type of an UnwrappedEditableField.
 * @alpha
 */
export function isUnwrappedNode(field) {
    return (typeof field === "object" &&
        isNodeProxyTarget(field[proxyTargetSymbol]));
}
//# sourceMappingURL=editableTree.js.map