/**
 * 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 {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../../dom/customelement.mjs";
import { CustomControl } from "../../../dom/customcontrol.mjs";
import { fireCustomEvent, fireEvent } from "../../../dom/events.mjs";
import { getLocaleOfDocument } from "../../../dom/locale.mjs";
import { isArray, isObject, isString } from "../../../types/is.mjs";
import { Observer } from "../../../types/observer.mjs";
import { FilterControlsDefaultsStyleSheet } from "../stylesheet/filter-controls-defaults.mjs";
import { FilterStyleSheet } from "../stylesheet/filter.mjs";
import "../../form/input-group.mjs";
import "./select.mjs";

export { SelectOperator };

const operatorElementSymbol = Symbol("operatorElement");
const selectElementSymbol = Symbol("selectElement");
const lastSnapshotSymbol = Symbol("lastSnapshot");

/**
 * The FilterSelectOperator component combines a select-operator with a multi select.
 *
 * @fragments /fragments/components/datatable/filter/select-operator
 *
 * @example /examples/components/datatable/filter-controls Filter controls
 *
 * @copyright Volker Schukai
 * @summary The FilterSelectOperator component combines operator and select input.
 */
class SelectOperator extends CustomControl {
	constructor() {
		super();
		initOptionObserver.call(this);
	}
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/filter/select-operator@@instance",
		);
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Text labels
	 * @property {Array<Object>} operators Operator definitions `{ value, label }`
	 * @property {Object} select Options forwarded to the nested `monster-filter-select`
	 */
	get defaults() {
		const labels = getTranslations();
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels,
			operators: getDefaultOperators(labels),
			select: {},
		});
	}

	/**
	 * @return {string}
	 */
	static getTag() {
		return "monster-filter-select-operator";
	}

	/**
	 * @return {SelectOperator}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		updateOperators.call(this);
		applySelectOptions.call(this);
		initEventHandlers.call(this);
		syncFormValue.call(this);
		return this;
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [FilterControlsDefaultsStyleSheet, FilterStyleSheet];
	}

	/**
	 * Ensure option updates are forwarded to the nested select.
	 *
	 * @param {string|object} options
	 * @return {SelectOperator}
	 */
	setOptions(options) {
		super.setOptions(options);
		if (this[operatorElementSymbol]) {
			updateOperators.call(this);
		}
		applySelectOptions.call(this);
		return this;
	}

	/**
	 * @return {string}
	 */
	get operator() {
		return this[operatorElementSymbol]?.value || "";
	}

	/**
	 * @param {string} value
	 */
	set operator(value) {
		if (isString(value) && this[operatorElementSymbol]) {
			this[operatorElementSymbol].value = value;
			syncFormValue.call(this);
		}
	}

	/**
	 * @return {string[]}
	 */
	get selection() {
		return normalizeSelection(this[selectElementSymbol]?.value);
	}

	/**
	 * @param {string[]|string} value
	 */
	set selection(value) {
		if (this[selectElementSymbol]) {
			this[selectElementSymbol].value = value;
			syncFormValue.call(this);
		}
	}

	/**
	 * @return {string}
	 */
	get value() {
		return buildValue.call(this);
	}

	/**
	 * @param {string|Array|Object} value
	 */
	set value(value) {
		applyValue.call(this, value);
		syncFormValue.call(this);
	}
}

function initOptionObserver() {
	this[lastSnapshotSymbol] = "";
	this.attachObserver(
		new Observer(() => {
			const snapshot = JSON.stringify({
				operators: this.getOption("operators"),
				labels: this.getOption("labels"),
				select: this.getOption("select"),
			});
			if (snapshot !== this[lastSnapshotSymbol]) {
				this[lastSnapshotSymbol] = snapshot;
				if (this[operatorElementSymbol]) {
					updateOperators.call(this);
				}
				applySelectOptions.call(this);
			}
		}),
	);
}

function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[operatorElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=operator]",
	);
	this[selectElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=select]",
	);
}

function updateOperators() {
	const operators = this.getOption("operators");
	const list =
		isArray(operators) && operators.length
			? operators
			: getDefaultOperators(this.getOption("labels"));
	const currentValue = this[operatorElementSymbol].value;

	this[operatorElementSymbol].innerHTML = "";
	for (const operator of list) {
		const option = document.createElement("option");
		option.value = operator.value;
		option.textContent = operator.label;
		this[operatorElementSymbol].append(option);
	}

	if (currentValue) {
		this[operatorElementSymbol].value = currentValue;
	}
}

function applySelectOptions() {
	const selectOptions = this.getOption("select");
	if (!selectOptions || !this[selectElementSymbol]) {
		return;
	}
	this[selectElementSymbol].setOptions(selectOptions);
}

function initEventHandlers() {
	this[operatorElementSymbol].addEventListener("change", () => {
		syncFormValue.call(this, true);
	});

	this[selectElementSymbol].addEventListener("monster-selected", () => {
		syncFormValue.call(this, true);
	});

	this[selectElementSymbol].addEventListener("change", () => {
		syncFormValue.call(this, true);
	});
}

function syncFormValue(fire) {
	const value = this.value;
	try {
		this.setFormValue(value);
	} catch (error) {
		// ignore form association errors
	}
	if (fire) {
		fireCustomEvent(this, "monster-change", { value });
		fireEvent(this, "change");
	}
}

function buildValue() {
	const operator = this.operator || "";
	const selection = normalizeSelection(this[selectElementSymbol]?.value);
	if (!selection.length || !operator) {
		return "";
	}
	return `${operator}:${selection.join(",")}`;
}

