/*!
 * V4Fire Core
 * https://github.com/V4Fire/Core
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Core/blob/master/LICENSE
 */

import log from 'core/log';
import extend from 'core/prelude/extend';

import langPacs, { Translation, PluralTranslation } from 'lang';

import { locale } from 'core/prelude/i18n/const';
import type { I18nOpts, PluralizationCount, I18nMeta } from 'core/prelude/i18n/interface';

/** @see [[i18n]] */
extend(globalThis, 'i18n', i18nFactory);

const
	logger = log.namespace('i18n');

/**
 * Creates a function to internationalize strings in an application based on the given locale and keyset.
 * Keyset allows you to share the same keys in different contexts.
 * For example, the key "Next" may have a different value in different components of the application, therefore,
 * we can use the name of the component as a keyset value.
 *
 * @param keysetNameOrNames - the name of keyset or array with names of keysets to use.
 *   If passed as an array, the priority of the cases will be arranged in the order of the elements,
 *   the first one will have the highest priority.
 *
 * @param [customLocale] - the locale used to search for translations (the default is taken from
 *   the application settings)
 */
export function i18nFactory(
	keysetNameOrNames: string | string[], customLocale?: Language
): (key: string, params?: I18nParams) => string {
	const
		resolvedLocale = customLocale ?? locale.value,
		keysetNames = Object.isArray(keysetNameOrNames) ? keysetNameOrNames : [keysetNameOrNames];

	if (resolvedLocale == null) {
		throw new ReferenceError('The locale for internationalization is not defined');
	}

	const pluralRules: CanUndef<Intl.PluralRules> = getPluralRules(resolvedLocale);

	return function i18n(value: string | TemplateStringsArray, params?: I18nParams) {
		if (Object.isArray(value) && value.length !== 1) {
			throw new SyntaxError('Using i18n with template literals is allowed only without variables');
		}

		const
			key = Object.isString(value) ? value : value[0],
			correctKeyset = keysetNames.find((keysetName) => langPacs[resolvedLocale]?.[keysetName]?.[key]),
			translateValue = langPacs[resolvedLocale]?.[correctKeyset ?? '']?.[key],
			meta: I18nMeta = {language: resolvedLocale, keyset: correctKeyset, key};

		if (translateValue != null && translateValue !== '') {
			return resolveTemplate(translateValue, params, {pluralRules, meta});
		}

		logger.error(
			'Translation for the given key is not found',
			`Key: ${key}, KeysetNames: ${keysetNames.join(', ')}, LocaleName: ${resolvedLocale}, available locales: ${Object.keys(langPacs).join(', ')}`
		);

		return resolveTemplate(key, params, {pluralRules, meta});
	};
}

/**
 * Returns the form for plural sentences and resolves variables from the passed template
 *
 * @param value - a string for the default case, or an array of strings for the plural case
 * @param params - a dictionary with parameters for internationalization
 * @params [opts] - additional options for current translation
 *
 * @example
 * ```typescript
 * const example = resolveTemplate('My name is {name}, I live in {city}', {name: 'John', city: 'Denver'});
 *
 * console.log(example); // 'My name is John, I live in Denver'
 *
 * const examplePluralize = resolveTemplate({
 *  one: {count} product,
 *  few: {count} products,
 *  many: {count} products,
 *  zero: {count} products,
 * }, {count: 5});
 *
 * console.log(examplePluralize); // '5 products'
 * ```
 */
export function resolveTemplate(value: Translation, params?: I18nParams, opts: I18nOpts = {}): string {
	const
		template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts) : value;

	return template.replace(/{([^}]+)}/g, (_, key) => {
		if (params?.[key] == null) {
			logger.error('Undeclared variable', `Name: "${key}", Template: "${template}"`);
			return key;
		}

		return params[key];
	});
}

/**
 * Returns the correct plural form to translate based on the given count
 *
 * @param pluralTranslation - list of translation variants
 * @param count - the value on the basis of which the form of pluralization will be selected
 * @params [opts] - additional options for current translation
 *
 * @example
 * ```typescript
 * const result = pluralizeText({
 *  one: {count} product,
 *  few: {count} products,
 *  many: {count} products,
 *  zero: {count} products,
 *  other: {count} products,
 * }, 5, {pluralRules: new Intl.PluralRulse('en')});
 *
 * console.log(result); // '{count} products'
 * ```
 */
export function pluralizeText(
	pluralTranslation: PluralTranslation,
	count: CanUndef<PluralizationCount>,
	opts: I18nOpts = {}
): string {
	const {pluralRules, meta} = opts;

	let normalizedCount;

	if (Object.isNumber(count)) {
		normalizedCount = count;

	} else if (Object.isString(count)) {
		const translation = pluralTranslation[count];

		if (translation != null) {
			return translation;
		}
	}

	if (normalizedCount == null) {
		logger.error(
			'Invalid value of the `count` parameter for string pluralization',
			`Count: ${count}, Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`
		);

		normalizedCount = 1;
	}

	const
		pluralFormName = getPluralFormName(normalizedCount, pluralRules),
		translation = pluralTranslation[pluralFormName];

	if (translation == null) {
		logger.error(
			`Plural form ${pluralFormName} doesn't exist.`,
			`Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`
		);

		return pluralTranslation.one;
	}

	return translation;
}

/**
 * Returns the plural form name for a given number `n` based on the specified pluralization rules.
 * Otherwise will be used default set of rules.
 *
 * If a `rules` object implementing `Intl.PluralRules` is provided, it will use that to determine the plural form.
 * Otherwise, it will fall back to a custom rule set:
 * - Returns 'zero' for `n === 0`.
 * - Returns 'one' for `n === 1`.
 * - Returns 'few' for `n > 1 && n < 5`.
 * - Returns 'many' for all other values of `n`.
 *
 * @param n - The number to evaluate for pluralization.
 * @param rules - Plural rules object. If undefined, a default rule set is used.
 */
export function getPluralFormName(n: number, rules?: CanUndef<Intl.PluralRules>): keyof Required<PluralTranslation> {
	if (rules != null) {
		return <keyof PluralTranslation>rules.select(n);
	}

	switch (n) {
		case 0:
			return 'zero';

		case 1:
			return 'one';

		default:
			if (n > 1 && n < 5) {
				return 'few';
			}

			return 'many';
	}
}

/**
 * Returns an instance of `Intl.PluralRules` for a given locale, if supported.
 * @param locale - The locale for which to generate plural rules.
 */
export function getPluralRules(locale: Language): CanUndef<Intl.PluralRules> {
	if ('PluralRules' in globalThis['Intl']) {
		return new globalThis['Intl'].PluralRules(locale);
	}
}
