/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { findElementWithIdUpwards, getDocument, getWindow } from "./util.mjs";
import { internalSymbol } from "../constants.mjs";
import { extend } from "../data/extend.mjs";
import { Pathfinder } from "../data/pathfinder.mjs";
import { Formatter } from "../text/formatter.mjs";

import { parseDataURL } from "../types/dataurl.mjs";
import { getGlobalObject } from "../types/global.mjs";
import {
	isArray,
	isElement,
	isFunction,
	isIterable,
	isObject,
	isString,
} from "../types/is.mjs";
import { Observer } from "../types/observer.mjs";
import { ProxyObserver } from "../types/proxyobserver.mjs";
import {
	validateFunction,
	validateInstance,
	validateObject,
} from "../types/validate.mjs";
import { clone } from "../util/clone.mjs";
import { getLinkedObjects, hasObjectLink } from "./attributes.mjs";
import {
	ATTRIBUTE_DISABLED,
	ATTRIBUTE_OPTIONS,
	ATTRIBUTE_INIT_CALLBACK,
	ATTRIBUTE_OPTIONS_SELECTOR,
	ATTRIBUTE_SCRIPT_HOST,
	customElementUpdaterLinkSymbol,
	initControlCallbackName,
} from "./constants.mjs";
import { findDocumentTemplate, Template } from "./template.mjs";
import { addObjectWithUpdaterToElement } from "./updater.mjs";
import { instanceSymbol } from "../constants.mjs";
import { getDocumentTranslations } from "../i18n/translations.mjs";
import { getSlottedElements } from "./slotted.mjs";
import { initOptionsFromAttributes } from "./util/init-options-from-attributes.mjs";
import { setOptionFromAttribute } from "./util/set-option-from-attribute.mjs";
import { addErrorAttribute } from "./error.mjs";

export {
	CustomElement,
	initMethodSymbol,
	assembleMethodSymbol,
	attributeObserverSymbol,
	registerCustomElement,
	getSlottedElements,
	updaterTransformerMethodsSymbol,
};

/**
 * @type {symbol}
 */
const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol");

/**
 * @type {symbol}
 */
const assembleMethodSymbol = Symbol.for(
	"@schukai/monster/dom/@@assembleMethodSymbol",
);

/**
 * @type {symbol}
 */
const updaterTransformerMethodsSymbol = Symbol.for(
	"@schukai/monster/dom/@@updaterTransformerMethodsSymbol",
);

/**
 * this symbol holds the attribute observer callbacks. The key is the attribute name.
 * @type {symbol}
 */
const attributeObserverSymbol = Symbol.for(
	"@schukai/monster/dom/@@attributeObserver",
);

/**
 * @private
 * @type {symbol}
 */
const attributeMutationObserverSymbol = Symbol(
	"@schukai/monster/dom/@@mutationObserver",
);
const childMutationObserverSymbol = Symbol(
	"@schukai/monster/dom/@@childMutationObserver",
);
const childMutationTimerSymbol = Symbol(
	"@schukai/monster/dom/@@childMutationTimer",
);

/**
 * @private
 * @type {symbol}
 */
const updateCloneDataSymbol = Symbol("@schukai/monster/dom/@@updateCloneData");

/**
 * @private
 * @type {symbol}
 */
const scriptHostElementSymbol = Symbol("scriptHostElement");
const managedShadowRootSymbol = Symbol("managedShadowRoot");
const visibilityStateSymbol = Symbol("visibilityState");
let hostVisibilityStyleSheet = null;

/**
 * The `CustomElement` class provides a way to define a new HTML element using the power of Custom Elements.
 *
 * **IMPORTANT:** After defining a `CustomElement`, the `registerCustomElement` method must be called with the new class name
 * to make the tag defined via the `getTag` method known to the DOM.
 *
 * You can create an instance of the object via the `document.createElement()` function.
 *
 * ## Styling
 *
 * To display custom elements optimally, the `:defined` pseudo-class can be used. To prevent custom elements from being displayed and flickering until the control is registered,
 * it is recommended to create a CSS directive.
 *
 * In the simplest case, you can simply hide the control:
 *
 * ```html
 * <style>
 * my-custom-element:not(:defined) {
 *     display: none;
 * }
 *
 * my-custom-element:defined {
 *     display: flex;
 * }
 * </style>
 * ```
 *
 * Alternatively, you can display a loader:
 *
 * ```css
 * my-custom-element:not(:defined) {
 *     display: flex;
 *     box-shadow: 0 4px 10px 0 rgba(33, 33, 33, 0.15);
 *     border-radius: 4px;
 *     height: 200px;
 *     position: relative;
 *     overflow: hidden;
 * }
 *
 * my-custom-element:not(:defined)::before {
 *     content: '';
 *     display: block;
 *     position: absolute;
 *     left: -150px;
 *     top: 0;
 *     height: 100%;
 *     width: 150px;
 *     background: linear-gradient(to right, transparent 0%, #E8E8E8 50%, transparent 100%);
 *     animation: load 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
 * }
 *
 * @keyframes load {
 *     from {
 *         left: -150px;
 *     }
 *     to {
 *         left: 100%;
 *     }
 * }
 *
 * my-custom-element:defined {
 *     display: flex;
 * }
 * ```
 *
 * More information about Custom Elements can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements).
 * And in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements).
 *
 * @fragments /fragments/libraries/dom/customelement/
 * @example /examples/libraries/dom/customelement/simple-card/ Register a minimal element with inline template and stylesheet
 * @example /examples/libraries/dom/customelement/options-from-attributes/ Read declarative options from data-monster-options
 * @example /examples/libraries/dom/customelement/runtime-option-updates/ Update component state via setOption and setOptions
 * @example /examples/libraries/dom/customelement/document-template/ Resolve the template from the surrounding document
 * @example /examples/libraries/dom/customelement/visibility-events/ React to show hide and visibility events
 *
 * @license AGPLv3
 * @since 1.7.0
 * @copyright Volker Schukai
 * @summary The core base class for building Monster custom elements with options, lifecycle hooks and templating support.
 */
