/**
 * Copyright © Volker Schukai and all contributing authors, 2025. 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 {
	CustomElement,
	getSlottedElements,
	registerCustomElement,
	assembleMethodSymbol,
} from "../../dom/customelement.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { SiteNavigationStyleSheet } from "./stylesheet/site-navigation.mjs";
import {
	computePosition,
	autoUpdate,
	flip,
	shift,
	offset,
	size,
} from "@floating-ui/dom";
import { fireCustomEvent } from "../../dom/events.mjs";

export { SiteNavigation };

const resizeObserverSymbol = Symbol("resizeObserver");
const timerCallbackSymbol = Symbol("timerCallback");
const navElementSymbol = Symbol("navElement");
const visibleElementsSymbol = Symbol("visibleElements");
const hiddenElementsSymbol = Symbol("hiddenElements");
const hamburgerButtonSymbol = Symbol("hamburgerButton");
const hamburgerNavSymbol = Symbol("hamburgerNav");
const instanceSymbol = Symbol("instanceSymbol");
const activeSubmenuHiderSymbol = Symbol("activeSubmenuHider");
const hideHamburgerMenuSymbol = Symbol("hideHamburgerMenu");
const hamburgerCloseButtonSymbol = Symbol("hamburgerCloseButton");
const layoutFrameSymbol = Symbol("layoutFrame");
const layoutSignatureSymbol = Symbol("layoutSignature");
const measurementCacheSymbol = Symbol("measurementCache");

const navigationItemIds = new WeakMap();
let navigationItemId = 0;

function getAutoUpdateOptions() {
	return {
		elementResize: typeof ResizeObserver === "function",
		layoutShift: false,
	};
}

/**
 * A responsive site navigation that automatically moves menu items into a hamburger menu
 * when there isn't enough available space.
 *
 * @summary A site navigation component for primary menus with adaptive submenu behavior.
 * @fragments /fragments/components/navigation/site-navigation/
 *
 * @example /examples/components/navigation/site-navigation-simple Simple Navigation
 * @example /examples/components/navigation/site-navigation-with-submenus Navigation with Submenus
 * @example /examples/components/navigation/site-navigation-with-mega-menu Navigation with Mega Menu
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/336.html
 *
 * @fires monster-layout-change - Fired when the layout of menu items changes. The event detail contains `{visibleItems, hiddenItems}`.
 * @fires monster-hamburger-show - Fired when the hamburger menu is shown. The event detail contains `{button, menu}`.
 * @fires monster-hamburger-hide - Fired when the hamburger menu is hidden. The event detail contains `{button, menu}`.
 * @fires monster-submenu-show - Fired when a submenu is shown. The event detail contains `{context, trigger, submenu, level}`.
 * @fires monster-submenu-hide - Fired when a submenu is hidden. The event detail contains `{context, trigger, submenu, level}`.
 */
class SiteNavigation extends CustomElement {
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/navigation/site@@instance");
	}

	/**
	 * Configuration options for the SiteNavigation component.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * To set these options via an HTML tag, use the `data-monster-options` attribute.
	 * The individual configuration values are detailed in the table below.
	 *
	 * @property {Object} templates - Template definitions.
	 * @property {string} templates.main - The main HTML template for the component.
	 * @property {string} interactionModel="auto" - Defines the interaction with submenus. Possible values: `auto`, `click`, `hover`. With `auto`, `hover` is used on desktop and `click` is used in the hamburger menu.
	 * @property {Object} features - Container for additional feature flags.
	 * @property {boolean} features.resetOnClose=true - If `true`, all open submenus within the hamburger menu will be reset when it is closed.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: { main: getTemplate() },
			interactionModel: "auto", // 'auto', 'click', 'hover'
			features: {
				resetOnClose: true,
			},
		});
	}

	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initEventHandler.call(this);
	}

	static getCSSStyleSheet() {
		return [SiteNavigationStyleSheet];
	}

	static getTag() {
		return "monster-site-navigation";
	}

	connectedCallback() {
		super.connectedCallback();
		attachResizeObserver.call(this);
		schedulePopulateTabs.call(this);
	}

	disconnectedCallback() {
		super.disconnectedCallback();
		detachResizeObserver.call(this);
		if (typeof this[layoutFrameSymbol] === "number") {
			cancelAnimationFrame(this[layoutFrameSymbol]);
		}
		this[layoutFrameSymbol] = null;
	}
}

/**
 * Queries the shadow DOM for essential elements and stores their references.
 * @private
 * @this {SiteNavigation}
 */
