/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { dummyRoot, keyAsDetachedField, rootFieldKey } from "../core/index.js";
import { getSchemaAndPolicy, FieldKinds, cursorForMapTreeField, throwOutOfSchema, } from "../feature-libraries/index.js";
import { normalizeFieldSchema } from "./fieldSchema.js";
import { flexTreeFromInsertable, } from "./unhydratedFlexTreeFromInsertable.js";
import { createField, getInnerNode, getKernel, isTreeNode, UnhydratedFlexTreeNode, } from "./core/index.js";
import { assert, fail } from "@fluidframework/core-utils/internal";
import { combineChunks, isFieldInSchema } from "../feature-libraries/index.js";
import { getUnhydratedContext } from "./createContext.js";
import { convertField } from "./toStoredSchema.js";
/**
 * For now, schema validation for inserted content is always enabled.
 * @remarks
 * If this ends up being too much of a performance overhead, AND nothing depends on it (like staged allowed types likely will),
 * this could be changed.
 */
const validateSchema = true;
// IDEA:
// Have prepareForInsertion return a TreeChunk, and a function which should be called after its insertion with the path to its location (as Inner node?)
function isHydrated(node) {
    return isTreeNode(node) && getKernel(node).isHydrated();
}
function assertIsDetachedFlexTreeNode(node) {
    if (node.parentField.parent.parent !== undefined ||
        node.parentField.parent.key === rootFieldKey) {
        throw new UsageError("Can only attach a detached node (i.e., a root with TreeStatus.Removed status)");
    }
}
/**
 * Prepare content from a user for insertion into a tree.
 * @remarks
 * This validates and converts the input, and if necessary invokes {@link prepareContentForHydration}.
 *
 * The next edit made to `destinationContext`'s forest must be the creation of a detached field containing this content,
 * (Triggering {@link ForestEvents.afterRootFieldCreated}) otherwise hydration will break.
 */
export function prepareForInsertion(data, schema, destinationContext, destinationSchema) {
    if (data !== undefined && isHydrated(data)) {
        const treeNode = getInnerNode(data);
        assertIsDetachedFlexTreeNode(treeNode);
        return treeNode;
    }
    const content = prepareForInsertionContextlessInternal(data, schema, getSchemaAndPolicy(destinationContext), destinationContext.isHydrated() ? destinationContext : undefined, destinationSchema);
    assert(content.toAttach !== undefined, "Expected toAttach to be defined");
    assert(content.toAttach.length < 2, "Expected toAttach to be a single element on none");
    // TODO: when supporting back compat to not edit detached fields, caller will have to finalize after attaching to in document location.
    content.finalize(content.toAttach);
    return (content.toAttach.length === 0 ? undefined : content.toAttach[0]);
}
/**
 * {@link prepareForInsertion} but batched for array content.
 * @remarks
 * This is for inserting items into an array, not a inserting a {@link TreeArrayNode} (that would use {@link prepareForInsertion}).
 *
 * The next edits made to `destinationContext`'s forest must be the creation of a detached field.
 * One edit for each item in `data`, in order.
 *
 * @privateRemarks
 * This has to be done as a single operation for all items in data
 * (as opposed to mapping {@link prepareForInsertion} over the array)
 * due to how the eventing in prepareContentForHydration works.
 */
