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

import { instanceSymbol } from "../../constants.mjs";
import { extend } from "../../data/extend.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import {
	addAttributeToken,
	addToObjectLink,
	hasObjectLink,
} from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_PREFIX,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import {
	findTargetElementFromEvent,
	fireCustomEvent,
} from "../../dom/events.mjs";
import { getDocument, getWindow } from "../../dom/util.mjs";
import { random } from "../../math/random.mjs";
import { getGlobal } from "../../types/global.mjs";
import { ID } from "../../types/id.mjs";
import { isArray, isString } from "../../types/is.mjs";
import { TokenList } from "../../types/tokenlist.mjs";
import { clone } from "../../util/clone.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { Processing } from "../../util/processing.mjs";
import {
	ATTRIBUTE_BUTTON_LABEL,
	ATTRIBUTE_FORM_RELOAD,
	ATTRIBUTE_FORM_URL,
	STYLE_DISPLAY_MODE_BLOCK,
} from "../form/constants.mjs";

import { TabsStyleSheet } from "./stylesheet/tabs.mjs";
import { VerticalTabsStyleSheet } from "./stylesheet/vertical-tabs.mjs";
import { loadAndAssignContent } from "../form/util/fetch.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import {
	createFloatingPopper,
	popperInstanceSymbol,
	setEventListenersModifiers,
} from "../form/util/popper.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";

export { VerticalTabs };

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

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

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

/**
 * @private
 * @type {symbol}
 */
const navElementSymbol = Symbol("navElement");
/**
 * @private
 * @type {symbol}
 */
const switchElementSymbol = Symbol("switchElement");

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

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

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

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

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

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

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

/**
 * A Vertical Tabs Control
 *
 * @fragments /fragments/components/layout/tabs/
 *
 * @example /examples/components/layout/tabs-simple Simple Tabs
 * @example /examples/components/layout/tabs-active Active Tabs
 * @example /examples/components/layout/tabs-removable Removable Tabs
 * @example /examples/components/layout/tabs-with-icon Tabs with Icon
 * @example /examples/components/layout/tabs-fetch Fetch Tab Content from URL
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/268.html
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/271.html
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/273.html
 *
 * @since 3.74.0
 * @copyright Volker Schukai
 * @summary This CustomControl creates a vertical tab element with a variety of options.
 */
