/**
 * Copyright © schukai GmbH 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 schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol } from "../../constants.mjs";
import { internalSymbol } from "../../constants.mjs";
import { buildMap } from "../../data/buildmap.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { positionPopper } from "./util/floating-ui.mjs";
import {
	addAttributeToken,
	findClosestByAttribute,
	removeAttributeToken,
} from "../../dom/attributes.mjs";
import { ATTRIBUTE_PREFIX, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import {
	findTargetElementFromEvent,
	fireCustomEvent,
	fireEvent,
} from "../../dom/events.mjs";
import { getDocument } from "../../dom/util.mjs";
import { Formatter } from "../../text/formatter.mjs";
import { getGlobal } from "../../types/global.mjs";
import { ID } from "../../types/id.mjs";
import {
	isArray,
	isFunction,
	isInteger,
	isIterable,
	isObject,
	isPrimitive,
	isString,
} from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { ProxyObserver } from "../../types/proxyobserver.mjs";
import { validateArray, validateString } from "../../types/validate.mjs";
import { Processing } from "../../util/processing.mjs";
import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
import { SelectStyleSheet } from "./stylesheet/select.mjs";
import {
	getDocumentTranslations,
	Translations,
} from "../../i18n/translations.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute, removeErrorAttribute } from "../../dom/error.mjs";

export {
	Select,
	popperElementSymbol,
	getSummaryTemplate,
	getSelectionTemplate,
};

/**
 * @private
 * @type {Symbol}
 */
const timerCallbackSymbol = Symbol("timerCallback");

/**
 * @private
 * @type {Symbol}
 */
const keyFilterEventSymbol = Symbol("keyFilterEvent");

/**
 * @private
 * @type {Symbol}
 */
const lazyLoadDoneSymbol = Symbol("lazyLoadDone");

/**
 * @private
 * @type {Symbol}
 */
