import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Splitter, } from "../ui/Splitter";
import { useEditContext } from "../client/editContext";
import { useFieldModification } from "../client/fieldModificationStore";
import { useSlotContext } from "../views/editorSlotContext";
import { useEffect, useRef, useState } from "react";
import { getLookupSources } from "../services/editService";
import { SimpleIconButton } from "../ui/SimpleIconButton";
import { normalizeGuid } from "../utils";
import ItemSearch from "../ui/ItemSearch";
import { Button } from "../../components/ui/button";
import { ChevronLeft, ChevronRight, ExternalLink, GripVertical, } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
import ContentTree from "../ContentTree";
import { localStorageService } from "../services/localStorageService";
const SELECTED_LIST_DRAG_TYPE = "application/x-parhelia-treelist-selected-index";
export default function TreeListEditor({ field, readOnly, fillAvailableHeight = false, }) {
    const editContext = useEditContext();
    const slotContext = useSlotContext();
    const [selectedItemNodesInTree, setSelectedItemNodesInTree] = useState([]);
    const [selectedFromListIds, setSelectedFromListIds] = useState([]);
    const [activeOpenButtonId, setActiveOpenButtonId] = useState("");
    const [rootItemIds, setRootItemIds] = useState([]);
    const [database, setDatabase] = useState("master");
    const [values, setValues] = useState([]);
    const [pendingAddedItems, setPendingAddedItems] = useState({});
    const [pendingRemovedIds, setPendingRemovedIds] = useState({});
    const [hoveredItemId, setHoveredItemId] = useState();
    const [hoveredItem, setHoveredItem] = useState();
    const hoveredItemCacheRef = useRef({});
    const [draggedIndex, setDraggedIndex] = useState(null);
    const [dropIndicator, setDropIndicator] = useState(null);
    const [lastClickedIndex, setLastClickedIndex] = useState(null);
    const [isResizingHeight, setIsResizingHeight] = useState(false);
    const [isDragOverList, setIsDragOverList] = useState(false);
    const [isDragOverTree, setIsDragOverTree] = useState(false);
    // Default height: 240px (h-60 = 15rem = 240px)
    const defaultHeight = 240;
    const minHeight = 150;
    const maxHeight = 800;
    const [editorHeight, setEditorHeight] = useState(() => {
        const stored = localStorageService.getString("editor.treeListEditorHeight");
        if (stored) {
            const parsed = parseInt(stored, 10);
            if (!isNaN(parsed) && parsed >= minHeight && parsed <= maxHeight) {
                return parsed;
            }
        }
        return defaultHeight;
    });
    // Track if the field is currently dirty (has local unsaved edits)
    const { isModified, isSaved } = useFieldModification(field.id, field.descriptor.item.id, field.descriptor.item.language, field.descriptor.item.version, slotContext?.fieldModificationStore);
    if (!editContext)
        return;
    // Helper: ensure unique items by normalized id while preserving order (first wins)
    const dedupeById = (items) => {
        const seen = new Set();
        const result = [];
        for (const it of items) {
            const key = normalizeGuid(it.id);
            if (!seen.has(key)) {
                seen.add(key);
                result.push(it);
            }
        }
        return result;
    };
    // Save height to localStorage when it changes
    useEffect(() => {
        localStorageService.setString("editor.treeListEditorHeight", editorHeight.toString());
    }, [editorHeight]);
    // Click outside handler to reset active button
    useEffect(() => {
        const handleClickOutside = () => {
            setActiveOpenButtonId("");
        };
        if (activeOpenButtonId) {
            document.addEventListener("click", handleClickOutside);
            return () => document.removeEventListener("click", handleClickOutside);
        }
    }, [activeOpenButtonId]);
    useEffect(() => {
        const loadItem = async () => {
            if (!hoveredItemId)
                return;
            const normalizedId = normalizeGuid(hoveredItemId);
            // Check cache first to avoid redundant requests
            const cached = hoveredItemCacheRef.current[normalizedId];
            if (cached) {
                setHoveredItem(cached);
                return;
            }
            const item = await editContext.itemsRepository.getItem({
                id: hoveredItemId,
                language: editContext.currentItemDescriptor?.language ?? "en",
                version: 0,
                database: database || "master",
            });
            if (item) {
                hoveredItemCacheRef.current[normalizedId] = item;
                setHoveredItem(item);
            }
        };
        loadItem();
    }, [hoveredItemId, database, editContext]);
    useEffect(() => {
        // Ignore server payloads while the field is dirty to avoid race-condition duplicates
        if (isModified && !isSaved)
            return;
        // Reconcile server updates with any local optimistic additions still in-flight
        const serverValues = field.value || [];
        const serverIdsNormalized = new Set(serverValues.map((x) => normalizeGuid(x.id)));
        // Clear pending items that are now present on the server (compare by normalized id)
        let pendingChanged = false;
        const nextPending = {
            ...pendingAddedItems,
        };
        for (const key of Object.keys(nextPending)) {
            if (serverIdsNormalized.has(key)) {
                delete nextPending[key];
                pendingChanged = true;
            }
        }
        // Clear pending-removed ids that the server no longer includes
        let removedChanged = false;
        const nextRemoved = { ...pendingRemovedIds };
        for (const key of Object.keys(nextRemoved)) {
            if (!serverIdsNormalized.has(key)) {
                delete nextRemoved[key];
                removedChanged = true;
            }
        }
        // Build merged list, deduping by normalized id while preserving order (server wins)
        const mergedList = dedupeById([
            ...serverValues,
            ...Object.values(pendingAddedItems),
        ]);
        const filtered = mergedList.filter((x) => !nextRemoved[normalizeGuid(x.id)]);
        setValues(filtered);
        if (pendingChanged)
            setPendingAddedItems(nextPending);
        if (removedChanged)
            setPendingRemovedIds(nextRemoved);
    }, [field.value, pendingAddedItems, pendingRemovedIds, isModified, isSaved]);
    useEffect(() => {
        const loadLookupSources = async () => {
            const response = await getLookupSources(field);
            if (response.items.length) {
                setRootItemIds(response.items.map((x) => x.id));
            }
            else
                setRootItemIds(["{11111111-1111-1111-1111-111111111111}"]);
            setDatabase(response.database || "master");
        };
        loadLookupSources();
    }, [field.id, field.descriptor.item.id]);
    const trimPath = (path) => {
        const splitPath = path.split("/");
        if (splitPath.length > 2) {
            return (splitPath[splitPath.length - 2] + "/" + splitPath[splitPath.length - 1]);
        }
        return path;
    };
    async function removeFromList(items) {
        const idsToRemove = new Set(items.map((i) => normalizeGuid(i.id)));
        const newValues = values.filter((x) => !idsToRemove.has(normalizeGuid(x.id)));
        setValues(dedupeById(newValues));
        // Mask removed items locally until server acknowledges
        if (items.length) {
            setPendingRemovedIds((prev) => {
                const next = { ...prev };
                for (const item of items)
                    next[normalizeGuid(item.id)] = true;
                return next;
            });
        }
        // Cancel any optimistic additions for items being removed
        if (items.length) {
            setPendingAddedItems((prev) => {
                const next = { ...prev };
                for (const item of items)
                    delete next[normalizeGuid(item.id)];
                return next;
            });
        }
        await editContext?.operations.editField({
            field: field.descriptor,
            value: newValues,
            rawValue: newValues.map((x) => normalizeGuid(x.id)).join("|"),
            refresh: "waitForQuietPeriod",
            originatingSlotId: slotContext?.slotId,
        });
    }
    async function addToList(itemNodes) {
        setSelectedItemNodesInTree([]);
        const newValues = [...values];
        const addedNow = [];
        itemNodes.forEach((node) => {
            const nodeKey = normalizeGuid(node.id);
            const exists = newValues.some((x) => normalizeGuid(x.id) === nodeKey);
            if (!exists) {
                const item = {
                    id: node.id,
                    path: node.path || node.name,
                    name: node.name,
                    icon: node.icon,
                    idPath: node.idPath,
                };
                newValues.push(item);
                addedNow.push(item);
            }
        });
        setValues(dedupeById(newValues));
        if (addedNow.length) {
            setPendingAddedItems((prev) => {
                const next = { ...prev };
                for (const item of addedNow)
                    next[normalizeGuid(item.id)] = item;
                return next;
            });
        }
        // If we are re-adding any id that was pending-removed, clear it from mask
        if (addedNow.length) {
            setPendingRemovedIds((prev) => {
                const next = { ...prev };
                for (const item of addedNow)
                    delete next[normalizeGuid(item.id)];
                return next;
            });
        }
        await editContext?.operations.editField({
            field: field.descriptor,
            value: newValues,
            rawValue: newValues.map((x) => normalizeGuid(x.id)).join("|"),
            refresh: "waitForQuietPeriod",
            originatingSlotId: slotContext?.slotId,
        });
    }
    const handleDragStart = (event, index) => {
        event.dataTransfer.setData("text/plain", index.toString());
        event.dataTransfer.setData(SELECTED_LIST_DRAG_TYPE, index.toString());
        event.dataTransfer.effectAllowed = "move";
        setDraggedIndex(index);
    };
    const isSelectedListDrag = (event) => Array.from(event.dataTransfer.types).includes(SELECTED_LIST_DRAG_TYPE) ||
        draggedIndex !== null;
    const getItemsForSelectedListDrag = (index) => {
        const draggedItem = values[index];
        if (!draggedItem)
            return [];
        const draggedItemId = normalizeGuid(draggedItem.id);
        if (selectedFromListIds.includes(draggedItemId)) {
            const selectedIds = new Set(selectedFromListIds);
            return values.filter((item) => selectedIds.has(normalizeGuid(item.id)));
        }
        return [draggedItem];
    };
    const getDropPosition = (event) => {
        const rect = event.currentTarget.getBoundingClientRect();
        return event.clientY < rect.top + rect.height / 2 ? "above" : "below";
    };
    const handleDragOver = (event, index) => {
        if (!isSelectedListDrag(event))
            return;
        event.preventDefault();
        event.dataTransfer.dropEffect = "move";
        const position = getDropPosition(event);
        const insertionIndex = position === "above" ? index : index + 1;
        // Hide the indicator when dropping here would not move the dragged item
        if (draggedIndex !== null &&
            (insertionIndex === draggedIndex || insertionIndex === draggedIndex + 1)) {
            setDropIndicator(null);
            return;
        }
        setDropIndicator({ index, position });
    };
    const handleDrop = async (event, dropIndex) => {
        if (!isSelectedListDrag(event))
            return;
        event.preventDefault();
        const position = getDropPosition(event);
        setDropIndicator(null);
        const dragIndex = parseInt(event.dataTransfer.getData("text/plain"));
        if (Number.isNaN(dragIndex)) {
            setDraggedIndex(null);
            return;
        }
        let insertionIndex = position === "above" ? dropIndex : dropIndex + 1;
        if (dragIndex < insertionIndex)
            insertionIndex -= 1;
        if (dragIndex === insertionIndex) {
            setDraggedIndex(null);
            return;
        }
        const newValues = [...values];
        const [draggedItem] = newValues.splice(dragIndex, 1);
        if (!draggedItem)
            return;
        newValues.splice(insertionIndex, 0, draggedItem);
        setValues(dedupeById(newValues));
        setDraggedIndex(null);
        await editContext?.operations.editField({
            field: field.descriptor,
            value: newValues,
            rawValue: newValues.map((x) => normalizeGuid(x.id)).join("|"),
            refresh: "waitForQuietPeriod",
            originatingSlotId: slotContext?.slotId,
        });
    };
    const handleDragEnd = () => {
        setDraggedIndex(null);
        setDropIndicator(null);
        setIsDragOverTree(false);
    };
    const handleDropOnTree = async (event) => {
        if (!isSelectedListDrag(event) || readOnly)
            return;
        event.preventDefault();
        event.stopPropagation();
        setIsDragOverTree(false);
        const dragIndex = parseInt(event.dataTransfer.getData(SELECTED_LIST_DRAG_TYPE) ||
            event.dataTransfer.getData("text/plain"));
        const itemsToRemove = getItemsForSelectedListDrag(Number.isNaN(dragIndex) ? (draggedIndex ?? -1) : dragIndex);
        if (itemsToRemove.length) {
            await removeFromList(itemsToRemove);
            setSelectedFromListIds((selectedIds) => {
                const removedIds = new Set(itemsToRemove.map((item) => normalizeGuid(item.id)));
                return selectedIds.filter((id) => !removedIds.has(id));
            });
        }
        setDraggedIndex(null);
    };
    const handleDragOverTree = (event) => {
        if (!isSelectedListDrag(event) || readOnly)
            return;
        event.preventDefault();
        event.stopPropagation();
        event.dataTransfer.dropEffect = "move";
        setIsDragOverTree(true);
    };
    const handleDragLeaveTree = (event) => {
        const relatedTarget = event.relatedTarget;
        const currentTarget = event.currentTarget;
        if (!currentTarget.contains(relatedTarget)) {
            setIsDragOverTree(false);
        }
    };
    // Handle drop from ContentTree onto the selected list panel
    const handleDropFromTree = (event) => {
        event.preventDefault();
        setIsDragOverList(false);
        // Get the dragged items from editContext
        const dragObject = editContext?.dragObject;
        if (dragObject?.type === "items" && dragObject.items) {
            const itemsToAdd = dragObject.items.map((item) => ({
                id: item.id,
                path: item.path || item.name,
                idPath: item.idPath,
                name: item.displayName || item.name || "",
                icon: item.icon,
            }));
            addToList(itemsToAdd);
        }
    };
    const handleDragOverList = (event) => {
        // Check if this is a drop from the ContentTree (has items in editContext.dragObject)
        const dragObject = editContext?.dragObject;
        if (dragObject?.type === "items" && !readOnly) {
            event.preventDefault();
            event.dataTransfer.dropEffect = "copy";
            setIsDragOverList(true);
        }
    };
    const handleDragLeaveList = (event) => {
        // Only reset if we're leaving the container itself, not entering a child
        const relatedTarget = event.relatedTarget;
        const currentTarget = event.currentTarget;
        if (!currentTarget.contains(relatedTarget)) {
            setIsDragOverList(false);
            setDropIndicator(null);
        }
    };
    const handleHeightResizeStart = (clientY) => {
        setIsResizingHeight(true);
        const startY = clientY;
        const startHeight = editorHeight;
        const handleMove = (moveClientY) => {
            const delta = moveClientY - startY;
            const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + delta));
            setEditorHeight(newHeight);
        };
        const handleEnd = () => {
            setIsResizingHeight(false);
            window.removeEventListener("mousemove", onMouseMove);
            window.removeEventListener("mouseup", onMouseUp);
            window.removeEventListener("touchmove", onTouchMove);
            window.removeEventListener("touchend", onTouchEnd);
        };
        const onMouseMove = (moveEvent) => {
            handleMove(moveEvent.clientY);
        };
        const onMouseUp = () => {
            handleEnd();
        };
        const onTouchMove = (moveEvent) => {
            if (moveEvent.touches.length === 1) {
                const touch = moveEvent.touches[0];
                if (touch) {
                    handleMove(touch.clientY);
                }
            }
        };
        const onTouchEnd = () => {
            handleEnd();
        };
        window.addEventListener("mousemove", onMouseMove);
        window.addEventListener("mouseup", onMouseUp);
        window.addEventListener("touchmove", onTouchMove, { passive: false });
        window.addEventListener("touchend", onTouchEnd);
    };
    const panels = [
        {
            name: "tree",
            defaultSize: "auto",
            className: "relative",
            content: (_jsx("div", { className: `absolute inset-0 overflow-auto transition-colors ${readOnly ? "bg-neutral-grey-5" : ""} ${isDragOverTree ? "bg-feedback-blue-light ring-feedback-blue ring-2 ring-inset" : ""}`, onDrop: handleDropOnTree, onDragOver: handleDragOverTree, onDragLeave: handleDragLeaveTree, children: _jsx(ContentTree, { expandIdPath: hoveredItem?.idPath, language: editContext.currentItemDescriptor?.language ?? "en", onDoubleClick: (node) => {
                        if (!readOnly)
                            addToList([node]);
                    }, selectionMode: "multiple", selectedItemIds: selectedItemNodesInTree.length
                        ? selectedItemNodesInTree.map((x) => x.id)
                        : hoveredItemId
                            ? [hoveredItemId]
                            : [], onSelectionChange: (e) => {
                        setSelectedItemNodesInTree(e);
                    }, rootItemIds: rootItemIds, includeItemPath: true, database: database, enableDragAndDrop: !readOnly, enableItemDrop: false, isDraggable: () => !readOnly, showGrabCursorForDraggableNodes: !readOnly }) })),
        },
        {
            name: "selectedList",
            defaultSize: "auto",
            className: "relative",
            content: (_jsx("div", { className: "relative h-full w-full", onDrop: handleDropFromTree, onDragOver: handleDragOverList, onDragLeave: handleDragLeaveList, children: _jsx("div", { className: `selected-list absolute inset-0 overflow-auto text-xs transition-colors ${readOnly ? "bg-neutral-grey-5" : "bg-white"} ${isDragOverList ? "bg-feedback-blue-light ring-feedback-blue ring-2 ring-inset" : ""}`, children: values.length === 0 ? (_jsx("div", { className: "text-neutral-grey-50 p-3 text-center", children: isDragOverList ? "Drop items here" : "None selected" })) : (values.map((option, index) => (_jsxs(Tooltip, { delayDuration: 1000, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { draggable: !readOnly, onDragStart: (e) => handleDragStart(e, index), onDragOver: (e) => handleDragOver(e, index), onDrop: (e) => handleDrop(e, index), onDragEnd: handleDragEnd, "data-selected": selectedFromListIds.includes(normalizeGuid(option.id)), className: `group selected-list-item border-border-default hover:bg-neutral-grey-5 relative flex cursor-pointer items-center gap-1.5 border-b p-1 font-light select-none last:border-b-0 ${selectedFromListIds.includes(normalizeGuid(option.id))
                                        ? "bg-feedback-blue-light"
                                        : ""} ${draggedIndex === index ? "opacity-50" : ""} ${!readOnly ? "cursor-move" : ""}`, onClick: (e) => {
                                        const optionId = normalizeGuid(option.id);
                                        if (e.shiftKey && lastClickedIndex !== null) {
                                            const startIndex = Math.min(lastClickedIndex, index);
                                            const endIndex = Math.max(lastClickedIndex, index);
                                            const rangeIds = values
                                                .slice(startIndex, endIndex + 1)
                                                .map((v) => normalizeGuid(v.id));
                                            setSelectedFromListIds(rangeIds);
                                        }
                                        else if (e.ctrlKey) {
                                            if (selectedFromListIds.includes(optionId)) {
                                                setSelectedFromListIds(selectedFromListIds.filter((id) => id !== optionId));
                                            }
                                            else {
                                                setSelectedFromListIds([
                                                    ...selectedFromListIds,
                                                    optionId,
                                                ]);
                                            }
                                            setLastClickedIndex(index);
                                        }
                                        else {
                                            if (selectedFromListIds.length === 1 &&
                                                selectedFromListIds[0] === optionId) {
                                                setSelectedFromListIds([]);
                                                setLastClickedIndex(null);
                                            }
                                            else {
                                                setSelectedFromListIds([optionId]);
                                                setLastClickedIndex(index);
                                            }
                                        }
                                    }, onMouseEnter: async () => {
                                        setHoveredItemId(option.id);
                                        setSelectedItemNodesInTree([
                                            {
                                                id: option.id,
                                                path: option.path,
                                                name: option.name,
                                                icon: option.icon,
                                                idPath: option.idPath || "",
                                            },
                                        ]);
                                    }, onDoubleClick: () => {
                                        if (!readOnly)
                                            removeFromList([option]);
                                    }, children: [dropIndicator?.index === index && (_jsx("div", { "data-testid": "treelist-drop-indicator", "data-position": dropIndicator.position, className: `bg-feedback-blue pointer-events-none absolute inset-x-0 z-10 h-0.5 rounded-full ${dropIndicator.position === "above"
                                                ? "top-0"
                                                : "-bottom-px"}` })), _jsx("img", { src: option.icon, className: "h-4 w-4" }), trimPath(option.path), " ", activeOpenButtonId === option.id ? (_jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { className: "h-4 p-1", size: "sm", onClick: (e) => {
                                                        e.stopPropagation();
                                                        editContext.loadItem(option.id);
                                                        setActiveOpenButtonId("");
                                                    }, children: "Open" }), _jsx(Button, { className: "h-4 p-1", size: "sm", onClick: (e) => {
                                                        e.stopPropagation();
                                                        editContext.loadItem(option.id, {
                                                            openInNewSlot: true,
                                                        });
                                                        setActiveOpenButtonId("");
                                                    }, children: "Open in new slot" })] })) : (_jsx("button", { className: "cursor-pointer opacity-0 transition-opacity group-hover:opacity-100", onClick: (e) => {
                                                e.stopPropagation();
                                                setActiveOpenButtonId(option.id);
                                            }, title: "Open", children: _jsx(ExternalLink, { strokeWidth: 1, className: "text-neutral-grey-50 h-3.5 w-3.5" }) }))] }) }), _jsx(TooltipContent, { children: option.path })] }, `${option.id}-${index}`)))) }) })),
        },
    ];
    return (_jsxs("div", { "data-testid": "treelist-editor", className: `focus-shadow border-border-default flex min-h-0 flex-col overflow-hidden rounded-[4px] border ${fillAvailableHeight ? "h-full" : ""} ${readOnly ? "bg-neutral-grey-5" : ""}`, style: fillAvailableHeight ? undefined : { height: `${editorHeight}px` }, children: [database === "master" && (_jsx("div", { className: `border-border-default border-b p-2 ${readOnly ? "bg-neutral-grey-5" : ""}`, children: _jsx(ItemSearch, { rootItemIds: rootItemIds.map((x) => normalizeGuid(x)), disabled: readOnly, database: database, itemSelected: async (item) => {
                        addToList([
                            {
                                id: item.id,
                                path: item.path || item.name,
                                idPath: item.idPath,
                                name: item.name || "",
                                icon: item.icon,
                            },
                        ]);
                    } }) })), _jsx(Splitter, { className: "min-h-0 flex-1", panels: panels, localStorageKey: "editor.treeListEditorSize", handleContent: (index) => index === 0 ? (_jsxs("div", { className: `bg-neutral-grey-5 flex flex-col items-center gap-1 rounded py-1 ${readOnly ? "opacity-50" : ""}`, children: [_jsx(SimpleIconButton, { label: "Add", icon: _jsx(ChevronRight, { size: 14, strokeWidth: 1 }), onClick: () => addToList(selectedItemNodesInTree), disabled: readOnly, showTooltip: false, className: "px-0" }), _jsx(SimpleIconButton, { label: "Remove", icon: _jsx(ChevronLeft, { size: 14, strokeWidth: 1 }), onClick: () => removeFromList(values.filter((v) => selectedFromListIds.includes(normalizeGuid(v.id)))), disabled: readOnly, showTooltip: false, className: "px-0" })] })) : null }), !fillAvailableHeight && (_jsx("div", { "data-testid": "treelist-height-resize-handle", className: `border-border-default bg-neutral-grey-5 hover:bg-neutral-grey-5 relative h-2 cursor-ns-resize border-t transition-colors ${readOnly ? "cursor-not-allowed opacity-50" : ""} ${isResizingHeight ? "bg-feedback-blue-light" : ""}`, onMouseDown: (e) => {
                    if (!readOnly) {
                        e.preventDefault();
                        handleHeightResizeStart(e.clientY);
                    }
                }, onTouchStart: (e) => {
                    if (!readOnly && e.touches.length === 1) {
                        const touch = e.touches[0];
                        if (touch) {
                            e.preventDefault();
                            handleHeightResizeStart(touch.clientY);
                        }
                    }
                }, style: { touchAction: "none" }, children: _jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(GripVertical, { size: 12, className: "text-neutral-grey-50", strokeWidth: 1 }) }) }))] }));
}
//# sourceMappingURL=TreeListEditor.js.map