function initControlReferences() {
	if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
	this[navElementSymbol] = this.shadowRoot.querySelector(
		'[data-monster-role="navigation"]',
	);
	this[visibleElementsSymbol] =
		this.shadowRoot.querySelector("#visible-elements");
	this[hiddenElementsSymbol] =
		this.shadowRoot.querySelector("#hidden-elements");
	this[hamburgerButtonSymbol] =
		this.shadowRoot.querySelector("#hamburger-button");
	this[hamburgerNavSymbol] = this.shadowRoot.querySelector(
		'[data-monster-role="hamburger-nav"]',
	);
	this[hamburgerCloseButtonSymbol] = this.shadowRoot.querySelector(
		'[part="hamburger-close-button"]',
	);
}

/**
 * Initializes event handlers for the hamburger menu button and its functionality.
 * @private
 * @this {SiteNavigation}
 */
function initEventHandler() {
	if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");

	const hamburgerButton = this[hamburgerButtonSymbol];
	const hamburgerNav = this[hamburgerNavSymbol];
	const hamburgerCloseButton = this[hamburgerCloseButtonSymbol];
	let cleanup;

	if (!hamburgerButton || !hamburgerNav || !hamburgerCloseButton) return;

	const getBestPositionStrategy = (element) => {
		let parent = element.parentElement;
		while (parent) {
			const parentPosition = window.getComputedStyle(parent).position;
			if (["fixed", "sticky"].includes(parentPosition)) {
				return "fixed";
			}
			parent = parent.parentElement;
		}
		return "absolute";
	};

	const handleOutsideClick = (event) => {
		if (
			!hamburgerButton.contains(event.target) &&
			!hamburgerNav.contains(event.target)
		) {
			hideMenu();
		}
	};

	const hideMenu = () => {
		hamburgerNav.style.display = "none";
		document.body.classList.remove("monster-navigation-open");

		fireCustomEvent(this, "monster-hamburger-hide", {
			button: hamburgerButton,
			menu: hamburgerNav,
		});

		if (this.getOption("features.resetOnClose") === true) {
			this[hiddenElementsSymbol]
				.querySelectorAll(".is-open")
				.forEach((submenu) => submenu.classList.remove("is-open"));
		}

		if (cleanup) {
			cleanup();
			cleanup = undefined;
		}
		document.removeEventListener("click", handleOutsideClick);
	};

	this[hideHamburgerMenuSymbol] = hideMenu;

	const showMenu = () => {
		this[activeSubmenuHiderSymbol]?.();
		hamburgerNav.style.display = "block";
		document.body.classList.add("monster-navigation-open");

		fireCustomEvent(this, "monster-hamburger-show", {
			button: hamburgerButton,
			menu: hamburgerNav,
		});

		hamburgerNav.scrollIntoView({ block: "start", behavior: "smooth" });

		cleanup = autoUpdate(
			hamburgerButton,
			hamburgerNav,
			() => {
				if (window.innerWidth > 768) {
					const strategy = getBestPositionStrategy(this);

					computePosition(hamburgerButton, hamburgerNav, {
						placement: "bottom-end",
						strategy: strategy,
						middleware: [
							offset(8),
							flip(),
							shift({ padding: 8 }),
							size({
								apply: ({ availableHeight, elements }) => {
									Object.assign(elements.floating.style, {
										maxHeight: `${availableHeight}px`,
										overflowY: "auto",
									});
								},
								padding: 8,
							}),
						],
					}).then(({ x, y, strategy }) => {
						Object.assign(hamburgerNav.style, {
							position: strategy,
							left: `${x}px`,
							top: `${y}px`,
						});
					});
				} else {
					// Mobile view (fullscreen overlay), position is handled by CSS
					Object.assign(hamburgerNav.style, {
						position: "",
						left: "",
						top: "",
					});
				}
			},
			getAutoUpdateOptions(),
		);
		setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
	};

	hamburgerButton.addEventListener("click", (event) => {
		event.stopPropagation();
		const isVisible = hamburgerNav.style.display === "block";
		if (isVisible) {
			hideMenu();
		} else {
			showMenu();
		}
	});

	hamburgerCloseButton.addEventListener("click", (event) => {
		event.stopPropagation();
		hideMenu();
	});
}

