/**
 * 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 {
	findTargetElementFromEvent,
	fireCustomEvent,
} from "../../dom/events.mjs";
import {
	findElementWithIdUpwards,
	findElementWithSelectorUpwards,
} from "../../dom/util.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { ID } from "../../types/id.mjs";
import { Settings } from "./filter/settings.mjs";
import { FilterStyleSheet } from "./stylesheet/filter.mjs";
import { getDocument, getWindow } from "../../dom/util.mjs";
import { getGlobal } from "../../types/global.mjs";
import {
	isInstance,
	isFunction,
	isObject,
	isArray,
	isString,
} from "../../types/is.mjs";
import { Host } from "../host/host.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import "../form/message-state-button.mjs";
import { Formatter } from "../../text/formatter.mjs";
import { generateRangeComparisonExpression } from "../../text/util.mjs";

import {
	parseBracketedKeyValueHash,
	createBracketedKeyValueHash,
} from "../../text/bracketed-key-value-hash.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { SpaceStyleSheet } from "../stylesheet/space.mjs";
import { FormStyleSheet } from "../stylesheet/form.mjs";

import {
	getStoredFilterConfigKey,
	getFilterConfigKey,
	parseDateInput,
} from "./filter/util.mjs";

import "./filter/select.mjs";

import "../form/button.mjs";
import "../form/select.mjs";
import "../form/popper-button.mjs";
import "../form/toggle-switch.mjs";
import "../form/context-help.mjs";
import "../form/context-error.mjs";
import "../form/message-state-button.mjs";

import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { normalizeNumber } from "../../i18n/util.mjs";

export { Filter };

/**
 * @private
 * @type {symbol}
 */
const filterSelectElementSymbol = Symbol("filterSelectElement");

/**
 * @private
 * @type {symbol}
 */
const searchButtonElementSymbol = Symbol("searchButtonElement");

/**
 * @private
 * @type {symbol}
 */
const resetButtonElementSymbol = Symbol("resetButtonElement");

/**
 * @private
 * @type {symbol}
 */
const saveButtonElementSymbol = Symbol("saveButtonElement");

/**
 * @private
 * @type {symbol}
 */
const filterControlElementSymbol = Symbol("filterControlElement");

/**
 * @private
 * @type {symbol}
 */
const filterSaveActionButtonElementSymbol = Symbol(
	"filterSaveActionButtonElement",
);

/**
 * @private
 * @type {symbol}
 */
const filterTabElementSymbol = Symbol("filterTabElement");

/**
 * @private
 * @type {symbol}
 */
const locationChangeHandlerSymbol = Symbol("locationChangeHandler");

/**
 * @private
 * @type {symbol}
 */
const settingsSymbol = Symbol("settings");

/**
 * @private
 * @type {symbol}
 */
const resizeObserverSymbol = Symbol("resizeObserver");

/**
 * @private
 * @type {symbol}
 */
const sizeDataSymbol = Symbol("sizeData");

/**
 * @private
 * @type {symbol}
 */
const debounceSizeSymbol = Symbol("debounceSize");

/**
 * @private
 * @type {symbol}
 */
const hashChangeSymbol = Symbol("hashChange");

/**
 * The Filter component is used to show and handle the filter values.
 *
 * @fragments /fragments/components/datatable/filter
 *
 * @example /examples/components/datatable/filter-simple First filter
 * @example /examples/components/datatable/filter-advanced Advanced filter
 * @example /examples/components/datatable/filter-store Store filter
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/272.html
 *
 * @copyright Volker Schukai
 * @summary The Filter component is used to show and handle the filter values.
 */
class Filter extends CustomElement {
	/**
	 *
	 */
	constructor() {
		super();
		this[settingsSymbol] = new Settings();

		// debounce the hash change event if doSearch is called by click the search button
		this[hashChangeSymbol] = 0;
	}

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

	/**
	 *
	 * @param {string} message
	 * @return {Filter}
	 */
	showFailureMessage(message) {
		this[searchButtonElementSymbol].setState(
			"failed",
			this.getOption("timeouts.message", 4000),
		);
		this[searchButtonElementSymbol]
			.setMessage(message.toString())
			.showMessage(this.getOption("timeouts.message", 4000));
		return this;
	}

	/**
	 *
	 * @return {Filter}
	 */
	resetFailureMessage() {
		this[searchButtonElementSymbol].hideMessage();
		this[searchButtonElementSymbol].removeState();
		return this;
	}

	/**
	 *
	 * @return {Filter}
	 */
	showSuccess() {
		this[searchButtonElementSymbol].setState(
			"successful",
			this.getOption("timeouts.message", 4000),
		);
		return this;
	}

