/** @license MIT | @cube-dev/ui-kit v0.142.7 | Cube Dev Team */
import { extractStyles } from "../../../utils/styles.js";
import { mergeRefs } from "../../../utils/react/useCombinedRefs.js";
import { useEvent } from "../../../_internal/hooks/use-event.js";
import { SIZES, SIZE_NAME_TO_KEY } from "../../../tokens/sizes.js";
import { TreeElement, TreeScrollContainer } from "./styled.js";
import { buildTreeIndex } from "./tree-index.js";
import { TreeNode } from "./TreeNode.js";
import { useCheckboxTree } from "./use-checkbox-tree.js";
import { useLoadData } from "./use-load-data.js";
import { OUTER_STYLES } from "@tenphi/tasty";
import { forwardRef, useLayoutEffect, useMemo, useRef } from "react";
import { jsx } from "react/jsx-runtime";
import { useTree } from "react-aria";
import { Item, useTreeState } from "react-stately";
import { useVirtualizer } from "@tanstack/react-virtual";

//#region src/components/content/Tree/Tree.tsx
/**
* Render an `<Item>` for React Stately's collection builder.
*
* The visible content is rendered by `TreeNode` via `nodesByKey`,
* so `<Item>`'s body is only used by Stately for the collection
* (`textValue` for keyboard nav, `childItems` for nested traversal).
*/
function renderItem(node) {
	return /* @__PURE__ */ jsx(Item, {
		textValue: typeof node.title === "string" ? node.title : String(node.key),
		childItems: node.children,
		children: node.title
	}, node.key);
}
function collectDisabledKeys(treeData, treeIsDisabled) {
	const keys = [];
	const visit = (nodes) => {
		for (const node of nodes) {
			if (treeIsDisabled || node.isDisabled) keys.push(node.key);
			if (node.children) visit(node.children);
		}
	};
	visit(treeData);
	return keys;
}
/** Expand the supplied keys with all of their ancestor keys. */
function expandWithParents(keys, parentOf) {
	if (!keys || keys.length === 0) return keys;
	const set = new Set(keys);
	for (const key of keys) {
		let parent = parentOf.get(key);
		while (parent) {
			set.add(parent);
			parent = parentOf.get(parent);
		}
	}
	return Array.from(set);
}
/**
* Diff two key sets to find the single key that was added or removed.
* Returns `null` when both sets are equal.
*/
function findToggledKey(previous, next) {
	for (const k of next) if (!previous.has(k)) return {
		key: k,
		added: true
	};
	for (const k of previous) if (!next.has(k)) return {
		key: k,
		added: false
	};
	return null;
}
function resolveSelectionMode(selectionMode, isSelectable) {
	if (selectionMode != null) return selectionMode;
	if (isSelectable === false) return "none";
	return "single";
}
function TreeBase(props, ref) {
	const { treeData, isCheckable = false, isSelectable, selectionMode: selectionModeProp, size = "medium", shape = "default", containerPadding = 4, isDisabled = false, defaultExpandedKeys, expandedKeys, autoExpandParent = false, defaultCheckedKeys, checkedKeys, defaultSelectedKeys, selectedKeys, height, loadData, onExpand, onCheck, onSelect, itemProps, rowStyles, ariaLabel, qa, menu: treeMenu, contextMenu: treeContextMenu, onAction: treeOnAction, menuTriggerProps, menuProps, ...otherProps } = props;
	const baseStyles = extractStyles(otherProps, OUTER_STYLES);
	const treeRef = useRef(null);
	const scrollRef = useRef(null);
	const treeIndex = useMemo(() => buildTreeIndex(treeData), [treeData]);
	const nodesByKey = treeIndex.byKey;
	const parentOf = treeIndex.parentOf;
	const disabledKeys = useMemo(() => collectDisabledKeys(treeData, isDisabled), [treeData, isDisabled]);
	const selectionMode = resolveSelectionMode(selectionModeProp, isSelectable);
	const effectiveExpandedKeys = useMemo(() => autoExpandParent ? expandWithParents(expandedKeys, parentOf) : expandedKeys, [
		autoExpandParent,
		expandedKeys,
		parentOf
	]);
	const effectiveDefaultExpandedKeys = useMemo(() => autoExpandParent ? expandWithParents(defaultExpandedKeys, parentOf) : defaultExpandedKeys, [
		autoExpandParent,
		defaultExpandedKeys,
		parentOf
	]);
	const checkbox = useCheckboxTree({
		treeData,
		index: treeIndex,
		isCheckable,
		defaultCheckedKeys,
		checkedKeys,
		onCheck
	});
	const loadDataController = useLoadData({
		nodesByKey,
		loadData,
		initialExpandedKeys: effectiveExpandedKeys ?? effectiveDefaultExpandedKeys
	});
	/**
	* In controlled mode, `previous` is read from `effectiveExpandedKeys`;
	* in uncontrolled mode this ref is the only source. Initialized from
	* the *effective* (ancestor-expanded) keys so the first toggle diffs
	* correctly.
	*/
	const previousExpandedRef = useRef(new Set(effectiveExpandedKeys ?? effectiveDefaultExpandedKeys ?? []));
	const handleExpandedChange = useEvent((nextSet) => {
		loadDataController.onExpandedChanged(nextSet);
		const previous = expandedKeys !== void 0 ? new Set(effectiveExpandedKeys ?? []) : previousExpandedRef.current;
		previousExpandedRef.current = new Set(nextSet);
		if (!onExpand) return;
		const toggled = findToggledKey(previous, nextSet);
		if (!toggled) return;
		const node = nodesByKey.get(String(toggled.key));
		if (!node) return;
		const info = {
			expanded: toggled.added,
			node
		};
		onExpand(Array.from(nextSet), info);
	});
	/**
	* Same pattern as `previousExpandedRef`: in controlled mode we read
	* from `selectedKeys`; in uncontrolled mode this ref tracks state.
	*/
	const previousSelectionRef = useRef(new Set(selectedKeys ?? defaultSelectedKeys ?? []));
	/**
	* `useEvent` captures the closure lazily, so `state` (declared below
	* `ariaProps` / `useTreeState`) is safely accessible at call time.
	*/
	const handleSelectionChange = useEvent((selection) => {
		if (!onSelect) return;
		let arr;
		if (selection === "all") {
			arr = [];
			for (const key of state.collection.getKeys()) {
				const node = state.collection.getItem(key);
				if (node && node.type === "item") arr.push(key);
			}
		} else arr = Array.from(selection);
		const nextSet = new Set(arr);
		const previous = selectedKeys !== void 0 ? new Set(selectedKeys ?? []) : previousSelectionRef.current;
		previousSelectionRef.current = nextSet;
		const toggled = findToggledKey(previous, nextSet);
		if (!toggled) return;
		const node = nodesByKey.get(String(toggled.key));
		if (!node) return;
		const selectedNodes = arr.map((k) => nodesByKey.get(String(k))).filter((n) => !!n);
		const info = {
			selected: toggled.added,
			node,
			selectedNodes
		};
		onSelect(arr, info);
	});
	const hasOnSelect = onSelect != null;
	const ariaProps = useMemo(() => ({
		selectionMode,
		selectedKeys,
		defaultSelectedKeys,
		onSelectionChange: hasOnSelect && selectionMode !== "none" ? handleSelectionChange : void 0,
		expandedKeys: effectiveExpandedKeys,
		defaultExpandedKeys: effectiveDefaultExpandedKeys,
		onExpandedChange: handleExpandedChange,
		disabledKeys,
		disabledBehavior: "all",
		items: treeData,
		children: renderItem,
		"aria-label": ariaLabel ?? "Tree"
	}), [
		selectionMode,
		selectedKeys,
		defaultSelectedKeys,
		hasOnSelect,
		handleSelectionChange,
		effectiveExpandedKeys,
		effectiveDefaultExpandedKeys,
		handleExpandedChange,
		disabledKeys,
		treeData,
		ariaLabel
	]);
	const baseState = useTreeState(ariaProps);
	const state = useMemo(() => {
		const collection = baseState.collection;
		if (typeof collection.getChildren === "function") return baseState;
		const patched = Object.create(collection);
		patched.getChildren = (key) => {
			const node = collection.getItem(key);
			return node ? Array.from(node.childNodes) : [];
		};
		return {
			...baseState,
			collection: patched
		};
	}, [baseState]);
	const { gridProps } = useTree(ariaProps, state, treeRef);
	const visibleNodes = useMemo(() => {
		const out = [];
		for (const key of state.collection.getKeys()) {
			const node = state.collection.getItem(key);
			if (node && node.type === "item") out.push(node);
		}
		return out;
	}, [state.collection]);
	const visibleNodesRef = useRef(visibleNodes);
	visibleNodesRef.current = visibleNodes;
	const rowVirtualizer = useVirtualizer({
		count: visibleNodes.length,
		getScrollElement: () => scrollRef.current,
		getItemKey: (index) => {
			return visibleNodesRef.current[index]?.key ?? index;
		},
		estimateSize: () => SIZES[SIZE_NAME_TO_KEY[size]],
		gap: 1,
		overscan: 10,
		paddingStart: containerPadding,
		paddingEnd: containerPadding
	});
	const treeStyle = useMemo(() => {
		if (height == null) return void 0;
		return { ["--tree-height"]: `${height}px` };
	}, [height]);
	const sizerStyle = useMemo(() => ({
		position: "relative",
		width: "100%",
		height: `${rowVirtualizer.getTotalSize()}px`
	}), [rowVirtualizer.getTotalSize()]);
	/**
	* Each scroll effect tracks its own "last scrolled" target instead
	* of sharing one ref. A shared ref creates a feedback loop: keyboard
	* nav updates the focused-key ref to the new row, and any subsequent
	* `visibleNodes` change OR `selectedKeys` array-identity change
	* (very common — `selectedKeys={[value]}` allocates a fresh array
	* every parent render) re-runs the selectedKeys effect, which sees
	* the ref no longer matches `selectedKeys[0]` and yanks the viewport
	* back to the selected row. Two refs let each effect dedupe against
	* its own intent only; the cost is at most one duplicate
	* `scrollToIndex` call when click updates focus and selection in the
	* same render (idempotent — both target the same index).
	*/
	const lastFocusedScrollRef = useRef(null);
	const lastSelectedScrollRef = useRef(null);
	/**
	* Keep the focused row visible during keyboard navigation.
	*
	* Uses `rowVirtualizer.scrollToIndex` (instead of
	* `querySelector + scrollIntoView`) so the scroll lands even when the
	* target row is outside the virtualizer's currently rendered window.
	* `align: 'auto'` only scrolls when needed, matching the prior
	* `scrollIntoView({ block: 'nearest' })` behavior.
	*/
	useLayoutEffect(() => {
		const focusedKey = state.selectionManager.focusedKey;
		if (focusedKey == null) return;
		if (lastFocusedScrollRef.current === focusedKey) return;
		const index = visibleNodesRef.current.findIndex((n) => n.key === focusedKey);
		if (index < 0) return;
		rowVirtualizer.scrollToIndex(index, { align: "auto" });
		lastFocusedScrollRef.current = focusedKey;
	}, [state.selectionManager.focusedKey, rowVirtualizer]);
	/**
	* Scroll into view when controlled `selectedKeys` changes externally.
	*
	* Selection-manager `focusedKey` only updates on explicit user
	* interaction (click / keyboard) — it does NOT track controlled
	* `selectedKeys` prop changes. So when a parent updates the prop
	* (e.g. opening a file from elsewhere), we need a separate effect
	* to bring the row into view.
	*
	* Re-runs on `visibleNodes` so we retry once parents are expanded
	* (the row is invisible to the virtualizer until then).
	*/
	useLayoutEffect(() => {
		if (selectedKeys == null) return;
		const target = selectedKeys[0];
		if (target == null) return;
		if (lastSelectedScrollRef.current === target) return;
		const index = visibleNodes.findIndex((n) => String(n.key) === target);
		if (index < 0) return;
		rowVirtualizer.scrollToIndex(index, { align: "auto" });
		lastSelectedScrollRef.current = target;
	}, [
		selectedKeys,
		visibleNodes,
		rowVirtualizer
	]);
	const mods = useMemo(() => ({
		"has-height": height != null,
		shape
	}), [height, shape]);
	const mergedScrollRef = useMemo(() => mergeRefs(ref, scrollRef), [ref, scrollRef]);
	return /* @__PURE__ */ jsx(TreeElement, {
		...gridProps,
		ref: treeRef,
		qa: qa ?? "Tree",
		mods,
		styles: baseStyles,
		style: treeStyle,
		children: /* @__PURE__ */ jsx(TreeScrollContainer, {
			ref: mergedScrollRef,
			children: /* @__PURE__ */ jsx("div", {
				role: "presentation",
				style: sizerStyle,
				children: rowVirtualizer.getVirtualItems().map((virtualItem) => {
					const node = visibleNodes[virtualItem.index];
					if (!node) return null;
					const keyStr = String(node.key);
					const data = nodesByKey.get(keyStr);
					if (!data) return null;
					return /* @__PURE__ */ jsx(TreeNode, {
						node,
						data,
						state,
						isCheckable,
						isExpanded: state.expandedKeys.has(node.key),
						isChecked: checkbox.checkedSet.has(keyStr),
						isIndeterminate: checkbox.halfCheckedSet.has(keyStr),
						isLoading: loadDataController.loadingKeys.has(keyStr),
						size,
						itemProps,
						rowStyles,
						virtualStyle: {
							position: "absolute",
							top: 0,
							left: containerPadding,
							right: containerPadding,
							transform: `translateY(${virtualItem.start}px)`
						},
						virtualRef: rowVirtualizer.measureElement,
						virtualIndex: virtualItem.index,
						menu: treeMenu,
						contextMenu: treeContextMenu,
						menuTriggerProps,
						menuProps,
						onToggleChecked: checkbox.toggle,
						onAction: treeOnAction
					}, node.key);
				})
			})
		})
	});
}
const _Tree = forwardRef(TreeBase);
_Tree.displayName = "Tree";
/**
* Hierarchical data display with optional checkboxes, single/multi
* selection, async lazy loading, and keyboard navigation.
*
* Built on `useTree` / `useTreeState` (React Aria + React Stately) and
* styled with Tasty.
*
* @example
* ```tsx
* <Tree
*   treeData={data}
*   isCheckable
*   selectionMode="none"
*   defaultExpandedKeys={['root']}
*   onCheck={(keys) => console.log(keys)}
* />
* ```
*/
const Tree = _Tree;

//#endregion
export { Tree };
//# sourceMappingURL=Tree.js.map