/**
 * Copyright schukai GmbH and contributors 2023. 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 {instanceSymbol} from "../constants.mjs";
import { Base } from "../types/base.mjs";
import { validateString } from "../types/validate.mjs";
import { clone } from "../util/clone.mjs";

export { Locale, parseLocale };

/**
 * @memberOf Monster.I18n
 * @type {symbol}
 */
const propertiesSymbol = Symbol("properties");

/**
 * @type {symbol}
 * @memberOf Monster.I18n
 */
const localeStringSymbol = Symbol("localeString");

/**
 * The Locale class is a base class for the language classes.
 *
 * RFC
 *
 * ```
 * A Language-Tag consists of:
 * langtag                           ; generated tag
 *           -or- private-use        ; a private use tag
 *
 * langtag       = (language
 *                    ["-" script]
 *                    ["-" region]
 *                    *("-" variant)
 *                    *("-" extension)
 *                    ["-" privateuse])
 *
 * language      = "en", "ale", or a registered value
 *
 * script        = "Latn", "Cyrl", "Hant" ISO 15924 codes
 *
 * region        = "US", "CS", "FR" ISO 3166 codes
 *                 "419", "019",  or UN M.49 codes
 *
 * variant       = "rozaj", "nedis", "1996", multiple subtags can be used in a tag
 *
 * extension     = single letter followed by additional subtags; more than one extension
 *                 may be used in a language tag
 *
 * private-use   = "x-" followed by additional subtags, as many as are required
 *                 Note that these can start a tag or appear at the end (but not
 *                 in the middle)
 * ```
 *
 * @license AGPLv3
 * @since 1.13.0
 * @copyright schukai GmbH
 * @memberOf Monster.I18n
 * @see https://datatracker.ietf.org/doc/html/rfc3066
 */
class Locale extends Base {
    /**
     * @param {string} language
     * @param {string} [region]
     * @param {string} [script]
     * @param {string} [variants]
     * @param {string} [extlang]
     * @param {string} [privateUse]
     * @throws {Error} unsupported locale
     */
    constructor(language, region, script, variants, extlang, privateUse) {
        super();

        this[propertiesSymbol] = {
            language: language === undefined ? undefined : validateString(language),
            script: script === undefined ? undefined : validateString(script),
            region: region === undefined ? undefined : validateString(region),
            variants: variants === undefined ? undefined : validateString(variants),
            extlang: extlang === undefined ? undefined : validateString(extlang),
            privateUse: privateUse === undefined ? undefined : validateString(privateUse),
        };

        let s = [];
        if (language !== undefined) s.push(language);
        if (script !== undefined) s.push(script);
        if (region !== undefined) s.push(region);
        if (variants !== undefined) s.push(variants);
        if (extlang !== undefined) s.push(extlang);
        if (privateUse !== undefined) s.push(privateUse);

        if (s.length === 0) {
            throw new Error("unsupported locale");
        }

        this[localeStringSymbol] = s.join("-");
    }

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

    /**
     * @return {string}
     */
    get localeString() {
        return this[localeStringSymbol];
    }

    /**
     * @return {string|undefined}
     */
    get language() {
        return this[propertiesSymbol].language;
    }

    /**
     * @return {string|undefined}
     */
    get region() {
        return this[propertiesSymbol].region;
    }

    /**
     * @return {string|undefined}
     */
    get script() {
        return this[propertiesSymbol].script;
    }

    /**
     * @return {string|undefined}
     */
    get variants() {
        return this[propertiesSymbol].variants;
    }

    /**
     * @return {string|undefined}
     */
    get extlang() {
        return this[propertiesSymbol].extlang;
    }

    /**
     * @return {string|undefined}
     */
    get privateUse() {
        return this[propertiesSymbol].privateValue;
    }

    /**
     * @return {string}
     */
    toString() {
        return `${this.localeString}`;
    }

    /**
     * The structure has the following: language, script, region, variants, extlang, privateUse
     *
     * @return {Monster.I18n.LocaleMap}
     */
    getMap() {
        return clone(this[propertiesSymbol]);
    }
}

/**
 * @typedef {Object} LocaleMap
 * @property {string} language
 * @property {string} script
 * @property {string} region
 * @property {string} variants
 * @property {string} extlang
 * @property {string} privateUse
 * @memberOf Monster.I18n
 */