const isLoadingSymbol = Symbol("isLoading");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const closeEventHandler = Symbol("closeEventHandler");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const clearOptionEventHandler = Symbol("clearOptionEventHandler");

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

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const keyEventHandler = Symbol("keyEventHandler");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const lastFetchedDataSymbol = Symbol("lastFetchedData");
/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const inputEventHandler = Symbol("inputEventHandler");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const changeEventHandler = Symbol("changeEventHandler");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const controlElementSymbol = Symbol("controlElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const selectionElementSymbol = Symbol("selectionElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const containerElementSymbol = Symbol("containerElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const popperElementSymbol = Symbol("popperElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const inlineFilterElementSymbol = Symbol("inlineFilterElement");
/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const popperFilterElementSymbol = Symbol("popperFilterElement");
/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const popperFilterContainerElementSymbol = Symbol(
	"popperFilterContainerElement",
);

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const optionsElementSymbol = Symbol("optionsElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const noOptionsAvailableElementSymbol = Symbol("noOptionsAvailableElement");

/**
 * local symbol
 * @private
 * @type {Symbol}
 */
const statusOrRemoveBadgesElementSymbol = Symbol("statusOrRemoveBadgesElement");

/**
 * @private
 * @type {Symbol}
 */
const areOptionsAvailableAndInitSymbol = Symbol("@@areOptionsAvailableAndInit");

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

/**
 * @private
 * @type {number}
 */
const FOCUS_DIRECTION_UP = 1;
/**
 * @private
 * @type {number}
 */
const FOCUS_DIRECTION_DOWN = 2;

/**
 * @private
 * @type {string}
 */
const FILTER_MODE_REMOTE = "remote";

/**
 * @private
 * @type {string}
 */
const FILTER_MODE_OPTIONS = "options";

/**
 * @private
 * @type {string}
 */
const FILTER_MODE_DISABLED = "disabled";

/**
 * @private
 * @type {string}
 */
const FILTER_POSITION_POPPER = "popper";
/**
 * @private
 * @type {string}
 */
const FILTER_POSITION_INLINE = "inline";

/**
 * A select control that can be used to select one or more options from a list.
 *
 * @issue @issue https://localhost.alvine.dev:8444/development/issues/closed/280.html
 *
 * @fragments /fragments/components/form/select/
 *
 * @example /examples/components/form/select-with-options Select with options
 * @example /examples/components/form/select-with-html-options Select with HTML options
 * @example /examples/components/form/select-multiple Multiple selection
 * @example /examples/components/form/select-filter Filter
 * @example /examples/components/form/select-fetch Fetch options
 * @example /examples/components/form/select-lazy Lazy load
 * @example /examples/components/form/select-remote-filter Remote filter
 *
 * @copyright schukai GmbH
 * @summary A beautiful select control that can make your life easier and also looks good.
 * @fires monster-change
 * @fires monster-changed
 */
class Select 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/form/select@@instance");
	}

	/**
	 * The current selection of the Select
	 *
	 * ```
	 * e = document.querySelector('monster-select');
	 * console.log(e.value)
	 * // ↦ 1
	 * // ↦ ['1','2']
	 * ```
	 *
	 * @return {string}
	 */
	get value() {
		return convertSelectionToValue.call(this, this.getOption("selection"));
	}

	/**
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
	 * @return {boolean}
	 */
	static get formAssociated() {
		return true;
	}

	/**
	 * Set selection
	 *
	 * ```
	 * e = document.querySelector('monster-select');
	 * e.value=1
	 * ```
	 *
	 * @property {string|array} value
	 * @throws {Error} unsupported type
	 * @fires monster-selected this event is fired when the selection is set
	 */
	set value(value) {
		const result = convertValueToSelection.call(this, value);
		setSelection
			.call(this, result.selection)
			.then(() => {})
			.catch((e) => {
				addErrorAttribute(this, e);
			});
	}

	/**
	 * 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} toggleEventType=click,touch List of event types to be observed for opening the dropdown
	 * @property {boolean} delegatesFocus=false lorem [see mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus)
	 * @property {Object[]} options Selection of key identifier pairs available for selection and displayed in the dropdown.
	 * @property {string} options[].label
	 * @property {string} options[].value
	 * @property {string} options[].visibility hidden or visible
	 * @property {Array} selection Selected options
	 * @property {Integer} showMaxOptions=10 Maximum number of visible options before a scroll bar should be displayed.
	 * @property {string} type=radio Multiple (checkbox) or single selection (radio)
	 * @property {string} name=(random id) Name of the form field
	 * @property {string} url Load options from server per url
	 * @property {object} lookup Load options from server per url
	 * @property {string} lookup.url=null Load options from server per url
	 * @property {boolean} lookup.grouping=false Load all selected options from server per url at once (true) or one by one (false)
	 * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
	 * @property {String} fetch.redirect=error
	 * @property {String} fetch.method=GET
	 * @property {String} fetch.mode=same-origin
	 * @property {String} fetch.credentials=same-origin
	 * @property {Object} fetch.headers={"accept":"application/json"}}
	 * @property {Object} labels
	 * @property {string} labels.cannot-be-loaded cannot be loaded
	 * @property {string} labels.no-options-available no options available
	 * @property {string} labels.select-an-option select an option
	 * @property {string} labels.no-option no option in the list, maybe you have to change the filter
	 * @property {Object} features List with features
	 * @property {Boolean} features.clearAll=true Display of a delete button to delete the entire selection
	 * @property {Boolean} features.clear=true Display of a delete key for deleting the specific selection
	 * @property {Boolean} features.lazyLoad=false Load options when first opening the dropdown. (Hint; lazylLoad is not supported with remote filter)
	 * @property {Boolean} features.closeOnSelect=false Close the dropdown when an option is selected (since 3.54.0)
	 * @property {Boolean} features.emptyValueIfNoOptions=false If no options are available, the selection is set to an empty array
	 * @property {Boolean} features.storeFetchedData=false Store fetched data in the object
	 * @property {Boolean} features.useStrictValueComparison=true Use strict value comparison for the selection
	 * @property {string} filter.defaultValue=null Default filter value, if the filter is empty, if the default value is null, then no request is made
	 * @property {Boolean} filter.mode=options Filter mode, values: options, remote, disabled (Hint; lazylLoad is not supported with remote filter, if you use remote filter, the lazyLoad is disabled)
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {string} templateMapping Mapping of the template placeholders
	 * @property {string} templateMapping.selected Selected Template
	 * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/)
	 * @property {string} popper.placement=bottom PopperJS placement
	 * @property {Object[]} modifiers={name:offset} PopperJS placement
	 * @property {Object} mapping
	 * @property {String} mapping.selector=* Path to select the appropriate entries
	 * @property {String} mapping.labelTemplate="" template with the label placeholders in the form ${name}, where name is the key (**)
	 * @property {String} mapping.valueTemplate="" template with the value placeholders in the form ${name}, where name is the key
	 * @property {Monster.Components.Form~exampleFilterCallback|undefined} mapping.filter Filtering of values via a function
	 * @property {Object} formatter
	 * @property {Monster.Components.Form~formatterSelectionCallback|undefined} formatter.selection format selection label
	 */
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				toggleEventType: ["click", "touch"],
				delegatesFocus: false,
				options: [],
				selection: [],
				showMaxOptions: 10,
				type: "radio",
				name: new ID("s").toString(),
				features: {
					clearAll: true,
					clear: true,
					lazyLoad: false,
					closeOnSelect: false,
					emptyValueIfNoOptions: false,
					storeFetchedData: false,
					useStrictValueComparison: false,
				},
				url: null,
				lookup: {
					url: null,
					grouping: false,
				},
				labels: getTranslations(),
				messages: {
					control: null,
					selected: null,
					emptyOptions: null,
				},
				fetch: {
					redirect: "error",
					method: "GET",
					mode: "same-origin",
					credentials: "same-origin",
					headers: {
						accept: "application/json",
					},
				},
				filter: {
					defaultValue: null,
					mode: FILTER_MODE_DISABLED,
					position: FILTER_POSITION_INLINE,
					marker: {
						open: "{",
						close: "}",
					},
				},
				classes: {
					badge: "monster-badge-primary",
					statusOrRemoveBadge: "empty",
				},
				mapping: {
					selector: "*",
					labelTemplate: "",
					valueTemplate: "",
					filter: null,
				},
				formatter: {
					selection: buildSelectionLabel,
				},
				templates: {
					main: getTemplate(),
				},
				templateMapping: {
					/** with the attribute `data-monster-selected-template` the template for the selected options can be defined. */
					selected: getSelectionTemplate(),
				},

				popper: {
					placement: "bottom",
					middleware: ["flip", "offset:1"],
				},
			},
			initOptionsFromArguments.call(this),
		);
	}

	/**
	 * @return {Select}
	 */
	[assembleMethodSymbol]() {
		const self = this;
		super[assembleMethodSymbol]();

		initControlReferences.call(self);
		initEventHandler.call(self);

		let lazyLoadFlag = self.getOption("features.lazyLoad", false);
		let remoteFilterFlag = getFilterMode.call(this) === FILTER_MODE_REMOTE;

		if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
			self.getOption("features.lazyLoad", false);
			if (lazyLoadFlag === true) {
				addErrorAttribute(this, "lazyLoad is not supported with remote filter");
				lazyLoadFlag = false;
			}
		}

		if (self.hasAttribute("value")) {
			new Processing(10, () => {
				this.value = this.getAttribute("value");
			})
				.run()
				.catch((e) => {
					addErrorAttribute(this, e);
				});
		}

		if (self.getOption("url") !== null) {
			if (lazyLoadFlag || remoteFilterFlag) {
				lookupSelection.call(self);
			} else {
				self.fetch().catch((e) => {
					addErrorAttribute(self, e);
				});
			}
		}

		let lastValue = self.value;
		self[internalSymbol].attachObserver(
			new Observer(function () {
				if (isObject(this) && this instanceof ProxyObserver) {
					const n = this.getSubject()?.options?.value;

					if (lastValue !== n) {
						lastValue = n;
						setSelection
							.call(self, n)
							.then(() => {})
							.catch((e) => {
								addErrorAttribute(self, e);
							});
					}
				}
			}),
		);

		areOptionsAvailableAndInit.call(self);

		return this;
	}

	/**
	 *
	 * @return {*}
	 * @throws {Error} storeFetchedData is not enabled
	 * @since 3.66.0
	 */
	getLastFetchedData() {
		if (this.getOption("features.storeFetchedData") === false) {
			throw new Error("storeFetchedData is not enabled");
		}

		return this?.[lastFetchedDataSymbol];
	}

	/**
	 * The Button.click() method simulates a click on the internal button element.
	 *
	 * @since 3.27.0
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
	 */
	click() {
		if (this.getOption("disabled") === true) {
			return;
		}

		toggle.call(this);
	}

	/**
	 * The Button.focus() method sets focus on the internal button element.
	 *
	 * @since 3.27.0
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
	 */
	focus(options) {
		if (this.getOption("disabled") === true) {
			return;
		}

		new Processing(() => {
			gatherState.call(this);
			focusFilter.call(this, options);
		})
			.run()
			.catch((e) => {
				addErrorAttribute(this, e);
			});
	}

	/**
	 * The Button.blur() method removes focus from the internal button element.
	 * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur
	 */
	blur() {
		new Processing(() => {
			gatherState.call(this);
			blurFilter.call(this);
		})
			.run()
			.catch((e) => {
				addErrorAttribute(this, e);
			});
	}

	/**
	 * If no url is specified, the options are taken from the Component itself.
	 *
	 * @param {string|URL} url URL to fetch the options
	 * @return {Promise}
	 */
	fetch(url) {
		return fetchIt.call(this, url);
	}

	/**
	 * @return {void}
	 */
	connectedCallback() {
		super.connectedCallback();
		const document = getDocument();

		for (const [, type] of Object.entries(["click", "touch"])) {
			// close on outside ui-events
			document.addEventListener(type, this[closeEventHandler]);
		}

		parseSlotsToOptions.call(this);
		attachResizeObserver.call(this);
		updatePopper.call(this);

		new Processing(() => {
			gatherState.call(this);
			focusFilter.call(this);
		})
			.run()
			.catch((e) => {
				addErrorAttribute(this, e);
			});
	}

	/**
	 * @return {void}
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
		const document = getDocument();

		// close on outside ui-events
		for (const [, type] of Object.entries(["click", "touch"])) {
			document.removeEventListener(type, this[closeEventHandler]);
		}

		disconnectResizeObserver.call(this);
	}

	/**
	 * Import Select Options from dataset
	 * Not to be confused with the control defaults/options
	 *
	 * @param {array|object|Map|Set} data
	 * @return {Select}
	 * @throws {Error} map is not iterable
	 * @throws {Error} missing label configuration
	 * @fires monster-options-set this event is fired when the options are set
	 */
	importOptions(data) {
		const mappingOptions = this.getOption("mapping", {});
		const selector = mappingOptions?.["selector"];
		const labelTemplate = mappingOptions?.["labelTemplate"];
		const valueTemplate = mappingOptions?.["valueTemplate"];
		const filter = mappingOptions?.["filter"];

		let flag = false;
		if (labelTemplate === "") {
			addErrorAttribute(this, "empty label template");
			flag = true;
		}

		if (valueTemplate === "") {
			addErrorAttribute(this, "empty value template");
			flag = true;
		}

		if (flag === true) {
			throw new Error("missing label configuration");
		}

		const map = buildMap(data, selector, labelTemplate, valueTemplate, filter);

		const options = [];

		if (!isIterable(map)) {
			throw new Error("map is not iterable");
		}

		const visibility = "visible";

		map.forEach((label, value) => {
			options.push({
				value,
				label,
				visibility,
				data: map.get(value),
			});
		});

		runAsOptionLengthChanged.call(this, map.size);
		this.setOption("options", options);

		fireCustomEvent(this, "monster-options-set", {
			options,
		});

		setTimeout(() => {
			setSelection
				.call(this, this.getOption("selection"))
				.then(() => {})
				.catch((e) => {
					addErrorAttribute(this, e);
				});
		}, 10);

		return this;
	}

	/**
	 * @private
	 * @return {Select}
	 */
	calcAndSetOptionsDimension() {
		calcAndSetOptionsDimension.call(this);
		return this;
	}

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

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [SelectStyleSheet];
	}
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				"cannot-be-loaded": "Kann nicht geladen werden",
				"no-options-available": "Keine Optionen verfügbar.",
				"click-to-load-options": "Klicken, um Optionen zu laden.",
				"select-an-option": "Wähle eine Option",
				"summary-text": {
					zero: "Keine Einträge ausgewählt",
					one: '<span class="monster-badge-primary-pill">1</span> Eintrag ausgewählt',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> Einträge ausgewählt',
				},
				"no-options": "Leider gibt es keine Optionen in der Liste.",
				"no-options-found":
					"Keine Optionen in der Liste verfügbar. Bitte ändern Sie den Filter.",
			};
		case "fr":
			return {
				"cannot-be-loaded": "Impossible de charger",
				"no-options-available": "Aucune option disponible.",
				"click-to-load-options": "Cliquez pour charger les options.",
				"select-an-option": "Sélectionnez une option",
				"summary-text": {
					zero: "Aucune entrée sélectionnée",
					one: '<span class="monster-badge-primary-pill">1</span> entrée sélectionnée',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> entrées sélectionnées',
				},
				"no-options":
					"Malheureusement, il n'y a pas d'options disponibles dans la liste.",
				"no-options-found":
					"Aucune option disponible dans la liste. Veuillez modifier le filtre.",
			};

		case "sp":
			return {
				"cannot-be-loaded": "No se puede cargar",
				"no-options-available": "No hay opciones disponibles.",
				"click-to-load-options": "Haga clic para cargar opciones.",
				"select-an-option": "Seleccione una opción",
				"summary-text": {
					zero: "No se seleccionaron entradas",
					one: '<span class="monster-badge-primary-pill">1</span> entrada seleccionada',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> entradas seleccionadas',
				},
				"no-options":
					"Desafortunadamente, no hay opciones disponibles en la lista.",
				"no-options-found":
					"No hay opciones disponibles en la lista. Considere modificar el filtro.",
			};
		case "it":
			return {
				"cannot-be-loaded": "Non può essere caricato",
				"no-options-available": "Nessuna opzione disponibile.",
				"click-to-load-options": "Clicca per caricare le opzioni.",
				"select-an-option": "Seleziona un'opzione",
				"summary-text": {
					zero: "Nessuna voce selezionata",
					one: '<span class="monster-badge-primary-pill">1</span> voce selezionata',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> voci selezionate',
				},
				"no-options": "Purtroppo, non ci sono opzioni disponibili nella lista.",
				"no-options-found":
					"Nessuna opzione disponibile nella lista. Si prega di modificare il filtro.",
			};
		case "pl":
			return {
				"cannot-be-loaded": "Nie można załadować",
				"no-options-available": "Brak dostępnych opcji.",
				"click-to-load-options": "Kliknij, aby załadować opcje.",
				"select-an-option": "Wybierz opcję",
				"summary-text": {
					zero: "Nie wybrano żadnych wpisów",
					one: '<span class="monster-badge-primary-pill">1</span> wpis został wybrany',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> wpisy zostały wybrane',
				},
				"no-options": "Niestety, nie ma dostępnych opcji na liście.",
				"no-options-found":
					"Brak dostępnych opcji na liście. Rozważ zmianę filtra.",
			};
		case "no":
			return {
				"cannot-be-loaded": "Kan ikke lastes",
				"no-options-available": "Ingen alternativer tilgjengelig.",
				"click-to-load-options": "Klikk for å laste alternativer.",
				"select-an-option": "Velg et alternativ",
				"summary-text": {
					zero: "Ingen oppføringer ble valgt",
					one: '<span class="monster-badge-primary-pill">1</span> oppføring valgt',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> oppføringer valgt',
				},
				"no-options":
					"Dessverre er det ingen alternativer tilgjengelig i listen.",
				"no-options-found":
					"Ingen alternativer tilgjengelig på listen. Vurder å endre filteret.",
			};

		case "dk":
			return {
				"cannot-be-loaded": "Kan ikke indlæses",
				"no-options-available": "Ingen muligheder tilgængelige.",
				"click-to-load-options": "Klik for at indlæse muligheder.",
				"select-an-option": "Vælg en mulighed",
				"summary-text": {
					zero: "Ingen indlæg blev valgt",
					one: '<span class="monster-badge-primary-pill">1</span> indlæg blev valgt',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> indlæg blev valgt',
				},
				"no-options":
					"Desværre er der ingen muligheder tilgængelige på listen.",
				"no-options-found":
					"Ingen muligheder tilgængelige på listen. Overvej at ændre filteret.",
			};
		case "sw":
			return {
				"cannot-be-loaded": "Kan inte laddas",
				"no-options-available": "Inga alternativ tillgängliga.",
				"click-to-load-options": "Klicka för att ladda alternativ.",
				"select-an-option": "Välj ett alternativ",
				"summary-text": {
					zero: "Inga poster valdes",
					one: '<span class="monster-badge-primary-pill">1</span> post valdes',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> poster valdes',
				},
				"no-options": "Tyvärr finns det inga alternativ tillgängliga i listan.",
				"no-options-found":
					"Inga alternativ finns tillgängliga i listan. Överväg att modifiera filtret.",
			};

		default:
		case "en":
			return {
				"cannot-be-loaded": "Cannot be loaded",
				"no-options-available": "No options available.",
				"click-to-load-options": "Click to load options.",
				"select-an-option": "Select an option",
				"summary-text": {
					zero: "No entries were selected",
					one: '<span class="monster-badge-primary-pill">1</span> entry was selected',
					other:
						'<span class="monster-badge-primary-pill">${count}</span> entries were selected',
				},
				"no-options":
					"Unfortunately, there are no options available in the list.",
				"no-options-found":
					"No options are available in the list. Please consider modifying the filter.",
			};
	}
}

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

	setTimeout(() => {
		const selection = self.getOption("selection");
		if (selection.length === 0) {
			return;
		}

		if (self[isLoadingSymbol] === true) {
			return;
		}

		if (self[lazyLoadDoneSymbol] === true) {
			return;
		}

		let url = self.getOption("url");
		let lookupUrl = self.getOption("lookup.url");
		if (lookupUrl !== null) {
			url = lookupUrl;
		}

		if (this.getOption("lookup.grouping") === true) {
			filterFromRemoteByValue
				.call(
					self,
					url,
					selection.map((s) => s?.["value"]),
				)
				.catch((e) => {
					addErrorAttribute(self, e);
				});
			return;
		}

		for (const s of selection) {
			if (s?.["value"]) {
				filterFromRemoteByValue.call(self, url, s?.["value"]).catch((e) => {
					addErrorAttribute(self, e);
				});
			}
		}
	}, 100);
}

