'use no memo'; // composition props usually invalidate the generated wrapper caches

import { createContext, Fragment, type ReactElement, type ReactNode, useContext } from 'react';

import { Select as BaseSelect } from '@base-ui/react/select';
import { clsx } from 'clsx';

import * as styles from '#/components/Select.css';

import CheckIcon from '#/icons/central/Checkmark2_round_outlined_radius1_stroke2.svg';
import ChevronDownIcon from '#/icons/central/ChevronBottom_round_outlined_radius1_stroke2.svg';
import ChevronUpIcon from '#/icons/central/ChevronTop_round_outlined_radius1_stroke2.svg';

export type SelectItem<Value = string> = {
	label: string;
	value: Value;
};

// feeds the current value to `Content`'s `renderItem` so consumers can style an item against the
// selection (Base UI only exposes per-item `data-selected`, not the value, inside the render closure).
const SelectedValueContext = createContext<unknown>(null);
SelectedValueContext.displayName = 'SelectSelectedValueContext';

export type RootProps<Value = string> = {
	children: ReactNode;
	value: Value;
	onValueChange: (value: Value) => void;
	disabled?: boolean;
	/** The option list. Required for `Value` to auto-render the selected item's label. */
	items?: SelectItem<Value>[];
};

/** Groups the parts of a single-select dropdown built on Base UI's Select. */
export function Root<Value = string>({ children, disabled, items, onValueChange, value }: RootProps<Value>) {
	return (
		<SelectedValueContext.Provider value={value}>
			<BaseSelect.Root
				items={items}
				value={value}
				disabled={disabled}
				onValueChange={(next) => {
					if (next !== null) {
						onValueChange(next);
						return;
					}
					// Base UI reports `null` both for a cleared selection and for an option carrying `null`.
					// reading the value back off the option is what keeps it typed as `Value`.
					const nullItem = items?.find((item) => item.value === null);
					if (nullItem) {
						onValueChange(nullItem.value);
					}
				}}
			>
				{children}
			</BaseSelect.Root>
		</SelectedValueContext.Provider>
	);
}

export type TriggerProps = {
	children: ReactNode;
	/**
	 * Accessible name, applied as `aria-label` to the default trigger button. Omit when `render` supplies an
	 * element that carries its own label.
	 */
	label?: string;
	/**
	 * replaces the default themed button with a custom web-native element. prefer the children form for the
	 * common case; reach for `render` only to supply a custom trigger element.
	 */
	render?: BaseSelect.Trigger.Props['render'];
};

/** The button that opens the dropdown. Compose `Value` + `Icon` inside, or pass a custom `render` element. */
export function Trigger({ children, label, render }: TriggerProps) {
	if (render) {
		return <BaseSelect.Trigger render={render}>{children}</BaseSelect.Trigger>;
	}
	return (
		<BaseSelect.Trigger aria-label={label} className={styles.trigger}>
			{children}
		</BaseSelect.Trigger>
	);
}

export type ValueProps = {
	placeholder?: string;
	className?: string;
	/** Custom formatter for the selected value; defaults to the matched item's label. */
	children?: (value: string) => ReactNode;
};

/** Shows the selected item's label (or `placeholder` when nothing is selected). */
export function Value({ children, className, placeholder }: ValueProps) {
	return (
		<BaseSelect.Value className={clsx(styles.value, className)} placeholder={placeholder}>
			{children}
		</BaseSelect.Value>
	);
}

export type IconProps = {
	className?: string;
};

/** The chevron affordance inside the trigger. */
export function Icon({ className }: IconProps) {
	return (
		<BaseSelect.Icon className={clsx(styles.icon, className)}>
			<ChevronDownIcon className={styles.chevronIcon} />
		</BaseSelect.Icon>
	);
}

export type ContentProps<Value = string> = {
	/** How the popup aligns to the trigger along its width. Defaults to Base UI's `center`. */
	align?: 'center' | 'end' | 'start';
	/** The options to render. */
	items: SelectItem<Value>[];
	/** stretch the popup to at least the trigger's width. pass `false` to size it to its content instead. */
	matchTriggerWidth?: boolean;
	/** Renders one option; receives the current selection so an item can style itself against it. */
	renderItem: (item: SelectItem<Value>, selectedValue: Value) => ReactElement;
};

/** The portalled, positioned popup that holds the option list. */
export function Content<Value = string>({
	align,
	items,
	matchTriggerWidth = true,
	renderItem,
}: ContentProps<Value>) {
	// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- context can't carry `Root`'s generic
	const selectedValue = useContext(SelectedValueContext) as Value;
	return (
		<BaseSelect.Portal>
			<BaseSelect.Positioner
				className={styles.positioner}
				align={align}
				sideOffset={5}
				alignItemWithTrigger={false}
			>
				<BaseSelect.Popup className={styles.popup({ matchTriggerWidth })}>
					<BaseSelect.ScrollUpArrow className={styles.scrollUpArrow}>
						<ChevronUpIcon className={styles.chevronIcon} />
					</BaseSelect.ScrollUpArrow>
					<BaseSelect.List className={styles.list}>
						{items.map((item) => (
							<Fragment key={String(item.value)}>{renderItem(item, selectedValue)}</Fragment>
						))}
					</BaseSelect.List>
					<BaseSelect.ScrollDownArrow className={styles.scrollDownArrow}>
						<ChevronDownIcon className={styles.chevronIcon} />
					</BaseSelect.ScrollDownArrow>
				</BaseSelect.Popup>
			</BaseSelect.Positioner>
		</BaseSelect.Portal>
	);
}

export type ItemProps<Value = string> = {
	children: ReactNode;
	value: Value;
	/** Text used for keyboard typeahead; defaults to the item's text content. */
	label: string;
	className?: string;
};

/** A single option within `Content`. */
export function Item<Value = string>({ children, className, label, value }: ItemProps<Value>) {
	return (
		<BaseSelect.Item value={value} label={label} className={clsx(styles.item, className)}>
			{children}
		</BaseSelect.Item>
	);
}

/** The selection checkmark, absolutely positioned in the item's gutter. */
export function ItemIndicator() {
	return (
		<BaseSelect.ItemIndicator className={styles.indicator}>
			<CheckIcon className={styles.checkIcon} />
		</BaseSelect.ItemIndicator>
	);
}

/** The option's text label. */
export function ItemText({ children }: { children: ReactNode }) {
	return <BaseSelect.ItemText>{children}</BaseSelect.ItemText>;
}
