<script lang="ts">
	import { type Snippet } from 'svelte';
	import {
		type Camera,
		type CanvasObject,
		type GridConfig,
		type ViewportSize,
		visibleObjects,
		screenToWorld,
		snap,
		clampZoom,
		ZOOM_MIN,
		ZOOM_MAX,
		ZOOM_FIT_MIN,
	} from './types.js';

	/**
	 * Canvas Primitive — felles fritt-canvas underlag.
	 *
	 * Håndterer kamera (pan/zoom), viewport culling, objekt-plassering
	 * og interaksjon. Consumer bestemmer hva som rendres via renderObject snippet.
	 */

	interface Props {
		/** Objects to render on the canvas */
		objects: CanvasObject[];
		/** Render callback for each visible object */
		renderObject: Snippet<[CanvasObject]>;
		/** Grid configuration */
		grid?: GridConfig;
		/** Initial camera position */
		initialCamera?: Camera;
		/** Callback when an object is moved (drag-drop) */
		onObjectMove?: (id: string, x: number, y: number) => void;
		/** Callback when camera changes */
		onCameraChange?: (camera: Camera) => void;
		/** Callback when selection changes */
		onSelectionChange?: (ids: string[]) => void;
		/** Callback when grid is toggled */
		onGridChange?: (enabled: boolean) => void;
	}

	let {
		objects,
		renderObject,
		grid = { enabled: false, size: 20 },
		initialCamera = { x: 0, y: 0, zoom: 1.0 },
		onObjectMove,
		onCameraChange,
		onGridChange,
		onSelectionChange,
	}: Props = $props();

	// --- State ---
	let camera = $state<Camera>({ ...initialCamera });
	let selectedIds = $state<Set<string>>(new Set());
	let isFullscreen = $state(false);
	let viewport = $state<ViewportSize>({ width: 0, height: 0 });

	// Interaction state (not reactive — internal bookkeeping)
	let isPanning = $state(false);
	let isDragging = $state(false);
	let isLassoing = $state(false);
	let spaceHeld = $state(false);
	let dragTargetId: string | null = null;
	let topZCounter = $state(1);
	let zOrderMap = $state(new Map<string, number>());
	let dragStartWorld = { x: 0, y: 0 };
	let dragObjectStart = { x: 0, y: 0 };
	let dragGroupStarts = new Map<string, { x: number; y: number }>();
	let panStart = { x: 0, y: 0 };
	let cameraStart = { x: 0, y: 0 };
	let lassoStart = { x: 0, y: 0 };
	let lassoEnd = $state({ x: 0, y: 0 });

	// Touch tracking for pinch-zoom
	let activeTouches = new Map<number, { x: number; y: number }>();
	let lastPinchDist = 0;
	let lastPinchCenter = { x: 0, y: 0 };

	// DOM refs
	let containerEl: HTMLDivElement | undefined = $state();

	// --- Derived ---
	let visibleObjs = $derived(visibleObjects(objects, camera, viewport));

	let transformStyle = $derived(
		`translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`
	);

	let lassoRect = $derived.by(() => {
		if (!isLassoing) return null;
		const x = Math.min(lassoStart.x, lassoEnd.x);
		const y = Math.min(lassoStart.y, lassoEnd.y);
		const w = Math.abs(lassoEnd.x - lassoStart.x);
		const h = Math.abs(lassoEnd.y - lassoStart.y);
		return { x, y, width: w, height: h };
	});

	// Notify consumers of camera changes
	$effect(() => {
		// Access camera fields to create dependency
		const _ = camera.x + camera.y + camera.zoom;
		onCameraChange?.(camera);
	});

	// ResizeObserver for viewport size
	$effect(() => {
		if (!containerEl) return;
		const ro = new ResizeObserver((entries) => {
			for (const entry of entries) {
				viewport.width = entry.contentRect.width;
				viewport.height = entry.contentRect.height;
			}
		});
		ro.observe(containerEl);
		return () => ro.disconnect();
	});

	// Keyboard listeners
	$effect(() => {
		function onKeyDown(e: KeyboardEvent) {
			if (e.code === 'Space' && !e.repeat) {
				spaceHeld = true;
				e.preventDefault();
			}
			if (e.code === 'Escape' && isFullscreen) {
				isFullscreen = false;
			}
			// 'g' toggles grid snap
			if (e.code === 'KeyG' && !e.ctrlKey && !e.metaKey) {
				grid = { ...grid, enabled: !grid.enabled };
				onGridChange?.(grid.enabled);
			}
		}
		function onKeyUp(e: KeyboardEvent) {
			if (e.code === 'Space') {
				spaceHeld = false;
			}
		}
		window.addEventListener('keydown', onKeyDown);
		window.addEventListener('keyup', onKeyUp);
		return () => {
			window.removeEventListener('keydown', onKeyDown);
			window.removeEventListener('keyup', onKeyUp);
		};
	});

	// --- Zoom ---
	function handleWheel(e: WheelEvent) {
		e.preventDefault();
		const zoomFactor = e.deltaY > 0 ? 0.92 : 1.08;
		zoomAt(e.clientX, e.clientY, zoomFactor);
	}

	function zoomAt(screenX: number, screenY: number, factor: number) {
		const rect = containerEl?.getBoundingClientRect();
		if (!rect) return;
		const cx = screenX - rect.left;
		const cy = screenY - rect.top;

		const newZoom = clampZoom(camera.zoom * factor);
		const scale = newZoom / camera.zoom;

		camera = {
			x: cx - scale * (cx - camera.x),
			y: cy - scale * (cy - camera.y),
			zoom: newZoom,
		};
	}

	// --- Pointer events ---
	function handlePointerDown(e: PointerEvent) {
		if (!containerEl) return;
		// Ignore clicks on toolbar and other UI overlays
		if ((e.target as HTMLElement).closest('.canvas-toolbar')) return;

		const rect = containerEl.getBoundingClientRect();
		const sx = e.clientX - rect.left;
		const sy = e.clientY - rect.top;

		// Middle button or space+left = pan
		if (e.button === 1 || (e.button === 0 && spaceHeld)) {
			isPanning = true;
			panStart = { x: e.clientX, y: e.clientY };
			cameraStart = { x: camera.x, y: camera.y };
			containerEl.setPointerCapture(e.pointerId);
			e.preventDefault();
			return;
		}

		// Left button on object = drag object
		if (e.button === 0) {
			const target = (e.target as HTMLElement).closest('[data-canvas-object-id]');
			if (target) {
				const id = target.getAttribute('data-canvas-object-id')!;
				isDragging = true;
				dragTargetId = id;
				const world = screenToWorld(sx, sy, camera);
				dragStartWorld = { x: world.x, y: world.y };
				const obj = objects.find(o => o.id === id);
				if (obj) {
					dragObjectStart = { x: obj.x, y: obj.y };
				}

				// Selection logic
				if (e.shiftKey) {
					const newSet = new Set(selectedIds);
					if (newSet.has(id)) newSet.delete(id);
					else newSet.add(id);
					selectedIds = newSet;
				} else if (!selectedIds.has(id)) {
					selectedIds = new Set([id]);
				}
				onSelectionChange?.([...selectedIds]);

				// Bring to front
				topZCounter++;
				zOrderMap = new Map(zOrderMap);
				for (const sid of selectedIds) {
					zOrderMap.set(sid, topZCounter);
				}

				// Record start positions for all selected objects (group drag)
				dragGroupStarts = new Map();
				for (const sid of selectedIds) {
					const sobj = objects.find(o => o.id === sid);
					if (sobj) dragGroupStarts.set(sid, { x: sobj.x, y: sobj.y });
				}

				containerEl.setPointerCapture(e.pointerId);
				e.preventDefault();
				return;
			}

			// Left button on empty space = pan (or lasso with shift)
			if (e.shiftKey) {
				// Shift+drag = lasso selection
				isLassoing = true;
				lassoStart = { x: sx, y: sy };
				lassoEnd = { x: sx, y: sy };
			} else {
				// Normal drag on empty space = pan
				isPanning = true;
				panStart = { x: e.clientX, y: e.clientY };
				cameraStart = { x: camera.x, y: camera.y };
				// Deselect all
				selectedIds = new Set();
				onSelectionChange?.([]);
			}
			containerEl.setPointerCapture(e.pointerId);
			e.preventDefault();
		}
	}

	function handlePointerMove(e: PointerEvent) {
		if (!containerEl) return;

		if (isPanning) {
			const dx = e.clientX - panStart.x;
			const dy = e.clientY - panStart.y;
			camera = {
				...camera,
				x: cameraStart.x + dx,
				y: cameraStart.y + dy,
			};
			return;
		}

		if (isDragging && dragTargetId) {
			const rect = containerEl.getBoundingClientRect();
			const sx = e.clientX - rect.left;
			const sy = e.clientY - rect.top;
			const world = screenToWorld(sx, sy, camera);
			const dx = world.x - dragStartWorld.x;
			const dy = world.y - dragStartWorld.y;

			// Move all selected objects together
			for (const [sid, start] of dragGroupStarts) {
				let newX = start.x + dx;
				let newY = start.y + dy;
				if (grid.enabled) {
					newX = snap(newX, grid.size);
					newY = snap(newY, grid.size);
				}
				onObjectMove?.(sid, newX, newY);
			}

			// Edge-pan: scroll canvas when dragging near edges
			const edgeMargin = 40;
			const edgeSpeed = 8;
			let panDx = 0, panDy = 0;
			if (sx < edgeMargin) panDx = edgeSpeed;
			if (sx > rect.width - edgeMargin) panDx = -edgeSpeed;
			if (sy < edgeMargin) panDy = edgeSpeed;
			if (sy > rect.height - edgeMargin) panDy = -edgeSpeed;
			if (panDx || panDy) {
				camera = { ...camera, x: camera.x + panDx, y: camera.y + panDy };
			}
			return;
		}

		if (isLassoing) {
			const rect = containerEl.getBoundingClientRect();
			lassoEnd = { x: e.clientX - rect.left, y: e.clientY - rect.top };
			return;
		}
	}

	function handlePointerUp(e: PointerEvent) {
		if (isPanning) {
			isPanning = false;
			return;
		}

		if (isDragging) {
			isDragging = false;
			dragTargetId = null;
			return;
		}

		if (isLassoing && lassoRect) {
			// Select objects within lasso (in screen space)
			const newSelection = new Set(selectedIds);
			for (const obj of visibleObjs) {
				const screenPos = {
					x: obj.x * camera.zoom + camera.x,
					y: obj.y * camera.zoom + camera.y,
					width: obj.width * camera.zoom,
					height: obj.height * camera.zoom,
				};
				// Check if object intersects lasso rect
				if (
					screenPos.x < lassoRect.x + lassoRect.width &&
					screenPos.x + screenPos.width > lassoRect.x &&
					screenPos.y < lassoRect.y + lassoRect.height &&
					screenPos.y + screenPos.height > lassoRect.y
				) {
					newSelection.add(obj.id);
				}
			}
			selectedIds = newSelection;
			onSelectionChange?.([...selectedIds]);
			isLassoing = false;
			return;
		}
	}

	// --- Touch: pinch-zoom and two-finger pan ---
	function handleTouchStart(e: TouchEvent) {
		for (const touch of e.changedTouches) {
			activeTouches.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
		}
		if (activeTouches.size === 2) {
			const [a, b] = [...activeTouches.values()];
			lastPinchDist = Math.hypot(b.x - a.x, b.y - a.y);
			lastPinchCenter = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
			e.preventDefault();
		}
	}

	function handleTouchMove(e: TouchEvent) {
		for (const touch of e.changedTouches) {
			activeTouches.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
		}
		if (activeTouches.size === 2) {
			const [a, b] = [...activeTouches.values()];
			const dist = Math.hypot(b.x - a.x, b.y - a.y);
			const center = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };

			// Pinch zoom
			if (lastPinchDist > 0) {
				const factor = dist / lastPinchDist;
				zoomAt(center.x, center.y, factor);
			}

			// Two-finger pan
			const dx = center.x - lastPinchCenter.x;
			const dy = center.y - lastPinchCenter.y;
			camera = { ...camera, x: camera.x + dx, y: camera.y + dy };

			lastPinchDist = dist;
			lastPinchCenter = center;
			e.preventDefault();
		}
	}

	function handleTouchEnd(e: TouchEvent) {
		for (const touch of e.changedTouches) {
			activeTouches.delete(touch.identifier);
		}
		if (activeTouches.size < 2) {
			lastPinchDist = 0;
		}
	}

	// --- Toolbar zoom (zoom toward viewport center) ---
	function zoomStep(delta: number) {
		const cx = viewport.width / 2;
		const cy = viewport.height / 2;
		const newZoom = clampZoom(camera.zoom * (1 + delta));
		const scale = newZoom / camera.zoom;
		camera = {
			x: cx - scale * (cx - camera.x),
			y: cy - scale * (cy - camera.y),
			zoom: newZoom,
		};
	}

	// --- Fullscreen ---
	function toggleFullscreen() {
		isFullscreen = !isFullscreen;
	}

	// --- Public API via bindable ---
	export function getCamera(): Camera {
		return { ...camera };
	}

	export function setCamera(cam: Camera) {
		camera = { ...cam, zoom: clampZoom(cam.zoom) };
	}

	export function zoomToFit(padding: number = 50) {
		if (objects.length === 0) return;
		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
		for (const obj of objects) {
			minX = Math.min(minX, obj.x);
			minY = Math.min(minY, obj.y);
			maxX = Math.max(maxX, obj.x + obj.width);
			maxY = Math.max(maxY, obj.y + obj.height);
		}
		const contentW = maxX - minX;
		const contentH = maxY - minY;
		if (contentW <= 0 || contentH <= 0) return;

		const zoomX = (viewport.width - padding * 2) / contentW;
		const zoomY = (viewport.height - padding * 2) / contentH;
		const zoom = clampZoom(Math.min(zoomX, zoomY));

		camera = {
			x: (viewport.width - contentW * zoom) / 2 - minX * zoom,
			y: (viewport.height - contentH * zoom) / 2 - minY * zoom,
			zoom,
		};
	}
