/**
 * 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 {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findElementWithSelectorUpwards, getWindow } from "../../dom/util.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs";
import { Datasource } from "./datasource.mjs";
import { Observer } from "../../types/observer.mjs";
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { PaginationStyleSheet } from "./stylesheet/pagination.mjs";
import { DisplayStyleSheet } from "../stylesheet/display.mjs";
import { isString } from "../../types/is.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { instanceSymbol } from "../../constants.mjs";
import { Formatter } from "../../text/formatter.mjs";
import "../form/select.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";

import "./datasource/dom.mjs";
import "./datasource/rest.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";

export { Pagination };

/**
 * @private
 * @type {symbol}
 */
const paginationElementSymbol = Symbol.for("paginationElement");

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

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

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

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

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

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

/**
 * @private
 * @type {symbol}
 */
const labelStateSymbol = Symbol("labelState");
const layoutModeSymbol = Symbol("layoutMode");
const layoutMeasurementCacheSymbol = Symbol("layoutMeasurementCache");
const lastNavClickTimeSymbol = Symbol("lastNavClickTime");
const lastNavClickTargetSymbol = Symbol("lastNavClickTarget");

const minNavDoubleClickDelayMs = 200;

const compactPrevIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0m3.5 7.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5z"/></svg>`;
const compactNextIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z"/></svg>`;

/**
 * A Pagination component
 *
 * @fragments /fragments/components/datatable/pagination
 *
 * @example /examples/components/datatable/pagination-simple Datasource pagination
 * @example /examples/components/datatable/pagination-programmatic Programmatic pagination state
 *
 * @copyright Volker Schukai
 * @summary A pagination control for navigating multi-page result sets and synchronizing table state.
 */
class Pagination extends CustomElement {
	/**
	 */
	constructor() {
		super();
		this[datasourceLinkedElementSymbol] = null;
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/pagination");
	}

	/**
	 * 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} datasource Datasource configuration
	 * @property {string} datasource.selector Datasource selector
	 * @property {Object} labels Label definitions
	 * @property {string} labels.page Page label
	 * @property {string} labels.description Description label
	 * @property {string} labels.previous Previous label
	 * @property {string} labels.next Next label
	 * @property {string} labels.of Of label
	 * @property {string} href Href
	 * @property {number} currentPage Current page
	 * @property {number} pages Pages
	 * @property {number} objectsPerPage Objects per page
	 * @property {Object} layout Layout configuration
	 * @property {"auto"|"compact"} layout.mode Layout mode. Use `"compact"` to always show the small arrow pagination.
	 * @property {Object} mapping Mapping
	 * @property {string} mapping.pages Pages mapping
	 * @property {string} mapping.objectsPerPage Objects per page mapping
	 * @property {string} mapping.currentPage Current page mapping
	 */
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				templates: {
					main: getTemplate(),
				},

				datasource: {
					selector: null,
				},

				labels: getTranslations(),

				callbacks: {
					click: null,
				},

				href: "page-${page}",

				pages: null,
				objectsPerPage: 20,
				currentPage: null,

				layout: {
					mode: "auto",
				},

				mapping: {
					pages: "sys.pagination.pages",
					objectsPerPage: "sys.pagination.objectsPerPage",
					currentPage: "sys.pagination.currentPage",
				},

