/**
 * 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 { ATTRIBUTE_DISABLED, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	attributeObserverSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { isArray, isString } from "../../types/is.mjs";
import { validateString } from "../../types/validate.mjs";
import { Popper } from "../layout/popper.mjs";
import { MessageStateButtonStyleSheet } from "./stylesheet/message-state-button.mjs";
import { StateButtonStyleSheet } from "./stylesheet/state-button.mjs";
import "./state-button.mjs";
import { isFunction } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";

export { MessageStateButton };

/**
 * @private
 * @type {symbol}
 */
const buttonElementSymbol = Symbol("buttonElement");
const innerDisabledObserverSymbol = Symbol("innerDisabledObserver");
const popperElementSymbol = Symbol("popperElement");
const contentElementSymbol = Symbol("contentElement");
const messageElementSymbol = Symbol("messageElement");
const measurementPopperSymbol = Symbol("measurementPopper");
const autoHideTimerSymbol = Symbol("autoHideTimer");
const MESSAGE_LAYOUT_OVERLAY = "overlay";
const MESSAGE_LAYOUT_PROSE = "prose";
const MESSAGE_LAYOUT_WIDE = "wide";

/**
 * A specialized button component that combines state management with message display capabilities.
 * It extends the Popper component to show messages in a popup overlay and can be used for form submissions
 * or manual actions.
 *
 * @fragments /fragments/components/form/message-state-button/
 * @example /examples/components/form/message-state-button-simple Success message
 * @example /examples/components/form/message-state-button-retry Retry and error message
 *
 * @since 2.11.0
 * @copyright Volker Schukai
 * @summary A stateful button that combines action feedback, progress states and inline status messaging.
 * @fires monster-state-changed - Fired when button state changes
 * @fires monster-message-shown - Fired when message is displayed
 * @fires monster-message-hidden - Fired when message is hidden
 * @fires monster-click - Fired when button is clicked
 */