/**
 * Attaches a ResizeObserver to the main navigation element to recalculate
 * tab distribution on size changes. A DeadMansSwitch is used for debouncing.
 * @private
 * @this {SiteNavigation}
 */
function attachResizeObserver() {
	this[resizeObserverSymbol] = new ResizeObserver(() => {
		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
			try {
				this[timerCallbackSymbol].touch();
				return;
			} catch (e) {
				delete this[timerCallbackSymbol];
			}
		}
		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
			schedulePopulateTabs.call(this);
		});
	});
	this[resizeObserverSymbol].observe(this);
}

/**
 * @private
 * @this {SiteNavigation}
 * @return {void}
 */
function schedulePopulateTabs() {
	if (typeof this[layoutFrameSymbol] === "number") {
		return;
	}

	this[layoutFrameSymbol] = requestAnimationFrame(() => {
		this[layoutFrameSymbol] = null;
		populateTabs.call(this);
	});
}

/**
 * Disconnects and cleans up the ResizeObserver instance.
 * @private
 * @this {SiteNavigation}
 */
function detachResizeObserver() {
	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
		this[resizeObserverSymbol].disconnect();
		delete this[resizeObserverSymbol];
	}
}

/**
 * Sets up interaction logic (hover, click, or touch) for a submenu.
 * This function is called recursively for nested submenus.
 * @private
 * @this {SiteNavigation}
 * @param {HTMLLIElement} parentLi The list item containing the submenu.
 * @param {'visible'|'hidden'} context The context (main nav or hamburger).
 * @param {number} level The nesting level of the submenu (starts at 1).
 */