				/* @private */
				pagination: {
					items: [],
				},
			},
			initOptionsFromArguments.call(this),
		);
	}
	/**
	 * Sets the pagination state directly, without requiring a datasource.
	 * This is useful for controlling the component programmatically.
	 *
	 * @param {object} state - The state object for the pagination.
	 * @param {number} state.currentPage - The current active page number.
	 * @param {number} state.totalPages - The total number of available pages.
	 * @return {void}
	 */
	setPaginationState({ currentPage, totalPages }) {
		if (typeof currentPage !== "number" || typeof totalPages !== "number") {
			console.error(
				"setPaginationState requires currentPage and totalPages to be numbers.",
			);
			return;
		}

		const previousCurrentPage = Number.parseInt(
			this.getOption("currentPage"),
			10,
		);
		const previousTotalPages = Number.parseInt(this.getOption("pages"), 10);
		const stateUnchanged =
			Number.isFinite(previousCurrentPage) &&
			Number.isFinite(previousTotalPages) &&
			previousCurrentPage === currentPage &&
			previousTotalPages === totalPages;

		// 1. Update the component's internal options with the new values.
		this.setOption("currentPage", currentPage);
		this.setOption("pages", totalPages);

		if (stateUnchanged) {
			this.refreshLayout({ schedule: true });
			return;
		}

		// 2. Call the existing buildPagination function to recalculate the links.
		const pagination = buildPagination.call(
			this,
			this.getOption("currentPage"),
			this.getOption("pages"),
		);

		// 3. Preserve the responsive visibility of page numbers.
		if (
			!isAdaptivePagination.call(this) &&
			this?.[sizeDataSymbol]?.showNumbers !== true
		) {
			pagination.items = [];
		}

		// 4. Set the 'pagination' option, which will trigger the component to re-render.
		this.setOption("pagination", pagination);

		this.refreshLayout({ schedule: true });

		syncPaginationStateToDom.call(this, pagination);
	}

	refreshLayout({ schedule = false } = {}) {
		if (isForcedCompactPagination.call(this)) {
			applyForcedCompactPaginationLayout.call(this);
			return this;
		}

		if (schedule === true) {
			if (isAdaptivePagination.call(this)) {
				schedulePaginationLayoutUpdate.call(this);
			} else {
				const list = this.shadowRoot?.querySelector(".pagination-list");
				if (list) {
					list.setAttribute("data-monster-adaptive-ready", "true");
				}
			}
			return this;
		}

		if (isAdaptivePagination.call(this)) {
			applyPaginationLayout.call(this);
			return this;
		}

		const list = this.shadowRoot?.querySelector(".pagination-list");
		if (list) {
			list.setAttribute("data-monster-adaptive-ready", "true");
		}

		return this;
	}
	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-pagination";
	}

	/**
	 * @return {void}
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
		if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
			this[resizeObserverSymbol].disconnect();
		}
		if (this?.[debounceSizeSymbol] instanceof DeadMansSwitch) {
			try {
				this[debounceSizeSymbol].defuse();
				delete this[debounceSizeSymbol];
			} catch (e) {
				// already fired
			}
		}
	}

	/**
	 * @return {void}
	 */
	connectedCallback() {
		super.connectedCallback();

		const parentNode = this.parentNode;
		if (!parentNode) {
			return;
		}

		try {
			handleDataSourceChanges.call(this);
		} catch (e) {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e?.message || `${e}`);
		}

		setTimeout(() => {
			const parentParentNode = parentNode?.parentNode || parentNode;
			const sizeContainer =
				parentParentNode instanceof HTMLElement
					? parentParentNode
					: parentNode instanceof HTMLElement
						? parentNode
						: this;

			this[resizeObserverSymbol] = new ResizeObserver(() => {
				if (this[layoutApplySymbol]) {
					return;
				}
				if (this[debounceSizeSymbol] instanceof DeadMansSwitch) {
					try {
						this[debounceSizeSymbol].touch();
						return;
					} catch (e) {
						delete this[debounceSizeSymbol];
					}
				}

				this[debounceSizeSymbol] = new DeadMansSwitch(250, () => {
					queueMicrotask(() => {
						if (isAdaptivePagination.call(this)) {
							schedulePaginationLayoutUpdate.call(this);
							return;
						}

						const parentWidth = sizeContainer.offsetWidth;
						const ownWidth = this.clientWidth;

						if (this[sizeDataSymbol]?.last?.parentWidth === parentWidth) {
							return;
						}

						this[sizeDataSymbol].last = {
							parentWidth: parentWidth,
						};

						this[sizeDataSymbol].showNumbers = ownWidth <= parentWidth;
						handleDataSourceChanges.call(this);
					});
				});
			});

			if (isForcedCompactPagination.call(this)) {
				this[sizeDataSymbol] = {
					last: {
						parentWidth: 0,
					},
					showNumbers: false,
				};
				applyForcedCompactPaginationLayout.call(this);
			} else if (isAdaptivePagination.call(this)) {
				this[sizeDataSymbol] = {
					last: {
						parentWidth: 0,
					},
					showNumbers: true,
				};
				schedulePaginationLayoutUpdate.call(this);
			} else {
				const parentWidth = sizeContainer.offsetWidth;
				const ownWidth = this.offsetWidth;

				this[sizeDataSymbol] = {
					last: {
						parentWidth: 0,
					},
					showNumbers: ownWidth < parentWidth,
				};
				const list = this.shadowRoot?.querySelector(".pagination-list");
				if (list) {
					list.setAttribute("data-monster-adaptive-ready", "true");
				}
			}

			this[resizeObserverSymbol].observe(sizeContainer);
		}, 0);
	}

	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

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

		const selector = this.getOption("datasource.selector", "");

		if (isString(selector)) {
			const element = findElementWithSelectorUpwards(this, selector);
			if (element === null) {
				throw new Error("the selector must match exactly one element");
			}

			if (!(element instanceof Datasource)) {
				throw new TypeError("the element must be a datasource");
			}

			this[datasourceLinkedElementSymbol] = element;
			element.datasource.attachObserver(
				new Observer(handleDataSourceChanges.bind(this)),
			);

			element.attachObserver(new Observer(handleDataSourceChanges.bind(this)));

			handleDataSourceChanges.call(this);
		}
	}

	/**
	 * @private
	 * @return {CSSStyleSheet}
	 */
	static getControlCSSStyleSheet() {
		return PaginationStyleSheet;
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [this.getControlCSSStyleSheet(), DisplayStyleSheet, ThemeStyleSheet];
	}
}

