/** @license MIT | @cube-dev/ui-kit v0.147.0 | Cube Dev Team */
import { useEvent } from "../../../_internal/hooks/use-event.js";
import { BoardWidgetStore } from "./board-store.js";
import { collides } from "./grid-core/collision.js";
import { bottom, cloneLayout, getLayoutItem, moveElement } from "./grid-core/layout.js";
import { calcXYRaw } from "./grid-core/calculate.js";
import { applyPositionConstraints } from "./grid-core/constraints.js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

//#region src/components/layout/Board/use-board-registry.ts
/**
* The dragged widget's center in viewport coordinates.
*
* This is the anchor the drop-target hit-test uses to pick which board a drag is
* over, so a board (including an empty one) opens as soon as the widget's center
* enters it rather than only once its whole top-left corner is inside.
*
* The landing itself stays anchored to the grabbed top-left (`rect.left/top` in
* `computeLanding`): that point is this center offset by the widget's own fixed
* pixel size, so the resolved landing cell keeps corresponding to where the
* floating overlay clone is drawn (see `WidgetHost`) and the placeholder keeps
* tracking the ghost. The only unavoidable divergence is while the widget
* physically overhangs a board edge - a placeholder cannot render outside row 0
* / column 0 - which is the accepted tradeoff for opening on the center.
*/
function rectCenter(rect) {
	return {
		x: rect.left + rect.width / 2,
		y: rect.top + rect.height / 2
	};
}
/**
* Canonical key for the unordered pair `{id1, id2}`. Widget ids are unrestricted
* strings, so a naive `` `${id1}|${id2}` `` join is ambiguous - an id containing
* the delimiter lets two different pairs collide (e.g. `{"a|b","c"}` and
* `{"a","b|c"}` both yield `"a|b|c"`). `JSON.stringify` of the sorted tuple
* quotes and escapes each id, so distinct pairs always produce distinct keys.
*/
function pairKey(id1, id2) {
	return id1 < id2 ? JSON.stringify([id1, id2]) : JSON.stringify([id2, id1]);
}
/**
* Keys of every overlapping pair in the layout (see `pairKey`), so two layouts'
* overlap sets can be compared to tell which overlaps a move *introduced* versus
* which already existed.
*/
function overlappingPairs(layout) {
	const pairs = /* @__PURE__ */ new Set();
	for (let i = 0; i < layout.length; i++) for (let j = i + 1; j < layout.length; j++) {
		const a = layout[i];
		const b = layout[j];
		if (collides(a, b)) pairs.add(pairKey(a.i, b.i));
	}
	return pairs;
}
/**
* Whether `after` contains an overlapping pair that `before` did not. Used to
* reject only moves that *create* a new stack. A board may already hold
* overlapping widgets - a `compact={null}` layout is never gap-compacted, and an
* app can supply overlapping items directly (or toggle `allowOverlap` off after
* stacking) - and those pre-existing overlaps must not freeze every subsequent
* move by making a whole-layout collision check always true.
*/
function hasNewOverlap(before, after) {
	const afterPairs = overlappingPairs(after);
	for (const key of afterPairs) if (!before.has(key)) return true;
	return false;
}
/**
* First position at or below the preferred cell where `item` fits without
* overlapping any of `others` (assumed overlap-free), scanning left-to-right
* then top-to-bottom, never past `maxRows`. Last-resort placement for a legacy
* (no-op compaction) cross-board drop that would otherwise stack widgets.
*
* A finite `maxRows` mirrors the `gridBounds` constraint the normal landing path
* applies (`clamp(y, 0, maxRows - h)`): the widget's bottom edge (`y + h`) stays
* within the row limit even when the landing cell is blocked, so the downward
* scan can't push it off the board. Rows are searched from the (clamped) landing
* row down to the limit first, then upward toward the top; if the whole valid
* band is full the widget is clamped to the limit (bounds win over overlap, as
* `gridBounds` does). With an unbounded `maxRows` there is always a free slot
* below every existing item, so the downward scan terminates on its own.
*/
function placeInFreeSlot(others, item, cols, maxRows = Infinity) {
	const w = Math.min(item.w, cols);
	const maxY = Number.isFinite(maxRows) ? Math.max(0, maxRows - item.h) : Infinity;
	const fits = (x, y) => !others.some((o) => collides(o, {
		...item,
		x,
		y,
		w
	}));
	const preferX = Math.min(Math.max(0, item.x), cols - w);
	const startY = Math.min(Math.max(0, item.y), maxY);
	if (fits(preferX, startY)) return {
		...item,
		x: preferX,
		y: startY,
		w
	};
	const scanBottom = Number.isFinite(maxRows) ? maxY : startY + others.reduce((m, o) => Math.max(m, o.y + o.h), 0) + 1;
	for (let y = startY; y <= scanBottom; y++) for (let x = 0; x <= cols - w; x++) if (fits(x, y)) return {
		...item,
		x,
		y,
		w
	};
	for (let y = startY - 1; y >= 0; y--) for (let x = 0; x <= cols - w; x++) if (fits(x, y)) return {
		...item,
		x,
		y,
		w
	};
	return {
		...item,
		x: preferX,
		y: startY,
		w
	};
}
function useBoardRegistry(options) {
	const store = useMemo(() => new BoardWidgetStore(), []);
	const onTransferRef = useRef(options?.onWidgetTransfer);
	onTransferRef.current = options?.onWidgetTransfer;
	const overlayRef = useRef(null);
	const boardsRef = useRef(/* @__PURE__ */ new Map());
	const dragStateRef = useRef(null);
	const affectedRef = useRef(/* @__PURE__ */ new Set());
	const sourceSnapshotRef = useRef([]);
	const targetSnapshotsRef = useRef(/* @__PURE__ */ new Map());
	const previewRef = useRef(null);
	const frozenRectsRef = useRef(/* @__PURE__ */ new Map());
	const nestedInDraggedRef = useRef(/* @__PURE__ */ new Set());
	const lastLandingRef = useRef(null);
	const pointerPosRef = useRef(null);
	const [dragState, setDragStateInternal] = useState(null);
	const setDragState = useCallback((next) => {
		dragStateRef.current = next;
		setDragStateInternal(next);
	}, []);
	const trackPointer = useCallback((e) => {
		pointerPosRef.current = {
			x: e.clientX,
			y: e.clientY
		};
	}, []);
	useEffect(() => () => window.removeEventListener("pointermove", trackPointer, true), [trackPointer]);
	const registerBoard = useCallback((entry) => {
		boardsRef.current.set(entry.id, entry);
		return () => {
			if (boardsRef.current.get(entry.id) === entry) boardsRef.current.delete(entry.id);
		};
	}, []);
	const getBoardRect = useCallback((entry) => {
		const frozen = frozenRectsRef.current.get(entry.id);
		if (frozen && frozen.width > 0 && frozen.height > 0) return frozen;
		const live = entry.getContentRect();
		if (live && live.width > 0 && live.height > 0) {
			frozenRectsRef.current.set(entry.id, live);
			return live;
		}
		return frozen ?? live;
	}, []);
	/** True when board `outer` DOM-contains board `inner` (outer is an ancestor). */
	const isAncestorBoard = useCallback((outer, inner) => {
		const outerNode = outer.getContentNode();
		const innerNode = inner.getContentNode();
		return !!outerNode && !!innerNode && outerNode.contains(innerNode);
	}, []);
	/**
	* Viewport rect of the widget host a nested board lives in, or null for a
	* top-level board. Lets a drag stay anchored to a nested board while the anchor
	* is still within its container widget (e.g. over a Tabs header sitting above
	* the board), so an ancestor board does not reflow prematurely.
	*/
	const getBoardContainerRect = useCallback((entry) => {
		return (entry.getContentNode()?.parentElement?.closest("[data-board-widget-host]"))?.getBoundingClientRect() ?? null;
	}, []);
	/** Whether a point falls within a rect (inclusive of edges). */
	const pointInRect = useCallback((point, rect) => point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom, []);
	/** Find the most-nested droppable board whose content rect contains a point. */
	const hitTest = useCallback((point) => {
		let best = null;
		let bestArea = Infinity;
		const overlay = overlayRef.current;
		boardsRef.current.forEach((entry) => {
			if (!entry.isDroppable()) return;
			if (nestedInDraggedRef.current.has(entry.id)) return;
			if (overlay && overlay.contains(entry.getContentNode())) return;
			const liveRect = entry.getContentRect();
			if (!liveRect || liveRect.width === 0 || liveRect.height === 0) return;
			const rect = getBoardRect(entry);
			if (!rect) return;
			if (point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom) {
				const area = rect.width * rect.height;
				if (area < bestArea) {
					bestArea = area;
					best = entry;
				}
			}
		});
		return best;
	}, [getBoardRect]);
	const onDragStart = useEvent((boardId, itemId, rect, pointerType, widgetNode) => {
		const entry = boardsRef.current.get(boardId);
		const item = entry ? getLayoutItem(entry.getLayout(), itemId) : void 0;
		if (!entry || !item) return;
		const nested = /* @__PURE__ */ new Set();
		if (widgetNode) boardsRef.current.forEach((e) => {
			const node = e.getContentNode();
			if (node && widgetNode.contains(node)) nested.add(e.id);
		});
		nestedInDraggedRef.current = nested;
		const frozen = /* @__PURE__ */ new Map();
		boardsRef.current.forEach((e) => {
			const r = e.getContentRect();
			if (r) frozen.set(e.id, r);
		});
		frozenRectsRef.current = frozen;
		affectedRef.current = new Set([boardId]);
		sourceSnapshotRef.current = cloneLayout(entry.getLayout());
		previewRef.current = null;
		lastLandingRef.current = {
			x: item.x,
			y: item.y
		};
		pointerPosRef.current = null;
		if (pointerType !== "keyboard") window.addEventListener("pointermove", trackPointer, true);
		setDragState({
			sourceBoardId: boardId,
			currentBoardId: boardId,
			itemId,
			item: {
				...item,
				constraints: item.constraints ?? store.get(itemId)?.constraints
			},
			rect,
			pointerType,
			nestedBoardIds: nested
		});
		entry.setPlaceholder({ ...item });
	});
	/** Compute the constrained landing position of an item on a target board. */
	const computeLanding = useCallback((target, item, rect) => {
		const pp = target.getPositionParams();
		const contentRect = getBoardRect(target);
		const localLeft = rect.left - (contentRect?.left ?? 0);
		const raw = calcXYRaw(pp, rect.top - (contentRect?.top ?? 0), localLeft);
		return applyPositionConstraints(target.getConstraints(), item, raw.x, raw.y, {
			cols: pp.cols,
			maxRows: target.getMaxRows(),
			containerWidth: pp.containerWidth,
			containerHeight: target.getContainerHeight(),
			rowHeight: pp.rowHeight,
			margin: pp.margin,
			layout: target.getLayout()
		});
	}, [getBoardRect]);
	const moveWithinBoard = useCallback((entry, item, x, y) => {
		const pp = entry.getPositionParams();
		const compactor = entry.getCompactor();
		const layout = entry.getLayout();
		const working = getLayoutItem(layout, item.i) ? cloneLayout(layout) : [...cloneLayout(layout), {
			...item,
			x,
			y
		}];
		const target = getLayoutItem(working, item.i);
		if (!target) return;
		const moved = moveElement(working, target, x, y, true, compactor.preventCollision, compactor.type, pp.cols, compactor.allowOverlap);
		const compacted = [...compactor.compact(moved, pp.cols)];
		if (!compactor.allowOverlap && hasNewOverlap(overlappingPairs(layout), compacted)) return;
		entry.applyLayout(compacted, false);
		entry.setPlaceholder(getLayoutItem(compacted, item.i) ?? null);
	}, []);
	/**
	* Move a widget by one logical keyboard step. Unlike pointer movement, this
	* starts from the live grid coordinates rather than DOM geometry and scans
	* farther in the requested direction when the adjacent slot cannot produce a
	* valid move.
	*/
	const moveWithKeyboard = useCallback((entry, item, deltaX, deltaY) => {
		const pp = entry.getPositionParams();
		const compactor = entry.getCompactor();
		const layout = entry.getLayout();
		const live = getLayoutItem(layout, item.i);
		if (!live) return;
		const directionX = Math.sign(deltaX);
		const directionY = Math.sign(deltaY);
		if (directionX === 0 && directionY === 0) return;
		const maxRows = entry.getMaxRows();
		const attempts = directionX < 0 ? live.x : directionX > 0 ? Math.max(0, pp.cols - live.w - live.x) : directionY < 0 ? live.y : Number.isFinite(maxRows) ? Math.max(0, maxRows - live.h - live.y) : Math.max(1, bottom(layout) - live.y);
		const seen = /* @__PURE__ */ new Set();
		const beforePairs = overlappingPairs(layout);
		for (let distance = 1; distance <= attempts; distance++) {
			const rawX = live.x + directionX * distance;
			const rawY = live.y + directionY * distance;
			const candidate = applyPositionConstraints(entry.getConstraints(), item, rawX, rawY, {
				cols: pp.cols,
				maxRows,
				containerWidth: pp.containerWidth,
				containerHeight: entry.getContainerHeight(),
				rowHeight: pp.rowHeight,
				margin: pp.margin,
				layout
			});
			const candidateKey = `${candidate.x}:${candidate.y}`;
			if (seen.has(candidateKey)) continue;
			seen.add(candidateKey);
			if (directionX !== 0 && (Math.sign(candidate.x - live.x) !== directionX || candidate.y !== live.y) || directionY !== 0 && (Math.sign(candidate.y - live.y) !== directionY || candidate.x !== live.x)) continue;
			const working = cloneLayout(layout);
			const target = getLayoutItem(working, item.i);
			if (!target) return;
			const moved = moveElement(working, target, candidate.x, candidate.y, true, compactor.preventCollision, compactor.type, pp.cols, compactor.allowOverlap);
			const compacted = [...compactor.compact(moved, pp.cols)];
			const landed = getLayoutItem(compacted, item.i);
			if (!landed) continue;
			const advanced = directionX !== 0 ? Math.sign(landed.x - live.x) === directionX && landed.y === live.y : Math.sign(landed.y - live.y) === directionY && landed.x === live.x;
			const overlaps = !compactor.allowOverlap && hasNewOverlap(beforePairs, compacted);
			if (!advanced || overlaps) continue;
			entry.applyLayout(compacted, false);
			entry.setPlaceholder(landed);
			lastLandingRef.current = {
				x: landed.x,
				y: landed.y
			};
			return;
		}
	}, []);
	/**
	* Live cross-board reflow preview. Pushes the target board's *other* widgets
	* aside to make room for the incoming item, without ever adding the dragged
	* item's host to the target (it stays visualized as a placeholder + overlay
	* clone, and its gesture host remains mounted in the source board).
	*
	* Continuity: the working layout (including the incoming item) is carried
	* across frames via `previewRef`, so a small pointer movement moves the item
	* one step and reflows the neighbours incrementally - identical to an in-board
	* drag. Recomputing from the clean snapshot every frame instead lets
	* `moveElement`/compaction sink the item to the bottom of an occupied column
	* (the "pushed too far below" bug). On (re-)entry the working layout is seeded
	* from a *clone* of the snapshot so the stored snapshot is never mutated.
	*/
	const previewOnTarget = useCallback((target, item, x, y) => {
		const pp = target.getPositionParams();
		const compactor = target.getCompactor();
		const carried = previewRef.current?.boardId === target.id ? previewRef.current.working : null;
		const base = cloneLayout(carried ?? targetSnapshotsRef.current.get(target.id) ?? target.getLayout()).filter((l) => l.i !== item.i);
		const prevItem = carried ? getLayoutItem(carried, item.i) : void 0;
		const dragged = prevItem ? { ...prevItem } : compactor.type === "horizontal" ? {
			...item,
			x: Math.max(0, x) - 1,
			y
		} : {
			...item,
			x,
			y: Math.max(0, y) - 1
		};
		const moved = moveElement([...base, dragged], dragged, x, y, true, compactor.preventCollision, compactor.type, pp.cols, compactor.allowOverlap);
		const compacted = [...compactor.compact(moved, pp.cols)];
		if (!compactor.allowOverlap && hasNewOverlap(overlappingPairs(base), compacted)) return;
		previewRef.current = {
			boardId: target.id,
			working: compacted
		};
		const landed = getLayoutItem(compacted, item.i) ?? {
			...item,
			x,
			y
		};
		target.applyLayout(compacted.filter((l) => l.i !== item.i), false);
		target.setPlaceholder({ ...landed });
	}, []);
	const onDragMove = useEvent((deltaX, deltaY, pointerType) => {
		const ds = dragStateRef.current;
		if (!ds) return;
		if (pointerType === "keyboard") {
			const source = boardsRef.current.get(ds.sourceBoardId);
			if (source) moveWithKeyboard(source, ds.item, deltaX, deltaY);
			return;
		}
		const newRect = {
			...ds.rect,
			left: ds.rect.left + deltaX,
			top: ds.rect.top + deltaY
		};
		const source = boardsRef.current.get(ds.sourceBoardId);
		const anchor = rectCenter(newRect);
		let target = hitTest(anchor) ?? source ?? null;
		if (target && source && target.id !== source.id && isAncestorBoard(target, source)) {
			const containerRect = getBoardContainerRect(source);
			const gatePoint = pointerPosRef.current ?? anchor;
			if (containerRect && pointInRect(gatePoint, containerRect)) target = source;
		}
		if (!target) {
			setDragState({
				...ds,
				rect: newRect
			});
			return;
		}
		const { x, y } = computeLanding(target, ds.item, newRect);
		lastLandingRef.current = {
			x,
			y
		};
		if (target.id === ds.sourceBoardId) {
			if (ds.currentBoardId !== ds.sourceBoardId) {
				const prev = boardsRef.current.get(ds.currentBoardId);
				const snap = targetSnapshotsRef.current.get(ds.currentBoardId);
				if (snap) prev?.applyLayout(cloneLayout(snap), false);
				prev?.setPlaceholder(null);
				previewRef.current = null;
			}
			moveWithinBoard(target, ds.item, x, y);
			setDragState({
				...ds,
				currentBoardId: ds.sourceBoardId,
				rect: newRect
			});
			return;
		}
		if (ds.currentBoardId !== target.id) {
			const prev = boardsRef.current.get(ds.currentBoardId);
			if (ds.currentBoardId === ds.sourceBoardId) prev?.applyLayout(cloneLayout(sourceSnapshotRef.current), false);
			else {
				const snap = targetSnapshotsRef.current.get(ds.currentBoardId);
				if (snap) prev?.applyLayout(cloneLayout(snap), false);
			}
			prev?.setPlaceholder(null);
			previewRef.current = null;
			if (!targetSnapshotsRef.current.has(target.id)) targetSnapshotsRef.current.set(target.id, cloneLayout(target.getLayout()));
		}
		previewOnTarget(target, ds.item, x, y);
		affectedRef.current.add(target.id);
		setDragState({
			...ds,
			currentBoardId: target.id,
			rect: newRect
		});
	});
	const onDragEnd = useEvent(() => {
		window.removeEventListener("pointermove", trackPointer, true);
		pointerPosRef.current = null;
		const ds = dragStateRef.current;
		if (!ds) return;
		const source = boardsRef.current.get(ds.sourceBoardId);
		const target = boardsRef.current.get(ds.currentBoardId);
		if (!target || target.id === ds.sourceBoardId) {
			if (source) {
				const sp = source.getPositionParams();
				const sc = source.getCompactor();
				source.applyLayout([...sc.compact(source.getLayout(), sp.cols)], true);
			}
		} else {
			const landing = lastLandingRef.current ?? {
				x: ds.item.x,
				y: ds.item.y
			};
			if (source) {
				const sp = source.getPositionParams();
				const sc = source.getCompactor();
				const remaining = source.getLayout().filter((l) => l.i !== ds.itemId);
				source.applyLayout([...sc.compact(remaining, sp.cols)], true);
			}
			const tp = target.getPositionParams();
			const tc = target.getCompactor();
			const carried = previewRef.current?.boardId === target.id && getLayoutItem(previewRef.current.working, ds.itemId) ? previewRef.current.working : null;
			let finalLayout;
			if (carried) finalLayout = [...tc.compact(cloneLayout(carried), tp.cols)];
			else {
				const newItem = tc.type === "horizontal" ? {
					...ds.item,
					x: Math.max(0, landing.x) - 1,
					y: landing.y
				} : {
					...ds.item,
					x: landing.x,
					y: Math.max(0, landing.y) - 1
				};
				const moved = moveElement([...cloneLayout((targetSnapshotsRef.current.get(target.id) ?? target.getLayout()).filter((l) => l.i !== ds.itemId)), newItem], newItem, landing.x, landing.y, true, tc.preventCollision, tc.type, tp.cols, tc.allowOverlap);
				finalLayout = [...tc.compact(moved, tp.cols)];
			}
			const targetOthers = cloneLayout((targetSnapshotsRef.current.get(target.id) ?? target.getLayout()).filter((l) => l.i !== ds.itemId));
			if (!tc.allowOverlap && hasNewOverlap(overlappingPairs(targetOthers), finalLayout)) finalLayout = [...targetOthers, placeInFreeSlot(targetOthers, {
				...ds.item,
				x: landing.x,
				y: landing.y
			}, tp.cols, tp.maxRows)];
			target.applyLayout(finalLayout, true);
			onTransferRef.current?.({
				widgetId: ds.itemId,
				fromBoardId: ds.sourceBoardId,
				toBoardId: target.id,
				item: getLayoutItem(finalLayout, ds.itemId) ?? {
					...ds.item,
					x: landing.x,
					y: landing.y
				}
			});
		}
		const ids = new Set(affectedRef.current);
		ids.add(ds.sourceBoardId);
		ids.add(ds.currentBoardId);
		ids.forEach((id) => boardsRef.current.get(id)?.setPlaceholder(null));
		affectedRef.current.clear();
		sourceSnapshotRef.current = [];
		targetSnapshotsRef.current.clear();
		frozenRectsRef.current.clear();
		nestedInDraggedRef.current = /* @__PURE__ */ new Set();
		previewRef.current = null;
		lastLandingRef.current = null;
		setDragState(null);
	});
	return useMemo(() => ({
		store,
		overlayRef,
		registerBoard,
		onDragStart,
		onDragMove,
		onDragEnd,
		dragState
	}), [
		store,
		registerBoard,
		onDragStart,
		onDragMove,
		onDragEnd,
		dragState
	]);
}

//#endregion
export { useBoardRegistry };
//# sourceMappingURL=use-board-registry.js.map