function setupSubmenu(parentLi, context = "visible", level = 1) {
	const submenu = parentLi.querySelector(
		":scope > ul, :scope > div[part='mega-menu']",
	);
	if (!submenu) return;

	if (submenu.tagName === "UL") {
		submenu.setAttribute("part", "submenu");
	}

	const interaction = this.getOption("interactionModel", "auto");
	const isTouchDevice =
		"ontouchstart" in window || navigator.maxTouchPoints > 0;

	const effectiveInteraction =
		interaction === "auto"
			? context === "visible"
				? "hover"
				: "click"
			: interaction;

	const component = this;
	let cleanup;

	const immediateHide = () => {
		submenu.style.display = "none";
		Object.assign(submenu.style, {
			maxHeight: "",
			overflowY: "",
		});
		submenu
			.querySelectorAll(
				"ul[style*='display: block'], div[part='mega-menu'][style*='display: block']",
			)
			.forEach((sub) => {
				sub.style.display = "none";
			});
		fireCustomEvent(this, "monster-submenu-hide", {
			context,
			trigger: parentLi,
			submenu,
			level,
		});
		if (cleanup) {
			cleanup();
			cleanup = null;
		}
		if (level === 1 && component[activeSubmenuHiderSymbol] === immediateHide) {
			component[activeSubmenuHiderSymbol] = null;
		}
	};

	const show = () => {
		component[hideHamburgerMenuSymbol]?.();
		if (level === 1) {
			if (
				component[activeSubmenuHiderSymbol] &&
				component[activeSubmenuHiderSymbol] !== immediateHide
			) {
				component[activeSubmenuHiderSymbol]();
			}
			component[activeSubmenuHiderSymbol] = immediateHide;
		} else {
			[...parentLi.parentElement.children]
				.filter((li) => li !== parentLi)
				.forEach((sibling) => {
					const siblingSubmenu = sibling.querySelector(
						":scope > ul, :scope > div[part='mega-menu']",
					);
					if (siblingSubmenu) {
						siblingSubmenu.style.display = "none";
					}
				});
		}
		submenu.style.display = "block";
		fireCustomEvent(this, "monster-submenu-show", {
			context,
			trigger: parentLi,
			submenu,
			level,
		});
		if (!cleanup) {
			cleanup = autoUpdate(
				parentLi,
				submenu,
				() => {
					const middleware = [offset(8), flip(), shift({ padding: 8 })];
					const containsSubmenus = submenu.querySelector(
						"ul, div[part='mega-menu']",
					);
					if (!containsSubmenus) {
						middleware.push(
							size({
								apply: ({ availableHeight, elements }) => {
									Object.assign(elements.floating.style, {
										maxHeight: `${availableHeight}px`,
										overflowY: "auto",
									});
								},
								padding: 8,
							}),
						);
					}
					computePosition(parentLi, submenu, {
						placement: level === 1 ? "bottom-start" : "right-start",
						middleware: middleware,
					}).then(({ x, y, strategy }) => {
						Object.assign(submenu.style, {
							position: strategy,
							left: `${x}px`,
							top: `${y}px`,
						});
					});
				},
				getAutoUpdateOptions(),
			);
		}
	};

	if (effectiveInteraction === "hover" && isTouchDevice) {
		let lastTap = 0;
		const DOUBLE_TAP_DELAY = 300;
		const anchor = parentLi.querySelector(":scope > a");

		if (!anchor) return;

		const handleOutsideClickForSubmenu = (event) => {
			if (
				submenu.style.display === "block" &&
				!parentLi.contains(event.target)
			) {
				immediateHide();
				document.removeEventListener(
					"click",
					handleOutsideClickForSubmenu,
					true,
				);
			}
		};

		anchor.addEventListener("click", (event) => {
			const now = Date.now();
			const timeSinceLastTap = now - lastTap;
			lastTap = now;

			if (timeSinceLastTap < DOUBLE_TAP_DELAY && timeSinceLastTap > 0) {
				lastTap = 0;
				document.removeEventListener(
					"click",
					handleOutsideClickForSubmenu,
					true,
				);
				immediateHide();
			} else {
				event.preventDefault();
				event.stopPropagation();

				const isMenuOpen = submenu.style.display === "block";

				if (isMenuOpen) {
					document.removeEventListener(
						"click",
						handleOutsideClickForSubmenu,
						true,
					);
					immediateHide();
				} else {
					show();
					setTimeout(() => {
						document.addEventListener(
							"click",
							handleOutsideClickForSubmenu,
							true,
						);
					}, 0);
				}
			}
		});
	} else if (effectiveInteraction === "hover" && !isTouchDevice) {
		let hideTimeout;
		let isHovering = false;

		const handleMouseEnter = () => {
			isHovering = true;
			clearTimeout(hideTimeout);
			if (submenu.style.display !== "block") {
				show();
			}
		};

		const handleMouseLeave = () => {
			isHovering = false;
			hideTimeout = setTimeout(() => {
				if (!isHovering) {
					immediateHide();
				}
			}, 250);
		};

		parentLi.addEventListener("mouseenter", handleMouseEnter);
		parentLi.addEventListener("mouseleave", handleMouseLeave);
		submenu.addEventListener("mouseenter", handleMouseEnter);
		submenu.addEventListener("mouseleave", handleMouseLeave);
	} else {
		const anchor = parentLi.querySelector(":scope > a");
		if (anchor) {
			anchor.addEventListener("click", (event) => {
				event.preventDefault();
				event.stopPropagation();
				if (!submenu.classList.contains("is-open")) {
					[...parentLi.parentElement.children]
						.filter((li) => li !== parentLi)
						.forEach((sibling) => {
							const siblingSubmenu = sibling.querySelector(
								":scope > ul, :scope > div[part='mega-menu']",
							);
							if (siblingSubmenu) {
								siblingSubmenu.classList.remove("is-open");
							}
						});
				}
				const isOpen = submenu.classList.toggle("is-open");
				const eventName = isOpen
					? "monster-submenu-show"
					: "monster-submenu-hide";
				fireCustomEvent(this, eventName, {
					context,
					trigger: parentLi,
					submenu,
					level,
				});
			});
		}
	}

	if (submenu.tagName === "UL") {
		submenu
			.querySelectorAll(":scope > li")
			.forEach((li) => setupSubmenu.call(this, li, context, level + 1));
	}
}