function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				page: "${page}",
				description: "Seite ${page}",
				previous: "Vorherige",
				next: "Nächste",
				of: "von",
				summary: "Seite ${current} von ${max}",
			};
		case "fr":
			return {
				page: "${page}",
				description: "Page ${page}",
				previous: "Précédent",
				next: "Suivant",
				of: "de",
				summary: "Page ${current} sur ${max}",
			};
		case "sp":
			return {
				page: "${page}",
				description: "Página ${page}",
				previous: "Anterior",
				next: "Siguiente",
				of: "de",
				summary: "Página ${current} de ${max}",
			};
		case "it":
			return {
				page: "${page}",
				description: "Pagina ${page}",
				previous: "Precedente",
				next: "Successivo",
				of: "di",
				summary: "Pagina ${current} di ${max}",
			};
		case "pl":
			return {
				page: "${page}",
				description: "Strona ${page}",
				previous: "Poprzednia",
				next: "Następna",
				of: "z",
				summary: "Strona ${current} z ${max}",
			};
		case "no":
			return {
				page: "${page}",
				description: "Side ${page}",
				previous: "Forrige",
				next: "Neste",
				of: "av",
				summary: "Side ${current} av ${max}",
			};
		case "dk":
			return {
				page: "${page}",
				description: "Side ${page}",
				previous: "Forrige",
				next: "Næste",
				of: "af",
				summary: "Side ${current} af ${max}",
			};
		case "sw":
			return {
				page: "${page}",
				description: "Sida ${page}",
				previous: "Föregående",
				next: "Nästa",
				of: "av",
				summary: "Sida ${current} av ${max}",
			};
		default:
		case "en":
			return {
				page: "${page}",
				description: "Page ${page}",
				previous: "Previous",
				next: "Next",
				of: "of",
				summary: "${current} of ${max}",
			};
	}
}

/**
 * @private
 * @return {Select}
 * @throws {Error} no shadow-root is defined
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[paginationElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=pagination]",
	);
}

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

	self[paginationElementSymbol].addEventListener("click", function (event) {
		const prevTarget = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"pagination-prev",
		);
		const nextTarget = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"pagination-next",
		);
		const itemTarget = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"pagination-item",
		);
		const element = itemTarget || prevTarget || nextTarget;

		if (
			!(element instanceof HTMLElement) ||
			!element.hasAttribute("data-page-no")
		) {
			return;
		}

		if (
			(event.detail === 1 || event.detail === 0) &&
			(prevTarget || nextTarget)
		) {
			self[lastNavClickTimeSymbol] = event.timeStamp;
			self[lastNavClickTargetSymbol] = prevTarget ? "prev" : "next";
		}

		const page = element.getAttribute("data-page-no");

		if (!page || page === "…" || page === "null" || page === "undefined") {
			return;
		}

		event.preventDefault();

		const datasource = self[datasourceLinkedElementSymbol];
		const clickCallback = self.getOption("callbacks.click");

		if (datasource && typeof datasource.setParameters === "function") {
			datasource.setParameters({ page });
			if (typeof datasource.reload === "function") {
				datasource.reload();
			}
		} else if (typeof clickCallback === "function") {
			clickCallback(parseInt(page, 10), event);
		}
	});

	self[paginationElementSymbol].addEventListener("dblclick", function (event) {
		const prevTarget = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"pagination-prev",
		);
		const nextTarget = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"pagination-next",
		);

		if (!prevTarget && !nextTarget) {
			return;
		}

		event.preventDefault();

		const lastTarget = self[lastNavClickTargetSymbol];
		const lastTime = self[lastNavClickTimeSymbol];
		const currentTarget = prevTarget ? "prev" : "next";
		if (
			lastTarget !== currentTarget ||
			!Number.isFinite(lastTime) ||
			event.timeStamp - lastTime < minNavDoubleClickDelayMs
		) {
			return;
		}

		const maxPage = parseInt(self.getOption("pages"), 10);
		if (!Number.isFinite(maxPage) || maxPage <= 0) {
			return;
		}

		const targetPage = prevTarget ? 1 : maxPage;

		const datasource = self[datasourceLinkedElementSymbol];
		const clickCallback = self.getOption("callbacks.click");

		if (datasource && typeof datasource.setParameters === "function") {
			datasource.setParameters({ page: targetPage });
			if (typeof datasource.reload === "function") {
				datasource.reload();
			}
		} else if (typeof clickCallback === "function") {
			clickCallback(targetPage, event);
		}
	});
}

/**
 * This attribute can be used to pass a URL to this select.
 *
 * ```
 * <monster-form data-monster-datasource="restapi:....."></monster-form>
 * ```
 *
 * @private
 * @return {object}
 * @throws {TypeError} incorrect arguments passed for the datasource
 * @throws {Error} the datasource could not be initialized
 */