class CustomElement extends HTMLElement {
	/**
	 * A new object is created. First, the `initOptions` method is called. Here the
	 * options can be defined in derived classes. Subsequently, the shadowRoot is initialized.
	 *
	 * IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via <code>document.createElement()</code>.
	 *
	 * @throws {Error} the option attribute does not contain a valid JSON definition.
	 */
	constructor() {
		super();

		this[attributeObserverSymbol] = {};

		const options = initOptionsFromAttributes(this, extend({}, this.defaults));
		if (!isObject(options)) {
			throw new Error(
				`The options are not defined correctly in the ${this.getTag()} element.`,
			);
		}

		if (this.customization instanceof Map) {
			const pathfinder = new Pathfinder(options);
			this.customization.forEach((value, key) => {
				if (key === undefined || key === null || key === "") {
					return;
				}
				pathfinder.setVia(key, value);
			});
		}

		this[internalSymbol] = new ProxyObserver({ options });
		this[initMethodSymbol]();
		initOptionObserver.call(this);
		this[scriptHostElementSymbol] = [];
		// Catch attribute changes made before connectedCallback runs.
		attachAttributeChangeMutationObserver.call(this);
		attachChildMutationObserver.call(this);
	}

	/**
	 * This method is called by the `instanceof` operator.
	 *
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/dom/custom-element@@instance");
	}

	/**
	 * This method determines which attributes are to be
	 * monitored by `attributeChangedCallback()`. Unfortunately, this method is static.
	 * Therefore, the `observedAttributes` property cannot be changed during runtime.
	 *
	 * @return {string[]}
	 * @since 1.15.0
	 */
	static get observedAttributes() {
		return [];
	}

	/**
	 *
	 * @param attribute
	 * @param callback
	 * @return {CustomElement}
	 */
	addAttributeObserver(attribute, callback) {
		validateFunction(callback);
		this[attributeObserverSymbol][attribute] = callback;
		return this;
	}

	/**
	 *
	 * @param attribute
	 * @return {CustomElement}
	 */
	removeAttributeObserver(attribute) {
		delete this[attributeObserverSymbol][attribute];
		return this;
	}

	/**
	 * Returns whether the host element is currently visible.
	 *
	 * @returns {boolean}
	 */
	get visible() {
		return !this.hidden;
	}

	/**
	 * Alias for the current host visibility state.
	 *
	 * @returns {boolean}
	 */
	get isVisible() {
		return this.visible;
	}

	/**
	 * The `customization` property allows overwriting the defaults.
	 * Unlike the defaults that expect an object, the customization is a Map.
	 * This also allows overwriting individual values in a deeper structure
	 * without having to redefine the entire structure and thus changing the defaults.
	 * @returns {Map}
	 */
	get customization() {
		return new Map();
	}

	/**
	 * The `defaults` property defines the default values for a control. If you want to override these,
	 * you can use various methods, which are described in the documentation available at
	 * {@link https://monsterjs.orgendocconfigurate-a-monster-control}.
	 *
	 * The individual configuration values are listed below:
	 *
	 * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow),
	 * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements).
	 *
	 * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
	 *
	 * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).
	 *
	 * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form.
	 * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it.
	 * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling.
	 * @property {Object} templates Specifies the templates used by the control.
	 * @property {string} templates.main=undefined Specifies the main template used by the control.
	 * @property {Object} templateMapping Specifies the mapping of templates.
	 * @property {Object} templateFormatter Specifies the formatter for the templates.
	 * @property {Object} templateFormatter.marker Specifies the marker for the templates.
	 * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates.
	 * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates.
	 * @property {Boolean} templateFormatter.i18n=false Specifies whether the templates should be formatted with i18n.
	 * @property {Boolean} eventProcessing=false Specifies whether the control processes events.
	 * @property {Object} updater Specifies updater options.
	 * @property {Boolean} updater.batchUpdates=false Batches updater content/attribute updates per diff.
	 * @since 1.8.0
	 */
	get defaults() {
		return {
			disabled: false,
			shadowMode: "open",
			delegatesFocus: true,
			templates: {
				main: undefined,
			},
			templateMapping: {},
			templateFormatter: {
				marker: {
					open: null,
					close: null,
				},
				i18n: false,
			},

			eventProcessing: false,
			updater: {
				batchUpdates: false,
			},
		};
	}