/**
 * Parse local according to rfc4646 standard
 *
 * Limitations: The regex cannot handle multiple variants or private.
 *
 * You can call the method via this function individually:
 *
 * ```javascript
 * import {createLocale} from '@schukai/monster/source/i18n/locale.mjs';
 * createLocale()
 * ```
 *
 * RFC
 *
 * ```
 *   The syntax of the language tag in ABNF [RFC4234] is:
 *
 *   Language-Tag  = langtag
 *                 / privateuse             ; private use tag
 *                 / grandfathered          ; grandfathered registrations
 *
 *   langtag       = (language
 *                    ["-" script]
 *                    ["-" region]
 *                    *("-" variant)
 *                    *("-" extension)
 *                    ["-" privateuse])
 *
 *   language      = (2*3ALPHA [ extlang ]) ; shortest ISO 639 code
 *                 / 4ALPHA                 ; reserved for future use
 *                 / 5*8ALPHA               ; registered language subtag
 *
 *   extlang       = *3("-" 3ALPHA)         ; reserved for future use
 *
 *   script        = 4ALPHA                 ; ISO 15924 code
 *
 *   region        = 2ALPHA                 ; ISO 3166 code
 *                 / 3DIGIT                 ; UN M.49 code
 *
 *   variant       = 5*8alphanum            ; registered variants
 *                 / (DIGIT 3alphanum)
 *
 *   extension     = singleton 1*("-" (2*8alphanum))
 *
 *   singleton     = %x41-57 / %x59-5A / %x61-77 / %x79-7A / DIGIT
 *                 ; "a"-"w" / "y"-"z" / "A"-"W" / "Y"-"Z" / "0"-"9"
 *                 ; Single letters: x/X is reserved for private use
 *
 *   privateuse    = ("x"/"X") 1*("-" (1*8alphanum))
 *
 *   grandfathered = 1*3ALPHA 1*2("-" (2*8alphanum))
 *                   ; grandfathered registration
 *                   ; Note: i is the only singleton
 *                   ; that starts a grandfathered tag
 *
 *   alphanum      = (ALPHA / DIGIT)       ; letters and numbers
 *
 *                        Figure 1: Language Tag ABNF
 * ```
 *
 * @param {string} locale
 * @returns {Locale}
 * @license AGPLv3
 * @since 1.14.0
 * @copyright schukai GmbH
 * @memberOf Monster.I18n
 * @throws {TypeError} value is not a string
 * @throws {Error} unsupported locale
 */
function parseLocale(locale) {
    locale = validateString(locale).replace(/_/g, "-");

    let language;
    let region;
    let variants;
    let parts;
    let script;
    let extlang;
    let regexRegular = "(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)";
    let regexIrregular =
        "(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)";
    let regexGrandfathered = `(${regexIrregular}|${regexRegular})`;
    let regexPrivateUse = "(x(-[A-Za-z0-9]{1,8})+)";
    let regexSingleton = "[0-9A-WY-Za-wy-z]";
    let regexExtension = `(${regexSingleton}(-[A-Za-z0-9]{2,8})+)`;
    let regexVariant = "([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})";
    let regexRegion = "([A-Za-z]{2}|[0-9]{3})";
    let regexScript = "([A-Za-z]{4})";
    let regexExtlang = "([A-Za-z]{3}(-[A-Za-z]{3}){0,2})";
    let regexLanguage = `(([A-Za-z]{2,3}(-${regexExtlang})?)|[A-Za-z]{4}|[A-Za-z]{5,8})`;
    let regexLangtag = `(${regexLanguage}(-${regexScript})?(-${regexRegion})?(-${regexVariant})*(-${regexExtension})*(-${regexPrivateUse})?)`;
    let regexLanguageTag = `^(${regexGrandfathered}|${regexLangtag}|${regexPrivateUse})$`;
    let regex = new RegExp(regexLanguageTag);
    let match;

    if ((match = regex.exec(locale)) !== null) {
        if (match.index === regex.lastIndex) {
            regex.lastIndex++;
        }
    }

    if (match === undefined || match === null) {
        throw new Error("unsupported locale");
    }

    if (match[6] !== undefined) {
        language = match[6];

        parts = language.split("-");
        if (parts.length > 1) {
            language = parts[0];
            extlang = parts[1];
        }
    }

    if (match[14] !== undefined) {
        region = match[14];
    }

    if (match[12] !== undefined) {
        script = match[12];
    }

    if (match[16] !== undefined) {
        variants = match[16];
    }

    return new Locale(language, region, script, variants, extlang);
}