/**
 * 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 { Datasource } from "./datasource.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
	getSlottedElements,
	updaterTransformerMethodsSymbol,
} from "../../dom/customelement.mjs";
import {
	findTargetElementFromEvent,
	fireCustomEvent,
} from "../../dom/events.mjs";
import { clone } from "../../util/clone.mjs";
import {
	isString,
	isFunction,
	isInstance,
	isObject,
	isArray,
} from "../../types/is.mjs";
import {
	validateArray,
	validateInteger,
	validateObject,
} from "../../types/validate.mjs";
import { Observer } from "../../types/observer.mjs";
import {
	ATTRIBUTE_DATATABLE_HEAD,
	ATTRIBUTE_DATATABLE_GRID_TEMPLATE,
	ATTRIBUTE_DATASOURCE_SELECTOR,
	ATTRIBUTE_DATATABLE_ALIGN,
	ATTRIBUTE_DATATABLE_SORTABLE,
	ATTRIBUTE_DATATABLE_MODE,
	ATTRIBUTE_DATATABLE_INDEX,
	ATTRIBUTE_DATATABLE_MODE_HIDDEN,
	ATTRIBUTE_DATATABLE_FEATURES,
	ATTRIBUTE_DATATABLE_MODE_VISIBLE,
	ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
	ATTRIBUTE_DATATABLE_MODE_FIXED,
} from "./constants.mjs";
import { instanceSymbol } from "../../constants.mjs";
import {
	Header,
	createOrderStatement,
	DIRECTION_ASC,
	DIRECTION_DESC,
	DIRECTION_NONE,
} from "./datatable/header.mjs";
import { DatatableStyleSheet } from "./stylesheet/datatable.mjs";
import {
	handleDataSourceChanges,
	datasourceLinkedElementSymbol,
} from "./util.mjs";
import "./columnbar.mjs";
import "./filter-button.mjs";
import {
	findElementWithSelectorUpwards,
	getWindow,
	waitForCustomElement,
} from "../../dom/util.mjs";

import { getDocumentTranslations } from "../../i18n/translations.mjs";
import "../state/state.mjs";
import "../host/collapse.mjs";
import { generateComponentConfigKey } from "../host/util.mjs";

import "./datasource/dom.mjs";
import "./datasource/rest.mjs";

import "../form/context-help.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";

export { DataTable };

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

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

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

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

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

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

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

/**
 * A DataTable
 *
 * @fragments /fragments/components/datatable/datatable/
 *
 * @example /examples/components/datatable/empty The empty state
 * @example /examples/components/datatable/data-using-javascript The data using javascript
 * @example /examples/components/datatable/alignment The alignment
 * @example /examples/components/datatable/row-mode The row mode
 * @example /examples/components/datatable/grid-template The grid template
 * @example /examples/components/datatable/overview-class The overview class
 * @example /examples/components/datatable/datasource Use a datasource
 * @example /examples/components/datatable/pagination Use pagination
 * @example /examples/components/datatable/filter Filer the data
 * @example /examples/components/datatable/order-by Sort data
 * @example /examples/components/datatable/select-rows Select rows
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/277.html
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/289.html
 *
 * @copyright Volker Schukai
 * @summary A configurable data table for datasets, datasources, filtering, sorting, pagination and row selection workflows.
 * @fires monster-datatable-row-copied
 * @fires monster-datatable-copy-error
 * @fires monster-datatable-row-removed
 * @fires monster-datatable-row-added
 * @fires monster-datatable-row-selected
 * @fires monster-datatable-row-deselected
 * @fires monster-datatable-all-rows-selected
 * @fires monster-datatable-all-rows-deselected
 * @fires monster-datatable-selection-changed
 **/