	/**
	 * This method updates the labels of the element.
	 * The labels are defined in the option object.
	 * The key of the label is used to retrieve the translation from the document.
	 * If the translation is different from the label, the label is updated.
	 *
	 * Before you can use this method, you must have loaded the translations.
	 *
	 * @return {CustomElement}
	 * @throws {Error}  Cannot find an element with translations. Add a translation object to the document.
	 */
	updateI18n() {
		let translations;

		try {
			translations = getDocumentTranslations();
		} catch (e) {
			addErrorAttribute(this, e);
			return this;
		}

		if (!translations) {
			return this;
		}

		const labels = this.getOption("labels");
		if (!(isObject(labels) || isIterable(labels))) {
			return this;
		}

		for (const key in labels) {
			const def = labels[key];

			if (isString(def)) {
				const text = translations.getText(key, def);
				if (text !== def) {
					this.setOption(`labels.${key}`, text);
				}
				continue;
			} else if (isObject(def)) {
				for (const k in def) {
					const d = def[k];

					const text = translations.getPluralRuleText(key, k, d);
					if (!isString(text)) {
						throw new Error("Invalid labels definition");
					}
					if (text !== d) {
						this.setOption(`labels.${key}.${k}`, text);
					}
				}
				continue;
			}

			throw new Error("Invalid labels definition");
		}
		return this;
	}

	/**
	 * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten
	 * by the derived class.
	 *
	 * Note that there is no check on the name of the tag in this class. It is the responsibility of
	 * the developer to assign an appropriate tag name. If the name is not valid, the
	 * `registerCustomElement()` method will issue an error.
	 *
	 * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
	 * @throws {Error} This method must be overridden by the derived class.
	 * @return {string} The tag name associated with the custom element.
	 * @since 1.7.0
	 */
	static getTag() {
		throw new Error(
			"The method `getTag()` must be overridden by the derived class.",
		);
	}

	/**
	 * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element.
	 * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour.
	 *
	 * If `undefined` is returned, then the shadow root does not receive a stylesheet.
	 *
	 * Example usage:
	 *
	 * ```js
	 * class MyElement extends CustomElement {
	 *   static getCSSStyleSheet() {
	 *       const sheet = new CSSStyleSheet();
	 *       sheet.replaceSync("p { color: red; }");
	 *       return sheet;
	 *   }
	 * }
	 * ```
	 *
	 * If the environment does not support the `CSSStyleSheet` constructor,
	 * you can use the following workaround to create the stylesheet:
	 *
	 * ```js
	 * const doc = document.implementation.createHTMLDocument('title');
	 * let style = doc.createElement("style");
	 * style.innerHTML = "p { color: red; }";
	 * style.appendChild(document.createTextNode(""));
	 * doc.head.appendChild(style);
	 * return doc.styleSheets[0];
	 * ```
	 *
	 * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied.
	 */
	static getCSSStyleSheet() {
		return undefined;
	}

	/**
	 * attach a new observer
	 *
	 * @param {Observer} observer
	 * @return {CustomElement}
	 */
	attachObserver(observer) {
		this[internalSymbol].attachObserver(observer);
		return this;
	}

	/**
	 * detach a observer
	 *
	 * @param {Observer} observer
	 * @return {CustomElement}
	 */
	detachObserver(observer) {
		this[internalSymbol].detachObserver(observer);
		return this;
	}

	/**
	 * @param {Observer} observer
	 * @return {ProxyObserver}
	 */
	containsObserver(observer) {
		return this[internalSymbol].containsObserver(observer);
	}

	/**
	 * nested options can be specified by path `a.b.c`
	 *
	 * @param {string} path
	 * @param {*} defaultValue
	 * @return {*}
	 * @since 1.10.0
	 */
	getOption(path, defaultValue = undefined) {
		let value;

		try {
			value = new Pathfinder(
				this[internalSymbol].getRealSubject()["options"],
			).getVia(path);
		} catch (e) {}

		if (value === undefined) return defaultValue;
		return value;
	}

	/**
	 * Set option and inform elements
	 *
	 * @param {string} path
	 * @param {*} value
	 * @return {CustomElement}
	 * @since 1.14.0
	 */
	setOption(path, value) {
		new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia(
			path,
			value,
		);
		return this;
	}

	/**
	 * @since 1.15.0
	 * @param {string|object} options
	 * @return {CustomElement}
	 */
	setOptions(options) {
		if (isString(options)) {
			options = parseOptionsJSON.call(this, options);
		}
		// 2024-01-21: remove this.defaults, otherwise it will overwrite
		// the current settings that have already been made.
		// https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136
		extend(this[internalSymbol].getSubject()["options"], options);

		return this;
	}

	/**
	 * Shows the host element.
	 *
	 * @returns {CustomElement}
	 */
	show() {
		return this.setVisible(true);
	}

	/**
	 * Hides the host element.
	 *
	 * @returns {CustomElement}
	 */
	hide() {
		return this.setVisible(false);
	}