</script>

<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
	bind:this={containerEl}
	class="canvas-container"
	class:canvas-fullscreen={isFullscreen}
	class:canvas-panning={isPanning || spaceHeld}
	onwheel={handleWheel}
	onpointerdown={handlePointerDown}
	onpointermove={handlePointerMove}
	onpointerup={handlePointerUp}
	ondblclick={(e) => {
		if (!(e.target as HTMLElement).closest('[data-canvas-object-id]') &&
			!(e.target as HTMLElement).closest('.canvas-toolbar')) {
			zoomToFit();
		}
	}}
	ontouchstart={handleTouchStart}
	ontouchmove={handleTouchMove}
	ontouchend={handleTouchEnd}
	role="application"
	aria-label="Canvas"
>
	<!-- Toolbar -->
	<!-- svelte-ignore a11y_click_events_have_key_events -->
	<div class="canvas-toolbar" onclick={(e) => e.stopPropagation()} onpointerdown={(e) => e.stopPropagation()}>
		<button
			class="canvas-toolbar-btn"
			onclick={() => zoomStep(-0.15)}
			title="Zoom ut (−15%)"
			aria-label="Zoom ut"
		>−</button>
		<span class="canvas-toolbar-zoom">{Math.round(camera.zoom * 100)}%</span>
		<button
			class="canvas-toolbar-btn"
			onclick={() => zoomStep(0.15)}
			title="Zoom inn (+15%)"
			aria-label="Zoom inn"
		>+</button>
		<button
			class="canvas-toolbar-btn"
			onclick={() => zoomToFit()}
			title="Tilpass visning"
			aria-label="Tilpass visning"
		>⊡</button>
		<div class="canvas-toolbar-sep"></div>
		<button
			class="canvas-toolbar-btn"
			class:canvas-toolbar-active={grid.enabled}
			onclick={() => { grid = { ...grid, enabled: !grid.enabled }; onGridChange?.(grid.enabled); }}
			title="Snap-to-grid (G)"
			aria-label="Snap-to-grid"
		>#</button>
		<button
			class="canvas-toolbar-btn"
			onclick={toggleFullscreen}
			title="Fullskjerm"
			aria-label="Fullskjerm"
		>{isFullscreen ? '⊖' : '⊕'}</button>
	</div>

	<!-- Grid background (when enabled) -->
	{#if grid.enabled}
		<div
			class="canvas-grid"
			style:background-size="{grid.size * camera.zoom}px {grid.size * camera.zoom}px"
			style:background-position="{camera.x}px {camera.y}px"
		></div>
	{/if}

	<!-- World layer — CSS transformed -->
	<div
		class="canvas-world"
		style:transform={transformStyle}
	>
		{#each visibleObjs as obj (obj.id)}
			<div
				class="canvas-object"
				class:canvas-object-selected={selectedIds.has(obj.id)}
				data-canvas-object-id={obj.id}
				style:left="{obj.x}px"
				style:top="{obj.y}px"
				style:width="{obj.width}px"
				style:height="{obj.height}px"
				style:z-index={zOrderMap.get(obj.id) ?? 1}
			>
				{@render renderObject(obj)}
			</div>
		{/each}
	</div>

	<!-- Lasso selection overlay (screen space) -->
	{#if isLassoing && lassoRect}
		<div
			class="canvas-lasso"
			style:left="{lassoRect.x}px"
			style:top="{lassoRect.y}px"
			style:width="{lassoRect.width}px"
			style:height="{lassoRect.height}px"
		></div>
	{/if}

	<!-- Mobile: touch hint -->
	{#if viewport.width < 768}
		<div class="canvas-touch-hint">
			Bruk to fingre for å panorere og zoome
		</div>
	{/if}
</div>

<style>
	.canvas-container {
		position: relative;
		width: 100%;
		height: 100%;
		overflow: hidden;
		background: var(--color-bg, #0a0a0b);
		touch-action: none; /* We handle all touch ourselves */
		user-select: none;
		cursor: default;
	}

	.canvas-container.canvas-panning {
		cursor: grab;
	}

	.canvas-container.canvas-fullscreen {
		position: fixed;
		inset: 0;
		z-index: 9999;
		background: var(--color-bg, #0a0a0b);
	}

	.canvas-world {
		position: absolute;
		top: 0;
		left: 0;
		transform-origin: 0 0;
		will-change: transform;
	}

	.canvas-object {
		position: absolute;
		cursor: default;
		overflow: visible;
	}

	.canvas-object-selected {
		outline: 1px solid rgba(99, 102, 241, 0.4);
		outline-offset: 1px;
		border-radius: 4px;
	}

	/* Grid pattern */
	.canvas-grid {
		position: absolute;
		inset: 0;
		pointer-events: none;
		background-image:
			linear-gradient(to right, rgba(99, 102, 241, 0.12) 1px, transparent 1px),
			linear-gradient(to bottom, rgba(99, 102, 241, 0.12) 1px, transparent 1px);
	}

	/* Lasso */
	.canvas-lasso {
		position: absolute;
		border: 1px dashed #3b82f6;
		background: rgba(59, 130, 246, 0.08);
		pointer-events: none;
		z-index: 10;
	}

	/* Toolbar */
	.canvas-toolbar {
		position: absolute;
		bottom: 12px;
		left: 50%;
		transform: translateX(-50%);
		z-index: 20;
		display: flex;
		align-items: center;
		gap: 4px;
		padding: 4px 8px;
		background: var(--color-surface, #1c1c20);
		border: 1px solid var(--color-border, #2a2a2e);
		border-radius: 8px;
		box-shadow: 0 1px 3px rgba(0,0,0,0.1);
	}

	.canvas-toolbar-btn {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 32px;
		height: 32px;
		border: none;
		background: transparent;
		border-radius: 6px;
		cursor: pointer;
		font-size: 16px;
		color: var(--color-text-muted, #8a8a96);
		transition: background 0.15s;
	}

	.canvas-toolbar-btn:hover {
		background: var(--color-surface-hover, #242428);
	}

	.canvas-toolbar-active {
		background: var(--color-accent-glow, rgba(99, 102, 241, 0.15));
		color: var(--color-accent, #3b82f6);
	}

	.canvas-toolbar-zoom {
		font-size: 12px;
		color: var(--color-text-muted, #8a8a96);
		min-width: 40px;
		text-align: center;
		font-variant-numeric: tabular-nums;
	}

	.canvas-toolbar-sep {
		width: 1px;
		height: 20px;
		background: var(--color-border, #2a2a2e);
		margin: 0 4px;
	}

	/* Touch hint for mobile */
	.canvas-touch-hint {
		position: absolute;
		top: 12px;
		left: 50%;
		transform: translateX(-50%);
		z-index: 20;
		padding: 4px 12px;
		background: rgba(0,0,0,0.6);
		color: white;
		font-size: 12px;
		border-radius: 16px;
		pointer-events: none;
		opacity: 0.8;
	}

	/* Responsive touch targets */
	@media (max-width: 768px) {
		.canvas-toolbar-btn {
			width: 44px;
			height: 44px;
			font-size: 20px;
		}

		.canvas-toolbar {
			bottom: 16px;
			gap: 2px;
			padding: 4px;
		}
	}

	@media (min-width: 768px) and (max-width: 1024px) {
		.canvas-toolbar-btn {
			width: 38px;
			height: 38px;
		}
	}
</style>