	/**
	 * 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}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Label definitions
	 * @property {string} labels.search Search button label
	 * @property {string} labels.reset Reset button label
	 * @property {string} labels.save Save button label
	 * @property {string} labels.filter-name Filter name label
	 * @property {string} labels.empty-query-and-no-default Empty query and no default query label
	 * @property {string} labels.query-not-changed Query not changed label
	 * @property {Object} formatter Formatter definitions
	 * @property {Object} formatter.marker Marker definitions
	 * @property {Object} formatter.marker.open Marker open
	 * @property {Object} formatter.marker.close Marker close
	 * @property {Object} features Feature definitions
	 * @property {boolean} features.storedConfig Stored configuration, this replaces the setting `storedConfig.enabled` @since 3.97.0
	 * @property {boolean} features.autoFilter Auto filter @since 3.100.0
	 * @property {boolean} features.preventSameQuery Prevent same query @since 3.103.0
	 * @property {Object} storedConfig Stored configuration
	 * @property {boolean} storedConfig.enabled The store has been enabled, this option will no longer have any effect. @deprecated 20250101
	 * @property {string} storedConfig.selector Selector
	 * @property {Object} timeouts Timeout definitions
	 * @property {number} timeouts.message Message timeout
	 * @property {Object} queries Query definitions
	 * @property {Function} queries.wrap Wrap callback
	 * @property {Function} queries.join Join callback
	 * @property {string} query Query
	 * @property {string} defaultQuery Default query
	 * @property {boolean} eventProcessing Event processing
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			formatter: {
				marker: {
					open: null,
					close: null,
				},
			},

			labels: getTranslations(),

			templateMapping: {
				"filter-save-label": null,
				"filter-name-label": name,
			},

			features: {
				storedConfig: false,
				autoFilter: true,
				preventSameQuery: false,
			},

			storedConfig: {
				enabled: true,
				selector: "",
			},

			config: {
				instanceKey: null,
			},

			timeouts: {
				message: 4000,
			},

			queries: {
				wrap: (value, definition) => {
					return value;
				},
				join: (queries) => {
					if (queries.length === 0) {
						return "";
					}
					return queries.join(" AND ");
				},
			},

			query: null,
			defaultQuery: null,
			eventProcessing: true,

			templateFormatter: {
				marker: {
					open: null,
					close: null,
				},
				i18n: true,
			},
		});
	}

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

	/**
	 * @return {FilterButton}
	 * @fires monster-filter-initialized
	 */
	[assembleMethodSymbol]() {
		const self = this;

		this.setOption(
			"templateMapping.filter-save-label",
			this.getOption("labels.save"),
		);

		this.setOption(
			"templateMapping.filter-name-label",
			this.getOption("labels.filter-name"),
		);

		super[assembleMethodSymbol]();

		initControlReferences.call(self);
		getWindow().requestAnimationFrame(() => {
			initEventHandler.call(self);
		});

		initFromConfig
			.call(self)
			.then(() => {})
			.catch((error) => {
				addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, error?.message);
			})
			.finally(() => {
				initFilter.call(self);
				updateFilterTabs.call(self);

				if (self.getOption("features.autoFilter") === true) {
					doSearch
						.call(self, { showEffect: false })
						.then(() => {
							fireCustomEvent(self, "monster-filter-initialized");
						})
						.catch(() => {});
				}
			});
	}

	/**
	 *
	 */
	connectedCallback() {
		super.connectedCallback();
		getWindow().addEventListener(
			"hashchange",
			this[locationChangeHandlerSymbol],
		);
	}

	/**
	 *
	 */
	disconnectedCallback() {
		super.disconnectedCallback();

		getWindow().removeEventListener(
			"hashchange",
			this[locationChangeHandlerSymbol],
		);
	}

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

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				search: "Suchen",
				reset: "Zurücksetzen",
				save: "Speichern",
				"filter-name": "Filtername",
				"empty-query-and-no-default":
					"Die Abfrage ist leer und es gibt keine Standardabfrage.",
				"query-not-changed":
					"Die Suchanfrage hat sich nicht verändert, daher ist keine Suche erforderlich.",
			};
		case "fr":
			return {
				search: "Chercher",
				reset: "Réinitialiser",
				save: "Sauvegarder",
				"filter-name": "Nom du filtre",
				"empty-query-and-no-default":
					"La requête est vide et il n'y a pas de requête par défaut.",
				"query-not-changed":
					"La requête de recherche n'a pas changé, donc aucune recherche n'est nécessaire.",
			};
		case "es":
			return {
				search: "Buscar",
				reset: "Restablecer",
				save: "Guardar",
				"filter-name": "Nombre del filtro",
				"empty-query-and-no-default":
					"La consulta está vacía y no hay una consulta predeterminada.",
				"query-not-changed":
					"La solicitud de búsqueda no ha cambiado, por lo que no se requiere búsqueda.",
			};
		case "it":
			return {
				search: "Cerca",
				reset: "Reimposta",
				save: "Salva",
				"filter-name": "Nome del filtro",
				"empty-query-and-no-default":
					"La query è vuota e non c'è una query predefinita.",
				"query-not-changed":
					"La richiesta di ricerca non è cambiata, quindi non è necessaria una ricerca.",
			};
		case "pl":
			return {
				search: "Szukaj",
				reset: "Resetuj",
				save: "Zapisz",
				"filter-name": "Nazwa filtra",
				"empty-query-and-no-default":
					"Zapytanie jest puste i nie ma domyślnego zapytania.",
				"query-not-changed":
					"Żądanie wyszukiwania nie zmieniło się, więc wyszukiwanie nie jest wymagane.",
			};
		case "no":
			return {
				search: "Søk",
				reset: "Tilbakestill",
				save: "Lagre",
				"filter-name": "Filternavn",
				"empty-query-and-no-default":
					"Spørringen er tom og det er ingen standardspørring.",
				"query-not-changed":
					"Søkeforespørselen har ikke endret seg, så ingen søk er nødvendig.",
			};
		case "da":
			return {
				search: "Søg",
				reset: "Nulstil",
				save: "Gem",
				"filter-name": "Filternavn",
				"empty-query-and-no-default":
					"Forespørgslen er tom og der er ingen standardforespørgsel.",
				"query-not-changed":
					"Søgeanmodningen er ikke ændret, så ingen søgning er nødvendig.",
			};
		case "sv":
			return {
				search: "Sök",
				reset: "Återställ",
				save: "Spara",
				"filter-name": "Filternamn",
				"empty-query-and-no-default":
					"Förfrågan är tom och det finns ingen standardförfrågan.",
				"query-not-changed":
					"Sökförfrågan har inte ändrats, så ingen sökning krävs.",
			};
		case "nl":
			return {
				search: "Zoeken",
				reset: "Resetten",
				save: "Opslaan",
				"filter-name": "Filternaam",
				"empty-query-and-no-default":
					"De zoekopdracht is leeg en er is geen standaardzoekopdracht.",
				"query-not-changed":
					"De zoekopdracht is niet gewijzigd, dus zoeken is niet nodig.",
			};
		case "fi":
			return {
				search: "Haku",
				reset: "Palauta",
				save: "Tallenna",
				"filter-name": "Suodattimen nimi",
				"empty-query-and-no-default":
					"Hakukysely on tyhjä eikä oletushakua ole määritetty.",
				"query-not-changed":
					"Hakupyyntö ei ole muuttunut, joten hakua ei tarvita.",
			};
		case "cs":
			return {
				search: "Hledat",
				reset: "Resetovat",
				save: "Uložit",
				"filter-name": "Název filtru",
				"empty-query-and-no-default":
					"Dotaz je prázdný a není nastavena žádná výchozí hodnota.",
				"query-not-changed":
					"Dotaz na hledání se nezměnil, takže hledání není nutné.",
			};
		case "pt":
			return {
				search: "Buscar",
				reset: "Redefinir",
				save: "Salvar",
				"filter-name": "Nome do filtro",
				"empty-query-and-no-default":
					"A consulta está vazia e não há uma consulta padrão.",
				"query-not-changed":
					"A solicitação de pesquisa não foi alterada, portanto, nenhuma pesquisa é necessária.",
			};
		case "ru":
			return {
				search: "Поиск",
				reset: "Сброс",
				save: "Сохранить",
				"filter-name": "Имя фильтра",
				"empty-query-and-no-default": "Запрос пуст и нет запроса по умолчанию.",
				"query-not-changed":
					"Поисковый запрос не изменился, поэтому поиск не требуется.",
			};
		case "zh":
			return {
				search: "搜索",
				reset: "重置",
				save: "保存",
				"filter-name": "过滤器名称",
				"empty-query-and-no-default": "查询为空，且没有默认查询。",
				"query-not-changed": "搜索请求没有更改，因此不需要进行搜索。",
			};
		case "hi":
			return {
				search: "खोजें",
				reset: "रीसेट करें",
				save: "सहेजें",
				"filter-name": "फ़िल्टर नाम",
				"empty-query-and-no-default": "क्वेरी खाली है और कोई डिफ़ॉल्ट क्वेरी नहीं है।",
				"query-not-changed":
					"खोज अनुरोध में कोई बदलाव नहीं हुआ है, इसलिए खोज आवश्यक नहीं है।",
			};
		case "bn":
			return {
				search: "অনুসন্ধান",
				reset: "রিসেট",
				save: "সংরক্ষণ করুন",
				"filter-name": "ফিল্টারের নাম",
				"empty-query-and-no-default": "কোয়েরি খালি এবং কোনো ডিফল্ট কোয়েরি নেই।",
				"query-not-changed":
					"অনুসন্ধানের অনুরোধ পরিবর্তন হয়নি, তাই অনুসন্ধান প্রয়োজন নয়।",
			};
		case "ja":
			return {
				search: "検索",
				reset: "リセット",
				save: "保存",
				"filter-name": "フィルター名",
				"empty-query-and-no-default":
					"クエリが空で、デフォルトクエリがありません。",
				"query-not-changed": "検索リクエストに変更がないため、検索は不要です。",
			};
		case "pa":
			return {
				search: "ਖੋਜੋ",
				reset: "ਰੀਸੈੱਟ ਕਰੋ",
				save: "ਸੇਵ ਕਰੋ",
				"filter-name": "ਫਿਲਟਰ ਦਾ ਨਾਂ",
				"empty-query-and-no-default": "ਕੁਐਰੀ ਖਾਲੀ ਹੈ ਅਤੇ ਕੋਈ ਡਿਫੌਲਟ ਕੁਐਰੀ ਨਹੀਂ ਹੈ।",
				"query-not-changed":
					"ਖੋਜ ਦੀ ਬੇਨਤੀ ਵਿੱਚ ਕੋਈ ਤਬਦੀਲੀ ਨਹੀਂ ਆਈ ਹੈ, ਇਸ ਲਈ ਖੋਜ ਦੀ ਲੋੜ ਨਹੀਂ ਹੈ।",
			};
		default:
		case "en":
			return {
				search: "Search",
				reset: "Reset",
				save: "Save",
				"filter-name": "Filter name",
				"empty-query-and-no-default":
					"The query is empty and there is no default query.",
				"query-not-changed":
					"The search request has not changed, so no search is required.",
			};
	}
}