	/**
	 * Sets the host visibility.
	 *
	 * @param {boolean} visible
	 * @returns {CustomElement}
	 */
	setVisible(visible) {
		if (visible) {
			this.removeAttribute("hidden");
		} else {
			this.setAttribute("hidden", "");
		}

		return this;
	}

	/**
	 * Toggles the host visibility.
	 *
	 * @param {boolean} [force]
	 * @returns {CustomElement}
	 */
	toggleVisibility(force) {
		if (typeof force === "boolean") {
			return this.setVisible(force);
		}

		return this.setVisible(!this.visible);
	}

	/**
	 * Is called once via the constructor
	 *
	 * @return {CustomElement}
	 * @since 1.8.0
	 */
	[initMethodSymbol]() {
		return this;
	}

	/**
	 * This method is called once when the object is equipped with update for the dynamic change of the dom.
	 * The functions returned here can be used as pipe functions in the template.
	 *
	 * In the example, the function `my-transformer` is defined. In the template, you can use it as follows:
	 *
	 * ```html
	 * <my-element
	 *   data-monster-option-transformer="path:my-value | call:my-transformer">
	 * </my-element>
	 * ```
	 *
	 * The function `my-transformer` is called with the value of `my-value` as a parameter.
	 *
	 * ```js
	 * class MyElement extends CustomElement {
	 * [updaterTransformerMethodsSymbol]() {
	 *    return {
	 *       "my-transformer": (value) => {
	 *           switch (typeof Wert) {
	 *           case "string":
	 *               return value + "!";
	 *           case "Zahl":
	 *               return value + 1;
	 *           default:
	 *               return value;
	 *           }
	 *    }
	 *    };
	 *  };
	 *  }
	 * ```
	 *
	 * @return {object}
	 * @since 2.43.0
	 */
	[updaterTransformerMethodsSymbol]() {
		return {};
	}

	/**
	 * This method is called once when the object is included in the DOM for the first time. It performs the following actions:
	 *
	 * <ol>
	 * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li>
	 * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li>
	 * <li>Initializes the HTML content of the element.</li>
	 * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li>
	 * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li>
	 *
	 * @return {CustomElement} - The updated custom element.
	 * @since 1.8.0
	 */
	[assembleMethodSymbol]() {
		let elements;
		let nodeList;
		// Extract options from attributes and set them
		const AttributeOptions = getOptionsFromAttributes.call(this);
		if (
			isObject(AttributeOptions) &&
			Object.keys(AttributeOptions).length > 0
		) {
			this.setOptions(AttributeOptions);
		}
		// Extract options from script tag and set them
		const ScriptOptions = getOptionsFromScriptTag.call(this);
		if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) {
			this.setOptions(ScriptOptions);
		}
		// Initialize the shadow root and its CSS stylesheet
		if (this.getOption("shadowMode", false) !== false) {
			try {
				initShadowRoot.call(this);
				elements = getManagedShadowRoot.call(this)?.childNodes;
			} catch (e) {
				addErrorAttribute(this, e);
			}
			try {
				initCSSStylesheet.call(this);
			} catch (e) {
				addErrorAttribute(this, e);
			}
		}
		// If the elements are not found inside the shadow root, initialize the HTML content of the element
		if (!(elements instanceof NodeList)) {
			initHtmlContent.call(this);
			elements = this.childNodes;
		}
		// Initialize the custom elements inside the shadow root and the slotted elements
		initFromCallbackHost.call(this);
		try {
			nodeList = new Set([...elements, ...getSlottedElements.call(this)]);
		} catch (e) {
			nodeList = elements;
		}
		try {
			this[updateCloneDataSymbol] = clone(
				this[internalSymbol].getRealSubject()["options"],
			);
		} catch (e) {
			addErrorAttribute(this, e);
		}
		const cfg = {};
		if (this.getOption("eventProcessing") === true) {
			cfg.eventProcessing = true;
		}
		if (this.getOption("updater.batchUpdates") === true) {
			cfg.batchUpdates = true;
		}
		addObjectWithUpdaterToElement.call(
			this,
			nodeList,
			customElementUpdaterLinkSymbol,
			this[updateCloneDataSymbol],
			cfg,
		);
		// Attach a mutation observer to observe changes to the attributes of the element
		attachAttributeChangeMutationObserver.call(this);

		if (isFunction(this[internalSymbol]?.syncDisabledState)) {
			this[internalSymbol].syncDisabledState();
		}

		syncVisibilityState.call(this);

