/**
 * The on/off switch, wrapping React Aria's.
 *
 * A switch, not a checkbox: every call site flips a setting that takes effect
 * the moment it moves, with no Save between. A checkbox states what a form will
 * submit, which is a promise these controls do not make.
 *
 * React Aria hides the real input and leaves the track to the call site, so the
 * track and thumb below are what the reader sees. The caller always holds the
 * state — none of these controls owns the value it shows, because the server's
 * answer is the value.
 */

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

interface Common {
	isSelected: boolean;
	onChange: (isSelected: boolean) => void;
	/** Visible but inert; React Aria also drops it from the tab order. */
	isDisabled?: boolean;
}

/** With visible words beside the track, which name it. */
interface Labelled extends Common {
	children: ReactNode;
	"aria-label"?: never;
}

/** Bare, in a table cell whose column heading cannot name a single row. */
interface Named extends Common {
	children?: never;
	"aria-label": string;
}

/**
 * A switch has to be named, and exactly one of the two ways: visible words that
 * `aria-label` would then override and hide from the reader who can see them,
 * or no words and a label only the screen reader gets.
 */
type Props = Labelled | Named;

const styles = stylex.create({
	/**
	 * The whole label is the hit area, so it carries the 44px pointer-target
	 * floor on both axes, the same floor `Checkbox` and the dialog's Close
	 * button already set. The track itself is far under it.
	 */
	label: {
		minWidth: 44,
		minHeight: 44,
		display: "inline-flex",
		alignItems: "center",
		gap: "0.5rem",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
	},
	/** Bare switches sit in a table cell, where the row sets the rhythm. */
	track: {
		flexShrink: 0,
		boxSizing: "border-box",
		width: 28,
		height: 16,
		display: "inline-flex",
		alignItems: "center",
		justifyContent: "flex-start",
		borderRadius: 8,
		padding: 2,
		backgroundColor: colors.borderStrong,
	},
	/**
	 * The thumb moves by the box's own alignment rather than by a transform, so
	 * there is no transition to withhold from a reader who asked for less motion.
	 */
	trackSelected: {
		justifyContent: "flex-end",
		backgroundColor: colors.primary,
	},
	/** As on `Checkbox`: the ring follows the state React Aria reports, because
	 *  `:focus-visible` lands on the hidden input rather than on this track. */
	trackFocused: {
		outlineWidth: 2,
		outlineStyle: "solid",
		outlineColor: colors.focus,
		outlineOffset: 2,
	},
	/** The track dims, not the words: the label still has to be readable. */
	trackDisabled: {
		opacity: 0.5,
	},
	thumb: {
		width: 12,
		height: 12,
		borderRadius: 6,
		backgroundColor: colors.surfaceRaised,
	},
});

export default function Switch({ children, ...state }: Props) {
	return (
		<AriaSwitch {...state} className={() => stylex.props(styles.label).className ?? ""}>
			{(renderProps) => (
				<>
					<span
						{...stylex.props(
							styles.track,
							renderProps.isSelected && styles.trackSelected,
							renderProps.isFocusVisible && styles.trackFocused,
							renderProps.isDisabled && styles.trackDisabled,
						)}
					>
						<span {...stylex.props(styles.thumb)} />
					</span>
					{children}
				</>
			)}
		</AriaSwitch>
	);
}
