/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol } from "../constants.mjs";
import { getLinkedObjects, hasObjectLink } from "../dom/attributes.mjs";
import { ATTRIBUTE_OBJECTLINK } from "../dom/constants.mjs";
import { getDocument } from "../dom/util.mjs";
import { Base } from "../types/base.mjs";
import { isObject, isString } from "../types/is.mjs";
import {
	validateInteger,
	validateObject,
	validateString,
} from "../types/validate.mjs";
import { Locale, parseLocale } from "./locale.mjs";
import { translationsLinkSymbol } from "./provider.mjs";

import { getLocaleOfDocument } from "../dom/locale.mjs";

export { Translations, getDocumentTranslations };

/**
 * With this class you can manage translations and access the keys.
 *
 * @fragments /fragments/libraries/i18n/translations/
 *
 * @externalExample ../../example/i18n/translations.mjs
 * @example /examples/libraries/i18n/translations/simple/ Resolve singular and plural translations
 * @example /examples/libraries/i18n/translations/bulk-assignment/ Load multiple translation keys from plain objects
 * @example /examples/libraries/i18n/translations/fallbacks/ Use explicit default text when a key is missing
 * @example /examples/libraries/i18n/translations/plural-keywords/ Resolve plural texts with explicit plural rule keywords
 * @license AGPLv3
 * @since 1.13.0
 * @copyright Volker Schukai
 * @summary A translation registry for storing localized text keys and resolving plural-aware output.
 * @see https://datatracker.ietf.org/doc/html/rfc3066
 */
class Translations extends Base {
	/**
	 *
	 * @param {Locale} locale
	 */
	constructor(locale) {
		super();

		try {
			if (locale instanceof Locale) {
				this.locale = locale;
			} else if (isString(locale)) {
				this.locale = parseLocale(validateString(locale));
			} else {
				this.locale = getLocaleOfDocument();
			}
		} catch (e) {
		} finally {
			if (!(this.locale instanceof Locale)) {
				this.locale = new Locale("en");
			}
		}

		this.storage = new Map();
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 3.27.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/i18n/translations@@instance");
	}

	/**
	 * Fetches a text using the specified key.
	 * If no suitable key is found, `defaultText` is taken.
	 *
	 * @param {string} key
	 * @param {string|undefined} defaultText
	 * @return {string}
	 * @throws {Error} key not found
	 */
	getText(key, defaultText) {
		if (!this.storage.has(key)) {
			if (defaultText === undefined) {
				throw new Error(`key ${key} not found`);
			}

			return validateString(defaultText);
		}

		const r = this.storage.get(key);
		if (isObject(r)) {
			return this.getPluralRuleText(key, "other", defaultText);
		}

		return this.storage.get(key);
	}

	/**
	 * A number `count` can be passed to this method. In addition to a number, one of the keywords can also be passed directly.
	 * "zero", "one", "two", "few", "many" and "other". Remember: not every language has all rules.
	 *
	 * The appropriate text for this number is then selected. If no suitable key is found, `defaultText` is taken.
	 *
	 * @param {string} key
	 * @param {integer|string} count
	 * @param {string|undefined} defaultText
	 * @return {string}
	 */
	getPluralRuleText(key, count, defaultText) {
		if (!this.storage.has(key)) {
			return validateString(defaultText);
		}

		const r = validateObject(this.storage.get(key));

		let keyword;
		if (isString(count)) {
			keyword = count.toLocaleString();
		} else {
			count = validateInteger(count);
			if (count === 0) {
				// special handling for zero count
				if (r.hasOwnProperty("zero")) {
					return validateString(r?.zero);
				}
			}

			keyword = new Intl.PluralRules(this.locale.toString()).select(
				validateInteger(count),
			);
		}

		if (r.hasOwnProperty(keyword)) {
			return validateString(r[keyword]);
		}

		// @deprecated since 2023-03-14
		// DEFAULT_KEY is undefined
		// if (r.hasOwnProperty(DEFAULT_KEY)) {
		//     return validateString(r[DEFAULT_KEY]);
		// }

		return validateString(defaultText);
	}

	/**
	 * Set a text for a key
	 *
	 * ```
	 * translations.setText("text1", "Make my day!");
	 * // plural rules
	 * translations.setText("text6", {
	 *     "zero": "There are no files on Disk.",
	 *     "one": "There is one file on Disk.",
	 *     "other": "There are files on Disk."
	 *     "default": "There are files on Disk."
	 * });
	 * ```
	 *
	 * @param {string} key
	 * @param {string|object} text
	 * @return {Translations}
	 * @throws {TypeError} value is not a string or object
	 */
	setText(key, text) {
		if (isString(text) || isObject(text)) {
			this.storage.set(validateString(key), text);
			return this;
		}

		throw new TypeError("value is not a string or object");
	}

	/**
	 * This method can be used to transfer overlays from an object. The keys are transferred, and the values are entered
	 * as text.
	 *
	 * The values can either be character strings or, in the case of texts with plural forms, objects. The plural forms
	 * must be stored as text via a standard key "zero", "one", "two", "few", "many" and "other".
	 *
	 * Additionally, the key default can be specified, which will be used if no other key fits.
	 *
	 * In some languages, like for example in German, there is no own more number at the value 0. In these languages,
	 * the function applies additionally zero.
	 *
	 * ```
	 * translations.assignTranslations({
	 *   "text1": "Make my day!",
	 *   "text2": "I'll be back!",
	 *   "text6": {
	 *     "zero": "There are no files on Disk.",
	 *     "one": "There is one file on Disk.",
	 *     "other": "There are files on Disk."
	 *     "default": "There are files on Disk."
	 * });
	 * ```
	 *
	 * @param {object} translations
	 * @return {Translations}
	 */
	assignTranslations(translations) {
		validateObject(translations);

		if (translations instanceof Translations) {
			translations.storage.forEach((v, k) => {
				this.setText(k, v);
			});
			return this;
		}

		for (const [k, v] of Object.entries(translations)) {
			this.setText(k, v);
		}

		return this;
	}
}

/**
 * Returns the translations for the current document.
 *
 * @param {HTMLElement|undefined} [element] - Element to search for translations. Default: element with objectlink @schukai/monster/i18n/translations@@link.
 * @return {Translations}
 * @throws {Error} Element is not an HTMLElement.
 * @throws {Error} Cannot find the element with translations. Add the translation object to the document.
 * @throws {Error} This element has no translations.
 * @throws {Error} Missing translations.
 */
function getDocumentTranslations(element) {
	const d = getDocument();

	if (!(element instanceof HTMLElement)) {
		element = d.querySelector(
			`[${ATTRIBUTE_OBJECTLINK}~="${translationsLinkSymbol.toString()}"]`,
		);

		if (element === null) {
			throw new Error(
				"Cannot find the element with translations. Add the translation object to the document.",
			);
		}
	}

	if (!(element instanceof HTMLElement)) {
		throw new Error("Element is not an HTMLElement.");
	}

	if (!hasObjectLink(element, translationsLinkSymbol)) {
		throw new Error("This element has no translations.");
	}

	const obj = getLinkedObjects(element, translationsLinkSymbol);

	for (const t of obj) {
		if (t instanceof Translations) {
			return t;
		}
	}

	throw new Error("Missing translations.");
}
