/**
 * 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 { Pathfinder } from "../../data/pathfinder.mjs";
import {
	addAttributeToken,
	removeAttributeToken,
} from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import {
	CustomElement,
	attributeObserverSymbol,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { getDocument } from "../../dom/util.mjs";
import { getGlobal } from "../../types/global.mjs";
import { ID } from "../../types/id.mjs";
import { Observer } from "../../types/observer.mjs";
import { Queue } from "../../types/queue.mjs";
import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
import { ControlBarStyleSheet } from "./stylesheet/control-bar.mjs";
import { positionPopper } from "./util/floating-ui.mjs";
import { convertToPixels } from "../../dom/dimension.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { Processing } from "../../util/processing.mjs";
export { ControlBar };

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

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

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

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

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

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

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

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

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

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

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

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

/**
 * @private
 * @type {symbol}
 */
const layoutStateSymbol = Symbol("layoutState");
const layoutFrameSymbol = Symbol("layoutFrame");
const layoutTokenSymbol = Symbol("layoutToken");
const observedLayoutNodesSignatureSymbol = Symbol(
	"observedLayoutNodesSignature",
);
const layoutChangedEventQueueSymbol = Symbol("layoutChangedEventQueue");
const layoutChangedEventSwitchSymbol = Symbol("layoutChangedEventSwitch");
const layoutChangedEventLastSignatureSymbol = Symbol(
	"layoutChangedEventLastSignature",
);

/**
 * @private
 * @type {WeakMap<HTMLElement, number>}
 */
const layoutNodeIds = new WeakMap();
let layoutNodeId = 0;

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_POPPER_POSITION = "data-monster-popper-position";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_LAYOUT_ALIGNMENT = "data-monster-layout-alignment";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_LAYOUT_STACKED = "data-monster-layout-stacked";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT =
	"data-monster-option-layout-alignment";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT =
	"data-monster-option-layout-stacked-alignment";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT =
	"data-monster-option-layout-stacked-breakpoint";

/**
 * @private
 * @type {string}
 */
const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER =
	"data-monster-option-layout-stacked-breakpoint-container";

/**
 * @private
 * @type {string}
 */
const EVENT_LAYOUT_CHANGED = "monster-control-bar-layout-changed";

/**
 * @private
 * @type {number}
 */
const LAYOUT_CHANGED_EVENT_DELAY = 20;
const LAYOUT_SWITCH_HYSTERESIS = 16;

/**
 * A control bar control.
 *
 * @fragments /fragments/components/form/control-bar/
 *
 * @example /examples/components/form/control-bar-simple Control bar
 *
 * @copyright Volker Schukai
 * @summary A responsive control bar that groups multiple controls and moves overflowed controls into a menu.
 * @fires monster-fetched
 */
class ControlBar extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/control-bar@@instance");
	}

	/**
	 * 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 {Object} popper FloatingUI popper configuration
	 * @property {string} popper.placement=top Placement of the popper
	 * @property {Array<string>} popper.middleware Middleware for the popper
	 */
	get defaults() {
		const obj = Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: {},
			layout: {
				alignment: "left",
				stackedAlignment: undefined,
				stackedBreakpoint: undefined,
				stackedBreakpointContainer: undefined,
			},
			popper: {
				placement: "left",
				middleware: ["flip", "shift", "offset:5"],
			},
		});

		initDefaultsFromAttributes.call(this, obj);

		return obj;
	}

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

		this[dimensionsSymbol] = new Pathfinder({ data: {} });
		this[layoutStateSymbol] = {
			scheduled: false,
			running: false,
			needsMeasure: true,
			needsLayout: true,
			needsObserve: true,
			initialLayoutPending: true,
			initialLayoutOpacity: undefined,
			suppressSlotChange: false,
			suppressMutation: false,
			suppressResize: false,
		};

		initControlReferences.call(this);
		hideControlBarUntilInitialLayout.call(this);
		initEventHandler.call(this);

		// setup structure
		initControlBar.call(this);
		initPopperSwitch.call(this);
		setLayoutStackedState.call(this, false);
		applyLayoutAlignment.call(this);
		this[layoutChangedEventQueueSymbol] = new Queue();
		this.attachObserver(
			new Observer(() => {
				applyLayoutAlignment.call(this);
			}),
		);
	}

	/**
	 * Set option and sync layout state for reactive layout options.
	 *
	 * @param {string} path
	 * @param {*} value
	 * @return {ControlBar}
	 */
	setOption(path, value) {
		super.setOption(path, value);
		if (
			path === "layout.alignment" ||
			path === "layout.stackedAlignment" ||
			path === "layout.stackedBreakpoint" ||
			path === "layout.stackedBreakpointContainer"
		) {
			syncLayoutState.call(this, {
				observe: path === "layout.stackedBreakpointContainer",
			});
		}
		return this;
	}

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

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

	/**
	 * 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]);
		}

		scheduleLayout.call(this, { measure: true, layout: true, observe: true });
	}

	/**
	 * This method determines which attributes are to be monitored by `attributeChangedCallback()`.
	 *
	 * @return {string[]}
	 */
	static get observedAttributes() {
		const attributes = super.observedAttributes;
		attributes.push(ATTRIBUTE_POPPER_POSITION);
		attributes.push(ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT);
		attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
		attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT);
		attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER);
		return attributes;
	}

	/**
	 * 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]);
		}

		if (typeof this[layoutFrameSymbol] === "number") {
			cancelAnimationFrame(this[layoutFrameSymbol]);
		}
		delete this[layoutFrameSymbol];
		this[layoutTokenSymbol] = (this[layoutTokenSymbol] || 0) + 1;
		if (this[layoutStateSymbol]) {
			this[layoutStateSymbol].scheduled = false;
		}

		disconnectResizeObserver.call(this);
		defuseLayoutChangedEvent.call(this);
		if (this[mutationObserverSymbol]) {
			this[mutationObserverSymbol].disconnect();
		}
	}

	/**
	 * Close the slotted dialog.
	 * @return {ControlBar}
	 */
	hideDialog() {
		hide.call(this);
		return this;
	}

	/**
	 * Open the slotted dialog.
	 * @return {ControlBar}
	 */
	showDialog() {
		show.call(this);
		return this;
	}

	/**
	 * Toggle the slotted dialog.
	 * @return {ControlBar}
	 */
	toggleDialog() {
		toggle.call(this);
		return this;
	}
}

