/**
 * The checkbox, wrapping React Aria's.
 *
 * React Aria hides the real input and leaves the mark to the call site, so the
 * box below is what the reader sees. It is drawn to the native control's size
 * so a row that held a native checkbox keeps its height and its baseline.
 *
 * One component covers both uses: pass `value` for a box inside a
 * `CheckboxGroup`, which owns the selection, or `isSelected`/`onChange` for a
 * standalone box that owns its own.
 */

import type { ReactNode } from "react";
import * as stylex from "@stylexjs/stylex";
import { Checkbox as AriaCheckbox } from "react-aria-components";
import { colors } from "./tokens.stylex";

/**
 * Re-exported rather than wrapped: the group adds no styling of its own, and
 * routing it through here keeps React Aria's checkbox parts to one import site,
 * the same rule `Select` follows.
 */
export { CheckboxGroup } from "react-aria-components";

interface Common {
	/** The visible label, which is also the accessible name. */
	children: ReactNode;
	/** Visible but inert; React Aria also drops it from the tab order. */
	isDisabled?: boolean;
}

/** Inside a `CheckboxGroup`, which holds the selection for every box in it. */
interface Grouped extends Common {
	value: string;
	isSelected?: never;
	onChange?: never;
}

/** On its own, where the caller holds the state and is told to move it. */
interface Standalone extends Common {
	value?: never;
	isSelected: boolean;
	onChange: (isSelected: boolean) => void;
}

/**
 * The two modes are exclusive: a grouped box that also carried `isSelected`
 * would have two sources of truth, and a standalone one without `onChange`
 * could never move. The union is what makes both unrepresentable.
 */
type Props = Grouped | Standalone;

const styles = stylex.create({
	/**
	 * The whole label is the hit area, so it carries the 44px pointer-target
	 * floor the dialog's Close button already sets, on both axes. The text is
	 * 20px tall, and a one-word source name is narrower than 44px again.
	 */
	label: {
		minHeight: 44,
		minWidth: 44,
		display: "inline-flex",
		alignItems: "center",
		gap: "0.5rem",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
	},
	box: {
		flexShrink: 0,
		boxSizing: "border-box",
		width: "0.875rem",
		height: "0.875rem",
		display: "inline-flex",
		alignItems: "center",
		justifyContent: "center",
		borderRadius: "0.1875rem",
		borderWidth: 1,
		borderStyle: "solid",
		borderColor: colors.borderStrong,
		backgroundColor: colors.surfaceRaised,
	},
	boxSelected: {
		borderColor: colors.primary,
		backgroundColor: colors.primary,
		color: colors.primaryText,
	},
	/**
	 * The shared ring keys off `:focus-visible`, which lands on the hidden input
	 * rather than on this box, so the ring follows the state React Aria reports.
	 */
	boxFocused: {
		outlineWidth: 2,
		outlineStyle: "solid",
		outlineColor: colors.focus,
		outlineOffset: 2,
	},
	/** The box dims, not the words: the label still has to be readable. */
	boxDisabled: {
		opacity: 0.5,
	},
});

/** `currentColor` so the mark follows the selected box's foreground token. */
function CheckMark() {
	return (
		<svg aria-hidden="true" viewBox="0 0 12 12" width="10" height="10" fill="none">
			<path
				d="M2.5 6.5 5 9l4.5-5.5"
				stroke="currentColor"
				strokeWidth="2"
				strokeLinecap="round"
				strokeLinejoin="round"
			/>
		</svg>
	);
}

export default function Checkbox({ children, ...state }: Props) {
	return (
		<AriaCheckbox {...state} className={() => stylex.props(styles.label).className ?? ""}>
			{(renderProps) => (
				<>
					<span
						{...stylex.props(
							styles.box,
							renderProps.isSelected && styles.boxSelected,
							renderProps.isFocusVisible && styles.boxFocused,
							renderProps.isDisabled && styles.boxDisabled,
						)}
					>
						{renderProps.isSelected && <CheckMark />}
					</span>
					{children}
				</>
			)}
		</AriaCheckbox>
	);
}