class DataTable extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/datatable@@instance");
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} datasource Datasource configuration
	 * @property {string} datasource.selector Selector for the datasource
	 * @property {string} datasource.orderDelimiter Order delimiter
	 * @property {Object} mapping Mapping configuration
	 * @property {string} mapping.data Data mapping
	 * @property {Array} data Data
	 * @property {Array} headers Headers
	 * @property {Object} responsive Responsive configuration
	 * @property {number} responsive.breakpoint Breakpoint for responsive mode
	 * @property {Object} labels Labels
	 * @property {string} labels.theListContainsNoEntries Label for empty state
	 * @property {Object} classes Classes
	 * @property {string} classes.container Container class
	 * @property {Object} features Features
	 * @property {boolean} features.settings Settings feature
	 * @property {boolean} features.footer Footer feature
	 * @property {boolean} features.autoInit Auto init feature (init datasource automatically)
	 * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature
	 * @property {boolean} features.copyAll Copy all feature
	 * @property {boolean} features.help Help feature
	 * @property {boolean} features.resizeable Columns are resizable
	 * @property {Object} copy Copy configuration
	 * @property {string} copy.delimiter Delimiter character
	 * @property {string} copy.quoteOpen Quote open character
	 * @property {string} copy.quoteClose Quote close character
	 * @property {string} copy.rowBreak Row break character
	 * @property {Object} updater Updater configuration
	 * @property {boolean} updater.batchUpdates Enables batched updater content/attribute updates
	 * @property {Object} templateMapping Template mapping
	 * @property {string} templateMapping.row-key Row key
	 * @property {string} templateMapping.filter-id Filter id
	 **/
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				templates: {
					main: getTemplate(),
					emptyState: getEmptyTemplate(),
				},

				datasource: {
					selector: null,
					orderDelimiter: ",", // see initOptionsFromArguments()
				},

				mapping: {
					data: "dataset",
				},

				data: [],
				headers: [],

				responsive: {
					breakpoint: 1440,
				},

				labels: getTranslations(),

				classes: {
					control: "monster-theme-control-container-1",
					container: "",
					row: "monster-theme-control-row-1",
				},

				features: {
					settings: true,
					footer: true,
					autoInit: true,
					doubleClickCopyToClipboard: true,
					copyAll: true,
					help: true,
					resizable: true,
				},

				copy: {
					delimiter: ";",
					quoteOpen: '"',
					quoteClose: '"',
					rowBreak: "\n",
				},

				updater: {
					batchUpdates: true,
				},

				templateMapping: {
					"row-key": null,
					"filter-id": null,
				},

				config: {
					instanceKey: null,
				},
			},
			initOptionsFromArguments.call(this),
		);
	}

	/**
	 *
	 * @param {string} selector
	 * @return {NodeList}
	 */
	getGridElements(selector) {
		return this[gridElementSymbol].querySelectorAll(selector);
	}

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

	/**
	 * @return {void}
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
		if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
			this[resizeObserverSymbol].disconnect();
		}
		const resizeFrameRequest = this?.[resizeFrameRequestSymbol];
		if (typeof resizeFrameRequest === "number") {
			getWindow().cancelAnimationFrame?.(resizeFrameRequest);
		}
		this[resizeFrameRequestSymbol] = null;
	}

	/**
	 * @return {void}
	 */
	connectedCallback() {
		const self = this;
		super.connectedCallback();

		this[resizeObserverSymbol] = new ResizeObserver((entries) => {
			scheduleGridUpdate.call(self);
		});

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

			if (parent instanceof HTMLElement) {
				this[resizeObserverSymbol].observe(parent);
			}
		});
	}

	/**
	 * Get the row number of the selected rows as an array
	 *
	 * @returns {number[]}
	 */
	getSelectedRows() {
		const rows = this.getGridElements(`[data-monster-role="select-row"]`);
		const selectedRows = [];
		rows.forEach((row) => {
			if (row.checked) {
				const key = row.parentNode.getAttribute(
					"data-monster-insert-reference",
				);
				const index = key.split("-").pop();
				selectedRows.push(parseInt(index, 10));
			}
		});

		return selectedRows;
	}

	/**
	 * @return void
	 */
	[assembleMethodSymbol]() {
		const self = this;
		const rawKey = this.getOption("templateMapping.row-key");

		if (rawKey === null) {
			if (this.id !== null && this.id !== "") {
				const rawKey = this.getOption("templateMapping.row-key");
				if (rawKey === null) {
					this.setOption("templateMapping.row-key", this.id + "-row");
				}
			} else {
				this.setOption("templateMapping.row-key", "row");
			}
		}

		if (this.id !== null && this.id !== "") {
			this.setOption("templateMapping.filter-id", "" + this.id + "-filter");
		} else {
			this.setOption("templateMapping.filter-id", "filter");
		}

		super[assembleMethodSymbol]();

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

		getSlottedElements
			.call(this, "[data-monster-role=row-action-button]", "bar")
			.forEach((i, e) => {
				if (e instanceof HTMLElement) {
					e.style.visibility = "hidden";
					e.style.width = "max-content";

					const pN = e.parentNode;
					if (pN instanceof HTMLElement) {
						pN.style.flexGrow = "10";
						pN.style.display = "flex";
						pN.style.justifyContent = "flex-start";
					}
				}
			});

		getHostConfig
			.call(this, getColumnVisibilityConfigKey)
			.then((config) => {
				const headerOrderMap = new Map();

				return getHostConfig
					.call(this, getStoredOrderConfigKey)
					.then((orderConfig) => {
						if (isArray(orderConfig) || orderConfig.length > 0) {
							for (let i = 0; i < orderConfig.length; i++) {
								const item = orderConfig[i];
								const parts = item.split(" ");
								const field = parts[0];
								const direction = parts[1] || DIRECTION_ASC;
								headerOrderMap.set(field, direction);
							}
						}
					})
					.then(() => {
						try {
							initGridAndStructs.call(this, config, headerOrderMap);
						} catch (error) {
							addErrorAttribute(this, error);
						}

						updateColumnBar.call(this);
					});
			})
			.catch((error) => {
				addErrorAttribute(this, error);
			})
			.finally(() => {
				const selector = this.getOption("datasource.selector");

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

					waitForCustomElement(element, {
						readyCheck: (el) => isInstance(el, Datasource),
					})
						.then((readyElement) => {
							this[datasourceLinkedElementSymbol] = readyElement;

							queueMicrotask(() => {
								handleDataSourceChanges.call(this, true);
								if (readyElement && "datasource" in readyElement) {
									readyElement.datasource.attachObserver(
										new Observer(handleDataSourceChanges.bind(this)),
									);
								}
							});
						})
						.catch((error) => {
							addErrorAttribute(this, error);
						});
				}
			});

		this.attachObserver(
			new Observer((a) => {
				const selectAll = this[gridHeadersElementSymbol].querySelector(
					`[data-monster-role="select-all"]`,
				);
				if (selectAll instanceof HTMLInputElement) {
					selectAll.checked = false;
				}

				getSlottedElements
					.call(this, "[data-monster-role=row-action-button]", "bar")
					.forEach((i, e) => {
						const selected = self.getSelectedRows();
						if (e instanceof HTMLElement) {
							e.style.visibility = "hidden";
						}
					});

				const rows = this.getGridElements(`[data-monster-role="select-row"]`);
				rows.forEach((row) => {
					row.checked = false;
				});
			}),
		);
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [DatatableStyleSheet];
	}

	/**
	 * Copy a row from the datatable
	 *
	 * @param {number|string} fromIndex
	 * @param {number|string} toIndex
	 * @return {DataTable}
	 * @fires monster-datatable-row-copied
	 */
	copyRow(fromIndex, toIndex) {
		const datasource = this[datasourceLinkedElementSymbol];
		if (!datasource) {
			return this;
		}
		let d = datasource.data;
		let c = clone(d);

		let rows = c;
		const mapping = this.getOption("mapping.data");

		if (mapping) {
			rows = c?.[mapping];
		}

		if (rows === undefined || rows === null) {
			rows = [];
		}

		if (toIndex === undefined) {
			toIndex = rows.length;
		}

		if (isString(fromIndex)) {
			fromIndex = parseInt(fromIndex);
		}
		if (isString(toIndex)) {
			toIndex = parseInt(toIndex);
		}

		if (toIndex < 0 || toIndex > rows.length) {
			throw new RangeError("index out of bounds");
		}

		validateArray(rows);
		validateInteger(fromIndex);
		validateInteger(toIndex);

		if (fromIndex < 0 || fromIndex >= rows.length) {
			throw new RangeError("index out of bounds");
		}

		rows.splice(toIndex, 0, clone(rows[fromIndex]));
		datasource.data = c;

		fireCustomEvent(this, "monster-datatable-row-copied", {
			index: toIndex,
		});

		return this;
	}

	/**
	 * Remove a row from the datatable
	 *
	 * @param {number|string} index
	 * @return {DataTable}
	 * @fires monster-datatable-row-removed
	 */
	removeRow(index) {
		const datasource = this[datasourceLinkedElementSymbol];
		if (!datasource) {
			return this;
		}
		let d = datasource.data;
		let c = clone(d);

		let rows = c;
		const mapping = this.getOption("mapping.data");

		if (mapping) {
			rows = c?.[mapping];
		}

		if (rows === undefined || rows === null) {
			rows = [];
		}

		if (isString(index)) {
			index = parseInt(index);
		}

		validateArray(rows);
		validateInteger(index);

		if (index < 0 || index >= rows.length) {
			throw new RangeError("index out of bounds");
		}
		if (mapping) {
			rows = c?.[mapping];
		}

		rows.splice(index, 1);
		datasource.data = c;

		fireCustomEvent(this, "monster-datatable-row-removed", {
			index: index,
		});

		return this;
	}

	/**
	 * Add a row to the datatable
	 *
	 * @param {Object} data
	 * @return {DataTable}
	 *
	 * @fires monster-datatable-row-added
	 **/
	addRow(data) {
		const datasource = this[datasourceLinkedElementSymbol];
		if (!datasource) {
			return this;
		}
		let d = datasource.data;
		let c = clone(d);

		let rows = c;

		const mapping = this.getOption("mapping.data");
		if (mapping) {
			rows = c?.[mapping];
		}

		if (rows === undefined || rows === null) {
			rows = [];
		}

		validateArray(rows);
		validateObject(data);

		rows.push(data);
		datasource.data = c;

		fireCustomEvent(this, "monster-datatable-row-added", {
			index: rows.length - 1,
		});

		return this;
	}
}

