"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEditContext, useEditContextRef } from "../client/editContext";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { getComponentCommands } from "../commands/componentCommands";
import ConfirmationDialog from "../ConfirmationDialog";
import { useSlotContext } from "../views/editorSlotContext";
import { PerfectTree } from "../ui/PerfectTree";
import { isValidPlaceholder } from "../componentTreeHelper";
import { buildInsertMenuItems, getVisibleInsertOptions, } from "../insertMenuItems";
import { Plus, LayoutTemplate, Pentagon, Folder, Square, ChevronRight, Circle, File, Home, } from "lucide-react";
import { SimpleIconButton } from "../ui/SimpleIconButton";
import { Spinner } from "../ui/Spinner";
import { FilterInput } from "../../components/FilterInput";
export function ComponentTree({ pageViewContext, itemDescriptor, item, isLoading, }) {
    const [expandedKeys, setExpandedKeys] = useState(["page"]);
    const [selectedKeys, setSelectedKeys] = useState(["page"]);
    const [filter, setFilter] = useState("");
    const [rootNodes, setRootNodes] = useState([]);
    const [nodeDictionary, setNodeDictionary] = useState({});
    const [isTreeReady, setIsTreeReady] = useState(false);
    const [treeKey, setTreeKey] = useState(null);
    let editContextRef = useEditContextRef();
    const editContext = useEditContext();
    const slotContext = useSlotContext();
    // Prefer the nearest slot context so multiple visible trees can render the
    // correct page instead of following the globally active slot.
    const page = pageViewContext?.page ?? slotContext?.primaryPageViewContext.page ?? editContext?.page;
    const pageItemDescriptor = itemDescriptor ?? page?.item ?? slotContext?.itemDescriptor ?? editContext?.currentItemDescriptor;
    const resolvedItem = item ?? slotContext?.item ?? editContext?.item;
    const isSlotLoading = isLoading ??
        slotContext?.isLoading ??
        editContext?.getActiveSlotContext?.()?.isLoading ??
        false;
    const pageLanguage = page?.item.language ?? pageItemDescriptor?.language;
    const pageVersion = page?.item.version ?? pageItemDescriptor?.version;
    const treeRef = useRef(null);
    const confirmDialogRef = useRef(null);
    const pendingDragStartTimeoutRef = useRef(null);
    // Helper function to clean placeholder labels by removing _{guid} pattern
    function cleanPlaceholderLabel(label) {
        return label.replace(/_{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}$/, "");
    }
    // Helper function to check if a component has editable descendants
    // function hasEditableDescendants(component: Component): boolean {
    //   if (!component.placeholders) return false;
    //   for (const placeholder of component.placeholders) {
    //     // If placeholder itself is editable, we have editable descendants
    //     if (placeholder.editable) return true;
    //     // Check if any component in this placeholder is editable or has editable descendants
    //     for (const comp of placeholder.components) {
    //       if (comp.editable || hasEditableDescendants(comp)) {
    //         return true;
    //       }
    //     }
    //   }
    //   return false;
    // }
    // Helper function to get all editable descendants, flattening the tree
    function getEditableDescendants(component, parent) {
        const editableNodes = [];
        if (component.editable) {
            // If this component is editable, include it and map its children normally
            const node = mapComponentNode(component, parent);
            editableNodes.push(node);
        }
        else {
            // If this component is not editable, skip it and collect its editable descendants
            if (component.placeholders) {
                for (const placeholder of component.placeholders) {
                    // if (placeholder.editable) {
                    //   const placeholderNode = mapPlaceholderNode(placeholder, parent);
                    //   editableNodes.push(placeholderNode);
                    // } else {
                    for (const childComponent of placeholder.components) {
                        const childEditableNodes = getEditableDescendants(childComponent, parent);
                        editableNodes.push(...childEditableNodes);
                    }
                    // }
                }
            }
        }
        return editableNodes;
    }
    useEffect(() => {
        const container = treeRef.current;
        const selectedNodeKey = selectedKeys[0];
        if (!container || !selectedNodeKey) {
            return;
        }
        let cancelled = false;
        let attempts = 0;
        const maxAttempts = 30;
        const tryScroll = () => {
            if (cancelled)
                return;
            let selectedNodeElement = null;
            try {
                selectedNodeElement = container.querySelector(`[data-node-key="${CSS.escape(selectedNodeKey)}"]`);
            }
            catch {
                selectedNodeElement = container.querySelector(`[data-node-key="${selectedNodeKey}"]`);
            }
            if (selectedNodeElement) {
                const containerRect = container.getBoundingClientRect();
                const nodeRect = selectedNodeElement.getBoundingClientRect();
                const isFullyVisible = nodeRect.top >= containerRect.top &&
                    nodeRect.bottom <= containerRect.bottom;
                if (!isFullyVisible) {
                    selectedNodeElement.scrollIntoView({
                        behavior: "smooth",
                        block: "nearest",
                    });
                }
                return;
            }
            attempts += 1;
            if (attempts < maxAttempts) {
                requestAnimationFrame(tryScroll);
            }
        };
        requestAnimationFrame(tryScroll);
        return () => {
            cancelled = true;
        };
    }, [selectedKeys, nodeDictionary, expandedKeys]);
    useEffect(() => {
        if (editContext?.selection && editContext.selection.length > 0) {
            setSelectedKeys(editContext.selection);
            return;
        }
        // Keep non-component tree selection (e.g. placeholder) when editor selection is empty.
        // This allows users to click and work with placeholders without being reset to "page".
        setSelectedKeys((prev) => {
            if (prev.length === 1) {
                const current = prev[0];
                if (current) {
                    const node = nodeDictionary[current];
                    if (node && node.type !== "component") {
                        return prev;
                    }
                }
            }
            return ["page"];
        });
    }, [editContext?.selection, nodeDictionary]);
    const selection = editContext?.selection || [];
    useEffect(() => {
        const newExpandedKeys = [...expandedKeys];
        for (const id of selection) {
            if (nodeDictionary[id]?.parent) {
                expandParents(nodeDictionary[id].parent, newExpandedKeys);
            }
        }
        if (newExpandedKeys.length !== expandedKeys.length ||
            !newExpandedKeys.every((key, i) => key === expandedKeys[i])) {
            setExpandedKeys(newExpandedKeys);
        }
    }, [selection, nodeDictionary]);
    function appendNodeClassName(node, className) {
        node.className = `${node.className || ""} ${className}`.trim();
    }
    function reparentVisibleNodes(nodes, parent, inheritedClassName) {
        return nodes.map((node) => {
            node.parent = parent;
            if (inheritedClassName) {
                appendNodeClassName(node, inheritedClassName);
            }
            return node;
        });
    }
    function mapVisibleComponentNodes(component, parent, inheritedClassName) {
        const componentNode = mapComponentNode(component, parent);
        if (inheritedClassName) {
            appendNodeClassName(componentNode, inheritedClassName);
        }
        if (!component.layoutId) {
            return [componentNode];
        }
        return reparentVisibleNodes((componentNode.children || []), parent, inheritedClassName);
    }
    function mapPlaceholderNode(p, parent) {
        const rawLabel = p.description || p.name;
        let cleanedLabel = cleanPlaceholderLabel(rawLabel);
        // Improve implicit placeholder labels
        const isImplicit = p.key.startsWith("implicit_");
        if (isImplicit) {
            cleanedLabel = cleanedLabel.replace(/^Implicit placeholder:\s*/i, "Items");
        }
        const node = {
            key: p.key,
            componentId: p.key,
            label: cleanedLabel,
            icon: _jsx(Folder, { className: "h-4 w-4" }),
            data: p,
            parent: parent,
            type: "placeholder",
            className: !p.editable ? "text-gray-400" : "",
            tags: [],
        };
        // Add tag for implicit placeholders
        if (isImplicit) {
            node.tags.push({
                id: "implicit",
                severity: "info",
                value: "Implicit",
                icon: "",
                tooltip: "Implicit placeholder for multi-component editor",
                color: "rgb(59, 130, 246)", // blue-500
            });
        }
        if (!editContext?.showLayoutComponents) {
            // Hide layout nodes, but keep their visible descendants in the tree.
            node.children = p.components.flatMap((component) => mapVisibleComponentNodes(component, node));
        }
        else {
            // Always map components, but filter based on editability and descendants
            const mappedComponents = p.components.map((c) => mapComponentNode(c, node));
            node.children = mappedComponents.filter((componentNode) => {
                const component = componentNode.data;
                return component.editable || true; //hasEditableDescendants(component);
            });
        }
        return node;
    }
    function mapPlaceholders(c, parent) {
        if (!c?.placeholders) {
            return [];
        }
        const placeholdersToShow = editContext?.showLayoutComponents
            ? c.placeholders
            : c.placeholders.filter((p) => {
                // Show placeholder if it's editable or has editable descendants.
                // Implicit placeholders are intentionally included here so their
                // components are visible when placeholders are flattened.
                if (p.editable)
                    return true;
                return p.components.some((comp) => comp.editable || true);
            });
        if (!editContext?.showLayoutComponents) {
            // When not showing layout components, always flatten placeholders (hide placeholder nodes)
            // Collect all components from all placeholders
            const allComponents = [];
            const seenKeys = new Set();
            for (const placeholder of placeholdersToShow) {
                const placeholderClassName = !placeholder.editable
                    ? "text-gray-400"
                    : undefined;
                const visibleComponents = placeholder.components.flatMap((component) => mapVisibleComponentNodes(component, parent, placeholderClassName));
                // Add only components we haven't seen yet to prevent duplicate keys
                for (const componentNode of visibleComponents) {
                    if (!seenKeys.has(componentNode.key)) {
                        seenKeys.add(componentNode.key);
                        allComponents.push(componentNode);
                    }
                }
            }
            return allComponents;
        }
        else {
            // When showing layout components, ALWAYS show placeholder nodes (don't flatten)
            // This shows the complete structure including all placeholders
            return placeholdersToShow.map((x) => mapPlaceholderNode(x, parent));
        }
        return [];
    }
    function mapComponentNode(c, parent) {
        const node = {
            key: c.id,
            componentId: c.id,
            label: c.name,
            icon: c.datasourceItem?.icon ? (_jsx("img", { src: c.datasourceItem.icon, width: 16, height: 16 })) : c.icon ? (_jsx("img", { src: c.icon, width: 16, height: 16 })) : (_jsx(Square, { className: "h-4 w-4" })),
            data: c,
            parent: parent,
            isDraggable: true,
            tags: [],
            className: c.isRemovedFromMasterLanguage || !c.editable ? "text-gray-400" : "",
            type: "component",
        };
        if (c.isInheritedFromMasterLanguage) {
            node.tags.push({
                id: "inherited",
                severity: "info",
                value: "Inherited " + (c.isRemovedFromMasterLanguage ? " / removed" : ""),
                tooltip: c.isRemovedFromMasterLanguage
                    ? "Component was removed from master language"
                    : "Component is inherited from master language",
                icon: _jsx(ChevronRight, { className: "h-3 w-3" }),
            });
        }
        if (c.layoutId) {
            node.tags.push({
                id: "layout",
                severity: "warning",
                value: "Layout",
                icon: _jsx(ChevronRight, { className: "h-3 w-3" }),
                color: "rgb(192 132 252)",
                tooltip: "Component is inherited from layout. Click to edit layout.",
                onClick: async () => {
                    if (!editContext || !c.layoutId)
                        return;
                    const hostItem = await editContext.itemsRepository.getItem({
                        id: c.layoutId,
                        language: page.item.language,
                        version: 0,
                    });
                    if (!hostItem)
                        return;
                    confirmDialogRef.current?.confirm({
                        header: "Edit layout",
                        message: "Edit layout " + hostItem.path + "?",
                        accept: () => editContext.loadItem({
                            id: c.layoutId,
                            language: page.item.language,
                            version: 0,
                        }),
                    });
                },
            });
        }
        if (c.isSharedLayout) {
            node.tags.push({
                id: "shared-layout",
                severity: "warning",
                value: "",
                icon: _jsx(Pentagon, { className: "h-3 w-3 text-gray-400", strokeWidth: 1 }),
                tooltip: "Component is shared across all languages",
            });
        }
        if (c.isShared) {
            node.tags.push({
                id: "linked",
                severity: "warning",
                value: "Shared",
                icon: "",
                tooltip: "Component is linked to " + c.datasourceItem?.id,
                color: "rgb(251, 146, 60)",
            });
        }
        if (c.type && c.type !== c.name) {
            node.tags.push({
                id: "rendering",
                severity: "info",
                value: c.type,
                icon: "",
                tooltip: "Rendering",
                color: "rgb(107, 114, 128)",
            });
        }
        node.children = mapPlaceholders(c, node);
        if (c.items.length > 0 &&
            c.items[0].id !== c.datasourceItem?.id &&
            c.items[0].id !== page?.item.id) {
            const itemsNode = {
                key: "items" + c.id,
                label: "items",
                icon: _jsx(Circle, { className: "h-4 w-4" }),
                parent: node,
                type: "items",
            };
            itemsNode.children = c.items.map((x) => {
                const itemNode = {
                    key: `${c.id}-item-${x.id}`,
                    label: x.name,
                    icon: _jsx(File, { className: "h-4 w-4" }),
                    parent: itemsNode,
                    type: "item",
                };
                return itemNode;
            });
            node.children.push(itemsNode);
        }
        return node;
    }
    useEffect(() => {
        const nextKey = page?.item
            ? `${page.item.id}-${page.item.language}-${page.item.version}`
            : null;
        if (treeKey !== nextKey) {
            setTreeKey(nextKey);
            setIsTreeReady(false);
        }
    }, [page?.item?.id, page?.item?.language, page?.item?.version, treeKey]);
    useEffect(() => {
        if (!page)
            return;
        if (!page.rootComponent) {
            return;
        }
        const treeNodes = mapPlaceholders(page.rootComponent, {
            key: "root",
            label: "Root",
            icon: _jsx(Home, { className: "h-4 w-4" }),
            children: [],
            type: "placeholder",
        });
        const dict = {};
        createMap(treeNodes, dict);
        const otherNode = {
            key: "other",
            label: "Other",
            icon: _jsx(File, { className: "h-4 w-4" }),
            children: [],
            type: "other",
        };
        if (otherNode.children?.length) {
            treeNodes.push(otherNode);
        }
        const pageNode = {
            key: "page",
            label: "Page",
            icon: _jsx(Home, { className: "h-4 w-4" }),
            parent: undefined,
            componentId: page.item.id,
            type: "page",
            children: treeNodes,
        };
        // Add pageNode to dictionary so getSiblingsForParent can find its children
        dict["page"] = pageNode;
        // Update parent references for root nodes
        treeNodes.forEach((node) => {
            node.parent = pageNode;
        });
        const finalNodes = [pageNode];
        setRootNodes(finalNodes);
        setNodeDictionary(dict);
        setIsTreeReady(true);
    }, [page, editContext?.showLayoutComponents]);
    const { visibleRootNodes, effectiveExpandedKeys, isFiltering } = useMemo(() => {
        const query = filter.trim();
        if (!query) {
            return {
                visibleRootNodes: rootNodes,
                effectiveExpandedKeys: expandedKeys,
                isFiltering: false,
            };
        }
        const filteredTree = filterComponentTreeNodes(rootNodes, query);
        return {
            visibleRootNodes: filteredTree.nodes,
            effectiveExpandedKeys: filteredTree.expandedKeys,
            isFiltering: true,
        };
    }, [expandedKeys, filter, rootNodes]);
    const handleTreeSelection = useCallback((key, event) => {
        let newKeys = [];
        // Helper: gather visible component keys using current tree data and expanded state
        const getVisibleComponentKeys = () => {
            const keys = [];
            const visit = (nodes) => {
                for (const n of nodes) {
                    if (n.type === "component")
                        keys.push(n.key);
                    if (n.children && effectiveExpandedKeys.includes(n.key)) {
                        visit(n.children);
                    }
                }
            };
            visit(visibleRootNodes);
            return keys;
        };
        if (event.shiftKey) {
            const anchorKey = selectedKeys && selectedKeys.length > 0
                ? selectedKeys[selectedKeys.length - 1]
                : undefined;
            const orderedKeys = getVisibleComponentKeys();
            const aIdx = anchorKey ? orderedKeys.indexOf(anchorKey) : -1;
            const bIdx = orderedKeys.indexOf(key);
            if (aIdx !== -1 && bIdx !== -1) {
                const start = Math.min(aIdx, bIdx);
                const end = Math.max(aIdx, bIdx);
                const rangeKeys = orderedKeys.slice(start, end + 1);
                if (event.ctrlKey) {
                    const union = new Set([
                        ...selectedKeys.filter((k) => nodeDictionary[k]?.type === "component"),
                        ...rangeKeys,
                    ]);
                    newKeys = Array.from(union);
                }
                else {
                    newKeys = rangeKeys;
                }
            }
            else {
                // Fallback to single selection when anchor or target not found
                newKeys = [key];
            }
        }
        else if (event.ctrlKey) {
            if (selectedKeys.includes(key)) {
                newKeys = selectedKeys.filter((x) => x !== key);
            }
            else {
                newKeys = [...selectedKeys, key];
            }
        }
        else {
            newKeys = [key];
        }
        setSelectedKeys(newKeys);
        const selectedComponentIds = Array.from(new Set(newKeys
            .filter((k) => nodeDictionary[k]?.type === "component")
            .map((k) => nodeDictionary[k].componentId)));
        editContextRef.current?.select(selectedComponentIds);
        if (selectedComponentIds.length === 1) {
            editContextRef.current?.setScrollIntoView(selectedComponentIds[0]);
        }
    }, [selectedKeys, nodeDictionary, editContextRef, visibleRootNodes, effectiveExpandedKeys]);
    // Handle tree context menu
    const handleTreeContextMenu = async (node, event) => {
        const componentId = nodeDictionary[node.key]?.componentId;
        let selectedComponentIds = editContext?.selection || [];
        if (componentId) {
            if (selectedComponentIds.indexOf(componentId) < 0) {
                editContextRef.current?.select([componentId]);
                selectedComponentIds = [componentId];
            }
        }
        function isDefinedEntity(entity) {
            return entity !== undefined;
        }
        let selectedEntities = selectedComponentIds
            .filter((key) => nodeDictionary[key])
            .map((key) => nodeDictionary[key].data)
            .filter(isDefinedEntity);
        if (selectedEntities.length === 0 && nodeDictionary[node.key]?.data) {
            selectedEntities = [nodeDictionary[node.key].data];
        }
        let commands = await getComponentCommands(selectedEntities, editContextRef.current);
        const menu = commands.map((x) => ({
            id: x.id,
            label: x.label,
            icon: x.icon,
            disabled: editContextRef.current?.isCommandDisabled({ command: x }),
            command: async (ev) => {
                await editContextRef.current?.executeCommand({
                    command: x,
                    data: { components: selectedEntities },
                    event: event,
                });
            },
        }));
        if (editContext?.mode === "edit") {
            // Check if we have a placeholder to add insert options
            const placeholder = getPlaceholder(nodeDictionary[node.key] || null);
            if (placeholder && placeholder.insertOptions) {
                const insertOptions = getVisibleInsertOptions(placeholder.insertOptions);
                const insertMenuItems = buildInsertMenuItems(insertOptions, async (x) => {
                    await editContextRef.current?.operations.addComponent(x.typeId, placeholder.key, 0, // Insert at the beginning by default
                    pageItemDescriptor);
                });
                if (insertMenuItems.length > 0) {
                    // Add separator if there are existing menu items
                    if (menu.length > 0) {
                        menu.push({ id: "separator-insert", separator: true });
                    }
                    menu.push({
                        id: "insert",
                        label: "Insert",
                        icon: _jsx(Plus, { size: 16 }),
                        items: insertMenuItems,
                    });
                }
            }
        }
        editContextRef.current?.showContextMenu(event, menu);
    };
    // Convert expanded keys for PerfectTree
    const expandedItems = effectiveExpandedKeys;
    // Convert selected keys for PerfectTree
    const selectedItems = selectedKeys;
    const handleCollapseItem = useCallback((key) => {
        // First, get the node we're collapsing
        const nodeToCollapse = nodeDictionary[key];
        // Create a new array for expanded keys
        const newExpandedKeys = [...expandedKeys].filter((x) => x !== key);
        setExpandedKeys(newExpandedKeys);
    }, [expandedKeys, nodeDictionary]);
    // Handle expanding nodes
    const handleToggle = useCallback((key) => {
        if (expandedKeys.includes(key)) {
            handleCollapseItem(key);
        }
        else {
            const newExpandedKeys = [...expandedKeys];
            newExpandedKeys.push(key);
            setExpandedKeys(newExpandedKeys);
        }
    }, [expandedKeys, handleCollapseItem]);
    // Handle collapsing nodes
    const getPlaceholder = (node) => {
        if (!node)
            return null;
        if (node.type === "placeholder") {
            const placeholder = node.data;
            return placeholder.editable ? placeholder : null;
        }
        if (node.type === "component" && node.data.placeholders) {
            const editablePlaceholders = node.data.placeholders.filter((p) => p.editable);
            if (editablePlaceholders.length === 1)
                return editablePlaceholders[0];
        }
        return null;
    };
    // Resolve placeholder and correct index for drop zones even when placeholders are hidden/flattened
    const getSiblingsForParent = (parent) => {
        if (!parent)
            return rootNodes || [];
        const parentNode = nodeDictionary[parent.key];
        return (parentNode?.children ||
            []);
    };
    const findNearestComponentNeighbor = (siblings, index) => {
        // Prefer right neighbor
        for (let i = index; i < siblings.length; i++) {
            if (siblings[i]?.type === "component")
                return siblings[i];
        }
        // Fallback to left neighbor
        for (let j = index - 1; j >= 0; j--) {
            if (siblings[j]?.type === "component")
                return siblings[j];
        }
        return null;
    };
    const resolveDropContext = (dragOverParent, index) => {
        // Direct placeholder parent
        if (dragOverParent && dragOverParent.type === "placeholder") {
            const placeholder = dragOverParent.data;
            return {
                placeholder: placeholder.editable ? placeholder : null,
                indexInPlaceholder: Math.max(0, index),
            };
        }
        // Single editable placeholder on a component
        if (dragOverParent && dragOverParent.type === "component") {
            const component = dragOverParent.data;
            const editablePlaceholders = (component.placeholders || []).filter((p) => p.editable);
            const siblings = getSiblingsForParent(dragOverParent);
            if (editablePlaceholders.length === 1) {
                const ph = editablePlaceholders[0];
                // Count how many siblings in this placeholder appear before the zone
                const indexInPlaceholder = siblings
                    .slice(0, Math.max(0, index))
                    .filter((n) => n.type === "component" &&
                    n.data?.parentPlaceholder?.key === ph.key).length;
                return { placeholder: ph, indexInPlaceholder };
            }
            // Multiple placeholders: infer from nearest neighbor component
            const neighbor = findNearestComponentNeighbor(siblings, Math.max(0, index));
            const ph = neighbor?.data?.parentPlaceholder;
            if (ph && ph.editable) {
                const indexInPlaceholder = siblings
                    .slice(0, Math.max(0, index))
                    .filter((n) => n.type === "component" &&
                    n.data?.parentPlaceholder?.key === ph.key).length;
                return { placeholder: ph, indexInPlaceholder };
            }
            return { placeholder: null, indexInPlaceholder: 0 };
        }
        // Page node parent: treat like root-level but use page's children
        if (dragOverParent && dragOverParent.type === "page") {
            const siblings = getSiblingsForParent(dragOverParent);
            const neighbor = findNearestComponentNeighbor(siblings, Math.max(0, index));
            const ph = neighbor?.data?.parentPlaceholder;
            if (ph && ph.editable) {
                const indexInPlaceholder = siblings
                    .slice(0, Math.max(0, index))
                    .filter((n) => n.type === "component" &&
                    n.data?.parentPlaceholder?.key === ph.key).length;
                return { placeholder: ph, indexInPlaceholder };
            }
            return { placeholder: null, indexInPlaceholder: 0 };
        }
        // Root-level (no parent): infer from neighbors among root nodes
        const siblings = getSiblingsForParent(null);
        const neighbor = findNearestComponentNeighbor(siblings, Math.max(0, index));
        const ph = neighbor?.data?.parentPlaceholder;
        if (ph && ph.editable) {
            const indexInPlaceholder = siblings
                .slice(0, Math.max(0, index))
                .filter((n) => n.type === "component" &&
                n.data?.parentPlaceholder?.key === ph.key).length;
            return { placeholder: ph, indexInPlaceholder };
        }
        return { placeholder: null, indexInPlaceholder: 0 };
    };
    // Handle drag over node (memoized)
    const handleDragOverZone = useCallback((dragOverNode, index, event) => {
        if (!editContext?.dragObject)
            return false;
        const { placeholder } = resolveDropContext(dragOverNode, index);
        if (!placeholder)
            return false;
        const isValid = isValidPlaceholder(placeholder, editContext.dragObject);
        event.dataTransfer.dropEffect = isValid ? "move" : "none";
        return isValid;
    }, [editContext?.dragObject]);
    // Handle drop on node (memoized)
    const handleDropZone = useCallback((droppedOnNode, index, event) => {
        // When dropping on a node (index < 0), use legacy behavior
        if (index < 0) {
            const placeholder = getPlaceholder(droppedOnNode);
            if (!placeholder)
                return;
            editContext.droppedInPlaceholder(placeholder.key, index);
            return;
        }
        const { placeholder, indexInPlaceholder } = resolveDropContext(droppedOnNode, index);
        if (!placeholder)
            return;
        editContext.droppedInPlaceholder(placeholder.key, indexInPlaceholder);
    }, [editContext]);
    // Memoize isValidDropZone callback
    const handleIsValidDropZone = useCallback((parent, index) => {
        const { placeholder } = resolveDropContext(parent, index);
        if (!placeholder)
            return false;
        if (!editContext?.dragObject)
            return false;
        const result = isValidPlaceholder(placeholder, editContext.dragObject);
        return result;
    }, [editContext?.dragObject]);
    // Memoize onStartDrag callback
    const handleStartDrag = useCallback((data) => {
        const component = data.node.data;
        // Initialize drag data to make sure dataTransfer is set
        data.event.dataTransfer.setData("text/plain", data.node.key);
        // Align with FrameMenu: include componentId and allowed effect
        data.event.dataTransfer.setData("componentId", component.id);
        data.event.dataTransfer.effectAllowed = "copyMove";
        // Only create drag object for components with datasourceItem
        if (!component?.datasourceItem)
            return;
        pendingDragStartTimeoutRef.current = setTimeout(() => {
            if (!pageLanguage || pageVersion == null)
                return;
            const language = pageLanguage;
            const version = pageVersion;
            const selectedIds = (editContext?.selection?.length || 0) > 1 &&
                editContext.selection.includes(component.id)
                ? editContext.selection
                : [component.id];
            const componentsToDrag = selectedIds.map((id) => ({
                id,
                language,
                version,
            }));
            editContext.dragStart({
                type: "component",
                typeId: component.typeId,
                templateId: component.datasourceItem?.templateId,
                name: component.name,
                components: componentsToDrag,
            });
        }, 50);
        // Prevent any upstream handlers from interfering
        data.event.stopPropagation();
    }, [editContext]);
    // Memoize onDragEnd callback
    const handleDragEnd = useCallback((event) => {
        if (pendingDragStartTimeoutRef.current) {
            clearTimeout(pendingDragStartTimeoutRef.current);
            pendingDragStartTimeoutRef.current = null;
        }
        editContext.dragEnd();
    }, [editContext]);
    // Cleanup pending drag start timeout on unmount
    useEffect(() => {
        return () => {
            if (pendingDragStartTimeoutRef.current) {
                clearTimeout(pendingDragStartTimeoutRef.current);
                pendingDragStartTimeoutRef.current = null;
            }
        };
    }, []);
    // Check if we have a valid page model
    const hasPageModel = page && page.rootComponent && page.item;
    const isRefreshing = editContext?.isRefreshing ?? false;
    // Check if the page model failed to load (invalid page model)
    // Only show error if page model is missing, not if page is just empty (no components)
    const hasInvalidPageModel = !hasPageModel;
    // Check if the tree is empty (no components/placeholders) - this is valid, not an error
    const hasEmptyTree = !!hasPageModel &&
        (!page?.rootComponent?.placeholders ||
            page.rootComponent.placeholders.length === 0) &&
        rootNodes.length <= 1 &&
        (rootNodes.length === 0 || rootNodes[0]?.key === "page");
    if (!page) {
        // If item has no versions, show appropriate message instead of spinner
        if (resolvedItem?.versions === 0) {
            return (_jsx("div", { className: "flex h-full items-center justify-center text-xs text-gray-500", children: "Item has no versions." }));
        }
        if (resolvedItem?.hasLayout || isSlotLoading || isRefreshing) {
            return (_jsx("div", { className: "flex h-full items-center justify-center text-xs text-gray-500", children: _jsx(Spinner, {}) }));
        }
        return (_jsx("div", { className: "flex h-full items-center justify-center text-xs text-gray-500", children: "This item has no layout." }));
    }
    // If the page model is incomplete but we're still loading, show a spinner instead of error
    if (hasInvalidPageModel && (!isTreeReady || isSlotLoading || isRefreshing)) {
        return (_jsx("div", { className: "flex h-full items-center justify-center text-xs text-gray-500", children: _jsx(Spinner, {}) }));
    }
    const canWriteItem = page?.item.canWriteItem ??
        resolvedItem?.canWriteItem ??
        false;
    // Get insert mode state for toggle button
    const insertMode = editContext?.insertMode ?? false;
    // Toggle insert mode function
    const toggleInsertMode = () => {
        if (!editContext || editContext.readonly || editContext.mode !== "edit")
            return;
        editContext.setInsertMode((x) => !x);
    };
    // Only show error if page model failed to load (not if page is just empty)
    if (hasInvalidPageModel && isTreeReady && !isSlotLoading && !isRefreshing) {
        return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 px-2 py-1", children: [_jsx("div", { className: "flex items-center gap-1", children: _jsx(SimpleIconButton, { icon: _jsx(Plus, { strokeWidth: 1, className: "h-4 w-4" }), label: "Add component", size: "small", "data-testid": "component-tree-add-component", selected: insertMode, disabled: !canWriteItem ||
                                    !editContext ||
                                    editContext.readonly ||
                                    editContext.mode !== "edit", onClick: toggleInsertMode }) }), _jsx("div", { className: "flex items-center gap-1", children: _jsx(SimpleIconButton, { icon: _jsx(LayoutTemplate, { strokeWidth: 1, className: "h-4 w-4" }), label: editContext?.showLayoutComponents
                                    ? "Hide layout"
                                    : "Show layout", size: "small", selected: editContext?.showLayoutComponents, "data-testid": "component-tree-toggle-layout", onClick: () => editContext?.setShowLayoutComponents(!editContext.showLayoutComponents) }) })] }), _jsx("div", { className: "flex flex-1 items-center justify-center text-xs text-gray-500", children: "Page did not load. Please refresh or try again." }), _jsx(ConfirmationDialog, { ref: confirmDialogRef })] }));
    }
    return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 px-2 py-1", children: [_jsx("div", { className: "flex items-center gap-1", children: _jsx(SimpleIconButton, { icon: _jsx(Plus, { strokeWidth: 1, className: "h-4 w-4" }), label: "Add component", size: "small", "data-testid": "component-tree-add-component", selected: insertMode, disabled: !canWriteItem ||
                                !editContext ||
                                editContext.readonly ||
                                editContext.mode !== "edit", onClick: toggleInsertMode }) }), _jsx("div", { className: "flex items-center gap-1", children: _jsx(SimpleIconButton, { icon: _jsx(LayoutTemplate, { strokeWidth: 1, className: "h-4 w-4" }), label: editContext?.showLayoutComponents ? "Hide layout" : "Show layout", size: "small", selected: editContext?.showLayoutComponents, "data-testid": "component-tree-toggle-layout", onClick: () => editContext?.setShowLayoutComponents(!editContext.showLayoutComponents) }) })] }), _jsx("div", { className: "border-b border-gray-200 p-2", "data-testid": "component-tree-search", children: _jsx(FilterInput, { value: filter, onChange: setFilter, className: "w-full", placeholder: "Search IDs, names, types..." }) }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto overflow-x-hidden p-2", ref: treeRef, "data-testid": "component-tree", children: isFiltering && visibleRootNodes.length === 0 ? (_jsxs("div", { className: "flex h-full items-center justify-center px-4 text-center text-xs text-gray-500", children: ["No components match \"", filter.trim(), "\"."] })) : (_jsx(PerfectTree, { nodes: visibleRootNodes, isDragging: !isFiltering && !!editContext?.dragObject, multiDragNoun: "components", selectedKeys: selectedItems, expandedKeys: expandedItems, onToggleExpand: isFiltering ? undefined : handleToggle, onSelect: handleTreeSelection, onContextMenu: handleTreeContextMenu, enableDragAndDrop: !isFiltering, onDragOverZone: handleDragOverZone, isValidDropZone: handleIsValidDropZone, onStartDrag: handleStartDrag, onDragEnd: handleDragEnd, onDrop: handleDropZone, renderNode: (node) => renderNode(node, filter) })) }), _jsx(ConfirmationDialog, { ref: confirmDialogRef })] }));
}
function renderNode(node, filter = "") {
    const trimmedFilter = filter.trim();
    const filterDetails = trimmedFilter ? getNodeFilterDetails(node) : null;
    return (_jsxs("div", { className: `component-tree-node ${node.type ? `component-tree-${node.type}` : ""}`, "data-component-node": node.type === "component" ? "true" : undefined, "data-testid": "component-tree-node", "data-component-key": node.key, "data-component-type": node.type, children: [_jsxs("div", { className: "flex items-center gap-2 text-[12px] text-gray-600", children: [typeof node.icon === "string" ? (_jsx("i", { className: node.icon })) : (node.icon), _jsx("span", { className: node.className, "data-testid": "component-tree-node-label", children: highlightSearchMatch(node.label, trimmedFilter) }), node.tags?.map((x) => (_jsx("div", { children: _jsxs("span", { id: `id-${node.key}-${x.id}`, className: "ml-2 flex rounded p-0.5 px-1.5 text-xs text-white", style: {
                                backgroundColor: x.color,
                                marginLeft: "8px",
                                fontSize: "10px",
                            }, title: x.tooltip, onClick: x.onClick
                                ? (e) => {
                                    e.stopPropagation();
                                    x.onClick();
                                }
                                : undefined, children: [x.icon, highlightSearchMatch(x.value, trimmedFilter)] }) }, `tag-${node.key}-${x.id}`)))] }), filterDetails ? (_jsx("div", { className: "ml-6 mt-0.5 truncate text-[10px] text-gray-400", title: filterDetails, children: highlightSearchMatch(filterDetails, trimmedFilter) })) : null] }));
}
function highlightSearchMatch(text, filter) {
    if (!filter.trim())
        return text;
    const regex = new RegExp(`(${filter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
    return text.split(regex).map((part, index) => part.toLowerCase() === filter.toLowerCase() && part ? (_jsx("span", { className: "bg-yellow-200/80", children: part }, `${part}-${index}`)) : (part));
}
function getNodeFilterDetails(node) {
    if (node.type === "component" && node.data) {
        const component = node.data;
        return [
            component.id ? `ID: ${component.id}` : "",
            component.type ? `Type: ${component.type}` : "",
            component.typeId ? `Type ID: ${component.typeId}` : "",
            component.datasourceItem?.name
                ? `Datasource: ${component.datasourceItem.name}`
                : "",
            component.parentPlaceholder?.key
                ? `Placeholder: ${component.parentPlaceholder.key}`
                : "",
        ]
            .filter(Boolean)
            .join(" | ");
    }
    if (node.type === "placeholder" && node.data) {
        const placeholder = node.data;
        return [
            placeholder.key ? `Key: ${placeholder.key}` : "",
            placeholder.description
                ? `Description: ${placeholder.description}`
                : "",
        ]
            .filter(Boolean)
            .join(" | ");
    }
    return node.componentId ? `ID: ${node.componentId}` : null;
}
function createMap(treeNodes, nodeDictionary) {
    treeNodes.forEach((n) => {
        if (n.key)
            nodeDictionary[n.key] = n;
        if (n.children)
            createMap(n.children, nodeDictionary);
    });
}
function expandParents(node, expandedKeysList) {
    if (!node)
        return;
    if (node.key && !expandedKeysList.includes(node.key)) {
        expandedKeysList.push(node.key);
    }
    if (node.parent) {
        expandParents(node.parent, expandedKeysList);
    }
}
function hasSameProps(obj1, obj2) {
    var obj1Props = Object.keys(obj1), obj2Props = Object.keys(obj2);
    if (obj1Props.length == obj2Props.length) {
        return obj1Props.every(function (prop) {
            return obj2Props.indexOf(prop) >= 0;
        });
    }
    return false;
}
function addSearchValue(values, value) {
    if (!value)
        return;
    const trimmed = value.trim();
    if (trimmed) {
        values.push(trimmed);
    }
}
function getNodeSearchText(node) {
    const values = [];
    addSearchValue(values, node.label);
    addSearchValue(values, getNodeFilterDetails(node));
    addSearchValue(values, node.type);
    node.tags?.forEach((tag) => {
        addSearchValue(values, tag.id);
        addSearchValue(values, tag.value);
    });
    return values.join(" ").toLowerCase();
}
function filterComponentTreeNodes(nodes, filter) {
    const normalizedFilter = filter.trim().toLowerCase();
    if (!normalizedFilter) {
        return { nodes, expandedKeys: [] };
    }
    const expandedKeys = new Set();
    const filterNode = (node, ancestors) => {
        const filteredChildren = Array.isArray(node.children)
            ? node.children
                .map((child) => filterNode(child, [...ancestors, node.key]))
                .filter((child) => child !== null)
            : node.children;
        const hasMatchingChildren = Array.isArray(filteredChildren) && filteredChildren.length > 0;
        const nodeMatches = getNodeSearchText(node).includes(normalizedFilter);
        if (!nodeMatches && !hasMatchingChildren) {
            return null;
        }
        ancestors.forEach((ancestorKey) => expandedKeys.add(ancestorKey));
        if (hasMatchingChildren) {
            expandedKeys.add(node.key);
        }
        return {
            ...node,
            children: Array.isArray(node.children)
                ? filteredChildren
                : node.children,
        };
    };
    return {
        nodes: nodes
            .map((node) => filterNode(node, []))
            .filter((node) => node !== null),
        expandedKeys: Array.from(expandedKeys),
    };
}
//# sourceMappingURL=ComponentTree.js.map