/**
 * 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 {
	arrow,
	autoPlacement,
	autoUpdate,
	computePosition,
	offset,
	flip,
	hide,
	size,
	shift,
} from "@floating-ui/dom";
import { isArray, isFunction, isObject, isString } from "../../../types/is.mjs";
import { Processing } from "../../../util/processing.mjs";
import {
	FLOATING_LAYOUT_REASON,
	cancelFloatingLayout,
	enqueueFloatingLayout,
} from "./floating-layout-queue.mjs";

export {
	applyAdaptiveFloatingElementSize,
	closePositionedPopper,
	resolveClippingBoundaryElement,
	resolveParentPopperContentBoundary,
	isPositionedPopperOpen,
	openPositionedPopper,
	positionPopper,
};

const autoUpdateCleanupMap = new WeakMap();
const settlingFrameMap = new WeakMap();
const floatingResizeObserverMap = new WeakMap();
const floatingSyncCycleMap = new WeakMap();
const floatingAppearanceFrameMap = new WeakMap();
const floatingAppearanceTimeoutMap = new WeakMap();
const adaptiveScrollHeightDatasetKey = "monsterAdaptiveScrollHeight";

/**
 * @private
 * @param controlElement
 * @param popperElement
 * @param options
 * @return {Promise|*}
 */
function positionPopper(controlElement, popperElement, options) {
	const config = normalizePopperConfig(options, controlElement, popperElement);

	return new Processing(() => {
		return enqueueFloatingLayout({
			popperElement,
			reason: FLOATING_LAYOUT_REASON.POSITION,
			isActive: () => isPositionedPopperOpen(popperElement),
			position: () => {
				enableFloatingPositioning(controlElement, popperElement, config);
			},
		});
	}).run();
}

function openPositionedPopper(controlElement, popperElement, options) {
	const config = normalizePopperConfig(options, controlElement, popperElement);

	stopAutoUpdate(popperElement);
	cancelFloatingAppearanceFrame(popperElement);
	popperElement.dataset.monsterAppearance = "opening";
	popperElement.style.display = "block";
	popperElement.style.position = config.strategy;
	popperElement.style.removeProperty("transform");

	// Keep the call signature stable even though only Floating UI is used now.
	void controlElement;
}

function enableFloatingPositioning(controlElement, popperElement, config) {
	if (!isPositionedPopperOpen(popperElement)) {
		return;
	}

	popperElement.style.removeProperty("visibility");
	scheduleFloatingAppearanceOpen(popperElement);

	startFloatingResizeObserver(controlElement, popperElement, config);
	startAutoUpdate(controlElement, popperElement, () => {
		if (!isPositionedPopperOpen(popperElement)) {
			return;
		}

		enqueueFloatingLayout({
			popperElement,
			reason:
				FLOATING_LAYOUT_REASON.POSITION | FLOATING_LAYOUT_REASON.RESIZE,
			isActive: () => isPositionedPopperOpen(popperElement),
			position: () => {
				runFloatingUpdateHook(popperElement);
				syncFloatingPopover(controlElement, popperElement, config);
			},
		});
	});
	runFloatingUpdateHook(popperElement);
	syncFloatingPopover(controlElement, popperElement, config);
}