/**
 * @private
 * @return {string}
 */
function getColumnVisibilityConfigKey() {
	return generateComponentConfigKey("datatable", this?.id, "columns-visibility", {
		instanceKey: this.getOption("config.instanceKey"),
	});
}

/**
 * @private
 * @return {string}
 */
function hasConfigIdentity() {
	return (
		(isString(this.id) && this.id !== "") ||
		(isString(this.getOption("config.instanceKey")) &&
			this.getOption("config.instanceKey").trim() !== "")
	);
}

/**
 * @private
 * @return {Promise}
 */
function getHostConfig(callback) {
	const host = findElementWithSelectorUpwards(this, "monster-host");

	if (!host) {
		addErrorAttribute(this, "no host found");
		return Promise.resolve({});
	}

	if (!hasConfigIdentity.call(this)) {
		addErrorAttribute(
			this,
			"no id or config.instanceKey found; one is required for config",
		);
		return Promise.resolve({});
	}

	if (!host || !isFunction(host?.getConfig)) {
		throw new TypeError("the host must be a monster-host");
	}

	const configKey = callback.call(this);
	return host.hasConfig(configKey).then((hasConfig) => {
		if (hasConfig) {
			return host.getConfig(configKey);
		} else {
			return {};
		}
	});
}

/**
 * @private
 */
function updateColumnBar() {
	if (!this[columnBarElementSymbol]) {
		return;
	}

	const columns = [];
	for (const header of this.getOption("headers")) {
		const mode = header.getInternal("mode");

		if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
			continue;
		}

		columns.push({
			visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
			name: header.label,
			index: header.index,
		});
	}

	this[suppressColumnConfigSaveSymbol] = true;
	this[columnBarElementSymbol].setOption("columns", columns);
	queueMicrotask(() => {
		this[suppressColumnConfigSaveSymbol] = false;
	});
}

/**
 * @private
 */
function updateHeaderFromColumnBar() {
	if (!this[columnBarElementSymbol]) {
		return;
	}

	const options = this[columnBarElementSymbol].getOption("columns");
	if (!isArray(options)) return;

	const invisibleMap = {};

	for (let i = 0; i < options.length; i++) {
		const option = options[i];
		invisibleMap[option.index] = option.visible;
	}

	for (const header of this.getOption("headers")) {
		const mode = header.getInternal("mode");

		if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
			continue;
		}

		if (invisibleMap[header.index] === false) {
			header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN);
		} else {
			header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE);
		}
	}
}