function initOptionsFromArguments() {
	const options = {};
	const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
	if (selector) {
		options.datasource = { selector: selector };
	}

	return options;
}

/**
 * @private
 */
function handleDataSourceChanges() {
	let pagination;

	if (!this[datasourceLinkedElementSymbol]) {
		return;
	}

	const mapping = this.getOption("mapping");
	const pf = new Pathfinder(this[datasourceLinkedElementSymbol].data);

	for (const key in mapping) {
		const path = mapping[key];

		if (pf.exists(path)) {
			const value = pf.getVia(path);
			this.setOption(key, value);
		}

		const o = this[datasourceLinkedElementSymbol].getOption(path);
		if (o !== undefined && o !== null) {
			this.setOption(key, o);
		}
	}

	pagination = buildPagination.call(
		this,
		this.getOption("currentPage"),
		this.getOption("pages"),
	);
	if (this?.[sizeDataSymbol]?.showNumbers !== true) {
		pagination.items = [];
	}

	this.setOption("pagination", pagination);

	this.refreshLayout({ schedule: isAdaptivePagination.call(this) });
}

/**
 * @private
 * @param current
 * @param max
 * @return  {object}
 */
function buildPagination(current, max) {
	const parsedCurrent = Number.parseInt(current, 10);
	const parsedMax = Number.parseInt(max, 10);

	max = Number.isFinite(parsedMax) && parsedMax >= 1 ? parsedMax : 1;
	current = Number.isFinite(parsedCurrent) ? parsedCurrent : 1;
	if (current < 1) {
		current = 1;
	} else if (current > max) {
		current = max;
	}

	let prev = current === 1 ? null : current - 1;
	let next = current === max ? null : current + 1;
	const itemList = [1];

	if (current > 4) itemList.push(-1);

	const r = 2;
	const r1 = current - r;
	const r2 = current + r;

	for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) itemList.push(i);

	if (r2 + 1 < max) itemList.push(-1);
	if (r2 < max) itemList.push(max);

	let prevClass = "";

	if (prev === null) {
		prevClass = " disabled";
	}

	let nextClass = "";
	if (next === null) {
		nextClass = " disabled";
	}

	const items = itemList.map((item) => {
		let p = `${item}`;

		if (item === -1) {
			item = null;
			p = "…";
		}

		const c = `${current}`;

		const obj = {
			pageNo: item, // as integer
			page: p, // as string
			current: p === c,
			class: (p === c ? "current" : "").trim(),
		};

		if (item === null) {
			obj.class += " disabled".trim();
		}

		const formatter = new Formatter(obj);

		obj.description = formatter.format(this.getOption("labels.description"));
		obj.label = formatter.format(this.getOption("labels.page"));
		obj.href =
			item === null
				? "#"
				: p === c
					? "#"
					: p === "1"
						? "#"
						: `#${formatter.format(this.getOption("href"))}`;
		return obj;
	});

	const nextNo = next;
	next = `${next}`;

	const nextHref =
		next === "null"
			? "#"
			: `#${new Formatter({ page: next }).format(this.getOption("href"))}`;
	const prevNo = prev;
	prev = `${prev}`;
	const prevHref =
		prev === "null"
			? "#"
			: `#${new Formatter({ page: prev }).format(this.getOption("href"))}`;
	const ofLabel = this.getOption("labels.of") || "of";
	const summaryTemplate = this.getOption("labels.summary");
	const summaryContext = {
		current: String(current),
		max: String(max),
		of: String(ofLabel),
	};
	const summary = isString(summaryTemplate)
		? new Formatter(summaryContext).format(summaryTemplate)
		: `${summaryContext.current} ${summaryContext.of} ${summaryContext.max}`;

	return {
		current,
		nextNo,
		next,
		nextClass,
		nextHref,
		prevNo,
		prev,
		prevClass,
		prevHref,
		summary,
		items,
	};
}

/**
 * @private
 * @param {object} pagination
 * @returns {void}
 */