class VerticalTabs extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/layout/vertical-tabs");
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels
	 * @property {string} labels.new-tab-label="New Tab"
	 * @property {Object} features
	 * @property {number} features.openDelay=500 Open delay in milliseconds
	 * @property {string} features.removeBehavior Remove behavior, auto, next, previous and none
	 * @property {boolean} features.openFirst Open the first tab when no active tab is set
	 * @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":"text/html"}}
	 * @property {Object} popper Popper configuration
	 * @property {string} popper.placement=right-start Popper placement
	 * @property {Object[]} popper.modifiers={name:offset} Placement middleware configuration
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			buttons: {
				standard: [],
				popper: [],
			},
			fetch: {
				redirect: "error",
				method: "GET",
				mode: "same-origin",
				credentials: "same-origin",
				headers: {
					accept: "text/html",
				},
			},

			features: {
				openDelay: null,
				removeBehavior: "auto",
				openFirst: true,
			},

			classes: {
				button: "monster-theme-primary-1",
				popper: "monster-theme-primary-1",
				navigation: "monster-theme-primary-1",
			},

			popper: {
				placement: "right-start",
				modifiers: [
					{
						name: "offset",
						options: {
							offset: [0, 2],
						},
					},

					{
						name: "eventListeners",
						enabled: false,
					},
				],
			},
		});
	}

	/**
	 * This method is called internal and should not be called directly.
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);

		this[dimensionsSymbol] = new Pathfinder({ data: {} });

		initEventHandler.call(this);

		// setup structure
		initTabButtons.call(this).then(() => {
			initPopperSwitch.call(this);
			initPopper.call(this);
			attachResizeObserver.call(this);
			attachTabChangeObserver.call(this);
		});
	}

	/**
	 * This method is called internal and should not be called directly.
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [TabsStyleSheet, VerticalTabsStyleSheet];
	}

	/**
	 * This method is called internal and should not be called directly.
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-vertical-tabs";
	}

	/**
	 * This method is called internal and should not be called directly.
	 * @param tabId
	 * @returns {VerticalTabs}
	 */
	removeTab(tabId) {
		const tabs = this.getTabs();
		for (const tab of tabs) {
			if (
				(tab.getAttribute("id") === tabId ||
					tab.getAttribute("data-monster-name") === tabId) &&
				tab.hasAttribute("data-monster-removable")
			) {
				tab.remove();
				initTabButtons.call(this);
				return this;
			}
		}
		return this;
	}

	/**
	 * This method is called internal and should not be called directly.
	 * @returns {[]}
	 */
	getTabs() {
		const nodes = getSlottedElements.call(this);
		const tabs = [];
		for (const node of nodes) {
			if (node instanceof HTMLElement) {
				tabs.push(node);
			}
		}
		return tabs;
	}

	/**
	 * This method is called internal and should not be called directly.
	 * @param content
	 * @param active
	 * @param label
	 * @param tabId
	 * @param removable
	 * @returns {VerticalTabs}
	 */
	addTab(
		content,
		{ active = false, label = null, tabId = null, removable = true } = {},
	) {
		const tab = document.createElement("div");
		if (!isString(label) || label.trim() === "") {
			label = this.getOption("labels.new-tab-label");
		}
		tab.setAttribute("data-monster-button-label", label);

		if (!isString(tabId) || tabId.trim() === "") {
			let thisID = this.getAttribute("id");
			if (!thisID) {
				thisID = new ID("tab").toString();
			}

			tabId = new ID(thisID).toString();
		}

		// check if id is already used
		const existingTabs = this.getTabs();
		for (const existingTab of existingTabs) {
			if (
				existingTab.getAttribute("id") === tabId ||
				existingTab.getAttribute("data-monster-name") === tabId
			) {
				throw new Error(`Tab with id "${tabId}" already exists.`);
			}
		}

		tab.setAttribute("id", tabId);

		if (active === true) {
			tab.classList.add("active");
		}

		tab.setAttribute(ATTRIBUTE_ROLE, "tab");

		if (removable === true) {
			tab.setAttribute("data-monster-removable", "true");
		}

		if (content instanceof HTMLElement) {
			tab.appendChild(content);
		} else if (isString(content)) {
			tab.innerHTML = content;
		}

		this.appendChild(tab);
		return this;
	}

	/**
	 * A function that activates a tab based on the provided name.
	 *
	 * The tabs have to be named with the `data-monster-name` attribute.
	 *
	 * @param {type} idOrName - the name or id of the tab to activate
	 * @return {VerticalTabs} - The current instance
	 */
	activeTab(idOrName) {
		let found = false;

		getSlottedElements.call(this).forEach((node) => {
			if (found === true) {
				return;
			}

			if (node.getAttribute("data-monster-name") === idOrName) {
				const tabControl = this.shadowRoot.querySelector(
					`[data-monster-tab-reference="${node.getAttribute("id")}"]`,
				);

				if (tabControl) {
					tabControl.click();
					found = true;
				}
			}

			if (node.getAttribute("id") === idOrName) {
				const tabControl = this.shadowRoot.querySelector(
					`[data-monster-tab-reference="${node.getAttribute("id")}"]`,
				);

				if (tabControl) {
					tabControl.click();
					found = true;
				}
			}
		});

		return this;
	}

	/**
	 * A function that returns the name or id of the currently active tab.
	 *
	 * The tabs have to be named with the `data-monster-name` attribute.
	 *
	 * @return {string|null}
	 */
	getActiveTab() {
		const nodes = getSlottedElements.call(this);
		for (const node of nodes) {
			if (node.matches(".active") === true) {
				if (node.hasAttribute("data-monster-name")) {
					return node.getAttribute("data-monster-name");
				}

				return node.getAttribute("id");
			}
		}
		return null;
	}

	/**
	 * This method is called by the dom and should not be called directly.
	 *
	 * @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]);
		}
	}

	/**
	 * This method is called by the dom and should not be called directly.
	 *
	 * @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]);
		}

		this[popperInstanceSymbol]?.destroy();
	}
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				"new-tab-label": "Neuer Tab",
			};
		case "fr":
			return {
				"new-tab-label": "Nouvel Onglet",
			};
		case "sp":
			return {
				"new-tab-label": "Nueva Pestaña",
			};
		case "it":
			return {
				"new-tab-label": "Nuova Scheda",
			};
		case "pl":
			return {
				"new-tab-label": "Nowa Karta",
			};
		case "no":
			return {
				"new-tab-label": "Ny Fane",
			};
		case "dk":
			return {
				"new-tab-label": "Ny Fane",
			};
		case "sw":
			return {
				"new-tab-label": "Ny Flik",
			};
		default:
		case "en":
			return {
				"new-tab-label": "New Tab",
			};
	}
}

/**
 * @private
 */