/**
 * @private
 * @return {FilterButton}
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[filterControlElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=control]",
	);
	this[filterSelectElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=filter-select]",
	);
	this[searchButtonElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=search-button]",
	);
	this[resetButtonElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=reset-button]",
	);

	this[saveButtonElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=save-button]",
	);

	this[filterSaveActionButtonElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=save-action-button]",
	);

	this[filterTabElementSymbol] = findElementWithSelectorUpwards(
		this,
		this.getOption("storedConfig.selector", ""),
	);

	return this;
}

/**
 * @private
 */
function updateFilterSelections() {
	queueMicrotask(() => {
		const options = this[settingsSymbol].getOptions();
		this[filterSelectElementSymbol].setOption("options", options);

		setTimeout(() => {
			this[filterSelectElementSymbol].value =
				this[settingsSymbol].getSelected();
		}, 10);
	});
}

/**
 * @private
 * @throws {Error} no filter label is defined
 */
function initFilter() {
	const storedSetting = this[settingsSymbol];
	this[settingsSymbol] = new Settings();

	const result = parseBracketedKeyValueHash(getGlobal().location.hash);
	let valuesFromHash = {};
	if (isObject(result) && result?.[this.id]) {
		valuesFromHash = result[this.id];
	}

	getSlottedElements
		.call(this, "label[data-monster-label]")
		.forEach((element) => {
			const label = element.getAttribute("data-monster-label");
			if (!label) {
				addAttributeToken(
					this,
					ATTRIBUTE_ERRORMESSAGE,
					"no filter label is defined",
				);
				return;
			}

			let value = element.id;
			if (!value) {
				const prefix = label.replace(/\W/g, "-");
				prefix.charAt(0).match(/[\d_]/g)?.length ? `f${prefix}` : prefix;

				value = new ID(prefix + "-").toString();
				element.id = value;
			}

			let setting = storedSetting.get(value);

			if (setting) {
				this[settingsSymbol].set(setting);
			}

			if (valuesFromHash?.[element.id]) {
				const v = valuesFromHash[element.id];
				const searchInput = element.firstElementChild;
				try {
					searchInput.value = normalizeFilterValue(v);
				} catch (error) {}
			}

			setting = this[settingsSymbol].get(value);
			let visible = false;
			if (setting) {
				setSlotAttribute(element, setting.visible);
				visible = setting.visible;
			} else {
				visible = getVisibilityFromSlotAttribute(element);
			}

			this[settingsSymbol].set({ value, label, visible });
		});

	updateFilterSelections.call(this);
}

