/**
 * 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 { addAttributeToken } from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Observer } from "../../types/observer.mjs";
import { isArray, isObject, isString } from "../../types/is.mjs";
import { ID } from "../../types/id.mjs";
import { clone } from "../../util/clone.mjs";

import { SheetStyleSheet } from "./stylesheet/sheet.mjs";

export { Sheet };

const gridElementSymbol = Symbol("sheetGrid");
const gridWrapperSymbol = Symbol("sheetGridWrapper");
const spacerElementSymbol = Symbol("sheetGridSpacer");
const addRowButtonSymbol = Symbol("sheetAddRowButton");
const addColumnButtonSymbol = Symbol("sheetAddColumnButton");
const lastSnapshotSymbol = Symbol("sheetLastSnapshot");
const resizeStateSymbol = Symbol("sheetResizeState");
const skipRenderSymbol = Symbol("sheetSkipRender");
const lastViewportSymbol = Symbol("sheetLastViewport");
const scrollFrameSymbol = Symbol("sheetScrollFrame");
const forceRenderSymbol = Symbol("sheetForceRender");
const resizeFrameSymbol = Symbol("sheetResizeFrame");
const selectionSymbol = Symbol("sheetSelection");
const selectionBoxSymbol = Symbol("sheetSelectionBox");
const fillHandleSymbol = Symbol("sheetFillHandle");
const statusSymbol = Symbol("sheetStatus");
const statusTimeoutSymbol = Symbol("sheetStatusTimeout");
const contextMenuSymbol = Symbol("sheetContextMenu");
const menuStateSymbol = Symbol("sheetMenuState");
const dragFillSymbol = Symbol("sheetDragFill");
const lastCopySymbol = Symbol("sheetLastCopy");
const dragSelectSymbol = Symbol("sheetDragSelect");
const pendingResizeMoveSymbol = Symbol("sheetPendingResizeMove");
const resizeMoveFrameSymbol = Symbol("sheetResizeMoveFrame");
const pendingSelectionMoveSymbol = Symbol("sheetPendingSelectionMove");
const selectionMoveFrameSymbol = Symbol("sheetSelectionMoveFrame");
const pendingFillMoveSymbol = Symbol("sheetPendingFillMove");
const fillMoveFrameSymbol = Symbol("sheetFillMoveFrame");

class Sheet extends CustomControl {
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/sheet@@instance");
	}

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

	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: { main: getTemplate() },
			labels: getTranslations(),
			classes: {
				button: "monster-button-outline-primary",
			},
			features: {
				addRows: false,
				addColumns: false,
				editable: true,
				resizeRows: true,
				resizeColumns: true,
				virtualize: false,
			},
			virtualization: {
				rowBuffer: 4,
				columnBuffer: 2,
			},
			columns: defaultColumns(3),
			rows: defaultRows(3),
			sizes: {
				columns: {},
				rows: {},
				rowHeaderWidth: 56,
				headerHeight: 32,
			},
			constraints: {
				minColumnWidth: 64,
				maxColumnWidth: 360,
				minRowHeight: 28,
				maxRowHeight: 120,
			},
			value: { cells: {}, formulas: {} },
			disabled: false,
			cell: {
				placeholder: "",
			},
		});
	}

	static getTag() {
		return "monster-sheet";
	}

	static getCSSStyleSheet() {
		return [SheetStyleSheet];
	}

	get value() {
		return this.getOption("value");
	}

	set value(value) {
		this.setOption("value", value);
		this[forceRenderSymbol] = true;
		setFormValueSafe.call(this);
		updateControl.call(this);
	}
}

function initControlReferences() {
	const root = this.shadowRoot;
	this[gridElementSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=grid]`);
	this[gridWrapperSymbol] = root.querySelector(
		`[${ATTRIBUTE_ROLE}=grid-wrapper]`,
	);
	if (this[gridWrapperSymbol]) {
		let spacer = this[gridWrapperSymbol].querySelector(
			`[${ATTRIBUTE_ROLE}=spacer]`,
		);
		if (!spacer) {
			spacer = document.createElement("div");
			spacer.setAttribute(ATTRIBUTE_ROLE, "spacer");
			spacer.setAttribute("part", "spacer");
			this[gridWrapperSymbol].prepend(spacer);
		}
		this[spacerElementSymbol] = spacer;
	}
	this[addRowButtonSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=add-row]`);
	this[addColumnButtonSymbol] = root.querySelector(
		`[${ATTRIBUTE_ROLE}=add-column]`,
	);
	this[selectionBoxSymbol] = root.querySelector(
		`[${ATTRIBUTE_ROLE}=selection]`,
	);
	this[fillHandleSymbol] = root.querySelector(
		`[${ATTRIBUTE_ROLE}=fill-handle]`,
	);
	this[statusSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=status]`);
	this[contextMenuSymbol] = root.querySelector(
		`[${ATTRIBUTE_ROLE}=context-menu]`,
	);
}

function initEventHandler() {
	this[addRowButtonSymbol].addEventListener("click", () => {
		if (!canAddRows.call(this)) return;
		addRow.call(this);
	});

	this[addColumnButtonSymbol].addEventListener("click", () => {
		if (!canAddColumns.call(this)) return;
		addColumn.call(this);
	});

	this[gridElementSymbol].addEventListener("input", (event) => {
		const input = event.target;
		if (!(input instanceof HTMLInputElement)) return;
		const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (!rowId || !colId) return;
		setCellValue.call(this, rowId, colId, input.value);
	});

	this[gridElementSymbol].addEventListener("focusin", (event) => {
		const input = event.target;
		if (!(input instanceof HTMLInputElement)) return;
		const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (!rowId || !colId) return;
		const data = normalizeValue.call(this);
		const formula = data.formulas?.[rowId]?.[colId];
		if (isString(formula)) {
			input.value = formula;
		}
		const selection = this[selectionSymbol];
		const isMulti =
			selection?.anchor &&
			selection?.focus &&
			(selection.anchor.rowId !== selection.focus.rowId ||
				selection.anchor.colId !== selection.focus.colId);
		if (!event.shiftKey && isMulti) return;
		setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
	});

	this[gridElementSymbol].addEventListener("focusout", (event) => {
		const input = event.target;
		if (!(input instanceof HTMLInputElement)) return;
		const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (!rowId || !colId) return;
		refreshDisplayValues.call(this);
	});

	this[gridElementSymbol].addEventListener("pointerdown", (event) => {
		if (event.button !== 0) return;
		const columnHandle = event.target.closest(
			`[${ATTRIBUTE_ROLE}=column-resize]`,
		);
		if (columnHandle) {
			if (!this.getOption("features.resizeColumns", true)) return;
			startColumnResize.call(this, event, columnHandle);
			return;
		}

		const rowHandle = event.target.closest(`[${ATTRIBUTE_ROLE}=row-resize]`);
		if (rowHandle) {
			if (!this.getOption("features.resizeRows", true)) return;
			startRowResize.call(this, event, rowHandle);
			return;
		}

		const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (!rowId || !colId) return;
		setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
		startSelectionDrag.call(this, event);
	});

	this[gridElementSymbol].addEventListener("keydown", (event) => {
		if (!(event.ctrlKey || event.metaKey)) return;
		if (event.key === "c" || event.key === "C") {
			event.preventDefault();
			void copySelectionToClipboard.call(this);
			return;
		}
		if (event.key === "x" || event.key === "X") {
			event.preventDefault();
			void cutSelectionToClipboard.call(this);
			return;
		}
		if (event.key === "v" || event.key === "V") {
			event.preventDefault();
			void pasteFromClipboard.call(this);
		}
	});

	this[gridElementSymbol].addEventListener("contextmenu", (event) => {
		const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		event.preventDefault();
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (rowId && colId) {
			setSelectionFromCell.call(this, rowId, colId, false);
		}
		showContextMenu.call(this, event.clientX, event.clientY);
	});

	this.shadowRoot.addEventListener("pointerdown", (event) => {
		const menu = this[contextMenuSymbol];
		if (!menu || menu.hidden) return;
		if (menu.contains(event.target)) return;
		hideContextMenu.call(this);
	});

	if (this[gridWrapperSymbol]) {
		this[gridWrapperSymbol].addEventListener("scroll", () => {
			if (!this.getOption("features.virtualize", false)) return;
			if (this[scrollFrameSymbol]) return;
			this[scrollFrameSymbol] = requestAnimationFrame(() => {
				this[scrollFrameSymbol] = null;
				updateControl.call(this);
			});
		});
	}

	if (this[gridWrapperSymbol]) {
		this[gridWrapperSymbol].addEventListener("scroll", () => {
			hideContextMenu.call(this);
		});
	}

	if (this[fillHandleSymbol]) {
		this[fillHandleSymbol].addEventListener("pointerdown", (event) => {
			startFillDrag.call(this, event);
		});
	}

	wireContextMenuActions.call(this);
}

function initOptionObserver() {
	this[lastSnapshotSymbol] = "";
	this.attachObserver(
		new Observer(() => {
			const snapshot = JSON.stringify({
				value: this.getOption("value"),
				columns: this.getOption("columns"),
				rows: this.getOption("rows"),
				sizes: this.getOption("sizes"),
				features: this.getOption("features"),
				disabled: this.getOption("disabled"),
				classes: this.getOption("classes"),
				labels: this.getOption("labels"),
				cell: this.getOption("cell"),
			});
			if (this[skipRenderSymbol]) {
				this[lastSnapshotSymbol] = snapshot;
				this[skipRenderSymbol] = false;
				return;
			}
			if (snapshot === this[lastSnapshotSymbol]) return;
			this[lastSnapshotSymbol] = snapshot;
			updateControl.call(this);
		}),
	);
}

function updateControl() {
	const normalized = normalizeValue.call(this);
	const current = this.getOption("value");
	if (!isSameValue(current, normalized)) {
		this.setOption("value", normalized);
		setFormValueSafe.call(this);
	}

	const virtualize = this.getOption("features.virtualize", false) === true;
	if (virtualize) {
		this.setAttribute("data-virtualized", "");
		if (this[forceRenderSymbol]) {
			this[lastViewportSymbol] = null;
			this[forceRenderSymbol] = false;
		}
		renderVirtual.call(this, normalized);
	} else {
		if (this.hasAttribute("data-virtualized")) {
			this.removeAttribute("data-virtualized");
		}
		if (this[spacerElementSymbol]) {
			this[spacerElementSymbol].style.width = "0px";
			this[spacerElementSymbol].style.height = "0px";
		}
		this[lastViewportSymbol] = null;
		const displayCells = buildDisplayCells.call(this, normalized);
		renderGrid.call(this, normalized, displayCells);
	}
	syncToolbarState.call(this);
	applyDisabledState.call(this);
	updateSelectionDisplay.call(this);
}

function syncToolbarState() {
	const addRow = canAddRows.call(this);
	const addCol = canAddColumns.call(this);
	const labels = this.getOption("labels", {});
	const buttonClass = this.getOption("classes.button", "");

	this[addRowButtonSymbol].hidden = !addRow;
	this[addColumnButtonSymbol].hidden = !addCol;
	this[addRowButtonSymbol].disabled =
		this.getOption("disabled", false) || !addRow;
	this[addColumnButtonSymbol].disabled =
		this.getOption("disabled", false) || !addCol;

	this[addRowButtonSymbol].className = buttonClass;
	this[addColumnButtonSymbol].className = buttonClass;
	if (isString(labels.addRow)) {
		this[addRowButtonSymbol].textContent = labels.addRow;
	}
	if (isString(labels.addColumn)) {
		this[addColumnButtonSymbol].textContent = labels.addColumn;
	}
	syncContextMenuLabels.call(this);
}

function applyDisabledState() {
	const disabled = this.getOption("disabled", false);
	const editable = this.getOption("features.editable", true);
	const inputs = this.shadowRoot.querySelectorAll(
		`[${ATTRIBUTE_ROLE}=cell-input]`,
	);
	inputs.forEach((input) => {
		input.disabled = disabled || !editable;
	});
}

function renderGrid(value, displayCells) {
	const grid = this[gridElementSymbol];
	if (!grid) return;
	grid.style.position = "";
	grid.style.left = "";
	grid.style.top = "";
	grid.style.transform = "";
	grid.textContent = "";

	const fragment = document.createDocumentFragment();
	fragment.appendChild(buildCornerCell.call(this));

	const lastColumnId =
		value.columns.length > 0
			? value.columns[value.columns.length - 1].id
			: null;

	for (const col of value.columns) {
		fragment.appendChild(
			buildColumnHeader.call(this, col, col.id === lastColumnId),
		);
	}

	for (const row of value.rows) {
		fragment.appendChild(buildRowHeader.call(this, row));
		for (const col of value.columns) {
			fragment.appendChild(
				buildCell.call(this, row, col, displayCells, col.id === lastColumnId),
			);
		}
	}

	grid.appendChild(fragment);
	applyGridTemplate.call(this, value);
}

function renderVirtual(value) {
	const grid = this[gridElementSymbol];
	const wrapper = this[gridWrapperSymbol];
	const spacer = this[spacerElementSymbol];
	if (!grid || !wrapper || !spacer) return;

	const sizes = getVirtualSizes.call(this, value);
	const viewportWidth = wrapper.clientWidth;
	const viewportHeight = wrapper.clientHeight;
	if (viewportWidth === 0 || viewportHeight === 0) {
		if (!this[scrollFrameSymbol]) {
			this[scrollFrameSymbol] = requestAnimationFrame(() => {
				this[scrollFrameSymbol] = null;
				updateControl.call(this);
			});
		}
		return;
	}
	const scrollLeft = wrapper.scrollLeft;
	const scrollTop = wrapper.scrollTop;
	const bufferCols = getSizeNumber(
		this.getOption("virtualization.columnBuffer"),
		2,
	);
	const bufferRows = getSizeNumber(
		this.getOption("virtualization.rowBuffer"),
		4,
	);

	const visible = getVisibleRange(
		sizes.columnOffsets,
		sizes.rowOffsets,
		scrollLeft - sizes.rowHeaderWidth,
		scrollTop - sizes.headerHeight,
		viewportWidth,
		viewportHeight,
	);

	const colStart = Math.max(0, visible.colStart - bufferCols);
	const colEnd = Math.min(
		value.columns.length - 1,
		visible.colEnd + bufferCols,
	);
	const rowStart = Math.max(0, visible.rowStart - bufferRows);
	const rowEnd = Math.min(value.rows.length - 1, visible.rowEnd + bufferRows);

	const viewportKey = `${colStart}-${colEnd}-${rowStart}-${rowEnd}-${sizes.totalWidth}-${sizes.totalHeight}`;
	if (this[lastViewportSymbol] === viewportKey) return;
	this[lastViewportSymbol] = viewportKey;

	spacer.style.width = `${sizes.totalWidth}px`;
	spacer.style.height = `${sizes.totalHeight}px`;

	const offsetX = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
	const offsetY = sizes.headerHeight + sizes.rowOffsets[rowStart];

	grid.style.position = "absolute";
	grid.style.left = "0";
	grid.style.top = "0";
	grid.style.transform = `translate(${offsetX}px, ${offsetY}px)`;

	const visibleColumns = value.columns.slice(colStart, colEnd + 1);
	const visibleRows = value.rows.slice(rowStart, rowEnd + 1);

	const visibleWidths = visibleColumns.map(
		(col, index) => `${sizes.columnWidths[colStart + index]}px`,
	);
	const visibleHeights = visibleRows.map(
		(row, index) => `${sizes.rowHeights[rowStart + index]}px`,
	);

	grid.style.gridTemplateColumns = `${sizes.rowHeaderWidth}px ${visibleWidths.join(" ")}`;
	grid.style.gridTemplateRows = `${sizes.headerHeight}px ${visibleHeights.join(" ")}`;

	grid.textContent = "";
	const fragment = document.createDocumentFragment();
	fragment.appendChild(buildCornerCell.call(this));

	const lastVisibleColumnId =
		visibleColumns.length > 0
			? visibleColumns[visibleColumns.length - 1].id
			: null;

	for (const col of visibleColumns) {
		fragment.appendChild(
			buildColumnHeader.call(this, col, col.id === lastVisibleColumnId),
		);
	}

	const errors = [];
	const getDisplayValue = (rowId, colId) => {
		const formula = value.formulas?.[rowId]?.[colId];
		if (isString(formula)) {
			const evaluated = evaluateFormula.call(this, value, formula);
			if (Number.isFinite(evaluated)) return evaluated;
			errors.push({ rowId, colId, formula });
			return "#ERR";
		}
		const raw = value.cells?.[rowId]?.[colId];
		return raw === undefined || raw === null ? "" : raw;
	};

	for (const row of visibleRows) {
		fragment.appendChild(buildRowHeader.call(this, row));
		for (const col of visibleColumns) {
			fragment.appendChild(
				buildCell.call(
					this,
					row,
					col,
					getDisplayValue,
					col.id === lastVisibleColumnId,
				),
			);
		}
	}

	if (errors.length > 0) {
		fireCustomEvent(this, "monster-sheet-formula-error", { errors });
	}

	grid.appendChild(fragment);
}

function buildCornerCell() {
	const labels = this.getOption("labels", {});
	const cell = document.createElement("div");
	cell.setAttribute(ATTRIBUTE_ROLE, "corner");
	cell.setAttribute("part", "corner");
	cell.textContent = isString(labels.corner) ? labels.corner : "";
	return cell;
}

function buildColumnHeader(column, isLastColumn) {
	const cell = document.createElement("div");
	cell.setAttribute(ATTRIBUTE_ROLE, "column-header");
	cell.setAttribute("part", "column-header");
	cell.dataset.colId = column.id;
	if (isLastColumn) {
		cell.dataset.lastColumn = "true";
	}
	cell.textContent = column.label ?? column.id;

	if (this.getOption("features.resizeColumns", true)) {
		const handle = document.createElement("span");
		handle.setAttribute(ATTRIBUTE_ROLE, "column-resize");
		handle.setAttribute("part", "column-resize");
		handle.dataset.colId = column.id;
		cell.appendChild(handle);
	}
	return cell;
}

function buildRowHeader(row) {
	const cell = document.createElement("div");
	cell.setAttribute(ATTRIBUTE_ROLE, "row-header");
	cell.setAttribute("part", "row-header");
	cell.dataset.rowId = row.id;
	cell.textContent = row.label ?? row.id;

	if (this.getOption("features.resizeRows", true)) {
		const handle = document.createElement("span");
		handle.setAttribute(ATTRIBUTE_ROLE, "row-resize");
		handle.setAttribute("part", "row-resize");
		handle.dataset.rowId = row.id;
		cell.appendChild(handle);
	}
	return cell;
}

function buildCell(row, column, cells, isLastColumn) {
	const wrapper = document.createElement("div");
	wrapper.setAttribute(ATTRIBUTE_ROLE, "cell");
	wrapper.setAttribute("part", "cell");
	wrapper.dataset.rowId = row.id;
	wrapper.dataset.colId = column.id;
	if (isLastColumn) {
		wrapper.dataset.lastColumn = "true";
	}

	const input = document.createElement("input");
	input.setAttribute(ATTRIBUTE_ROLE, "cell-input");
	input.setAttribute("part", "cell-input");
	input.type = "text";
	input.autocomplete = "off";
	input.inputMode = "text";
	input.placeholder = this.getOption("cell.placeholder", "");
	const value =
		typeof cells === "function"
			? cells(row.id, column.id)
			: cells?.[row.id]?.[column.id];
	input.value = value === undefined || value === null ? "" : String(value);
	input.disabled =
		this.getOption("disabled", false) ||
		!this.getOption("features.editable", true);

	wrapper.appendChild(input);
	return wrapper;
}

function applyGridTemplate(value) {
	const sizes = this.getOption("sizes", {});
	const rowHeaderWidth = getRowHeaderWidthNumber.call(
		this,
		sizes.rowHeaderWidth,
		value.rows,
	);
	const headerHeight = getSizeNumber(sizes.headerHeight, 32);

	const columnSizes = value.columns.map((col) =>
		getColumnWidth.call(this, col.id),
	);
	const rowSizes = value.rows.map((row) => getRowHeight.call(this, row.id));

	this[gridElementSymbol].style.gridTemplateColumns =
		`${rowHeaderWidth}px ${columnSizes.join(" ")}`;
	this[gridElementSymbol].style.gridTemplateRows =
		`${headerHeight}px ${rowSizes.join(" ")}`;
}

function getVirtualSizes(value) {
	const sizes = this.getOption("sizes", {});
	const rowHeaderWidth = getRowHeaderWidthNumber.call(
		this,
		sizes.rowHeaderWidth,
		value.rows,
	);
	const headerHeight = getSizeNumber(sizes.headerHeight, 32);
	const columnWidths = value.columns.map((col) =>
		getColumnWidthNumber.call(this, col.id),
	);
	const rowHeights = value.rows.map((row) =>
		getRowHeightNumber.call(this, row.id),
	);

	const columnOffsets = buildOffsets(columnWidths);
	const rowOffsets = buildOffsets(rowHeights);

	const totalWidth =
		rowHeaderWidth +
		(columnWidths.length > 0
			? columnOffsets[columnOffsets.length - 1] +
				columnWidths[columnWidths.length - 1]
			: 0);
	const totalHeight =
		headerHeight +
		(rowHeights.length > 0
			? rowOffsets[rowOffsets.length - 1] + rowHeights[rowHeights.length - 1]
			: 0);

	return {
		rowHeaderWidth,
		headerHeight,
		columnWidths,
		rowHeights,
		columnOffsets,
		rowOffsets,
		totalWidth,
		totalHeight,
	};
}

function buildOffsets(values) {
	const offsets = [];
	let current = 0;
	for (const value of values) {
		offsets.push(current);
		current += value;
	}
	return offsets;
}

function getVisibleRange(
	columnOffsets,
	rowOffsets,
	scrollLeft,
	scrollTop,
	viewportWidth,
	viewportHeight,
) {
	const colStart = findStartIndex(columnOffsets, Math.max(0, scrollLeft));
	const colEnd = findEndIndex(
		columnOffsets,
		Math.max(0, scrollLeft) + viewportWidth,
	);
	const rowStart = findStartIndex(rowOffsets, Math.max(0, scrollTop));
	const rowEnd = findEndIndex(
		rowOffsets,
		Math.max(0, scrollTop) + viewportHeight,
	);
	return { colStart, colEnd, rowStart, rowEnd };
}

function findStartIndex(offsets, value) {
	let low = 0;
	let high = offsets.length - 1;
	let result = 0;
	while (low <= high) {
		const mid = Math.floor((low + high) / 2);
		if (offsets[mid] <= value) {
			result = mid;
			low = mid + 1;
		} else {
			high = mid - 1;
		}
	}
	return result;
}

function findEndIndex(offsets, value) {
	let low = 0;
	let high = offsets.length - 1;
	let result = offsets.length - 1;
	while (low <= high) {
		const mid = Math.floor((low + high) / 2);
		if (offsets[mid] < value) {
			low = mid + 1;
		} else {
			result = mid;
			high = mid - 1;
		}
	}
	return Math.max(0, result);
}

function normalizeValue() {
	const optionsValue = this.getOption("value");
	const columns = normalizeColumns(
		optionsValue?.columns ?? this.getOption("columns"),
	);
	const rows = normalizeRows(optionsValue?.rows ?? this.getOption("rows"));
	const cells = isObject(optionsValue?.cells) ? clone(optionsValue.cells) : {};
	const formulas = isObject(optionsValue?.formulas)
		? clone(optionsValue.formulas)
		: {};

	return { columns, rows, cells, formulas };
}

function normalizeColumns(columns) {
	const list = isArray(columns) ? columns : [];
	return list.map((col, index) => {
		if (isString(col)) {
			return { id: col, label: col };
		}
		if (isObject(col)) {
			const id = isString(col.id) ? col.id : nextColumnLabel(index);
			return { id, label: col.label ?? id };
		}
		const id = nextColumnLabel(index);
		return { id, label: id };
	});
}

function normalizeRows(rows) {
	const list = isArray(rows) ? rows : [];
	return list.map((row, index) => {
		if (isString(row)) {
			return { id: row, label: row };
		}
		if (isObject(row)) {
			const id = isString(row.id) ? row.id : nextRowLabel(index);
			return { id, label: row.label ?? id };
		}
		const id = nextRowLabel(index);
		return { id, label: id };
	});
}

function setCellValue(rowId, colId, value) {
	const data = normalizeValue.call(this);
	if (!data.cells[rowId]) data.cells[rowId] = {};
	if (!data.formulas[rowId]) data.formulas[rowId] = {};
	const next = isString(value) ? value : String(value);
	if (next.trim().startsWith("=")) {
		data.formulas[rowId][colId] = next.trim();
		delete data.cells[rowId][colId];
	} else {
		delete data.formulas[rowId][colId];
		data.cells[rowId][colId] = value;
	}
	this.setOption("value", data);
	this[skipRenderSymbol] = true;
	this[forceRenderSymbol] = true;
	setFormValueSafe.call(this);
	fireCustomEvent(this, "monster-sheet-change", {
		value: data,
		cell: { rowId, colId, value },
	});
}

function addRow() {
	const data = normalizeValue.call(this);
	const newRow = createRow(data.rows.length, data.rows);
	data.rows.push(newRow);
	this.setOption("value", data);
	this[skipRenderSymbol] = true;
	setFormValueSafe.call(this);
	fireCustomEvent(this, "monster-sheet-add-row", { row: newRow, value: data });
	updateControl.call(this);
}

function addColumn() {
	const data = normalizeValue.call(this);
	const newColumn = createColumn(data.columns.length, data.columns);
	data.columns.push(newColumn);
	this.setOption("value", data);
	this[skipRenderSymbol] = true;
	setFormValueSafe.call(this);
	fireCustomEvent(this, "monster-sheet-add-column", {
		column: newColumn,
		value: data,
	});
	updateControl.call(this);
}

function canAddRows() {
	return this.getOption("features.addRows", false) === true;
}

function canAddColumns() {
	return this.getOption("features.addColumns", false) === true;
}

function createColumn(index, columns) {
	const label = nextColumnLabel(index);
	const existing = new Set(columns.map((col) => col.id));
	const id = existing.has(label) ? new ID("column-").toString() : label;
	return { id, label };
}

function createRow(index, rows) {
	const label = nextRowLabel(index);
	const existing = new Set(rows.map((row) => row.id));
	const id = existing.has(label) ? new ID("row-").toString() : label;
	return { id, label };
}

function nextColumnLabel(index) {
	let value = index + 1;
	let label = "";
	while (value > 0) {
		const mod = (value - 1) % 26;
		label = String.fromCharCode(65 + mod) + label;
		value = Math.floor((value - 1) / 26);
	}
	return label;
}

function nextRowLabel(index) {
	return String(index + 1);
}

function defaultColumns(count) {
	return Array.from({ length: count }, (_, i) => {
		const label = nextColumnLabel(i);
		return { id: label, label };
	});
}

function defaultRows(count) {
	return Array.from({ length: count }, (_, i) => {
		const label = nextRowLabel(i);
		return { id: label, label };
	});
}

function getSizeNumber(value, fallback) {
	const n = Number(value);
	return Number.isFinite(n) ? n : fallback;
}

function getColumnWidth(columnId) {
	const sizes = this.getOption("sizes.columns", {});
	const width = getSizeNumber(sizes?.[columnId], 120);
	const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
	const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
	return `${clamp(width, min, max)}px`;
}

function getColumnWidthNumber(columnId) {
	const sizes = this.getOption("sizes.columns", {});
	const width = getSizeNumber(sizes?.[columnId], 120);
	const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
	const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
	return clamp(width, min, max);
}

function getRowHeight(rowId) {
	const sizes = this.getOption("sizes.rows", {});
	const height = getSizeNumber(sizes?.[rowId], 32);
	const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
	const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
	return `${clamp(height, min, max)}px`;
}

function getRowHeightNumber(rowId) {
	const sizes = this.getOption("sizes.rows", {});
	const height = getSizeNumber(sizes?.[rowId], 32);
	const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
	const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
	return clamp(height, min, max);
}

function getRowHeaderWidthNumber(value, rows) {
	if (value === "auto" || value === null || value === undefined) {
		const maxLen = getMaxRowLabelLength(rows);
		const base = 16;
		const charWidth = 8;
		return Math.max(56, base + maxLen * charWidth);
	}
	return getSizeNumber(value, 56);
}

function getMaxRowLabelLength(rows) {
	if (!isArray(rows) || rows.length === 0) return 1;
	const last = rows[rows.length - 1];
	const label = isString(last?.label) ? last.label : last?.id;
	const text = label === undefined || label === null ? "" : String(label);
	if (/^\d+$/.test(text)) {
		return String(rows.length).length;
	}
	return text.length;
}

function clamp(value, min, max) {
	return Math.min(Math.max(value, min), max);
}

function startColumnResize(event, handle) {
	const colId = handle.dataset.colId;
	if (!colId) return;
	event.preventDefault();
	event.stopPropagation();
	const grid = this[gridElementSymbol];
	const headerCell = handle.parentElement;
	if (!headerCell) return;
	const startWidth = headerCell.getBoundingClientRect().width;
	this.setAttribute("data-resizing", "");
	this[resizeStateSymbol] = {
		kind: "column",
		id: colId,
		start: event.clientX,
		startSize: startWidth,
	};
	grid.setPointerCapture(event.pointerId);
	grid.addEventListener("pointermove", handleResizeMove);
	grid.addEventListener("pointerup", handleResizeEnd);
	grid.addEventListener("pointercancel", handleResizeEnd);
}

function startRowResize(event, handle) {
	const rowId = handle.dataset.rowId;
	if (!rowId) return;
	event.preventDefault();
	event.stopPropagation();
	const headerCell = handle.parentElement;
	if (!headerCell) return;
	const startHeight = headerCell.getBoundingClientRect().height;
	const grid = this[gridElementSymbol];
	this.setAttribute("data-resizing", "");
	this[resizeStateSymbol] = {
		kind: "row",
		id: rowId,
		start: event.clientY,
		startSize: startHeight,
	};
	grid.setPointerCapture(event.pointerId);
	grid.addEventListener("pointermove", handleResizeMove);
	grid.addEventListener("pointerup", handleResizeEnd);
	grid.addEventListener("pointercancel", handleResizeEnd);
}

function handleResizeMove(event) {
	const grid = event.currentTarget;
	const sheet = grid.getRootNode().host;
	if (!sheet || !sheet[resizeStateSymbol]) return;
	scheduleResizeMove.call(sheet, event.clientX, event.clientY);
}

function scheduleResizeMove(clientX, clientY) {
	this[pendingResizeMoveSymbol] = { clientX, clientY };
	if (this[resizeMoveFrameSymbol]) return;
	this[resizeMoveFrameSymbol] = requestAnimationFrame(() => {
		this[resizeMoveFrameSymbol] = null;
		flushResizeMove.call(this);
	});
}

function flushResizeMove() {
	const pending = this[pendingResizeMoveSymbol];
	this[pendingResizeMoveSymbol] = null;
	if (!pending || !this[resizeStateSymbol]) return;
	const state = this[resizeStateSymbol];
	if (state.kind === "column") {
		const delta = pending.clientX - state.start;
		const size = state.startSize + delta;
		setColumnSize.call(this, state.id, size);
	} else if (state.kind === "row") {
		const delta = pending.clientY - state.start;
		const size = state.startSize + delta;
		setRowSize.call(this, state.id, size);
	}
}

function handleResizeEnd(event) {
	const grid = event.currentTarget;
	const sheet = grid.getRootNode().host;
	if (!sheet) return;
	if (sheet.hasAttribute("data-resizing")) {
		sheet.removeAttribute("data-resizing");
	}
	grid.releasePointerCapture(event.pointerId);
	grid.removeEventListener("pointermove", handleResizeMove);
	grid.removeEventListener("pointerup", handleResizeEnd);
	grid.removeEventListener("pointercancel", handleResizeEnd);
	if (sheet[resizeMoveFrameSymbol]) {
		cancelAnimationFrame(sheet[resizeMoveFrameSymbol]);
		sheet[resizeMoveFrameSymbol] = null;
	}
	flushResizeMove.call(sheet);
	sheet[resizeStateSymbol] = null;
}

function setColumnSize(columnId, size) {
	const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
	const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
	const next = clamp(size, min, max);
	if (getColumnWidthNumber.call(this, columnId) === next) {
		return;
	}
	this.setOption(`sizes.columns.${columnId}`, next);
	this[skipRenderSymbol] = true;
	if (this.getOption("features.virtualize", false) === true) {
		scheduleVirtualResizeRender.call(this);
	} else {
		applyGridTemplate.call(this, normalizeValue.call(this));
	}
	fireCustomEvent(this, "monster-sheet-resize-column", {
		columnId,
		width: next,
	});
}

function setRowSize(rowId, size) {
	const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
	const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
	const next = clamp(size, min, max);
	if (getRowHeightNumber.call(this, rowId) === next) {
		return;
	}
	this.setOption(`sizes.rows.${rowId}`, next);
	this[skipRenderSymbol] = true;
	if (this.getOption("features.virtualize", false) === true) {
		scheduleVirtualResizeRender.call(this);
	} else {
		applyGridTemplate.call(this, normalizeValue.call(this));
	}
	fireCustomEvent(this, "monster-sheet-resize-row", {
		rowId,
		height: next,
	});
}

function setFormValueSafe() {
	try {
		this.setFormValue(this.value);
	} catch (e) {
		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
	}
}

function isSameValue(a, b) {
	try {
		return JSON.stringify(a) === JSON.stringify(b);
	} catch (e) {
		return false;
	}
}

function refreshDisplayValues() {
	const data = normalizeValue.call(this);
	const virtualize = this.getOption("features.virtualize", false) === true;
	const displayCells = virtualize ? null : buildDisplayCells.call(this, data);
	const active = this.shadowRoot?.activeElement;
	const inputs = this.shadowRoot.querySelectorAll(
		`[${ATTRIBUTE_ROLE}=cell-input]`,
	);

	inputs.forEach((input) => {
		if (!(input instanceof HTMLInputElement)) return;
		if (input === active) return;
		const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (!cell) return;
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		if (!rowId || !colId) return;
		const value = virtualize
			? getCellDisplayValue.call(this, data, rowId, colId)
			: displayCells?.[rowId]?.[colId];
		input.value = value === undefined || value === null ? "" : String(value);
	});
}

function setSelectionFromCell(rowId, colId, expand) {
	hideContextMenu.call(this);
	const selection = this[selectionSymbol] || {};
	if (expand && selection.anchor) {
		selection.focus = { rowId, colId };
	} else {
		selection.anchor = { rowId, colId };
		selection.focus = { rowId, colId };
	}
	this[selectionSymbol] = selection;
	updateSelectionDisplay.call(this);
}

function setSelectionFocus(rowId, colId) {
	const selection = this[selectionSymbol] || {};
	if (!selection.anchor) {
		selection.anchor = { rowId, colId };
	}
	selection.focus = { rowId, colId };
	this[selectionSymbol] = selection;
	updateSelectionDisplay.call(this);
}

function startSelectionDrag(event) {
	if (event.button !== 0) return;
	const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
	if (!cell) return;
	const state = {
		active: true,
		startX: event.clientX,
		startY: event.clientY,
		moved: false,
	};
	this[dragSelectSymbol] = state;
	this.setAttribute("data-selecting", "");
	const move = (moveEvent) => {
		handleSelectionDragMove.call(this, moveEvent);
	};
	const end = () => {
		handleSelectionDragEnd.call(this);
	};
	window.addEventListener("pointermove", move);
	window.addEventListener("pointerup", end, { once: true });
	window.addEventListener("pointercancel", end, { once: true });
	state.cleanup = () => {
		window.removeEventListener("pointermove", move);
	};
}

function handleSelectionDragMove(event) {
	const state = this[dragSelectSymbol];
	if (!state?.active) return;
	if (!state.moved) {
		const dx = event.clientX - state.startX;
		const dy = event.clientY - state.startY;
		if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
		state.moved = true;
	}
	scheduleSelectionDragMove.call(this, event.clientX, event.clientY);
}

function scheduleSelectionDragMove(clientX, clientY) {
	this[pendingSelectionMoveSymbol] = { clientX, clientY };
	if (this[selectionMoveFrameSymbol]) return;
	this[selectionMoveFrameSymbol] = requestAnimationFrame(() => {
		this[selectionMoveFrameSymbol] = null;
		flushSelectionDragMove.call(this);
	});
}

function flushSelectionDragMove() {
	const pending = this[pendingSelectionMoveSymbol];
	this[pendingSelectionMoveSymbol] = null;
	if (!pending || !this[dragSelectSymbol]?.active) return;
	const cell = getCellFromPoint.call(this, pending.clientX, pending.clientY);
	if (!cell) return;
	const rowId = cell.dataset.rowId;
	const colId = cell.dataset.colId;
	if (!rowId || !colId) return;
	setSelectionFocus.call(this, rowId, colId);
}

function handleSelectionDragEnd() {
	const state = this[dragSelectSymbol];
	if (!state) return;
	if (this[selectionMoveFrameSymbol]) {
		cancelAnimationFrame(this[selectionMoveFrameSymbol]);
		this[selectionMoveFrameSymbol] = null;
	}
	flushSelectionDragMove.call(this);
	if (state.cleanup) state.cleanup();
	this[dragSelectSymbol] = null;
	this.removeAttribute("data-selecting");
}

function updateSelectionDisplay() {
	const grid = this[gridElementSymbol];
	if (!grid) return;
	const data = normalizeValue.call(this);
	const selection = this[selectionSymbol];
	const range = selection ? getSelectionRange(selection, data) : null;
	const virtualize = this.getOption("features.virtualize", false) === true;

	const cells = grid.querySelectorAll(`[${ATTRIBUTE_ROLE}=cell]`);
	let overlay = null;
	const wrapper = this[gridWrapperSymbol];
	const wrapperRect = wrapper?.getBoundingClientRect() ?? null;
	let minLeft = Infinity;
	let minTop = Infinity;
	let maxRight = -Infinity;
	let maxBottom = -Infinity;

	cells.forEach((cell) => {
		const rowId = cell.dataset.rowId;
		const colId = cell.dataset.colId;
		let selected = false;
		if (range && rowId && colId) {
			const { rowIndexById, colIndexById } = range;
			const rowIndex = rowIndexById.get(rowId);
			const colIndex = colIndexById.get(colId);
			if (
				rowIndex !== undefined &&
				colIndex !== undefined &&
				rowIndex >= range.rowStart &&
				rowIndex <= range.rowEnd &&
				colIndex >= range.colStart &&
				colIndex <= range.colEnd
			) {
				selected = true;
			}
		}

		if (selected) {
			cell.dataset.selected = "true";
			if (wrapperRect) {
				const rect = cell.getBoundingClientRect();
				minLeft = Math.min(minLeft, rect.left);
				minTop = Math.min(minTop, rect.top);
				maxRight = Math.max(maxRight, rect.right);
				maxBottom = Math.max(maxBottom, rect.bottom);
			}
		} else {
			delete cell.dataset.selected;
		}

		if (
			selection?.focus?.rowId === rowId &&
			selection?.focus?.colId === colId
		) {
			cell.dataset.active = "true";
		} else {
			delete cell.dataset.active;
		}
	});

	if (Number.isFinite(minLeft) && Number.isFinite(maxRight) && wrapperRect) {
		overlay = {
			left: minLeft - wrapperRect.left + wrapper.scrollLeft,
			top: minTop - wrapperRect.top + wrapper.scrollTop,
			width: Math.max(0, maxRight - minLeft),
			height: Math.max(0, maxBottom - minTop),
		};
	} else if (range && virtualize) {
		const sizes = getVirtualSizes.call(this, data);
		const colStart = range.colStart;
		const colEnd = range.colEnd;
		const rowStart = range.rowStart;
		const rowEnd = range.rowEnd;
		const left = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
		const top = sizes.headerHeight + sizes.rowOffsets[rowStart];
		const width =
			sizes.columnOffsets[colEnd] +
			sizes.columnWidths[colEnd] -
			sizes.columnOffsets[colStart];
		const height =
			sizes.rowOffsets[rowEnd] +
			sizes.rowHeights[rowEnd] -
			sizes.rowOffsets[rowStart];
		overlay = { left, top, width, height };
	}

	updateSelectionOverlay.call(this, overlay);
}

function updateSelectionOverlay(bounds) {
	const box = this[selectionBoxSymbol];
	const wrapper = this[gridWrapperSymbol];
	const handle = this[fillHandleSymbol];
	if (!box || !wrapper || !handle) return;
	if (!bounds) {
		box.style.display = "none";
		handle.style.display = "none";
		return;
	}
	box.style.display = "block";
	box.style.left = `${bounds.left}px`;
	box.style.top = `${bounds.top}px`;
	box.style.width = `${bounds.width}px`;
	box.style.height = `${bounds.height}px`;
	handle.style.display = "block";
	handle.style.left = `${bounds.left + bounds.width - 4}px`;
	handle.style.top = `${bounds.top + bounds.height - 4}px`;
}

function getSelectionRange(selection, data) {
	if (!selection?.anchor || !selection?.focus) return null;
	const { rowIndexById, colIndexById } = getIndexMaps(data);
	const startRow = rowIndexById.get(selection.anchor.rowId);
	const endRow = rowIndexById.get(selection.focus.rowId);
	const startCol = colIndexById.get(selection.anchor.colId);
	const endCol = colIndexById.get(selection.focus.colId);
	if (
		startRow === undefined ||
		endRow === undefined ||
		startCol === undefined ||
		endCol === undefined
	) {
		return null;
	}
	return {
		rowStart: Math.min(startRow, endRow),
		rowEnd: Math.max(startRow, endRow),
		colStart: Math.min(startCol, endCol),
		colEnd: Math.max(startCol, endCol),
		rowIndexById,
		colIndexById,
	};
}

function getIndexMaps(data) {
	const rowIndexById = new Map();
	const colIndexById = new Map();
	data.rows.forEach((row, index) => {
		if (row?.id) rowIndexById.set(row.id, index);
	});
	data.columns.forEach((col, index) => {
		if (col?.id) colIndexById.set(col.id, index);
	});
	return { rowIndexById, colIndexById };
}

function getActiveCell() {
	const active = this.shadowRoot?.activeElement;
	if (!(active instanceof HTMLInputElement)) return null;
	const cell = active.closest(`[${ATTRIBUTE_ROLE}=cell]`);
	if (!cell) return null;
	const rowId = cell.dataset.rowId;
	const colId = cell.dataset.colId;
	if (!rowId || !colId) return null;
	return { rowId, colId };
}

async function copySelectionToClipboard() {
	const data = normalizeValue.call(this);
	const range = getSelectionRange.call(this, this[selectionSymbol], data);
	const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
	if (!resolvedRange) return;

	const text = buildClipboardText(data, resolvedRange);
	this[lastCopySymbol] = {
		text,
		range: {
			rowStart: resolvedRange.rowStart,
			rowEnd: resolvedRange.rowEnd,
			colStart: resolvedRange.colStart,
			colEnd: resolvedRange.colEnd,
		},
		values: buildClipboardMatrix(data, resolvedRange),
	};
	const success = await writeClipboardText(text);
	if (success) {
		showStatus.call(this, this.getOption("labels.copied") || "Copied");
	}
}

async function cutSelectionToClipboard() {
	const data = normalizeValue.call(this);
	const range = getSelectionRange.call(this, this[selectionSymbol], data);
	const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
	if (!resolvedRange) return;

	const text = buildClipboardText(data, resolvedRange);
	const success = await writeClipboardText(text);
	if (!success) return;

	const next = clearRange(data, resolvedRange);
	this.value = next;
	showStatus.call(this, this.getOption("labels.cutDone") || "Cut");
}

async function pasteFromClipboard() {
	const text = await readClipboardText();
	if (text === null) return;
	const data = normalizeValue.call(this);
	const range = getSelectionRange.call(this, this[selectionSymbol], data);
	const startRange = range ?? getRangeFromActiveCell.call(this, data);
	if (!startRange) return;

	const { startRow, startCol } = {
		startRow: startRange.rowStart,
		startCol: startRange.colStart,
	};
	const cached = this[lastCopySymbol];
	const next =
		cached && cached.text === text
			? applyPasteCached(data, cached, startRow, startCol)
			: applyPasteText(data, text, startRow, startCol);
	this.value = next.value;
	setSelectionByRange.call(this, next.range, next.value);
	showStatus.call(this, this.getOption("labels.pasted") || "Pasted");
}

function buildClipboardText(data, range) {
	const rows = [];
	for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
		const rowId = data.rows[r]?.id;
		if (!rowId) continue;
		const cols = [];
		for (let c = range.colStart; c <= range.colEnd; c += 1) {
			const colId = data.columns[c]?.id;
			if (!colId) continue;
			const value = getCellRawValue(data, rowId, colId);
			cols.push(value);
		}
		rows.push(cols.join("\t"));
	}
	return rows.join("\n");
}

function buildClipboardMatrix(data, range) {
	const rows = [];
	for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
		const rowId = data.rows[r]?.id;
		if (!rowId) continue;
		const cols = [];
		for (let c = range.colStart; c <= range.colEnd; c += 1) {
			const colId = data.columns[c]?.id;
			if (!colId) continue;
			cols.push(getCellRawValue(data, rowId, colId));
		}
		rows.push(cols);
	}
	return rows;
}

function getCellRawValue(data, rowId, colId) {
	const formula = data.formulas?.[rowId]?.[colId];
	if (isString(formula)) return formula;
	const raw = data.cells?.[rowId]?.[colId];
	return raw === undefined || raw === null ? "" : String(raw);
}

function clearRange(data, range) {
	const next = {
		...data,
		cells: data.cells ? { ...data.cells } : {},
		formulas: data.formulas ? { ...data.formulas } : {},
	};
	for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
		const rowId = data.rows[r]?.id;
		if (!rowId) continue;
		for (let c = range.colStart; c <= range.colEnd; c += 1) {
			const colId = data.columns[c]?.id;
			if (!colId) continue;
			if (next.cells[rowId]) delete next.cells[rowId][colId];
			if (next.formulas[rowId]) delete next.formulas[rowId][colId];
		}
	}
	return next;
}

function getRangeFromActiveCell(data) {
	const active = getActiveCell.call(this);
	if (!active) return null;
	const { rowIndexById, colIndexById } = getIndexMaps(data);
	const rowIndex = rowIndexById.get(active.rowId);
	const colIndex = colIndexById.get(active.colId);
	if (rowIndex === undefined || colIndex === undefined) return null;
	return {
		rowStart: rowIndex,
		rowEnd: rowIndex,
		colStart: colIndex,
		colEnd: colIndex,
		rowIndexById,
		colIndexById,
	};
}

function applyPasteText(data, text, startRowIndex, startColIndex) {
	const rows = normalizeClipboardRows(text);
	const next = {
		...data,
		cells: data.cells ? { ...data.cells } : {},
		formulas: data.formulas ? { ...data.formulas } : {},
	};
	let rowEnd = startRowIndex;
	let colEnd = startColIndex;

	rows.forEach((rowText, rowOffset) => {
		const rowIndex = startRowIndex + rowOffset;
		if (rowIndex >= data.rows.length) return;
		const rowId = data.rows[rowIndex]?.id;
		if (!rowId) return;
		const cols = rowText.split("\t");
		cols.forEach((cellText, colOffset) => {
			const colIndex = startColIndex + colOffset;
			if (colIndex >= data.columns.length) return;
			const colId = data.columns[colIndex]?.id;
			if (!colId) return;
			setCellData(next, rowId, colId, cellText);
			rowEnd = Math.max(rowEnd, rowIndex);
			colEnd = Math.max(colEnd, colIndex);
		});
	});

	return {
		value: next,
		range: {
			rowStart: startRowIndex,
			rowEnd,
			colStart: startColIndex,
			colEnd,
		},
	};
}

function applyPasteCached(data, cached, startRowIndex, startColIndex) {
	const next = {
		...data,
		cells: data.cells ? { ...data.cells } : {},
		formulas: data.formulas ? { ...data.formulas } : {},
	};
	const deltaRow = startRowIndex - cached.range.rowStart;
	const deltaCol = startColIndex - cached.range.colStart;
	let rowEnd = startRowIndex;
	let colEnd = startColIndex;

	cached.values.forEach((rowValues, rowOffset) => {
		const rowIndex = startRowIndex + rowOffset;
		if (rowIndex >= data.rows.length) return;
		const rowId = data.rows[rowIndex]?.id;
		if (!rowId) return;
		rowValues.forEach((cellValue, colOffset) => {
			const colIndex = startColIndex + colOffset;
			if (colIndex >= data.columns.length) return;
			const colId = data.columns[colIndex]?.id;
			if (!colId) return;
			let nextValue = cellValue;
			if (isFormulaString(cellValue)) {
				nextValue = adjustFormulaReferences(cellValue, deltaRow, deltaCol);
			}
			setCellData(next, rowId, colId, nextValue);
			rowEnd = Math.max(rowEnd, rowIndex);
			colEnd = Math.max(colEnd, colIndex);
		});
	});

	return {
		value: next,
		range: {
			rowStart: startRowIndex,
			rowEnd,
			colStart: startColIndex,
			colEnd,
		},
	};
}

function normalizeClipboardRows(text) {
	const normalized = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
	const rows = normalized.split("\n");
	while (rows.length > 1 && rows[rows.length - 1] === "") {
		rows.pop();
	}
	return rows;
}

function setCellData(data, rowId, colId, value) {
	if (!data.cells) data.cells = {};
	if (!data.formulas) data.formulas = {};
	if (!data.cells[rowId]) data.cells[rowId] = {};
	if (!data.formulas[rowId]) data.formulas[rowId] = {};
	const next = String(value ?? "");
	if (next.trim().startsWith("=")) {
		data.formulas[rowId][colId] = next.trim();
		delete data.cells[rowId][colId];
	} else {
		delete data.formulas[rowId][colId];
		data.cells[rowId][colId] = next;
	}
}

function isFormulaString(value) {
	return isString(value) && value.trim().startsWith("=");
}

function adjustFormulaReferences(formula, deltaRow, deltaCol) {
	const expr = formula.trim();
	if (!expr.startsWith("=")) return formula;
	const body = expr.slice(1);
	const adjusted = body.replace(/([A-Za-z]+)([0-9]+)/g, (match, col, row) => {
		const colIndex = columnLabelToIndex(col);
		if (colIndex === null) return match;
		const rowIndex = Number(row) - 1;
		if (!Number.isFinite(rowIndex)) return match;
		const nextCol = colIndex + deltaCol;
		const nextRow = rowIndex + deltaRow;
		if (nextCol < 0 || nextRow < 0) return match;
		return `${indexToColumnLabel(nextCol)}${nextRow + 1}`;
	});
	return `=${adjusted}`;
}

function columnLabelToIndex(label) {
	const text = String(label || "").toUpperCase();
	if (!/^[A-Z]+$/.test(text)) return null;
	let index = 0;
	for (let i = 0; i < text.length; i += 1) {
		index = index * 26 + (text.charCodeAt(i) - 64);
	}
	return index - 1;
}

function indexToColumnLabel(index) {
	if (!Number.isFinite(index) || index < 0) return "A";
	return nextColumnLabel(index);
}

function setSelectionByRange(range, data) {
	if (!range) return;
	const rows = data.rows;
	const cols = data.columns;
	if (
		range.rowStart < 0 ||
		range.colStart < 0 ||
		range.rowEnd >= rows.length ||
		range.colEnd >= cols.length
	) {
		return;
	}
	this[selectionSymbol] = {
		anchor: {
			rowId: rows[range.rowStart].id,
			colId: cols[range.colStart].id,
		},
		focus: {
			rowId: rows[range.rowEnd].id,
			colId: cols[range.colEnd].id,
		},
	};
	updateSelectionDisplay.call(this);
}

async function writeClipboardText(text) {
	try {
		if (navigator.clipboard?.writeText) {
			await navigator.clipboard.writeText(text);
			return true;
		}
	} catch (e) {
		return false;
	}
	return false;
}

async function readClipboardText() {
	try {
		if (navigator.clipboard?.readText) {
			return await navigator.clipboard.readText();
		}
	} catch (e) {
		return null;
	}
	return null;
}

function showStatus(message) {
	const status = this[statusSymbol];
	if (!status) return;
	status.textContent = message;
	status.dataset.show = "true";
	if (this[statusTimeoutSymbol]) {
		clearTimeout(this[statusTimeoutSymbol]);
	}
	this[statusTimeoutSymbol] = setTimeout(() => {
		status.dataset.show = "false";
	}, 1200);
}

function syncContextMenuLabels() {
	const menu = this[contextMenuSymbol];
	if (!menu) return;
	const labels = this.getOption("labels", {});
	const copy = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-copy]`);
	const paste = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-paste]`);
	const cut = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-cut]`);
	if (copy && isString(labels.copy)) copy.textContent = labels.copy;
	if (paste && isString(labels.paste)) paste.textContent = labels.paste;
	if (cut && isString(labels.cut)) cut.textContent = labels.cut;
}

function wireContextMenuActions() {
	const menu = this[contextMenuSymbol];
	if (!menu) return;
	const copy = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-copy]`);
	const paste = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-paste]`);
	const cut = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-cut]`);
	const labels = this.getOption("labels", {});
	if (copy) copy.textContent = labels.copy || "Copy";
	if (paste) paste.textContent = labels.paste || "Paste";
	if (cut) cut.textContent = labels.cut || "Cut";

	if (copy) {
		copy.addEventListener("click", async () => {
			await copySelectionToClipboard.call(this);
			hideContextMenu.call(this);
		});
	}
	if (paste) {
		paste.addEventListener("click", async () => {
			await pasteFromClipboard.call(this);
			hideContextMenu.call(this);
		});
	}
	if (cut) {
		cut.addEventListener("click", async () => {
			await cutSelectionToClipboard.call(this);
			hideContextMenu.call(this);
		});
	}
}

function showContextMenu(clientX, clientY) {
	const menu = this[contextMenuSymbol];
	const wrapper = this[gridWrapperSymbol];
	if (!menu || !wrapper) return;
	menu.hidden = false;
	menu.style.display = "block";
	const wrapperRect = wrapper.getBoundingClientRect();
	const offsetX = clientX - wrapperRect.left + wrapper.scrollLeft;
	const offsetY = clientY - wrapperRect.top + wrapper.scrollTop;
	const menuWidth = menu.offsetWidth || 160;
	const menuHeight = menu.offsetHeight || 96;
	const maxLeft = wrapper.scrollLeft + wrapper.clientWidth - menuWidth;
	const maxTop = wrapper.scrollTop + wrapper.clientHeight - menuHeight;
	menu.style.left = `${Math.max(wrapper.scrollLeft, Math.min(offsetX, maxLeft))}px`;
	menu.style.top = `${Math.max(wrapper.scrollTop, Math.min(offsetY, maxTop))}px`;
	this[menuStateSymbol] = { open: true };
}

function hideContextMenu() {
	const menu = this[contextMenuSymbol];
	if (!menu || menu.hidden) return;
	menu.hidden = true;
	menu.style.display = "none";
	this[menuStateSymbol] = { open: false };
}

function startFillDrag(event) {
	if (event.button !== 0) return;
	const data = normalizeValue.call(this);
	const range = getSelectionRange.call(this, this[selectionSymbol], data);
	if (!range) return;
	event.preventDefault();
	event.stopPropagation();
	if (event.target?.setPointerCapture) {
		event.target.setPointerCapture(event.pointerId);
	}
	const state = {
		origin: {
			rowStart: range.rowStart,
			rowEnd: range.rowEnd,
			colStart: range.colStart,
			colEnd: range.colEnd,
		},
		current: null,
		startX: event.clientX,
		startY: event.clientY,
		moved: false,
	};
	this[dragFillSymbol] = state;
	this.setAttribute("data-filling", "");

	const move = (moveEvent) => {
		handleFillMove.call(this, moveEvent);
	};
	const end = (endEvent) => {
		handleFillEnd.call(this, endEvent);
	};
	window.addEventListener("pointermove", move);
	window.addEventListener("pointerup", end, { once: true });
	window.addEventListener("pointercancel", end, { once: true });
	state.cleanup = () => {
		window.removeEventListener("pointermove", move);
	};
}

function handleFillMove(event) {
	const state = this[dragFillSymbol];
	if (!state) return;
	if (!state.moved) {
		const dx = event.clientX - state.startX;
		const dy = event.clientY - state.startY;
		if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
		state.moved = true;
	}
	scheduleFillMove.call(this, event.clientX, event.clientY);
}

function scheduleFillMove(clientX, clientY) {
	this[pendingFillMoveSymbol] = { clientX, clientY };
	if (this[fillMoveFrameSymbol]) return;
	this[fillMoveFrameSymbol] = requestAnimationFrame(() => {
		this[fillMoveFrameSymbol] = null;
		flushFillMove.call(this);
	});
}

function flushFillMove() {
	const state = this[dragFillSymbol];
	const pending = this[pendingFillMoveSymbol];
	this[pendingFillMoveSymbol] = null;
	if (!state || !pending) return;
	const data = normalizeValue.call(this);
	const cell = getCellFromPoint.call(this, pending.clientX, pending.clientY);
	if (!cell) return;
	const rowId = cell.dataset.rowId;
	const colId = cell.dataset.colId;
	if (!rowId || !colId) return;
	const { rowIndexById, colIndexById } = getIndexMaps(data);
	const rowIndex = rowIndexById.get(rowId);
	const colIndex = colIndexById.get(colId);
	if (rowIndex === undefined || colIndex === undefined) return;

	const nextRange = {
		rowStart: Math.min(state.origin.rowStart, rowIndex),
		rowEnd: Math.max(state.origin.rowEnd, rowIndex),
		colStart: Math.min(state.origin.colStart, colIndex),
		colEnd: Math.max(state.origin.colEnd, colIndex),
	};
	state.current = nextRange;
	setSelectionByRange.call(this, nextRange, data);
}

function handleFillEnd() {
	const state = this[dragFillSymbol];
	if (!state) return;
	if (this[fillMoveFrameSymbol]) {
		cancelAnimationFrame(this[fillMoveFrameSymbol]);
		this[fillMoveFrameSymbol] = null;
	}
	flushFillMove.call(this);
	if (state.cleanup) state.cleanup();
	this.removeAttribute("data-filling");
	const data = normalizeValue.call(this);
	const target = state.current;
	const origin = state.origin;
	this[dragFillSymbol] = null;
	if (!state.moved || !target) return;
	const next = applyFillRange(data, origin, target);
	this.value = next;
	setSelectionByRange.call(this, target, next);
}

function getCellFromPoint(clientX, clientY) {
	if (!this.shadowRoot) return null;
	const elements = document.elementsFromPoint(clientX, clientY);
	for (const el of elements) {
		if (!this.shadowRoot.contains(el)) continue;
		const cell = el.closest(`[${ATTRIBUTE_ROLE}=cell]`);
		if (cell) return cell;
	}
	const virtualize = this.getOption("features.virtualize", false) === true;
	if (!virtualize) return null;
	const wrapper = this[gridWrapperSymbol];
	if (!wrapper) return null;
	const rect = wrapper.getBoundingClientRect();
	const data = normalizeValue.call(this);
	const sizes = getVirtualSizes.call(this, data);
	const x = clientX - rect.left + wrapper.scrollLeft - sizes.rowHeaderWidth;
	const y = clientY - rect.top + wrapper.scrollTop - sizes.headerHeight;
	if (x < 0 || y < 0) return null;
	const colIndex = findStartIndex(sizes.columnOffsets, x);
	const rowIndex = findStartIndex(sizes.rowOffsets, y);
	if (
		colIndex < 0 ||
		rowIndex < 0 ||
		colIndex >= data.columns.length ||
		rowIndex >= data.rows.length
	) {
		return null;
	}
	return {
		dataset: {
			rowId: data.rows[rowIndex].id,
			colId: data.columns[colIndex].id,
		},
	};
}

function applyFillRange(data, origin, target) {
	const next = {
		...data,
		cells: data.cells ? { ...data.cells } : {},
		formulas: data.formulas ? { ...data.formulas } : {},
	};
	const originRows = origin.rowEnd - origin.rowStart + 1;
	const originCols = origin.colEnd - origin.colStart + 1;
	for (let r = target.rowStart; r <= target.rowEnd; r += 1) {
		const rowId = data.rows[r]?.id;
		if (!rowId) continue;
		for (let c = target.colStart; c <= target.colEnd; c += 1) {
			if (
				r >= origin.rowStart &&
				r <= origin.rowEnd &&
				c >= origin.colStart &&
				c <= origin.colEnd
			) {
				continue;
			}
			const colId = data.columns[c]?.id;
			if (!colId) continue;
			const sourceRow = origin.rowStart + ((r - origin.rowStart) % originRows);
			const sourceCol = origin.colStart + ((c - origin.colStart) % originCols);
			const sourceRowId = data.rows[sourceRow]?.id;
			const sourceColId = data.columns[sourceCol]?.id;
			if (!sourceRowId || !sourceColId) continue;
			let value = getCellRawValue(data, sourceRowId, sourceColId);
			if (isFormulaString(value)) {
				const deltaRow = r - sourceRow;
				const deltaCol = c - sourceCol;
				value = adjustFormulaReferences(value, deltaRow, deltaCol);
			}
			setCellData(next, rowId, colId, value);
		}
	}
	return next;
}

function scheduleVirtualResizeRender() {
	if (this[resizeFrameSymbol]) return;
	this[resizeFrameSymbol] = requestAnimationFrame(() => {
		this[resizeFrameSymbol] = null;
		this[forceRenderSymbol] = true;
		updateControl.call(this);
	});
}

function getCellDisplayValue(data, rowId, colId) {
	const formula = data.formulas?.[rowId]?.[colId];
	if (isString(formula)) {
		const evaluated = evaluateFormula.call(this, data, formula);
		return Number.isFinite(evaluated) ? evaluated : "#ERR";
	}
	const raw = data.cells?.[rowId]?.[colId];
	return raw === undefined || raw === null ? "" : raw;
}

function buildDisplayCells(data) {
	const result = clone(data.cells);
	const errors = [];

	for (const rowId of Object.keys(data.formulas || {})) {
		for (const colId of Object.keys(data.formulas[rowId] || {})) {
			const formula = data.formulas[rowId][colId];
			if (!isString(formula)) continue;
			const value = evaluateFormula.call(this, data, formula);
			if (!result[rowId]) result[rowId] = {};
			if (Number.isFinite(value)) {
				result[rowId][colId] = value;
			} else {
				result[rowId][colId] = "#ERR";
				errors.push({ rowId, colId, formula });
			}
		}
	}

	if (errors.length > 0) {
		fireCustomEvent(this, "monster-sheet-formula-error", { errors });
	}

	return result;
}

function evaluateFormula(data, formula, stack = []) {
	const expr = formula.trim().startsWith("=")
		? formula.trim().slice(1)
		: formula.trim();
	if (expr === "") return NaN;
	try {
		const tokens = tokenizeFormula(expr);
		const rpn = toRpn(tokens);
		const value = evalRpn.call(this, data, rpn, stack);
		return Number.isFinite(value) ? value : NaN;
	} catch (e) {
		return NaN;
	}
}

function tokenizeFormula(expr) {
	const tokens = [];
	const pattern = /\s*([A-Za-z]+[0-9]+|\d+(?:\.\d+)?|[()+\-*/])\s*/g;
	let match;
	while ((match = pattern.exec(expr)) !== null) {
		tokens.push(match[1]);
	}
	return tokens;
}