/**
 * @private
 */
function updateConfigColumnBar() {
	if (!this[columnBarElementSymbol]) {
		return;
	}

	const options = this[columnBarElementSymbol].getOption("columns");
	if (!isArray(options)) return;

	const map = {};
	for (let i = 0; i < options.length; i++) {
		const option = options[i];
		map[option.name] = option.visible;
	}

	const host = findElementWithSelectorUpwards(this, "monster-host");
	if (!(host && hasConfigIdentity.call(this))) {
		return;
	}
	const configKey = getColumnVisibilityConfigKey.call(this);

	try {
		host.setConfig(configKey, map);
	} catch (error) {
		addErrorAttribute(this, error);
	}
}

/**
 * @private
 * @param {HTMLElement} cell
 * @return {string}
 */
function getCellValueForCopy(cell) {
	if (cell.hasAttribute("data-monster-raw-value")) {
		return cell.getAttribute("data-monster-raw-value");
	}
	return cell.textContent.trim();
}

/**
 * @private
 */
function initEventHandler() {
	const self = this;
	const tableScrollElement = this[tableScrollElementSymbol];

	// --- Column resizing state ---
	let isResizing = false;
	let resizingColumnIndex = -1;
	let startX = 0;
	let startWidth = 0;

	// --- Table drag scrolling state ---
	let isTableDragging = false;
	let tableDragMoved = false;
	let tableDragPointerId = null;
	let tableDragStartX = 0;
	let tableDragStartY = 0;
	let tableDragStartScrollLeft = 0;
	let suppressClickUntil = 0;

	const isInteractiveDragTarget = (event) => {
		if (
			findTargetElementFromEvent(
				event,
				"data-monster-role",
				"datatable-headers",
			)
		) {
			return true;
		}

		if (
			findTargetElementFromEvent(event, "data-monster-role", "resize-handle")
		) {
			return true;
		}

		const path = event.composedPath?.();
		if (!isArray(path)) {
			return false;
		}

		for (const node of path) {
			if (!(node instanceof HTMLElement)) {
				continue;
			}

			if (node === tableScrollElement) {
				return false;
			}

			if (
				node.matches(
					[
						"button",
						"a",
						"input",
						"select",
						"textarea",
						"label",
						"summary",
						"[contenteditable]",
						"[contenteditable='true']",
						"[draggable='true']",
						"[role='button']",
						"[role='link']",
						"[role='textbox']",
						"[role='checkbox']",
						"[role='switch']",
						"[role='menuitem']",
					].join(","),
				)
			) {
				return true;
			}
		}

		return false;
	};

	const stopTableDragging = () => {
		if (tableScrollElement instanceof HTMLElement) {
			tableScrollElement.classList.remove("is-dragging");
		}

		getWindow().removeEventListener("pointermove", onTablePointerMove);
		getWindow().removeEventListener("pointerup", onTablePointerUp);
		getWindow().removeEventListener("pointercancel", onTablePointerUp);

		if (tableDragPointerId !== null) {
			tableScrollElement?.releasePointerCapture?.(tableDragPointerId);
		}

		isTableDragging = false;
		tableDragMoved = false;
		tableDragPointerId = null;
	};

	const onTablePointerDown = (event) => {
		if (!(tableScrollElement instanceof HTMLElement)) {
			return;
		}

		if (
			(event.pointerType === "mouse" && event.button !== 0) ||
			event.isPrimary === false
		) {
			return;
		}

		if (tableScrollElement.scrollWidth <= tableScrollElement.clientWidth) {
			return;
		}

		if (isInteractiveDragTarget(event)) {
			return;
		}

		isTableDragging = true;
		tableDragMoved = false;
		tableDragPointerId = event.pointerId;
		tableDragStartX = event.clientX;
		tableDragStartY = event.clientY;
		tableDragStartScrollLeft = tableScrollElement.scrollLeft;

		tableScrollElement.setPointerCapture?.(event.pointerId);
		getWindow().addEventListener("pointermove", onTablePointerMove);
		getWindow().addEventListener("pointerup", onTablePointerUp);
		getWindow().addEventListener("pointercancel", onTablePointerUp);
	};

	const onTablePointerMove = (event) => {
		if (
			!isTableDragging ||
			tableDragPointerId === null ||
			event.pointerId !== tableDragPointerId ||
			!(tableScrollElement instanceof HTMLElement)
		) {
			return;
		}

		const deltaX = event.clientX - tableDragStartX;
		const deltaY = event.clientY - tableDragStartY;

		if (!tableDragMoved) {
			if (Math.abs(deltaX) < 8 || Math.abs(deltaX) <= Math.abs(deltaY)) {
				return;
			}

			tableDragMoved = true;
			tableScrollElement.classList.add("is-dragging");
		}

		event.preventDefault();
		tableScrollElement.scrollLeft = tableDragStartScrollLeft - deltaX;
	};

	const onTablePointerUp = (event) => {
		if (tableDragPointerId === null || event.pointerId !== tableDragPointerId) {
			return;
		}

		if (tableDragMoved) {
			suppressClickUntil = Date.now() + 250;
		}

		stopTableDragging();
	};

	const onTableClickCapture = (event) => {
		if (suppressClickUntil < Date.now()) {
			return;
		}

		event.preventDefault();
		event.stopPropagation();
	};

	tableScrollElement?.addEventListener("pointerdown", onTablePointerDown);
	tableScrollElement?.addEventListener("click", onTableClickCapture, true);

	// --- Pointer-based resize handlers (robust for mouse/touch/stylus) ---
	const onPointerDown = (event) => {
		// Important: Call helper with (attrName, value) – not with CSS selector
		const resizeHandle = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"resize-handle",
		);
		if (!resizeHandle) return;

		event.preventDefault();
		isResizing = true;

		const headerCell = resizeHandle.closest("[data-monster-index]");
		resizingColumnIndex = parseInt(
			headerCell.getAttribute("data-monster-index"),
			10,
		);
		startX = event.clientX;
		startWidth = headerCell.offsetWidth;

		// Capture to prevent losing events during fast dragging
		event.target.setPointerCapture?.(event.pointerId);

		getWindow().addEventListener("pointermove", onPointerMove);
		getWindow().addEventListener("pointerup", onPointerUp);

		// Global cursor styling during resizing
		self.shadowRoot.host.classList.add("is-resizing");
	};

	const onPointerMove = (event) => {
		if (!isResizing) return;

		const deltaX = event.clientX - startX;
		let newWidth = startWidth + deltaX;

		const headers = self.getOption("headers");
		const resizingHeader = headers[resizingColumnIndex];

		const minWidth = 50;

		const visibleHeaders = headers.filter(
			(h) => h.getInternal("mode") !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
		);
		const numOtherVisibleColumns =
			visibleHeaders.length > 1 ? visibleHeaders.length - 1 : 0;

		const reservedSpaceForOthers = numOtherVisibleColumns * 50;
		const tableContainerWidth = self[gridElementSymbol].clientWidth;
		const maxWidth = tableContainerWidth - reservedSpaceForOthers;

		newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));

		resizingHeader.setInternal("grid", `${newWidth}px`);

		updateGrid.call(self);
	};

	const onPointerUp = () => {
		if (!isResizing) return;

		isResizing = false;
		getWindow().removeEventListener("pointermove", onPointerMove);
		getWindow().removeEventListener("pointerup", onPointerUp);

		self.shadowRoot.host.classList.remove("is-resizing");
	};

	// Use pointer events instead of mouse: more resilient
	this[gridHeadersElementSymbol].addEventListener("pointerdown", onPointerDown);

	// --- Copy configuration ---
	const quoteOpenChar = this.getOption("copy.quoteOpen");
	const quoteCloseChar = this.getOption("copy.quoteClose");
	const delimiterChar = this.getOption("copy.delimiter");
	const rowBreak = this.getOption("copy.rowBreak");

	const writeTextToClipboard = (text) => {
		if (!text) {
			return;
		}

		const clipboard = getWindow().navigator.clipboard;
		if (!clipboard?.writeText) {
			const error = new Error("Clipboard API is not available.");
			addErrorAttribute(self, error);
			fireCustomEvent(self, "monster-datatable-copy-error", { error, text });
			return;
		}

		clipboard.writeText(text).catch((error) => {
			addErrorAttribute(self, error);
			fireCustomEvent(self, "monster-datatable-copy-error", { error, text });
		});
	};

	const getCopyTextForRow = (headCell) => {
		const index = headCell.getAttribute("data-monster-insert-reference");
		if (!index) {
			return "";
		}

		const cols = self.getGridElements(
			`[data-monster-insert-reference="${index}"]`,
		);
		const colTexts = [];

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

			if (
				col.querySelector("monster-button-bar") ||
				col.querySelector("monster-button")
			) {
				continue;
			}

			const cellValue = getCellValueForCopy(col);
			if (cellValue) {
				colTexts.push(quoteOpenChar + cellValue + quoteCloseChar);
			}
		}

		return colTexts.join(delimiterChar);
	};

	const getCopyTextForCell = (headCell) => {
		if (
			headCell.querySelector("monster-button-bar") ||
			headCell.querySelector("monster-button")
		) {
			return "";
		}

		return getCellValueForCopy(headCell);
	};

	// --- Column-Bar -> Header visibility & persistence ---
	if (self[columnBarElementSymbol]) {
		self[columnBarElementSymbol].attachObserver(
			new Observer(() => {
				updateHeaderFromColumnBar.call(self);
				updateGrid.call(self);
				if (!self[suppressColumnConfigSaveSymbol]) {
					updateConfigColumnBar.call(self);
				}
			}),
		);
	}

	// --- Sorting (click on header link) ---
	self[gridHeadersElementSymbol].addEventListener("click", function (event) {
		const datasource = self[datasourceLinkedElementSymbol];
		if (!datasource) return;

		const element = findTargetElementFromEvent(event, "data-monster-sortable");
		if (element instanceof HTMLElement) {
			const index = element.parentNode?.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
			const headers = self.getOption("headers");

			event.preventDefault();

			headers[index].changeDirection();

			queueMicrotask(function () {
				element.setAttribute(
					ATTRIBUTE_DATATABLE_SORTABLE,
					`${headers[index].field} ${headers[index].direction}`,
				);

				storeOrderStatement.call(self, true);
			});
		}
	});

	// --- Copy interactions ---
	const eventHandlerShiftClickCopyRowToClipboard = (event) => {
		const headCell = findTargetElementFromEvent(event, "data-monster-head");
		if (!headCell || !event.shiftKey) {
			return;
		}

		writeTextToClipboard(getCopyTextForRow(headCell));
	};

	const eventHandlerDoubleClickCopyToClipboard = (event) => {
		const headCell = findTargetElementFromEvent(event, "data-monster-head");
		if (!headCell || event.shiftKey) {
			return;
		}

		writeTextToClipboard(getCopyTextForCell(headCell));
	};

	if (self.getOption("features.doubleClickCopyToClipboard")) {
		self[gridElementSymbol].addEventListener(
			"click",
			eventHandlerShiftClickCopyRowToClipboard,
		);
		self[gridElementSymbol].addEventListener(
			"dblclick",
			eventHandlerDoubleClickCopyToClipboard,
		);
	}

	// --- Copy All ---
	if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) {
		this[copyAllElementSymbol].addEventListener("click", (event) => {
			event.preventDefault();

			const table = [];
			let currentRow = [];

			const cols = self.getGridElements(`[data-monster-insert-reference]`);
			const rowIndexes = new Map();
			cols.forEach((col) => {
				const index = col.getAttribute("data-monster-insert-reference");
				rowIndexes.set(index, true);
			});

			rowIndexes.forEach((_, key) => {
				const rowCols = self.getGridElements(
					`[data-monster-insert-reference="${key}"]`,
				);

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

					if (
						col.querySelector("monster-button-bar") ||
						col.querySelector("monster-button")
					) {
						continue;
					}

					const cellValue = getCellValueForCopy(col);
					if (cellValue) {
						currentRow.push(quoteOpenChar + cellValue + quoteCloseChar);
					}
				}

				if (currentRow.length > 0) {
					table.push(currentRow);
				}
				currentRow = [];
			});

			if (table.length > 0) {
				const text = table.map((row) => row.join(delimiterChar)).join(rowBreak);
				writeTextToClipboard(text);
			}
		});
	}

	// --- Row Selection (single) ---
	const selectRowCallback = (event) => {
		const checkbox = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"select-row",
		);

		if (!(checkbox instanceof HTMLInputElement)) return;

		const parentNode = checkbox.parentNode;
		if (!(parentNode instanceof HTMLDivElement)) return;

		const key = parentNode.getAttribute("data-monster-insert-reference");
		const row = self.getGridElements(
			`[data-monster-insert-reference="${key}"]`,
		);
		const index = key.split("-").pop();

		if (checkbox.checked) {
			row.forEach((col) => col.classList.add("selected"));
			fireCustomEvent(self, "monster-datatable-row-selected", { index });
		} else {
			row.forEach((col) => col.classList.remove("selected"));
			fireCustomEvent(self, "monster-datatable-row-deselected", { index });
		}

		fireCustomEvent(this, "monster-datatable-selection-changed", {});

		const rows = self.getGridElements(`[data-monster-role="select-row"]`);
		const allSelected = Array.from(rows).every((r) => r.checked);

		const selectAll = this[gridHeadersElementSymbol].querySelector(
			`[data-monster-role="select-all"]`,
		);

		// Row-Action-Buttons visible/invisible depending on selection
		getSlottedElements
			.call(this, "[data-monster-role=row-action-button]", "bar")
			.forEach((i, e) => {
				const selected = self.getSelectedRows();
				const mode = selected.length === 0 ? "hidden" : "visible";
				if (e instanceof HTMLElement) e.style.visibility = mode;
			});

		if (selectAll) selectAll.checked = allSelected;
	};

	this[gridElementSymbol].addEventListener("click", selectRowCallback);
	this[gridElementSymbol].addEventListener("touch", selectRowCallback);

	// --- Row Selection (all) ---
	const selectAllCallback = (event) => {
		const toggle = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"select-all",
		);
		if (!toggle) return;

		const mode = toggle.checked;
		const rows = this.getGridElements(`[data-monster-role="select-row"]`);
		rows.forEach((row) => {
			row.checked = mode;
		});

		if (mode) {
			fireCustomEvent(this, "monster-datatable-all-rows-selected", {});
		} else {
			fireCustomEvent(this, "monster-datatable-all-rows-deselected", {});
		}

		getSlottedElements
			.call(this, "[data-monster-role=row-action-button]", "bar")
			.forEach((i, e) => {
				if (e instanceof HTMLElement)
					e.style.visibility = mode ? "visible" : "hidden";
			});

		fireCustomEvent(this, "monster-datatable-selection-changed", {});
	};

	this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback);
	this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback);
}

