<script module lang="ts">
	import type { Component } from "svelte";
	import type { SvelteHTMLElements } from "svelte/elements";
	import { tv, type VariantProps } from "tailwind-variants";

	export interface TabDef {
		id: string;
		label: string;
		href: string;
		icon?: Component<SvelteHTMLElements["svg"]>;
		count?: number;
	}

	export const tabs = tv({
		slots: {
			nav: "",
			plate:
				"pointer-events-none absolute top-0 left-0 rounded-t border border-b-0 border-border-default bg-background-default",
			item: "flex items-center no-underline hover:no-underline",
			icon: "size-4",
			label: "flex flex-col",
			ghost: "invisible h-0 overflow-hidden font-medium select-none",
			count: "rounded-sm px-1 typography-paragraph-small"
		},
		variants: {
			vertical: {
				true: {
					nav: "h-fit divide-y divide-border-default overflow-hidden rounded-sm border border-border-default",
					item: "gap-3 px-3 py-2 typography-paragraph-small",
					icon: "shrink-0",
					count: "ml-auto"
				},
				false: {
					nav: "relative z-10 flex w-full overflow-x-auto overflow-y-hidden pl-4",
					item: "relative mr-1 rounded-t border border-b-0 border-transparent px-4 pt-1 pb-1.25 whitespace-nowrap text-foreground-default",
					icon: "mr-2",
					count: "ml-1"
				}
			},
			selected: {
				true: {
					count: "bg-background-muted"
				},
				false: {
					count: "bg-background-strong"
				}
			},
			ready: { true: { plate: "opacity-100" }, false: { plate: "opacity-0" } },
			overlapBottom: { true: { nav: "-mb-px" }, false: {} }
		},
		compoundVariants: [
			{
				vertical: true,
				selected: true,
				class: { item: "bg-background-default text-foreground-default dark:bg-background-inset" }
			},
			{
				vertical: true,
				selected: false,
				class: {
					item: "bg-background-inset text-foreground-muted hover:text-foreground-default dark:bg-background-default"
				}
			},
			{
				vertical: false,
				selected: true,
				class: { item: "[-webkit-text-stroke:0.3px_currentColor]" }
			},
			{
				vertical: false,
				selected: true,
				ready: false,
				class: { item: "border-border-default bg-background-default" }
			},
			{
				vertical: false,
				selected: false,
				class: { item: "hover:bg-background-muted hover:dark:bg-background-subtle/50" }
			}
		],
		defaultVariants: {
			vertical: false,
			selected: false,
			ready: false,
			overlapBottom: false
		}
	});

	export type TabsVariants = VariantProps<typeof tabs>;
</script>

<script lang="ts">
	import { resolve } from "$app/paths";
	import { navigating } from "$app/state";
	import type { ResolvedPathname } from "$app/types";
	import { Spring, prefersReducedMotion } from "svelte/motion";

	interface Props {
		tabs: TabDef[];
		active: string;
		label?: string;
		vertical?: boolean;
		overlapBottom?: boolean;
	}

	let { tabs: defs, active, label, vertical = false, overlapBottom = false }: Props = $props();

	const items = $derived(
		defs.map((tab) => ({
			...tab,
			url: (tab.href.startsWith("/") ? resolve(tab.href as "/") : tab.href) as ResolvedPathname
		}))
	);

	const selected = $derived.by(() => {
		const dest = navigating.to?.url;
		if (!dest) return active;

		let best: string | undefined;
		let bestLength = -1;
		for (const item of items) {
			if (item.url === dest.pathname + dest.search) return item.id;
			if (item.url.includes("?")) continue;
			const owns = dest.pathname === item.url || dest.pathname.startsWith(`${item.url}/`);
			if (owns && item.url.length > bestLength) {
				best = item.id;
				bestLength = item.url.length;
			}
		}
		return best ?? active;
	});

	let nav = $state<HTMLElement>();
	let nodes = $state<(HTMLElement | undefined)[]>([]);
	let box = $state<{ x: number; width: number; height: number }>();
	let ready = $state(false);

	const plate = new Spring({ x: 0, width: 0 }, { stiffness: 0.145, damping: 0.65, precision: 0.1 });

	const measure = () => {
		const node = nodes[items.findIndex((item) => item.id === selected)];
		if (!node) return;
		box = { x: node.offsetLeft, width: node.offsetWidth, height: node.offsetHeight };
	};

	$effect(() => {
		if (vertical) return;
		measure();
	});

	$effect(() => {
		if (vertical || !nav) return;
		const observer = new ResizeObserver(measure);
		observer.observe(nav);
		for (const node of nodes) if (node) observer.observe(node);
		return () => observer.disconnect();
	});

	let placed = false;
	$effect(() => {
		if (!box) return;
		plate.set({ x: box.x, width: box.width }, { instant: !placed || prefersReducedMotion.current });
		placed = true;
		ready = true;
	});

	const style = $derived(tabs({ vertical, ready, overlapBottom }));
</script>

<nav class={style.nav()} aria-label={label} bind:this={nav}>
	{#if !vertical}
		<span
			aria-hidden="true"
			class={style.plate()}
			style="translate: {plate.current.x}px; width: {plate.current.width}px; height: {box?.height ??
				0}px"
		></span>
	{/if}
	{#each items as tab, i (tab.id)}
		{@const isSelected = selected === tab.id}
		{@const Glyph = tab.icon}
		<a
			bind:this={nodes[i]}
			href={tab.url}
			aria-current={active === tab.id ? "page" : undefined}
			class={style.item({ selected: isSelected })}
		>
			{#if Glyph}
				<Glyph class={style.icon()} aria-hidden="true" />
			{/if}
			{#if vertical}
				{tab.label}
			{:else}
				<span class={style.label()}>
					{tab.label}
					<span aria-hidden="true" class={style.ghost()}>{tab.label}</span>
				</span>
			{/if}
			{#if tab.count}
				<span class={style.count({ selected: isSelected })}>{tab.count}</span>
			{/if}
		</a>
	{/each}
</nav>