/**
 * @private
 * @param {*} value
 * @return {*}
 */
function normalizeFilterValue(value) {
	if (!isString(value)) {
		return value;
	}

	const trimmed = value.trim();
	if (trimmed.length < 2) {
		return value;
	}

	const first = trimmed[0];
	const last = trimmed[trimmed.length - 1];
	if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
		return trimmed.slice(1, -1);
	}

	return value;
}

/**
 * @private
 * @param {string} input
 * @return {*}
 */
function escapeAttributeValue(input) {
	if (input === undefined || input === null) {
		return input;
	}

	return input
		.replace(/&/g, "&amp;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#x27;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;");
}

/**
 *
 * @param {HTMLElement} element
 * @return {boolean}
 */
function getVisibilityFromSlotAttribute(element) {
	return !(
		element.hasAttribute("slot") && element.getAttribute("slot") === "hidden"
	);
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {boolean} visible
 */
function setSlotAttribute(element, visible) {
	if (visible) {
		element.removeAttribute("slot");
		return;
	}

	element.setAttribute("slot", "hidden");
}

/**
 * @private
 */
function initEventHandler() {
	const self = this;

	let lastHash = getGlobal().location.hash;
	self[locationChangeHandlerSymbol] = () => {
		if (lastHash === getGlobal().location.hash) {
			return;
		}

		/**
		 * debounce the hash change event if doSearch
		 * is called by click the search button
		 */
		if (self[hashChangeSymbol] > 0) {
			self[hashChangeSymbol]--;
			return;
		}

		initFilter.call(this);

		doSearch
			.call(self)
			.then(() => {})
			.catch((error) => {})
			.finally(() => {
				lastHash = getGlobal().location.hash;
			});
	};

	/**
	 * Monster.Components.Form.event:monster-selection-cleared
	 */
	if (self[filterSelectElementSymbol]) {
		self[filterSelectElementSymbol].addEventListener(
			"monster-selection-cleared",
			function () {
				const settings = self[settingsSymbol].getOptions();

				for (const setting of settings) {
					const filterElement = findElementWithIdUpwards(self, setting.value);
					if (filterElement) {
						setSlotAttribute(filterElement, false);

						self[settingsSymbol].set({ value: setting.value, visible: false });
					}
				}

				updateConfig.call(self);
			},
		);

		self[filterSelectElementSymbol].addEventListener(
			"monster-changed",
			function (event) {
				const filterElement = findElementWithIdUpwards(
					self,
					event.detail.value,
				);
				if (filterElement) {
					setSlotAttribute(filterElement, event.detail.checked);
				}

				self[settingsSymbol].set({
					value: event.detail.value,
					visible: event.detail.checked,
				});

				updateConfig.call(self);
			},
		);
	}

	/** save the current filter */
	if (self[filterSaveActionButtonElementSymbol]) {
		self[filterSaveActionButtonElementSymbol].setOption(
			"actions.click",
			function (event) {
				const button = findTargetElementFromEvent(
					event,
					"data-monster-role",
					"save-action-button",
				);
				const form = button.closest("[data-monster-role=form]");

				if (!form) {
					button.setState("failed", self.getOption("timeouts.message", 4000));
					return;
				}

				const input = form.querySelector("input[name=filter-name]");
				if (!input) {
					button.setState("failed", self.getOption("timeouts.message", 4000));
					return;
				}

				const name = input.value;
				if (!name) {
					button.setState("failed", self.getOption("timeouts.message", 4000));
					button.setMessage("Please enter a name").showMessage();
					return;
				}

				doSearch
					.call(self, { showEffect: false })
					.then(() => {
						const configKey = getStoredFilterConfigKey.call(self);
						const host = getDocument().querySelector("monster-host");
						if (!(host && hasConfigIdentity.call(self))) {
							return;
						}

						const query = self.getOption("query");
						if (!query) {
							button.setState(
								"failed",
								self.getOption(
									"timeouts.message",
									self.getOption("timeouts.message", 4000),
								),
							);
							button
								.setMessage("No query found")
								.showMessage(self.getOption("timeouts.message", 4000));
							return;
						}

						host
							.hasConfig(configKey)
							.then((hasConfig) => {
								return new Promise((resolve, reject) => {
									if (hasConfig) {
										host.getConfig(configKey).then(resolve).catch(reject);
										return;
									}
									return resolve({});
								});
							})
							.then((config) => {
								config[name] = query;
								return host.setConfig(configKey, {
									...config,
								});
							})
							.then(() => {
								button.setState(
									"successful",
									self.getOption("timeouts.message", 4000),
								);
								updateFilterTabs.call(self);
							})
							.catch((error) => {
								button.setState(
									"failed",
									self.getOption("timeouts.message", 4000),
								);
								button
									.setMessage(error.message)
									.showMessage(self.getOption("timeouts.message", 4000));
							});
					})
					.catch((error) => {
						button.setState("failed", self.getOption("timeouts.message", 4000));
						const msg = error.message || error;
						button
							.setMessage(msg)
							.showMessage(self.getOption("timeouts.message", 4000));
					});
			},
		);
	}

	self[searchButtonElementSymbol].setOption("actions.click", () => {
		self[hashChangeSymbol] = 1;

		doSearch
			.call(self)
			.then(() => {})
			.catch((error) => {});
	});

	// the reset button should reset the filter and the search query
	// all input elements should be reset to their default values
	// which is the empty string. we search for all input elements
	// in the filter and reset them to their default value
	self[resetButtonElementSymbol].setOption("actions.click", () => {
		getSlottedElements
			.call(self, "label[data-monster-label]")
			.forEach((element) => {
				const label = element.getAttribute("data-monster-label");
				if (!label) {
					return;
				}

				const input = element.firstElementChild;

				if (input) {
					input.value = "";
				}
			});

		doSearch
			.call(self, { showEffect: false })
			.then(() => {})
			.catch((e) => addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message));
	});

	self.addEventListener("keyup", (event) => {
		const path = event.composedPath();
		if (path.length === 0) {
			return;
		}

		if (!(path[0] instanceof HTMLInputElement)) {
			return;
		}

		if (event.keyCode === 13) {
			doSearch
				.call(self, { showEffect: false })
				.then(() => {})
				.catch((error) => {});
		}
	});

	// tabs
	const element = this[filterTabElementSymbol];
	if (element) {
		initTabEvents.call(this);
	}
}