		return this;
	}

	/**
	 * You know what you are doing? This function is only for advanced users.
	 * The result is a clone of the internal data.
	 *
	 * @return {*}
	 */
	getInternalUpdateCloneData() {
		return clone(this[updateCloneDataSymbol]);
	}

	/**
	 * This method is called every time the element is inserted into the DOM. It checks if the custom element
	 * has already been initialized and if not, calls the assembleMethod to initialize it.
	 *
	 * @return {void}
	 * @since 1.7.0
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback
	 */
	connectedCallback() {
		// Check if the object has already been initialized
		if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) {
			try {
				initOptionsFromAttributes(
					this,
					this[internalSymbol].getSubject()?.["options"],
				);
			} catch (e) {
				addErrorAttribute(this, e);
			}
			// If not, call the assembleMethod to initialize the object
			this[assembleMethodSymbol]();
		}
	}

	/**
	 * Called every time the element is removed from the DOM. Useful for running clean up code.
	 *
	 * @return {void}
	 * @since 1.7.0
	 */
	disconnectedCallback() {}

	/**
	 * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)).
	 *
	 * @return {void}
	 * @since 1.7.0
	 */
	adoptedCallback() {}

	/**
	 * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial
	 * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes
	 * property will receive this callback.
	 *
	 * @param {string} attrName
	 * @param {string} oldVal
	 * @param {string} newVal
	 * @return {void}
	 * @since 1.15.0
	 */
	attributeChangedCallback(attrName, oldVal, newVal) {
		if (attrName.startsWith("data-monster-option-")) {
			setOptionFromAttribute(
				this,
				attrName,
				this[internalSymbol].getSubject()["options"],
			);
		}

		if (attrName === "hidden") {
			syncVisibilityState.call(this);
		}

		const callback = this[attributeObserverSymbol]?.[attrName];
		if (isFunction(callback)) {
			try {
				callback.call(this, newVal, oldVal);
			} catch (e) {
				addErrorAttribute(this, e);
			}
		}
	}

	/**
	 * Checks if the provided node is part of this component's child nodes,
	 * including those within the shadow root, if present.
	 *
	 * @param {Node} node - The node to check for within this component's child nodes.
	 * @return {boolean} Returns true if the given node is found, otherwise false.
	 * @throws {TypeError} value is not an instance of
	 * @since 1.19.0
	 */
	hasNode(node) {
		if (containChildNode.call(this, validateInstance(node, Node))) {
			return true;
		}

		const shadowRoot = getManagedShadowRoot.call(this);
		if (!(shadowRoot instanceof ShadowRoot)) {
			return false;
		}

		return containChildNode.call(shadowRoot, node);
	}

	/**
	 * Invokes a callback function with the given name and arguments.
	 *
	 * @param {string} name - The name of the callback to be executed.
	 * @param {Array} args - An array of arguments to be passed to the callback function.
	 * @return {*} The result of the callback function execution.
	 */
	callCallback(name, args) {
		return callControlCallback.call(this, name, ...args);
	}
}

/**
 * @param {string} callBackFunctionName
 * @param {*}  args
 * @return {any}
 */
function callControlCallback(callBackFunctionName, ...args) {
	if (!isString(callBackFunctionName) || callBackFunctionName === "") {
		return;
	}

	if (callBackFunctionName in this) {
		return this[callBackFunctionName](this, ...args);
	}

	if (!this.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) {
		return;
	}

	if (this[scriptHostElementSymbol].length === 0) {
		const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST);
		if (!targetId) {
			return;
		}

		const list = targetId.split(",");
		for (const id of list) {
			const host = findElementWithIdUpwards(this, id.trim());
			if (!(host instanceof HTMLElement)) {
				continue;
			}

			this[scriptHostElementSymbol].push(host);
		}
	}

	for (const host of this[scriptHostElementSymbol]) {
		if (callBackFunctionName in host) {
			try {
				return host[callBackFunctionName](this, ...args);
			} catch (e) {
				addErrorAttribute(this, e);
			}
		}
	}

	addErrorAttribute(this, `callback ${callBackFunctionName} not found`);
}

/**
 * Initializes the custom element based on the provided callback function.
 *
 * This function is called when the element is attached to the DOM. It checks if the
 * `data-monster-option-callback` attribute is set, and if not, the default callback
 * `initCustomControlCallback` is called. The callback function is searched for in this
 * element and in the host element. If the callback is found, it is called with the element
 * as a parameter.
 *
 * @this CustomElement
 * @see https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#providing_a_construction_callback
 * @since 1.8.0
 */
function initFromCallbackHost() {
	// Set the default callback function name
	let callBackFunctionName = initControlCallbackName;

	// If the `data-monster-option-callback` attribute is set, use its value as the callback function name
	if (this.hasAttribute(ATTRIBUTE_INIT_CALLBACK)) {
		callBackFunctionName = this.getAttribute(ATTRIBUTE_INIT_CALLBACK);
	}

	// Call the callback function with the element as a parameter if it exists
	callControlCallback.call(this, callBackFunctionName);
}

/**
 * This method is called when the element is first created.
 *
 * @private
 * @this CustomElement
 */
