export function clamp({
	value,
	min,
	max,
}: {
	value: number;
	min: number;
	max: number;
}): number {
	return Math.max(min, Math.min(max, value));
}

export function clampRound({
	value,
	min,
	max,
}: {
	value: number;
	min: number;
	max: number;
}): number {
	return Math.round(clamp({ value, min, max }));
}

export function getFractionDigitsForStep({ step }: { step: number }): number {
	const normalizedStep = step.toString().toLowerCase();
	if (normalizedStep.includes("e-")) {
		return Number(normalizedStep.split("e-")[1] ?? 0);
	}
	const [, fractionalPart = ""] = normalizedStep.split(".");
	return fractionalPart.length;
}

export function snapToStep({
	value,
	step,
}: {
	value: number;
	step: number;
}): number {
	if (step <= 0) return value;
	const snappedValue = Math.round(value / step) * step;
	return Number(
		snappedValue.toFixed(getFractionDigitsForStep({ step })),
	);
}

export function isNearlyEqual({
	leftValue,
	rightValue,
	epsilon = 0.0001,
}: {
	leftValue: number;
	rightValue: number;
	epsilon?: number;
}): boolean {
	return Math.abs(leftValue - rightValue) <= epsilon;
}

export function formatNumberForDisplay({
	value,
	fractionDigits,
	minFractionDigits = 0,
	maxFractionDigits = 6,
}: {
	value: number;
	fractionDigits?: number;
	minFractionDigits?: number;
	maxFractionDigits?: number;
}): string {
	const resolvedMaxFractionDigits = Math.max(
		0,
		fractionDigits ?? maxFractionDigits,
	);
	const resolvedMinFractionDigits = Math.min(
		Math.max(0, fractionDigits ?? minFractionDigits),
		resolvedMaxFractionDigits,
	);
	const fixedValue = value.toFixed(resolvedMaxFractionDigits);

	if (resolvedMaxFractionDigits === 0) {
		return Number(fixedValue) === 0 ? "0" : fixedValue;
	}

	const [integerPart, fractionPart = ""] = fixedValue.split(".");
	const normalizedIntegerPart = Number(fixedValue) === 0 ? "0" : integerPart;
	let trimmedFractionPart = fractionPart;

	while (
		trimmedFractionPart.length > resolvedMinFractionDigits &&
		trimmedFractionPart.endsWith("0")
	) {
		trimmedFractionPart = trimmedFractionPart.slice(0, -1);
	}

	return trimmedFractionPart
		? `${normalizedIntegerPart}.${trimmedFractionPart}`
		: normalizedIntegerPart;
}

/**
 * Safely evaluates a mathematical expression without using eval() or new Function().
 * Supports +, -, *, /, parentheses, and decimal numbers.
 */
export function evaluateMathExpression({
	input,
}: {
	input: string;
}): number | null {
	const sanitized = input.trim();
	if (!/^[\d.\s+\-*/()]+$/.test(sanitized)) return null;
	try {
		return parseExpression(sanitized);
	} catch {
		return null;
	}
}

/**
 * Recursive descent parser for basic arithmetic expressions.
 * Grammar:
 *   expression = term { ('+' | '-') term }
 *   term = factor { ('*' | '/') factor }
 *   factor = number | '(' expression ')'
 */
function parseExpression(input: string): number | null {
	const tokens = tokenize(input);
	if (tokens.length === 0) return null;

	let index = 0;

	function peek(): Token | null {
		return tokens[index] ?? null;
	}

	function consume(): Token | null {
		return tokens[index++] ?? null;
	}

	function parseExpressionLevel(): number {
		let left = parseTerm();
		while (true) {
			const token = peek();
			if (token?.type === "PLUS") {
				consume();
				left = left + parseTerm();
			} else if (token?.type === "MINUS") {
				consume();
				left = left - parseTerm();
			} else {
				break;
			}
		}
		return left;
	}

	function parseTerm(): number {
		let left = parseFactor();
		while (true) {
			const token = peek();
			if (token?.type === "MULTIPLY") {
				consume();
				left = left * parseFactor();
			} else if (token?.type === "DIVIDE") {
				consume();
				const right = parseFactor();
				if (right === 0) throw new Error("Division by zero");
				left = left / right;
			} else {
				break;
			}
		}
		return left;
	}

	function parseFactor(): number {
		const token = peek();
		if (!token) throw new Error("Unexpected end of expression");

		if (token.type === "NUMBER") {
			consume();
			return token.value;
		}

		if (token.type === "MINUS") {
			consume();
			return -parseFactor();
		}

		if (token.type === "LPAREN") {
			consume();
			const value = parseExpressionLevel();
			const next = peek();
			if (next?.type !== "RPAREN") {
				throw new Error("Missing closing parenthesis");
			}
			consume();
			return value;
		}

		throw new Error(`Unexpected token: ${token.type}`);
	}

	const result = parseExpressionLevel();

	if (index !== tokens.length) {
		return null;
	}

	if (!Number.isFinite(result)) return null;
	return result;
}

type Token =
	| { type: "NUMBER"; value: number }
	| { type: "PLUS" }
	| { type: "MINUS" }
	| { type: "MULTIPLY" }
	| { type: "DIVIDE" }
	| { type: "LPAREN" }
	| { type: "RPAREN" };

function tokenize(input: string): Token[] {
	const tokens: Token[] = [];
	let i = 0;
	const str = input.trim();

	while (i < str.length) {
		const char = str[i];

		if (/\s/.test(char)) {
			i++;
			continue;
		}

		if (/\d/.test(char)) {
			let numStr = "";
			while (i < str.length && (/\d/.test(str[i]) || str[i] === ".")) {
				numStr += str[i];
				i++;
			}
			const value = Number(numStr);
			if (!Number.isFinite(value)) {
				throw new Error(`Invalid number: ${numStr}`);
			}
			tokens.push({ type: "NUMBER", value });
			continue;
		}

		switch (char) {
			case "+":
				tokens.push({ type: "PLUS" });
				break;
			case "-":
				tokens.push({ type: "MINUS" });
				break;
			case "*":
				tokens.push({ type: "MULTIPLY" });
				break;
			case "/":
				tokens.push({ type: "DIVIDE" });
				break;
			case "(":
				tokens.push({ type: "LPAREN" });
				break;
			case ")":
				tokens.push({ type: "RPAREN" });
				break;
			default:
				throw new Error(`Invalid character: ${char}`);
		}
		i++;
	}

	return tokens;
}