function initTabEvents() {
	const self = this;
	this[filterTabElementSymbol].addEventListener(
		"monster-tab-changed",
		(event) => {
			const query = event?.detail?.data?.["data-monster-query"];
			const q = this.getOption("query");
			if (query !== q) {
				this.setOption("query", query);
			}
		},
	);

	this[filterTabElementSymbol].addEventListener(
		"monster-tab-remove",
		(event) => {
			const labels = [];
			const buttons = this[filterTabElementSymbol].getOption("buttons");

			const keys = ["popper", "standard"];
			for (let i = 0; i < keys.length; i++) {
				const key = keys[i];

				for (const button of buttons[key]) {
					if (button.label !== event.detail.label) {
						labels.push(button.label);
					}
				}
			}

			const host = findElementWithSelectorUpwards(this, "monster-host");
			if (!(host && hasConfigIdentity.call(this))) {
				return;
			}

			const configKey = getStoredFilterConfigKey.call(this);
			host
				.hasConfig(configKey)
				.then((hasConfig) => {
					if (!hasConfig) {
						return;
					}

					return host.getConfig(configKey);
				})
				.then((config) => {
					for (const [name, query] of Object.entries(config)) {
						if (labels.includes(name)) {
							continue;
						}

						delete config[name];
					}

					return host.setConfig(configKey, {
						...config,
					});
				});
		},
	);
}