function syncFloatingPopover(
	controlElement,
	popperElement,
	config,
	allowSettlingPass = true,
) {
	if (!isPositionedPopperOpen(popperElement)) {
		return;
	}

	const arrowElement = popperElement.querySelector("[data-monster-role=arrow]");
	const syncCycleId = claimFloatingSyncCycle(popperElement);
	const floatingMiddleware = buildFloatingMiddleware(
		config.middleware,
		config.placement,
		config.detectOverflowOptions,
		popperElement,
		syncCycleId,
		config.adaptiveSize,
	);

	if (config.adaptiveSize !== false) {
		resetAdaptiveFloatingElementSize(popperElement);
	}

	if (
		arrowElement instanceof HTMLElement &&
		config.middlewareTokens.includes("arrow")
	) {
		floatingMiddleware.push(arrow({ element: arrowElement }));
	}

	if (
		config.adaptiveSize !== false &&
		!config.middlewareTokens.includes("size")
	) {
		floatingMiddleware.push(
			createAdaptiveSizeMiddleware(
				config.detectOverflowOptions,
				popperElement,
				syncCycleId,
			),
		);
	}

	computePosition(
		controlElement,
		popperElement,
		Object.assign({}, config, {
			middleware: floatingMiddleware,
		}),
	).then(({ x, y, placement, middlewareData }) => {
		if (!isActiveFloatingSyncCycle(popperElement, syncCycleId)) {
			return;
		}
		if (!isPositionedPopperOpen(popperElement)) {
			return;
		}

		Object.assign(popperElement.style, {
			top: "0",
			left: "0",
			position: config.strategy,
			transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`,
		});

		if (middlewareData.arrow) {
			applyFloatingArrowStyles(arrowElement, placement, middlewareData.arrow);
		}

		if (allowSettlingPass) {
			scheduleSettlingPass(controlElement, popperElement, config);
		}
	});
}

function closePositionedPopper(popperElement) {
	cancelFloatingLayout(popperElement);
	stopAutoUpdate(popperElement);
	cancelFloatingAppearanceFrame(popperElement);
	delete popperElement.dataset.monsterAppearance;
	popperElement.style.display = "none";
	popperElement.style.removeProperty("visibility");
	popperElement.style.removeProperty("position");
	popperElement.style.removeProperty("transform");
}

function isPositionedPopperOpen(popperElement) {
	return popperElement.style.display === "block";
}

function normalizePopperConfig(options, controlElement, popperElement) {
	const config = Object.assign(
		{},
		{
			placement: "top",
			engine: "floating",
			strategy: "absolute",
			adaptiveSize: true,
		},
		options,
	);

	if (
		config.boundaryElement !== null &&
		!(config.boundaryElement instanceof HTMLElement)
	) {
		config.boundaryElement = resolveClippingBoundaryElement(
			controlElement,
			popperElement,
		);
	}
	config.detectOverflowOptions = buildDetectOverflowOptions(
		config.boundaryElement,
	);
	config.middleware = normalizeMiddleware(config);
	config.middlewareTokens = config.middleware.filter((line) => isString(line));

	return config;
}

function normalizeMiddleware(config) {
	const middleware = config?.middleware;
	if (isArray(middleware)) {
		return [...middleware];
	}

	if (isString(middleware)) {
		return middleware.split(",").filter((line) => line.trim().length > 0);
	}

	return [];
}

function buildFloatingMiddleware(
	middleware,
	placement,
	detectOverflowOptions,
	popperElement,
	syncCycleId = null,
	adaptiveSize = true,
) {
	const result = [...middleware];

	for (const key in result) {
		const line = result[key];
		if (isFunction(line) || isObject(line)) {
			continue;
		}

		if (!isString(line)) {
			throw new Error(
				`Middleware must be a string, a function or an object. Got ${typeof line}`,
			);
		}

		const kv = line.split(":");
		const fn = kv.shift();

		switch (fn) {
			case "flip":
				result[key] = flip(detectOverflowOptions);
				break;
			case "shift":
				result[key] = shift(normalizeShiftOptions(kv, detectOverflowOptions));
				break;
			case "autoPlacement": {
				let defaultAllowedPlacements = ["top", "bottom", "left", "right"];

				const defPlacement = kv?.shift();
				if (isString(defPlacement) && defPlacement.trim().length > 0) {
					defaultAllowedPlacements = defPlacement
						.split(",")
						.filter((entry) => entry.trim().length > 0);
				}

				if (defaultAllowedPlacements.includes(placement)) {
					defaultAllowedPlacements.splice(
						defaultAllowedPlacements.indexOf(placement),
						1,
					);
				}
				defaultAllowedPlacements.unshift(placement);

				result[key] = autoPlacement(
					Object.assign({}, detectOverflowOptions, {
						crossAxis: true,
						autoAlignment: true,
						allowedPlacements: defaultAllowedPlacements,
					}),
				);
				break;
			}
			case "arrow":
				result[key] = null;
				break;
			case "size":
				result[key] =
					adaptiveSize === false
						? null
						: createAdaptiveSizeMiddleware(
								detectOverflowOptions,
								popperElement,
								syncCycleId,
							);
				break;
			case "offset":
				result[key] = offset(parseInt(kv?.shift(), 10) || 10);
				break;
			case "hide":
				result[key] = hide(detectOverflowOptions);
				break;
			default:
				throw new Error(`Unknown function: ${fn}`);
		}
	}

	return result.filter(Boolean);
}

function normalizeShiftOptions(tokens, detectOverflowOptions) {
	const options = Object.assign({}, detectOverflowOptions);
	const normalizedTokens = tokens
		.join(":")
		.split(",")
		.map((token) => token.trim())
		.filter((token) => token.length > 0);

	for (const token of normalizedTokens) {
		switch (token) {
			case "crossAxis":
				options.crossAxis = true;
				break;
			case "mainAxis=false":
				options.mainAxis = false;
				break;
			default:
				throw new Error(`Unknown shift option: ${token}`);
		}
	}

	return options;
}

function createAdaptiveSizeMiddleware(
	detectOverflowOptions,
	popperElement,
	syncCycleId = null,
) {
	return size(
		Object.assign({}, detectOverflowOptions, {
			apply({ availableWidth, availableHeight, elements }) {
				const floatingElement = elements?.floating || popperElement;
				if (!(floatingElement instanceof HTMLElement)) {
					return;
				}
				if (
					syncCycleId !== null &&
					!isActiveFloatingSyncCycle(floatingElement, syncCycleId)
				) {
					return;
				}

				applyAdaptiveFloatingElementSize(floatingElement, {
					availableWidth,
					availableHeight,
				});
			},
		}),
	);
}

function applyAdaptiveFloatingElementSize(
	floatingElement,
	{ availableWidth, availableHeight },
) {
	const contentElement = getFloatingContentElement(floatingElement);
	const usesVisibleOverflow =
		contentElement instanceof HTMLElement &&
		contentElement.dataset.monsterOverflowMode === "visible";
	const maxWidth = clampAvailableDimension(
		availableWidth,
		readMaxDimension(floatingElement, "maxWidth"),
	);
	const maxHeight = clampAvailableDimension(
		availableHeight,
		readMaxDimension(floatingElement, "maxHeight"),
	);
	const nextStyle = {
		boxSizing: "border-box",
	};

	if (Number.isFinite(maxWidth) && maxWidth > 0) {
		nextStyle.maxWidth = `${maxWidth}px`;
	} else {
		nextStyle.maxWidth = "";
	}

	if (!usesVisibleOverflow && Number.isFinite(maxHeight) && maxHeight > 0) {
		nextStyle.maxHeight = `${maxHeight}px`;
	} else {
		nextStyle.maxHeight = "";
	}

	Object.assign(floatingElement.style, nextStyle);

	syncPreferredFloatingWidth(floatingElement, maxWidth);
	syncPreferredFloatingMaxWidth(floatingElement, maxWidth);
	applyAdaptiveFloatingContentSize(floatingElement, maxHeight);
}

function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) {
	const contentElement = getFloatingContentElement(floatingElement);
	if (!(contentElement instanceof HTMLElement)) {
		return;
	}

	const overflowMode = contentElement.dataset.monsterOverflowMode;
	if (overflowMode === "visible") {
		return;
	}

	const reservedHeight = getReservedFloatingHeight(
		floatingElement,
		contentElement,
	);
	const contentMaxHeight = clampAvailableDimension(
		Number.isFinite(maxHeight) ? maxHeight - reservedHeight : null,
		readMaxDimension(contentElement, "maxHeight"),
	);
	const minimumReadableHeight = getMinimumReadableContentHeight(contentElement);
	const nextContentMaxHeight =
		Number.isFinite(contentMaxHeight) && contentMaxHeight > 0
			? Math.max(contentMaxHeight, minimumReadableHeight)
			: contentMaxHeight;
	const preferredContentHeight = getRequiredContentHeight(contentElement);

	if (overflowMode === "horizontal" || overflowMode === "both") {
		// Overlay-aware modes keep the parent wrapper sized to the real inline content.
		// Nested poppers may still escape, but they must not inflate the outer popper height.
		const contentHeightConstrained = applyOverlayAwareContentHeight(
			contentElement,
			preferredContentHeight,
			nextContentMaxHeight,
			minimumReadableHeight,
		);
		syncNestedScrollContainerHeight(
			contentElement,
			contentHeightConstrained ? nextContentMaxHeight : null,
		);
		return;
	}

	if (Number.isFinite(nextContentMaxHeight) && nextContentMaxHeight > 0) {
		contentElement.style.maxHeight = `${nextContentMaxHeight}px`;
	} else {
		contentElement.style.maxHeight = "";
	}

	syncNestedScrollContainerHeight(contentElement, nextContentMaxHeight);
}

function applyOverlayAwareContentHeight(
	contentElement,
	preferredContentHeight,
	contentMaxHeight,
	minimumReadableHeight,
) {
	const preferredHeight =
		Number.isFinite(preferredContentHeight) && preferredContentHeight > 0
			? Math.max(preferredContentHeight, minimumReadableHeight)
			: minimumReadableHeight;
	const constrainedHeight =
		Number.isFinite(contentMaxHeight) && contentMaxHeight > 0
			? Math.min(preferredHeight, contentMaxHeight)
			: preferredHeight;
	const isConstrained =
		Number.isFinite(contentMaxHeight) &&
		contentMaxHeight > 0 &&
		preferredHeight > contentMaxHeight;

	contentElement.style.removeProperty("overflow");
	contentElement.style.overflowX =
		contentElement.dataset.monsterOverflowMode === "horizontal"
			? "auto"
			: "visible";
	contentElement.style.overflowY = isConstrained ? "auto" : "visible";

	if (
		isConstrained &&
		Number.isFinite(constrainedHeight) &&
		constrainedHeight > 0
	) {
		contentElement.style.height = `${constrainedHeight}px`;
		contentElement.style.maxHeight = `${constrainedHeight}px`;
		return true;
	}

	// overflow-x:auto computes overflow-y to auto in browsers, so an unconstrained
	// horizontal wrapper must keep its natural height to avoid a spurious scrollbar.
	contentElement.style.removeProperty("height");
	if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) {
		contentElement.style.maxHeight = `${contentMaxHeight}px`;
	} else {
		contentElement.style.removeProperty("maxHeight");
	}
	return false;
}

function getFloatingContentElement(floatingElement) {
	for (const child of floatingElement.children) {
		if (!(child instanceof HTMLElement)) {
			continue;
		}

		const part = child.getAttribute("part");
		if (
			isString(part) &&
			part
				.split(/\s+/)
				.filter((token) => token.length > 0)
				.includes("content")
		) {
			return child;
		}
	}

	return null;
}

function getReservedFloatingHeight(floatingElement, contentElement) {
	const floatingStyle = getComputedStyle(floatingElement);
	let reservedHeight = 0;

	reservedHeight += readBoxDimension(floatingStyle.paddingTop);
	reservedHeight += readBoxDimension(floatingStyle.paddingBottom);
	reservedHeight += readBoxDimension(floatingStyle.borderTopWidth);
	reservedHeight += readBoxDimension(floatingStyle.borderBottomWidth);

	for (const child of floatingElement.children) {
		if (!(child instanceof HTMLElement) || child === contentElement) {
			continue;
		}

		const childStyle = getComputedStyle(child);
		reservedHeight += child.getBoundingClientRect().height;
		reservedHeight += readBoxDimension(childStyle.marginTop);
		reservedHeight += readBoxDimension(childStyle.marginBottom);
	}

	return Math.max(0, reservedHeight);
}

function readBoxDimension(rawValue) {
	const value = Number.parseFloat(rawValue);
	return Number.isFinite(value) ? value : 0;
}

function getMinimumReadableContentHeight(contentElement) {
	if (!(contentElement instanceof HTMLElement)) {
		return 0;
	}

	const measurementTarget =
		getPrimaryReadableContentElement(contentElement) || contentElement;
	const style = getComputedStyle(measurementTarget);
	const lineHeight = readLineHeight(style);
	const paddingHeight =
		readBoxDimension(getComputedStyle(contentElement).paddingTop) +
		readBoxDimension(getComputedStyle(contentElement).paddingBottom);

	return Math.max(0, lineHeight + paddingHeight);
}

function readLineHeight(style) {
	if (!style) {
		return 0;
	}

	const lineHeight = Number.parseFloat(style.lineHeight);
	if (Number.isFinite(lineHeight) && lineHeight > 0) {
		return lineHeight;
	}

	const fontSize = Number.parseFloat(style.fontSize);
	if (Number.isFinite(fontSize) && fontSize > 0) {
		return fontSize * 1.4;
	}

	return 0;
}

function getPrimaryReadableContentElement(contentElement) {
	if (!(contentElement instanceof HTMLElement)) {
		return null;
	}

	const slotElement = contentElement.querySelector("slot");
	if (slotElement?.assignedElements instanceof Function) {
		for (const element of slotElement.assignedElements({ flatten: true })) {
			if (element instanceof HTMLElement) {
				return element;
			}
		}
	}

	for (const child of contentElement.children) {
		if (child instanceof HTMLElement) {
			return child;
		}
	}

	return null;
}

function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
	const nestedScrollableElement = getNestedScrollableElement(contentElement);
	if (!(nestedScrollableElement instanceof HTMLElement)) {
		return;
	}

	if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) {
		const nextNestedHeight = resolveNestedScrollContainerHeight(
			nestedScrollableElement,
			contentMaxHeight,
		);
		nestedScrollableElement.style.height = `${nextNestedHeight}px`;
		nestedScrollableElement.style.maxHeight = `${nextNestedHeight}px`;
		nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey] = "true";
		return;
	}

	if (
		nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey] !== "true"
	) {
		return;
	}

	nestedScrollableElement.style.height = "";
	nestedScrollableElement.style.maxHeight = "";
	delete nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey];
}

function resolveNestedScrollContainerHeight(
	nestedScrollableElement,
	contentMaxHeight,
) {
	const declaredHeight = readDeclaredDimension(
		nestedScrollableElement,
		"height",
	);
	const declaredMaxHeight = readDeclaredDimension(
		nestedScrollableElement,
		"maxHeight",
	);
	const scrollHeight = nestedScrollableElement.scrollHeight;
	const preferredHeightCandidates = [
		declaredHeight,
		declaredMaxHeight,
		scrollHeight,
	];
	const preferredHeight = preferredHeightCandidates.find((value) => {
		return Number.isFinite(value) && value > 0;
	});

	if (Number.isFinite(preferredHeight) && preferredHeight > 0) {
		return Math.min(contentMaxHeight, preferredHeight);
	}

	return contentMaxHeight;
}

function readDeclaredDimension(element, property) {
	if (!(element instanceof HTMLElement)) {
		return NaN;
	}

	const value = Number.parseFloat(element.style?.[property] || "");
	return Number.isFinite(value) ? value : NaN;
}

function syncPreferredFloatingWidth(floatingElement, maxWidth) {
	const preferredWidth = Number.parseFloat(
		floatingElement.dataset.monsterPreferredWidth || "",
	);
	if (!Number.isFinite(preferredWidth) || preferredWidth <= 0) {
		return;
	}
	const widthBehavior = floatingElement.dataset.monsterWidthBehavior;
	if (widthBehavior === "preferred") {
		const nextFloatingWidth = clampAvailableDimension(
			preferredWidth,
			Number.isFinite(maxWidth) ? maxWidth : Infinity,
		);
		if (Number.isFinite(nextFloatingWidth) && nextFloatingWidth > 0) {
			floatingElement.style.width = `${nextFloatingWidth}px`;
			floatingElement.style.minWidth = `${nextFloatingWidth}px`;
		}
		return;
	}

	const contentElement = getFloatingContentElement(floatingElement);
	if (!(contentElement instanceof HTMLElement)) {
		floatingElement.style.width = `${preferredWidth}px`;
		return;
	}

	const floatingRect = floatingElement.getBoundingClientRect();
	const contentRect = contentElement.getBoundingClientRect();
	const reservedWidth = Math.max(0, floatingRect.width - contentRect.width);
	const contentRequiredWidth = getRequiredContentWidth(contentElement);
	const preferredFloatingWidth = preferredWidth;
	const requiredFloatingWidth = Math.max(
		preferredFloatingWidth,
		contentRequiredWidth + reservedWidth,
	);
	const nextFloatingWidth = clampAvailableDimension(
		requiredFloatingWidth,
		Number.isFinite(maxWidth) ? maxWidth : Infinity,
	);

	if (Number.isFinite(nextFloatingWidth) && nextFloatingWidth > 0) {
		floatingElement.style.width = `${nextFloatingWidth}px`;
	}
}

function syncPreferredFloatingMaxWidth(floatingElement, maxWidth) {
	const preferredMaxWidth = Number.parseFloat(
		floatingElement.dataset.monsterPreferredMaxWidth || "",
	);
	if (!Number.isFinite(preferredMaxWidth) || preferredMaxWidth <= 0) {
		return;
	}

	const nextFloatingMaxWidth = clampAvailableDimension(
		preferredMaxWidth,
		Number.isFinite(maxWidth) ? maxWidth : Infinity,
	);
	if (Number.isFinite(nextFloatingMaxWidth) && nextFloatingMaxWidth > 0) {
		floatingElement.style.maxWidth = `${nextFloatingMaxWidth}px`;
	}
}

function resetAdaptiveFloatingElementSize(floatingElement) {
	if (!(floatingElement instanceof HTMLElement)) {
		return;
	}

	floatingElement.style.removeProperty("maxHeight");

	const contentElement = getFloatingContentElement(floatingElement);
	if (!(contentElement instanceof HTMLElement)) {
		return;
	}

	contentElement.style.removeProperty("height");
	contentElement.style.removeProperty("maxHeight");
	contentElement.style.removeProperty("overflow");
	contentElement.style.removeProperty("overflowX");
	contentElement.style.removeProperty("overflowY");

	const nestedScrollableElement = getNestedScrollableElement(contentElement);
	if (!(nestedScrollableElement instanceof HTMLElement)) {
		return;
	}

	nestedScrollableElement.style.removeProperty("height");
	nestedScrollableElement.style.removeProperty("maxHeight");
	delete nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey];
}

function runFloatingUpdateHook(popperElement) {
	const hook = popperElement?.monsterBeforeFloatingUpdate;
	if (typeof hook === "function") {
		hook();
	}
}

function getRequiredContentWidth(contentElement) {
	const nestedScrollableElement = getNestedScrollableElement(contentElement);
	if (nestedScrollableElement instanceof HTMLElement) {
		return Math.max(
			contentElement.scrollWidth,
			nestedScrollableElement.scrollWidth,
			contentElement.clientWidth,
		);
	}

	return Math.max(contentElement.scrollWidth, contentElement.clientWidth);
}

function getRequiredContentHeight(contentElement) {
	const nestedScrollableElement = getNestedScrollableElement(contentElement);
	if (nestedScrollableElement instanceof HTMLElement) {
		const contentRect = contentElement.getBoundingClientRect();
		const nestedRect = nestedScrollableElement.getBoundingClientRect();
		const reservedHeight = Math.max(0, contentRect.height - nestedRect.height);
		return Math.max(
			contentElement.scrollHeight,
			nestedScrollableElement.scrollHeight + reservedHeight,
			contentElement.clientHeight,
		);
	}

	return Math.max(contentElement.scrollHeight, contentElement.clientHeight);
}

function getNestedScrollableElement(contentElement) {
	for (const child of contentElement.children) {
		if (!(child instanceof HTMLElement)) {
			continue;
		}

		const style = getComputedStyle(child);
		if (["auto", "scroll"].includes(style.overflowY)) {
			return child;
		}
	}

	return null;
}

function buildDetectOverflowOptions(boundaryElement) {
	const result = {
		rootBoundary: "viewport",
		padding: 0,
	};

	if (boundaryElement instanceof HTMLElement) {
		result.boundary = boundaryElement;
	}

	return result;
}

function resolveClippingBoundaryElement(...elements) {
	for (const element of elements) {
		const clippingBoundary = findNearestClippingContainer(element);
		if (clippingBoundary instanceof HTMLElement) {
			return clippingBoundary;
		}
	}

	return null;
}

function resolveParentPopperContentBoundary(...elements) {
	for (const element of elements) {
		const clippingBoundary = findNearestIgnoredClippingContainer(element);
		if (clippingBoundary instanceof HTMLElement) {
			return clippingBoundary;
		}
	}

	return null;
}

function findNearestClippingContainer(element) {
	let current = getComposedParent(element);

	while (current) {
		if (
			current instanceof HTMLElement &&
			isClippingContainer(getComputedStyle(current)) &&
			!shouldIgnoreClippingContainer(current)
		) {
			return current;
		}

		current = getComposedParent(current);
	}

	return null;
}

function findNearestIgnoredClippingContainer(element) {
	let current = getComposedParent(element);

	while (current) {
		if (
			current instanceof HTMLElement &&
			shouldIgnoreClippingContainer(current) &&
			shouldEscapeParentPopperContentWrapper(current)
		) {
			return current;
		}

		current = getComposedParent(current);
	}

	return null;
}

function shouldEscapeParentPopperContentWrapper(element) {
	if (!(element instanceof HTMLElement) || !isPopperContentWrapper(element)) {
		return false;
	}

	const overflowMode = element.getAttribute("data-monster-overflow-mode");
	if (overflowMode === "both") {
		return true;
	}

	return isClippingContainer(getComputedStyle(element));
}

function getComposedParent(node) {
	if (!node) {
		return null;
	}

	if (node instanceof Element && node.assignedSlot) {
		return node.assignedSlot;
	}

	if (node instanceof ShadowRoot) {
		return node.host || null;
	}

	if (node.parentElement instanceof HTMLElement) {
		return node.parentElement;
	}

	const rootNode = node.getRootNode?.();
	if (rootNode instanceof ShadowRoot) {
		return rootNode.host || null;
	}

	return node.parentNode || null;
}

function shouldIgnoreClippingContainer(element) {
	if (!(element instanceof HTMLElement)) {
		return false;
	}

	if (!isPopperContentWrapper(element)) {
		return false;
	}

	const parent = getComposedParent(element);
	return (
		parent instanceof HTMLElement &&
		parent.getAttribute("data-monster-role") === "popper"
	);
}

function isPopperContentWrapper(element) {
	if (!(element instanceof HTMLElement)) {
		return false;
	}

	if (!element.hasAttribute("data-monster-overflow-mode")) {
		return false;
	}

	const part = element.getAttribute("part");
	return (
		isString(part) &&
		part
			.split(/\s+/)
			.filter((token) => token.length > 0)
			.includes("content")
	);
}

function isClippingContainer(style) {
	if (!style) {
		return false;
	}

	return [style.overflow, style.overflowX, style.overflowY].some((value) => {
		return ["hidden", "auto", "scroll", "clip"].includes(value);
	});
}

function readMaxDimension(element, property) {
	const rawValue = getComputedStyle(element)?.[property];
	if (!rawValue || rawValue === "none") {
		return Infinity;
	}

	const value = Number.parseFloat(rawValue);
	return Number.isFinite(value) ? value : Infinity;
}

function clampAvailableDimension(available, configuredMax) {
	if (!Number.isFinite(available) || available <= 0) {
		return null;
	}

	if (!Number.isFinite(configuredMax)) {
		return available;
	}

	return Math.min(available, configuredMax);
}

function startAutoUpdate(controlElement, popperElement, callback) {
	stopAutoUpdate(popperElement);
	autoUpdateCleanupMap.set(
		popperElement,
		autoUpdate(controlElement, popperElement, callback, {
			elementResize: false,
			layoutShift: false,
		}),
	);
}

function stopAutoUpdate(popperElement) {
	cancelSettlingPass(popperElement);
	stopFloatingResizeObserver(popperElement);
	const cleanup = autoUpdateCleanupMap.get(popperElement);
	if (typeof cleanup === "function") {
		cleanup();
	}
	autoUpdateCleanupMap.delete(popperElement);
}

function startFloatingResizeObserver(controlElement, popperElement, config) {
	stopFloatingResizeObserver(popperElement);

	if (typeof ResizeObserver !== "function") {
		return;
	}

	const observer = new ResizeObserver(() => {
		if (!isPositionedPopperOpen(popperElement)) {
			return;
		}

		enqueueFloatingLayout({
			popperElement,
			reason: FLOATING_LAYOUT_REASON.SETTLE,
			isActive: () => isPositionedPopperOpen(popperElement),
			position: () => {
				runFloatingUpdateHook(popperElement);
				syncFloatingPopover(controlElement, popperElement, config, false);
			},
		});
	});

	observer.observe(popperElement);
	floatingResizeObserverMap.set(popperElement, observer);
}

function stopFloatingResizeObserver(popperElement) {
	const observer = floatingResizeObserverMap.get(popperElement);
	if (
		typeof ResizeObserver === "function" &&
		observer instanceof ResizeObserver
	) {
		observer.disconnect();
	}
	floatingResizeObserverMap.delete(popperElement);
}

function scheduleSettlingPass(controlElement, popperElement, config) {
	if (settlingFrameMap.has(popperElement)) {
		return;
	}

	const frameId = requestAnimationFrame(() => {
		settlingFrameMap.delete(popperElement);
		if (!isPositionedPopperOpen(popperElement)) {
			return;
		}

		enqueueFloatingLayout({
			popperElement,
			reason: FLOATING_LAYOUT_REASON.SETTLE,
			isActive: () => isPositionedPopperOpen(popperElement),
			position: () => {
				runFloatingUpdateHook(popperElement);
				syncFloatingPopover(controlElement, popperElement, config, false);
			},
		});
	});

	settlingFrameMap.set(popperElement, frameId);
}

function claimFloatingSyncCycle(popperElement) {
	const nextCycleId = (floatingSyncCycleMap.get(popperElement) || 0) + 1;
	floatingSyncCycleMap.set(popperElement, nextCycleId);
	return nextCycleId;
}

function isActiveFloatingSyncCycle(popperElement, syncCycleId) {
	return floatingSyncCycleMap.get(popperElement) === syncCycleId;
}

function cancelSettlingPass(popperElement) {
	const frameId = settlingFrameMap.get(popperElement);
	if (typeof frameId === "number") {
		cancelAnimationFrame(frameId);
	}
	settlingFrameMap.delete(popperElement);
}

function scheduleFloatingAppearanceOpen(popperElement) {
	if (!(popperElement instanceof HTMLElement)) {
		return;
	}

	if (popperElement.dataset.monsterAppearance !== "opening") {
		return;
	}

	if (floatingAppearanceFrameMap.has(popperElement)) {
		return;
	}

	const finishAppearanceOpen = () => {
		cancelFloatingAppearanceFrame(popperElement);

		if (!isPositionedPopperOpen(popperElement)) {
			return;
		}

		popperElement.dataset.monsterAppearance = "open";
	};
	const schedule =
		typeof requestAnimationFrame === "function"
			? requestAnimationFrame
			: (callback) => {
					return setTimeout(callback, 16);
				};
	const frameId = schedule(() => {
		finishAppearanceOpen();
	});
	const timeoutId = setTimeout(() => {
		finishAppearanceOpen();
	}, 24);

	floatingAppearanceFrameMap.set(popperElement, frameId);
	floatingAppearanceTimeoutMap.set(popperElement, timeoutId);
}

function cancelFloatingAppearanceFrame(popperElement) {
	const frameId = floatingAppearanceFrameMap.get(popperElement);
	if (
		frameId !== undefined &&
		frameId !== null &&
		Number.isNaN(frameId) === false
	) {
		if (typeof cancelAnimationFrame === "function") {
			cancelAnimationFrame(frameId);
		} else {
			clearTimeout(frameId);
		}
	}
	floatingAppearanceFrameMap.delete(popperElement);

	const timeoutId = floatingAppearanceTimeoutMap.get(popperElement);
	if (
		timeoutId !== undefined &&
		timeoutId !== null &&
		Number.isNaN(timeoutId) === false
	) {
		clearTimeout(timeoutId);
	}
	floatingAppearanceTimeoutMap.delete(popperElement);
}

function applyFloatingArrowStyles(arrowElement, placement, arrowData) {
	if (!(arrowElement instanceof HTMLElement)) {
		return;
	}

	const side = placement.split("-")[0];
	const staticSide = {
		top: "bottom",
		right: "left",
		bottom: "top",
		left: "right",
	}[side];
	const arrowLen = arrowElement.offsetWidth + 4;
	const borderStyle = {
		borderLeft: "transparent",
		borderRight: "transparent",
		borderBottom: "transparent",
		borderTop: "transparent",
	};
	const defaultBorder =
		"var(--monster-border-width) var(--monster-border-style) var(--monster-bg-color-primary-4)";

	switch (side) {
		case "top":
			borderStyle.borderRight = defaultBorder;
			borderStyle.borderBottom = defaultBorder;
			break;
		case "bottom":
			borderStyle.borderTop = defaultBorder;
			borderStyle.borderLeft = defaultBorder;
			break;
		case "left":
			borderStyle.borderRight = defaultBorder;
			borderStyle.borderTop = defaultBorder;
			break;
		case "right":
			borderStyle.borderBottom = defaultBorder;
			borderStyle.borderLeft = defaultBorder;
			break;
	}

	Object.assign(
		arrowElement.style,
		{
			left: arrowData.x != null ? `${arrowData.x}px` : "",
			top: arrowData.y != null ? `${arrowData.y}px` : "",
			right: "",
			bottom: "",
			[staticSide]: `${-arrowLen / 2}px`,
			transform: "rotate(45deg)",
		},
		borderStyle,
	);
}

function roundByDPR(value) {
	const dpr = window.devicePixelRatio || 1;
	return Math.round(value * dpr) / dpr;
}