/**
 * @private
 * @param obj
 * @return {*}
 */
function initDefaultsFromAttributes(obj) {
	if (this.hasAttribute(ATTRIBUTE_POPPER_POSITION)) {
		obj.popper.placement = this.getAttribute(ATTRIBUTE_POPPER_POSITION);
	}

	return obj;
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isElementTrulyVisible(element) {
	if (!(element instanceof HTMLElement)) {
		return false;
	}
	const computedStyle = getComputedStyle(element);
	return (
		computedStyle.display !== "none" &&
		computedStyle.visibility !== "hidden" &&
		computedStyle.opacity !== "0" &&
		element.offsetWidth > 0 &&
		element.offsetHeight > 0
	);
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isElementSelfHidden(element) {
	if (!(element instanceof HTMLElement)) {
		return true;
	}

	const computedStyle = getComputedStyle(element);
	return (
		element.hidden === true ||
		element.hasAttribute("hidden") ||
		computedStyle.display === "none" ||
		computedStyle.visibility === "hidden"
	);
}

/**
 * @private
 * @return {void}
 */
function hideControlBarUntilInitialLayout() {
	const state = this[layoutStateSymbol];
	if (!state || !(this[controlBarElementSymbol] instanceof HTMLElement)) {
		return;
	}

	state.initialLayoutOpacity = this[controlBarElementSymbol].style.opacity;
	this[controlBarElementSymbol].style.opacity = "0";
}

/**
 * @private
 * @return {void}
 */
function revealControlBarAfterInitialLayout() {
	const state = this[layoutStateSymbol];
	if (
		!state ||
		state.initialLayoutPending !== true ||
		!(this[controlBarElementSymbol] instanceof HTMLElement)
	) {
		return;
	}

	state.initialLayoutPending = false;
	const initialOpacity = state.initialLayoutOpacity;
	if (typeof initialOpacity === "string" && initialOpacity !== "") {
		this[controlBarElementSymbol].style.opacity = initialOpacity;
		return;
	}

	this[controlBarElementSymbol].style.removeProperty("opacity");
}

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

	const mutationCallback = (mutationList) => {
		if (self[layoutStateSymbol]?.suppressMutation) {
			return;
		}

		let needsRecalc = false;
		for (const mutation of mutationList) {
			if (mutation.type === "attributes") {
				const target = mutation.target;
				if (target instanceof HTMLElement) {
					const ref = target.getAttribute("data-monster-reference");
					if (ref) {
						if (!isElementTrulyVisible(target)) {
							self[dimensionsSymbol].setVia(`data.item.${ref}`, 0);
						}
						needsRecalc = true;
					}
				}
			}
		}
		if (needsRecalc) {
			scheduleLayout.call(self, { measure: true, layout: true });
		}
	};

	/**
	 * @param {Event} event
	 */
	self[closeEventHandler] = (event) => {
		const path = event.composedPath();
		for (const element of path) {
			if (element === self) return;
		}
		hide.call(self);
	};

	if (self[controlBarSlotElementSymbol]) {
		self[controlBarSlotElementSymbol].addEventListener("slotchange", () => {
			if (self[layoutStateSymbol]?.suppressSlotChange) {
				return;
			}
			scheduleLayout.call(self, {
				measure: true,
				layout: true,
				observe: true,
			});
		});
	}

	if (self[popperElementSymbol]) {
		self[popperElementSymbol].addEventListener("slotchange", () => {
			if (self[layoutStateSymbol]?.suppressSlotChange) {
				return;
			}
			scheduleLayout.call(self, {
				measure: true,
				layout: true,
				observe: true,
			});
		});
	}

	self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = (value) => {
		self.setOption("popper.placement", value);
		updatePopper.call(self);
	};

	self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT] = () => {
		syncLayoutState.call(self);
	};

	self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT] =
		() => {
			syncLayoutState.call(self);
		};

	self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT] =
		() => {
			syncLayoutState.call(self);
		};

	self[attributeObserverSymbol][
		ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER
	] = () => {
		syncLayoutState.call(self, { observe: true });
	};

	self[resizeObserverSymbol] = new ResizeObserver((entries) => {
		if (self[layoutStateSymbol]?.suppressResize) {
			return;
		}
		if (!hasContainerResizeEntry.call(self, entries)) {
			return;
		}
		scheduleLayout.call(self, { measure: true, layout: true });
	});
	self[mutationObserverSymbol] = new MutationObserver(mutationCallback);

	initSlotChangedHandler.call(self);
}

function initSlotChangedHandler() {
	this[controlBarElementSymbol].addEventListener("slotchange", () => {
		if (this[layoutStateSymbol]?.suppressSlotChange) {
			return;
		}
		scheduleLayout.call(this, { observe: true });
	});
}

function scheduleLayout(options = {}) {
	if (!this[layoutStateSymbol]) {
		return;
	}

	const state = this[layoutStateSymbol];
	state.needsMeasure = state.needsMeasure || options.measure === true;
	state.needsLayout = state.needsLayout || options.layout === true;
	state.needsObserve = state.needsObserve || options.observe === true;

	if (state.scheduled || state.running) {
		return;
	}

	scheduleLayoutFrame.call(this);
}