function toRpn(tokens) {
	const output = [];
	const ops = [];
	const precedence = { "+": 1, "-": 1, "*": 2, "/": 2 };

	for (const token of tokens) {
		if (isNumberToken(token) || isRefToken(token)) {
			output.push(token);
			continue;
		}

		if (token === "(") {
			ops.push(token);
			continue;
		}
		if (token === ")") {
			while (ops.length > 0 && ops[ops.length - 1] !== "(") {
				output.push(ops.pop());
			}
			ops.pop();
			continue;
		}

		while (
			ops.length > 0 &&
			ops[ops.length - 1] !== "(" &&
			precedence[ops[ops.length - 1]] >= precedence[token]
		) {
			output.push(ops.pop());
		}
		ops.push(token);
	}

	while (ops.length > 0) {
		output.push(ops.pop());
	}

	return output;
}

function evalRpn(data, rpn, stack) {
	const s = [];
	for (const token of rpn) {
		if (isNumberToken(token)) {
			s.push(Number(token));
			continue;
		}
		if (isRefToken(token)) {
			const { rowId, colId } = parseRefToken(token);
			const value = getCellNumericValue.call(this, data, rowId, colId, stack);
			s.push(value);
			continue;
		}
		const b = s.pop();
		const a = s.pop();
		if (!Number.isFinite(a) || !Number.isFinite(b)) return NaN;
		switch (token) {
			case "+":
				s.push(a + b);
				break;
			case "-":
				s.push(a - b);
				break;
			case "*":
				s.push(a * b);
				break;
			case "/":
				s.push(b === 0 ? NaN : a / b);
				break;
			default:
				return NaN;
		}
	}
	return s.length === 1 ? s[0] : NaN;
}