function initPopperSwitch() {
	const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots
	let switchButton;
	if (nodes.size === 0) {
		switchButton = document.createElement("button");
		switchButton.setAttribute(ATTRIBUTE_ROLE, "switch");
		switchButton.setAttribute("part", "switch");
		switchButton.classList.add("hidden");
		const classList = this.getOption("classes.button");
		if (classList) {
			switchButton.classList.add(classList);
		}
		switchButton.innerHTML =
			'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>';
		this[navElementSymbol].prepend(switchButton);
	} else {
		switchButton = nodes.next();
	}

	/**
	 * @param {Event} event
	 */
	this[popperSwitchEventHandler] = (event) => {
		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch");

		if (element instanceof HTMLButtonElement) {
			togglePopper.call(this);
		}
	};

	for (const type of ["click", "touch"]) {
		switchButton.addEventListener(type, this[popperSwitchEventHandler]);
	}

	this[switchElementSymbol] = switchButton;
}

/**
 * @private
 */
function hidePopper() {
	if (!this[popperInstanceSymbol]) {
		return;
	}

	this[popperElementSymbol].style.display = "none";
	// performance https://popper.js.org/docs/v2/tutorial/#performance
	setEventListenersModifiers.call(this, false);
}

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

	this[popperElementSymbol].style.visibility = "hidden";
	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
	// performance https://popper.js.org/docs/v2/tutorial/#performance
	setEventListenersModifiers.call(this, true);

	this[popperInstanceSymbol].update();

	new Processing(() => {
		this[popperElementSymbol].style.removeProperty("visibility");
	})
		.run(undefined)
		.then(() => {})
		.catch((e) => {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
		});
}

/**
 * @private
 */
function togglePopper() {
	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
		hidePopper.call(this);
	} else {
		showPopper.call(this);
	}
}

/**
 * @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, () => {
			this[dimensionsSymbol].setVia("data.calculated", false);
			checkAndRearrangeButtons.call(this);
		});
	});

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

/**
 * @private
 */
function attachTabChangeObserver() {
	// against flickering
	new MutationObserver((mutations) => {
		let runUpdate = false;

		for (const mutation of mutations) {
			if (mutation.type === "childList") {
				if (
					mutation.addedNodes.length > 0 ||
					mutation.removedNodes.length > 0
				) {
					runUpdate = true;
					break;
				}
			}
		}

		if (runUpdate === true) {
			this[dimensionsSymbol].setVia("data.calculated", false);
			initTabButtons.call(this);
		}
	}).observe(this, {
		childList: true,
	});
}

/**
 * @private
 * @return {Select}
 */
