/**
 * Copyright schukai GmbH and contributors 2022. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

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";


export {Translations, getDocumentTranslations};

/**
 * With this class you can manage translations and access the keys.
 *
 * @externalExample ../../example/i18n/translations.mjs
 * @license AGPLv3
 * @since 1.13.0
 * @copyright schukai GmbH
 * @memberOf Monster.I18n
 * @see https://datatracker.ietf.org/doc/html/rfc3066
 */
class Translations extends Base {
    /**
     *
     * @param {Locale} locale
     */
    constructor(locale) {
        super();

        if (locale instanceof Locale) {
            this.locale = locale;
        } else {
            this.locale = parseLocale(validateString(locale));
        }

        this.storage = new Map();
    }

    /**
     * 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);
        }

        let 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|count} count
     * @param {string|undefined} defaultText
     * @return {string}
     */
    getPluralRuleText(key, count, defaultText) {
        if (!this.storage.has(key)) {
            return validateString(defaultText);
        }

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

        let keyword;
        if (isString(count)) {
            keyword = count.toLocaleString();
        } else {
            count = validateInteger(count);
            if (count === 0) {
                // special handlig 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]);
        }

        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.
 * @returns {Translations}
 * @throws {Error} Element is not an HTMLElement.
 * @throws {Error} Cannot find element with translations. Add a translations 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 element with translations. Add a translations 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.");
    }
    
    let obj = getLinkedObjects(element, translationsLinkSymbol);
    
    for (const t of obj) {
        if (t instanceof Translations) {
            return t;
        }
    }

    throw new Error("Missing translations.");

}