/**
 * The single-choice picker (milestone 23, ruling 4), replacing every native
 * `<select>` in the app.
 *
 * RAC composes a Select out of Label, Button, SelectValue, Popover, ListBox and
 * ListBoxItem; those parts are the Select, not extra components adopted beyond
 * ruling 4's scope, and nothing outside this file imports them.
 *
 * Keys are strings because a `<select>`'s value was a string. A call site that
 * models an id converts on both edges.
 */

import * as stylex from "@stylexjs/stylex";
import {
	Button,
	Label,
	ListBox,
	ListBoxItem,
	Popover,
	Select as AriaSelect,
	SelectValue,
	Text,
} from "react-aria-components";
import { colors } from "./tokens.stylex";
import { styles as shared } from "./styles";

export interface SelectOption {
	value: string;
	label: string;
}

interface Props {
	options: readonly SelectOption[];
	value: string;
	onChange: (value: string) => void;
	/** The visible label. Omit it only when `aria-label` names the control. */
	label?: string;
	"aria-label"?: string;
	/**
	 * Text under the control that qualifies it. RAC gives it an id and points the
	 * trigger's `aria-describedby` at it, which a sibling rendered by the caller
	 * cannot be: the trigger is this file's, so only this file can name it.
	 */
	description?: string;
	/**
	 * `field` matches a full-width form input, `compactField` the smaller one a
	 * dialog uses, `inline` a control sitting in a row of other controls.
	 */
	variant?: "field" | "compactField" | "inline";
	/** Visible but inert, keeping its value on screen; RAC also drops it from the tab order. */
	isDisabled?: boolean;
}

const styles = stylex.create({
	root: {
		display: "block",
	},
	label: {
		display: "block",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
		fontWeight: 500,
	},
	trigger: {
		display: "flex",
		alignItems: "center",
		justifyContent: "space-between",
		gap: "0.5rem",
		textAlign: "left",
		// RAC renders a real `<button disabled>`, so the state is reachable as a
		// pseudo-class rather than needing a second style object.
		cursor: { default: "pointer", ":disabled": "not-allowed" },
		opacity: { default: null, ":disabled": 0.55 },
	},
	compact: {
		marginTop: "0.25rem",
		width: "100%",
	},
	/** Explicit, so RAC's default `react-aria-SelectValue` class does not land. */
	value: {
		overflow: "hidden",
		textOverflow: "ellipsis",
		whiteSpace: "nowrap",
	},
	chevron: {
		color: colors.textMuted,
	},
	description: {
		display: "block",
		fontSize: "0.75rem",
		lineHeight: "1rem",
		color: colors.textMuted,
	},
	popover: {
		width: "var(--trigger-width)",
		maxHeight: "16rem",
		overflowY: "auto",
		borderRadius: "0.25rem",
		borderWidth: 1,
		borderStyle: "solid",
		borderColor: colors.borderStrong,
		backgroundColor: colors.surfaceRaised,
		color: colors.text,
		boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
	},
	listBox: {
		outlineStyle: "none",
		paddingBlock: "0.25rem",
	},
	item: {
		paddingInline: "0.75rem",
		paddingBlock: "0.375rem",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
		cursor: "pointer",
	},
	/**
	 * The inverted row is the listbox convention, and the ring is the milestone-9
	 * floor; an option gets both. RAC focuses the option's own DOM node, so the
	 * shared `:focus-visible` ring does apply here — it is drawn inset because an
	 * option flush against a scrolling popover clips an outset one, and recoloured
	 * because the focus token is the same blue this row just painted behind it.
	 */
	itemFocused: {
		backgroundColor: colors.primary,
		color: colors.primaryText,
		outlineColor: { default: null, ":focus-visible": colors.primaryText },
	},
	itemSelected: {
		fontWeight: 600,
	},
});

export default function Select({
	options,
	value,
	onChange,
	label,
	"aria-label": ariaLabel,
	description,
	variant = "field",
	isDisabled = false,
}: Props) {
	const base = variant === "field" ? shared.input : shared.smallInput;
	const block = variant === "compactField" ? styles.compact : null;
	return (
		<AriaSelect
			aria-label={ariaLabel}
			isDisabled={isDisabled}
			value={value}
			onChange={(key) => onChange(String(key ?? ""))}
			{...stylex.props(styles.root)}
		>
			{label !== undefined && <Label {...stylex.props(styles.label)}>{label}</Label>}
			<Button className={() => stylex.props(base, block, styles.trigger, shared.focusRing).className ?? ""}>
				<SelectValue className={() => stylex.props(styles.value).className ?? ""} />
				<span aria-hidden="true" {...stylex.props(styles.chevron)}>
					▾
				</span>
			</Button>
			{description !== undefined && (
				<Text slot="description" {...stylex.props(styles.description)}>
					{description}
				</Text>
			)}
			<Popover className={() => stylex.props(styles.popover).className ?? ""}>
				<ListBox {...stylex.props(styles.listBox)}>
					{options.map((option) => (
						<ListBoxItem
							key={option.value}
							id={option.value}
							textValue={option.label}
							className={({ isFocused, isSelected }) =>
								stylex.props(
									styles.item,
									shared.insetFocusRing,
									isSelected && styles.itemSelected,
									isFocused && styles.itemFocused,
								).className ?? ""
							}
						>
							{option.label}
						</ListBoxItem>
					))}
				</ListBox>
			</Popover>
		</AriaSelect>
	);
}