function syncPaginationStateToDom(pagination) {
	if (!this.shadowRoot || !pagination) {
		return;
	}

	const prevLink = this.shadowRoot.querySelector(
		"a[data-monster-role=pagination-prev]",
	);
	if (prevLink instanceof HTMLElement) {
		prevLink.setAttribute("href", pagination.prevHref || "#");
		prevLink.setAttribute("data-page-no", `${pagination.prevNo ?? ""}`);
		prevLink.className = `previous${pagination.prevClass || ""}`.trim();
	}

	const nextLink = this.shadowRoot.querySelector(
		"a[data-monster-role=pagination-next]",
	);
	if (nextLink instanceof HTMLElement) {
		nextLink.setAttribute("href", pagination.nextHref || "#");
		nextLink.setAttribute("data-page-no", `${pagination.nextNo ?? ""}`);
		nextLink.className = `next${pagination.nextClass || ""}`.trim();
	}

	const summary = this.shadowRoot.querySelector(
		"[data-monster-role=pagination-summary] span",
	);
	if (summary instanceof HTMLElement) {
		summary.textContent = pagination.summary || "";
	}
}

/**
 * @private
 */
function schedulePaginationLayoutUpdate() {
	if (isForcedCompactPagination.call(this)) {
		applyForcedCompactPaginationLayout.call(this);
		return;
	}

	if (!isAdaptivePagination.call(this)) {
		return;
	}
	if (this[layoutUpdateSymbol] || this[layoutApplySymbol]) {
		return;
	}

	this[layoutUpdateSymbol] = true;
	requestAnimationFrame(() => {
		this[layoutUpdateSymbol] = false;
		applyPaginationLayout.call(this);
	});
}

/**
 * @private
 */
function applyPaginationLayout() {
	if (isForcedCompactPagination.call(this)) {
		applyForcedCompactPaginationLayout.call(this);
		return;
	}

	if (!this.isConnected || !this.shadowRoot) {
		return;
	}

	if (!isAdaptivePagination.call(this)) {
		return;
	}

	const parentNode = this.parentNode;
	const parentParentNode = parentNode?.parentNode || parentNode;
	const availableWidth = isSelectPagination.call(this)
		? getSelectAvailableWidth.call(this, parentNode)
		: isEmbeddedPagination.call(this)
			? getEmbeddedAvailableWidth.call(this, parentNode)
			: this.getBoundingClientRect().width ||
				parentParentNode?.offsetWidth ||
				0;

	if (!availableWidth) {
		return;
	}

	this[layoutApplySymbol] = true;
	try {
		ensureLabelState.call(this);

		const list = this.shadowRoot.querySelector(".pagination-list");
		if (!list) {
			return;
		}

		const prevLink = this.shadowRoot.querySelector(
			"a[data-monster-role=pagination-prev]",
		);
		const nextLink = this.shadowRoot.querySelector(
			"a[data-monster-role=pagination-next]",
		);
		const signature = getPaginationLayoutSignature.call(this, list);
		const cache = getPaginationLayoutCache.call(this);

		if (
			cache.lastAppliedSignature === signature &&
			cache.lastAvailableWidth === availableWidth &&
			cache.lastAppliedMode === this[layoutModeSymbol] &&
			list.getAttribute("data-monster-adaptive-ready") === "true"
		) {
			return;
		}

		setClassEnabled(list, "pagination-no-wrap", true);
		setAttributeValue(list, "data-monster-adaptive-ready", "false");

		const { widthFull, widthNoNumbers, widthCompactSummary, widthCompact } =
			measurePaginationModeWidths.call(
				this,
				list,
				prevLink,
				nextLink,
				signature,
			);

		const nextMode = choosePaginationMode.call(this, {
			availableWidth,
			widthFull,
			widthNoNumbers,
			widthCompactSummary,
			widthCompact,
		});

		applyPaginationMode.call(this, list, prevLink, nextLink, nextMode);
		setAttributeValue(list, "data-monster-adaptive-ready", "true");
		cache.lastAppliedSignature = signature;
		cache.lastAvailableWidth = availableWidth;
		cache.lastAppliedMode = nextMode;
	} finally {
		this[layoutApplySymbol] = false;
	}
}

/**
 * @private
 */
function applyForcedCompactPaginationLayout() {
	if (!this.isConnected || !this.shadowRoot) {
		return;
	}

	if (this[layoutApplySymbol]) {
		return;
	}

	this[layoutApplySymbol] = true;
	try {
		ensureLabelState.call(this);

		const list = this.shadowRoot.querySelector(".pagination-list");
		if (!list) {
			return;
		}

		const prevLink = this.shadowRoot.querySelector(
			"a[data-monster-role=pagination-prev]",
		);
		const nextLink = this.shadowRoot.querySelector(
			"a[data-monster-role=pagination-next]",
		);

		setClassEnabled(list, "pagination-no-wrap", true);
		applyPaginationMode.call(this, list, prevLink, nextLink, "compact");
		setAttributeValue(list, "data-monster-adaptive-ready", "true");
		this[layoutModeSymbol] = "compact";
	} finally {
		this[layoutApplySymbol] = false;
	}
}

