<script lang="ts" module>
	export interface GridItemLayout {
		id: number;
		x: number;
		y: number;
		w: number;
		h: number;
	}
</script>

<script lang="ts">
	import { untrack } from 'svelte';

	interface Props {
		items: GridItemLayout[];
		cols?: number;
		rowHeight?: number;
		gap?: number;
		minW?: number;
		maxW?: number;
		minH?: number;
		maxH?: number;
		onchange?: (items: GridItemLayout[]) => void;
		onitemclick?: (id: number) => void;
		children: import('svelte').Snippet<[{ item: GridItemLayout; width: number; height: number }]>;
	}

	let {
		items = $bindable([]),
		cols = 4,
		rowHeight = 175,
		gap = 10,
		minW = 1,
		maxW = 2,
		minH = 1,
		maxH = 4,
		onchange,
		onitemclick,
		children
	}: Props = $props();

	// Drag threshold - if mouse moves less than this, treat as click
	const DRAG_THRESHOLD = 5;

	let containerRef: HTMLDivElement;
	let containerWidth = $state(0);

	// Calculate column width based on container
	const colWidth = $derived(containerWidth > 0 ? (containerWidth - (cols - 1) * gap) / cols : 280);

	// Calculate max rows based on items
	const maxRow = $derived(
		items.length > 0 ? Math.max(...items.map((item) => item.y + item.h)) : 1
	);

	// Grid height based on content
	const gridHeight = $derived(maxRow * rowHeight + (maxRow - 1) * gap);

	// Dragging state
	let dragItem = $state<GridItemLayout | null>(null);
	let dragStartX = $state(0);
	let dragStartY = $state(0);
	let dragOffsetX = $state(0);
	let dragOffsetY = $state(0);

	// Resizing state
	let resizeItem = $state<GridItemLayout | null>(null);
	let resizeStartW = $state(0);
	let resizeStartH = $state(0);
	let resizeStartX = $state(0);
	let resizeStartY = $state(0);
	// Pixel-based resize offsets for smooth visual feedback
	let resizePixelDeltaX = $state(0);
	let resizePixelDeltaY = $state(0);

	// Preview position during drag/resize
	let previewX = $state(0);
	let previewY = $state(0);
	let previewW = $state(1);
	let previewH = $state(1);

	// Store original positions before drag/resize for real-time displacement
	let originalPositions = $state<Map<number, { x: number; y: number }>>(new Map());
	let lastPreviewX = $state(-1);
	let lastPreviewY = $state(-1);
	let lastPreviewW = $state(-1);
	let lastPreviewH = $state(-1);
	let dragActuallyMoved = $state(false);

	// Convert pixel position to grid units
	function pixelToGrid(px: number, cellSize: number): number {
		return Math.round(px / (cellSize + gap));
	}

	// Convert grid units to pixel position
	function gridToPixel(grid: number, cellSize: number): number {
		return grid * (cellSize + gap);
	}

	// Get item position in pixels
	function getItemStyle(item: GridItemLayout): string {
		const left = gridToPixel(item.x, colWidth);
		const top = gridToPixel(item.y, rowHeight);
		const width = item.w * colWidth + (item.w - 1) * gap;
		const height = item.h * rowHeight + (item.h - 1) * gap;
		return `left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;`;
	}

	// Check for collisions with other items
	function hasCollision(
		item: GridItemLayout,
		testItem: { x: number; y: number; w: number; h: number }
	): boolean {
		if (item.id === dragItem?.id || item.id === resizeItem?.id) return false;
		return !(
			testItem.x + testItem.w <= item.x ||
			testItem.x >= item.x + item.w ||
			testItem.y + testItem.h <= item.y ||
			testItem.y >= item.y + item.h
		);
	}

	// Find valid position avoiding collisions
	function findValidPosition(
		x: number,
		y: number,
		w: number,
		h: number,
		excludeId: number
	): { x: number; y: number } {
		// Clamp to grid bounds
		x = Math.max(0, Math.min(x, cols - w));
		y = Math.max(0, y);

		const testItem = { x, y, w, h };

		// Check for collisions
		const hasAnyCollision = items.some((item) => item.id !== excludeId && hasCollision(item, testItem));

		if (!hasAnyCollision) {
			return { x, y };
		}

		// If collision, try to find nearby valid position
		for (let offsetY = 0; offsetY <= 10; offsetY++) {
			for (let offsetX = -2; offsetX <= 2; offsetX++) {
				const testX = Math.max(0, Math.min(x + offsetX, cols - w));
				const testY = Math.max(0, y + offsetY);
				const test = { x: testX, y: testY, w, h };
				const collision = items.some((item) => item.id !== excludeId && hasCollision(item, test));
				if (!collision) {
					return { x: testX, y: testY };
				}
			}
		}

		return { x, y };
	}

	// Push colliding items down (returns new array)
	function pushCollidingItems(movedItem: GridItemLayout, sourceItems: GridItemLayout[]): GridItemLayout[] {
		const newItems = sourceItems.map(item => ({ ...item }));
		let changed = true;
		let iterations = 0;
		const maxIterations = 100; // Prevent infinite loops

		while (changed && iterations < maxIterations) {
			changed = false;
			iterations++;
			for (const item of newItems) {
				if (item.id === movedItem.id) continue;

				if (hasCollision(item, movedItem)) {
					// Push this item down
					item.y = movedItem.y + movedItem.h;
					changed = true;
				}
			}
		}

		return newItems;
	}

	// Push items in real-time during drag/resize
	function pushItemsRealTime(ghostItem: { x: number; y: number; w: number; h: number }, excludeId: number) {
		// First restore original positions
		const baseItems = items.map(item => {
			const orig = originalPositions.get(item.id);
			if (orig && item.id !== excludeId) {
				return { ...item, x: orig.x, y: orig.y };
			}
			return { ...item };
		});

		// Then push items based on ghost position
		const movedItem = { ...ghostItem, id: excludeId };
		items = pushCollidingItems(movedItem as GridItemLayout, baseItems);
	}

	// Drag handlers
	function handleDragStart(e: PointerEvent, item: GridItemLayout) {
		if (e.button !== 0) return;
		e.preventDefault();

		dragItem = item;
		dragStartX = e.clientX;
		dragStartY = e.clientY;
		dragOffsetX = 0;
		dragOffsetY = 0;
		dragActuallyMoved = false;
		previewX = item.x;
		previewY = item.y;
		previewW = item.w;
		previewH = item.h;
		lastPreviewX = item.x;
		lastPreviewY = item.y;

		// Store original positions for all items
		originalPositions = new Map(items.map(i => [i.id, { x: i.x, y: i.y }]));

		(e.target as HTMLElement).setPointerCapture(e.pointerId);
		window.addEventListener('pointermove', handleDragMove);
		window.addEventListener('pointerup', handleDragEnd);
	}

	function handleDragMove(e: PointerEvent) {
		if (!dragItem) return;

		dragOffsetX = e.clientX - dragStartX;
		dragOffsetY = e.clientY - dragStartY;

		// Check if movement exceeds threshold (for click vs drag detection)
		if (!dragActuallyMoved && (Math.abs(dragOffsetX) > DRAG_THRESHOLD || Math.abs(dragOffsetY) > DRAG_THRESHOLD)) {
			dragActuallyMoved = true;
		}

		// Calculate new grid position
		const startLeft = gridToPixel(dragItem.x, colWidth);
		const startTop = gridToPixel(dragItem.y, rowHeight);
		const newLeft = startLeft + dragOffsetX;
		const newTop = startTop + dragOffsetY;

		const newX = pixelToGrid(newLeft, colWidth);
		const newY = pixelToGrid(newTop, rowHeight);

		// Clamp to valid range
		previewX = Math.max(0, Math.min(newX, cols - dragItem.w));
		previewY = Math.max(0, newY);

		// Push items in real-time if preview position changed
		if (previewX !== lastPreviewX || previewY !== lastPreviewY) {
			lastPreviewX = previewX;
			lastPreviewY = previewY;
			pushItemsRealTime({ x: previewX, y: previewY, w: dragItem.w, h: dragItem.h }, dragItem.id);
		}
	}

	function handleDragEnd(e: PointerEvent) {
		if (!dragItem) return;

		const clickedItemId = dragItem.id;

		// If we didn't actually drag (just clicked), treat as a click
		if (!dragActuallyMoved) {
			window.removeEventListener('pointermove', handleDragMove);
			window.removeEventListener('pointerup', handleDragEnd);
			dragItem = null;
			dragOffsetX = 0;
			dragOffsetY = 0;
			originalPositions = new Map();
			onitemclick?.(clickedItemId);
			return;
		}

		// Update item position (items are already pushed during drag)
		items = items.map((item) =>
			item.id === dragItem!.id ? { ...item, x: previewX, y: previewY } : item
		);

		onchange?.(items);

		window.removeEventListener('pointermove', handleDragMove);
		window.removeEventListener('pointerup', handleDragEnd);
		dragItem = null;
		dragOffsetX = 0;
		dragOffsetY = 0;
		originalPositions = new Map();
	}

	// Resize handlers
	function handleResizeStart(e: PointerEvent, item: GridItemLayout) {
		if (e.button !== 0) return;
		e.preventDefault();
		e.stopPropagation();

		resizeItem = item;
		resizeStartW = item.w;
		resizeStartH = item.h;
		resizeStartX = e.clientX;
		resizeStartY = e.clientY;
		resizePixelDeltaX = 0;
		resizePixelDeltaY = 0;
		previewX = item.x;
		previewY = item.y;
		previewW = item.w;
		previewH = item.h;
		lastPreviewW = item.w;
		lastPreviewH = item.h;

		// Store original positions for all items
		originalPositions = new Map(items.map(i => [i.id, { x: i.x, y: i.y }]));

		(e.target as HTMLElement).setPointerCapture(e.pointerId);
		window.addEventListener('pointermove', handleResizeMove);
		window.addEventListener('pointerup', handleResizeEnd);
	}

	function handleResizeMove(e: PointerEvent) {
		if (!resizeItem) return;

		const deltaX = e.clientX - resizeStartX;
		const deltaY = e.clientY - resizeStartY;

		// Track raw pixel delta for smooth visual feedback
		resizePixelDeltaX = deltaX;
		resizePixelDeltaY = deltaY;

		// Calculate new size in grid units
		const newW = Math.round(resizeStartW + deltaX / (colWidth + gap));
		const newH = Math.round(resizeStartH + deltaY / (rowHeight + gap));

		// Clamp to min/max
		previewW = Math.max(minW, Math.min(maxW, newW, cols - resizeItem.x));
		previewH = Math.max(minH, Math.min(maxH, newH));

		// Push items in real-time if preview size changed
		if (previewW !== lastPreviewW || previewH !== lastPreviewH) {
			lastPreviewW = previewW;
			lastPreviewH = previewH;
			pushItemsRealTime({ x: resizeItem.x, y: resizeItem.y, w: previewW, h: previewH }, resizeItem.id);
		}
	}

	function handleResizeEnd(e: PointerEvent) {
		if (!resizeItem) return;

		// Update item size (items are already pushed during resize)
		items = items.map((item) =>
			item.id === resizeItem!.id ? { ...item, w: previewW, h: previewH } : item
		);

		onchange?.(items);

		window.removeEventListener('pointermove', handleResizeMove);
		window.removeEventListener('pointerup', handleResizeEnd);
		resizeItem = null;
		resizePixelDeltaX = 0;
		resizePixelDeltaY = 0;
		originalPositions = new Map();
	}

	// Observe container width
	$effect(() => {
		if (!containerRef) return;

		const observer = new ResizeObserver((entries) => {
			containerWidth = entries[0].contentRect.width;
		});
		observer.observe(containerRef);

		return () => observer.disconnect();
	});