/**
 * @private
 */
function initGridAndStructs(hostConfig, headerOrderMap) {
	const rowID = this.getOption("templateMapping.row-key");

	if (!this[gridElementSymbol]) {
		addErrorAttribute(this, "no grid element found");
		return;
	}

	let template;
	getSlottedElements.call(this).forEach((e) => {
		if (e instanceof HTMLTemplateElement && e.id === rowID) {
			template = e;
		}
	});

	if (!template) {
		addErrorAttribute(this, "no template found, please add a template");
		return;
	}

	const rowCount = template.content.children.length;

	const headers = [];

	for (let i = 0; i < rowCount; i++) {
		let hClass = "";
		const row = template.content.children[i];

		let mode = "";
		if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) {
			mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE);
		}

		let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE);
		if (!grid || grid === "" || grid === "auto") {
			grid = "minmax(0, 1fr)";
		}

		let label = "";
		let labelKey = "";

		if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) {
			label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD);
			labelKey = label;

			try {
				if (label.startsWith("i18n:")) {
					label = label.substring(5, label.length);
					label = getDocumentTranslations().getText(label, label);
				}
			} catch (e) {
				label = "i18n error " + label;
			}
		}

		if (!label) {
			label = i + 1 + "";
			mode = ATTRIBUTE_DATATABLE_MODE_FIXED;
			labelKey = label;
		}

		if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) {
			if (hostConfig[label] === false) {
				mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN;
			} else {
				mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE;
			}
		}

		let align = "";
		if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) {
			align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN);
		}

		switch (align) {
			case "center":
				hClass = "flex-center";
				break;
			case "end":
				hClass = "flex-end";
				break;
			case "start":
				hClass = "flex-start";
				break;
			default:
				hClass = "flex-start";
		}

		let field = "";
		let direction = DIRECTION_NONE;
		if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) {
			field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim();
			const parts = field.split(" ").map((item) => item.trim());
			field = parts[0];

			if (headerOrderMap.has(field)) {
				direction = headerOrderMap.get(field);
			} else if (
				parts.length === 2 &&
				[DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1
			) {
				direction = parts[1];
			}
		}

		if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
			hClass += " hidden";
		}

		const features = [];

		if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) {
			const fl = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" ");

			fl.forEach((feature) => {
				features.push(feature.trim());

				if (feature === "select") {
					label = "<input type='checkbox' data-monster-role='select-all' />";

					while (row.firstChild) {
						row.removeChild(row.firstChild);
					}

					const checkbox = document.createElement("input");
					checkbox.type = "checkbox";
					checkbox.setAttribute("data-monster-role", "select-row");
					row.appendChild(checkbox);
				}
			});
		}

		let orderTemplate = "${field} ${direction}";
		if (row.hasAttribute("data-monster-order-template")) {
			orderTemplate = row.getAttribute("data-monster-order-template");
		}

		const isResizable = this.getOption("features.resizable");
		const header = new Header();
		header.setInternals({
			field: field,
			label: label,
			classes: hClass,
			index: i,
			mode: mode,
			grid: grid,
			labelKey: labelKey + "-" + i,
			direction: direction,
			features: features,
			orderTemplate: orderTemplate,
			resizable: isResizable,
		});

		headers.push(header);
	}

	this.setOption("headers", headers);
	requestAnimationFrame(() => {
		storeOrderStatement.call(this, this.getOption("features.autoInit"));

		const headerElements = this[gridHeadersElementSymbol].querySelectorAll(
			"[data-monster-index]",
		);
		headerElements.forEach((cell) => {
			const index = parseInt(cell.getAttribute("data-monster-index"), 10);
			if (headers[index]) {
				headers[index].setInternal("initialWidth", cell.offsetWidth);
			}
		});
	});
}