export function prepareArrayContentForInsertion(data, schema, destinationContext, destinationSchema) {
    const hasHydratedData = data.find(isHydrated) !== undefined;
    if (hasHydratedData) {
        const hasUnhydratedData = data.find((item) => !isHydrated(item)) !== undefined;
        if (hasUnhydratedData) {
            throw new UsageError("Mixed hydrated and unhydrated data not supported");
        }
        const treeNodes = data.map((item) => getInnerNode(item));
        for (const node of treeNodes) {
            assertIsDetachedFlexTreeNode(node);
        }
        return treeNodes;
    }
    const mapTrees = data.map((item) => flexTreeFromInsertable(item, schema));
    const fieldSchema = convertField(normalizeFieldSchema(schema));
    const normalizedFieldSchema = normalizeFieldSchema(schema);
    const field = createField(getUnhydratedContext(normalizedFieldSchema).flexContext, fieldSchema.kind, dummyRoot, mapTrees);
    const content = validateAndPrepare(getSchemaAndPolicy(destinationContext), destinationContext.isHydrated() ? destinationContext : undefined, {
        kind: FieldKinds.sequence.identifier,
        types: destinationSchema,
        persistedMetadata: undefined,
    }, field);
    assert(content.toAttach !== undefined, "Expected toAttach to be defined");
    // TODO: when supporting back compat to not edit detached fields, caller will have to finalize after attaching to in document location.
    content.finalize(content.toAttach);
    return content.toAttach;
}
/**
 * Split out from {@link prepareForInsertion} as to allow use without a context.
 *
 * @param hydratedData - If specified, the `mapTrees` will be prepared for hydration into this context.
 * `undefined` when `mapTrees` are being inserted into an {@link Unhydrated} tree.
 *
 * @remarks
 * Adding this entry point is a workaround for initialize not currently having a context.
 */
export function prepareForInsertionContextless(data, schema, schemaAndPolicy, hydratedData, destinationSchema) {
    const final = prepareForInsertionContextlessInternal(data, schema, schemaAndPolicy, hydratedData, destinationSchema);
    assert(final.rootsLocations !== undefined, "Expected rootIds to be defined");
    assert(final.rootsLocations.length <= 1, "Expected at most one node to be returned from prepareForInsertionContextless");
    // For some reason TypeScript can't figure out that final.rootsLocations is defined unless we handle is separately.
    return { ...final, rootsLocations: final.rootsLocations };
}
/**
 * Split out from {@link prepareForInsertion} as to allow use without a context.
 *
 * @param hydratedData - If specified, the `mapTrees` will be prepared for hydration into this context.
 * `undefined` when `mapTrees` are being inserted into an {@link Unhydrated} tree.
 */
function prepareForInsertionContextlessInternal(data, schema, schemaAndPolicy, hydratedData, destinationSchema) {
    const mapTree = flexTreeFromInsertable(data, schema);
    const contentArray = mapTree === undefined ? [] : [mapTree];
    const normalizedFieldSchema = normalizeFieldSchema(schema);
    const fieldSchema = convertField(normalizedFieldSchema);
    const field = createField(getUnhydratedContext(normalizedFieldSchema).flexContext, fieldSchema.kind, dummyRoot, contentArray);
    return validateAndPrepare(schemaAndPolicy, hydratedData, destinationSchema, field);
}
/**
 * If hydrating, do a final validation against the schema and prepare the content for hydration.
 *
 * @param hydratedData - If specified, the `mapTrees` will be prepared for hydration into this context.
 * `undefined` when `mapTrees` are being inserted into an {@link Unhydrated} tree.
 *
 * TODO: return nodes flex tree nodes to insert (might be newly hydrated) or reused unhydrated nodes when hydratedData is undefined.
 */