function fetchIt(url, controlOptions) {
	if (url instanceof URL) {
		url = url.toString();
	}

	if (url !== undefined && url !== null) {
		url = validateString(url);
	} else {
		url = this.getOption("url");
		if (url === null) {
			return Promise.reject(new Error("No url defined"));
		}
	}

	return new Promise((resolve, reject) => {
		setStatusOrRemoveBadges.call(this, "loading");

		new Processing(10, () => {
			fetchData
				.call(this, url)
				.then((map) => {
					if (
						isObject(map) ||
						isArray(map) ||
						map instanceof Set ||
						map instanceof Map
					) {
						try {
							this.importOptions(map);
						} catch (e) {
							setStatusOrRemoveBadges.call(this, "error");
							reject(e);
							return;
						}

						this[lastFetchedDataSymbol] = map;

						let result;
						const selection = this.getOption("selection");
						let newValue = [];
						if (selection) {
							newValue = selection;
						} else if (this.hasAttribute("value")) {
							newValue = this.getAttribute("value");
						}

						result = setSelection.call(this, newValue);
						requestAnimationFrame(() => {
							checkOptionState.call(this);
							setStatusOrRemoveBadges.call(this, "closed");
							updatePopper.call(this);
							resolve(result);
						});

						return;
					}

					setStatusOrRemoveBadges.call(this, "error");
					reject(new Error("invalid response"));
				})
				.catch((e) => {
					setStatusOrRemoveBadges.call(this, "error");
					reject(e);
				});
		})
			.run()
			.catch((e) => {
				setStatusOrRemoveBadges.call(this, "error");
				addErrorAttribute(this, e);
				reject(e);
			});
	});
}