function initPopper() {
	const self = this;

	const options = extend({}, self.getOption("popper"));

	self[popperInstanceSymbol] = createFloatingPopper(
		self[switchElementSymbol],
		self[popperElementSymbol],
		options,
	);

	const observer1 = new MutationObserver(function (mutations) {
		let runUpdate = false;
		for (const mutation of mutations) {
			if (mutation.type === "childList") {
				if (
					mutation.addedNodes.length > 0 ||
					mutation.removedNodes.length > 0
				) {
					runUpdate = true;
					break;
				}
			}
		}

		if (runUpdate === true) {
			self[popperInstanceSymbol].update();
		}
	});

	observer1.observe(self[popperNavElementSymbol], {
		childList: true,
		subtree: true,
	});

	return self;
}

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

	const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`);

	const nodes = getSlottedElements.call(this);
	for (const node of nodes) {
		const id = node.getAttribute("id");

		if (id === reference) {
			node.classList.add("active");

			fireCustomEvent(this, "monster-tab-change", {
				reference,
			});

			const openDelay = Number.parseInt(
				this.getOption("features.openDelay"),
				10,
			);

			if (!Number.isNaN(openDelay) && openDelay > 0) {
				node.style.visibility = "hidden";

				setTimeout(() => {
					node.style.visibility = "visible";
				}, openDelay);
			}

			// get all data- from button and filter out data-monster-attributes and data-monster-insert
			const data = {};
			const mask = [
				"data-monster-attributes",
				"data-monster-insert-reference",
				"data-monster-state",
				"data-monster-button-label",
				"data-monster-objectlink",
				"data-monster-role",
			];

			for (const [, attr] of Object.entries(node.attributes)) {
				if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) {
					data[attr.name] = attr.value;
				}
			}

			if (node.hasAttribute(ATTRIBUTE_FORM_URL)) {
				const url = node.getAttribute(ATTRIBUTE_FORM_URL);

				if (
					!node.hasAttribute(ATTRIBUTE_FORM_RELOAD) ||
					node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow"
				) {
					node.removeAttribute(ATTRIBUTE_FORM_URL);
				}

				const options = this.getOption("fetch", {});
				const filter = undefined;
				loadAndAssignContent(node, url, options, filter)
					.then(() => {
						fireCustomEvent(this, "monster-tab-changed", {
							reference,
						});
					})
					.catch((e) => {
						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
					});
			} else {
				fireCustomEvent(this, "monster-tab-changed", {
					reference,
					data,
				});
			}
		} else {
			node.classList.remove("active");
		}
	}

	const standardButtons = this.getOption("buttons.standard");
	for (const index in standardButtons) {
		const button = standardButtons[index];
		const state = button["reference"] === reference ? "active" : "inactive";
		this.setOption(`buttons.standard.${index}.state`, state);
	}

	const popperButton = this.getOption("buttons.popper");
	for (const index in popperButton) {
		const button = popperButton[index];
		const state = button["reference"] === reference ? "active" : "inactive";
		this.setOption(`buttons.popper.${index}.state`, state);
	}

	hidePopper.call(this);
}

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

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

	/**
	 * @param {Event} event
	 * @fires monster-tab-remove
	 */
	this[removeTabEventHandler] = (event) => {
		const element = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"remove-tab",
		);

		if (element instanceof HTMLElement) {
			const button = findTargetElementFromEvent(
				event,
				ATTRIBUTE_ROLE,
				"button",
			);

			if (button instanceof HTMLButtonElement && button.disabled !== true) {
				const reference = button.getAttribute(
					`${ATTRIBUTE_PREFIX}tab-reference`,
				);

				let doChange = false;
				let nextName = null;
				let previousName = null;
				let targetReference = null;
				let activeReference = null;
				let restoreOpenFirst = null;

				const btn = this.getOption("buttons");
				if (btn?.standard) {
					const activeButton = btn.standard.find(
						(item) => item.state === "active",
					);
					activeReference = activeButton?.reference || null;
				}

				const nodes = getSlottedElements.call(this);
				const orderedTabs = [];
				for (const node of nodes) {
					if (node instanceof HTMLElement) {
						orderedTabs.push(node);
					}
				}

				for (let i = 0; i < orderedTabs.length; i++) {
					const node = orderedTabs[i];
					if (node.getAttribute("id") === reference) {
						if (reference === activeReference) {
							doChange = true;
							nextName = orderedTabs[i + 1]?.getAttribute?.("id") || null;
							previousName = orderedTabs[i - 1]?.getAttribute?.("id") || null;
						}
						break;
					}
				}

				if (reference) {
					const escapedReference =
						typeof CSS !== "undefined" && CSS.escape
							? CSS.escape(reference)
							: reference;
					const container = this.querySelector(`[id=${escapedReference}]`);
					if (container instanceof HTMLElement) {
						if (doChange) {
							switch (this.getOption("features.removeBehavior")) {
								case "auto":
									if (nextName !== null) {
										targetReference = nextName;
									} else {
										if (previousName !== null) {
											targetReference = previousName;
										}
									}
									break;
								case "next":
									if (nextName !== null) {
										targetReference = nextName;
									}
									break;
								case "previous":
									if (previousName !== null) {
										targetReference = previousName;
									}
									break;

								default: // and "none"
									break;
							}
							if (targetReference) {
								self.activeTab(targetReference);
								restoreOpenFirst = this.getOption("features.openFirst");
								this.setOption("features.openFirst", false);
							}
						}

						container.remove();
						initTabButtons.call(this);
						if (targetReference) {
							if (restoreOpenFirst !== null) {
								this.setOption("features.openFirst", restoreOpenFirst);
							}
							self.activeTab(targetReference);
						}
						fireCustomEvent(this, "monster-tab-remove", {
							reference,
						});
					}
				}
			}
		}
	};

	/**
	 * @param {Event} event
	 */
	this[changeTabEventHandler] = (event) => {
		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");

		if (element instanceof HTMLButtonElement && element.disabled !== true) {
			show.call(this, element);
		}
	};

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

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

		hidePopper.call(this);
	};

	// the order is important, because the remove must be before the change
	this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]);
	this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]);

	this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
	this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);

	return this;
}

/**
 * @private
 * @param observedNode
 */
function attachTabMutationObserver(observedNode) {
	const self = this;

	if (hasObjectLink(observedNode, mutationObserverSymbol)) {
		return;
	}

	/**
	 * this construct monitors a node whether it is disabled or modified
	 * @type {MutationObserver}
	 */
	const observer = new MutationObserver(function (mutations) {
		if (isArray(mutations)) {
			const mutation = mutations.pop();
			if (mutation instanceof MutationRecord) {
				initTabButtons.call(self);
			}
		}
	});

	observer.observe(observedNode, {
		childList: false,
		attributes: true,
		subtree: false,
		attributeFilter: [
			"disabled",
			ATTRIBUTE_BUTTON_LABEL,
			`${ATTRIBUTE_PREFIX}button-icon`,
			"data-monster-tab-error",
			"data-monster-tab-error-message",
		],
	});

	addToObjectLink(observedNode, mutationObserverSymbol, observer);
}

/**
 * @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[navElementSymbol] = this.shadowRoot.querySelector(
		`nav[${ATTRIBUTE_ROLE}=nav]`,
	);
	this[popperElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper]`,
	);
	this[popperNavElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper-nav]`,
	);
}

/**
 * @private
 * @return {Promise<unknown>}
 * @throws {Error} no shadow-root is defined
 *
 */
function initTabButtons() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	let activeReference;

	const dimensionsCalculated = this[dimensionsSymbol].getVia(
		"data.calculated",
		false,
	);

	const buttons = [];
	const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots

	for (const node of nodes) {
		if (!(node instanceof HTMLElement)) continue;
		let label = getButtonLabel.call(this, node);

		let reference;
		if (node.hasAttribute("id")) {
			reference = node.getAttribute("id");
		}

		let disabled;
		if (node.hasAttribute("disabled") || node.disabled === true) {
			disabled = true;
		}

		if (!reference) {
			reference = new ID("tab").toString();
			node.setAttribute("id", reference);
		}

		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) {
			label = `<span part="label">${label}</span><img part="icon" alt="this is an icon" src="${node.getAttribute(
				`${ATTRIBUTE_PREFIX}button-icon`,
			)}">`;
		}

		const errorMarkup = getTabErrorMarkup(node);

		let remove = false;
		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) {
			remove = true;
		}

		if (node.matches(".active") === true && disabled !== true) {
			activeReference = reference;
		}

		const state = "";
		const classes = dimensionsCalculated ? "" : "invisible";

		buttons.push({
			reference,
			label,
			error: errorMarkup,
			state,
			class: classes,
			disabled,
			remove,
		});

		attachTabMutationObserver.call(this, node);
	}

	this.setOption("buttons.standard", clone(buttons));
	this.setOption("buttons.popper", []);
	this.setOption("marker", random());

	return adjustButtonVisibility.call(this).then(() => {
		if (!activeReference && this.getOption("features.openFirst") === true) {
			const firstButton = this.getOption("buttons.standard").find(
				(button) => button.disabled !== true,
			);
			if (firstButton) {
				activeReference = firstButton.reference;
			}
		}

		if (activeReference) {
			return new Processing(() => {
				const button = this.shadowRoot.querySelector(
					`[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`,
				);
				if (button instanceof HTMLButtonElement && button.disabled !== true) {
					show.call(this, button);
				}
			})
				.run(undefined)
				.then(() => {})
				.catch((e) => {
					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
				});
		}

		return Promise.resolve();
	});
}