class MessageStateButton extends Popper {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/form/message-state-button@@instance",
		);
	}

	/**
	 * Sets the state of the button which affects its visual appearance
	 *
	 * @param {string} state - The state to set (e.g. 'success', 'error', 'loading')
	 * @param {number} timeout - Optional timeout in milliseconds after which state is removed
	 * @return {MessageStateButton} Returns the button instance for chaining
	 * @throws {TypeError} When state is not a string or timeout is not a number
	 */
	setState(state, timeout) {
		return this[buttonElementSymbol].setState(state, timeout);
	}

	/**
	 *
	 * @return {MessageStateButton}
	 */
	removeState() {
		return this[buttonElementSymbol].removeState();
	}

	/**
	 * @return {MessageStateButton|undefined}
	 */
	getState() {
		return this[buttonElementSymbol].getState();
	}

	/**
	 * 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} message Message definition
	 * @property {string|HTMLElement} message.content The message content
	 * @property {string} message.title The message title
	 * @property {string} message.icon The message icon
	 * @property {Object} message.width Width options for the message popper
	 * @property {string|number} message.width.min Minimum width (px, rem, em, vw)
	 * @property {string|number} message.width.max Maximum width (px, rem, em, vw)
	 * @property {number} message.width.viewportRatio Max width as ratio of viewport width (0-1)
	 * @property {Object} popper Popper options inherited from the base popper
	 * @property {string} popper.contentOverflow Content clipping mode: both|horizontal|smart
	 * @property {string} mode The mode of the button, can be `manual` or `submit`
	 * @property {string} labels.button Button label
	 * @property {Object} classes Classes for internal elements
	 * @property {string} classes.button Button class
	 * @property {Object} actions Action callbacks
	 * @property {function} actions.click Action triggered on click
	 * @property {Object} aria Aria attributes
	 * @property {string} aria.role Aria role, only if the button is not a button
	 * @property {string} aria.label Aria label for the button
	 */
	get defaults() {
		const defaults = super.defaults;

		return Object.assign({}, defaults, {
			message: {
				title: undefined,
				content: undefined,
				icon: undefined,
				width: {
					min: "12rem",
					max: null,
					viewportRatio: null,
				},
			},
			templates: {
				main: getTemplate(),
			},
			popper: Object.assign({}, defaults.popper, {
				contentOverflow: "smart",
			}),
			mode: "manual",
			labels: {
				button: "<slot></slot>",
			},
			classes: {
				button: "monster-button-outline-primary",
			},
			actions: {
				click: (e) => {},
			},

			aria: {
				role: null,
				label: null,
			},
		});
	}

	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initDisabledSync.call(this);

		let modes = null;
		const modeOption = this.getOption("mode");
		if (typeof modeOption === "string") {
			modes = modeOption.split(" ");
		}

		if (
			modes === null ||
			modes === undefined ||
			isArray(modes) === false ||
			modes.length === 0
		) {
			modes = ["manual"];
		}

		for (const [, mode] of Object.entries(modes)) {
			initEventHandlerByMode.call(this, mode);
		}
	}

	/**
	 * Sets the message content to be displayed in the popup overlay
	 *
	 * @param {string|HTMLElement} message - The message content as string or HTML element
	 * @param {string} title - Optional title to show above the message
	 * @param {string} icon - Optional icon HTML to display next to the title
	 * @return {MessageStateButton} Returns the button instance for chaining
	 * @throws {TypeError} When message is empty or invalid type
	 */
	setMessage(message, title, icon) {
		if (isString(message)) {
			if (message === "") {
				throw new TypeError("message must not be empty");
			}

			const containerDiv = document.createElement("div");
			const messageDiv = document.createElement("div");
			const titleDiv = document.createElement("div");
			titleDiv.setAttribute(ATTRIBUTE_ROLE, "message-title-box");

			let titleElement, iconElement;
			if (title !== undefined) {
				title = validateString(title);
				titleElement = document.createElement("div");
				titleElement.setAttribute("class", "");
				titleElement.innerHTML = title;
				titleElement.setAttribute(ATTRIBUTE_ROLE, "message-title");
				titleDiv.appendChild(titleElement);
			}

			if (icon !== undefined) {
				icon = validateString(icon);
				iconElement = document.createElement("div");
				iconElement.setAttribute("class", "");
				iconElement.innerHTML = icon;
				iconElement.setAttribute(ATTRIBUTE_ROLE, "message-icon");
				titleDiv.appendChild(iconElement);
			}

			messageDiv.innerHTML = message;
			containerDiv.appendChild(titleDiv);
			containerDiv.appendChild(messageDiv);

			this.setOption("message.content", containerDiv);
		} else if (message instanceof HTMLElement) {
			this.setOption("message.content", message);
		} else {
			throw new TypeError(
				"message must be a string or an instance of HTMLElement",
			);
		}

		applyResolvedMessagePresentation.call(this);

		return this;
	}

	/**
	 * clears the Message
	 *
	 * @return {MessageStateButton}
	 */
	clearMessage() {
		this.setOption("message.title", undefined);
		this.setOption("message.content", undefined);
		this.setOption("message.icon", undefined);
		clearAutoHideTimer.call(this);
		applyResolvedMessagePresentation.call(this);
		return this;
	}

	/**
	 * Shows the message popup overlay with optional auto-hide timeout
	 *
	 * @param {number} timeout - Optional time in milliseconds after which the message will auto-hide
	 * @return {MessageStateButton} Returns the button instance for chaining
	 */
	showMessage(timeout) {
		clearAutoHideTimer.call(this);
		applyResolvedMessagePresentation.call(this);
		applyMeasuredMessageWidth.call(this);
		this.showDialog.call(this);

		if (timeout !== undefined) {
			this[autoHideTimerSymbol] = setTimeout(() => {
				this[autoHideTimerSymbol] = undefined;
				if (!this.isConnected) {
					return;
				}
				this.hideMessage();
			}, timeout);
		}

		return this;
	}

	/**
	 * With this method, you can show the popper.
	 *
	 * @return {MessageStateButton}
	 */
	showDialog() {
		if (this.getOption("message.content") === undefined) {
			return;
		}
		super.showDialog();
		return this;
	}

	/**
	 * @return {string}
	 */
	resolveContentOverflowMode() {
		return resolveContentOverflowMode.call(this);
	}

	/**
	 * Recalculates the open message popper after dynamic content changes.
	 *
	 * @return {MessageStateButton}
	 */
	recalcMessage() {
		applyResolvedMessagePresentation.call(this);
		applyMeasuredMessageWidth.call(this);
		super.recalcPopper();
		return this;
	}

	/**
	 *
	 * @return {MessageStateButton}
	 */
	hideMessage() {
		clearAutoHideTimer.call(this);
		super.hideDialog();
		return this;
	}

	disconnectedCallback() {
		clearAutoHideTimer.call(this);
		super.disconnectedCallback();
	}

	/**
	 *
	 * @return {MessageStateButton}
	 */
	toggleMessage() {
		super.toggleDialog();
		return this;
	}

	/**
	 *
	 * @return {Object}
	 */
	getMessage() {
		return this.getOption("message");
	}

	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-message-state-button";
	}

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		const styles = Popper.getCSSStyleSheet();
		styles.push(StateButtonStyleSheet);
		styles.push(MessageStateButtonStyleSheet);
		return styles;
	}

	/**
	 * Programmatically triggers a click event on the button
	 * Will not trigger if the button is disabled
	 *
	 * @since 3.27.0
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
	 * @fires monster-click
	 */
	click() {
		if (this.getOption("disabled") === true) {
			return;
		}

		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].click)
		) {
			this[buttonElementSymbol].click();
		}
	}

	/**
	 * The Button.focus() method sets focus on the internal button element.
	 *
	 * @since 3.27.0
	 * @param {Object} options
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
	 */
	focus(options) {
		if (this.getOption("disabled") === true) {
			return;
		}

		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].focus)
		) {
			this[buttonElementSymbol].focus(options);
		}
	}

	/**
	 * The Button.blur() method removes focus from the internal button element.
	 */
	blur() {
		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].blur)
		) {
			this[buttonElementSymbol].blur();
		}
	}
}