/**
 * @private
 * @return {boolean}
 */
function isForcedCompactPagination() {
	return this.getOption("layout.mode") === "compact";
}

/**
 * @private
 * @param {number} parentWidth
 * @return {boolean}
 */
function paginationFits(parentWidth) {
	const list = this.shadowRoot.querySelector(".pagination-list");
	if (!list) {
		return true;
	}
	return list.scrollWidth <= parentWidth;
}

/**
 * @private
 * @param {HTMLElement} list
 * @param {boolean} visible
 */
function setNumberItemsVisible(list, visible) {
	const numberItems = list.querySelectorAll(
		"[data-monster-role=pagination-item]",
	);
	for (const item of numberItems) {
		const container = item.parentElement;
		if (!container) continue;
		setStyleValue(container, "display", visible ? "" : "none");
	}
}

/**
 * @private
 * @param {HTMLElement} list
 * @param {boolean} visible
 */
function setSummaryVisible(list, visible) {
	const summaryItem = list.querySelector(
		"[data-monster-role=pagination-summary]",
	);
	if (!summaryItem) {
		return;
	}
	setStyleValue(summaryItem, "display", visible ? "flex" : "none");
}

/**
 * @private
 */
function ensureLabelState() {
	if (this[labelStateSymbol]) {
		return;
	}

	const prevLink = this.shadowRoot?.querySelector(
		"a[data-monster-role=pagination-prev]",
	);
	const nextLink = this.shadowRoot?.querySelector(
		"a[data-monster-role=pagination-next]",
	);

	this[labelStateSymbol] = {
		previous: prevLink?.innerHTML || this.getOption("labels.previous"),
		next: nextLink?.innerHTML || this.getOption("labels.next"),
	};
}

/**
 * @private
 * @param {boolean} compact
 */
function setCompactLabels(compact, prevLink, nextLink) {
	const state = this[labelStateSymbol];
	if (!state) return;

	if (compact) {
		setInnerHTML(prevLink, compactPrevIcon);
		setInnerHTML(nextLink, compactNextIcon);
	} else {
		setInnerHTML(prevLink, state.previous);
		setInnerHTML(nextLink, state.next);
	}
}

/**
 * @private
 * @return {boolean}
 */
function isEmbeddedPagination() {
	return this?.tagName?.toLowerCase?.() === "monster-embedded-pagination";
}

/**
 * @private
 * @return {boolean}
 */
function isSelectPagination() {
	const root = this.getRootNode?.();
	const host = root?.host;
	if (host?.tagName?.toLowerCase?.() === "monster-select") {
		return true;
	}
	return (
		host?.constructor?.[instanceSymbol] ===
		Symbol.for("@schukai/monster/components/form/select@@instance")
	);
}

/**
 * @private
 * @return {boolean}
 */
function isAdaptivePagination() {
	return isEmbeddedPagination.call(this) || isSelectPagination.call(this);
}

/**
 * @private
 * @param {HTMLElement|null} parentNode
 * @return {number}
 */
function getSelectAvailableWidth(parentNode) {
	if (!parentNode) {
		return this.getBoundingClientRect().width;
	}

	const rect = parentNode.getBoundingClientRect();
	const styles = getComputedStyle(parentNode);
	const paddingLeft = parseFloat(styles.paddingLeft || "0");
	const paddingRight = parseFloat(styles.paddingRight || "0");
	const parentWidth = Math.max(0, rect.width - paddingLeft - paddingRight);

	const root = this.getRootNode?.();
	const host = root?.host;
	const hostWidth = host?.getBoundingClientRect?.().width || 0;

	if (hostWidth > 0) {
		if (parentWidth > 0) {
			return Math.min(parentWidth, hostWidth);
		}
		return hostWidth;
	}

	return parentWidth || this.getBoundingClientRect().width;
}

/**
 * @private
 * @param {HTMLElement|null} parentNode
 * @return {number}
 */