function checkAndRearrangeButtons() {
	if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) {
		calculateNavigationButtonsDimensions.call(this);
	}

	rearrangeButtons.call(this);
}

/**
 * @private
 * @return {Promise<unknown>}
 */
function adjustButtonVisibility() {
	const self = this;

	return new Promise((resolve) => {
		const observer = new MutationObserver(function (mutations) {
			const defCount = self.getOption("buttons.standard").length;
			const domCount = self[navElementSymbol].querySelectorAll(
				'button[data-monster-role="button"]',
			).length;

			// in drawing
			if (defCount !== domCount) return;

			observer.disconnect();

			checkAndRearrangeButtons.call(self);

			resolve();
		});

		observer.observe(self[navElementSymbol], {
			attributes: true,
		});
	});
}

/**
 * @private
 * @param {string} value
 * @return {number}
 */
function getDimValue(value) {
	if ([undefined, null].indexOf(value) !== -1) {
		return 0;
	}

	const valueAsInt = parseInt(value, 10);

	if (isNaN(valueAsInt)) {
		return 0;
	}

	return valueAsInt;
}

/**
 * @private
 * @param {HTMLElement} node
 * @return {number}
 */
function calcBoxHeight(node) {
	const dim = getGlobal("window").getComputedStyle(node);
	const bounding = node.getBoundingClientRect();

	return (
		getDimValue(dim["border-top-width"]) +
		getDimValue(dim["padding-top"]) +
		getDimValue(dim["margin-top"]) +
		getDimValue(bounding["height"]) +
		getDimValue(dim["border-bottom-width"]) +
		getDimValue(dim["margin-bottom"]) +
		getDimValue(dim["padding-bottom"])
	);
}