/**
 * This attribute can be used to pass a URL to this select.
 *
 * ```
 * <monster-select data-monster-url="https://example.com/"></monster-select>
 * ```
 *
 * @private
 * @deprecated 2024-01-21 (you should use data-monster-option-...)
 * @return {object}
 */
function initOptionsFromArguments() {
	const options = {};

	const template = this.getAttribute("data-monster-selected-template");
	if (isString(template)) {
		if (!options["templateMapping"]) options["templateMapping"] = {};

		switch (template) {
			case "summary":
			case "default":
				options["templateMapping"]["selected"] = getSummaryTemplate();
				break;
			case "selected":
				options["templateMapping"]["selected"] = getSelectionTemplate();
				break;
			default:
				addErrorAttribute(this, "invalid template, use summary or selected");
		}
	}

	return options;
}

/**
 * @private
 */
function attachResizeObserver() {
	// against flickering
	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
			try {
				this[timerCallbackSymbol].touch();
				return;
			} catch (e) {
				delete this[timerCallbackSymbol];
			}
		}

		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
			updatePopper.call(this);
			delete this[timerCallbackSymbol];
		});
	});

	this[resizeObserverSymbol].observe(this.parentElement);
}

/**
 * @private
 */
function disconnectResizeObserver() {
	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
		this[resizeObserverSymbol].disconnect();
	}
}

/**
 * @private
 * @returns {string}
 */
function getSelectionTemplate() {
	return `<div data-monster-role="selection" part="selection"
                 data-monster-insert="selection path:selection" role="search"
            ><input type="text" role="searchbox"
                    part="inline-filter" name="inline-filter"
                    data-monster-role="filter"
                    autocomplete="off"
                    tabindex="0"
            ><div data-monster-replace="path:messages.control"></div>
            </div>`;
}

/**
 * @private
 * @returns {string}
 */
function getSummaryTemplate() {
	return `<div data-monster-role="selection" role="search" part="summary">
    <input type="text" role="searchbox"
           part="inline-filter" name="inline-filter"
           data-monster-role="filter"
           autocomplete="off"
           tabindex="0"
    >
    <div data-monster-replace="path:messages.selected"></div>    
</div>`;
}

/**
 * @return {void}
 * @private
 */
function parseSlotsToOptions() {
	let options = this.getOption("options");
	if (!isIterable(options)) {
		options = [];
	}

	let counter = 1;
	getSlottedElements.call(this, "div").forEach((node) => {
		let value = (counter++).toString();
		let visibility = "visible";

		if (node.hasAttribute("data-monster-value")) {
			value = node.getAttribute("data-monster-value");
		}

		let label = node.outerHTML;

		if (node.style.display === "none") {
			visibility = "hidden";
		}

		options.push({
			value,
			label,
			visibility,
		});
	});

	runAsOptionLengthChanged.call(this, options.length);
	this.setOption("options", options);
}

/**
 * wait until all options are finished rendering
 *
 * @private
 * @param {int} targetLength
 */
function runAsOptionLengthChanged(targetLength) {
	const self = this;

	if (!self[optionsElementSymbol]) {
		return;
	}

	const callback = function (mutationsList, observer) {
		const run = false;
		for (const mutation of mutationsList) {
			if (mutation.type === "childList") {
				const run = true;
				break;
			}
		}

		if (run === true) {
			const nodes = self[optionsElementSymbol].querySelectorAll(
				`div[${ATTRIBUTE_ROLE}=option]`,
			);

			if (nodes.length === targetLength) {
				checkOptionState.call(self);
				observer.disconnect();
			}
		}
	};

	const observer = new MutationObserver(callback);
	observer.observe(self[optionsElementSymbol], {
		attributes: false,
		childList: true,
		subtree: true,
	});
}

/**
 * @private
 * @param {*} value
 * @return {*}
 */
function buildSelectionLabel(value) {
	const options = this.getOption("options");

	for (let i = 0; i < options.length; i++) {
		let o = options?.[i];
		let l, v, v2;

		if (this.getOption("features.useStrictValueComparison") === true) {
			v = value;
		} else {
			v = `${value}`;
		}

		if (isPrimitive(o) && o === value) {
			return o;
		} else if (!isObject(o)) {
			continue;
		}

		if (this.getOption("features.useStrictValueComparison") === true) {
			l = o?.["label"];
			v2 = o?.["value"];
		} else {
			l = `${o?.["label"]}`;
			v2 = `${o?.["value"]}`;
		}

		if (v2 === v) {
			return l;
		}
	}

	return undefined;
}

/**
 * @private
 * @param {*} value
 * @return {string}
 * @throws {Error} no value found
 */
function getSelectionLabel(value) {
	const callback = this.getOption("formatter.selection");
	if (isFunction(callback)) {
		const label = callback.call(this, value);
		if (isString(label)) return label;
	}

	if (isString(value) || isInteger(value)) {
		return `${value}`;
	}

	return this.getOption("labels.cannot-be-loaded", value);
}

/**
 * @private
 * @param {Event} event
 */
function handleToggleKeyboardEvents(event) {
	switch (event?.["code"]) {
		case "Escape":
			toggle.call(this);
			event.preventDefault();
			break;
		case "Space":
			toggle.call(this);
			event.preventDefault();
			break;
		case "ArrowDown":
			show.call(this);
			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
			event.preventDefault();
			break;
		case "ArrowUp":
			hide.call(this);
			event.preventDefault();
			break;
	}
}

/**
 * @license AGPLv3
 * @since 1.15.0
 * @private
 * @this CustomElement
 */
function initOptionObserver() {
	const self = this;

	self.attachObserver(
		new Observer(function () {
			new Processing(() => {
				try {
					self.updateI18n();
				} catch (e) {
					addErrorAttribute(self, e);
					requestAnimationFrame(() => {
						setStatusOrRemoveBadges.call(self, "error");
					});
				}
				try {
					areOptionsAvailableAndInit.call(self);
				} catch (e) {
					addErrorAttribute(self, e);
					requestAnimationFrame(() => {
						setStatusOrRemoveBadges.call(self, "error");
					});
				}

				setSummaryAndControlText.call(self);
			}).run();
		}),
	);
}

/**
 * @private
 * @returns {Translations}
 */
function getDefaultTranslation() {
	const translation = new Translations("en").assignTranslations(
		this.getOption("labels", {}),
	);

	try {
		const doc = getDocumentTranslations();
		translation.locale = doc.locale;
	} catch (e) {}

	return translation;
}

/**
 * @private
 * @return {string|*}
 */
function setSummaryAndControlText() {
	const translations = getDefaultTranslation.call(this);
	const selections = this.getOption("selection");

	const text = translations.getPluralRuleText(
		"summary-text",
		selections.length,
		"",
	);

	const selectedText = new Formatter({
		count: String(selections.length),
	}).format(text);

	this.setOption("messages.selected", selectedText);

	const current = this.getOption("messages.control");
	const msg = this.getOption("labels.select-an-option");

	if (
		current === "" ||
		current === undefined ||
		current === msg ||
		current === null
	) {
		if (selections.length === 0) {
			this.setOption("messages.control", msg);
		} else {
			this.setOption("messages.control", "");
		}
	}
}

/**
 * @private
 * @return {NodeList}
 */
function getOptionElements() {
	return this[optionsElementSymbol].querySelectorAll(
		`[${ATTRIBUTE_ROLE}=option]`,
	);
}

/**
 * With the help of this filter callback, values can be filtered out. Only if the filter function returns true, the value is taken for the map.
 *
 * @callback Monster.Components.Form~exampleFilterCallback
 * @param {*} value Value
 * @param {string} key  Key
 * @see Monster.Data.buildMap
 */

