/**
 * 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_ERRORMESSAGE,
    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, getWindow} 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";

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

/**
 * @private
 * @type {string}
 */
const noOptionsAvailableMessage = "No options available.";

/**
 * @private
 * @type {string}
 */
const clickToLoadOptionsMessage = "Click to load options.";

/**
 * @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.
 *
 * @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) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
            });
    }

    /**
     * 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: {
                    "cannot-be-loaded": "Cannot be loaded",
                    "no-options-available": noOptionsAvailableMessage,
                    "click-to-load-options": clickToLoadOptionsMessage,
                    "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.",
                },
                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) {
                addAttributeToken(
                    this,
                    ATTRIBUTE_ERRORMESSAGE,
                    "lazyLoad is not supported with remote filter",
                );
                lazyLoadFlag = false;
            }
        }

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

        if (self.getOption("url") !== null) {
            if (lazyLoadFlag || remoteFilterFlag) {
                lookupSelection.call(self);
            } else {
                self.fetch().catch((e) => {
                    addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${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) => {
                                addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${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) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${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) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${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) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${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 === "") {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template");
            flag = true;
        }

        if (valueTemplate === "") {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "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,
        });

        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
 */
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) => {
                    addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${e}`);
                });
            return;
        }

        for (const s of selection) {
            if (s?.["value"]) {
                filterFromRemoteByValue.call(self, url, s?.["value"]).catch((e) => {
                    addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${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");
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
                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:
                addAttributeToken(
                    this,
                    ATTRIBUTE_ERRORMESSAGE,
                    "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);
}

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

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>`;
}

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) {
                    addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message);
                    		requestAnimationFrame(() => {
                    setStatusOrRemoveBadges.call(self, "error");
                    		});
                }
                try {
                    areOptionsAvailableAndInit.call(self);
                } catch (e) {
                    addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message);
                    		requestAnimationFrame(() => {
                    setStatusOrRemoveBadges.call(self, "error");
                    		});
                }

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

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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        });
}

/**
 * @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) => {
                    addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
                });
        })
        .catch((e) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        });
}

/**
 * @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) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
            });
        }

        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) {
        addAttributeToken(
            this,
            ATTRIBUTE_ERRORMESSAGE,
            "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)) {
        addAttributeToken(
            this,
            ATTRIBUTE_ERRORMESSAGE,
            "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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${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 = "";
        }
        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage);
        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);

    removeAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage);
    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)) {
        const result = convertValueToSelection.call(this, selection);
        selection = result?.selection;
    } else if (selection === undefined) {
        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) {
        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
    }

    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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        });
}

/**
 * @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) => {
                    addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
                    setStatusOrRemoveBadges.call(this, "error");
                });
        })
            .run()
            .catch((e) => {
                addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
                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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        });
}

/**
 * @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) => {
                        addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message);
                    });
            }
        }
    };

    /**
     * @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) => {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        });

    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);