/**
 * @private
 * @param mode
 */
function initEventHandlerByMode(mode) {
	switch (mode) {
		case "manual":
			this[buttonElementSymbol].setOption("actions.click", (e) => {
				const callback = this.getOption("actions.click");
				if (isFunction(callback)) {
					callback(e);
				}
			});

			break;
		case "submit":
			this[buttonElementSymbol].setOption("actions.click", (e) => {
				const form = this.form;

				if (form instanceof HTMLFormElement) {
					form.requestSubmit();
				}
			});

			break;
	}
}

function clearAutoHideTimer() {
	if (this[autoHideTimerSymbol] !== undefined) {
		clearTimeout(this[autoHideTimerSymbol]);
		this[autoHideTimerSymbol] = undefined;
	}
}

/**
 * @private
 * @return {Select}
 */
function initControlReferences() {
	this[buttonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=button]`,
	);
	this[popperElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper]`,
	);
	this[contentElementSymbol] =
		this.shadowRoot.querySelector(`[part="content"]`);
	this[messageElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=message]`,
	);
	if (this[popperElementSymbol] instanceof HTMLElement) {
		this[popperElementSymbol].monsterBeforeFloatingUpdate = () => {
			applyMeasuredMessageWidth.call(this);
		};
	}
	applyResolvedMessagePresentation.call(this);
}

/**
 * @private
 */
function initDisabledSync() {
	const self = this;
	const attachInnerObserver = (button) => {
		if (!button || !isFunction(button.attachObserver)) {
			return;
		}

		const existing = self[innerDisabledObserverSymbol];
		if (existing?.button === button) {
			return;
		}

		if (
			existing?.button &&
			isFunction(existing.button.detachObserver) &&
			existing.observer
		) {
			existing.button.detachObserver(existing.observer);
		}

		const observer = new Observer(syncDisabled);
		button.attachObserver(observer);
		self[innerDisabledObserverSymbol] = { button, observer };
	};

	const syncDisabled = () => {
		const disabled = self.getOption("disabled", false);
		const button =
			self.shadowRoot?.querySelector(`[${ATTRIBUTE_ROLE}=button]`) ??
			self[buttonElementSymbol];
		if (!button) {
			return;
		}

		self[buttonElementSymbol] = button;

		if (disabled) {
			button.setAttribute(ATTRIBUTE_DISABLED, "");
		} else {
			button.removeAttribute(ATTRIBUTE_DISABLED);
		}

		if (isFunction(button.setOption)) {
			button.setOption("disabled", disabled);
		}

		attachInnerObserver(button);
	};

	syncDisabled();
	const existingObserver = self[attributeObserverSymbol]?.[ATTRIBUTE_DISABLED];
	if (existingObserver) {
		self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
			existingObserver.call(self);
			syncDisabled();
		};
	}
	self.attachObserver(new Observer(syncDisabled));
	if (typeof customElements?.whenDefined === "function") {
		customElements.whenDefined("monster-state-button").then(() => {
			syncDisabled();
		});
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">

            <monster-state-button exportparts="button:button-button,control:button-control"
                                  data-monster-attributes="
                                       data-monster-option-classes-button path:classes.button, 
                                       data-monster-option-aria-role path:aria.role,
                                       data-monster-option-aria-label path:aria.label,
                                       disabled path:disabled | if:true"
                                  part="button"
                                  name="button"
                                  data-monster-role="button">
                <span data-monster-replace="path:labels.button"></span>
            </monster-state-button>

            <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
                <div data-monster-role="arrow"></div>
                <div part="content"
                     class="flex">
                    <div data-monster-role="message" part="message" class="flex"
                         data-monster-patch="path:message.content"></div>
                </div>
            </div>
        </div>
        </div>
    `;
}