function scheduleLayoutFrame() {
	const state = this[layoutStateSymbol];
	if (!state || state.scheduled || state.running) {
		return;
	}

	state.scheduled = true;
	const token = (this[layoutTokenSymbol] || 0) + 1;
	this[layoutTokenSymbol] = token;
	this[layoutFrameSymbol] = requestAnimationFrame(() => {
		if (this[layoutTokenSymbol] !== token) {
			return;
		}
		delete this[layoutFrameSymbol];
		runLayout.call(this);
	});
}

function runLayout() {
	const state = this[layoutStateSymbol];
	if (!state) {
		return;
	}

	state.scheduled = false;
	if (!this.isConnected) {
		return;
	}

	if (state.running) {
		return;
	}

	const needsObserve = state.needsObserve;
	const needsMeasure = state.needsMeasure;
	const needsLayout = state.needsLayout;

	state.needsObserve = false;
	state.needsMeasure = false;
	state.needsLayout = false;
	state.running = true;

	new Processing(() => {
		if (needsObserve) {
			updateResizeObserverObservation.call(this);
		}

		if (needsMeasure) {
			try {
				calculateControlBarDimensions.call(this);
			} catch (error) {
				addErrorAttribute(
					this,
					error?.message || "An error occurred while calculating dimensions",
				);
			}
		}

		if (needsLayout) {
			try {
				rearrangeItems.call(this);
			} catch (error) {
				addErrorAttribute(
					this,
					error?.message || "An error occurred while rearranging the items",
				);
			}
		}

		return updatePopper.call(this);
	})
		.run()
		.catch((error) => {
			addErrorAttribute(
				this,
				error?.message ||
					"An error occurred while running the control bar layout",
			);
		})
		.finally(() => {
			state.running = false;
			revealControlBarAfterInitialLayout.call(this);
			if (state.needsObserve || state.needsMeasure || state.needsLayout) {
				scheduleLayoutFrame.call(this);
			}
		});
}

/**
 * @private
 * @return {Object}
 */
function rearrangeItems() {
	let space = 0;
	try {
		space = this[dimensionsSymbol].getVia("data.space");
	} catch {}

	const itemReferences = this[dimensionsSymbol].getVia(
		"data.itemReferences",
		[],
	);
	const hasItems = itemReferences.length > 0;

	const itemEntries = [];

	for (const ref of itemReferences) {
		let elements = getSlottedElements.call(
			this,
			'[data-monster-reference="' + ref + '"]',
			null,
		); // null ↦ o
		if (elements.size === 0) {
			elements = getSlottedElements.call(
				this,
				'[data-monster-reference="' + ref + '"]',
				"popper",
			); // null ↦ o
		}

		const nextValue = elements.values().next();
		if (!nextValue) {
			continue;
		}

		const element = nextValue?.value;
		if (!(element instanceof HTMLElement)) {
			continue;
		}

		let itemWidth = 0;
		try {
			itemWidth = this[dimensionsSymbol].getVia(`data.item.${ref}`);
		} catch {
			// If the path does not exist, pathfinder throws an error.
			// In this case, we assume the width is 0.
			// This can happen for items that have never been visible.
		}
		itemEntries.push({
			element,
			width: itemWidth,
			hidden: isElementSelfHidden(element),
		});
	}

	const switchWidth = this[dimensionsSymbol].getVia("data.switchWidth") || 2;

	const layoutItems = (availableSpace) => {
		if (availableSpace < 0) {
			availableSpace = 0;
		}
		let sum = 0;
		let overflowStarted = false;
		const visibleItemsInMainSlot = [];
		const itemsToMoveToPopper = [];

		for (const entry of itemEntries) {
			if (entry.hidden) {
				visibleItemsInMainSlot.push(entry.element);
				continue;
			}

			if (overflowStarted || sum + entry.width > availableSpace) {
				overflowStarted = true;
				itemsToMoveToPopper.push(entry.element);
			} else {
				sum += entry.width;
				visibleItemsInMainSlot.push(entry.element);
			}
		}

		return { visibleItemsInMainSlot, itemsToMoveToPopper };
	};

	const switchCurrentlyVisible =
		this[switchElementSymbol] instanceof HTMLElement &&
		!this[switchElementSymbol].hasAttribute("hidden");
	let layout;
	if (switchCurrentlyVisible) {
		layout = layoutItems(space - switchWidth - LAYOUT_SWITCH_HYSTERESIS);
		if (layout.itemsToMoveToPopper.length === 0) {
			layout = layoutItems(space);
		}
	} else {
		layout = layoutItems(space - LAYOUT_SWITCH_HYSTERESIS);
		if (layout.itemsToMoveToPopper.length > 0) {
			layout = layoutItems(space - switchWidth);
		} else {
			layout = layoutItems(space);
		}
	}

	layout = normalizeSpacerOverflowLayout(
		layout,
		itemEntries.map((entry) => entry.element),
	);
	layout = stabilizeOverflowBoundary.call(
		this,
		layout,
		itemEntries,
		switchCurrentlyVisible,
	);

	const shouldShowSwitch = layout.itemsToMoveToPopper.length > 0 && hasItems;
	updateLastOverflowBoundary.call(
		this,
		layout,
		itemEntries.map((entry) => entry.element),
	);
	const shouldUseStackedLayout =
		shouldShowSwitch || isStackedBreakpointMatched.call(this);

	suppressLayoutFeedback.call(this);
	setLayoutStackedState.call(this, shouldUseStackedLayout);

	for (const item of layout.itemsToMoveToPopper) {
		if (item.getAttribute("slot") !== "popper") {
			item.setAttribute("slot", "popper");
		}
	}

	for (const item of layout.visibleItemsInMainSlot) {
		if (item.hasAttribute("slot")) {
			item.removeAttribute("slot");
		}
	}

	setSwitchVisible.call(this, shouldShowSwitch);
	updateControlSizing.call(this, layout, shouldShowSwitch);
	updateJoinedBorders.call(this, layout, shouldShowSwitch);
	applyLayoutAlignment.call(this);
	if (!shouldShowSwitch) {
		hide.call(this);
	}
}

