<script lang="ts">
	import * as d3 from 'd3';
	import {
		type AxisProps,
		Axis,
		type EnforceSizeType,
		enforceSizeHelper,
		PaddingManager
	} from './utils';

	export type LineGraphData = {
		ys: number[];
		xs: number[];
		name: string;
		color?: string;
	}[];

	const {
		class: className,
		data,
		title,
		useCompactNotation = 'auto',
		width: inWidth = 300,
		height: inHeight = 300,
		area,
		xAxis: inXAxis,
		yAxis: inYAxis,
		xIsDates = false,
		enforceSize
	}: {
		class?: string;
		data: LineGraphData;
		title?: string;
		useCompactNotation?: 'auto' | boolean;
		width?: number;
		height?: number;
		area?: boolean;
		xAxis?: AxisProps;
		yAxis?: AxisProps;
		xIsDates?: boolean;
		enforceSize?: EnforceSizeType;
	} = $props();

	const notation = 'compact';
	const locale = 'en-GB';

	let divRef: HTMLDivElement | null = $state(undefined);
	let yScaler: d3.ScaleLinear<number, number>;

	let optionsHeight = $state(0);

	let size: { width: number; height: 0 } = $state({ width: 0, height: 0 });

	let mask: Record<string, boolean> = $state({});

	const names = $derived(data.map((d) => d.name));
	const color = $derived.by(() => {
		let colors: readonly string[] = [];
		const groups = names;
		const len = groups.length;
		if (len === 0) {
			// Do nothing
		} else if (len >= 3 && len < 10) {
			colors = d3.schemeBlues[len];
		} else {
			colors = [...groups].map((_, i) => d3.interpolateBlues(i / len));
		}
		return d3.scaleOrdinal(groups, colors);
	});

	const internalEnforceSize = $derived.by(() => {
		return (
			enforceSizeHelper
				.fromEnforceSize(enforceSize)
				.h('p-1', '0.5rem')
				.h('left', 1, 'left')
				.h('p-2', '0.5rem')
				//.h('options', optionsHeight)
				.enforce()
				.toEnfoceSize('left')
		);
	});

	$effect(() => {
		if (!data || !divRef || data.length === 0) return;

		function gen<T extends number | Date>(xs: T[]) {
			// Get all possible values and sort them
			const ys = data.reduce((acc, d) => acc.concat(d.ys), [] as number[]);

			if (ys.length === 0) return;

			ys.sort((a, b) => a - b);

			const xAxisTitle = new Axis(inXAxis);
			const yAxis = new Axis(inYAxis);

			xAxisTitle.titlePos('bot');
			yAxis.titlePos('left');

			const padding = new PaddingManager({ width: inWidth, height: inHeight });
			padding.padAll(20);

			padding
				.padLeft(
					`${ys[ys.length - 1].toLocaleString(locale, { notation, maximumFractionDigits: ys[ys.length] > 100 ? 0 : 2 })}`,
					65
				)
				.padLeft(yAxis);

			padding.padBot(50).padTop(xAxisTitle);

			padding.enforce(internalEnforceSize);

			const div = divRef;
			// clear the html inside the div
			d3.select(div).html('');

			const svg = d3
				.select(div)
				.append('svg')
				.attr('width', padding.paddedWidth)
				.attr('height', padding.paddedHeight)
				.append('g')
				.attr('transform', padding.translateString);

			size = {
				width: padding.paddedWidth,
				height: padding.paddedHeight
			};

			let x: d3.ScaleTime<number, number, never> | d3.ScaleLinear<number, number, never>;

			let xAxis: d3.Selection<SVGGElement, unknown, null, undefined>;

			const width = padding.width;
			const height = padding.height;

			if (xIsDates) {
				x = d3
					.scaleTime()
					.domain([new Date(xs[0]), new Date(xs[xs.length - 1])])
					.range([0, width]);
				xAxis = svg
					.append('g')
					.attr('transform', `translate(0, ${height})`)
					.call(d3.axisBottom(x).ticks(10));
				xAxis
					.selectAll('text')
					.attr('transform', 'rotate(65)')
					.attr('dy', '-0.4em')
					.attr('dx', '2.9em');
			} else {
				x = d3
					.scaleLinear()
					.domain([xs[0], xs[xs.length - 1]])
					.range([0, width]);
				xAxis = svg
					.append('g')
					.attr('transform', `translate(0, ${height})`)
					.call(d3.axisBottom(x).ticks(10));
				xAxis
					.selectAll('text')
					.attr('transform', 'rotate(65)')
					.attr('dy', '-0.4em')
					.attr('dx', '2.9em');
			}

			const y = d3
				.scaleLinear()
				.domain([ys[0], ys[ys.length - 1]])
				.range([height, 0]);

			svg.append('g').call(
				d3.axisLeft(y).tickFormat((a) => a.toLocaleString(locale, { notation }))
			);

			yAxis.apply_title(svg, height, width, padding.left);
			xAxisTitle.apply_title(svg, height, width, padding.bot);

			yScaler = y;

			svg.append('defs')
				.append('svg:clipPath')
				.attr('id', 'clip')
				.append('svg:rect')
				.attr('width', width)
				.attr('height', height)
				.attr('x', 0)
				.attr('y', 0);

			/*// Create the circle that travels along the curve of chart
        const focus = svg
            .append("g")
            .append("circle")
            .style("fill", "none")
            .attr("stroke", "black")
            .attr("r", 8.5)
            .style("opacity", 0);

        // Create the text that travels along the curve of chart
        const focusText = svg
            .append("g")
            .append("text")
            .style("opacity", 0)
            .attr("text-anchor", "left")
            .attr("alignment-baseline", "middle");*/

			// Add brushing
			const brush = d3
				.brushX() // Add the brush feature using the d3.brush function
				.extent([
					[0, 0],
					[width, height]
				]) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
				.on('end', updateChart);

			//
			// Create a group that stores all lines
			//
			const lines = svg.append('g').attr('clip-path', 'url(#clip)');

			//
			// Create the lines
			//

			lines
				.selectAll('path')
				.data(data.map((l, i) => ({ ...l, i })))
				.join('path')
				.attr('lineId', (_, i) => `l-${i}`)
				.attr('transform', (l) => {
					if (!mask[l.name]) {
						return '';
					}
					const m = l.ys.reduce((acc, v) => Math.max(acc, v), 0);
					return `translate(0, ${height - (y(m) as number)})`;
				})
				.sort((a, b) => b.ys[0] - a.ys[0])
				.attr('fill', (l) => l.color ?? color(l.name))
				.attr('fill-opacity', 1)
				.attr('stroke', (l) => l.color ?? color(l.name))
				.attr('stroke-width', 1.5)
				.datum((l) => {
					return l.ys.map((a, j) => ({
						value: a,
						x: l.xs[j]
					}));
				})
				.attr('d', (d) => {
					const a = d3
						.area<{ x: number; value: number }>()
						.x((d) => x(xIsDates ? new Date(d.x).getTime() : d.x))
						.curve(d3.curveBumpX);

					if (area) {
						return a.y0(y(0)).y1((d) => y(d.value))(d as any);
					}
					return a.y((d) => y(d.value))(d as any);
				});

			/*lines
				.selectAll('circle')
				.data(
					data.reduce(
						(acc, a) => {
							for (let i = 0; i < a.xs.length; i++) {
								acc.push([a.xs[i], a.ys[i]]);
							}
							return acc;
						},
						[] as [number, number][]
					)
				)
				.join('circle')
				.attr('cx', (d) => x(xIsDates ? new Date(d[0]).getTime() : d[0]))
				.attr('cy', (d) => y(d[1]))
				.attr('fill', 'red')
				.attr('r', '2px');*/

			lines.append('g').attr('class', 'brush').call(brush);

			let idleTimeout: number | null;
			function idled() {
				idleTimeout = null;
			}

			// A function that update the chart for given boundaries
			function updateChart(e: any) {
				// What are the selected boundaries?
				const extent = e.selection;

				// If no selection, back to initial coordinate. Otherwise, update X axis domain
				if (!extent) {
					if (!idleTimeout) {
						idleTimeout = setTimeout(idled, 350) as unknown as number; // This allows to wait a little bit
						return;
					}
					x.domain([xs[0], xs[xs.length - 1]]);
				} else {
					x.domain([x.invert(extent[0]), x.invert(extent[1])]);
					lines.select('.brush').call(brush.move as any, null); // This remove the grey brush area as soon as the selection has been done
				}

				// Update axis and line position
				xAxis
					.transition()
					.duration(1000)
					.call(d3.axisBottom(x).ticks(10))
					.selectAll('text')
					.attr('transform', 'rotate(65)')
					.attr('dy', '-0.4em')
					.attr('dx', '2.9em');

				lines
					.selectAll<d3.BaseType, { value: number; x: number }[]>('path')
					.transition()
					.duration(1000)
					.attr('d', (d) => {
						const a = d3
							.area<{
								value: number;
								x: number;
							}>()
							.x((d) => x(xIsDates ? new Date(d.x).getTime() : d.x))
							.curve(d3.curveBumpX);

						if (area) {
							return a.y0(y(0)).y1((d) => y(d.value))(d as any);
						}
						return a.y((d) => y(d.value))(d as any);
					});
			}

			svg.on('dblclick', () => {
				x.domain([xs[0], xs[xs.length - 1]]);
				xAxis
					.transition()
					.call(d3.axisBottom(x).ticks(10))
					.selectAll('text')
					.attr('transform', 'rotate(65)')
					.attr('dy', '-0.4em')
					.attr('dx', '2.9em');

				lines
					.selectAll('path')
					.transition()
					.attr('d', (d) => {
						const a = d3
							.area<{
								x: number;
								value: number;
							}>()
							.x((d) => x(xIsDates ? new Date(d.x).getTime() : d.x))
							.curve(d3.curveBumpX);

						if (area) {
							return a.y0(y(0)).y1((d) => y(d.value))(d as any);
						}
						return a.y((d) => y(d.value))(d as any);
					});
			});
		}

		const xs = data.reduce((acc, d) => acc.concat(d.xs), [] as number[]);

		if (xIsDates) {
			xs.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
			gen(xs.map((a) => new Date(a)));
		} else {
			xs.sort((a, b) => a - b);
			gen(xs);
		}
	});