/**
 *
 * @callback Monster.Components.Form~formatterSelectionCallback
 * @param {*} value Value
 * @return {string|undefined}
 * @see Monster.Data.buildMap
 */

/**
 * @private
 */
function calcAndSetOptionsDimension() {
	const options = getOptionElements.call(this);
	const container = this[optionsElementSymbol];
	if (!(container instanceof HTMLElement && options instanceof NodeList)) {
		return;
	}

	let visible = 0;
	let optionHeight = 0;
	const max = this.getOption("showMaxOptions", 10);

	let scrollFlag = false;
	for (const [, option] of Object.entries(options)) {
		const computedStyle = getGlobal().getComputedStyle(option);
		if (computedStyle.display === "none") continue;

		let h = option.getBoundingClientRect().height;
		h += parseInt(computedStyle.getPropertyValue("margin-top"), 10);
		h += parseInt(computedStyle.getPropertyValue("margin-bottom"), 10);
		optionHeight += h;

		visible++;

		if (visible > max) {
			break;
		}
	}

	if (visible > max) {
		visible = max;
		scrollFlag = true;
	}

	if (visible === 0) {
		if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
			this.setOption(
				"messages.emptyOptions",
				this.getOption("labels.no-options-available"),
			);
		} else {
			this.setOption(
				"messages.emptyOptions",
				this.getOption("labels.no-options-found"),
			);
		}
		this[noOptionsAvailableElementSymbol].classList.remove("d-none");
	} else {
		this[noOptionsAvailableElementSymbol].classList.add("d-none");
	}

	const styles = getGlobal().getComputedStyle(this[optionsElementSymbol]);
	let padding = parseInt(styles.getPropertyValue("padding-top"), 10);
	padding += parseInt(styles.getPropertyValue("padding-bottom"), 10);

	let margin = parseInt(styles.getPropertyValue("margin-top"), 10);
	margin += parseInt(styles.getPropertyValue("margin-bottom"), 10);

	const containerHeight = optionHeight + padding + margin;
	container.style.height = `${containerHeight}px`;

	if (scrollFlag === true) {
		container.style.overflowY = "scroll";
	} else {
		container.style.overflowY = "auto";
	}

	const domRect = this[controlElementSymbol].getBoundingClientRect();

	this[popperElementSymbol].style.width = `${domRect.width}px`;
	container.style.overflowX = "auto";
}

/**
 * @private
 * @param {number} direction
 * @throws {Error} no shadow-root is defined
 */