function getEmbeddedAvailableWidth(parentNode) {
	if (!parentNode) {
		return this.getBoundingClientRect().width;
	}

	const rect = parentNode.getBoundingClientRect();
	if (!rect.width) {
		return this.getBoundingClientRect().width;
	}

	const styles = getComputedStyle(parentNode);
	const gapValue = styles.columnGap || styles.gap || "0";
	const gap = parseFloat(gapValue) || 0;

	const siblings = Array.from(parentNode.children).filter((el) => el !== this);
	let siblingsWidth = 0;
	for (const sibling of siblings) {
		const siblingStyles = getComputedStyle(sibling);
		if (siblingStyles.display === "none") {
			continue;
		}
		siblingsWidth += sibling.getBoundingClientRect().width;
	}

	const gaps = siblings.length > 0 ? siblings.length : 0;
	return Math.max(0, rect.width - siblingsWidth - gap * gaps);
}

/**
 * @private
 * @param {HTMLElement} list
 * @param {HTMLElement|null} prevLink
 * @param {HTMLElement|null} nextLink
 * @param {"full"|"no-numbers"|"compact-summary"|"compact"} mode
 * @return {number}
 */
function applyPaginationMode(list, prevLink, nextLink, mode) {
	switch (mode) {
		case "compact":
			setClassEnabled(list, "pagination-numbers-hidden", true);
			setClassEnabled(list, "pagination-compact", true);
			setNumberItemsVisible(list, false);
			setSummaryVisible(list, true);
			setCompactLabels.call(this, true, prevLink, nextLink);
			break;
		case "compact-summary":
			setClassEnabled(list, "pagination-numbers-hidden", true);
			setClassEnabled(list, "pagination-compact", true);
			setNumberItemsVisible(list, false);
			setSummaryVisible(list, true);
			setCompactLabels.call(this, true, prevLink, nextLink);
			break;
		case "no-numbers":
			setClassEnabled(list, "pagination-numbers-hidden", true);
			setClassEnabled(list, "pagination-compact", false);
			setNumberItemsVisible(list, false);
			setSummaryVisible(list, true);
			setCompactLabels.call(this, false, prevLink, nextLink);
			break;
		default:
			setClassEnabled(list, "pagination-numbers-hidden", false);
			setClassEnabled(list, "pagination-compact", false);
			setNumberItemsVisible(list, true);
			setSummaryVisible(list, false);
			setCompactLabels.call(this, false, prevLink, nextLink);
			break;
	}

	return list.scrollWidth;
}

/**
 * @private
 * @param {HTMLElement} list
 * @param {HTMLElement|null} prevLink
 * @param {HTMLElement|null} nextLink
 * @param {string} signature
 * @return {object}
 */
function measurePaginationModeWidths(list, prevLink, nextLink, signature) {
	const cache = getPaginationLayoutCache.call(this);
	if (cache.widthSignature === signature && cache.widths) {
		return cache.widths;
	}

	const widths = {
		widthFull: applyPaginationMode.call(this, list, prevLink, nextLink, "full"),
		widthNoNumbers: applyPaginationMode.call(
			this,
			list,
			prevLink,
			nextLink,
			"no-numbers",
		),
		widthCompactSummary: applyPaginationMode.call(
			this,
			list,
			prevLink,
			nextLink,
			"compact-summary",
		),
		widthCompact: applyPaginationMode.call(
			this,
			list,
			prevLink,
			nextLink,
			"compact",
		),
	};

	cache.widthSignature = signature;
	cache.widths = widths;
	return widths;
}

/**
 * @private
 * @return {object}
 */
function getPaginationLayoutCache() {
	if (!this[layoutMeasurementCacheSymbol]) {
		this[layoutMeasurementCacheSymbol] = {
			lastAppliedMode: null,
			lastAppliedSignature: null,
			lastAvailableWidth: null,
			widthSignature: null,
			widths: null,
		};
	}

	return this[layoutMeasurementCacheSymbol];
}

/**
 * @private
 * @param {HTMLElement} list
 * @return {string}
 */
function getPaginationLayoutSignature(list) {
	const pagination = this.getOption("pagination", {});
	const itemSignature = Array.isArray(pagination.items)
		? pagination.items
				.map((item) => {
					return [
						item?.class || "",
						item?.href || "",
						item?.label || "",
						item?.no ?? "",
					].join(":");
				})
				.join("|")
		: "";

	return [
		pagination.current ?? "",
		pagination.pages ?? "",
		pagination.summary ?? "",
		pagination.prevClass ?? "",
		pagination.prevHref ?? "",
		pagination.prevNo ?? "",
		pagination.nextClass ?? "",
		pagination.nextHref ?? "",
		pagination.nextNo ?? "",
		itemSignature,
		list.children.length,
	].join("::");
}

/**
 * @private
 * @param {HTMLElement|null} element
 * @param {string} className
 * @param {boolean} enabled
 * @return {void}
 */