/**
 * @private
 * @param {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}} layout
 * @param {{element: HTMLElement, hidden: boolean}[]} itemEntries
 * @param {boolean} switchCurrentlyVisible
 * @return {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}}
 */
function stabilizeOverflowBoundary(layout, itemEntries, switchCurrentlyVisible) {
	const state = this[layoutStateSymbol];
	if (!switchCurrentlyVisible || !state) {
		return layout;
	}

	const orderedItems = itemEntries.map((entry) => entry.element);
	const nextBoundary = getFirstOverflowIndex(layout, orderedItems);
	const previousBoundary = state.lastOverflowStartIndex;
	if (
		typeof previousBoundary !== "number" ||
		typeof nextBoundary !== "number" ||
		nextBoundary <= previousBoundary ||
		previousBoundary < 0 ||
		previousBoundary >= itemEntries.length
	) {
		return layout;
	}

	const popperItems = new Set();
	const mainItems = new Set();
	for (let index = 0; index < itemEntries.length; index++) {
		const entry = itemEntries[index];
		if (index >= previousBoundary && !entry.hidden) {
			popperItems.add(entry.element);
		} else {
			mainItems.add(entry.element);
		}
	}

	return normalizeSpacerOverflowLayout(
		{
			visibleItemsInMainSlot: orderedItems.filter((item) => mainItems.has(item)),
			itemsToMoveToPopper: orderedItems.filter((item) => popperItems.has(item)),
		},
		orderedItems,
	);
}

/**
 * @private
 * @param {{itemsToMoveToPopper: HTMLElement[]}} layout
 * @param {HTMLElement[]} orderedItems
 * @return {void}
 */
function updateLastOverflowBoundary(layout, orderedItems) {
	const state = this[layoutStateSymbol];
	if (!state) {
		return;
	}

	const boundary = getFirstOverflowIndex(layout, orderedItems);
	if (typeof boundary === "number") {
		state.lastOverflowStartIndex = boundary;
		return;
	}
	delete state.lastOverflowStartIndex;
}

/**
 * @private
 * @param {{itemsToMoveToPopper: HTMLElement[]}} layout
 * @param {HTMLElement[]} orderedItems
 * @return {number|undefined}
 */
function getFirstOverflowIndex(layout, orderedItems) {
	const popperItems = new Set(layout.itemsToMoveToPopper);
	for (let index = 0; index < orderedItems.length; index++) {
		if (popperItems.has(orderedItems[index])) {
			return index;
		}
	}
	return undefined;
}

/**
 * @private
 * @param {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}} layout
 * @param {HTMLElement[]} orderedItems
 * @return {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}}
 */
function normalizeSpacerOverflowLayout(layout, orderedItems) {
	if (
		!layout.itemsToMoveToPopper.some(
			(item) => !isControlBarSpacerElement(item),
		)
	) {
		return layout;
	}

	const popperItems = new Set(layout.itemsToMoveToPopper);
	const mainItems = new Set(layout.visibleItemsInMainSlot);

	for (let index = 0; index < orderedItems.length; index++) {
		const item = orderedItems[index];
		if (!isControlBarSpacerElement(item) || !mainItems.has(item)) {
			continue;
		}

		const previous = findAdjacentNonSpacerItem(orderedItems, index, -1);
		const next = findAdjacentNonSpacerItem(orderedItems, index, 1);
		if (popperItems.has(previous) || popperItems.has(next)) {
			mainItems.delete(item);
			popperItems.add(item);
		}
	}

	return {
		visibleItemsInMainSlot: orderedItems.filter((item) => mainItems.has(item)),
		itemsToMoveToPopper: orderedItems.filter((item) => popperItems.has(item)),
	};
}

/**
 * @private
 * @param {HTMLElement[]} orderedItems
 * @param {number} startIndex
 * @param {1|-1} direction
 * @return {HTMLElement|undefined}
 */
function findAdjacentNonSpacerItem(orderedItems, startIndex, direction) {
	for (
		let index = startIndex + direction;
		index >= 0 && index < orderedItems.length;
		index += direction
	) {
		const item = orderedItems[index];
		if (!isControlBarSpacerElement(item)) {
			return item;
		}
	}
	return undefined;
}

/**
 * @private
 * @param {HTMLElement} node
 * @return {number}
 */
function calcBoxWidth(node) {
	const dim = getGlobal()?.getComputedStyle(node);
	if (dim === null) {
		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "no computed style");
		throw new Error("no computed style");
	}
	const bounding = node.getBoundingClientRect();

	return (
		getComputedCssPixels(dim["border-left-width"]) +
		getComputedCssPixels(dim["padding-left"]) +
		getComputedCssPixels(dim["margin-left"]) +
		bounding["width"] +
		getComputedCssPixels(dim["border-right-width"]) +
		getComputedCssPixels(dim["margin-right"]) +
		getComputedCssPixels(dim["padding-right"])
	);
}

/**
 * @private
 * @param {string} value
 * @return {number}
 */
function getComputedCssPixels(value) {
	if (typeof value !== "string" || value === "") {
		return 0;
	}

	if (value.endsWith("px")) {
		return parseFloat(value) || 0;
	}

	try {
		return convertToPixels(value);
	} catch {
		return 0;
	}
}

/**
 * @private
 * @param {Object} layout
 * @param {boolean} shouldShowSwitch
 * @return {void}
 */