/**
 * @private
 */
function updateFilterTabs() {
	const element = this[filterTabElementSymbol];
	if (!element) {
		return;
	}

	const host = findElementWithSelectorUpwards(this, "monster-host");
	if (!(host && hasConfigIdentity.call(this))) {
		return;
	}

	const configKey = getStoredFilterConfigKey.call(this);
	host
		.hasConfig(configKey)
		.then((hasConfig) => {
			if (!hasConfig) {
				return;
			}

			return host.getConfig(configKey);
		})
		.then((config) => {
			for (const [name, query] of Object.entries(config)) {
				const found = element.querySelector(
					`[data-monster-button-label="${name}"]`,
				);
				if (found) {
					continue;
				}

				if (query === undefined || query === null) {
					continue;
				}

				const escapedQuery = escapeAttributeValue(query);

				element.insertAdjacentHTML(
					"beforeend",
					`<div data-monster-button-label="${name}"
             data-monster-removable="true" 
             data-monster-query="${escapedQuery}" data-monster-role="filter-tab" >
             </div>`,
				);
			}
		})
		.catch((error) => {
			if (error instanceof Error) {
				addAttributeToken(
					this,
					ATTRIBUTE_ERRORMESSAGE,
					error.message + " " + error.stack,
				);
			} else {
				addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error + "");
			}
		});
}

/**
 * @private
 * @param showEffect
 * @return {Promise}
 */
function doSearch({ showEffect } = { showEffect: true }) {
	this.resetFailureMessage();

	return collectSearchQueries
		.call(this)
		.then((query) => {
			const buildQuery = buildSearchQuery.call(this, query);
			if (buildQuery === null) {
				const msg = this.getOption("labels.empty-query-and-no-default");
				if (showEffect) {
					this[searchButtonElementSymbol].removeState();
					this[searchButtonElementSymbol]
						.setMessage(msg)
						.showMessage(this.getOption("timeouts.message", 4000));
				}
				return Promise.reject(new Error(msg));
			}

			if (buildQuery === "" && this.getOption("defaultQuery") === null) {
				const msg = this.getOption("labels.empty-query-and-no-default");

				if (showEffect) {
					this[searchButtonElementSymbol].removeState();
					this[searchButtonElementSymbol]
						.setMessage(msg)
						.showMessage(this.getOption("timeouts.message", 4000));
				}

				return Promise.reject(new Error(msg));
			}

			if (
				this.getOption("features.preventSameQuery") &&
				buildQuery === this.getOption("query")
			) {
				const msg = this.getOption("labels.query-not-changed");

				if (showEffect) {
					this[searchButtonElementSymbol].removeState();
					this[searchButtonElementSymbol]
						.setMessage(msg)
						.showMessage(this.getOption("timeouts.message", 4000));
				}

				return Promise.reject(new Error(msg));
			}

			if (showEffect) {
				this[searchButtonElementSymbol].removeState();
				this[searchButtonElementSymbol].setState(
					"activity",
					this.getOption("timeouts.message", 4000),
				);
			}

			this.setOption("query", buildSearchQuery.call(this, query));

			return Promise.resolve();
		})
		.catch((error) => {
			if (error instanceof Error) {
				addAttributeToken(
					this,
					ATTRIBUTE_ERRORMESSAGE,
					error.message + " " + error.stack,
				);
			} else {
				addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
			}

			if (showEffect) {
				this[searchButtonElementSymbol].setState(
					"failed",
					this.getOption("timeouts.message", 4000),
				);
				this[searchButtonElementSymbol].setMessage(error.message).showMessage();
			}

			return Promise.reject(error);
		});
}

/**
 * @private
 * @param queries
 * @return {*|string}
 */
function buildSearchQuery(queries) {
	if (!isArray(queries) || queries.length === 0) {
		return this.getOption("defaultQuery");
	}

	const joinCallback = this.getOption("queries.join");
	if (isFunction(joinCallback)) {
		return joinCallback(queries);
	}

	const q = queries.join(" ").trim();
	if (q.length === 0) {
		return this.getOption("defaultQuery");
	}

	return q;
}

