<script lang="ts">
	type Props = {
		label: string;
		src: string;
	};
	let { label, src }: Props = $props();

	let audio: HTMLAudioElement | undefined = $state();
	let paused = $state(true);
	let currentTime = $state(0);
	let duration = $state(0);
	let loading = $state(false);
	let errored = $state(false);

	function toggle() {
		if (errored) {
			retry();
			return;
		}
		if (!audio) return;
		if (audio.paused) {
			audio.play().catch(() => undefined);
		} else {
			audio.pause();
		}
	}

	function retry() {
		if (!audio) return;
		errored = false;
		loading = true;
		audio.load();
		audio.play().catch(() => undefined);
	}

	function onPlaying() {
		loading = false;
		// Single-player invariant: pause every other audio element on the page.
		if (audio) {
			for (const other of document.querySelectorAll('audio')) {
				if (other !== audio && !other.paused) other.pause();
			}
		}
	}

	function fmt(s: number) {
		if (!isFinite(s) || s < 0) return '0:00';
		const total = Math.floor(s);
		const h = Math.floor(total / 3600);
		const m = Math.floor((total % 3600) / 60);
		const sec = total % 60;
		return h > 0
			? `${h}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
			: `${m}:${sec.toString().padStart(2, '0')}`;
	}
</script>

<div class="player" class:playing={!paused && !loading && !errored} class:loading class:errored>
	<button
		type="button"
		class="play"
		onclick={toggle}
		aria-label={errored
			? `Tap to retry ${label}`
			: loading
			? `Loading ${label}`
			: paused
			? `Play ${label}`
			: `Pause ${label}`}
		aria-pressed={!paused}
	>
		{#if errored}
			<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
				<path d="M12 2L1 21h22L12 2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
				<path d="M12 10v5M12 17.5v.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
			</svg>
		{:else if loading}
			<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" class="spin">
				<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2.5" stroke-opacity="0.25" />
				<path d="M21 12a9 9 0 00-9-9" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
			</svg>
		{:else if paused}
			<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
				<path d="M8 5l11 7-11 7V5z" fill="currentColor" />
			</svg>
		{:else}
			<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
				<path d="M7 5h3v14H7zM14 5h3v14h-3z" fill="currentColor" />
			</svg>
		{/if}
	</button>

	<div class="meta">
		<span class="label">{label}</span>
		<input
			type="range"
			min="0"
			max={duration && isFinite(duration) ? duration : 0}
			step="0.1"
			bind:value={currentTime}
			disabled={!duration || !isFinite(duration)}
			aria-label="Seek {label}"
			aria-valuetext="{fmt(currentTime)} of {fmt(duration)}"
		/>
		<span class="time tabular">{fmt(currentTime)} / {fmt(duration)}</span>
	</div>

	<audio
		bind:this={audio}
		bind:paused
		bind:currentTime
		bind:duration
		onloadstart={() => { loading = true; errored = false; }}
		oncanplay={() => { loading = false; }}
		onwaiting={() => { loading = true; }}
		onplaying={onPlaying}
		onerror={() => { loading = false; errored = true; }}
		onended={() => { paused = true; }}
		preload="metadata"
		{src}
	></audio>
</div>

<style>
	.player {
		display: grid;
		grid-template-columns: auto 1fr;
		align-items: center;
		gap: 0.85rem;
		padding: 0.75rem 1rem;
		background: var(--bg-card);
		border-radius: var(--radius);
		box-shadow: var(--shadow);
	}
	.play {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		border-radius: 50%;
		background: var(--ink);
		color: #fff;
		flex-shrink: 0;
		transition: background 0.15s ease;
	}
	.player.errored .play {
		background: var(--title);
	}
	.play:hover {
		background: var(--ink-deep);
	}
	.play:focus-visible {
		outline: 2px solid var(--title);
		outline-offset: 3px;
	}
	.meta {
		display: grid;
		grid-template-columns: auto 1fr auto;
		align-items: center;
		gap: 0.6rem;
		min-width: 0;
	}
	.label {
		font-family: var(--font-serif);
		font-size: 0.95rem;
		font-weight: 600;
		color: var(--ink);
		white-space: nowrap;
	}
	.time {
		font-size: 0.8rem;
		color: var(--ink-mute);
		white-space: nowrap;
	}
	.tabular {
		font-variant-numeric: tabular-nums;
	}
	input[type='range'] {
		-webkit-appearance: none;
		appearance: none;
		width: 100%;
		height: 5px;
		background: var(--rule);
		border-radius: 3px;
		outline: none;
		margin: 0;
		min-width: 0;
	}
	input[type='range']:focus-visible {
		outline: 2px solid var(--title);
		outline-offset: 3px;
	}
	input[type='range']::-webkit-slider-thumb {
		-webkit-appearance: none;
		appearance: none;
		width: 18px;
		height: 18px;
		border-radius: 50%;
		background: var(--highlight);
		cursor: pointer;
		border: 0;
		box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
	}
	input[type='range']::-moz-range-thumb {
		width: 18px;
		height: 18px;
		border-radius: 50%;
		background: var(--highlight);
		cursor: pointer;
		border: 0;
		box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
	}
	input[type='range']:disabled {
		opacity: 0.4;
		cursor: not-allowed;
	}
	.spin {
		animation: spin 0.9s linear infinite;
	}
	@keyframes spin {
		to { transform: rotate(360deg); }
	}
	@media (prefers-reduced-motion: reduce) {
		.spin {
			animation: none;
		}
	}

	@media (max-width: 480px) {
		.meta {
			grid-template-columns: 1fr;
		}
		.meta .label {
			grid-column: 1 / -1;
		}
		.meta .time {
			grid-column: 1 / -1;
			text-align: right;
		}
	}
</style>