/**
 * @private
 * @param {string|HTMLElement} content
 * @return {HTMLElement|string|null}
 */
function getMeasurementContent(content) {
	if (isString(content)) {
		return content;
	}

	if (content instanceof HTMLElement) {
		return content.cloneNode(true);
	}

	return null;
}

/**
 * @private
 * @return {void}
 */
function applyResolvedMessagePresentation() {
	const contentElement = this[contentElementSymbol];
	const messageElement = this[messageElementSymbol];
	const layoutMode = resolveMessageLayoutMode.call(this);
	const overflowMode = this.resolveContentOverflowMode();

	if (contentElement instanceof HTMLElement) {
		contentElement.setAttribute("data-monster-overflow-mode", overflowMode);
		contentElement.setAttribute("data-monster-message-layout", layoutMode);
	}

	if (messageElement instanceof HTMLElement) {
		messageElement.setAttribute("data-monster-message-layout", layoutMode);
	}

	if (this[measurementPopperSymbol]?.content instanceof HTMLElement) {
		this[measurementPopperSymbol].content.setAttribute(
			"data-monster-overflow-mode",
			overflowMode,
		);
		this[measurementPopperSymbol].content.setAttribute(
			"data-monster-message-layout",
			layoutMode,
		);
	}

	if (this[measurementPopperSymbol]?.message instanceof HTMLElement) {
		this[measurementPopperSymbol].message.setAttribute(
			"data-monster-message-layout",
			layoutMode,
		);
	}
}

/**
 * @private
 * @return {string}
 */