/**
 * @private
 * @return {Promise<unknown>}
 */
function collectSearchQueries() {
	const currentHash = parseBracketedKeyValueHash(getGlobal().location.hash);
	const self = this;

	return new Promise((resolve, reject) => {
		const query = [];
		const wrapCallback = this.getOption("queries.wrap");

		let hasNoIdError = false;

		getSlottedElements
			.call(this, "label[data-monster-label]")
			.forEach((element) => {
				const label = element.getAttribute("data-monster-label");
				if (!label) {
					throw new Error("no filter label is defined");
				}

				const id = element.id;
				if (!id) {
					hasNoIdError = true;
					return;
				}

				const visible = getVisibilityFromSlotAttribute(element);
				if (!visible) {
					return;
				}

				let template = element.getAttribute("data-monster-template");
				if (!template) {
					template = "${id}=${value}";
				}

				const controlValue = getControlValuesFromLabel(element);
				const hasEmptyArrayValue =
					isArray(controlValue) && controlValue.length === 0;

				if (!controlValue || hasEmptyArrayValue) {
					if (
						(controlValue === "" || hasEmptyArrayValue) &&
						currentHash?.[this.id]?.[id]
					) {
						delete currentHash[this.id][id];
					}

					return;
				}

				if (!isObject(currentHash[this.id])) {
					currentHash[this.id] = {};
				}
				currentHash[this.id][id] = controlValue;

				const formatterValue = isArray(controlValue)
					? controlValue.join(",")
					: controlValue;

				const mapping = {
					id,
					value: formatterValue,
					label,
				};

				const formatter = new Formatter(mapping, {
					callbacks: {
						range: (value, key) => {
							return generateRangeComparisonExpression(value, key, {
								urlEncode: true,
								andOp: "AND",
								orOp: "OR",
								eqOp: "=",
								gtOp: ">",
								ltOp: "<",
							});
						},
						"tag-list": (value, key) => {
							if (isString(value)) {
								value = value.split(",");
							}

							if (!isArray(value)) {
								return "";
							}

							return (
								key +
								" IN " +
								value
									.map((v) => {
										return `"${encodeURIComponent(v)}"`;
									})
									.join(",")
							);
						},
						"list-tag": (value, key) => {
							if (isString(value)) {
								value = value.split(",");
							}

							if (!isArray(value)) {
								return "";
							}

							return (
								value
									.map((v) => {
										return `"${encodeURIComponent(v)}"`;
									})
									.join(",") +
								" IN " +
								encodeURIComponent(key)
							);
						},
						"tags-in-list": (value, key, op) => {
							if (isString(value)) {
								value = value.split(",");
							}

							if (!isArray(value)) {
								return "";
							}

							if (!op || !isString(op)) op = "OR";
							op = " " + op.toUpperCase().trim() + " ";

							let query = "";
							value.forEach((v) => {
								if (query.length > 0) {
									query += op;
								}
								query += `${encodeURIComponent(key)} IN "${encodeURIComponent(v)}"`;
							});

							return query;
						},
						"list-not-in-tags": (value, key, op) => {
							if (isString(value)) {
								value = value.split(",");
							}
							if (!isArray(value)) {
								return "";
							}
							if (!op || !isString(op)) op = "OR";
							op = " " + op.toUpperCase().trim() + " ";
							let query = "";
							value.forEach((v) => {
								if (query.length > 0) {
									query += op;
								}
								query += `"${encodeURIComponent(key)}" NOT IN ${encodeURIComponent(v)}`;
							});
							return query;
						},
						"list-in-tags": (value, key, op) => {
							if (isString(value)) {
								value = value.split(",");
							}

							if (!isArray(value)) {
								return "";
							}

							if (!op || !isString(op)) op = "OR";
							op = " " + op.toUpperCase().trim() + " ";

							let query = "";
							value.forEach((v) => {
								if (query.length > 0) {
									query += op;
								}
								query += `"${encodeURIComponent(v)}" IN ${encodeURIComponent(key)}`;
							});

							return query;
						},
						"array-list": (value, key) => {
							if (isString(value)) {
								value = value.split(",");
							}

							if (!isArray(value)) {
								return "";
							}

							return (
								key +
								"=" +
								value
									.map((v) => {
										return `"${encodeURIComponent(v)}"`;
									})
									.join(",")
							);
						},
						"date-range": (value, key) => {
							const query = parseDateInput(value, key);
							if (!query || query === "false") {
								return "";
							}

							// return query as url encoded
							return encodeURIComponent(query);
						},
						"to-int-2": (value, key) => {
							const query = normalizeNumber(value);
							if (Number.isNaN(query)) {
								return key + " IS NULL";
							}
							return key + "=" + encodeURIComponent(Math.round(query * 100));
						},
						"to-int-3": (value, key) => {
							const query = normalizeNumber(value);
							if (Number.isNaN(query)) {
								return "";
							}
							return key + "=" + encodeURIComponent(Math.round(query * 1000));
						},
						"to-int-4": (value, key) => {
							const query = normalizeNumber(value);
							if (Number.isNaN(query)) {
								return "";
							}
							return key + "=" + encodeURIComponent(Math.round(query * 10000));
						},
					},
				});

				if (self.getOption("formatter.marker.open")) {
					formatter.setMarker(
						self.getOption("formatter.marker.open"),
						self.getOption("formatter.marker.close"),
					);
				}

				let queryPart = formatter.format(template);
				if (queryPart) {
					if (isFunction(wrapCallback)) {
						queryPart = wrapCallback(queryPart, mapping);
					}
					query.push(queryPart);
				}
			});

		if (hasNoIdError) {
			reject(new Error("some or all filter elements have no id"));
			return;
		}

		getGlobal().location.hash = createBracketedKeyValueHash(currentHash);
		resolve(query);
	});
}