/**
 * @private
 */

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				theListContainsNoEntries: "Die Liste enthält keine Einträge",
				copyAll: "Alles kopieren",
				helpText:
					"<p>Sie können die Werte aus einzelnen Zeilen<br>" +
					"in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" +
					"<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" +
					"Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>",
			};
		case "fr":
			return {
				theListContainsNoEntries: "La liste ne contient aucune entrée",
				copyAll: "Copier tout",
				helpText:
					"<p>Vous pouvez copier les valeurs des rangées individuelles<br>" +
					"dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" +
					"<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" +
					"Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>",
			};
		case "sp":
			return {
				theListContainsNoEntries: "La lista no contiene entradas",
				copyAll: "Copiar todo",
				helpText:
					"<p>Puedes copiar los valores de filas individuales<br>" +
					"al portapapeles haciendo doble clic en la columna correspondiente.</p>" +
					"<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" +
					"Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>",
			};
		case "it":
			return {
				theListContainsNoEntries: "L'elenco non contiene voci",
				copyAll: "Copia tutto",
				helpText:
					"<p>Puoi copiare i valori dalle singole righe<br>" +
					"negli appunti facendo doppio clic sulla colonna relativa.</p>" +
					"<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" +
					"Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>",
			};
		case "pl":
			return {
				theListContainsNoEntries: "Lista nie zawiera wpisów",
				copyAll: "Kopiuj wszystko",
				helpText:
					"<p>Możesz skopiować wartości z poszczególnych wierszy<br>" +
					"do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" +
					"<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" +
					"Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>",
			};
		case "no":
			return {
				theListContainsNoEntries: "Listen inneholder ingen oppføringer",
				copyAll: "Kopier alt",
				helpText:
					"<p>Du kan kopiere verdier fra enkeltrader<br>" +
					"til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" +
					"<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" +
					"Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>",
			};
		case "dk":
			return {
				theListContainsNoEntries: "Listen indeholder ingen poster",
				copyAll: "Kopiér alt",
				helpText:
					"<p>Du kan kopiere værdier fra enkelte rækker<br>" +
					"til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" +
					"<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" +
					"Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>",
			};
		case "sw":
			return {
				theListContainsNoEntries: "Listan innehåller inga poster",
				copyAll: "Kopiera allt",
				helpText:
					"<p>Du kan kopiera värden från enskilda rader<br>" +
					"till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" +
					"<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" +
					"Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>",
			};

		case "en":
		default:
			return {
				theListContainsNoEntries: "The list contains no entries",
				copyAll: "Copy all",
				helpText:
					"<p>You can copy the values from individual rows<br>" +
					"to the clipboard by double-clicking on the relevant column.</p>" +
					"<p>To copy an entire row, hold down the Shift key while clicking.<br>" +
					"If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>",
			};
	}
}

