import { enUS } from './systems';
import { StringifySystem } from './common';

/**
 * Negative symbol.
 */
const NEGATIVE_SYMBOL = '-' as const;

/**
 * Allowed value type for {@link stringify}.
 */
type AllowedValue = string | number | bigint;

/**
 * Array of allowed types for {@link parse}.
 */
const ALLOWED_PARSE_RESULT_TYPES = [
	'string',
	'number',
	'bigint',
] as const;

/**
 * Allowed type for {@link parse}.
 */
type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];

/**
 * Options to use when converting a value to a string.
 */
export interface StringifyOptions {
	/**
	 * The system to use when converting a value to a string.
	 *
	 * Defaults to en-US (American short count).
	 */
	system?: StringifySystem;
	/**
	 * Options to use when making a group. This is used to override the default options for a group.
	 */
	makeGroupOptions?: Record<string, unknown>;
}

/**
 * Converts a numeric value to its name.
 * @param value - The value to convert.
 * @param options - Options to use when converting a value to its name.
 * @returns The name of the value.
 */
export const stringify = (
	value: AllowedValue,
	options = {} as StringifyOptions,
): string => {
	if (!(
		(ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
			.includes(typeof (value as unknown))
	)) {
		throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`);
	}

	const valueStr = value.toString().replace(/\s/g, '');
	const { system = enUS, makeGroupOptions } = options;

	if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
		return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
	}

	const groups = system
		.group(valueStr)
		.map(([group, place]) => (
			system.makeGroup(group, place, makeGroupOptions)
		));

	return system.finalize(groups);
};

/**
 * Options to use when parsing a name of a number to its numeric equivalent.
 */
export interface ParseOptions {
	/**
	 * The system to use when parsing a name of a number to its numeric equivalent.
	 *
	 * Defaults to en-US (American short count).
	 */
	system?: StringifySystem;
	/**
	 * The type to parse the value as.
	 */
	type?: ParseResult;
}

/**
 * Parses a name of a number to its numeric equivalent.
 * @param value - The value to parse.
 * @param options - Options to use when parsing a name of a number to its numeric equivalent.
 * @returns The numeric equivalent of the value.
 */
export const parse = (value: string, options = {} as ParseOptions) => {
	const { system = enUS, type = 'string' } = options;

	const tokens = system.tokenize(value);
	const groups = system.parseGroups(tokens);
	const stringValue = system.combineGroups(groups);

	switch (type) {
	case 'number':
		// Precision might be lost here. Use bigint when not using fractional parts.
		return Number(stringValue);
	case 'bigint':
		return BigInt(stringValue);
	default:
		break;
	}

	return stringValue;
};