/**
 * @private
 * @return {Object}
 */
function rearrangeButtons() {
	getWindow().requestAnimationFrame(() => {
		const standardButtons = [];
		const popperButtons = [];
		let sum = 0;
		const space = this[dimensionsSymbol].getVia("data.space");

		if (space <= 0) {
			return;
		}

		const buttons = this.getOption("buttons.standard");
		for (const [, button] of buttons.entries()) {
			const ref = button?.reference;

			sum += this[dimensionsSymbol].getVia(`data.button.${ref}`);

			if (sum > space) {
				popperButtons.push(clone(button));
			} else {
				standardButtons.push(clone(button));
			}
		}

		this.setOption("buttons.standard", standardButtons);
		this.setOption("buttons.popper", popperButtons);

		if (this[switchElementSymbol]) {
			if (popperButtons.length > 0) {
				this[switchElementSymbol].classList.remove("hidden");
			} else {
				this[switchElementSymbol].classList.add("hidden");
			}
		}
	});
}

/**
 * @private
 * @return {Object}
 */
function calculateNavigationButtonsDimensions() {
	const height = this[navElementSymbol].getBoundingClientRect().height;

	let startEndHeight = 0;

	getSlottedElements.call(this, undefined, "start").forEach((node) => {
		startEndHeight += calcBoxHeight.call(this, node);
	});

	getSlottedElements.call(this, undefined, "end").forEach((node) => {
		startEndHeight += calcBoxHeight.call(this, node);
	});

	this[dimensionsSymbol].setVia("data.space", height - startEndHeight - 2);
	this[dimensionsSymbol].setVia("data.visible", !(height === 0));

	const buttons = this.getOption("buttons.standard").concat(
		this.getOption("buttons.popper"),
	);

	for (const [i, button] of buttons.entries()) {
		const ref = button?.reference;
		const element = this[navElementSymbol].querySelector(
			`:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`,
		);
		if (!(element instanceof HTMLButtonElement)) continue;

		this[dimensionsSymbol].setVia(
			`data.button.${ref}`,
			calcBoxHeight.call(this, element),
		);
		button["class"] = new TokenList(button["class"])
			.remove("invisible")
			.toString();
	}

	const slots = this[controlElementSymbol].querySelectorAll(
		`nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`,
	);
	for (const [, slot] of slots.entries()) {
		slot.classList.remove("invisible");
	}

	this.setOption("buttons.standard", clone(buttons));

	getWindow().requestAnimationFrame(() => {
		this[dimensionsSymbol].setVia("data.calculated", true);
	});
}