function getCellNumericValue(data, rowId, colId, stack) {
	const key = `${rowId}:${colId}`;
	if (stack.includes(key)) return NaN;

	const formula = data.formulas?.[rowId]?.[colId];
	if (isString(formula)) {
		const nextStack = [...stack, key];
		return evaluateFormula.call(this, data, formula, nextStack);
	}

	const raw = data.cells?.[rowId]?.[colId];
	const number = Number(raw);
	return Number.isFinite(number) ? number : NaN;
}

function isNumberToken(token) {
	return /^-?\d+(?:\.\d+)?$/.test(token);
}

function isRefToken(token) {
	return /^[A-Za-z]+[0-9]+$/.test(token);
}

function parseRefToken(token) {
	const match = /^([A-Za-z]+)([0-9]+)$/.exec(token);
	return { colId: match[1].toUpperCase(), rowId: match[2] };
}

function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				addRow: "Zeile hinzufügen",
				addColumn: "Spalte hinzufügen",
				corner: "",
				copy: "Kopieren",
				paste: "Einfügen",
				cut: "Ausschneiden",
				copied: "Kopiert",
				pasted: "Eingefügt",
				cutDone: "Ausgeschnitten",
			};
		case "fr":
			return {
				addRow: "Ajouter une ligne",
				addColumn: "Ajouter une colonne",
				corner: "",
				copy: "Copier",
				paste: "Coller",
				cut: "Couper",
				copied: "Copié",
				pasted: "Collé",
				cutDone: "Coupé",
			};
		case "es":
			return {
				addRow: "Agregar fila",
				addColumn: "Agregar columna",
				corner: "",
				copy: "Copiar",
				paste: "Pegar",
				cut: "Cortar",
				copied: "Copiado",
				pasted: "Pegado",
				cutDone: "Cortado",
			};
		case "zh":
			return {
				addRow: "添加行",
				addColumn: "添加列",
				corner: "",
				copy: "复制",
				paste: "粘贴",
				cut: "剪切",
				copied: "已复制",
				pasted: "已粘贴",
				cutDone: "已剪切",
			};
		case "hi":
			return {
				addRow: "पंक्ति जोड़ें",
				addColumn: "स्तंभ जोड़ें",
				corner: "",
				copy: "कॉपी करें",
				paste: "पेस्ट करें",
				cut: "कट करें",
				copied: "कॉपी हो गया",
				pasted: "पेस्ट हो गया",
				cutDone: "कट हो गया",
			};
		case "bn":
			return {
				addRow: "সারি যোগ করুন",
				addColumn: "কলাম যোগ করুন",
				corner: "",
				copy: "কপি",
				paste: "পেস্ট",
				cut: "কাট",
				copied: "কপি করা হয়েছে",
				pasted: "পেস্ট করা হয়েছে",
				cutDone: "কাট করা হয়েছে",
			};
		case "pt":
			return {
				addRow: "Adicionar linha",
				addColumn: "Adicionar coluna",
				corner: "",
				copy: "Copiar",
				paste: "Colar",
				cut: "Recortar",
				copied: "Copiado",
				pasted: "Colado",
				cutDone: "Recortado",
			};
		case "ru":
			return {
				addRow: "Добавить строку",
				addColumn: "Добавить столбец",
				corner: "",
				copy: "Копировать",
				paste: "Вставить",
				cut: "Вырезать",
				copied: "Скопировано",
				pasted: "Вставлено",
				cutDone: "Вырезано",
			};
		case "ja":
			return {
				addRow: "行を追加",
				addColumn: "列を追加",
				corner: "",
				copy: "コピー",
				paste: "貼り付け",
				cut: "切り取り",
				copied: "コピーしました",
				pasted: "貼り付けました",
				cutDone: "切り取りました",
			};
		case "pa":
			return {
				addRow: "ਕਤਾਰ ਸ਼ਾਮਲ ਕਰੋ",
				addColumn: "ਕਾਲਮ ਸ਼ਾਮਲ ਕਰੋ",
				corner: "",
				copy: "ਕਾਪੀ",
				paste: "ਪੇਸਟ",
				cut: "ਕੱਟੋ",
				copied: "ਕਾਪੀ ਹੋਇਆ",
				pasted: "ਪੇਸਟ ਹੋਇਆ",
				cutDone: "ਕੱਟਿਆ ਗਿਆ",
			};
		case "mr":
			return {
				addRow: "ओळ जोडा",
				addColumn: "स्तंभ जोडा",
				corner: "",
				copy: "कॉपी",
				paste: "पेस्ट",
				cut: "कट",
				copied: "कॉपी झाले",
				pasted: "पेस्ट झाले",
				cutDone: "कट झाले",
			};
		case "it":
			return {
				addRow: "Aggiungi riga",
				addColumn: "Aggiungi colonna",
				corner: "",
				copy: "Copia",
				paste: "Incolla",
				cut: "Taglia",
				copied: "Copiato",
				pasted: "Incollato",
				cutDone: "Tagliato",
			};
		case "nl":
			return {
				addRow: "Rij toevoegen",
				addColumn: "Kolom toevoegen",
				corner: "",
				copy: "Kopiëren",
				paste: "Plakken",
				cut: "Knippen",
				copied: "Gekopieerd",
				pasted: "Geplakt",
				cutDone: "Geknipt",
			};
		case "sv":
			return {
				addRow: "Lägg till rad",
				addColumn: "Lägg till kolumn",
				corner: "",
				copy: "Kopiera",
				paste: "Klistra in",
				cut: "Klipp ut",
				copied: "Kopierad",
				pasted: "Inklistrad",
				cutDone: "Urklippt",
			};
		case "pl":
			return {
				addRow: "Dodaj wiersz",
				addColumn: "Dodaj kolumnę",
				corner: "",
				copy: "Kopiuj",
				paste: "Wklej",
				cut: "Wytnij",
				copied: "Skopiowano",
				pasted: "Wklejono",
				cutDone: "Wycięto",
			};
		case "da":
			return {
				addRow: "Tilføj række",
				addColumn: "Tilføj kolonne",
				corner: "",
				copy: "Kopier",
				paste: "Indsæt",
				cut: "Klip",
				copied: "Kopieret",
				pasted: "Indsat",
				cutDone: "Klippet",
			};
		case "fi":
			return {
				addRow: "Lisää rivi",
				addColumn: "Lisää sarake",
				corner: "",
				copy: "Kopioi",
				paste: "Liitä",
				cut: "Leikkaa",
				copied: "Kopioitu",
				pasted: "Liitetty",
				cutDone: "Leikattu",
			};
		case "no":
			return {
				addRow: "Legg til rad",
				addColumn: "Legg til kolonne",
				corner: "",
				copy: "Kopier",
				paste: "Lim inn",
				cut: "Klipp ut",
				copied: "Kopiert",
				pasted: "Limt inn",
				cutDone: "Klippet",
			};
		case "cs":
			return {
				addRow: "Přidat řádek",
				addColumn: "Přidat sloupec",
				corner: "",
				copy: "Kopírovat",
				paste: "Vložit",
				cut: "Vyjmout",
				copied: "Zkopírováno",
				pasted: "Vloženo",
				cutDone: "Vyjmuto",
			};
		default:
			return {
				addRow: "Add row",
				addColumn: "Add column",
				corner: "",
				copy: "Copy",
				paste: "Paste",
				cut: "Cut",
				copied: "Copied",
				pasted: "Pasted",
				cutDone: "Cut",
			};
	}
}

function getTemplate() {
	return `
    <div data-monster-role="control" part="control">
      <div data-monster-role="toolbar" part="toolbar">
        <button type="button" data-monster-role="add-row" part="add-row"></button>
        <button type="button" data-monster-role="add-column" part="add-column"></button>
      </div>
      <div data-monster-role="grid-wrapper" part="grid-wrapper">
        <div data-monster-role="grid" part="grid"></div>
        <div data-monster-role="selection" part="selection" aria-hidden="true"></div>
        <span data-monster-role="fill-handle" part="fill-handle" aria-hidden="true"></span>
        <div data-monster-role="status" part="status" role="status" aria-live="polite"></div>
        <div data-monster-role="context-menu" part="context-menu" role="menu" hidden>
          <button type="button" data-monster-role="menu-copy" part="menu-item" role="menuitem"></button>
          <button type="button" data-monster-role="menu-paste" part="menu-item" role="menuitem"></button>
          <button type="button" data-monster-role="menu-cut" part="menu-item" role="menuitem"></button>
        </div>
      </div>
    </div>
  `;
}

registerCustomElement(Sheet);