function resolveContentOverflowMode() {
	const configuredMode = this.getOption("popper.contentOverflow", "smart");

	if (configuredMode !== "smart") {
		return configuredMode;
	}

	if (resolveMessageLayoutMode.call(this) === MESSAGE_LAYOUT_OVERLAY) {
		return "horizontal";
	}

	return "both";
}

/**
 * @private
 * @return {string}
 */
function resolveMessageLayoutMode() {
	const content = this.getOption("message.content");

	if (containsNestedOverlayContent(content)) {
		return MESSAGE_LAYOUT_OVERLAY;
	}

	if (containsWideContent(content)) {
		return MESSAGE_LAYOUT_WIDE;
	}

	return MESSAGE_LAYOUT_PROSE;
}

/**
 * @private
 * @param {unknown} content
 * @return {boolean}
 */
function containsNestedOverlayContent(content) {
	const selector = [
		"monster-details",
		"monster-message-state-button",
		"monster-popper",
		"monster-popper-button",
		"monster-select",
		"details",
	].join(",");

	if (isString(content)) {
		const container = document.createElement("div");
		container.innerHTML = content;
		return container.querySelector(selector) instanceof HTMLElement;
	}

	if (!(content instanceof HTMLElement)) {
		return false;
	}

	if (content.matches(selector)) {
		return true;
	}

	return content.querySelector(selector) instanceof HTMLElement;
}

/**
 * @private
 * @param {unknown} content
 * @return {boolean}
 */
function containsWideContent(content) {
	const root = getContentSearchRoot(content);
	if (!(root instanceof HTMLElement)) {
		return false;
	}

	const selector = [
		'[data-monster-message-layout="wide"]',
		"pre",
		"table",
		'[style*="white-space: nowrap"]',
		'[style*="white-space:nowrap"]',
		'[style*="overflow-x: auto"]',
		'[style*="overflow-x:auto"]',
		'[style*="overflow-x: scroll"]',
		'[style*="overflow-x:scroll"]',
		'[style*="overflow: auto"]',
		'[style*="overflow:auto"]',
		'[style*="overflow: scroll"]',
		'[style*="overflow:scroll"]',
	].join(",");

	if (root.matches(selector)) {
		return true;
	}

	return root.querySelector(selector) instanceof HTMLElement;
}

/**
 * @private
 * @param {unknown} content
 * @return {HTMLElement|null}
 */
function getContentSearchRoot(content) {
	if (isString(content)) {
		const container = document.createElement("div");
		container.innerHTML = content;
		return container;
	}

	if (content instanceof HTMLElement) {
		return content;
	}

	return null;
}

/**
 * @private
 * @return {{popper: HTMLElement, content: HTMLElement, message: HTMLElement}|null}
 */
function ensureMeasurementPopper() {
	if (this[measurementPopperSymbol]) {
		return this[measurementPopperSymbol];
	}

	if (!this.shadowRoot) {
		return null;
	}

	const popper = document.createElement("div");
	popper.setAttribute(ATTRIBUTE_ROLE, "popper");
	popper.setAttribute("data-measurement", "true");
	popper.setAttribute("aria-hidden", "true");
	popper.style.position = "absolute";
	popper.style.left = "-10000px";
	popper.style.top = "-10000px";
	popper.style.visibility = "hidden";
	popper.style.display = "block";
	popper.style.pointerEvents = "none";
	popper.style.maxWidth = "none";
	popper.style.width = "max-content";
	if (this[popperElementSymbol]?.className) {
		popper.className = this[popperElementSymbol].className;
	}

	const content = document.createElement("div");
	content.setAttribute("part", "content");

	const message = document.createElement("div");
	message.setAttribute(ATTRIBUTE_ROLE, "message");
	message.className = "flex";

	content.appendChild(message);
	popper.appendChild(content);
	this.shadowRoot.appendChild(popper);

	this[measurementPopperSymbol] = { popper, content, message };
	applyResolvedMessagePresentation.call(this);
	return this[measurementPopperSymbol];
}

/**
 * @private
 */