/**
 * Creates a clone of a navigation list item, setting appropriate part attributes
 * for styling and handling active states.
 * @private
 * @param {HTMLLIElement} item The original list item to clone.
 * @returns {HTMLLIElement} The cloned and configured list item.
 */
function cloneNavItem(item) {
	const liClone = item.cloneNode(true);
	const aClone = liClone.querySelector("a");
	let navItemPart = "nav-item";
	let navLinkPart = "nav-link";

	if (item.classList.contains("active")) {
		navItemPart += " nav-item-active";
		if (aClone) navLinkPart += " nav-link-active";
	}

	liClone.setAttribute("part", navItemPart);
	if (aClone) aClone.setAttribute("part", navLinkPart);

	return liClone;
}

/**
 * @private
 * @param {HTMLElement} hamburgerButton
 * @return {number}
 */
function measureHamburgerWidth(hamburgerButton) {
	const originalDisplay = hamburgerButton.style.display;
	const originalVisibility = hamburgerButton.style.visibility;
	setStyleValue(hamburgerButton, "visibility", "hidden");
	setStyleValue(hamburgerButton, "display", "flex");
	const width = Math.ceil(hamburgerButton.getBoundingClientRect().width) || 0;
	setStyleValue(hamburgerButton, "display", originalDisplay);
	setStyleValue(hamburgerButton, "visibility", originalVisibility);
	return width;
}

/**
 * @private
 * @param {HTMLElement[]} sourceItems
 * @param {string} sourceSignature
 * @param {HTMLElement} visibleList
 * @param {HTMLElement} navEl
 * @return {object}
 */
function getNavigationMeasurements(
	sourceItems,
	sourceSignature,
	visibleList,
	navEl,
) {
	const cache = this[measurementCacheSymbol];
	if (cache?.sourceSignature === sourceSignature && cache.items.length > 0) {
		return cache;
	}

	const originalOverflow = navEl.style.overflow;
	const originalFlexWrap = visibleList.style.flexWrap;
	const originalVisibility = visibleList.style.visibility;

	setStyleValue(navEl, "overflow", "hidden");
	setStyleValue(visibleList, "flexWrap", "nowrap");
	setStyleValue(visibleList, "visibility", "hidden");

	const clones = sourceItems.map(cloneNavItem);
	visibleList.replaceChildren(...clones);

	const items = clones.map((clone) => {
		const submenu = clone.querySelector("ul, div[part='mega-menu']");
		if (submenu instanceof HTMLElement) {
			setStyleValue(submenu, "display", "none");
		}
		return {
			requiredWidth: clone.offsetLeft + clone.offsetWidth,
			width: clone.getBoundingClientRect().width || clone.offsetWidth || 0,
		};
	});
	const gap = parseFloat(getComputedStyle(visibleList).gap || "0") || 0;

	visibleList.replaceChildren();
	setStyleValue(navEl, "overflow", originalOverflow);
	setStyleValue(visibleList, "flexWrap", originalFlexWrap);
	setStyleValue(visibleList, "visibility", originalVisibility);

	this[measurementCacheSymbol] = {
		gap,
		items,
		sourceSignature,
	};
	return this[measurementCacheSymbol];
}

