/**
 * 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 { Base } from "../types/base.mjs";
import { validateString } from "../types/validate.mjs";
import { clone } from "../util/clone.mjs";

export { Locale, parseLocale };

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

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

/**
 * The Locale class is a base class for the language classes.
 *
 * @fragments /fragments/libraries/i18n/locale/
 *
 * 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 Volker Schukai
 * @summary A locale value object for parsing, validating and formatting language-region identifiers.
 * @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),
		};

		const 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.
	 * @return {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
 */

/**
 * 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
 * @return {Locale}
 * @license AGPLv3
 * @since 1.14.0
 * @copyright Volker Schukai
 * @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;
	const regexRegular =
		"(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)";
	const 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)";
	const regexGrandfathered = `(${regexIrregular}|${regexRegular})`;
	const regexPrivateUse = "(x(-[A-Za-z0-9]{1,8})+)";
	const regexSingleton = "[0-9A-WY-Za-wy-z]";
	const regexExtension = `(${regexSingleton}(-[A-Za-z0-9]{2,8})+)`;
	const regexVariant = "([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})";
	const regexRegion = "([A-Za-z]{2}|[0-9]{3})";
	const regexScript = "([A-Za-z]{4})";
	const regexExtlang = "([A-Za-z]{3}(-[A-Za-z]{3}){0,2})";
	const regexLanguage = `(([A-Za-z]{2,3}(-${regexExtlang})?)|[A-Za-z]{4}|[A-Za-z]{5,8})`;
	const regexLangtag = `(${regexLanguage}(-${regexScript})?(-${regexRegion})?(-${regexVariant})*(-${regexExtension})*(-${regexPrivateUse})?)`;
	const regexLanguageTag = `^(${regexGrandfathered}|${regexLangtag}|${regexPrivateUse})$`;
	const 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);
}