function attachAttributeChangeMutationObserver() {
	const self = this;

	if (typeof self[attributeMutationObserverSymbol] !== "undefined") {
		return;
	}

	self[attributeMutationObserverSymbol] = new MutationObserver(
		function (mutations, observer) {
			const observedAttributes = new Set(
				self.constructor.observedAttributes || [],
			);
			for (const mutation of mutations) {
				if (mutation.type === "attributes") {
					if (observedAttributes.has(mutation.attributeName)) {
						continue;
					}
					self.attributeChangedCallback(
						mutation.attributeName,
						mutation.oldValue,
						mutation.target.getAttribute(mutation.attributeName),
					);
				}
			}
		},
	);

	try {
		self[attributeMutationObserverSymbol].observe(self, {
			attributes: true,
			attributeOldValue: true,
		});
	} catch (e) {
		addErrorAttribute(self, e);
	}
}

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

	if (self[childMutationObserverSymbol]) {
		return;
	}

	if (self.getOption("features.mutationObserver") !== true) {
		return;
	}

	self[childMutationObserverSymbol] = new MutationObserver((mutations) => {
		let hasAdditions = false;
		for (const mutation of mutations) {
			if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
				hasAdditions = true;
				break;
			}
		}

		if (!hasAdditions) {
			return;
		}

		if (self[childMutationTimerSymbol]) {
			return;
		}

		self[childMutationTimerSymbol] = setTimeout(() => {
			self[childMutationTimerSymbol] = null;
			if (!self.isConnected) {
				return;
			}

			if (!hasObjectLink(self, customElementUpdaterLinkSymbol)) {
				return;
			}

			const updaters = getLinkedObjects(self, customElementUpdaterLinkSymbol);
			for (const list of updaters) {
				for (const updater of list) {
					if (
						typeof updater?.isDisposed === "function" &&
						updater.isDisposed() === true
					) {
						continue;
					}

					if (!updater?.[internalSymbol]?.element?.isConnected) {
						continue;
					}

					updater.run().catch((e) => {
						addErrorAttribute(self, e);
					});
				}
			}
		}, 50);
	});

	try {
		self[childMutationObserverSymbol].observe(self, {
			childList: true,
			subtree: true,
		});
	} catch (e) {
		addErrorAttribute(self, e);
	}
}

/**
 * @this CustomElement
 * @private
 * @param {Node} node
 * @return {boolean}
 */
function containChildNode(node) {
	if (this.contains(node)) {
		return true;
	}

	for (const [, e] of Object.entries(this.childNodes)) {
		if (e.contains(node)) {
			return true;
		}

		containChildNode.call(e, node);
	}

	return false;
}

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

	self[internalSymbol].lastDisabledValue = undefined;

	const syncDisabledState = () => {
		const flag = self.getOption("disabled", false);

		if (flag === self[internalSymbol].lastDisabledValue) {
			return;
		}

		const shadowRoot = getManagedShadowRoot.call(self);
		if (!(shadowRoot instanceof ShadowRoot) && !self.childNodes.length) {
			return;
		}

		self[internalSymbol].lastDisabledValue = flag;

		const query =
			"button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]";

		let elements = [];
		if (shadowRoot instanceof ShadowRoot) {
			elements = shadowRoot.querySelectorAll(query);
		}

		let nodeList;
		try {
			const baseElements =
				elements.length > 0 ? elements : self.querySelectorAll(query);
			nodeList = new Set([
				...baseElements,
				...getSlottedElements.call(self, query),
			]);
		} catch (e) {
			nodeList = elements;
		}

		for (const element of [...nodeList]) {
			if (flag === true) {
				element.setAttribute(ATTRIBUTE_DISABLED, "");
			} else {
				element.removeAttribute(ATTRIBUTE_DISABLED);
			}
		}
	};

	self.attachObserver(new Observer(syncDisabledState));

	self[internalSymbol].syncDisabledState = syncDisabledState;

	self.attachObserver(
		new Observer(function () {
			if (!hasObjectLink(self, customElementUpdaterLinkSymbol)) {
				return;
			}
			const updaters = getLinkedObjects(self, customElementUpdaterLinkSymbol);

			for (const list of updaters) {
				for (const updater of list) {
					const d = clone(self[internalSymbol].getRealSubject()["options"]);
					syncUpdaterSubject(updater.getSubject(), d);
				}
			}
		}),
	);

	self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
		if (self.hasAttribute(ATTRIBUTE_DISABLED)) {
			self.setOption(ATTRIBUTE_DISABLED, true);
		} else {
			self.setOption(ATTRIBUTE_DISABLED, false);
		}
	};

	self[attributeObserverSymbol][ATTRIBUTE_OPTIONS] = () => {
		const options = getOptionsFromAttributes.call(self);
		if (isObject(options) && Object.keys(options).length > 0) {
			self.setOptions(options);
		}
	};

	self[attributeObserverSymbol][ATTRIBUTE_OPTIONS_SELECTOR] = () => {
		const options = getOptionsFromScriptTag.call(self);
		if (isObject(options) && Object.keys(options).length > 0) {
			self.setOptions(options);
		}
	};
}

/**
 * @private
 * @param {object} target
 * @param {object} source
 * @return {void}
 */
function syncUpdaterSubject(target, source) {
	if (!isObject(source)) {
		return;
	}

	for (const [key, value] of Object.entries(source)) {
		if (
			isElement(value) ||
			(typeof Document !== "undefined" && value instanceof Document) ||
			(typeof DocumentFragment !== "undefined" &&
				value instanceof DocumentFragment)
		) {
			target[key] = value;
			continue;
		}

		if (isArray(value)) {
			if (!isArray(target?.[key])) {
				target[key] = [];
			}
			target[key].length = 0;
			target[key].push(...clone(value));
			continue;
		}

		if (isObject(value)) {
			const proto = Object.getPrototypeOf(value);
			if (proto && proto !== Object.prototype) {
				target[key] = value;
				continue;
			}
			if (!isObject(target?.[key]) || isArray(target?.[key])) {
				target[key] = {};
			}
			syncUpdaterSubject(target[key], value);
			continue;
		}

		target[key] = value;
	}
}

