/**
 * 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 { registerCustomElement } from "../dom/customelement.mjs";
import { sanitizeHtml } from "../dom/sanitize-html.mjs";
import { addErrorAttribute } from "../dom/error.mjs";
import { getDocument } from "../dom/util.mjs";
import { getDocumentTranslations } from "./translations.mjs";
import { isObject, isString } from "../types/is.mjs";
import { validateObject, validateString } from "../types/validate.mjs";

export {
	I18nReplaceMarker,
	setI18nReplaceSanitizePolicy,
	setI18nReplaceDefaultSanitizePolicy,
	getI18nReplaceSanitizePolicy,
};

/**
 * @private
 * @type {string}
 */
const TAG_PREFIX = "monster-";

/**
 * @private
 * @type {string}
 */
const TAG_NAME = `${TAG_PREFIX}i18n-replace`;

/**
 * @private
 * @type {Map<string, object>}
 */
const sanitizePolicies = new Map();

/**
 * @private
 * @type {string}
 */
let defaultSanitizePolicy = "default";

sanitizePolicies.set("default", {});
sanitizePolicies.set("strict", {
	blockedTags: ["script", "iframe", "object", "embed", "link", "meta"],
});

/**
 * Register or update a sanitize policy used by <monster-i18n-replace>.
 *
 * @param {string} name
 * @param {object} options
 * @return {void}
 */
function setI18nReplaceSanitizePolicy(name, options = {}) {
	name = validateString(name);
	if (!isObject(options)) {
		options = {};
	}
	sanitizePolicies.set(name, validateObject(options));
}

/**
 * Set the default sanitize policy name.
 *
 * @param {string} name
 * @return {void}
 */
function setI18nReplaceDefaultSanitizePolicy(name) {
	defaultSanitizePolicy = validateString(name);
}

/**
 * Get a sanitize policy by name.
 *
 * @param {string} name
 * @return {object|undefined}
 */
function getI18nReplaceSanitizePolicy(name) {
	if (!isString(name)) {
		return sanitizePolicies.get(defaultSanitizePolicy);
	}
	return sanitizePolicies.get(name);
}

/**
 * Replace marker element with translated HTML.
 */
class I18nReplaceMarker extends HTMLElement {
	/**
	 * @return {string}
	 */
	static getTag() {
		return TAG_NAME;
	}

	connectedCallback() {
		try {
			replaceWithTranslation.call(this);
		} catch (e) {
			addErrorAttribute(this, e);
		}
	}
}

registerCustomElement(I18nReplaceMarker);

/**
 * @private
 * @return {void}
 */
function replaceWithTranslation() {
	const key = this.getAttribute("key");
	if (!isString(key) || key.trim() === "") {
		return;
	}

	let translations = null;
	try {
		translations = getDocumentTranslations();
	} catch (e) {}

	const { countValue, countKeyword } = parseCount.call(this);
	const fallback = resolveFallback.call(
		this,
		countValue,
		countKeyword,
		translations,
	);

	let value = fallback;
	if (translations) {
		try {
			if (countValue !== undefined || countKeyword !== undefined) {
				const keyOrCount = countKeyword ?? countValue;
				value = translations.getPluralRuleText(key, keyOrCount, fallback);
			} else {
				value = translations.getText(key, fallback);
			}
		} catch (e) {
			value = fallback;
		}
	}

	if (!isString(value)) {
		value = fallback;
	}

	if (countValue !== undefined) {
		value = replaceCountPlaceholder(value, countValue, translations);
	}

	const policyName = this.getAttribute("sanitize") || defaultSanitizePolicy;
	const policy = getI18nReplaceSanitizePolicy(policyName) || {};
	const sanitized = sanitizeHtml(value, policy);

	replaceElementWithHtml(this, sanitized);
}

/**
 * @private
 * @return {{countValue: number|undefined, countKeyword: string|undefined}}
 */
function parseCount() {
	const raw = this.getAttribute("count");
	if (!isString(raw) || raw.trim() === "") {
		return { countValue: undefined, countKeyword: undefined };
	}

	const trimmed = raw.trim();
	const parsed = Number.parseInt(trimmed, 10);
	if (!Number.isNaN(parsed)) {
		return { countValue: parsed, countKeyword: undefined };
	}

	const keyword = trimmed.toLowerCase();
	return { countValue: undefined, countKeyword: keyword };
}

/**
 * @private
 * @param {number|undefined} countValue
 * @param {string|undefined} countKeyword
 * @param {object|null} translations
 * @return {string}
 */
function resolveFallback(countValue, countKeyword, translations) {
	if (countValue !== undefined || countKeyword !== undefined) {
		const keyword =
			countKeyword ?? resolvePluralKeyword(countValue, translations);
		const specific = this.getAttribute(`fallback-${keyword}`);
		if (isString(specific) && specific.length > 0) {
			return specific;
		}
	}

	const generic = this.getAttribute("fallback");
	if (isString(generic)) {
		return generic;
	}

	return this.innerHTML || "";
}

/**
 * @private
 * @param {number|undefined} countValue
 * @param {object|null} translations
 * @return {string}
 */
function resolvePluralKeyword(countValue, translations) {
	if (countValue === undefined) {
		return "other";
	}

	let locale = undefined;
	try {
		locale = translations?.locale?.toString();
	} catch (e) {}

	try {
		const pr = new Intl.PluralRules(locale);
		return pr.select(countValue);
	} catch (e) {
		return "other";
	}
}

/**
 * @private
 * @param {string} html
 * @param {number} countValue
 * @param {object|null} translations
 * @return {string}
 */
function replaceCountPlaceholder(html, countValue, translations) {
	let locale = undefined;
	try {
		locale = translations?.locale?.toString();
	} catch (e) {}

	let formatted = `${countValue}`;
	try {
		formatted = new Intl.NumberFormat(locale).format(countValue);
	} catch (e) {}

	return html.replaceAll("{count}", formatted);
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {string} html
 * @return {void}
 */
function replaceElementWithHtml(element, html) {
	const document = element.ownerDocument || getDocument();
	const template = document.createElement("template");
	template.innerHTML = html;

	const parent = element.parentNode;
	if (!parent) {
		return;
	}

	const fragment = template.content;
	parent.insertBefore(fragment, element);
	parent.removeChild(element);
}