</script>

<div>
	<div class="p-2">
		<div style="width: {size.width}px; height: {size.height}px" bind:this={divRef}></div>
	</div>
	<div class="flex flex-wrap" bind:clientHeight={optionsHeight}>
		{#each data as a, i}
			<div class="flex items-center p-2">
				<button
					type="button"
					onclick={() => {
						if (mask[a.name]) {
							d3.select(divRef)
								.selectAll(`path[lineId=l-${i}]`)
								.transition()
								.duration(1000)
								.attr('transform', '');
						} else {
							const m = a.ys.reduce((acc, v) => Math.max(acc, v), 0);
							const scale = yScaler ?? ((i) => i);

							d3.select(divRef)
								.selectAll(`path[lineId=l-${i}]`)
								.transition()
								.duration(1500)
								.attr(
									'transform',
									`translate(0, ${inHeight - (scale(m) as number)})`
								);
						}
						mask = { ...mask, [a.name]: !mask[a.name] };
					}}
					class="border-0 flex bg-transparent"
				>
					<div
						style="background-color: {mask[a.name]
							? 'gray'
							: a.color ?? color(a.name)}; width: 20px; height: 20px;"
					></div>
					<div style="textDecoration: {mask[a.name] ? 'line-through' : ''}" class="px-2">
						{a.name}
					</div>
				</button>
			</div>
		{/each}
	</div>
</div>