</script>

<div
	class="draggable-grid"
	bind:this={containerRef}
	style="height: {gridHeight}px;"
>
	{#each items as item (item.id)}
		{@const isDragTarget = dragItem?.id === item.id}
		{@const isDragging = isDragTarget && dragActuallyMoved}
		{@const isResizing = resizeItem?.id === item.id}
		{@const isActive = isDragging || isResizing}
		{@const itemWidth = item.w * colWidth + (item.w - 1) * gap}
		{@const itemHeight = item.h * rowHeight + (item.h - 1) * gap}

		<!-- Preview placeholder during drag/resize -->
		{#if isDragging || isResizing}
			<div
				class="grid-item-preview"
				data-size="{isResizing ? previewW : previewW}×{isResizing ? previewH : previewH}"
				style="left: {gridToPixel(isResizing ? item.x : previewX, colWidth)}px; top: {gridToPixel(isResizing ? item.y : previewY, rowHeight)}px; width: {previewW * colWidth + (previewW - 1) * gap}px; height: {previewH * rowHeight + (previewH - 1) * gap}px;"
			></div>
		{/if}

		<!-- Actual item -->
		{@const baseWidth = item.w * colWidth + (item.w - 1) * gap}
		{@const baseHeight = item.h * rowHeight + (item.h - 1) * gap}
		{@const minPixelW = minW * colWidth + (minW - 1) * gap}
		{@const maxPixelW = Math.min(maxW, cols - item.x) * colWidth + (Math.min(maxW, cols - item.x) - 1) * gap}
		{@const minPixelH = minH * rowHeight + (minH - 1) * gap}
		{@const maxPixelH = maxH * rowHeight + (maxH - 1) * gap}
		{@const currentWidth = isResizing ? Math.max(minPixelW, Math.min(maxPixelW, baseWidth + resizePixelDeltaX)) : baseWidth}
		{@const currentHeight = isResizing ? Math.max(minPixelH, Math.min(maxPixelH, baseHeight + resizePixelDeltaY)) : baseHeight}
		<div
			class="grid-item"
			class:dragging={isDragging}
			class:resizing={isResizing}
			style="left: {gridToPixel(item.x, colWidth)}px; top: {gridToPixel(item.y, rowHeight)}px; width: {currentWidth}px; height: {currentHeight}px; {isDragTarget ? `transform: translate(${dragOffsetX}px, ${dragOffsetY}px);` : ''}"
			onpointerdown={(e) => {
				// Check if clicking on resize handle area (bottom-right 28x28 corner)
				const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
				const x = e.clientX - rect.left;
				const y = e.clientY - rect.top;
				const isInResizeArea = x > rect.width - 28 && y > rect.height - 28;

				if (isInResizeArea) {
					handleResizeStart(e, item);
				} else {
					handleDragStart(e, item);
				}
			}}
		>
			<!-- Content -->
			<div class="grid-item-content">
				{@render children({ item, width: itemWidth, height: itemHeight })}
			</div>

			<!-- Resize handle visual indicator -->
			<div class="tile-resize-handle">
				<svg viewBox="0 0 10 10" fill="currentColor">
					<path d="M9 1v8H1" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/>
				</svg>
			</div>
		</div>
	{/each}
</div>

<style>
	.draggable-grid {
		position: relative;
		width: 100%;
		min-height: 300px;
	}

	.grid-item {
		position: absolute;
		backface-visibility: hidden;
		-webkit-backface-visibility: hidden;
		cursor: pointer;
		touch-action: none;
	}

	.grid-item.dragging {
		z-index: 100;
		box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
		cursor: grabbing;
	}

	.grid-item.dragging,
	.grid-item.dragging * {
		cursor: grabbing !important;
	}

	.grid-item.resizing {
		z-index: 100;
	}

	.grid-item.resizing,
	.grid-item.resizing * {
		cursor: grabbing !important;
	}

	.grid-item.dragging *,
	.grid-item.resizing * {
		user-select: none;
		-webkit-user-select: none;
	}

	.grid-item-content {
		width: 100%;
		height: 100%;
		overflow: hidden;
	}

	.tile-resize-handle {
		position: absolute;
		bottom: 0;
		right: 0;
		width: 28px;
		height: 28px;
		cursor: grab;
		z-index: 20;
		display: flex;
		align-items: flex-end;
		justify-content: flex-end;
		padding: 6px;
		opacity: 0;
		transition: opacity 0.2s ease;
		touch-action: none;
		user-select: none;
		-webkit-user-select: none;
		background: transparent;
	}

	.tile-resize-handle svg {
		width: 12px;
		height: 12px;
		color: hsl(var(--muted-foreground) / 0.6);
		transition: color 0.2s ease;
		pointer-events: none;
	}

	.grid-item:hover .tile-resize-handle {
		opacity: 1;
	}

	.tile-resize-handle:hover svg {
		color: hsl(var(--primary));
	}

	.grid-item-preview {
		position: absolute;
		background: hsl(var(--primary) / 0.12);
		border: 2px dashed hsl(var(--primary) / 0.6);
		border-radius: 8px;
		z-index: 0;
		transition: left 0.15s ease, top 0.15s ease, width 0.15s ease, height 0.15s ease;
		box-shadow:
			inset 0 0 20px hsl(var(--primary) / 0.1),
			0 0 15px hsl(var(--primary) / 0.15);
	}

	.grid-item-preview::before {
		content: '';
		position: absolute;
		inset: 0;
		border-radius: 6px;
		background: repeating-linear-gradient(
			45deg,
			transparent,
			transparent 8px,
			hsl(var(--primary) / 0.05) 8px,
			hsl(var(--primary) / 0.05) 16px
		);
		animation: stripes 0.5s linear infinite;
	}

	@keyframes stripes {
		0% {
			background-position: 0 0;
		}
		100% {
			background-position: 22.6px 0;
		}
	}

	/* Size indicator badge on preview */
	.grid-item-preview::after {
		content: attr(data-size);
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background: hsl(var(--primary));
		color: hsl(var(--primary-foreground));
		padding: 4px 10px;
		border-radius: 6px;
		font-size: 12px;
		font-weight: 600;
		white-space: nowrap;
		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
	}
</style>