/**
 * @private
 * @return {string}
 */
export function getStoredOrderConfigKey() {
	return generateComponentConfigKey("datatable", this?.id, "stored-order", {
		instanceKey: this.getOption("config.instanceKey"),
	});
}

/**
 * @private
 */
function storeOrderStatement(doFetch) {
	const headers = this.getOption("headers");
	const delimiter = this.getOption("datasource.orderDelimiter");
	const statement = createOrderStatement(headers, delimiter);
	setDataSource.call(this, { order: statement }, doFetch);

	const host = findElementWithSelectorUpwards(this, "monster-host");
	if (!(host && hasConfigIdentity.call(this))) {
		return;
	}

	const configKey = getStoredOrderConfigKey.call(this);

	// statement explode with , and remove all empty
	const list = statement.split(",").filter((item) => item.trim() !== "");
	if (list.length === 0) {
		return;
	}

	host.setConfig(configKey, list);
}

/**
 * @private
 */
function updateGrid() {
	if (!this[gridElementSymbol]) {
		throw new Error("no grid element is defined");
	}

	let gridTemplateColumns = "";

	const headers = this.getOption("headers");

	let styles = "";

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

		if (
			header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN &&
			header.mode !== ATTRIBUTE_DATATABLE_MODE_FIXED
		) {
			styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`;
			styles += `[data-monster-role=datatable] > div:nth-child(${headers.length}n+${i + 1}) { display: none; }\n`;
		} else {
			gridTemplateColumns += `${header.grid} `;
		}
	}

	const sheet = new CSSStyleSheet();
	if (styles !== "") sheet.replaceSync(styles);
	this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet];

	const bodyWidth = this.parentNode.clientWidth;

	const breakpoint = this.getOption("responsive.breakpoint");
	this[dataControlElementSymbol].classList.toggle(
		"small",
		bodyWidth <= breakpoint,
	);
	this[columnBarElementSymbol]?.classList.toggle(
		"small",
		bodyWidth <= breakpoint,
	);

	if (bodyWidth > breakpoint) {
		this[gridElementSymbol].style.gridTemplateColumns =
			`${gridTemplateColumns}`;
		this[gridHeadersElementSymbol].style.gridTemplateColumns =
			`${gridTemplateColumns}`;
	} else {
		this[gridElementSymbol].style.gridTemplateColumns = "auto";
		this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto";
	}
}

/**
 * @private
 * @return {void}
 */
function scheduleGridUpdate() {
	if (
		this[resizeFrameRequestSymbol] !== null &&
		this[resizeFrameRequestSymbol] !== undefined
	) {
		return;
	}

	this[resizeFrameRequestSymbol] = getWindow().requestAnimationFrame(() => {
		this[resizeFrameRequestSymbol] = null;
		updateGrid.call(this);
	});
}

/**
 * @private
 * @param {Header[]} headers
 * @param {bool} doFetch
 */
function setDataSource({ order }, doFetch) {
	const datasource = this[datasourceLinkedElementSymbol];

	if (!datasource) {
		return;
	}

	if (isFunction(datasource?.setParameters)) {
		datasource.setParameters({ order });
	}

	if (doFetch !== false && isFunction(datasource?.fetch)) {
		datasource.fetch();
	}
}

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

	this[dataControlElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=control]",
	);

	this[gridElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=datatable]",
	);

	this[gridHeadersElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=datatable-headers]",
	);

	this[columnBarElementSymbol] =
		this.shadowRoot.querySelector("monster-column-bar");

	this[copyAllElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=copy-all]",
	);

	this[tableScrollElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=table-scroll]",
	);

	return this;
}

/**
 * @private
 * @return {object}
 * @throws {TypeError} incorrect arguments passed for the datasource
 * @throws {Error} the datasource could not be initialized
 */
function initOptionsFromArguments() {
	const options = { datasource: {} };
	const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);

	if (selector) {
		options.datasource = { selector: selector };
	}

	options.datasource.orderDelimiter = ","; // workaround for the missing orderDelimiter

	const breakpoint = this.getAttribute(
		ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
	);

	if (breakpoint) {
		options.responsive = {};
		options.responsive.breakpoint = parseInt(breakpoint);
	}

	return options;
}

/**
 * @private
 * @return {string}
 */
function getEmptyTemplate() {
	return `<monster-state data-monster-role="empty-without-action">
    <div part="visual">
        <svg width="4rem" height="4rem" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/>
            <path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/>
        </svg>
    </div>
    <div part="content" data-monster-replace="path:labels.theListContainsNoEntries">
        The list contains no entries.
    </div>
</monster-state>`;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control">
            <template id="headers-row">
                <div data-monster-attributes="class path:headers-row.classes,
                                              data-monster-index path:headers-row.index"
                     data-monster-replace="path:headers-row.html"></div>
            </template>
            <slot></slot>
            <div data-monster-attributes="class path:classes.container"
                 data-monster-role="table-container" part="table-container">
                <div class="filter">
                    <slot name="filter"></slot>
                </div>
                <div class="bar">
                    <monster-context-help
                            data-monster-attributes="class path:features.help | ?::hidden"
                            data-monster-replace="path:labels.helpText"
                    ></monster-context-help>
                    <a href="#" data-monster-attributes="class path:features.copyAll | ?::copyAllHidden"
                       data-monster-role="copy-all" data-monster-replace="path:labels.copyAll">Copy all</a>
                    <monster-column-bar
                            data-monster-attributes="class path:features.settings | ?::hidden"></monster-column-bar>
                    <slot name="bar"></slot>

                </div>
                <div data-monster-role="table-scroll">
                    <div data-monster-role="datatable-headers" data-monster-insert="headers-row path:headers"></div>
                    <div data-monster-replace="path:templates.emptyState"
                         data-monster-attributes="class path:data | has-entries | ?:hidden:empty-state-container"></div>
                    <div data-monster-role="datatable" data-monster-insert="\${row-key} path:data">
                    </div>
                </div>
            </div>
            <div data-monster-role="footer" data-monster-select-this="true"
                 data-monster-attributes="class path:data | has-entries | ?::hidden">
                <slot name="footer" data-monster-attributes="class path:features.footer | ?::hidden"></slot>
            </div>

        </div>
    `;
}

registerCustomElement(DataTable);