function applyValue(value) {
	if (!this[operatorElementSymbol] || !this[selectElementSymbol]) {
		return;
	}

	if (isObject(value)) {
		const operator = value.operator || value.op;
		const selection = value.selection || value.values || value.value;
		if (isString(operator)) {
			this[operatorElementSymbol].value = operator;
		}
		if (selection !== undefined) {
			this[selectElementSymbol].value = selection;
		}
		return;
	}

	if (isArray(value)) {
		this[selectElementSymbol].value = value;
		return;
	}

	if (isString(value)) {
		const parsed = parseValue(value);
		if (parsed.operator) {
			this[operatorElementSymbol].value = parsed.operator;
		}
		this[selectElementSymbol].value = parsed.selection;
	}
}

function parseValue(value) {
	const raw = `${value ?? ""}`.trim();
	if (!raw) {
		return { operator: "", selection: [] };
	}
	const index = raw.indexOf(":");
	if (index === -1) {
		return { operator: "", selection: raw.split(",") };
	}
	const operator = raw.slice(0, index).trim();
	const selection = raw
		.slice(index + 1)
		.split(",")
		.map((item) => item.trim());
	return { operator, selection: selection.filter(Boolean) };
}

function normalizeSelection(value) {
	if (value === null || value === undefined || value === "") {
		return [];
	}
	if (isArray(value)) {
		return value.map((item) => String(item));
	}
	return [String(value)];
}

function getDefaultOperators(labels) {
	return [
		{ value: "one-of", label: labels.oneOf },
		{ value: "all-of", label: labels.allOf },
		{ value: "none-of", label: labels.noneOf },
		{ value: "any-of", label: labels.anyOf },
	];
}

function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				oneOf: "eins von",
				allOf: "alle",
				noneOf: "keins von",
				anyOf: "oder",
			};
		case "es":
			return {
				oneOf: "uno de",
				allOf: "todos",
				noneOf: "ninguno de",
				anyOf: "o",
			};
		case "zh":
			return {
				oneOf: "其中之一",
				allOf: "全部",
				noneOf: "都不",
				anyOf: "或",
			};
		case "hi":
			return {
				oneOf: "इनमें से एक",
				allOf: "सभी",
				noneOf: "कोई नहीं",
				anyOf: "या",
			};
		case "bn":
			return {
				oneOf: "একটি",
				allOf: "সব",
				noneOf: "কোনোটিই নয়",
				anyOf: "বা",
			};
		case "pt":
			return {
				oneOf: "um de",
				allOf: "todos",
				noneOf: "nenhum de",
				anyOf: "ou",
			};
		case "ru":
			return {
				oneOf: "один из",
				allOf: "все",
				noneOf: "ни один из",
				anyOf: "или",
			};
		case "ja":
			return {
				oneOf: "いずれか",
				allOf: "すべて",
				noneOf: "どれでもない",
				anyOf: "または",
			};
		case "pa":
			return {
				oneOf: "ਇਨ੍ਹਾਂ ਵਿੱਚੋਂ ਇੱਕ",
				allOf: "ਸਾਰੇ",
				noneOf: "ਕੋਈ ਨਹੀਂ",
				anyOf: "ਜਾਂ",
			};
		case "mr":
			return {
				oneOf: "यापैकी एक",
				allOf: "सर्व",
				noneOf: "कोणतेही नाही",
				anyOf: "किंवा",
			};
		case "fr":
			return {
				oneOf: "un de",
				allOf: "tous",
				noneOf: "aucun de",
				anyOf: "ou",
			};
		case "it":
			return {
				oneOf: "uno di",
				allOf: "tutti",
				noneOf: "nessuno di",
				anyOf: "o",
			};
		case "nl":
			return {
				oneOf: "een van",
				allOf: "alle",
				noneOf: "geen van",
				anyOf: "of",
			};
		case "sv":
			return {
				oneOf: "en av",
				allOf: "alla",
				noneOf: "ingen av",
				anyOf: "eller",
			};
		case "pl":
			return {
				oneOf: "jeden z",
				allOf: "wszystkie",
				noneOf: "żaden z",
				anyOf: "lub",
			};
		case "da":
			return {
				oneOf: "en af",
				allOf: "alle",
				noneOf: "ingen af",
				anyOf: "eller",
			};
		case "fi":
			return {
				oneOf: "yksi",
				allOf: "kaikki",
				noneOf: "ei mikään",
				anyOf: "tai",
			};
		case "no":
			return {
				oneOf: "en av",
				allOf: "alle",
				noneOf: "ingen av",
				anyOf: "eller",
			};
		case "cs":
			return {
				oneOf: "jeden z",
				allOf: "všechny",
				noneOf: "žádný z",
				anyOf: "nebo",
			};
		default:
		case "en":
			return {
				oneOf: "one of",
				allOf: "all of",
				noneOf: "none of",
				anyOf: "any of",
			};
	}
}

function getTemplate() {
	// language=HTML
	return `
        <style>
            monster-filter-select::part(control) {
                border: none;
                box-shadow: none;
            }
            monster-input-group {
                width: 100%;
                height: 100%;
                align-self: stretch;
            }
            monster-input-group::part(control) {
                align-items: stretch;
            }
            monster-input-group::part(prefix) {
                align-self: stretch;
            }
            [data-monster-role="operator"] {
                height: -webkit-fill-available;
                height: -moz-available;
                height: fill-available;
            }
        </style>
        <div data-monster-role="control" part="control">
            <slot></slot>
            <monster-input-group>
                <select slot="prefix" data-monster-role="operator" name="operator"></select>
                <monster-filter-select
                    data-monster-role="select"
                    style="flex: 1 1 auto; min-width: 0; width: 1%; height: -webkit-fill-available; height: -moz-available; height: fill-available; align-self: stretch;"
                ></monster-filter-select>
            </monster-input-group>
        </div>
    `;
}

registerCustomElement(SelectOperator);