/**
 * @private
 * @return {object}
 * @throws {TypeError} value is not an object
 */
function getOptionsFromScriptTag() {
	if (!this.hasAttribute(ATTRIBUTE_OPTIONS_SELECTOR)) {
		return {};
	}

	const node = document.querySelector(
		this.getAttribute(ATTRIBUTE_OPTIONS_SELECTOR),
	);
	if (!(node instanceof HTMLScriptElement)) {
		addErrorAttribute(
			this,
			`the selector ${ATTRIBUTE_OPTIONS_SELECTOR} for options was specified (${this.getAttribute(
				ATTRIBUTE_OPTIONS_SELECTOR,
			)}) but not found.`,
		);
		return {};
	}

	let obj = {};

	try {
		obj = parseOptionsJSON.call(this, node.textContent.trim());
	} catch (e) {
		addErrorAttribute(
			this,
			`when analyzing the configuration from the script tag there was an error. ${e}`,
		);
	}

	return obj;
}

/**
 * @private
 * @return {object}
 */
function getOptionsFromAttributes() {
	if (
		this.hasAttribute(ATTRIBUTE_DISABLED) &&
		this.getAttribute(ATTRIBUTE_DISABLED) !== null
	) {
		this.setOption(ATTRIBUTE_DISABLED, true);
	}

	if (this.hasAttribute(ATTRIBUTE_OPTIONS)) {
		try {
			return parseOptionsJSON.call(this, this.getAttribute(ATTRIBUTE_OPTIONS));
		} catch (e) {
			addErrorAttribute(
				this,
				`the options attribute ${ATTRIBUTE_OPTIONS} does not contain a valid json definition (actual: ${this.getAttribute(
					ATTRIBUTE_OPTIONS,
				)}).${e}`,
			);
		}
	}

	return {};
}

/**
 * Parses the given JSON data and returns the parsed object.
 *
 * @private
 * @param {string} data The JSON data to be parsed.
 * @return {Object} The parsed object.
 * @throws {error} Throws an error if the JSON data is not valid.
 */
function parseOptionsJSON(data) {
	let obj = {};

	if (!isString(data)) {
		return obj;
	}

	// the configuration can be specified as a data url.
	try {
		const dataUrl = parseDataURL(data);
		data = dataUrl.content;
	} catch (e) {}

	try {
		obj = JSON.parse(data);
	} catch (e) {
		throw e;
	}

	return validateObject(obj);
}

function getManagedShadowRoot() {
	if (this[managedShadowRootSymbol] instanceof ShadowRoot) {
		return this[managedShadowRootSymbol];
	}
	if (this.shadowRoot instanceof ShadowRoot) {
		return this.shadowRoot;
	}
	return null;
}

/**
 * @private
 * @param html
 * @returns {*|string}
 */
function substituteI18n(html) {
	if (!this.getOption("templateFormatter.i18n", false)) {
		return html;
	}

	const labels = this.getOption("labels", {});
	if (!(isObject(labels) || isIterable(labels))) {
		return html;
	}

	const formatter = new Formatter(labels, {});
	formatter.setMarker("i18n{", "}");
	return formatter.format(html);
}

/**
 * @private
 * @return {initHtmlContent}
 */
function initHtmlContent() {
	try {
		const template = findDocumentTemplate(this.constructor.getTag());
		this.appendChild(template.createDocumentFragment());
	} catch (e) {
		let html = this.getOption("templates.main", "");
		if (isString(html) && html.length > 0) {
			const mapping = this.getOption("templateMapping", {});
			if (isObject(mapping)) {
				html = new Formatter(mapping, {}).format(html);
			}
			this.innerHTML = substituteI18n.call(this, html);
		}
	}

	return this;
}

/**
 * @private
 * @returns {void}
 */
function syncVisibilityState() {
	const visible = !this.hidden;
	if (typeof this[visibilityStateSymbol] === "undefined") {
		this[visibilityStateSymbol] = visible;
		if (!visible) {
			blurFocusedElement.call(this);
		}
		return;
	}

	if (this[visibilityStateSymbol] === visible) {
		return;
	}

	if (!visible) {
		blurFocusedElement.call(this);
	}

	this[visibilityStateSymbol] = visible;
	dispatchVisibilityChangedEvent.call(this, visible);
}

/**
 * @private
 * @returns {void}
 */
function blurFocusedElement() {
	const shadowRoot = getManagedShadowRoot.call(this);
	const activeShadowElement = shadowRoot?.activeElement;

	if (activeShadowElement instanceof HTMLElement) {
		activeShadowElement.blur();
	}

	if (document.activeElement === this && typeof this.blur === "function") {
		this.blur();
	}

	if (shadowRoot?.activeElement instanceof HTMLElement) {
		const body = getDocument()?.body;
		if (body instanceof HTMLElement) {
			body.setAttribute("tabindex", "-1");
			body.focus();
			body.removeAttribute("tabindex");
		}
	}
}