function validateAndPrepare(schemaAndPolicy, hydratedData, fieldSchema, field) {
    const cleanup = () => {
        // Discard the parent dummy field for the nodes before returning so they are unparented and can be inserted.
        for (const child of field.children) {
            assert(child instanceof UnhydratedFlexTreeNode, "TODO");
            child.adoptBy(undefined);
        }
    };
    if (hydratedData !== undefined) {
        // Run `chunkForInsertion` before walking the tree in `isFieldInSchema`.
        // This ensures that when `isFieldInSchema` requests identifiers (or any other contextual defaults),
        // they were already creating used the more specific context we have access to from `hydratedData`.
        const chunk = chunkForInsertion(hydratedData, field);
        // TODO: AB#45723
        // Now that staged schema rely on this validation, its a bit odd we don't do it for insertion into unhydrated contexts.
        // We can't simply enable it for them however due to contextual default fields which would not have been created yet (see comment above).
        // Specifically at least clone can result in unhydrated trees which can end up violating their stored schema (but not view schema) just using the type safe APIs.
        if (validateSchema === true) {
            isFieldInSchema(field, fieldSchema, schemaAndPolicy, throwOutOfSchema);
        }
        const rootsLocations = hydratedData.checkout.editor.buildRoots(chunk.chunk);
        // console.log(`built nodes: ${JSON.stringify(rootsLocations)}`);
        const toAttach = hydratedData.detachedField === undefined
            ? undefined
            : rootsLocations.map((f) => {
                assert(hydratedData.detachedField !== undefined, "detachedField should not stop being defined");
                const rootField = hydratedData.detachedField(keyAsDetachedField(f.field), FieldKinds.optional.identifier);
                assert(rootField.length === 1, "Expected single root in detached field");
                return rootField.boxedAt(0) ?? fail("Expected root to be present");
            });
        return {
            toAttach,
            rootsLocations,
            finalize: (attached) => {
                // do edits to move existing content into newly built tree and hydrate nodes as needed
                attachAndHydratedNodes([...attached], chunk.attaches);
                cleanup();
            },
        };
    }
    else {
        return { toAttach: [...field.children], finalize: cleanup };
    }
}
function chunkForInsertion(context, field) {
    const x = chunkFieldForInsertion(context, field);
    const chunks = context.checkout.forest.chunkField(cursorForMapTreeField(x.chunk));
    const chunk = combineChunks(chunks);
    return {
        chunk,
        attaches: x.attaches,
    };
}
function chunkFieldForInsertion(context, field) {
    const chunk = [];
    const attaches = [];
    for (const [i, child] of field.children.entries()) {
        if (child.isHydrated()) {
            // TODO: error if there is a hydrated parent.
            attaches.push({
                index: i,
                content: child,
                type: "hydrated",
            });
        }
        else {
            assert(child instanceof UnhydratedFlexTreeNode, "Expected child to be an UnhydratedFlexTreeNode");
            const fields = new Map();
            const childAttaches = new Map();
            for (const [key, fieldInner] of child.allFieldsLazy) {
                fieldInner.fillPendingDefaults(context);
                if (fieldInner.length > 0) {
                    const inner = chunkFieldForInsertion(context, fieldInner);
                    fields.set(key, inner.chunk);
                    childAttaches.set(key, inner.attaches);
                }
            }
            // As an optimization, if there are no attach data, skip tracking it.
            if (childAttaches.size !== 0 || child.treeNode !== undefined) {
                attaches.push({
                    index: i,
                    toHydrate: child.treeNode,
                    content: childAttaches,
                    type: "unhydrated",
                });
            }
            chunk.push({ type: child.type, value: child.value, fields });
        }
    }
    return {
        chunk,
        attaches,
    };
}
function attachAndHydratedNodes(field, attaches) {
    for (const attach of attaches) {
        if (attach.type === "hydrated") {
            // TODO: suppress events for these attaches?
            if (Array.isArray(field)) {
                // Move items after attach over by 1.
                for (let index = field.length; index > attach.index; index--) {
                    field[index] = field[index - 1] ?? fail("No item at index");
                }
                field[attach.index] = attach.content;
            }
            else if (field.is(FieldKinds.sequence)) {
                field.editor.insert(attach.index, [attach.content]);
            }
            else if (field.is(FieldKinds.optional)) {
                assert(field.length === 0, "Expected empty field for hydrated attach");
                field.editor.set(attach.content, false);
            }
            else {
                // TODO: ensure a good user facing error for this case.
                fail("Invalid field kind for hydrated attach");
            }
        }
        else {
            const child = (Array.isArray(field) ? field[attach.index] : field.boxedAt(attach.index)) ??
                fail("No child at index");
            if (attach.toHydrate !== undefined) {
                assert(child.isHydrated(), "Expected child to be hydrated");
                getKernel(attach.toHydrate).hydrate(child);
            }
            for (const [key, children] of attach.content) {
                const childField = child.getBoxed(key);
                attachAndHydratedNodes(childField, children);
            }
        }
    }
}
//# sourceMappingURL=prepareForInsertion.js.map