/**
 * A breakdown as a ring, a legend and a table.
 *
 * The ring is decoration: it carries `aria-hidden` and `focusable="false"`,
 * because a non-focusable SVG is still in the accessibility tree and would
 * announce a pile of unlabelled paths. Everything the ring says is said again in
 * the legend — visibly, with the share and the count — and once more in a
 * visually hidden table, which is the surface a screen reader reads.
 *
 * Labels can collide: two rows can both be "Unknown", and one upstream name can
 * appear under two route kinds. Identity is therefore the caller's `key`, and an
 * entry that needs disambiguating carries `secondary` text saying which it is.
 */

import * as stylex from "@stylexjs/stylex";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { layoutDonut, type DonutSlice } from "./donutLayout";

const SIZE = 180;
const THICKNESS = 36;

const numberFormat = new Intl.NumberFormat();

/** The width at which the page puts the two donuts side by side, and the page's
 * own grid switches on the same query. StyleX will not take it from an import,
 * so it is written out in both modules and must be changed in both. */
const TWO_COLUMN = "@media (min-width: 1280px)";

const styles = stylex.create({
	empty: {
		display: "flex",
		alignItems: "center",
		justifyContent: "center",
		minHeight: SIZE,
		borderRadius: "0.25rem",
		borderWidth: 1,
		borderStyle: "dashed",
		borderColor: colors.borderStrong,
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
		color: colors.textMuted,
	},
	/**
	 * Centred while the panels are stacked, left-anchored once they are side by
	 * side. Stacked, the panel is as wide as the page and a ring pinned to the
	 * left edge reads as a mistake; in a column it is one of a pair and lines up
	 * with everything above it.
	 */
	body: {
		display: "flex",
		flexWrap: "wrap",
		alignItems: "center",
		justifyContent: { default: "center", [TWO_COLUMN]: "flex-start" },
		gap: "1.25rem",
	},
	ring: {
		flexShrink: 0,
	},
	/**
	 * Capped and left-anchored. Without the cap the row justifies across whatever
	 * the panel is given — most of a metre of whitespace on a wide monitor — and a
	 * label stops reading as belonging to the count opposite it.
	 */
	legend: {
		flex: 1,
		minWidth: "12rem",
		maxWidth: "24rem",
		display: "flex",
		flexDirection: "column",
		gap: "0.25rem",
		listStyleType: "none",
		padding: 0,
		margin: 0,
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
	},
	legendItem: {
		display: "flex",
		alignItems: "baseline",
		gap: "0.5rem",
	},
	swatch: {
		flexShrink: 0,
		alignSelf: "center",
		display: "inline-block",
		width: "0.625rem",
		height: "0.625rem",
		borderRadius: "0.125rem",
	},
	/** Dynamic: the swatch takes the colour the ring is drawn in. */
	swatchColor: (color: string) => ({ backgroundColor: color }),
	label: {
		flex: 1,
		minWidth: 0,
		overflowWrap: "anywhere",
	},
	secondary: {
		marginLeft: "0.375rem",
		fontSize: "0.75rem",
		lineHeight: "1rem",
		color: colors.textMuted,
	},
	count: {
		color: colors.textSecondary,
	},
	share: {
		minWidth: "3rem",
		textAlign: "right",
		color: colors.textMuted,
	},
});

function sharePercent(share: number): string {
	return `${(share * 100).toFixed(1)}%`;
}

export default function Donut({
	slices,
	caption,
	unit,
}: {
	slices: DonutSlice[];
	/** Names the hidden table, so a screen reader knows which breakdown it is in. */
	caption: string;
	/** The column header for the counted thing, e.g. "Queries". */
	unit: string;
}) {
	const layout = layoutDonut(slices, SIZE, THICKNESS);

	if (layout.total === 0) {
		return <div {...stylex.props(styles.empty)}>No queries in this period.</div>;
	}

	return (
		<div {...stylex.props(styles.body)}>
			<svg
				aria-hidden="true"
				focusable="false"
				width={SIZE}
				height={SIZE}
				viewBox={`0 0 ${SIZE} ${SIZE}`}
				{...stylex.props(styles.ring)}
			>
				{layout.arcs.map((arc) => (
					// The stroke is what keeps a shared hue from lying. Colour is a pure
					// function of identity, so two neighbouring slices can come out the
					// same; outlined in the panel's own colour they still read as two
					// shapes rather than merging into one. Attributes rather than a
					// class, as the client chart's segments are, so the separation is
					// visible to a test and not only to a stylesheet.
					<path
						key={arc.slice.key}
						d={arc.d}
						fill={arc.slice.color}
						fillRule="evenodd"
						stroke={colors.surfaceRaised}
						strokeWidth={1}
					/>
				))}
			</svg>
			<ul {...stylex.props(styles.legend)}>
				{layout.arcs.map((arc) => (
					<li key={arc.slice.key} {...stylex.props(styles.legendItem)}>
						<span
							aria-hidden="true"
							{...stylex.props(styles.swatch, styles.swatchColor(arc.slice.color))}
						/>
						<span {...stylex.props(styles.label)}>
							{arc.slice.label}
							{arc.slice.secondary !== undefined && (
								<span {...stylex.props(styles.secondary)}>{arc.slice.secondary}</span>
							)}
						</span>
						<span {...stylex.props(styles.count, shared.tabularNums)}>
							{numberFormat.format(arc.slice.value)}
						</span>
						<span {...stylex.props(styles.share, shared.tabularNums)}>{sharePercent(arc.share)}</span>
					</li>
				))}
			</ul>
			<div {...stylex.props(shared.srOnly)}>
				<table>
					<caption>{caption}</caption>
					<thead>
						<tr>
							<th scope="col">Entry</th>
							<th scope="col">{unit}</th>
							<th scope="col">Share</th>
						</tr>
					</thead>
					<tbody>
						{layout.arcs.map((arc) => (
							<tr key={arc.slice.key}>
								<th scope="row">
									{arc.slice.secondary === undefined
										? arc.slice.label
										: `${arc.slice.label} (${arc.slice.secondary})`}
								</th>
								<td>{arc.slice.value}</td>
								<td>{sharePercent(arc.share)}</td>
							</tr>
						))}
					</tbody>
				</table>
			</div>
		</div>
	);
}