/**
 * @private
 * @param label
 * @return {null|Array|undefined|string}
 */
function getControlValuesFromLabel(label) {
	// finde das erste Kind-Element vom type input
	// wenn es ein input-Element ist, dann @todo

	const foundControl = label.firstElementChild;

	if (foundControl) {
		if (foundControl.tagName === "INPUT") {
			if (foundControl.type === "checkbox") {
				const checkedControls = label.querySelectorAll(
					`${foundControl}:checked`,
				);
				const values = [];

				checkedControls.forEach((checkedControl) => {
					values.push(checkedControl.value);
				});

				return values;
			} else if (foundControl.type === "radio") {
				const checkedControl = label.querySelector(`${foundControl}:checked`);

				if (checkedControl) {
					return checkedControl.value;
				} else {
					return null;
				}
			} else {
				return foundControl.value;
			}
		} else {
			return foundControl.value;
		}
	}

	return null;
}

/**
 * @private
 * @return {Promise<unknown>}
 */
function initFromConfig() {
	const host = findElementWithSelectorUpwards(this, "monster-host");

	if (!(isInstance(host, Host) && hasConfigIdentity.call(this))) {
		return Promise.resolve();
	}

	const configKey = getFilterConfigKey.call(this);

	return new Promise((resolve, reject) => {
		host
			.getConfig(configKey)
			.then((config) => {
				if ((config && isObject(config)) || isArray(config)) {
					this[settingsSymbol].setOptions(config);
				}
				resolve();
			})
			.catch((error) => {
				if (error === undefined) {
					resolve();
					return;
				}

				// config not written
				if (error?.message?.match(/is not defined/)) {
					resolve();
					return;
				}

				addAttributeToken(
					this,
					ATTRIBUTE_ERRORMESSAGE,
					error?.message || error,
				);
				reject(error);
			});
	});
}

/**
 * @private
 */
function updateConfig() {
	const host = findElementWithSelectorUpwards(this, "monster-host");
	if (!(host && hasConfigIdentity.call(this))) {
		return;
	}
	const configKey = getFilterConfigKey.call(this);

	try {
		host.setConfig(configKey, this[settingsSymbol].getOptions());
	} catch (error) {
		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error?.message || error);
	}
}

/**
 * @private
 * @return {boolean}
 */
function hasConfigIdentity() {
	return (
		(isString(this.id) && this.id !== "") ||
		(isString(this.getOption("config.instanceKey")) &&
			this.getOption("config.instanceKey").trim() !== "")
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="container">
                <div data-monster-role="layout">
                    <div data-monster-role="filter">
                        <slot></slot>
                        <slot name="hidden"></slot>
                    </div>
                    <div data-monster-role="select-and-search">
                        <monster-button-bar style="max-width: max-content">
                            <monster-message-state-button data-monster-role="search-button" class="stretched-control">i18n{search}</monster-message-state-button>
                            <monster-select class="stretched-control"
                                            data-monster-selected-template="summary"
                                            data-monster-option-type="checkbox"
                                            data-monster-option-filter-mode="options"
                                            data-monster-option-filter-position="popper"
                                            data-monster-role="filter-select"></monster-select>
                            <monster-popper-button data-monster-role="save-button" class="stretched-control"
                                                   data-monster-attributes="data-monster-visible path:features.storedConfig">
                                <div slot="button">\${filter-save-label}</div>
                                <div class="monster-form" data-monster-role="form">

                                    <label for="filter-name">\${filter-name-label}
                                        <input name="filter-name"
                                               type="search"
                                               class="monster-margin-bottom-5"></label>
                                    <monster-message-state-button
                                            data-monster-role="save-action-button"
                                            data-monster-option-labels-button="\${filter-save-label}">
                                    </monster-message-state-button>

                                </div>
                            </monster-popper-button>
                            <monster-button data-monster-role="reset-button" class="stretched-control">i18n{reset}</monster-button>
                            </monster-button>
                        </monster-button-bar>
                    </div>

                </div>
                <input class="hidden" name="query" data-monster-role="query"
                       data-monster-attributes="value path:query">
            </div>
        </div>
    `;
}

registerCustomElement(Filter);
