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

import type { ComponentPropsWithoutRef, Ref } from 'react';

import { assignInlineVars } from '@vanilla-extract/dynamic';
import { clsx } from 'clsx';

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

import type { RecipeVariants } from '#/styles/recipe';

type TextVariants = RecipeVariants<typeof styles.text>;

type TextStyleProps = {
	size?: TextVariants['size'];
	weight?: TextVariants['weight'];
	color?: TextVariants['color'];
	align?: TextVariants['align'];
	leading?: TextVariants['leading'];
};

export type TextProps = Omit<ComponentPropsWithoutRef<'span'>, 'color' | 'style'> &
	TextStyleProps & {
		/** Clamp to this many lines with an ellipsis. */
		numberOfLines?: number;
		/** Forwarded to the `<span>` so the text can back a headless trigger (e.g. a Base UI tooltip). */
		ref?: Ref<HTMLSpanElement>;
		/** Tri-state text selection: omit for the browser default, `true` to force selectable, `false` to lock. */
		selectable?: boolean;
	};

/** The web-native text primitive. Renders a `<span>` styled through the `text` recipe. */
export function Text({
	size,
	weight,
	color,
	align,
	leading,
	numberOfLines,
	selectable,
	className,
	children,
	...rest
}: TextProps) {
	const clamped = numberOfLines != null;
	const singleLine = numberOfLines === 1;

	return (
		<span
			className={clsx(
				styles.text({ align, color, leading, size, weight }),
				clamped && (singleLine ? styles.clampSingleLine : styles.clampMultiLine),
				selectable === true && styles.userSelect.text,
				selectable === false && styles.userSelect.none,
				className,
			)}
			style={
				clamped && !singleLine
					? assignInlineVars({ [styles.lineClampVar]: String(numberOfLines) })
					: undefined
			}
			{...rest}
		>
			{children}
		</span>
	);
}

export type LabelTextProps = Omit<ComponentPropsWithoutRef<'label'>, 'color'> & TextStyleProps;

/** A `<label>` styled through the same `text` recipe as {@link Text}, for form-field labels. */
export function LabelText({
	size,
	weight,
	color,
	align,
	leading,
	className,
	children,
	...rest
}: LabelTextProps) {
	// renders a semantic <label> host element — the *Text-must-return-Text rule doesn't model this
	// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
	return (
		<label className={clsx(styles.text({ align, color, leading, size, weight }), className)} {...rest}>
			{children}
		</label>
	);
}