function activateCurrentOption(direction) {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	let focused = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`);

	if (
		!(focused instanceof HTMLElement) ||
		focused.matches("[data-monster-visibility=hidden]")
	) {
		for (const [, e] of Object.entries(
			this.shadowRoot.querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`),
		)) {
			if (e.matches("[data-monster-visibility=visible]")) {
				focused = e;
				break;
			}
		}
	} else {
		if (direction === FOCUS_DIRECTION_DOWN) {
			while (focused.nextSibling) {
				focused = focused.nextSibling;

				if (
					focused instanceof HTMLElement &&
					focused.hasAttribute(ATTRIBUTE_ROLE) &&
					focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
					focused.matches("[data-monster-visibility=visible]") &&
					focused.matches(":not([data-monster-filtered=true])")
				) {
					break;
				}
			}
		} else {
			let found = false;
			while (focused.previousSibling) {
				focused = focused.previousSibling;
				if (
					focused instanceof HTMLElement &&
					focused.hasAttribute(ATTRIBUTE_ROLE) &&
					focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
					focused.matches("[data-monster-visibility=visible]") &&
					focused.matches(":not([data-monster-filtered=true])")
				) {
					found = true;
					break;
				}
			}
			if (found === false) {
				focusFilter.call(this);
			}
		}
	}

	new Processing(() => {
		if (focused instanceof HTMLElement) {
			this.shadowRoot
				.querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`)
				.forEach((e) => {
					e.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
				});

			focused.focus();
			focused.setAttribute(`${ATTRIBUTE_PREFIX}focused`, true);
		}
	})
		.run()
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 */
function filterOptions() {
	new Processing(() => {
		let filterValue;

		switch (this.getOption("filter.position")) {
			case FILTER_POSITION_INLINE:
				if (this[inlineFilterElementSymbol] instanceof HTMLElement) {
					filterValue = this[inlineFilterElementSymbol].value.toLowerCase();
				} else {
					return;
				}

				break;
			case FILTER_POSITION_POPPER:
			default:
				if (this[popperFilterElementSymbol] instanceof HTMLInputElement) {
					filterValue = this[popperFilterElementSymbol].value.toLowerCase();
				} else {
					return;
				}
		}

		const options = this.getOption("options");
		for (const [i, option] of Object.entries(options)) {
			if (option.label.toLowerCase().indexOf(filterValue) === -1) {
				this.setOption(`options.${i}.filtered`, "true");
			} else {
				this.setOption(`options.${i}.filtered`, undefined);
			}
		}
	})
		.run()
		.then(() => {
			new Processing(100, () => {
				calcAndSetOptionsDimension.call(this);
				focusFilter.call(this);
			})
				.run()
				.catch((e) => {
					addErrorAttribute(this, e);
				});
		})
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 * @param {Event} event
 */
function handleFilterKeyboardEvents(event) {
	const shiftKey = event?.["shiftKey"];

	switch (event?.["code"]) {
		case "Tab":
			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
			event.preventDefault();
			break;
		case "Escape":
			toggle.call(this);
			event.preventDefault();
			break;
		case "Tab" && shiftKey === true:
		case "ArrowUp":
			activateCurrentOption.call(this, FOCUS_DIRECTION_UP);
			event.preventDefault();
			break;
		case "Tab" && !shiftKey:
		case "ArrowDown":
			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
			event.preventDefault();
			break;
		default:
			if (
				this.getOption("features.lazyLoad") === true &&
				this[lazyLoadDoneSymbol] !== true
			) {
				this.click();
			}

			handleFilterKeyEvents.call(this);
	}
}

/**
 * Method handleFilterKeyEvents is used to handle filter key events.
 * Debounce is used to prevent multiple calls.
 *
 * @function
 * @name handleFilterKeyEvents
 *
 * @private
 * @return {void} This method does not return anything.
 */
function handleFilterKeyEvents() {
	if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
		try {
			this[keyFilterEventSymbol].touch();
			return;
		} catch (e) {
			delete this[keyFilterEventSymbol];
		}
	}

	this[keyFilterEventSymbol] = new DeadMansSwitch(200, () => {
		if (getFilterMode.call(this) !== FILTER_MODE_REMOTE) {
			filterOptions.call(this);
		} else {
			filterFromRemote.call(this).catch((e) => {
				addErrorAttribute(this, e);
			});
		}

		delete this[keyFilterEventSymbol];
	});
}

/**
 * @private
 */
function filterFromRemote() {
	if (
		!(this[inlineFilterElementSymbol] instanceof HTMLElement) &&
		!(this[popperFilterElementSymbol] instanceof HTMLElement)
	) {
		return;
	}

	show.call(this);

	const url = this.getOption("url");
	if (!url) {
		addErrorAttribute(this, "Missing URL for Remote Filter.");
		return;
	}

	let filterValue;

	switch (this.getOption("filter.position")) {
		case FILTER_POSITION_INLINE:
			if (this[inlineFilterElementSymbol] instanceof HTMLElement) {
				filterValue = this[inlineFilterElementSymbol].value.toLowerCase();
			}

			break;
		case FILTER_POSITION_POPPER:
		default:
			if (this[popperFilterElementSymbol] instanceof HTMLInputElement) {
				filterValue = this[popperFilterElementSymbol].value.toLowerCase();
			}
	}

	return filterFromRemoteByValue.call(this, url, filterValue);
}

function formatURL(url, value) {
	if (value === undefined || value === null || value === "") {
		value = this.getOption("filter.defaultValue");
		if (value === undefined || value === null || value === "") {
			value = disabledRequestMarker.toString();
		}
	}

	const formatter = new Formatter({ filter: encodeURI(value) });
	const openMarker = this.getOption("filter.marker.open");
	let closeMarker = this.getOption("filter.marker.close");
	if (!closeMarker) {
		closeMarker = openMarker;
	}

	if (openMarker && closeMarker) {
		formatter.setMarker(openMarker, closeMarker);
	}

	return formatter.format(url);
}

/**
 * @private
 * @param optionUrl
 * @param value
 * @returns {Promise<unknown>}
 */
function filterFromRemoteByValue(optionUrl, value) {
	return new Processing(() => {
		let url = formatURL.call(this, optionUrl, value);
		if (url.indexOf(disabledRequestMarker.toString()) !== -1) {
			return;
		}

		fetchIt
			.call(this, url, {
				disableHiding: true,
			})
			.then(() => {
				checkOptionState.call(this);
				show.call(this);
			})
			.catch((e) => {
				throw e;
			});
	})
		.run()
		.catch((e) => {
			throw e;
		});
}

/**
 * @param {Event} event
 * @private
 */
function handleOptionKeyboardEvents(event) {
	const shiftKey = event?.["shiftKey"];

	switch (event?.["code"]) {
		case "Escape":
			toggle.call(this);
			event.preventDefault();
			break;
		case "Enter":
		case "Space":
			const path = event.composedPath();
			const element = path?.[0];
			if (element instanceof HTMLElement) {
				const input = element.getElementsByTagName("input");
				if (!input) {
					return;
				}
				fireEvent(input, "click");
			}
			event.preventDefault();
			break;

		case "Tab" && shiftKey === true:
		case "ArrowUp":
			activateCurrentOption.call(this, FOCUS_DIRECTION_UP);
			event.preventDefault();
			break;

		case "Tab" && !shiftKey:
		case "ArrowLeft":
		case "ArrowRight":
			// handled by tree select
			break;
		case "ArrowDown":
			activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
			event.preventDefault();
			break;
		default:
			const p = event.composedPath();
			if (p?.[0] instanceof HTMLInputElement) {
				return;
			}
			focusFilter.call(this);
			break;
	}
}

/**
 * @private
 * @return {string}
 */
function getFilterMode() {
	switch (this.getOption("filter.mode")) {
		case FILTER_MODE_OPTIONS:
			return FILTER_MODE_OPTIONS;
		case FILTER_MODE_REMOTE:
			return FILTER_MODE_REMOTE;
		default:
			return FILTER_MODE_DISABLED;
	}
}

/**
 * @private
 */
function blurFilter() {
	if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) {
		return;
	}

	if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
		return;
	}

	this[popperFilterContainerElementSymbol].classList.remove("active");
	this[popperFilterContainerElementSymbol].blur();

	this[inlineFilterElementSymbol].classList.remove("active");
	this[inlineFilterElementSymbol].blur();
}

/**
 * @private
 * @param focusOptions
 */
function focusPopperFilter(focusOptions) {
	this[popperFilterContainerElementSymbol].classList.remove("d-none");
	this[popperFilterElementSymbol].classList.add("active");
	this[inlineFilterElementSymbol].classList.remove("active");
	this[inlineFilterElementSymbol].classList.add("d-none");

	if (!(this[popperFilterElementSymbol] instanceof HTMLElement)) {
		addErrorAttribute(this, "Missing Popper Filter Element.");
		return;
	}

	// visibility is set to visible, because focus() does not work on invisible elements
	// and the class definition is assigned later in the processing
	setTimeout(() => {
		if (focusOptions === undefined || focusOptions === null) {
			this[popperFilterElementSymbol].focus();
		} else {
			this[popperFilterElementSymbol].focus(focusOptions);
		}
	}, 100);
}

/**
 * @private
 * @param focusOptions
 */
function focusInlineFilter(focusOptions) {
	const options = this.getOption("options");
	if (
		(!isArray(options) || options.length === 0) &&
		getFilterMode.call(this) !== FILTER_MODE_REMOTE
	) {
		return;
	}

	this[popperFilterContainerElementSymbol].classList.add("d-none");
	this[inlineFilterElementSymbol].classList.add("active");
	this[inlineFilterElementSymbol].classList.remove("d-none");

	// visibility is set to visible, because focus() does not work on invisible elements
	// and the class definition is assigned later in the processing
	setTimeout(() => {
		if (focusOptions === undefined || focusOptions === null) {
			this[inlineFilterElementSymbol].focus();
		} else {
			this[inlineFilterElementSymbol].focus(focusOptions);
		}
	}, 100);
}

/**
 * @private
 */
function focusFilter(focusOptions) {
	if (getFilterMode.call(this) === FILTER_MODE_DISABLED) {
		this[popperFilterContainerElementSymbol].classList.add("d-none");
		this[inlineFilterElementSymbol].classList.add("d-none");
		return;
	}

	if (this.getOption("filter.position") === FILTER_POSITION_INLINE) {
		return focusInlineFilter.call(this, focusOptions);
	}

	return focusPopperFilter.call(this, focusOptions);
}

/**
 * @private
 * @return {array}
 * @throws {Error} no shadow-root is defined
 * @throws {Error} unsupported type
 */
function gatherState() {
	const type = this.getOption("type");
	if (["radio", "checkbox"].indexOf(type) === -1) {
		throw new Error("unsupported type");
	}

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

	const selection = [];
	const elements = this.shadowRoot.querySelectorAll(
		`input[type=${type}]:checked`,
	);

	for (const e of elements) {
		selection.push({
			label: getSelectionLabel.call(this, e.value),
			value: e.value,
		});
	}

	setSelection
		.call(this, selection)
		.then(() => {})
		.catch((e) => {
			addErrorAttribute(this, e);
		});

	if (this.getOption("features.closeOnSelect") === true) {
		toggle.call(this);
	}

	return this;
}

/**
 * @private
 * @throws {Error} no shadow-root is defined
 * @throws {Error} unsupported type
 */
function clearSelection() {
	const type = this.getOption("type");
	if (["radio", "checkbox"].indexOf(type) === -1) {
		throw new Error("unsupported type");
	}

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

	setSelection
		.call(this, [])
		.then(() => {})
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 */
function areOptionsAvailableAndInit() {
	// prevent multiple calls
	if (this[areOptionsAvailableAndInitSymbol] === undefined) {
		this[areOptionsAvailableAndInitSymbol] = 0;
	}

	if (this[areOptionsAvailableAndInitSymbol] > 0) {
		this[areOptionsAvailableAndInitSymbol]--;
		return true;
	}

	this[areOptionsAvailableAndInitSymbol]++;

	const options = this.getOption("options");

	if (
		options === undefined ||
		options === null ||
		(isArray(options) && options.length === 0)
	) {
		setStatusOrRemoveBadges.call(this, "empty");

		// hide.call(this);

		let msg = this.getOption("labels.no-options-available");

		if (
			this.getOption("url") !== null &&
			this.getOption("features.lazyLoad") === true &&
			this[lazyLoadDoneSymbol] !== true
		) {
			msg = this.getOption("labels.click-to-load-options");
		}

		this.setOption("messages.control", msg);
		this.setOption("messages.summary", "");

		if (this.getOption("features.emptyValueIfNoOptions") === true) {
			this.value = "";
		}
		addErrorAttribute(this, "No options available.");
		return false;
	}

	const selections = this.getOption("selection");
	if (
		selections === undefined ||
		selections === null ||
		selections.length === 0
	) {
		this.setOption(
			"messages.control",
			this.getOption("labels.select-an-option"),
		);
	} else {
		this.setOption("messages.control", "");
	}

	this.setOption("messages.summary", setSummaryAndControlText.call(this));

	let updated = false;
	let valueCounter = 1;
	for (const option of options) {
		if (option?.visibility === undefined) {
			option.visibility = "visible";
			updated = true;
		}

		if (option?.value === undefined && option?.label === undefined) {
			option.value = `${valueCounter++}`;
			option.label = option.value;
			updated = true;
			continue;
		}

		if (option?.value === undefined) {
			option.value = option.label;
			updated = true;
		}

		if (option?.label === undefined) {
			option.label = option.value;
			updated = true;
		}
	}

	if (updated) {
		this.setOption("options", options);
	}

	setStatusOrRemoveBadges.call(this);

	removeErrorAttribute(this, "No options available.");
	return true;
}

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

	const elements = this.shadowRoot.querySelectorAll(
		`[${ATTRIBUTE_ROLE}=option] input`,
	);

	let selection = this.getOption("selection");
	if (!isArray(selection)) {
		selection = [];
	}

	const checkedValues = selection.map((a) => {
		return a.value;
	});

	for (const e of elements) {
		if (checkedValues.indexOf(e.value) !== -1) {
			if (e.checked !== true) e.checked = true;
		} else {
			if (e.checked !== false) e.checked = false;
		}
	}
}

/**
 * @private
 * @param {*} value
 * @return {Object}
 */
function convertValueToSelection(value) {
	const selection = [];

	if (isString(value)) {
		value = value
			.split(",")
			.map((a) => {
				return a.trim();
			})
			.filter((a) => {
				return a !== "";
			});
	}

	if (isString(value) || isInteger(value)) {
		selection.push({
			label: getSelectionLabel.call(this, value),
			value: value,
		});
	} else if (isArray(value)) {
		for (const v of value) {
			selection.push({
				label: getSelectionLabel.call(this, v),
				value: v,
			});
		}

		value = value.join(",");
	} else {
		throw new Error("unsupported type");
	}

	return {
		selection: selection,
		value: value,
	};
}

/**
 * @private
 * @param {array} selection
 * @return {string}
 */
function convertSelectionToValue(selection) {
	const value = [];

	if (isArray(selection)) {
		for (const obj of selection) {
			const v = obj?.["value"];
			if (v !== undefined) value.push(`${v}`);
		}
	}

	if (value.length === 0) {
		return "";
	} else if (value.length === 1) {
		const v = value.pop();
		if (v === undefined) return "";
		if (v === null) return "";
		return `${v}`;
	}

	return value.join(",");
}

/**
 * @private
 * @param {array} selection
 * @return {Promise}
 * @throws {Error} no shadow-root is defined
 */
function setSelection(selection) {
	if (isString(selection) || isInteger(selection)) {
		const result = convertValueToSelection.call(this, selection);
		selection = result?.selection;
	} else if (selection === undefined || selection === null) {
		selection = [];
	}

	validateArray(selection);

	for (let i = 0; i < selection.length; i++) {
		let l = getSelectionLabel.call(this, selection[i].value);
		if (l === selection[i].value) {
			l = selection[i].label;
		}

		selection[i] = {
			label: l,
			value: selection[i].value,
		};
	}

	this.setOption("selection", selection);
	checkOptionState.call(this);
	setSummaryAndControlText.call(this);

	try {
		this?.setFormValue(this.value);
	} catch (e) {
		addErrorAttribute(this, e);
	}

	fireCustomEvent(this, "monster-selected", {
		selection,
	});

	fireEvent(this, "change");

	return new Processing(() => {
		const CLASSNAME = "selected";

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

		const notSelected = this.shadowRoot.querySelectorAll(":not(:checked)");

		if (notSelected) {
			notSelected.forEach((node) => {
				const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`);
				if (parent) {
					parent.classList.remove(CLASSNAME);
				}
			});
		}

		const selected = this.shadowRoot.querySelectorAll(":checked");

		if (selected) {
			selected.forEach((node) => {
				const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`);
				if (parent) {
					parent.classList.add(CLASSNAME);
				}
			});
		}
	})
		.run()
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 * @param {string} url
 * @return {Promise}
 * @throws {TypeError} the result cannot be parsed
 * @throws {TypeError} unsupported response
 */
function fetchData(url) {
	const self = this;
	if (!url) url = this.getOption("url");
	if (!url) return Promise.resolve();

	const fetchOptions = this.getOption("fetch", {});

	let delayWatch = false;

	// if fetch short time, do not show loading badge, because of flickering
	requestAnimationFrame(() => {
		if (delayWatch === true) return;
		setStatusOrRemoveBadges.call(this, "loading");
		delayWatch = true;
	});

	url = formatURL.call(this, url);

	self[isLoadingSymbol] = true;
	const global = getGlobal();
	return global
		.fetch(url, fetchOptions)
		.then((response) => {
			self[isLoadingSymbol] = false;
			delayWatch = true;
			const contentType = response.headers.get("content-type");
			if (contentType && contentType.indexOf("application/json") !== -1) {
				return response.text();
			}

			throw new TypeError(`unsupported response ${contentType}`);
		})
		.then((text) => {
			try {
				return Promise.resolve(JSON.parse(String(text)));
			} catch (e) {
				throw new TypeError("the result cannot be parsed, check the URL");
			}
		})
		.catch((e) => {
			self[isLoadingSymbol] = false;
			delayWatch = true;
			throw e;
		});
}

/**
 * @private
 */
function hide() {
	this[popperElementSymbol].style.display = "none";
	setStatusOrRemoveBadges.call(this, "closed");
	removeAttributeToken(this[controlElementSymbol], "class", "open");
}

/**
 * @private
 */
function show() {
	if (this.getOption("disabled", undefined) === true) {
		return;
	}

	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
		return;
	}

	focusFilter.call(this);

	const lazyLoadFlag =
		this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;

	if (lazyLoadFlag === true) {
		this[lazyLoadDoneSymbol] = true;
		setStatusOrRemoveBadges.call(this, "loading");

		new Processing(200, () => {
			this.fetch()
				.then(() => {
					checkOptionState.call(this);
					requestAnimationFrame(() => {
						show.call(this);
					});
				})
				.catch((e) => {
					addErrorAttribute(this, e);
					setStatusOrRemoveBadges.call(this, "error");
				});
		})
			.run()
			.catch((e) => {
				addErrorAttribute(this, e);
				setStatusOrRemoveBadges.call(this, "error");
			});

		return;
	}

	const hasPopperFilterFlag =
		this.getOption("filter.position") === FILTER_POSITION_POPPER &&
		getFilterMode.call(this) !== FILTER_MODE_DISABLED;

	const options = getOptionElements.call(this);
	if (options.length === 0 && hasPopperFilterFlag === false) {
		return;
	}

	this[popperElementSymbol].style.visibility = "hidden";
	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
	setStatusOrRemoveBadges.call(this, "open");

	addAttributeToken(this[controlElementSymbol], "class", "open");

	new Processing(() => {
		calcAndSetOptionsDimension.call(this);
		focusFilter.call(this);
		this[popperElementSymbol].style.removeProperty("visibility");
		updatePopper.call(this);
	})
		.run()
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 */
function toggle() {
	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
		hide.call(this);
	} else {
		show.call(this);
	}
}

/**
 * @private
 * @fires monster-selection-removed
 * @fires monster-selection-cleared
 */
function initEventHandler() {
	const self = this;

	/**
	 * @param {Event} event
	 */
	self[clearOptionEventHandler] = (event) => {
		const element = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"remove-badge",
		);

		if (element instanceof HTMLElement) {
			const badge = findClosestByAttribute(element, ATTRIBUTE_ROLE, "badge");
			if (badge instanceof HTMLElement) {
				const value = badge.getAttribute(`${ATTRIBUTE_PREFIX}value`);

				let selection = self.getOption("selection");
				selection = selection.filter((b) => {
					return value !== b.value;
				});

				setSelection
					.call(self, selection)
					.then(() => {
						fireCustomEvent(self, "monster-selection-removed", {
							value,
						});
					})
					.catch((e) => {
						addErrorAttribute(self, e);
					});
			}
		}
	};

	/**
	 * @param {Event} event
	 */
	self[closeEventHandler] = (event) => {
		const path = event.composedPath();

		for (const [, element] of Object.entries(path)) {
			if (element === self) {
				return;
			}
		}
		hide.call(self);
	};

	/**
	 * @param {Event} event
	 */
	self[inputEventHandler] = (event) => {
		const path = event.composedPath();
		const element = path?.[0];

		if (element instanceof HTMLElement) {
			if (
				element.hasAttribute(ATTRIBUTE_ROLE) &&
				element.getAttribute(ATTRIBUTE_ROLE) === "option-control"
			) {
				fireCustomEvent(self, "monster-change", {
					type: event.type,
					value: element.value,
					checked: element.checked,
				});
			} else if (
				element.hasAttribute(ATTRIBUTE_ROLE) &&
				element.getAttribute(ATTRIBUTE_ROLE) === "filter"
			) {
			}
		}
	};

	/**
	 * @param {Event} event
	 */
	self[changeEventHandler] = (event) => {
		gatherState.call(self);
		fireCustomEvent(self, "monster-changed", event?.detail);
	};

	self[keyEventHandler] = (event) => {
		const path = event.composedPath();
		const element = path.shift();

		let role;

		if (element instanceof HTMLElement) {
			if (element.hasAttribute(ATTRIBUTE_ROLE)) {
				role = element.getAttribute(ATTRIBUTE_ROLE);
			} else if (element === this) {
				show.call(this);
				// focusFilter.call(self);
			} else {
				const e = element.closest(`[${ATTRIBUTE_ROLE}]`);
				if (e instanceof HTMLElement && e.hasAttribute(ATTRIBUTE_ROLE)) {
					role = e.getAttribute(ATTRIBUTE_ROLE);
				}
			}
		} else {
			return;
		}

		switch (role) {
			case "filter":
				handleFilterKeyboardEvents.call(self, event);
				break;
			case "option-label":
			case "option-control":
			case "option":
				handleOptionKeyboardEvents.call(self, event);
				break;
			case "control":
			case "toggle":
				handleToggleKeyboardEvents.call(self, event);
				break;
		}
	};

	const types = self.getOption("toggleEventType", ["click"]);

	for (const [, type] of Object.entries(types)) {
		self[controlElementSymbol]
			.querySelector(`[${ATTRIBUTE_ROLE}="container"]`)
			.addEventListener(type, function (event) {
				const element = findTargetElementFromEvent(
					event,
					ATTRIBUTE_ROLE,
					"remove-badge",
				);
				if (element instanceof HTMLElement) {
					return;
				}

				toggle.call(self);
			});

		self[controlElementSymbol]
			.querySelector(`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`)
			.addEventListener(type, function (event) {
				if (self.getOption("disabled", undefined) === true) {
					return;
				}

				const path = event.composedPath();
				const element = path?.[0];
				if (element instanceof HTMLElement) {
					const control = element.closest(
						`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`,
					);
					if (control instanceof HTMLElement) {
						if (control.classList.contains("clear")) {
							clearSelection.call(self);

							fireCustomEvent(self, "monster-selection-cleared", {});
						} else {
							const element = findTargetElementFromEvent(
								event,
								ATTRIBUTE_ROLE,
								"remove-badge",
							);
							if (element instanceof HTMLElement) {
								return;
							}

							toggle.call(self);
						}
					}
				}
			});

		// badge, selection
		self.addEventListener(type, self[clearOptionEventHandler]);
	}

	self.addEventListener("monster-change", self[changeEventHandler]);
	self.addEventListener("input", self[inputEventHandler]);
	self.addEventListener("keydown", self[keyEventHandler]);

	return self;
}

/**
 * @private
 * @return {Select}
 */
function setStatusOrRemoveBadges(suggestion) {
	requestAnimationFrame(() => {
		const selection = this.getOption("selection");

		const clearAllFlag =
			isArray(selection) &&
			selection.length > 0 &&
			this.getOption("features.clearAll") === true;

		const current = this.getOption("classes.statusOrRemoveBadge");

		if (suggestion === "error") {
			if (current !== "error") {
				this.setOption("classes.statusOrRemoveBadge", "error");
			}
			return;
		}

		if (this[isLoadingSymbol] === true) {
			if (current !== "loading") {
				this.setOption("classes.statusOrRemoveBadge", "loading");
			}
			return;
		}

		if (suggestion === "loading") {
			if (current !== "loading") {
				this.setOption("classes.statusOrRemoveBadge", "loading");
			}
			return;
		}

		if (clearAllFlag) {
			if (current !== "clear") {
				this.setOption("classes.statusOrRemoveBadge", "clear");
			}
			return;
		}

		if (this[controlElementSymbol].classList.contains("open")) {
			if (current !== "open") {
				this.setOption("classes.statusOrRemoveBadge", "open");
			}
			return;
		}

		const options = this.getOption("options");
		if (
			options === undefined ||
			options === null ||
			(isArray(options) && options.length === 0)
		) {
			if (current !== "empty") {
				this.setOption("classes.statusOrRemoveBadge", "empty");
			}
			return;
		}

		if (suggestion) {
			if (current !== suggestion) {
				this.setOption("classes.statusOrRemoveBadge", suggestion);
			}
			return;
		}
	});
}

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

	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=control]`,
	);
	this[selectionElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=selection]`,
	);
	this[containerElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=container]`,
	);
	this[popperElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper]`,
	);
	this[inlineFilterElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=filter][name="inline-filter"]`,
	);
	this[popperFilterElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=filter][name="popper-filter"]`,
	);
	this[popperFilterContainerElementSymbol] =
		this[popperFilterElementSymbol].parentElement;
	this[optionsElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=options]`,
	);
	this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="no-options"]`,
	);
	this[statusOrRemoveBadgesElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=status-or-remove-badges]`,
	);
}