function setClassEnabled(element, className, enabled) {
	if (!(element instanceof HTMLElement)) {
		return;
	}
	if (element.classList.contains(className) !== enabled) {
		element.classList.toggle(className, enabled);
	}
}

/**
 * @private
 * @param {HTMLElement|null} element
 * @param {string} name
 * @param {string} value
 * @return {void}
 */
function setAttributeValue(element, name, value) {
	if (!(element instanceof HTMLElement)) {
		return;
	}
	if (element.getAttribute(name) !== value) {
		element.setAttribute(name, value);
	}
}

/**
 * @private
 * @param {HTMLElement|null} 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|null} element
 * @param {string} value
 * @return {void}
 */
function setInnerHTML(element, value) {
	if (!(element instanceof HTMLElement)) {
		return;
	}
	if (element.innerHTML !== value) {
		element.innerHTML = value;
	}
}

/**
 * @private
 * @param {object} params
 * @return {"full"|"no-numbers"|"compact-summary"|"compact"}
 */
function choosePaginationMode({
	availableWidth,
	widthFull,
	widthNoNumbers,
	widthCompactSummary,
	widthCompact,
}) {
	const hysteresisUp = 24;
	const hysteresisDown = 8;
	const currentMode = this[layoutModeSymbol] || "full";

	if (currentMode === "full") {
		if (widthFull > availableWidth - hysteresisDown) {
			if (widthNoNumbers <= availableWidth - hysteresisDown) {
				this[layoutModeSymbol] = "no-numbers";
				return "no-numbers";
			}
			if (widthCompactSummary <= availableWidth - hysteresisDown) {
				this[layoutModeSymbol] = "compact-summary";
				return "compact-summary";
			}
			this[layoutModeSymbol] = "compact";
			return "compact";
		}
		this[layoutModeSymbol] = "full";
		return "full";
	}

	if (currentMode === "no-numbers") {
		if (widthNoNumbers > availableWidth - hysteresisDown) {
			if (widthCompactSummary <= availableWidth - hysteresisDown) {
				this[layoutModeSymbol] = "compact-summary";
				return "compact-summary";
			}
			this[layoutModeSymbol] = "compact";
			return "compact";
		}

		if (widthFull + hysteresisUp <= availableWidth) {
			this[layoutModeSymbol] = "full";
			return "full";
		}

		this[layoutModeSymbol] = "no-numbers";
		return "no-numbers";
	}

	if (currentMode === "compact-summary") {
		if (widthNoNumbers + hysteresisUp <= availableWidth) {
			this[layoutModeSymbol] = "no-numbers";
			return "no-numbers";
		}

		if (widthCompactSummary > availableWidth - hysteresisDown) {
			this[layoutModeSymbol] = "compact";
			return "compact";
		}

		this[layoutModeSymbol] = "compact-summary";
		return "compact-summary";
	}

	if (widthCompactSummary + hysteresisUp <= availableWidth) {
		this[layoutModeSymbol] = "compact-summary";
		return "compact-summary";
	}

	this[layoutModeSymbol] = "compact";
	return "compact";
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `

        <template id="items">
            <li part="item"><a part="link"
                data-monster-attributes="class path:items.class,
                                            href path:items.href,
                                            aria-label path:items.description,
                                            disabled path:items.disabled:?disabled:undefined,
                                            data-page-no path:items.pageNo,
                                            aria-current path:items.current"
                   data-monster-role="pagination-item"
                   data-monster-replace="path:items.label"></a></li>
        </template>

        <div data-monster-role="control" part="control" data-monster-select-this="true">
            <nav data-monster-role="pagination" role="navigation" aria-label="pagination" part="nav">
                <ul class="pagination-list" data-monster-insert="items path:pagination.items"
                    data-monster-adaptive-ready="false"
                    data-monster-select-this="true" part="list">
                    <li part="prev" data-monster-role="pagination-prev"><a
                            data-monster-role="pagination-prev"
                            part="prev-link"
                            data-monster-attributes="
            class path:pagination.prevClass | prefix: previous,
            data-page-no path:pagination.prevNo,
            href path:pagination.prevHref"
                            data-monster-replace="path:labels.previous">Previous</a></li>
                    <li part="summary" data-monster-role="pagination-summary">
                        <span data-monster-replace="path:pagination.summary"></span>
                    </li>
                    <li part="next" data-monster-role="pagination-next"><a
                            data-monster-role="pagination-next"
                            part="next-link"
                            data-monster-attributes="class path:pagination.nextClass | prefix: next,
            data-page-no path:pagination.nextNo,
        href path:pagination.nextHref"
                            data-monster-replace="path:labels.next">Next</a></li>
                </ul>
            </nav>
        </div>
    `;
}

registerCustomElement(Pagination);