function applyMeasuredMessageWidth() {
	const popper = this[popperElementSymbol];
	if (!popper) {
		return;
	}

	const content = this.getOption("message.content");
	const measureContent = getMeasurementContent(content);
	if (!measureContent) {
		return;
	}

	const measurement = ensureMeasurementPopper.call(this);
	if (!measurement?.message) {
		return;
	}

	if (popper.className && measurement.popper.className !== popper.className) {
		measurement.popper.className = popper.className;
	}

	applyResolvedMessagePresentation.call(this);

	measurement.message.innerHTML = "";
	if (isString(measureContent)) {
		measurement.message.innerHTML = measureContent;
	} else {
		measurement.message.appendChild(measureContent);
	}

	const measuredWidth = Math.ceil(
		measurement.popper.getBoundingClientRect().width,
	);
	const fontSize = parseFloat(getComputedStyle(popper).fontSize) || 16;
	const rootFontSize =
		parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
	const widthOptions = this.getOption("message.width", {});
	const layoutMode = resolveMessageLayoutMode.call(this);
	const minWidthOption = resolveLength(
		widthOptions?.min,
		fontSize,
		rootFontSize,
		window.innerWidth,
	);
	const maxWidthOption = resolveLength(
		widthOptions?.max,
		fontSize,
		rootFontSize,
		window.innerWidth,
	);
	const viewportPadding = rootFontSize * 2;
	const viewportWidthLimit = Math.max(0, window.innerWidth - viewportPadding);
	const viewportRatio = resolveViewportRatio(
		widthOptions?.viewportRatio,
		layoutMode,
	);
	const fallbackMaxWidth =
		layoutMode === MESSAGE_LAYOUT_WIDE ? viewportWidthLimit : fontSize * 32;

	const minWidth = Math.max(0, minWidthOption ?? Math.round(fontSize * 12));
	const maxViewportWidth = Math.max(
		minWidth,
		Math.min(window.innerWidth * viewportRatio, viewportWidthLimit),
	);
	const maxWidth = Math.max(
		minWidth,
		Math.min(maxWidthOption ?? fallbackMaxWidth, maxViewportWidth),
	);
	const targetWidth = Math.max(
		minWidth,
		Math.min(measuredWidth || minWidth, maxWidth),
	);

	popper.dataset.monsterPreferredMaxWidth = `${maxWidth}`;
	popper.style.width = `${targetWidth}px`;
	popper.style.minWidth = `${minWidth}px`;
	popper.style.maxWidth = `${maxWidth}px`;
	popper.style.removeProperty("white-space");
	popper.style.removeProperty("overflow-wrap");
}

/**
 * @private
 * @param {unknown} value
 * @param {string} layoutMode
 * @return {number}
 */
function resolveViewportRatio(value, layoutMode) {
	if (
		typeof value === "number" &&
		Number.isFinite(value) &&
		value > 0 &&
		value <= 1
	) {
		return value;
	}

	return layoutMode === MESSAGE_LAYOUT_WIDE ? 1 : 0.7;
}

/**
 * @private
 * @param {unknown} value
 * @param {number} fontSize
 * @param {number} rootFontSize
 * @param {number} viewportWidth
 * @return {number|null}
 */
function resolveLength(value, fontSize, rootFontSize, viewportWidth) {
	if (typeof value === "number" && Number.isFinite(value)) {
		return value;
	}

	if (!isString(value)) {
		return null;
	}

	const trimmed = value.trim();
	const number = parseFloat(trimmed);
	if (!Number.isFinite(number)) {
		return null;
	}

	if (trimmed.endsWith("rem")) {
		return number * rootFontSize;
	}

	if (trimmed.endsWith("em")) {
		return number * fontSize;
	}

	if (trimmed.endsWith("vw")) {
		return (number / 100) * viewportWidth;
	}

	if (trimmed.endsWith("px")) {
		return number;
	}

	return number;
}

registerCustomElement(MessageStateButton);