function updateControlSizing(layout, shouldShowSwitch) {
	const mainItems = [...layout.visibleItemsInMainSlot];
	if (shouldShowSwitch && this[popperNavElementSymbol] instanceof HTMLElement) {
		mainItems.push(this[popperNavElementSymbol]);
	}

	const controlItems = [...mainItems, ...layout.itemsToMoveToPopper];
	for (const element of controlItems) {
		applyControlSizing.call(this, element);
	}
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {void}
 */
function applyControlSizing(element) {
	setStylePropertyIfChanged(element, "boxSizing", "border-box");
	setStylePropertyIfChanged(
		element,
		"height",
		"var(--monster-control-bar-height)",
	);
	setStylePropertyIfChanged(
		element,
		"minHeight",
		"var(--monster-control-bar-height)",
	);
	if (isWrapperElement(element)) {
		setStylePropertyIfChanged(element, "display", "flex");
		setStylePropertyIfChanged(element, "alignItems", "stretch");
	}

	for (const control of getContainedControlElements(element)) {
		setStylePropertyIfChanged(control, "display", "flex");
		setStylePropertyIfChanged(control, "boxSizing", "border-box");
		setStylePropertyIfChanged(control, "height", "100%");
		setStylePropertyIfChanged(control, "minHeight", "0px");
		if (control.tagName === "MONSTER-INPUT-GROUP") {
			setStylePropertyIfChanged(control, "flex", "1 1 auto");
		}
	}
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isWrapperElement(element) {
	return ["DIV", "FORM", "SECTION"].includes(element.tagName);
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {HTMLElement[]}
 */
function getContainedControlElements(element) {
	if (!isWrapperElement(element)) {
		return [];
	}

	return Array.from(
		element.querySelectorAll(
			[
				"monster-input-group",
				"monster-select",
				"monster-button",
				"monster-state-button",
				"monster-message-state-button",
				"monster-action-button",
				"monster-api-button",
				"monster-confirm-button",
			].join(","),
		),
	).filter((control) => control instanceof HTMLElement);
}

/**
 * @private
 * @param {Object} layout
 * @param {boolean} shouldShowSwitch
 * @return {void}
 */
function updateJoinedBorders(layout, shouldShowSwitch) {
	const mainItems = [...layout.visibleItemsInMainSlot];
	if (
		shouldShowSwitch &&
		this[popperNavElementSymbol] instanceof HTMLElement &&
		this[switchElementSymbol] instanceof HTMLElement
	) {
		mainItems.push(this[popperNavElementSymbol]);
	}

	const marginLeftByElement = new Map();
	const marginTopByElement = new Map();

	collectInlineJoinedBorders.call(this, mainItems, marginLeftByElement);
	collectBlockJoinedBorders.call(
		this,
		layout.itemsToMoveToPopper,
		marginTopByElement,
	);
	updateSpacerLineWidths.call(this, mainItems, "inline", "right", "left");
	updateSpacerLineWidths.call(
		this,
		layout.itemsToMoveToPopper,
		"block",
		"bottom",
		"top",
	);
	applyJoinedBorderOffsets.call(this, marginLeftByElement, marginTopByElement);
}

/**
 * @private
 * @param {HTMLElement[]} elements
 * @param {"inline"|"block"} axis
 * @param {"right"|"bottom"} previousSide
 * @param {"left"|"top"} nextSide
 * @return {void}
 */
function updateSpacerLineWidths(elements, axis, previousSide, nextSide) {
	const property =
		axis === "inline"
			? "--monster-control-bar-spacer-line-inline-size"
			: "--monster-control-bar-spacer-line-block-size";

	for (let i = 0; i < elements.length; i++) {
		const spacer = elements[i];
		if (!isControlBarSpacerElement(spacer)) {
			continue;
		}

		const adjacentBorderWidths = [];
		const previous = elements[i - 1];
		const next = elements[i + 1];
		if (previous instanceof HTMLElement) {
			adjacentBorderWidths.push(
				getVisualBorderWidth.call(this, previous, previousSide),
			);
		}
		if (next instanceof HTMLElement) {
			adjacentBorderWidths.push(
				getVisualBorderWidth.call(this, next, nextSide),
			);
		}

		const lineWidth = Math.max(0, ...adjacentBorderWidths);
		setStylePropertyIfChanged(
			spacer,
			property,
			lineWidth > 0 ? `${lineWidth}px` : "",
		);
	}
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isControlBarSpacerElement(element) {
	return element.tagName === "MONSTER-CONTROL-BAR-SPACER";
}

/**
 * @private
 * @param {HTMLElement[]} elements
 * @param {Map<HTMLElement, string>} marginLeftByElement
 * @return {void}
 */
function collectInlineJoinedBorders(elements, marginLeftByElement) {
	for (let i = 1; i < elements.length; i++) {
		const previous = elements[i - 1];
		const current = elements[i];
		const overlap = getBorderOverlap(
			getVisualBorderWidth.call(this, previous, "right"),
			getVisualBorderWidth.call(this, current, "left"),
		);

		if (overlap > 0) {
			marginLeftByElement.set(current, `${-1 * overlap}px`);
		}
	}
}

/**
 * @private
 * @param {HTMLElement[]} elements
 * @param {Map<HTMLElement, string>} marginTopByElement
 * @return {void}
 */
function collectBlockJoinedBorders(elements, marginTopByElement) {
	for (let i = 1; i < elements.length; i++) {
		const previous = elements[i - 1];
		const current = elements[i];
		const overlap = getBorderOverlap(
			getVisualBorderWidth.call(this, previous, "bottom"),
			getVisualBorderWidth.call(this, current, "top"),
		);

		if (overlap > 0) {
			marginTopByElement.set(current, `${-1 * overlap}px`);
		}
	}
}

/**
 * @private
 * @param {Map<HTMLElement, string>} marginLeftByElement
 * @param {Map<HTMLElement, string>} marginTopByElement
 * @return {void}
 */
function applyJoinedBorderOffsets(marginLeftByElement, marginTopByElement) {
	for (const element of getJoinedBorderOffsetElements.call(this)) {
		setStylePropertyIfChanged(
			element,
			"marginLeft",
			marginLeftByElement.get(element) || "",
		);
		setStylePropertyIfChanged(
			element,
			"marginTop",
			marginTopByElement.get(element) || "",
		);
	}
}

/**
 * @private
 * @return {HTMLElement[]}
 */
function getJoinedBorderOffsetElements() {
	const elements = Array.from(this.children).filter(
		(element) => element instanceof HTMLElement,
	);
	if (this[popperNavElementSymbol] instanceof HTMLElement) {
		elements.push(this[popperNavElementSymbol]);
	}

	return elements;
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {"marginLeft"|"marginTop"} property
 * @param {string} value
 * @return {void}
 */
function setStylePropertyIfChanged(element, property, value) {
	if (property.startsWith("--")) {
		if (element.style.getPropertyValue(property) !== value) {
			if (value === "") {
				element.style.removeProperty(property);
			} else {
				element.style.setProperty(property, value);
			}
		}
		return;
	}

	if (element.style[property] !== value) {
		element.style[property] = value;
	}
}

/**
 * @private
 * @param {number} previousBorderWidth
 * @param {number} currentBorderWidth
 * @return {number}
 */
function getBorderOverlap(previousBorderWidth, currentBorderWidth) {
	return Math.min(previousBorderWidth, currentBorderWidth);
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {"top"|"right"|"bottom"|"left"} side
 * @return {number}
 */
function getVisualBorderWidth(element, side) {
	const visualElement = getVisualBorderElement.call(this, element, side);
	const computedStyle = getComputedStyle(visualElement);
	const value = computedStyle.getPropertyValue(`border-${side}-width`);

	return getComputedCssPixels(value);
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {"top"|"right"|"bottom"|"left"} side
 * @return {HTMLElement}
 */
function getVisualBorderElement(element, side) {
	if (element === this[popperNavElementSymbol]) {
		return this[switchElementSymbol] || element;
	}

	const selector = [
		"button",
		"input",
		"select",
		"textarea",
		"monster-input-group",
		"monster-select",
		"[data-monster-role=control]",
	].join(",");
	if (element.shadowRoot instanceof ShadowRoot) {
		return (
			element.shadowRoot.querySelector("button,input,select,textarea") ||
			element.shadowRoot.querySelector(
				"monster-input-group,monster-select,[data-monster-role=control]",
			) ||
			element
		);
	}

	const candidates = Array.from(element.querySelectorAll(selector)).filter(
		(candidate) => candidate instanceof HTMLElement,
	);
	if (candidates.length === 0) {
		return element;
	}

	if (side === "right" || side === "bottom") {
		return candidates[candidates.length - 1];
	}

	return candidates[0];
}

/**
 * @private
 * @return {Object}
 */
function calculateControlBarDimensions() {
	if (!(this.parentElement instanceof HTMLElement)) {
		this[dimensionsSymbol].setVia("data.space", 0);
		this[dimensionsSymbol].setVia("data.visible", false);
		this[dimensionsSymbol].setVia("data.calculated", true);
		this[dimensionsSymbol].setVia("data.itemReferences", []);
		return;
	}

	const computedStyle = getComputedStyle(this.parentElement);

	if (computedStyle === null) {
		throw new Error("no computed style");
	}

	let width = this.parentElement.clientWidth;
	let containerWidth = width;
	if (computedStyle.getPropertyValue("box-sizing") !== "border-box") {
		width = computedStyle.getPropertyValue("width");

		const pixel = getComputedCssPixels(width);
		containerWidth = pixel;

		this[dimensionsSymbol].setVia("data.space", pixel);
	} else {
		let borderWidth = getComputedStyle(this).getPropertyValue(
			"--monster-border-width",
		);
		if (borderWidth === null || borderWidth === "") {
			borderWidth = "0px";
		}

		const borderWidthWithoutUnit = getComputedCssPixels(borderWidth);

		// space to be allocated
		this[dimensionsSymbol].setVia(
			"data.space",
			width - 2 * borderWidthWithoutUnit,
		);
	}

	this[dimensionsSymbol].setVia("data.visible", !(containerWidth === 0));
	this[dimensionsSymbol].setVia("data.containerWidth", containerWidth);
	this[dimensionsSymbol].setVia(
		"data.stackedBreakpointContainerWidth",
		getStackedBreakpointContainerWidth.call(this),
	);

	const itemReferences = [];

	// Get all direct controls, regardless of their current slot.
	const combinedItems = Array.from(this.children).filter(
		(item, index, self) => {
			if (!(item instanceof HTMLElement)) {
				return false;
			}
			if (item.slot && item.slot !== "popper") {
				return false;
			}
			// Filter out duplicates based on data-monster-reference if present, or element itself
			return (
				self.findIndex((other) => {
					if (!(other instanceof HTMLElement)) {
						return false;
					}
					const itemReference = item.dataset.monsterReference;
					const otherReference = other.dataset.monsterReference;
					if (itemReference && otherReference) {
						return itemReference === otherReference;
					}
					return other === item;
				}) === index
			);
		},
	);

	for (const item of combinedItems) {
		if (!(item instanceof HTMLElement)) {
			continue;
		}

		if (!item.hasAttribute("data-monster-reference")) {
			item.setAttribute("data-monster-reference", new ID("ctrl").toString());
		}

		const ref = item.getAttribute("data-monster-reference");
		if (ref === null) continue;

		itemReferences.push(ref);

		// Only calculate width for visible items. Assume invisible ones
		// (e.g. in popper) have their width calculated previously and stored.
		if (isElementTrulyVisible(item)) {
			this[dimensionsSymbol].setVia(
				`data.item.${ref}`,
				calcBoxWidth.call(this, item),
			);
		}
	}

	if (this[switchElementSymbol]) {
		this[dimensionsSymbol].setVia(
			"data.switchWidth",
			this[switchElementSymbol].offsetWidth,
		);
	}

	this[dimensionsSymbol].setVia("data.calculated", true);
	this[dimensionsSymbol].setVia("data.itemReferences", itemReferences);
}

/**
 * @private
 * @return {number}
 */
function getStackedBreakpointContainerWidth() {
	const container = resolveStackedBreakpointContainer.call(this);
	if (!(container instanceof HTMLElement)) {
		return 0;
	}

	return getElementLayoutWidth(container);
}

/**
 * @private
 * @return {HTMLElement|undefined}
 */
function resolveStackedBreakpointContainer() {
	const selector = this.getOption("layout.stackedBreakpointContainer");
	if (typeof selector !== "string" || selector.trim() === "") {
		return undefined;
	}

	try {
		const closest = this.closest(selector);
		if (closest instanceof HTMLElement) {
			return closest;
		}

		const root = this.getRootNode();
		if (root && typeof root.querySelector === "function") {
			const found = root.querySelector(selector);
			if (found instanceof HTMLElement) {
				return found;
			}
		}
	} catch {}

	return undefined;
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {number}
 */
function getElementLayoutWidth(element) {
	const computedStyle = getComputedStyle(element);
	if (computedStyle === null) {
		return 0;
	}

	if (computedStyle.getPropertyValue("box-sizing") !== "border-box") {
		return getComputedCssPixels(computedStyle.getPropertyValue("width"));
	}

	return element.clientWidth;
}

/**
 * @private
 */
function updateResizeObserverObservation() {
	const observedNodes = getLayoutObservedNodes.call(this);
	const signature = getLayoutObservedNodesSignature(observedNodes);
	if (this[observedLayoutNodesSignatureSymbol] === signature) {
		return;
	}
	this[observedLayoutNodesSignatureSymbol] = signature;

	this[resizeObserverSymbol].disconnect();
	if (this[mutationObserverSymbol]) {
		this[mutationObserverSymbol].disconnect();
	}

	observedNodes.forEach((node) => {
		this[resizeObserverSymbol].observe(node);
		if (node !== this.parentElement && this[mutationObserverSymbol]) {
			this[mutationObserverSymbol].observe(node, {
				attributes: true,
				attributeFilter: ["style", "class", "hidden"],
			});
		}
	});
}

/**
 * @private
 * @return {HTMLElement[]}
 */
function getLayoutObservedNodes() {
	const observedNodes = [];
	const addObservedNode = (node) => {
		if (node instanceof HTMLElement && !observedNodes.includes(node)) {
			observedNodes.push(node);
		}
	};

	Array.from(this.children).forEach(addObservedNode);

	let parent = this.parentNode;
	while (!(parent instanceof HTMLElement) && parent !== null) {
		parent = parent.parentNode;
	}

	addObservedNode(parent);
	addObservedNode(resolveStackedBreakpointContainer.call(this));

	return observedNodes;
}

/**
 * @private
 * @param {HTMLElement[]} nodes
 * @return {string}
 */
function getLayoutObservedNodesSignature(nodes) {
	return nodes.map(getLayoutNodeId).join("|");
}

/**
 * @private
 * @param {HTMLElement} node
 * @return {number}
 */
function getLayoutNodeId(node) {
	let id = layoutNodeIds.get(node);
	if (id === undefined) {
		id = ++layoutNodeId;
		layoutNodeIds.set(node, id);
	}
	return id;
}

/**
 * @private
 * @return {void}
 */
function suppressLayoutFeedback() {
	const state = this[layoutStateSymbol];
	if (!state) {
		return;
	}

	state.suppressSlotChange = true;
	state.suppressMutation = true;
	state.suppressResize = true;
	requestAnimationFrame(() => {
		state.suppressSlotChange = false;
		state.suppressMutation = false;
		state.suppressResize = false;
	});
}

/**
 * @private
 * @param {ResizeObserverEntry[]} entries
 * @return {boolean}
 */
function hasContainerResizeEntry(entries) {
	const stackedBreakpointContainer =
		resolveStackedBreakpointContainer.call(this);
	return entries.some((entry) => {
		return (
			entry.target === this.parentElement ||
			entry.target === stackedBreakpointContainer
		);
	});
}

/**
 * @private
 * @param {boolean} visible
 * @return {void}
 */
function setSwitchVisible(visible) {
	if (!(this[switchElementSymbol] instanceof HTMLElement)) {
		return;
	}

	if (visible) {
		if (this[switchElementSymbol].hasAttribute("hidden")) {
			this[switchElementSymbol].removeAttribute("hidden");
		}
		if (this[switchElementSymbol].classList.contains("hidden")) {
			this[switchElementSymbol].classList.remove("hidden");
		}
		return;
	}

	if (!this[switchElementSymbol].hasAttribute("hidden")) {
		this[switchElementSymbol].setAttribute("hidden", "");
	}
	if (!this[switchElementSymbol].classList.contains("hidden")) {
		this[switchElementSymbol].classList.add("hidden");
	}
}

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

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

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

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

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

	this[popperElementSymbol].style.visibility = "hidden";
	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;

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

	updatePopper.call(this);
}

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

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

	positionPopper.call(
		this,
		this[switchElementSymbol],
		this[popperElementSymbol],
		this.getOption("popper", {}),
	);
}

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

	const layoutState = this[layoutStateSymbol] || {};
	const stackedAlignment = this.getOption("layout.stackedAlignment");
	let alignment = this.getOption("layout.alignment", "left");
	if (
		layoutState.stacked === true &&
		typeof stackedAlignment === "string" &&
		stackedAlignment !== ""
	) {
		alignment = stackedAlignment;
	}

	if (alignment === "right") {
		if (
			setAttributeIfChanged(
				this[controlBarElementSymbol],
				ATTRIBUTE_LAYOUT_ALIGNMENT,
				"right",
			)
		) {
			queueLayoutChangedEvent.call(this);
		}
		return;
	}

	if (
		setAttributeIfChanged(
			this[controlBarElementSymbol],
			ATTRIBUTE_LAYOUT_ALIGNMENT,
			"left",
		)
	) {
		queueLayoutChangedEvent.call(this);
	}
}

/**
 * @private
 * @param {{observe?: boolean}} options
 * @return {void}
 */
function syncLayoutState(options = {}) {
	applyLayoutAlignment.call(this);
	scheduleLayout.call(this, {
		measure: true,
		layout: true,
		observe: options.observe === true,
	});
}

/**
 * @private
 * @return {boolean}
 */
function isStackedBreakpointMatched() {
	const breakpoint = this.getOption("layout.stackedBreakpoint");
	if (typeof breakpoint !== "string" || breakpoint.trim() === "") {
		return false;
	}

	let width = 0;
	try {
		width = this[dimensionsSymbol].getVia(
			"data.stackedBreakpointContainerWidth",
		);
	} catch {}

	if (!(width > 0)) {
		try {
			width = this[dimensionsSymbol].getVia("data.containerWidth");
		} catch {
			try {
				width = this[dimensionsSymbol].getVia("data.space");
			} catch {}
		}
	}

	if (!(width > 0)) {
		width = getComputedCssPixels(width);
	}

	if (!(width > 0)) {
		return false;
	}

	const breakpointWidth = getComputedCssPixels(breakpoint);
	if (!(breakpointWidth > 0)) {
		return false;
	}

	return width <= breakpointWidth;
}

/**
 * @private
 * @param {boolean} stacked
 * @return {void}
 */
function setLayoutStackedState(stacked) {
	if (!this[layoutStateSymbol]) {
		return;
	}
	this[layoutStateSymbol].stacked = stacked === true;

	if (!(this[controlBarElementSymbol] instanceof HTMLElement)) {
		return;
	}

	if (
		setAttributeIfChanged(
			this[controlBarElementSymbol],
			ATTRIBUTE_LAYOUT_STACKED,
			this[layoutStateSymbol].stacked ? "true" : "false",
		)
	) {
		queueLayoutChangedEvent.call(this);
	}
	applyLayoutAlignment.call(this);
}

/**
 * @private
 * @return {void}
 */
function queueLayoutChangedEvent() {
	if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
		return;
	}

	this[layoutChangedEventQueueSymbol].add(
		getLayoutChangedEventDetail.call(this),
	);

	if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
		this[layoutChangedEventSwitchSymbol].touch();
		return;
	}

	this[layoutChangedEventSwitchSymbol] = new DeadMansSwitch(
		LAYOUT_CHANGED_EVENT_DELAY,
		() => {
			this[layoutChangedEventSwitchSymbol] = undefined;
			dispatchQueuedLayoutChangedEvent.call(this);
		},
	);
}

/**
 * @private
 * @return {void}
 */
function dispatchQueuedLayoutChangedEvent() {
	if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
		return;
	}

	let detail;
	while (!this[layoutChangedEventQueueSymbol].isEmpty()) {
		detail = this[layoutChangedEventQueueSymbol].poll();
	}

	if (!detail) {
		return;
	}

	const signature = JSON.stringify(detail);
	if (this[layoutChangedEventLastSignatureSymbol] === signature) {
		return;
	}
	this[layoutChangedEventLastSignatureSymbol] = signature;

	this.dispatchEvent(
		new CustomEvent(EVENT_LAYOUT_CHANGED, {
			bubbles: true,
			composed: true,
			detail,
		}),
	);
}

/**
 * @private
 * @return {void}
 */
function defuseLayoutChangedEvent() {
	if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
		this[layoutChangedEventSwitchSymbol].defuse();
		this[layoutChangedEventSwitchSymbol] = undefined;
	}
}

/**
 * @private
 * @return {Object}
 */
function getLayoutChangedEventDetail() {
	const layoutState = this[layoutStateSymbol] || {};
	const stacked = layoutState.stacked === true;
	const alignment = this[controlBarElementSymbol]?.getAttribute(
		ATTRIBUTE_LAYOUT_ALIGNMENT,
	);

	return {
		alignment: alignment || this.getOption("layout.alignment", "left"),
		configuredAlignment: this.getOption("layout.alignment", "left"),
		stacked,
		stackedAlignment: this.getOption("layout.stackedAlignment"),
		stackedBreakpoint: this.getOption("layout.stackedBreakpoint"),
		stackedBreakpointContainer: this.getOption(
			"layout.stackedBreakpointContainer",
		),
	};
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {string} attribute
 * @param {string} value
 * @return {boolean}
 */
function setAttributeIfChanged(element, attribute, value) {
	if (element.getAttribute(attribute) !== value) {
		element.setAttribute(attribute, value);
		return true;
	}

	return false;
}

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

	this[controlBarSlotElementSymbol] = null;
	if (this[controlBarElementSymbol])
		this[controlBarSlotElementSymbol] =
			this[controlBarElementSymbol].querySelector(`slot`);
	this[popperSlotElementSymbol] = null;
	if (this[popperElementSymbol])
		this[popperSlotElementSymbol] =
			this[popperElementSymbol].querySelector(`slot`);
}

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

	scheduleLayout.call(this, { measure: true, layout: true, observe: true });
}

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

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

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

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="control-bar">
                <slot></slot>
                <div part="popper-nav" data-monster-role="popper-nav"
                     tabindex="-1">
                    <button part="popper-switch" data-monster-role="switch"
                            class="monster-button-outline-tertiary hidden" hidden>
                        <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>
                    </button>
                </div>
            </div>
            <div data-monster-role="popper">
                <slot name="popper"></slot>
            </div>
        </div>`;
}

registerCustomElement(ControlBar);