/**
 * @private
 */
function updatePopper() {
	if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) {
		return;
	}

	if (this.getOption("disabled", false) === true) {
		return;
	}

	new Processing(() => {
		calcAndSetOptionsDimension.call(this);
		positionPopper.call(
			this,
			this[controlElementSymbol],
			this[popperElementSymbol],
			this.getOption("popper", {}),
		);
	})
		.run()
		.catch((e) => {
			addErrorAttribute(this, e);
		});

	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="options">
            <div data-monster-role="option" tabindex="-1"
                 data-monster-attributes="
                 data-monster-filtered path:options.filtered,
                 data-monster-visibility path:options.visibility">
                <label part="option">
                    <input data-monster-role="option-control"
                           data-monster-attributes="
            type path:type,
            role path:role,
            value path:options | index:value, 
            name path:name, 
            part path:type | prefix:option- | suffix: form,
            class path:options.class 
            " tabindex="-1">
                    <div data-monster-replace="path:options | index:label"
                         part="option-label"></div>
                </label>
            </div>
        </template>

        <template id="selection">
            <div data-monster-role="badge"
                 part="badge"
                 data-monster-attributes="
                 data-monster-value path:selection | index:value, 
                 class path:classes | index:badge, 
        part path:type | suffix:-option | prefix: form-" tabindex="-1">
                <div data-monster-replace="path:selection | index:label" part="badge-label"
                     data-monster-role="badge-label"></div>
                <div part="remove-badge" data-monster-select-this
                     data-monster-attributes="class path:features.clear | ?::hidden "
                     data-monster-role="remove-badge" tabindex="-1"></div>
            </div>
        </template>

        <slot class="hidden"></slot>

        <div data-monster-role="control" part="control" tabindex="0">
            <div data-monster-role="container">
                \${selected}
            </div>

            <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
                <div class="option-filter-control" role="search">
                    <input type="text" role="searchbox"
                           part="popper-filter" name="popper-filter"
                           data-monster-role="filter"
                           autocomplete="off"
                           tabindex="0">
                </div>
                <div part="content" class="flex" data-monster-replace="path:content">
                    <div part="options" data-monster-role="options" data-monster-insert="options path:options"
                         tabindex="-1"></div>
                </div>
                <div part="no-options" data-monster-role="no-options"
                     data-monster-replace="path:messages.emptyOptions"></div>
            </div>
            <div part="status-or-remove-badges" data-monster-role="status-or-remove-badges"
                 data-monster-attributes="class path:classes.statusOrRemoveBadge"></div>
        </div>
    `;
}

registerCustomElement(Select);