/**
 * @private
 * @param {HTMLElement[]} sourceItems
 * @return {string}
 */
function getSourceItemsSignature(sourceItems) {
	return sourceItems
		.map((item) => {
			return [
				getNavigationItemId(item),
				item.className || "",
				item.textContent || "",
				item.querySelectorAll("li").length,
			].join(":");
		})
		.join("|");
}

/**
 * @private
 * @param {HTMLElement} item
 * @return {number}
 */
function getNavigationItemId(item) {
	let id = navigationItemIds.get(item);
	if (id === undefined) {
		id = ++navigationItemId;
		navigationItemIds.set(item, id);
	}
	return id;
}

/**
 * @private
 * @param {object} params
 * @return {string}
 */
function getLayoutSignature({ fit, navWidth, rest }) {
	return [
		navWidth,
		fit.map(getNavigationItemId).join(","),
		rest.map(getNavigationItemId).join(","),
	].join("::");
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {string} property
 * @param {string} value
 * @return {void}
 */
function setStyleValue(element, property, value) {
	if (!(element instanceof HTMLElement)) {
		return;
	}
	if (element.style[property] !== value) {
		element.style[property] = value;
	}
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {HTMLElement[]} children
 * @return {void}
 */
function replaceChildrenIfChanged(element, children) {
	const currentSignature = Array.from(element.children)
		.map((child) => [child.className || "", child.textContent || ""].join(":"))
		.join("|");
	const nextSignature = children
		.map((child) => [child.className || "", child.textContent || ""].join(":"))
		.join("|");
	if (currentSignature !== nextSignature) {
		element.replaceChildren(...children);
	}
}

/**
 * Measures available space and distributes slotted navigation items between
 * the visible list and the hidden hamburger menu list.
 * @private
 * @this {SiteNavigation}
 */
/**
 * Misst den verfügbaren Platz und verteilt die Navigationselemente auf die sichtbare Liste
 * und das Hamburger-Menü. Die Methode fügt Elemente nacheinander hinzu und prüft auf
 * Überlauf (Overflow), um zu bestimmen, welche Elemente verschoben werden müssen.
 * @private
 * @this {SiteNavigation}
 */
function populateTabs() {
	const visibleList = this[visibleElementsSymbol];
	const hiddenList = this[hiddenElementsSymbol];
	const hamburgerButton = this[hamburgerButtonSymbol];
	const navEl = this[navElementSymbol];

	const topLevelUl = [...getSlottedElements.call(this, "ul")].find(
		(ul) => ul.parentElement === this,
	);

	this.style.visibility = "hidden";

	if (!topLevelUl) {
		replaceChildrenIfChanged.call(this, visibleList, []);
		replaceChildrenIfChanged.call(this, hiddenList, []);
		setStyleValue(hamburgerButton, "display", "none");
		this.style.visibility = "visible";
		return; // Nichts zu tun
	}
	const sourceItems = Array.from(topLevelUl.children).filter(
		(n) => n.tagName === "LI",
	);
	if (sourceItems.length === 0) {
		replaceChildrenIfChanged.call(this, visibleList, []);
		replaceChildrenIfChanged.call(this, hiddenList, []);
		setStyleValue(hamburgerButton, "display", "none");
		this.style.visibility = "visible";
		return;
	}

	const navWidth = navEl.clientWidth;
	const sourceSignature = getSourceItemsSignature(sourceItems);
	const hamburgerWidth = measureHamburgerWidth(hamburgerButton);
	const measurements = getNavigationMeasurements.call(
		this,
		sourceItems,
		sourceSignature,
		visibleList,
		navEl,
	);

	const fit = [];
	const rest = [];
	let hasOverflow = false;
	const availableWidth = navWidth - hamburgerWidth;
	const SAFETY_MARGIN = 1;

	for (let i = 0; i < sourceItems.length; i++) {
		const item = sourceItems[i];

		if (hasOverflow) {
			rest.push(item);
			continue;
		}

		const requiredWidth = measurements.items[i]?.requiredWidth || 0;

		if (requiredWidth > availableWidth + SAFETY_MARGIN) {
			hasOverflow = true;
			rest.push(item);
		} else {
			fit.push(item);
		}
	}

	if (fit.length > 0 && rest.length > 0) {
		const visibleItemsWidth =
			measurements.items[fit.length - 1]?.requiredWidth || 0;
		const firstHiddenIndex = sourceItems.indexOf(rest[0]);
		const firstHiddenItemWidth =
			measurements.items[firstHiddenIndex]?.width || 0;
		if (
			visibleItemsWidth + measurements.gap + firstHiddenItemWidth <=
			navWidth
		) {
			fit.push(rest.shift());
		}
	}

	const layoutSignature = getLayoutSignature({ fit, navWidth, rest });
	if (this[layoutSignatureSymbol] === layoutSignature) {
		setStyleValue(visibleList, "visibility", "visible");
		this.style.visibility = "visible";
		return;
	}
	this[layoutSignatureSymbol] = layoutSignature;

	const clonedVisible = fit.map(cloneNavItem);
	const clonedHidden = rest.map(cloneNavItem);
	replaceChildrenIfChanged.call(this, visibleList, clonedVisible);
	replaceChildrenIfChanged.call(this, hiddenList, clonedHidden);

	visibleList
		.querySelectorAll(":scope > li")
		.forEach((li) => setupSubmenu.call(this, li, "visible", 1));

	setStyleValue(hamburgerButton, "display", rest.length ? "flex" : "none");
	hiddenList
		.querySelectorAll(":scope > li")
		.forEach((li) => setupSubmenu.call(this, li, "hidden", 1));

	setStyleValue(visibleList, "visibility", "visible");
	this.style.visibility = "visible";
	fireCustomEvent(this, "monster-layout-change", {
		visibleItems: fit,
		hiddenItems: rest,
	});
}

/**
 * A simple template literal tag function for clarity.
 * @private
 * @param {TemplateStringsArray} strings
 * @returns {string} The combined string.
 */
function html(strings) {
	return strings.join("");
}

/**
 * Returns the HTML template for the component's shadow DOM.
 * @private
 * @returns {string} The HTML template string.
 */
function getTemplate() {
	return html`<div data-monster-role="control" part="control">
    <nav data-monster-role="navigation" role="navigation" part="nav">
      <ul id="visible-elements" part="visible-list"></ul>
    </nav>
    <div data-monster-role="hamburger-container" part="hamburger-container">
      <button
        id="hamburger-button"
        part="hamburger-button"
        aria-label="More navigation items"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          fill="currentColor"
          viewBox="0 0 16 16"
        >
          <path
            fill-rule="evenodd"
            d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"
          />
        </svg>
      </button>
      <nav
        data-monster-role="hamburger-nav"
        role="navigation"
        part="hamburger-nav"
        style="display: none;"
      >
        <div part="hamburger-header">
          <button part="hamburger-close-button" aria-label="Close navigation">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="24"
              height="24"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
        </div>
        <ul id="hidden-elements" part="hidden-list"></ul>
      </nav>
    </div>
    <slot class="hidden-slot" style="display: none;"></slot>
  </div>`;
}

registerCustomElement(SiteNavigation);