/**
 * @private
 * @param {HTMLElement} node
 * @return {string}
 */
function getButtonLabel(node) {
	let label;
	let setLabel = false;
	if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) {
		label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL);
	} else {
		label = node.innerText;
		setLabel = true;
	}

	if (!isString(label)) {
		label = "";
	}

	if (setLabel === true) {
		if (label.trim() === "") {
			label = node.textContent || "";
		}
		label = label.replace(/\s+/g, " ").trim();
		const words = label.split(" ").filter((word) => word !== "");
		if (words.length > 3) {
			label = `${words.slice(0, 3).join(" ")}…`;
		}
	}
	label = label.trim();

	if (label === "") {
		label = this.getOption("labels.new-tab-label", "New Tab");
	}

	if (label.length > 100) {
		label = `${label.substring(0, 99)}…`;
	}

	if (setLabel === true) {
		node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label);
	}

	return label;
}

/**
 * @private
 * @param {HTMLElement} node
 * @return {string}
 */
function getTabErrorMarkup(node) {
	if (!node.hasAttribute("data-monster-tab-error")) {
		return "";
	}

	let message = node.getAttribute("data-monster-tab-error-message");
	const errorAttr = node.getAttribute("data-monster-tab-error");

	if (!isString(message) || message.trim() === "") {
		if (
			isString(errorAttr) &&
			errorAttr.trim() !== "" &&
			errorAttr !== "true" &&
			errorAttr !== "false"
		) {
			message = errorAttr;
		}
	}

	let titleAttr = "";
	if (!isString(message) || message.trim() === "") {
		message = "";
	} else {
		message = escapeHtml(message.trim());
		titleAttr = ` title="${message}" aria-label="${message}"`;
	}

	return `<span class="monster-tab-error-icon"${titleAttr}><svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" viewBox="0 0 16 16"><path fill="var(--monster-color-error-2)" d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/><path fill="var(--monster-color-error-2)" d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/></svg></span>`;
}

/**
 * @private
 * @param {string} value
 * @return {string}
 */
function escapeHtml(value) {
	return value
		.replaceAll("&", "&amp;")
		.replaceAll("<", "&lt;")
		.replaceAll(">", "&gt;")
		.replaceAll('"', "&quot;")
		.replaceAll("'", "&#39;");
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="buttons">
            <button part="button"
                    tabindex="0"
                    data-monster-role="button"
                    data-monster-attributes="
                    class path:classes.button,
                    data-monster-state path:buttons.state,
                    disabled path:buttons.disabled | if:true,                    
                    data-monster-tab-reference path:buttons.reference"><span part="label"
                    data-monster-replace="path:buttons.label"></span><div part="error"
                    data-monster-replace="path:buttons.error"></div><span part="remove-tab"
                                                                           data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden "
                                                                           data-monster-role="remove-tab"
                                                                           tabindex="-1"></span></button>
        </template>
        <div data-monster-role="control" part="control">
            <nav data-monster-role="nav" part="nav"
                 data-monster-attributes="data-monster-marker path:marker, class path:classes.navigation"
                 data-monster-insert="buttons path:buttons.standard">
                <slot name="start" class="invisible"></slot>
                <div data-monster-role="popper" part="popper" tabindex="-1"
                     data-monster-attributes="class path:classes.popper">
                    <div data-popper-arrow></div>


                    <div part="popper-nav" data-monster-role="popper-nav"
                         data-monster-insert="buttons path:buttons.popper"
                         tabindex="-1"></div>
                </div>
                <slot name="popper" class="invisible"></slot>
                <slot name="end" class="invisible"></slot>
            </nav>
            <slot data-monster-role="slot" class="invisible"></slot>
        </div>`;
}

registerCustomElement(VerticalTabs);
