"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEditContext, useEditContextRef } from "./client/editContext";
import { useCallback, useEffect, useRef, useState, memo } from "react";
import { getChildren } from "./services/contentService";
import { getAbsoluteIconUrl, stripGuid } from "./utils";
import { Spinner } from "./ui/Spinner";
import { setContentTreeItemDragImage } from "./ui/DragPreview";
import { PerfectTree } from "./ui/PerfectTree";
import { loadInsertOptions } from "./services/editService";
import { cn } from "../lib/utils";
import { SquarePlus } from "lucide-react";
import { toast } from "sonner";
import { useGutterContextSafe } from "./tree-indicators";
// Create a memoized version of PerfectTree
const MemoizedPerfectTree = memo(PerfectTree);
function getArraySignature(values) {
    return (values ?? []).join("|");
}
function getTreeItemLabel(item, showDisplayNames, fallback = "") {
    if (showDisplayNames && item?.displayName?.trim()) {
        return item.displayName;
    }
    return item?.name || fallback;
}
function relabelTreeNodes(nodes, showDisplayNames, parent) {
    return nodes.map((node) => {
        const relabeledNode = {
            ...node,
            label: getTreeItemLabel(node.data, showDisplayNames, node.label),
            parent,
        };
        if (Array.isArray(node.children)) {
            relabeledNode.children = relabelTreeNodes(node.children, showDisplayNames, relabeledNode);
        }
        else {
            relabeledNode.children = node.children;
        }
        return relabeledNode;
    });
}
export default function ContentTree({ isDraggable, dragEnd, expandIdPath, initialExpandedKeys, selectionMode, multiSelectRequiresModifier = true, selectedItemIds, rootItemIds, onSelectionChange, onItemClick, filter, templateIds, className, includeEmbeddedItems, onDoubleClick, selectPagesOnly, renderNode, renderNodePrefix, onItemInserted, language, includeItemPath, scrollToSelected, disableAutoSelectOnExpand, showGrabCursorForDraggableNodes, nodeCursor, enableDragAndDrop, enableItemDrop = true, enableContextMenu, hideRootNodes, visibleRootItemIds, database, showCheckboxes, loadChildrenOverride, }) {
    const [treeNodes, setTreeNodes] = useState([]);
    const [isLoadingRootNodes, setIsLoadingRootNodes] = useState(false);
    const [selectedKeys, setSelectedKeys] = useState([]);
    const [expandedKeys, setExpandedKeys] = useState(initialExpandedKeys ?? []);
    const treeContainerRef = useRef(null);
    const editContext = useEditContext();
    const gutterContext = useGutterContextSafe();
    const showDisplayNames = gutterContext?.showDisplayNames ??
        editContext?.userInfo?.preferences?.showDisplayNames === true;
    const nodeDictionary = useRef({});
    const editContextRef = useEditContextRef();
    const lastSelectedItemId = useRef(undefined);
    const [isInitialized, setIsInitialized] = useState(false);
    const pendingDragStartTimeout = useRef(null);
    const dragStartPosition = useRef(null);
    const dragActivated = useRef(false);
    // Minimum distance (in pixels) the mouse must move before a drag is activated
    const DRAG_THRESHOLD = 5;
    const loadNodeChildren = useCallback(async (node) => {
        if (!editContext)
            return [];
        let children = loadChildrenOverride
            ? await loadChildrenOverride(node.key, language, database || "master")
            : await getChildren(node.key, editContext.sessionId, templateIds, includeEmbeddedItems ?? false, language, includeItemPath ? "path,idPath" : undefined, database || "master");
        if (filter)
            children = filter(children);
        return children.map((x) => ({
            key: x.id,
            label: getTreeItemLabel(x, showDisplayNames, x.name),
            iconUrl: getAbsoluteIconUrl(x.icon),
            isDraggable: isDraggable && isDraggable(x),
            selectable: !selectPagesOnly || x.hasLayout,
            data: x,
            hasChildren: x.hasChildren,
            parent: node,
        }));
    }, [
        editContext,
        filter,
        includeEmbeddedItems,
        showDisplayNames,
        isDraggable,
        language,
        selectPagesOnly,
        templateIds,
        includeItemPath,
        database,
        loadChildrenOverride,
    ]);
    // const updateCorrespondingNode = useCallback(
    //   (x: FullItem, parent: CustomTreeNode) => {
    //     const node = nodeDictionary.current[x.id];
    //     if (!node) return;
    //     node.label = x.name;
    //     node.iconUrl = getAbsoluteIconUrl(x.icon);
    //     node.isDraggable =
    //       isDraggable &&
    //       isDraggable({
    //         ...x,
    //         isComponent: !x.hasLayout,
    //         thumbUrl: "",
    //         previewUrl: "",
    //       });
    //     node.selectable = !selectPagesOnly || x.hasLayout;
    //     node.data = x;
    //     node.hasChildren = x.hasChildren;
    //     node.parent = parent;
    //   },
    //   [isDraggable, selectPagesOnly],
    // );
    const loadNode = useCallback(async (descriptor) => {
        const item = await editContext?.itemsRepository.getItem(descriptor);
        if (!item)
            return;
        return {
            key: item.id,
            label: getTreeItemLabel(item, showDisplayNames, item.name),
            iconUrl: getAbsoluteIconUrl(item.icon),
            isDraggable: isDraggable &&
                isDraggable({
                    ...item,
                    isComponent: !item.hasLayout,
                    thumbUrl: "",
                    previewUrl: "",
                }),
            selectable: !selectPagesOnly || item.hasLayout,
            data: item,
            hasChildren: item.hasChildren,
        };
    }, [editContext, isDraggable, selectPagesOnly, showDisplayNames]);
    const loadRootNodes = useCallback(async (force = false) => {
        if (!force && treeNodes.length > 0)
            return treeNodes;
        if (hideRootNodes) {
            // Load children of root nodes and show them as root level nodes
            const visibleRootItemIdSet = new Set(visibleRootItemIds?.map((id) => stripGuid(id)) ?? []);
            const rootNodes = await Promise.all(rootItemIds
                ?.map((rootItemId) => loadNode({
                id: rootItemId,
                language,
                version: 0,
                database: database || "master",
            }))
                .filter((x) => x) || []);
            const allChildren = [];
            for (const rootNode of rootNodes) {
                if (rootNode) {
                    if (visibleRootItemIdSet.has(stripGuid(rootNode.key))) {
                        allChildren.push(rootNode);
                        continue;
                    }
                    const children = await loadNodeChildren(rootNode);
                    // Remove parent reference since these will be displayed as root nodes
                    children.forEach((child) => {
                        child.parent = undefined;
                    });
                    allChildren.push(...children);
                }
            }
            return allChildren;
        }
        return await Promise.all(rootItemIds
            ?.map((rootItemId) => loadNode({
            id: rootItemId,
            language,
            version: 0,
            database: database || "master",
        }))
            .filter((x) => x) || []);
    }, [
        loadNode,
        loadNodeChildren,
        rootItemIds,
        visibleRootItemIds,
        language,
        treeNodes,
        hideRootNodes,
        database,
    ]);
    const doPreloadNodes = useCallback(async (expandedKeys, treeNodes) => {
        for (let i = 0; i < treeNodes.length; i++) {
            const node = treeNodes[i];
            if (!node)
                continue;
            let children = node.children;
            // Only load if children === undefined (not loaded yet)
            // Skip if children === null (loading in progress) to avoid race conditions
            if (expandedKeys.includes(node.key) &&
                children === undefined) {
                // When a caller has supplied its own children source, the cached
                // FullItem.hasChildren may not match the override's universe — ask
                // the override directly and let its result drive hasChildren.
                const shouldLoad = !!loadChildrenOverride || node.data.hasChildren;
                children = shouldLoad ? await loadNodeChildren(node) : [];
                node.children = children;
                node.hasChildren = children.length > 0;
            }
            if (expandedKeys.includes(node.key) &&
                children &&
                children.length > 0) {
                await doPreloadNodes(expandedKeys, children);
            }
        }
    }, [loadNodeChildren, loadChildrenOverride]);
    const preLoadNodes = useCallback(async (expandedKeys, force) => {
        // Increment version for this call to detect stale results
        const thisVersion = ++preLoadVersionRef.current;
        const rootNodes = await loadRootNodes(force);
        // Check if a newer preLoadNodes call was started - if so, abandon this one
        if (preLoadVersionRef.current !== thisVersion) {
            return;
        }
        await doPreloadNodes(expandedKeys, rootNodes);
        // Check again after async work
        if (preLoadVersionRef.current !== thisVersion) {
            return;
        }
        setTreeNodes([...rootNodes.filter((x) => x)]);
    }, [doPreloadNodes, loadRootNodes]);
    const refreshNode = useCallback(async (node, options) => {
        const item = await editContext?.itemsRepository.getItem(node.data);
        if (item) {
            const customNode = node;
            // Decide whether to reload children based on flags and expansion state
            const shouldReloadChildren = options?.reloadChildren === true;
            const isExpanded = expandedKeys?.includes(node.key);
            // Create a new node object with updated properties to trigger React re-render
            // (Mutating the existing object won't trigger memoized component updates)
            const updatedNode = {
                ...customNode,
                label: getTreeItemLabel(item, showDisplayNames, item.name),
                iconUrl: getAbsoluteIconUrl(item.icon),
                isDraggable: isDraggable &&
                    isDraggable({
                        ...item,
                        isComponent: !item.hasLayout,
                        thumbUrl: "",
                        previewUrl: "",
                    }),
                selectable: !selectPagesOnly || item.hasLayout,
                data: item,
                hasChildren: !!item.hasChildren,
            };
            if (shouldReloadChildren && isExpanded) {
                const newChildren = await loadNodeChildren(updatedNode);
                updatedNode.children = newChildren;
                updatedNode.hasChildren = newChildren.length > 0;
                // Ensure expanded descendants remain populated after a parent reload
                await doPreloadNodes(expandedKeys || [], newChildren);
            }
            else if (!isExpanded) {
                // Avoid holding children data when collapsed
                updatedNode.children = undefined;
            }
            // Replace the node in its parent's children array or in root nodes
            if (customNode.parent) {
                const parentChildren = customNode.parent.children;
                if (parentChildren) {
                    const index = parentChildren.findIndex((c) => c.key === node.key);
                    if (index !== -1) {
                        updatedNode.parent = customNode.parent;
                        parentChildren[index] = updatedNode;
                    }
                }
            }
            else {
                // It's a root node - update treeNodes directly
                setTreeNodes((prev) => {
                    const index = prev.findIndex((n) => n.key === node.key);
                    if (index !== -1) {
                        const newNodes = [...prev];
                        newNodes[index] = updatedNode;
                        return newNodes;
                    }
                    return prev;
                });
                return; // Don't trigger setTreeNodes again at the end
            }
        }
    }, [
        editContext?.itemsRepository,
        expandedKeys,
        loadNodeChildren,
        doPreloadNodes,
        isDraggable,
        selectPagesOnly,
        showDisplayNames,
    ]);
    useEffect(() => {
        setTreeNodes((prev) => {
            if (prev.length === 0)
                return prev;
            return relabelTreeNodes(prev, showDisplayNames);
        });
    }, [showDisplayNames]);
    // Track whether initial load has completed to avoid force-reloading on every itemsRepository reference change
    const hasInitializedRef = useRef(false);
    const prevLanguageRef = useRef(language);
    const prevRootItemIdsSignatureRef = useRef(getArraySignature(rootItemIds));
    const prevVisibleRootItemIdsSignatureRef = useRef(getArraySignature(visibleRootItemIds));
    const prevHideRootNodesRef = useRef(hideRootNodes);
    const prevDatabaseRef = useRef(database);
    // Counter to track preLoadNodes calls and prevent race conditions
    const preLoadVersionRef = useRef(0);
    const rootLoadVersionRef = useRef(0);
    // Handle root items or language changes
    useEffect(() => {
        if (!editContext?.itemsRepository)
            return;
        const rootItemIdsSignature = getArraySignature(rootItemIds);
        const visibleRootItemIdsSignature = getArraySignature(visibleRootItemIds);
        // Determine if we need a force reload (initial load or key props changed)
        const needsForceReload = !hasInitializedRef.current ||
            prevLanguageRef.current !== language ||
            prevRootItemIdsSignatureRef.current !== rootItemIdsSignature ||
            prevVisibleRootItemIdsSignatureRef.current !==
                visibleRootItemIdsSignature ||
            prevHideRootNodesRef.current !== hideRootNodes ||
            prevDatabaseRef.current !== database;
        // Update refs
        prevLanguageRef.current = language;
        prevRootItemIdsSignatureRef.current = rootItemIdsSignature;
        prevVisibleRootItemIdsSignatureRef.current = visibleRootItemIdsSignature;
        prevHideRootNodesRef.current = hideRootNodes;
        prevDatabaseRef.current = database;
        if (!needsForceReload) {
            // Skip reload - only itemsRepository reference changed, tree is already initialized
            return;
        }
        hasInitializedRef.current = true;
        setIsInitialized(false);
        setIsLoadingRootNodes(true);
        const rootLoadVersion = ++rootLoadVersionRef.current;
        const load = async () => {
            try {
                await preLoadNodes(expandedKeys || [], true);
            }
            finally {
                if (rootLoadVersionRef.current === rootLoadVersion) {
                    setIsLoadingRootNodes(false);
                    setIsInitialized(true);
                }
            }
        };
        load();
    }, [
        language,
        rootItemIds,
        visibleRootItemIds,
        hideRootNodes,
        database,
        editContext?.itemsRepository,
    ]);
    useEffect(() => {
        if (!editContext?.itemsRepository)
            return;
        const unsubscribe = editContext.itemsRepository.subscribeItemsChanged(async (changes) => {
            // Aggregate target nodes and whether their children should be reloaded
            const targetMap = new Map();
            for (const change of changes) {
                const isDelete = change?.action === "delete";
                const targetKey = isDelete
                    ? change?.parentId
                    : change?.item?.id;
                if (!targetKey)
                    continue;
                const node = nodeDictionary.current[targetKey];
                if (!node)
                    continue;
                // Default behavior: if flags are absent, keep previous behavior (reload children)
                const reloadChildrenFlag = isDelete
                    ? true
                    : (change?.changes?.children ?? true);
                const existing = targetMap.get(targetKey);
                if (existing) {
                    existing.reloadChildren =
                        existing.reloadChildren || reloadChildrenFlag;
                }
                else {
                    targetMap.set(targetKey, {
                        node,
                        reloadChildren: reloadChildrenFlag,
                    });
                }
            }
            if (targetMap.size === 0)
                return;
            await Promise.all(Array.from(targetMap.values()).map(({ node, reloadChildren }) => refreshNode(node, { reloadChildren })));
            setTreeNodes((prev) => [...prev]);
        });
        return () => {
            unsubscribe();
        };
    }, [editContext?.itemsRepository, refreshNode]);
    function getExpandedKeys() {
        const newExpandedKeys = [];
        if (!expandIdPath)
            return newExpandedKeys;
        const idPathArray = expandIdPath.toLowerCase().split("/");
        idPathArray.forEach((x) => {
            const key = stripGuid(x);
            if (key !== "")
                newExpandedKeys.push(key);
        });
        return newExpandedKeys;
    }
    useEffect(() => {
        const newExpandedKeys = getExpandedKeys();
        const hasNewKeys = newExpandedKeys.some((key) => !expandedKeys.includes(key));
        if (hasNewKeys) {
            const combinedKeys = [...new Set([...expandedKeys, ...newExpandedKeys])];
            setExpandedKeys(combinedKeys);
            // Preload nodes for programmatic expansion (from expandIdPath)
            if (isInitialized) {
                void preLoadNodes(combinedKeys, false);
            }
        }
        else if (isInitialized && newExpandedKeys.length > 0) {
            // When expandIdPath is applied before initialization finishes, retry the
            // preload once the root nodes are available so expanded nodes render children.
            void preLoadNodes(expandedKeys, false);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [expandIdPath, getArraySignature(rootItemIds), isInitialized]);
    useEffect(() => {
        const newNodeDictionary = {};
        createMap(treeNodes, newNodeDictionary);
        nodeDictionary.current = newNodeDictionary;
    }, [treeNodes]);
    function createMap(treeNodes, nodeDictionary) {
        treeNodes.forEach((n) => {
            if (n.key)
                nodeDictionary[n.key] = n;
            if (n.children)
                createMap(n.children, nodeDictionary);
        });
    }
    const nodeTemplate = useCallback((node) => {
        let label = _jsx("span", { children: node.label });
        if (node.url) {
            label = (_jsx("a", { onDragStart: (e) => e.preventDefault(), href: node.url, className: "text-primary font-medium hover:underline", children: node.label }));
        }
        const getNodeCursor = (node) => {
            if (showGrabCursorForDraggableNodes && node.isDraggable)
                return "cursor-grab";
            return "";
        };
        const isChecked = showCheckboxes && selectedKeys.includes(node.key);
        return (_jsxs("span", { className: cn("flex items-center gap-2 text-xs break-all", getNodeCursor(node), node.className), children: [showCheckboxes && (_jsx("input", { type: "checkbox", checked: isChecked, readOnly: true, tabIndex: -1, className: "border-border-default pointer-events-none h-3.5 w-3.5 shrink-0 rounded text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)]", "data-testid": `tree-item-checkbox-${node.key}` })), _jsx("img", { src: node.iconUrl, style: { height: "16px" }, width: "16", height: "16", onDragStart: (e) => e.preventDefault() }), label] }));
    }, [showCheckboxes, selectedKeys, showGrabCursorForDraggableNodes]);
    function handleDragStart(data) {
        try {
            // Allow both copy and move operations when dragging items
            data.event.dataTransfer.effectAllowed = "copyMove";
        }
        catch { }
        if (!data.isMultiSelect) {
            setContentTreeItemDragImage(data.event, data.node.label);
        }
        const items = data.isMultiSelect
            ? selectedItemIds
                ?.map((x) => nodeDictionary.current[x]?.data)
                .filter(Boolean)
            : [data.node.data];
        // Store initial drag position for threshold check
        dragStartPosition.current = {
            x: data.event.clientX,
            y: data.event.clientY,
        };
        dragActivated.current = false;
        // Function to check if drag should be activated based on mouse movement
        const checkAndActivateDrag = (e) => {
            if (dragActivated.current || !dragStartPosition.current)
                return;
            const dx = e.clientX - dragStartPosition.current.x;
            const dy = e.clientY - dragStartPosition.current.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            if (distance >= DRAG_THRESHOLD) {
                dragActivated.current = true;
                document.removeEventListener("drag", checkAndActivateDrag);
                editContextRef.current?.dragStart({
                    type: "items",
                    typeId: data.node.data.templateId,
                    items: items,
                });
            }
        };
        // Listen to drag events to detect movement
        document.addEventListener("drag", checkAndActivateDrag);
        // Fallback timeout - if no movement detected after 150ms, activate anyway
        // This handles edge cases where drag events might not fire
        pendingDragStartTimeout.current = setTimeout(() => {
            document.removeEventListener("drag", checkAndActivateDrag);
            if (!dragActivated.current) {
                dragActivated.current = true;
                editContextRef.current?.dragStart({
                    type: "items",
                    typeId: data.node.data.templateId,
                    items: items,
                });
            }
        }, 150);
        data.event.stopPropagation();
    }
    async function handleDrop(parent, index, event) {
        const object = editContextRef.current?.dragObject;
        if (!object)
            return;
        if (object.type === "items" && object.items) {
            // Check if this is a reorder operation (items are already children of the target parent)
            const isReorderOperation = object.items.every((item) => {
                const itemNode = nodeDictionary.current[item.id];
                // If the item's parent matches the target parent, it's a reorder
                return itemNode?.parent?.key === parent.key;
            });
            // Skip insert options check for reorder operations
            if (!isReorderOperation) {
                // Check insert options for validation
                try {
                    const insertOptions = await loadInsertOptions({
                        id: parent.data.id,
                        language: parent.data.language || language,
                        version: parent.data.version || 0,
                    });
                    // Check if any of the dragged items have compatible insert options
                    const hasValidInsertOptions = object.items.some((item) => {
                        const itemTemplateId = item.templateId || object.typeId;
                        return insertOptions.some((option) => {
                            // Check if the option's typeId matches the item's templateId
                            const optionTypeId = option.typeId || option.id;
                            if (optionTypeId === itemTemplateId) {
                                return true;
                            }
                            // Check if the item's templateId is in the option's compatibleTypeIds array
                            if (option.compatibleTypeIds &&
                                Array.isArray(option.compatibleTypeIds)) {
                                return option.compatibleTypeIds.includes(itemTemplateId);
                            }
                            return false;
                        });
                    });
                    if (!hasValidInsertOptions) {
                        const confirmed = await new Promise((resolve) => {
                            editContext?.confirm({
                                message: (_jsx("div", { children: "The selected items may not be compatible with this location. No matching insert options were found. Do you want to proceed anyway?" })),
                                header: "Insert Options Warning",
                                acceptLabel: "Proceed Anyway",
                                accept: () => resolve(true),
                                showCancel: true,
                            });
                        });
                        if (!confirmed) {
                            return;
                        }
                    }
                }
                catch (error) {
                    console.error("Error validating insert options:", error);
                    toast.error("Unable to validate insert options. Please try again.");
                    return;
                }
            }
            // Proceed with the original drop logic
            if (event.ctrlKey) {
                editContext?.executeCommand({
                    command: editContext.configuration.commands.copyItems,
                    data: {
                        items: object.items,
                        targetItem: parent.data,
                        index,
                    },
                });
            }
            else {
                editContext?.executeCommand({
                    command: editContext.configuration.commands.moveItems,
                    data: {
                        items: object.items,
                        targetItem: parent.data,
                        index,
                    },
                });
            }
            // Expand the parent node to show the dropped items
            const parentKey = parent.key;
            if (parentKey && !expandedKeys.includes(parentKey)) {
                setExpandedKeys((keys) => [...keys, parentKey]);
            }
        }
        event.stopPropagation();
    }
    function handleDragEnd() {
        if (pendingDragStartTimeout.current) {
            clearTimeout(pendingDragStartTimeout.current);
            pendingDragStartTimeout.current = null;
        }
        // Reset drag tracking state
        dragStartPosition.current = null;
        dragActivated.current = false;
        editContextRef.current?.dragEnd();
        if (dragEnd)
            dragEnd();
    }
    useEffect(() => {
        setSelectedKeys(selectedItemIds || []);
        // Update lastSelectedItemId when selectedItemIds prop changes to ensure
        // shift-click range selection works after page reload
        if ((selectedItemIds || []).length > 0) {
            lastSelectedItemId.current =
                selectedItemIds[selectedItemIds.length - 1];
        }
    }, [selectedItemIds]);
    // Cleanup pending drag start timeout on unmount
    useEffect(() => {
        return () => {
            if (pendingDragStartTimeout.current) {
                clearTimeout(pendingDragStartTimeout.current);
                pendingDragStartTimeout.current = null;
            }
        };
    }, []);
    const itemCommands = editContext?.configuration.commands.allItemCommands || [];
    const onContextMenu = useCallback(async (node, originalEvent) => {
        const key = String(node.key);
        const currentSelection = selectedItemIds ?? selectedKeys;
        let itemIds = selectionMode === "multiple" &&
            currentSelection?.includes(key) &&
            currentSelection.length > 1
            ? currentSelection
            : [key];
        if (!enableContextMenu)
            return;
        // Always block the native context menu immediately
        originalEvent.preventDefault();
        originalEvent.stopPropagation();
        const openInNewSlotItem = {
            id: key,
            language: node.data?.language || language,
            version: 0,
            database: database || "master",
        };
        if (itemIds.length === 1 && itemIds[0] === key) {
            setSelectedKeys([key]);
            if (onSelectionChange && selectionMode !== "none") {
                onSelectionChange([node.data]);
            }
        }
        // Prepare a synthetic event for later use (to avoid React event pooling)
        const { clientX, clientY, shiftKey, altKey, ctrlKey } = originalEvent;
        const syntheticEvent = new MouseEvent("contextmenu", {
            bubbles: true,
            cancelable: true,
            view: window,
            clientX,
            clientY,
            shiftKey,
            altKey,
            ctrlKey,
        });
        // Only show a spinner if work takes longer than 100ms
        let loadingShown = false;
        const loadingTimer = setTimeout(() => {
            loadingShown = true;
            editContext?.showContextMenu(syntheticEvent, [
                {
                    id: "loading",
                    label: "Loading…",
                    disabled: true,
                    icon: _jsx(Spinner, { size: "sm", className: "mr-2" }),
                },
            ]);
        }, 100);
        const items = await editContext?.itemsRepository.getItems(itemIds.map((x) => ({
            id: x,
            language: editContext.item?.language || "en",
            version: 0,
        })));
        // Build the final menu, then either show or update depending on whether loading is visible
        if (!items || items.length === 0) {
            clearTimeout(loadingTimer);
            const emptyMenu = [
                { id: "empty", label: "No actions available", disabled: true },
            ];
            const menuWithOpen = [
                {
                    id: "open-in-new-slot",
                    label: "Open in new slot",
                    icon: _jsx(SquarePlus, { strokeWidth: 1, className: "h-4 w-4" }),
                    command: async () => {
                        await editContext?.loadItem(openInNewSlotItem, {
                            openInNewSlot: true,
                        });
                    },
                },
                { id: "sep-open-in-new-slot", separator: true },
                ...emptyMenu,
            ];
            if (loadingShown)
                editContext?.updateContextMenu(menuWithOpen);
            else
                editContext?.showContextMenu(syntheticEvent, menuWithOpen);
            return;
        }
        const menuItems = await editContext?.configuration.editor.contentTree.contextMenu.factory({
            items: items,
            editContext: editContext,
            commandCallback: (command, result) => {
                if (command.id === "insertItem") {
                    const item = result;
                    if (item) {
                        setExpandedKeys((keys) => [...keys, item.id]);
                        onItemInserted?.(item);
                    }
                }
            },
        });
        if (menuItems && menuItems.length > 0) {
            clearTimeout(loadingTimer);
            const finalMenuItems = editContext?.isMobile
                ? menuItems
                : [
                    {
                        id: "open-in-new-slot",
                        label: "Open in new slot",
                        icon: _jsx(SquarePlus, { strokeWidth: 1, className: "h-4 w-4" }),
                        command: async () => {
                            await editContext?.loadItem(openInNewSlotItem, {
                                openInNewSlot: true,
                            });
                        },
                    },
                    { id: "sep-open-in-new-slot", separator: true },
                    ...menuItems,
                ];
            if (loadingShown)
                editContext?.updateContextMenu(finalMenuItems);
            else
                editContext?.showContextMenu(syntheticEvent, finalMenuItems);
        }
    }, [
        itemCommands,
        editContext,
        selectedItemIds,
        selectedKeys,
        onSelectionChange,
        selectionMode,
        onItemInserted,
        enableContextMenu,
        language,
        database,
    ]);
    // Memoize the toggle expand handler
    const handleToggleExpand = useCallback((nodeKey) => {
        setExpandedKeys((value) => {
            if (value.includes(nodeKey))
                return value.filter((x) => x !== nodeKey);
            else
                return [...value, nodeKey];
        });
    }, []);
    // Collect all descendant keys for a node (used by checkbox-mode shift cascade).
    const getDescendantKeys = useCallback((nodeKey) => {
        const start = nodeDictionary.current[nodeKey];
        if (!start?.children)
            return [];
        const out = [];
        const walk = (children) => {
            children?.forEach((c) => {
                out.push(c.key);
                if (c.children)
                    walk(c.children);
            });
        };
        walk(start.children);
        return out;
    }, []);
    // Memoize the node selection handler
    const handleNodeSelect = useCallback((nodeKey, ev) => {
        // Checkbox mode: click toggles selection of this node; shift cascades to
        // descendants. Range-select via Shift is replaced by cascade semantics.
        if (showCheckboxes) {
            const cascade = ev?.shiftKey === true;
            const targetKeys = cascade
                ? [nodeKey, ...getDescendantKeys(nodeKey)]
                : [nodeKey];
            const willCheck = !selectedKeys.includes(nodeKey);
            const next = new Set(selectedKeys);
            if (willCheck)
                targetKeys.forEach((k) => next.add(k));
            else
                targetKeys.forEach((k) => next.delete(k));
            const newSelection = Array.from(next);
            lastSelectedItemId.current = nodeKey;
            setSelectedKeys(newSelection);
            if (onSelectionChange) {
                const selectedNodes = newSelection
                    .map((k) => nodeDictionary.current[k]?.data)
                    .filter(Boolean);
                onSelectionChange(selectedNodes, newSelection);
            }
            return;
        }
        let newSelection = [...selectedKeys];
        if (selectionMode === "multiple" &&
            ev.shiftKey &&
            lastSelectedItemId.current) {
            // Get the nodes
            const lastNode = nodeDictionary.current[lastSelectedItemId.current];
            const currentNode = nodeDictionary.current[nodeKey];
            // Check if both nodes exist and have the same parent
            if (lastNode &&
                currentNode &&
                lastNode.parent?.key === currentNode.parent?.key) {
                const parentNode = lastNode.parent;
                // Get all children of the parent
                const siblings = parentNode?.children || [];
                // Find indices of selected nodes
                const siblingKeys = siblings.map((node) => node.key);
                const lastNodeIndex = siblingKeys.indexOf(lastSelectedItemId.current);
                const currentNodeIndex = siblingKeys.indexOf(nodeKey);
                // Determine start and end indices
                const startIdx = Math.min(lastNodeIndex, currentNodeIndex);
                const endIdx = Math.max(lastNodeIndex, currentNodeIndex);
                // Get all nodes between (inclusive)
                const nodesToSelect = siblingKeys.slice(startIdx, endIdx + 1);
                // Combine with existing selection
                newSelection = [...new Set([...newSelection, ...nodesToSelect])];
            }
            else {
                // If not on same level, just select the current node
                newSelection = [nodeKey];
            }
        }
        else if (selectionMode === "multiple" &&
            (ev.ctrlKey || ev.metaKey || !multiSelectRequiresModifier)) {
            lastSelectedItemId.current = nodeKey;
            newSelection = selectedKeys.includes(nodeKey)
                ? selectedKeys.filter((x) => x !== nodeKey)
                : [...selectedKeys, nodeKey];
        }
        else {
            lastSelectedItemId.current = nodeKey;
            newSelection = [nodeKey];
        }
        setSelectedKeys(newSelection);
        if (onSelectionChange) {
            const selectedNodes = newSelection
                .filter((key) => nodeDictionary.current[key])
                .map((key) => nodeDictionary.current[key]);
            onSelectionChange(selectedNodes.map((x) => x?.data), newSelection);
        }
        // Allow consumers to react to clicks with modifier keys (e.g. Ctrl+click)
        // Note: this runs after selection is updated.
        const clickedNode = nodeDictionary.current[nodeKey];
        if (onItemClick && clickedNode?.data) {
            // Only treat a "single selection click" as an item activation.
            // Ctrl-click is reserved for multi-selection in the tree.
            if (newSelection.length === 1 && newSelection[0] === nodeKey) {
                onItemClick(clickedNode.data, ev);
            }
        }
    }, [
        multiSelectRequiresModifier,
        onSelectionChange,
        onItemClick,
        selectionMode,
        selectedKeys,
        showCheckboxes,
        getDescendantKeys,
    ]);
    // RenderNode function
    const memoizedRenderNode = renderNode
        ? (node) => renderNode(node, (n) => nodeTemplate(n))
        : nodeTemplate;
    // Memoize the double click handler
    const handleDoubleClick = useCallback((node) => {
        onDoubleClick?.(node.data);
    }, [onDoubleClick]);
    // TreeNodes
    const memoizedTreeNodes = treeNodes;
    const loadChildren = useCallback(async (node, forceReload = false) => {
        // Only proceed if children is null (loading state) or undefined (not loaded)
        // Skip if children is already an array (already loaded by another operation)
        if (node.children === null ||
            node.children === undefined ||
            forceReload) {
            const nodes = await loadNodeChildren(node);
            // Only update if children is still null/undefined (not already loaded by another operation)
            if (node.children === null ||
                node.children === undefined ||
                forceReload) {
                node.children = nodes;
                setTreeNodes((prev) => [...prev]);
            }
        }
    }, [loadNodeChildren]);
    const handleDragOverZone = useCallback((node, index, event) => {
        event.dataTransfer.dropEffect = event.ctrlKey ? "copy" : "move";
        return true;
    }, []);
    // Memoize onLazyLoad callback
    const handleLazyLoad = useCallback((node) => {
        node.children = null;
        setTreeNodes((prev) => [...prev]);
        loadChildren(node);
    }, [loadChildren]);
    // Memoize onDrop callback
    const handleDropCallback = useCallback((parent, index, event) => {
        handleDrop(parent, index, event);
    }, [handleDrop]);
    if (!memoizedTreeNodes.length)
        return (_jsx("div", { className: cn(className, "relative h-full w-full"), "data-testid": "content-tree", children: isLoadingRootNodes || !isInitialized ? (_jsxs("div", { className: "text-neutral-grey-50 absolute inset-0 flex items-center justify-center gap-2 text-xs", children: [_jsx(Spinner, { size: "sm" }), " Loading..."] })) : (_jsx("div", { className: "text-neutral-grey-50 absolute inset-0 flex items-center justify-center text-xs", children: "No items found" })) }));
    return (_jsx(_Fragment, { children: _jsx("div", { className: cn(className, "text-neutral-grey-100 h-full w-full font-light"), ref: treeContainerRef, "data-parhelia-content-tree": "true", "data-testid": "content-tree", children: _jsx(MemoizedPerfectTree, { enableKeyboardSearch: true, nodes: memoizedTreeNodes, expandedKeys: expandedKeys, onToggleExpand: handleToggleExpand, renderNode: memoizedRenderNode, renderNodePrefix: renderNodePrefix, onLazyLoad: handleLazyLoad, onDoubleClick: handleDoubleClick, onSelect: handleNodeSelect, selectedKeys: selectedKeys, onContextMenu: onContextMenu, enableDragAndDrop: enableDragAndDrop, enableDropTargets: enableItemDrop, nodeCursor: nodeCursor, showSelectedBackground: !showCheckboxes, multiDragNoun: "items", onStartDrag: handleDragStart, onDragOverZone: handleDragOverZone, onDragEnd: handleDragEnd, isDragging: editContext?.dragObject !== undefined, onDrop: handleDropCallback, scrollToSelected: scrollToSelected, disableAutoSelectOnExpand: disableAutoSelectOnExpand }) }) }));
}
//# sourceMappingURL=ContentTree.js.map