<script lang="ts">
	import type { ChartElement, ChartSeries, ChartDataPoint } from '../types';
	import { onMount } from 'svelte';
	import { normalizeSeries, resolveColor, formatValue, computeYRange } from '../utils/chart';

	interface Props {
		element: ChartElement;
		slideId: string;
		/** Optional snapshot of the same chart on the OUTGOING slide. When set,
		 *  the renderer tweens data values from prior state to current. */
		previousElement?: ChartElement | null;
	}
	let { element, slideId, previousElement = null }: Props = $props();

	let animationProgress = $state(1);
	let morphProgress = $state(1);
	let animationFrame: number | null = null;

	function ease(t: number) { return 1 - Math.pow(1 - t, 3); }

	function animate(setter: (v: number) => void, duration: number, delay = 0) {
		if (animationFrame) cancelAnimationFrame(animationFrame);
		const start = performance.now() + delay;
		setter(0);
		function tick() {
			const now = performance.now();
			if (now < start) { animationFrame = requestAnimationFrame(tick); return; }
			const p = Math.min((now - start) / duration, 1);
			setter(ease(p));
			if (p < 1) animationFrame = requestAnimationFrame(tick);
		}
		animationFrame = requestAnimationFrame(tick);
	}

	function runAnimation() {
		if (previousElement) {
			animationProgress = 1;
			animate((v) => (morphProgress = v), element.animationDuration);
		} else {
			morphProgress = 1;
			animate((v) => (animationProgress = v), element.animationDuration, 200);
		}
	}

	onMount(() => {
		runAnimation();
		return () => { if (animationFrame) cancelAnimationFrame(animationFrame); };
	});

	$effect(() => { if (slideId) runAnimation(); });

	const seriesList = $derived(normalizeSeries(element));
	const prevSeriesList = $derived(previousElement ? normalizeSeries(previousElement) : null);
	const isMultiSeries = $derived((element.series?.length ?? 0) > 1);
	const barLayout = $derived(element.barLayout ?? 'grouped');
	const stagger = $derived(element.revealStagger ?? 0);

	function effectiveValue(sIdx: number, pIdx: number): number {
		const target = seriesList[sIdx]?.data[pIdx]?.value ?? 0;
		if (prevSeriesList) {
			const prevSeries =
				prevSeriesList.find((s) => s.id === seriesList[sIdx]?.id) ?? prevSeriesList[sIdx];
			const targetLabel = seriesList[sIdx]?.data[pIdx]?.label;
			const prevPoint =
				prevSeries?.data.find((p) => p.label === targetLabel) ?? prevSeries?.data[pIdx];
			const prev = prevPoint?.value ?? target;
			return prev + (target - prev) * morphProgress;
		}
		const totalPoints = seriesList[0]?.data.length ?? 1;
		if (stagger > 0 && element.animationDuration > 0) {
			const slice = stagger / element.animationDuration;
			const localStart = pIdx * slice;
			const localEnd = Math.min(1, localStart + (1 - slice * (totalPoints - 1)));
			const span = Math.max(0.0001, localEnd - localStart);
			const local = Math.min(1, Math.max(0, (animationProgress - localStart) / span));
			return target * local;
		}
		return target * animationProgress;
	}

	const yRange = $derived(computeYRange(element));
	const yTop = 10;
	const yBottom = 90;
	function yFor(value: number): number {
		const { min, max } = yRange;
		const frac = (value - min) / (max - min);
		return yBottom - frac * (yBottom - yTop);
	}
	const zeroY = $derived(yFor(0));

	const labelCount = $derived(seriesList[0]?.data.length ?? 0);
	const slotWidth = $derived(labelCount > 0 ? 80 / labelCount : 80);
	const slotPad = $derived(slotWidth * 0.15);
	function barSlotX(pIdx: number): number { return 10 + pIdx * slotWidth + slotPad; }
	function barSlotInnerWidth(): number { return slotWidth - slotPad * 2; }

	const pieSeries = $derived(seriesList[0]);
	const pieSegments = $derived.by(() => {
		const data = pieSeries?.data ?? [];
		const animated = data.map((_, i) => effectiveValue(0, i));
		const total = animated.reduce((s, v) => s + Math.max(0, v), 0) || 1;
		let cur = -90;
		return data.map((d, i) => {
			const v = Math.max(0, animated[i]);
			const pct = v / total;
			const start = cur;
			cur += pct * 360;
			return { point: d, index: i, startAngle: start, endAngle: cur, percentage: pct };
		});
	});

	function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number): string {
		if (endAngle - startAngle <= 0.01) return '';
		const start = polarToCartesian(cx, cy, r, endAngle);
		const end = polarToCartesian(cx, cy, r, startAngle);
		const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
		return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 0 ${end.x} ${end.y} Z`;
	}
	function describeDonutArc(cx: number, cy: number, outerR: number, innerR: number, startAngle: number, endAngle: number): string {
		if (endAngle - startAngle <= 0.01) return '';
		const outerStart = polarToCartesian(cx, cy, outerR, endAngle);
		const outerEnd = polarToCartesian(cx, cy, outerR, startAngle);
		const innerStart = polarToCartesian(cx, cy, innerR, endAngle);
		const innerEnd = polarToCartesian(cx, cy, innerR, startAngle);
		const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
		return `M ${outerStart.x} ${outerStart.y} A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${outerEnd.x} ${outerEnd.y} L ${innerEnd.x} ${innerEnd.y} A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${innerStart.x} ${innerStart.y} Z`;
	}
	function polarToCartesian(cx: number, cy: number, r: number, angle: number) {
		const rad = (angle * Math.PI) / 180;
		return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
	}

	const stackedBars = $derived.by(() => {
		if (!isMultiSeries || barLayout !== 'stacked' || element.chartType !== 'bar') return [];
		const labels = seriesList[0]?.data ?? [];
		return labels.map((basePoint, pIdx) => {
			let posOffset = 0;
			let negOffset = 0;
			const segments = seriesList.map((series, sIdx) => {
				const v = effectiveValue(sIdx, pIdx);
				const isPos = v >= 0;
				const fromOffset = isPos ? posOffset : negOffset;
				const toOffset = fromOffset + v;
				if (isPos) posOffset = toOffset; else negOffset = toOffset;
				const yStart = yFor(fromOffset);
				const yEnd = yFor(toOffset);
				return { sIdx, series, value: v, y: Math.min(yStart, yEnd), h: Math.abs(yStart - yEnd) };
			});
			return { basePoint, pIdx, segments };
		});
	});

	const yTicks = $derived.by(() => {
		if (!element.yAxis?.showLabels) return [];
		const { min, max } = yRange;
		const steps = 4;
		return Array.from({ length: steps + 1 }, (_, i) => {
			const v = min + (i / steps) * (max - min);
			return { value: v, y: yFor(v) };
		});
	});
</script>

<div class="animot-chart"
	style:background-color={element.backgroundColor}
	style:padding="{element.padding}px"
	style:border-radius="{element.borderRadius}px"
	style:color={element.textColor}
	style:font-size="{element.fontSize}px"
>
	{#if element.title}
		<div class="animot-chart-title">{element.title}</div>
	{/if}

	<div class="animot-chart-content">
		{#if element.chartType === 'bar'}
			<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="animot-bar-chart">
				{#if element.showGrid}
					{#each [0, 25, 50, 75, 100] as gy}
						<line x1="0" y1={gy} x2="100" y2={gy} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
					{/each}
				{/if}
				{#if yRange.min < 0}
					<line x1="0" y1={zeroY} x2="100" y2={zeroY} stroke="currentColor" stroke-opacity="0.3" stroke-width="0.5" />
				{/if}

				{#if isMultiSeries && barLayout === 'stacked'}
					{#each stackedBars as bar}
						{@const slotX = barSlotX(bar.pIdx)}
						{@const innerW = barSlotInnerWidth()}
						{#each bar.segments as seg}
							{#if seg.h > 0.01}
								<rect x={slotX} y={seg.y} width={innerW} height={seg.h}
									fill={resolveColor(element, seg.sIdx, bar.pIdx, seg.series.data[bar.pIdx], seg.series)} />
							{/if}
						{/each}
						{#if element.showLabels}
							<text x={slotX + innerW / 2} y="98" text-anchor="middle" font-size="4" fill="currentColor">{bar.basePoint.label}</text>
						{/if}
					{/each}
				{:else}
					{#each seriesList[0].data as basePoint, pIdx}
						{@const slotX = barSlotX(pIdx)}
						{@const innerW = barSlotInnerWidth()}
						{@const seriesCount = seriesList.length}
						{@const groupGap = innerW * 0.05}
						{@const barW = (innerW - groupGap * (seriesCount - 1)) / seriesCount}
						{#each seriesList as series, sIdx}
							{@const v = effectiveValue(sIdx, pIdx)}
							{@const yStart = zeroY}
							{@const yEnd = yFor(v)}
							{@const barY = Math.min(yStart, yEnd)}
							{@const barH = Math.abs(yStart - yEnd)}
							<rect x={slotX + sIdx * (barW + groupGap)} y={barY} width={barW} height={barH}
								fill={resolveColor(element, sIdx, pIdx, series.data[pIdx], series)} rx="0.5" />
							{#if element.showValues}
								<text x={slotX + sIdx * (barW + groupGap) + barW / 2}
									y={v >= 0 ? barY - 1 : barY + barH + 3.5}
									text-anchor="middle" font-size="3" fill="currentColor">{formatValue(v, element.valueFormat)}</text>
							{/if}
						{/each}
						{#if element.showLabels}
							<text x={slotX + innerW / 2} y="98" text-anchor="middle" font-size="4" fill="currentColor">{basePoint.label}</text>
						{/if}
					{/each}
				{/if}

				{#each yTicks as t}
					<text x="1" y={t.y + 1} font-size="3" fill="currentColor" opacity="0.7">{formatValue(t.value, element.valueFormat)}</text>
				{/each}
			</svg>

		{:else if element.chartType === 'line' || element.chartType === 'area'}
			<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="animot-line-chart">
				{#if element.showGrid}
					{#each [0, 25, 50, 75, 100] as gy}
						<line x1="0" y1={gy} x2="100" y2={gy} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
					{/each}
				{/if}
				{#if yRange.min < 0}
					<line x1="10" y1={zeroY} x2="90" y2={zeroY} stroke="currentColor" stroke-opacity="0.3" stroke-width="0.5" />
				{/if}

				{#each seriesList as series, sIdx}
					{@const pts = series.data.map((_, i) => {
						const x = 10 + (i / Math.max(series.data.length - 1, 1)) * 80;
						const y = yFor(effectiveValue(sIdx, i));
						return `${x},${y}`;
					}).join(' ')}
					{@const seriesColor = resolveColor(element, sIdx, 0, undefined, series)}
					{#if element.chartType === 'area'}
						{@const areaPath = `${pts} 90,${zeroY} 10,${zeroY}`}
						<polygon points={areaPath} fill={seriesColor} fill-opacity={isMultiSeries ? 0.18 : 0.3} />
					{/if}
					<polyline points={pts} fill="none" stroke={seriesColor} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
					{#each series.data as point, pIdx}
						{@const x = 10 + (pIdx / Math.max(series.data.length - 1, 1)) * 80}
						{@const y = yFor(effectiveValue(sIdx, pIdx))}
						<circle cx={x} cy={y} r="1.4" fill={resolveColor(element, sIdx, pIdx, point, series)} />
						{#if element.showValues}
							<text x={x} y={y - 2.5} text-anchor="middle" font-size="3" fill="currentColor">{formatValue(effectiveValue(sIdx, pIdx), element.valueFormat)}</text>
						{/if}
					{/each}
				{/each}

				{#if element.showLabels}
					{#each seriesList[0]?.data ?? [] as point, pIdx}
						{@const x = 10 + (pIdx / Math.max((seriesList[0]?.data.length ?? 1) - 1, 1)) * 80}
						<text x={x} y="98" text-anchor="middle" font-size="4" fill="currentColor">{point.label}</text>
					{/each}
				{/if}

				{#each yTicks as t}
					<text x="1" y={t.y + 1} font-size="3" fill="currentColor" opacity="0.7">{formatValue(t.value, element.valueFormat)}</text>
				{/each}
			</svg>

		{:else if element.chartType === 'pie'}
			<svg viewBox="0 0 100 100" class="animot-pie-chart">
				{#each pieSegments as seg}
					{#if seg.endAngle - seg.startAngle > 0.1}
						{@const midAngle = (seg.startAngle + seg.endAngle) / 2}
						{@const valuePos = polarToCartesian(50, 50, 28, midAngle)}
						{@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
						{@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
						{@const fill = resolveColor(element, 0, seg.index, seg.point, pieSeries)}
						<path d={describeArc(50, 50, 40, seg.startAngle, seg.endAngle)} {fill} />
						{#if element.showValues}
							<text x={valuePos.x} y={valuePos.y} text-anchor="middle" dominant-baseline="middle" font-size="4" fill="white" font-weight="600">{Math.round(seg.percentage * 100)}%</text>
						{/if}
						{#if element.showLabels}
							<text x={labelPos.x} y={labelPos.y} text-anchor={textAnchor} dominant-baseline="middle" font-size="3.5" fill="currentColor">{seg.point.label}</text>
						{/if}
					{/if}
				{/each}
				{#if element.showLegend && pieSeries}
					{#each pieSeries.data as point, i}
						<rect x="85" y={10 + i * 8} width="4" height="4" fill={resolveColor(element, 0, i, point, pieSeries)} />
						<text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
					{/each}
				{/if}
			</svg>

		{:else if element.chartType === 'donut'}
			<svg viewBox="0 0 100 100" class="animot-donut-chart">
				{#each pieSegments as seg}
					{#if seg.endAngle - seg.startAngle > 0.1}
						{@const midAngle = (seg.startAngle + seg.endAngle) / 2}
						{@const valuePos = polarToCartesian(50, 50, 32.5, midAngle)}
						{@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
						{@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
						{@const fill = resolveColor(element, 0, seg.index, seg.point, pieSeries)}
						<path d={describeDonutArc(50, 50, 40, 25, seg.startAngle, seg.endAngle)} {fill} />
						{#if element.showValues}
							<text x={valuePos.x} y={valuePos.y} text-anchor="middle" dominant-baseline="middle" font-size="3.5" fill="white" font-weight="600">{Math.round(seg.percentage * 100)}%</text>
						{/if}
						{#if element.showLabels}
							<text x={labelPos.x} y={labelPos.y} text-anchor={textAnchor} dominant-baseline="middle" font-size="3.5" fill="currentColor">{seg.point.label}</text>
						{/if}
					{/if}
				{/each}
				{#if element.showLegend && pieSeries}
					{#each pieSeries.data as point, i}
						<rect x="85" y={10 + i * 8} width="4" height="4" fill={resolveColor(element, 0, i, point, pieSeries)} />
						<text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
					{/each}
				{/if}
			</svg>
		{/if}

		{#if element.showLegend && isMultiSeries && (element.chartType === 'bar' || element.chartType === 'line' || element.chartType === 'area')}
			<div class="animot-series-legend">
				{#each seriesList as series, sIdx}
					<span class="animot-legend-item">
						<span class="animot-swatch" style:background={resolveColor(element, sIdx, 0, undefined, series)}></span>
						{series.name || `Series ${sIdx + 1}`}
					</span>
				{/each}
			</div>
		{/if}
	</div>
</div>

<style>
	.animot-chart {
		width: 100%;
		height: 100%;
		display: flex;
		flex-direction: column;
		box-sizing: border-box;
		overflow: hidden;
	}
	.animot-chart-title { font-weight: 600; text-align: center; margin-bottom: 8px; }
	.animot-chart-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
	.animot-chart-content svg { width: 100%; flex: 1; min-height: 0; }
	.animot-bar-chart, .animot-line-chart { overflow: visible; }
	.animot-series-legend {
		display: flex; flex-wrap: wrap; gap: 12px;
		justify-content: center; padding: 6px 0 0; font-size: 0.85em;
	}
	.animot-legend-item { display: inline-flex; align-items: center; gap: 5px; }
	.animot-swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
</style>