/**
 * @private
 * @param {boolean} visible
 * @returns {void}
 */
function dispatchVisibilityChangedEvent(visible) {
	this.dispatchEvent(
		new CustomEvent("monster-visibility-changed", {
			bubbles: true,
			composed: true,
			detail: { visible },
		}),
	);
}

/**
 * @private
 * @return {CustomElement}
 * @this CustomElement
 * @license AGPLv3
 * @since 1.16.0
 * @throws {TypeError} value is not an instance of
 */
function initCSSStylesheet() {
	const shadowRoot = getManagedShadowRoot.call(this);
	if (!(shadowRoot instanceof ShadowRoot)) {
		return this;
	}
	const visibilityStyleSheet = getHostVisibilityStyleSheet();

	const styleSheet = this.constructor.getCSSStyleSheet();

	if (styleSheet instanceof CSSStyleSheet) {
		if (styleSheet.cssRules.length > 0) {
			shadowRoot.adoptedStyleSheets = [visibilityStyleSheet, styleSheet];
		} else {
			shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
		}
	} else if (isArray(styleSheet)) {
		const assign = [visibilityStyleSheet];
		for (const s of styleSheet) {
			if (isString(s)) {
				const trimedStyleSheet = s.trim();
				if (trimedStyleSheet !== "") {
					const style = document.createElement("style");
					style.innerHTML = trimedStyleSheet;
					shadowRoot.prepend(style);
				}
				continue;
			}

			validateInstance(s, CSSStyleSheet);

			if (s.cssRules.length > 0) {
				assign.push(s);
			}
		}

		if (assign.length > 0) {
			shadowRoot.adoptedStyleSheets = assign;
		}
	} else if (isString(styleSheet)) {
		shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
		const trimedStyleSheet = styleSheet.trim();
		if (trimedStyleSheet !== "") {
			const style = document.createElement("style");
			style.innerHTML = styleSheet;
			shadowRoot.prepend(style);
		}
	} else {
		shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
	}

	return this;
}

/**
 * @private
 * @returns {CSSStyleSheet}
 */
function getHostVisibilityStyleSheet() {
	if (hostVisibilityStyleSheet instanceof CSSStyleSheet) {
		return hostVisibilityStyleSheet;
	}

	hostVisibilityStyleSheet = new CSSStyleSheet();
	hostVisibilityStyleSheet.replaceSync(
		":host([hidden]){display:none !important}",
	);

	return hostVisibilityStyleSheet;
}

/**
 * @private
 * @param {ShadowRoot} shadowRoot
 * @returns {void}
 */
function appendVisibilityHostStyle(shadowRoot) {
	if (!(shadowRoot instanceof ShadowRoot)) {
		return;
	}

	const style = document.createElement("style");
	style.setAttribute("data-monster-host-visibility", "true");
	style.textContent = ":host([hidden]){display:none !important}";
	shadowRoot.prepend(style);
}

/**
 * @private
 * @return {CustomElement}
 * @throws {Error} html is not set.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
 * @license AGPLv3
 * @since 1.8.0
 */
function initShadowRoot() {
	let template;
	let html;

	try {
		template = findDocumentTemplate(this.constructor.getTag());
	} catch (e) {
		html = this.getOption("templates.main", "");
		if (!isString(html) || html === undefined || html === "") {
			throw new Error("html is not set.");
		}
	}

	this[managedShadowRootSymbol] = this.attachShadow({
		mode: this.getOption("shadowMode", "open"),
		delegatesFocus: this.getOption("delegatesFocus", true),
	});

	if (template instanceof Template) {
		this[managedShadowRootSymbol].appendChild(
			template.createDocumentFragment(),
		);
		return this;
	}

	const mapping = this.getOption("templateMapping", {});
	if (isObject(mapping)) {
		const formatter = new Formatter(mapping);
		if (this.getOption("templateFormatter.marker.open") !== null) {
			formatter.setMarker(
				this.getOption("templateFormatter.marker.open"),
				this.getOption("templateFormatter.marker.close"),
			);
		}
		html = formatter.format(html);
	}

	this[managedShadowRootSymbol].innerHTML = substituteI18n.call(this, html);
	return this;
}

/**
 * This method registers a new element. The string returned by `CustomElement.getTag()` is used as the tag.
 *
 * @param {CustomElement} element
 * @return {void}
 * @license AGPLv3
 * @since 1.7.0
 * @copyright Volker Schukai
 * @throws {DOMException} Failed to execute 'define' on 'CustomElementRegistry': is not a valid custom element name
 */
function registerCustomElement(element) {
	validateFunction(element);
	const customElements = getGlobalObject("customElements");
	if (customElements === undefined) {
		throw new Error("customElements is not supported.");
	}

	const tag = element?.getTag();
	if (!isString(tag) || tag === "") {
		throw new Error("tag is not set.");
	}

	if (customElements.get(tag) !== undefined) {
		return;
	}

	customElements.define(tag, element);
}
