/* eslint-disable style/max-statements-per-line */
/* eslint-disable no-new */
/* eslint-disable ts/no-this-alias */
import type { IWebApiClient, WebClientApiMethod } from '../../common/IWebClient';
import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
import type { DaterangeChangedArgs } from '../input/daterange-picker';
import type { ColFilterModalShowResponse } from './filter-modal';
import Mark from 'mark.js';
import Sortable from 'sortablejs';
import { Prop, toNative } from 'vue-facing-decorator';
import { globalState } from '../../app/global-state';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import { DialogResult, DialogUtils } from '../../common/dialog-utils';
import { DialogIcons } from '../../common/enums/dialog-icons';
import { sortBy } from '../../common/extensions/array-extensions';
import { capitalize, latinize } from '../../common/extensions/string-extensions';
import StorageProvider from '../../common/local-storage-shim';
import { QueryStringUtils } from '../../common/query-string-utils';
import CheckboxUtils from '../../common/utils/checkbox-utils';
import DropdownUtils from '../../common/utils/dropdown-utils';
import { isNullOrEmpty } from '../../common/utils/is-null-or-empty';
import TemporalUtils from '../../common/utils/temporal-utils';
import { PortalUtils } from '../../common/utils/utils';
import Button from '../button/button';
import { ButtonLayout } from '../button/button-layout';
import DropdownList, { MultiSelectExclusivity, MultiselectMode } from '../dropdown';
import DropdownButton from '../dropdown-button/dropdown-button';
import DropdownButtonItem from '../dropdown-button/dropdown-button-item';
import CheckBox, { CheckBoxSkin } from '../input/checkbox';
import DaterangePicker from '../input/daterange-picker';
import LoadingIndicator from '../loading-indicator';
import Teleport from '../teleport/teleport';
import NotificationProvider from '../ui/notification';
import TimezoneHelper from './../../common/timezone-helper';
import TextBox from './../input/textbox';
import ColVisModal from './col-vis-modal';
import TableExportModal from './export-excel-modal';
import ColFilterModal from './filter-modal';
import loadingGif from './img/loading_16_16.gif';
import { DataTableConfig } from './ts/datatable-config';
import ReorderProvider from './ts/reorder';
import './css/datatable.css';

interface DataTableArgs {
	id: string;
	apiClient: IWebApiClient;
	apiMethod: WebClientApiMethod;
	allowMassOperations?: boolean;
	allowExport?: boolean;
	autoFetch?: boolean;
	apiArgs?: any;
	exportConfig?: DataTableExportConfig;
	paginations?: number[];
	paginationLength?: number;
	autoFilter?: boolean;
	fullSizeTable?: boolean;
	insetTop?: boolean;
	fullSizeHasButtonBelow?: boolean;
	filterMode?: DataTableFilterMode;
	checkboxesVisible?: boolean;
	checkboxButtonsVisible?: boolean;
	topVisible?: boolean;
	bottomVisible?: boolean;
	cssClass?: string;
	columns: TableColumn[];
	buttons?: TableButton[];
	rowIndexMode?: RowIndexMode;
	fulltextPlaceholder?: string;
	skin?: DataTableSkin;
	preserveOrderBy?: boolean;
	preserveFilter?: boolean;
	sortableRows?: boolean;
	customAjaxCall?: (args: DataTablePostBackData) => Promise<DataTableLoadedRowset>;
	parseLoadedRowset?: (serverResp: any) => DataTableLoadedRowset;
	rowClicked?: (row: any) => void;
	rowCssClass?: (row: any) => string;
	rowCheckstateChanged?: (row: any, checked: boolean, selectedRows: any[]) => void;
	/**
	 * Fired after the built-in mass-operations Cancel button runs its baseline
	 * reset (`clearSelection()` + `toggleCheckboxes(false)`). Use it to scrub
	 * page-level state the table doesn't own — e.g. a derived selection array
	 * or a bulk-warning alert. Not invoked when checkboxes are toggled off
	 * through other paths, only by the built-in Cancel button.
	 */
	massOperationsCancelClicked?: () => void;
	mobileBehavior?: DataTableMobileBehavior;
	mobileModeCustomRender?: (h, columns: TableColumn[], rows: any) => void;
	sortComplete?: (args: DataTableOnSortedArgs) => void;
	mobileModeRowIcon?: string;
	mobileModeShouldAutoCollapse?: boolean;
	massOperationOptions?: MassOperationItem[];
	handleInitialFilter?: boolean;
	timezoneGmtOffset?: number;
	timezoneDstOffset?: number;
	timeFilterShiftTimezone?: boolean;
	hidePagination?: boolean;
}

export interface DataTableExportConfig {
	attrName: string;
	limitName?: string;
	pageName?: string;
}

export interface DataTablePostBackData {
	ShowHidden: boolean;
	PaginationPosition: number;
	PaginationLength: number;
	SessionId: number;
	Filter: DataTableFilterDefinition;
	Sort: DataTableSortDefinition;
}

export interface MassOperationItem extends DropdownButtonItemArgs {

}

interface DataTableFilterDefinition {
	FullText: string;
	FilterItems: DataTablePostBackFilterItem[];
}

export interface DataTableOnSortedArgs {
	oldIndex: number;
	newIndex: number;
}

export interface DataTablePostBackFilterItem {
	PropertyName: string;
	ContainsValue?: string;
	EqualsValue?: string;
	FilterType: DataTableFilterItemType;
	NumFrom?: number;
	NumTo?: number;
	DateFrom?: Temporal.PlainDateTime;
	DateTo?: Temporal.PlainDateTime;
	ValueArr?: string[];
	ValueArrStrategy?: MultiSelectExclusivity;
}

interface DataTableSortDefinition {
	PropertyName: string;
	Direction: DataTableSortDirection;
}

export enum DataTableFilterMode {
	Serverside = 0,
	Clientside = 1,
}

export enum DataTableFilterItemType {
	None = 0,
	Text = 1,
	Dropdown = 2,
	DateRange = 3,
	NumericRange = 4,
	DateRangeWithTime = 5,
}

export enum DataTableSkin {
	Default = 'default',
	Compact = 'compact',
}

export enum DataTableMobileBehavior {
	MobileLayout = 'mobile',
	Compact = 'compact',
	VerticalTransform = 'vertical', // Do not use, shitty
}

export enum RowIndexMode {
	Identifier = 0,
	Index = 1,
}

export interface DataTableLoadedRowset {
	totalCount: number;
	totalFilteredCount: number;
	rows: any[];
	/**
	 * Count-free mode: the server skipped the (expensive) total count and
	 * signals whether a next page exists instead. Paired with
	 * `totalCount: -1`, it switches the table to simple prev/next pagination.
	 */
	hasMore?: boolean;
}

interface StoredState {
	sortOrder: string[];
	filter: DataTablePostBackFilterItem[];
	hiddenColumns: string[];
	visibleColumns: string[];
	paginationLength: number;
	orderBy: SortDefinition;
	skin: DataTableSkin;
	mobileBehavior: DataTableMobileBehavior;
}

class RowIdentifier {
	name: string = null;
	surname: string = null;
	dtColumns: TableColumn[] = null;

	constructor(name: string, surname: string) {
		this.name = name;
		this.surname = surname;
	}

	get fullName(): string {
		return (`${this.name || ''} ${this.surname || ''}`).trim();
	}
}

export enum DataTableSortDirection {
	Ascending = 0,
	Descending = 1,
}

interface SortDefinition {
	columnId: string;
	direction: DataTableSortDirection;
}

const enum DataTableBreakpoint {
	Desktop = 'desktop',
	Mobile = 'mobile',
}

/**
 * Page sizes offered when a table does not pass its own `paginations` list.
 * The `-1` entry renders as "All" and asks the API for every row.
 */
const DEFAULT_PAGINATIONS = [
	10,
	25,
	50,
	100,
	250,
	500,
	1000,
	-1,
];

class StorageHelper {
	private static getStorageKey(id: string) {
		return `bldt-${(globalState.location?.pathname || '').split('/').join('_')}${id}`;
	}

	static getStoredState(id: string): StoredState {
		const retVal = StorageProvider.getString(StorageHelper.getStorageKey(id));
		if (retVal != null) {
			try {
				return JSON.parse(retVal);
			} catch (e) { }
		}

		return {} as any;
	}

	static saveStoredState(id: string, state: StoredState): void {
		StorageProvider.setString(StorageHelper.getStorageKey(id), JSON.stringify(state));
	}

	static storeSortOrder(id: string, sortOrder: string[]) {
		const state = StorageHelper.getStoredState(id);
		if (sortOrder != null) {
			state.sortOrder = sortOrder;
		} else if (state.sortOrder != null) {
			delete state.sortOrder;
		}

		StorageHelper.saveStoredState(id, state);
	}

	static storeOrderBy(id: string, orderBy: SortDefinition) {
		const state = StorageHelper.getStoredState(id);
		if (orderBy != null) {
			state.orderBy = orderBy;
		} else if (state.orderBy != null) {
			delete state.orderBy;
		}

		StorageHelper.saveStoredState(id, state);
	}

	static storeColVis(id: string, columns: TableColumn[]) {
		const state = StorageHelper.getStoredState(id);
		state.hiddenColumns = columns.filter(p => p.visible == false).map(p => p.id);
		state.visibleColumns = columns.filter(p => (p as any)._enforceVisible == true).map(p => p.id);
		StorageHelper.saveStoredState(id, state);
	}

	static storePaginationLength(id: string, paginationLength: number) {
		const state = StorageHelper.getStoredState(id);
		state.paginationLength = paginationLength;
		StorageHelper.saveStoredState(id, state);
	}

	static storeSkin(id: string, skin: DataTableSkin) {
		const state = StorageHelper.getStoredState(id);
		state.skin = skin;
		StorageHelper.saveStoredState(id, state);
	}

	static storeMobileBehavior(id: string, mobileBehavior: DataTableMobileBehavior) {
		const state = StorageHelper.getStoredState(id);
		state.mobileBehavior = mobileBehavior;
		StorageHelper.saveStoredState(id, state);
	}

	static storeFilter(id: string, filter: DataTablePostBackFilterItem) {
		const state = StorageHelper.getStoredState(id);
		if (filter != null) {
			if (state.filter == null || state.filter.length == 0) {
				state.filter = [filter];
			} else {
				if (state.filter.some(p => p.PropertyName.includes(filter.PropertyName))) {
					state.filter = state.filter.filter(p => p.PropertyName != filter.PropertyName);
				}

				state.filter.push(filter);
			}
		} else if (state.filter != null) {
			delete state.filter;
		}

		StorageHelper.saveStoredState(id, state);
	}
}

export interface TableColumn {
	id: string;
	caption: string;
	cssClass?: string;
	visible?: boolean;
	sortable?: boolean;
	searchable?: boolean;
	filterType?: DataTableFilterItemType;
	filterItems?: DataTableFilterItemCollection;
	filterAllowExclusivity?: boolean;
	customFilterValue?: (row) => string;
	mobileOrder?: number;
	mobileRender?: (h, row) => void;
	mobileCaption?: boolean;
	mobileVisible?: boolean;
	mobileShouldRenderRow?: (row) => boolean;
	exportInclude?: boolean;
	exportValue?: (row) => any;
	customValue?: (row) => string;
	customRender?: (h, row) => void;
	clientsideFilter?: (row: any, filterDefinition: DataTablePostBackFilterItem) => boolean;
}

export interface TableButton {
	title: string;
	icon: string;
	clicked: () => void;
	childItems?: TableButtonChildItem[];
	customRender?: (h, row) => void;
}

export interface TableButtonChildItem extends TableButton {
	isSelectable?: boolean;
	isSelected?: boolean;
}

export interface DataTableFilterItem {
	id: string;
	text: string;
}

export class DataTableFilterItemCollection extends Array<DataTableFilterItem> {
	allowExclusiveSearch: boolean = false;

	constructor(allowExclusive: boolean, items: DataTableFilterItem[]) {
		super();
		this.allowExclusiveSearch = allowExclusive;
		(items || []).forEach((item) => {
			this.push(item);
		});
	}
}

@Component
class TableButtonComponent extends TsxComponent<TableButton> implements TableButton {
	@Prop() title!: string;
	@Prop() icon!: string;
	@Prop() cssClass!: string;
	@Prop() clicked: () => void;
	@Prop() customRender: (h, row) => void;
	@Prop() childItems!: TableButtonChildItem[];
	randomUUID: string = `ddl-${PortalUtils.randomString(10)}`;

	render(h) {
		if (isNullOrEmpty(this.childItems)) {
			return (
				<button class={`dt-button${this.cssClass != null ? ` ${this.cssClass}` : ''}`} title={this.title} onClick={this.clicked}>
					<span>
						<i class={this.icon}></i>
					</span>
				</button>
			);
		} else {
			return (
				<span class="nav-item dt-button dropdown">
					<span class="dropdown-toggle" id={this.randomUUID} data-toggle="dropdown" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
						<i class={this.icon}></i>
					</span>
					<div class="dropdown-menu dropdown-menu-right" aria-labelledby={this.randomUUID}>
						{this.childItems.map(childItem => (
							<a class={`dropdown-item${childItem.isSelectable == true ? ' dropdown-selectable-section' : ''}`} href="javascript:" onClick={() => childItem.clicked()}>
								<span>{childItem.title}</span>
								{childItem.isSelected && <i class="fas fa-check"></i>}
							</a>
						))}
					</div>
				</span>
			);
		}
	}
}

@Component
class DataTableComponent extends TsxComponent<DataTableArgs> implements DataTableArgs {
	@Prop() id!: string;
	@Prop() apiClient!: IWebApiClient;
	@Prop() apiMethod!: WebClientApiMethod;
	@Prop() allowMassOperations!: boolean;
	@Prop() allowExport!: boolean;
	@Prop() apiArgs!: any;
	@Prop() exportConfig?: DataTableExportConfig;
	@Prop() paginations?: number[];
	@Prop() paginationLengthProp!: number;
	@Prop() cssClass!: string;
	@Prop() autoFilter!: boolean;
	@Prop() insetTop!: boolean;
	@Prop() topVisible!: boolean;
	@Prop() bottomVisible!: boolean;
	@Prop() skin!: DataTableSkin;
	@Prop() preserveOrderBy!: boolean;
	@Prop() preserveFilter!: boolean;
	@Prop() sortableRows!: boolean;
	@Prop() filterMode!: DataTableFilterMode;
	@Prop() fullSizeTable!: boolean;
	@Prop() fullSizeHasButtonBelow!: boolean;
	@Prop() mobileBehavior!: DataTableMobileBehavior;
	@Prop() mobileModeCustomRender!: (h, columns: TableColumn[], rows: any) => void;
	@Prop() mobileModeRowIcon!: string;
	@Prop() mobileModeShouldAutoCollapse!: boolean;
	@Prop() handleInitialFilter!: boolean;
	@Prop() massOperationOptions!: MassOperationItem[];
	@Prop() checkboxesVisible!: boolean;
	@Prop() checkboxesTitle!: string;
	@Prop() checkboxButtonsVisible!: boolean;
	@Prop() rowIndexMode!: RowIndexMode;
	@Prop() fulltextPlaceholder!: string;
	@Prop() timezoneGmtOffset!: number;
	@Prop() timezoneDstOffset!: number;
	@Prop() timeFilterShiftTimezone!: boolean;
	@Prop() columns!: TableColumn[];
	@Prop() buttons!: TableButton[];
	@Prop() rowClicked: (row: any) => void;
	@Prop() rowCssClass: (row: any) => string;
	@Prop() rowCheckstateChanged: (row: any, checked: boolean, selectedRows: any[]) => void;
	@Prop() massOperationsCancelClicked?: () => void;
	@Prop() parseLoadedRowset?: (serverResp: any) => DataTableLoadedRowset;
	@Prop() customAjaxCall?: (args: DataTablePostBackData) => Promise<DataTableLoadedRowset>;
	@Prop() sortComplete?: (args: DataTableOnSortedArgs) => void;
	@Prop() autoFetch!: boolean;
	@Prop() hidePagination!: boolean;
	paginationLength: number = this.paginationLengthProp ?? 50;
	paginationPosition: number = 1;
	isLoading: boolean = false;
	initialized: boolean = false;
	initDataLoaded: boolean = false;
	currentMobileBehavior: DataTableMobileBehavior = null;
	currentSkin: DataTableSkin = null;
	currentAdvancedFilterState: any = {};
	fullTextQuery: string;

	rows: any[] = [];
	loadedRows: any[];
	tableFilterTimeout: any = null;
	filterArr: DataTablePostBackFilterItem[] = [];
	sessionId: number = null;
	totalCount: number = 0;
	totalFilteredCount: number = 0;
	hasMore: boolean = false;
	colSortOrder: string[] = null;
	enforceHeaderRedraw: boolean = false;
	enforceBodyRedraw: boolean = false;
	checkboxesShown: boolean = false;
	sortDefinition: SortDefinition = null;
	activeBreakPoint: DataTableBreakpoint = null;
	markInstance: any = null;

	mounted(): void {
		this.checkboxesShown = this.checkboxesVisible == true;
		this.currentSkin = this.skin;
		this.currentMobileBehavior = this.mobileBehavior;
		this.performColumnRefresh();
		this.handleWindowResized();
		globalState.addEventListener(
			'resize',
			this.handleWindowResized,
			true,
		);

		if (DataTableConfig.filterMarking) {
			this.markInstance = new Mark(this.$el);
		}

		if (this.handleInitialFilter) {
			this.parseInitialFilter();
		}

		if (this.fullSizeTable) {
			const htmlElem = document.documentElement;
			htmlElem.classList.add('has-dt-fullsize');

			if (PortalUtils.treatAsMobileDevice()) {
				htmlElem.classList.add('has-dt-fullsize-mobile');
			}

			if (PortalUtils.isIOS()) {
				htmlElem.classList.add('has-dt-fullsize-ios');
			}
		}

		if (this.sortableRows == true) {
			this.initRowSortable();
		}

		if (this.preserveFilter == true) {
			this.initRowFilters();
		}

		if (this.autoFetch != false) {
			setTimeout(async () => {
				this.loadInitialData();
			}, 300);
		} else {
			this.initialized = true;
		}
	}

	updated(): void {
		if (this.autoFetch != false) {
			this.loadInitialData();
		} else {
			this.initialized = true;
		}
	}

	beforeUnmount(): void {
		if (this.fullSizeTable) {
			this.removeFullsizeModeLayoutCssClass();

			document.documentElement.classList.remove(
				'has-dt-fullsize',
				'has-dt-fullsize-mobile',
				'has-dt-fullsize-ios',
			);
		}

		globalState.removeEventListener(
			'resize',
			this.handleWindowResized,
			true,
		);
	}

	loadInitialData(): void {
		if (this.apiArgs == null) {
			return;
		}

		if (!this.initialized && !this.initDataLoaded) {
			this.initDataLoaded = true;
			this.reloadDataPromise().then(() => {
				setTimeout(() => {
					this.initialized = true;
				}, 1);
			});
		}
	}

	parseInitialFilter(): void {
		const self = this;
		const filterBy = QueryStringUtils.getString('filterBy');
		const orderBy = QueryStringUtils.getString('orderBy');
		const getColumn = function (id: string) {
			let column = self.columns.filter(p => p.id == id)[0];
			if (column == null && id.toLowerCase() == 'name') {
				column = self.columns.filter(p => (p as any).customField != null && (p as any).customField.MappingType == 1)[0];
			}

			if (column == null && id.toLowerCase() == 'surname') {
				column = self.columns.filter(p => (p as any).customField != null && (p as any).customField.MappingType == 2)[0];
			}

			return column;
		};

		const getParamArr = function (val: string): Array<Array<string>> {
			let arrOfArr: Array<Array<string>>;
			try {
				arrOfArr = JSON.parse(val);
			} catch (e) { }

			if (arrOfArr == null) {
				return null;
			}

			if (arrOfArr.length && arrOfArr.splice) {
				if (PortalUtils.isString(arrOfArr[0])) {
					arrOfArr = [arrOfArr as any];
				}

				return arrOfArr;
			}

			return null;
		};

		if (!isNullOrEmpty(filterBy)) {
			const arrOfArr = getParamArr(filterBy);

			if (!isNullOrEmpty(arrOfArr)) {
				arrOfArr.forEach((pair) => {
					const column = getColumn(pair[0]);
					if (column != null) {
						if (column.filterType == null || column.filterType == DataTableFilterItemType.Text) {
							this.addFilterItem(column, {
								PropertyName: column.id,
								ContainsValue: pair[1],
								FilterType: DataTableFilterItemType.Text,
							});
						} else if (column.filterType == DataTableFilterItemType.Dropdown) {
							this.addFilterItem(column, {
								PropertyName: column.id,
								ValueArr: [pair[1]],
								ValueArrStrategy: MultiSelectExclusivity.Exclusive,
								FilterType: DataTableFilterItemType.Dropdown,
							});
						}
					}
				});
			}
		}

		if (!isNullOrEmpty(orderBy)) {
			const arrOfArr = getParamArr(orderBy);

			if (!isNullOrEmpty(arrOfArr)) {
				arrOfArr.forEach((pair) => {
					const column = getColumn(pair[0]);
					if (column != null) {
						this.sortDefinition = {
							columnId: column.id,
							direction: pair[1] == 'desc' ? DataTableSortDirection.Descending : DataTableSortDirection.Ascending,
						};
					}
				});
			}
		}
	}

	async ensureMassPaginationConsent(): Promise<DialogResult> {
		const selectedCount = this.getSelectedRows().length;
		const paginationLength = this.getPaginationLength();
		if (paginationLength < 0) {
			return DialogResult.Confirm;
		}

		if (this.isCountlessMode()) {
			// Count-free mode: no grand total; a single page with no further rows needs no consent.
			if (!this.hasMore && this.paginationPosition <= 1) {
				return DialogResult.Confirm;
			}
		} else if (this.totalCount < paginationLength) {
			return DialogResult.Confirm;
		}

		if (selectedCount < paginationLength - 7) {
			return DialogResult.Confirm;
		}

		const knownRows = (this.paginationPosition - 1) * paginationLength + (this.rows?.length || 0);
		const totalLabel = this.isCountlessMode() ? `${knownRows}${this.hasMore ? '+' : ''}` : this.totalCount.toString();
		const continueLabel = `${PowerduckState.getResourceValue('continueLabel')}&nbsp;&nbsp;<i class="icon icon-arrow-right-circle"></i>`;
		const messageHtml = PowerduckState.getResourceValue('dtMassOperationWarningText').replace('{0}', selectedCount.toString()).replace('{1}', totalLabel);
		const dialogResult = await DialogUtils.showConfirmDialog(
			PowerduckState.getResourceValue('warning'),
			messageHtml,
			continueLabel,
			PowerduckState.getResourceValue('cancel'),
			DialogIcons.Warning,
		);
		return dialogResult;
	}

	performColumnRefresh(): void {
		const savedState = StorageHelper.getStoredState(this.id);
		this.colSortOrder = savedState.sortOrder;
		this.handleColumnsVisibility(savedState, this.columns);
		this.paginationLength = savedState.paginationLength;

		// A stored page size outlives the option list it was picked from. Once a table stops
		// offering an option — e.g. one that drops the unlimited "All" (-1) — the stored value
		// would otherwise be restored on every mount, with no matching <option> left for the
		// user to switch away from. Discard a size the table no longer offers.
		if (this.paginationLength != null && !this.getEffectivePaginations().includes(this.paginationLength)) {
			this.paginationLength = this.paginationLengthProp ?? 50;
		}

		if (savedState.skin != null) {
			this.currentSkin = savedState.skin;
		}

		if (savedState.mobileBehavior != null) {
			this.currentMobileBehavior = savedState.mobileBehavior;
		}

		if (savedState.orderBy != null) {
			this.sortDefinition = savedState.orderBy;
		}

		if (savedState.filter != null) {
			this.filterArr = savedState.filter;
		}
	}

	initRowSortable(): void {
		const args = {
			animation: 150,
			handle: '.dt-col-index',
			onEnd: (evt) => {
				this.sortComplete(evt);
			},
		} as any;

		this.$nextTick(() => {
			this.$nextTick(() => {
				new Sortable(this.$el.querySelector('tbody'), args);
			});
		});
	}

	reorderColumns(sortOrder: string[], columns: TableColumn[]): void {
		if (sortOrder == null || sortOrder.length == 0 || columns.length == 0) {
			return;
		}

		const rank = (column: TableColumn): number => {
			const index = sortOrder.indexOf(column.id);

			return index === -1 ? Number.MAX_SAFE_INTEGER : index;
		};

		const reordered = columns
			.map((column, index) => ({ column, index }))
			.sort((a, b) => (rank(a.column) - rank(b.column)) || (a.index - b.index))
			.map(item => item.column);

		columns.splice(
			0,
			columns.length,
			...reordered,
		);
	}

	initRowFilters(): void {
		const savedState = StorageHelper.getStoredState(this.id);

		if (savedState.filter == null) {
			return;
		}

		for (const filterItem of savedState.filter) {
			if (filterItem.FilterType == DataTableFilterItemType.None) {
				continue;
			} else if (filterItem.FilterType == DataTableFilterItemType.Dropdown) {
				this.currentAdvancedFilterState[filterItem.PropertyName] = filterItem.ValueArr;
			} else if (filterItem.FilterType == DataTableFilterItemType.DateRange || filterItem.FilterType == DataTableFilterItemType.DateRangeWithTime) {
				this.currentAdvancedFilterState[filterItem.PropertyName] = {
					startTime: filterItem.DateFrom,
					endTime: filterItem.DateTo,
				};
			} else if (filterItem.FilterType == DataTableFilterItemType.NumericRange) {
				this.currentAdvancedFilterState[filterItem.PropertyName] = {
					from: filterItem.NumFrom,
					to: filterItem.NumTo,
				};
			} else if (filterItem.FilterType == DataTableFilterItemType.Text) {
				this.currentAdvancedFilterState[filterItem.PropertyName] = filterItem.ContainsValue;
			}
		}
	}

	handleWindowResized(): void {
		const width = globalState.innerWidth;
		const breakpointMode = width > 767 ? DataTableBreakpoint.Desktop : DataTableBreakpoint.Mobile;

		if (breakpointMode != this.activeBreakPoint) {
			this.activeBreakPoint = breakpointMode;
			this.handleMobileBreakpointChanged();
		}
	}

	handleMobileBreakpointChanged() {
		if (this.fullSizeTable && PortalUtils.treatAsMobileDevice()) {
			this.removeFullsizeModeLayoutCssClass();
			document.documentElement.classList.add(`dt-fullsize-beh-${this.getMobileBehavior()}`, `dt-fullsize-skin-${this.getSkin()}`);
		}
	}

	removeFullsizeModeLayoutCssClass() {
		if (this.fullSizeTable && PortalUtils.treatAsMobileDevice()) {
			document.documentElement.classList.remove(
				`dt-fullsize-beh-${DataTableMobileBehavior.Compact}`,
				`dt-fullsize-beh-${DataTableMobileBehavior.MobileLayout}`,
				`dt-fullsize-beh-${DataTableMobileBehavior.VerticalTransform}`,
				`dt-fullsize-skin-${DataTableSkin.Default}`,
				`dt-fullsize-skin-${DataTableSkin.Compact}`,
			);
		}
	}

	handleColumnsVisibility(savedState: StoredState, columns: TableColumn[]): void {
		if (savedState.hiddenColumns != null) {
			savedState.hiddenColumns.forEach((hc) => {
				const colItem = columns.filter(p => p.id == hc)[0];
				if (colItem != null) {
					colItem.visible = false;

					if ((colItem as any)._enforceVisible != null) {
						delete (colItem as any)._enforceVisible;
					}
				}
			});
		}

		if (savedState.visibleColumns != null) {
			savedState.visibleColumns.forEach((vc) => {
				const colItem = columns.filter(p => p.id == vc)[0];
				if (colItem != null) {
					colItem.visible = true;
					(colItem as any)._enforceVisible = true;
				}
			});
		}
	}

	getPaginationLength(): number {
		return this.paginationLength || 50;
	}

	getEffectivePaginations(): number[] {
		return this.paginations || DEFAULT_PAGINATIONS;
	}

	getTableColumnHeaderCssClass(column: TableColumn, index: number): string {
		return `dt-header-${column.id.split(':').join('_').toLowerCase()} dt-header-i${index} ${column.cssClass || ''}${column.sortable == false ? ' dt-header-notsortable' : ''}`;
	}

	getTableColumnHeaderFilterCssClass(column: TableColumn, index: number): string {
		return `header-filter-cell dt-filter-${column.id.split(':').join('_').toLowerCase()} dt-filter-i${index} dt-filtertype-${this.getFilterTypeName(column.filterType)}`;
	}

	getTableItemCssClass(column: TableColumn, index: number): string {
		return `dt-col-${column.id.split(':').join('_').toLowerCase()} dt-col-i${index} ${column.cssClass || ''}`;
	}

	getFilterTypeName(filterType: DataTableFilterItemType): string {
		switch (filterType) {
			case DataTableFilterItemType.None:
				return 'none';

			case null:
			case undefined:
			case DataTableFilterItemType.Text:
				return 'text';

			case DataTableFilterItemType.Dropdown:
				return 'dropdown';

			case DataTableFilterItemType.DateRange:
				return 'daterange';

			case DataTableFilterItemType.DateRangeWithTime:
				return 'daterange-time';

			case DataTableFilterItemType.NumericRange:
				return 'numericrange';

			default:
		}
	}

	getRowIdProp(row: any): string {
		if (row?.Id != null) {
			return 'Id';
		}

		return 'id';
	}

	getRowId(row: any): number {
		return row[this.getRowIdProp(row)];
	}

	getSelectedRows(): any[] {
		const self = this;
		const selectedRows = [];

		if (isNullOrEmpty(self.rows)) {
			return [];
		}

		const rowIdProp = this.getRowIdProp(self.rows[0]);

		document.querySelectorAll<HTMLInputElement>('.dt-selection-checkbox input:checked').forEach((input) => {
			const rowId = Number(input.closest('.dt-selection-checkbox')?.getAttribute('data-id'));
			const existingRow = selectedRows.filter(p => p[rowIdProp] == rowId)[0];

			if (existingRow == null) {
				const selectedRow = self.rows.filter(p => p[rowIdProp] == rowId)[0];
				if (selectedRow != null) {
					selectedRows.push(selectedRow);
				}
			}
		});

		return selectedRows;
	}

	unmarkSelection(cb: () => void): void {
		if (DataTableConfig.filterMarking) {
			this.markInstance.unmark({
				done: () => {
					this.enforceBodyRedraw = true;
					this.$nextTick(() => {
						this.enforceBodyRedraw = false;
						this.$nextTick(() => {
							cb();
						});
					});
				},
			});
		} else {
			cb();
		}
	}

	markSelection(): void {
		const mySelf = this;
		const escapeRegExp = function (string) {
			return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
		};

		const getValueArr = function (filterItem: DataTablePostBackFilterItem): string[] {
			if (!isNullOrEmpty(filterItem.EqualsValue)) {
				return [filterItem.EqualsValue];
			} else if (!isNullOrEmpty(filterItem.ContainsValue)) {
				return [filterItem.ContainsValue];
			} else if (!isNullOrEmpty(filterItem.ValueArr)) {
				return [];
				// return filterItem.ValueArr;
			}
		};

		let regexBuilder = '';
		const markArr = [];
		if (this.filterArr != null) {
			this.filterArr.forEach((filterItem) => {
				if (regexBuilder.length > 0) {
					regexBuilder += '|';
				}

				const columnId = filterItem.PropertyName?.toLowerCase();
				const tds = Array.from((mySelf.$el as HTMLElement).querySelectorAll(`td.dt-col-${columnId}`));
				if (tds == null || tds.length == 0) {
					return;
				}

				(getValueArr(filterItem) || []).forEach((filterValue) => {
					markArr.push({
						value: escapeRegExp(filterValue),
						elements: tds,
					});
				});
			});
		}

		if (!isNullOrEmpty(this.fullTextQuery)) {
			const value = escapeRegExp(this.fullTextQuery);
			const tds = Array.from((mySelf.$el as HTMLElement).querySelectorAll(`td`));
			regexBuilder += value;

			markArr.push({
				value,
				elements: tds,
			});
		}

		if (DataTableConfig.filterMarking) {
			this.unmarkSelection(() => {
				for (const item of markArr) {
					this.markInstance.mark(item.value, {
						filter: (node) => {
							// mark.js hands us the text node of a match; walk up to an Element
							// before calling closest('td').
							const startElem: Element | null = node.nodeType === 1
								? (node as Element)
								: node.parentElement;
							const parentTd = startElem?.closest('td');
							if (!parentTd) {
								return false;
							}

							const matchesColumn = item.elements?.some(el => parentTd?.isEqualNode(el));
							const matchesText = parentTd?.textContent?.toLowerCase().includes(item.value.toLowerCase());
							return matchesColumn && matchesText;
						},
					});
				}
			});
		}
	}

	addFilterItem(dtColumn: TableColumn, newFilter: DataTablePostBackFilterItem): void {
		const existingIndex = this.filterArr.findIndex(p => p.PropertyName == dtColumn.id);
		const hasValue
            = !isNullOrEmpty(newFilter.ContainsValue)
            	|| !isNullOrEmpty(newFilter.EqualsValue)
            	|| !isNullOrEmpty(newFilter.ValueArr)
            	|| newFilter.DateFrom != null
            	|| newFilter.DateTo != null
            	|| newFilter.NumFrom != null
            	|| newFilter.NumTo != null;

		if (existingIndex === -1) {
			if (hasValue) {
				this.filterArr.push(newFilter);
			}

			return;
		}

		this.filterArr.splice(existingIndex, 1);
		if (hasValue) {
			this.filterArr.push(newFilter);
		}
	}

	filterDataHaveChanged(dtColumn: TableColumn, filterItem: DataTablePostBackFilterItem): boolean {
		const currentData: DataTablePostBackFilterItem = this.filterArr.filter(p => p.PropertyName == dtColumn.id)[0] || ({} as any);
		return JSON.stringify(currentData) != JSON.stringify(filterItem);
	}

	handleClientsideFilter(markResults?: boolean, resetPagination?: boolean): boolean {
		if (this.filterMode == DataTableFilterMode.Clientside) {
			if (resetPagination) {
				this.paginationPosition = 1;
			}

			this.unmarkSelection(() => {
				this.performClientsideFilter();

				if (markResults) {
					this.markSelection();
				}
			});

			return true;
		}

		return false;
	}

	performFullTextSearch(e): void {
		if (this.tableFilterTimeout != null) {
			clearTimeout(this.tableFilterTimeout);
		}

		this.fullTextQuery = e.target.value;

		e = e || globalState.event;
		let charCode: number = -1;
		if (e.which) {
			charCode = e.which;
		} else if (e.keyCode) {
			charCode = e.keyCode;
		}

		if (charCode == 13) {
			if (this.handleClientsideFilter(true, true)) {
				return;
			}

			this.reloadDataAfterFilterChange();
		} else {
			this.tableFilterTimeout = setTimeout(() => {
				if (this.handleClientsideFilter(true, true)) {
					return;
				}

				this.reloadDataAfterFilterChange();
			}, 750);
		}
	}

	performFullTextClear(e): void {
		// The native "X" clear button of <input type="search"> fires `input`/`search`
		// events but NOT `keyup`, so `performFullTextSearch` (bound to onKeyup) never runs
		// and the table keeps its stale filtered rows (QA_GO-121). Re-run the search only
		// for the clear case (empty value); `search` also fires on Enter, which onKeyup
		// already handles, so this guard avoids a redundant reload across all admin tables.
		if (!isNullOrEmpty(e.target.value)) {
			return;
		}

		this.performFullTextSearch(e);
	}

	performInputFilter(dtColumn: TableColumn, e): void {
		if (this.tableFilterTimeout != null) {
			clearTimeout(this.tableFilterTimeout);
		}

		this.currentAdvancedFilterState[dtColumn.id] = e;

		const filterItem = {
			PropertyName: dtColumn.id,
			ContainsValue: (e || ''),
			FilterType: DataTableFilterItemType.Text,
		};

		this.addFilterItem(dtColumn, filterItem);

		if (this.preserveFilter == true) {
			if (isNullOrEmpty(filterItem.ContainsValue)) {
				StorageHelper.storeFilter(this.id, null);
			} else {
				StorageHelper.storeFilter(this.id, filterItem);
			}
		}

		if (this.handleClientsideFilter(true, true)) {
			return;
		}

		e = (e) || globalState.event;
		let charCode: number = -1;
		if (e.which) {
			charCode = e.which;
		} else if (e.keyCode) {
			charCode = e.keyCode;
		}

		if (charCode == 13) {
			this.reloadDataAfterFilterChange();
		} else {
			this.tableFilterTimeout = setTimeout(() => {
				this.reloadDataAfterFilterChange();
			}, 750);
		}
	}

	performSelectionFilter(
		dtColumn: TableColumn,
		data: DataTableFilterItem[],
		exclusivity: MultiSelectExclusivity,
	): void {
		this.currentAdvancedFilterState[dtColumn.id] = data;

		const filterValue: DataTablePostBackFilterItem = {
			PropertyName: dtColumn.id,
			ValueArr: (data || []).map(p => p.id),
			ValueArrStrategy: exclusivity == MultiSelectExclusivity.Exclusive ? MultiSelectExclusivity.Exclusive : MultiSelectExclusivity.Inclusive,
			FilterType: DataTableFilterItemType.Dropdown,
		};

		if (!this.filterDataHaveChanged(dtColumn, filterValue)) {
			return;
		}

		if (data == null || data.length == 0) {
			this.addFilterItem(dtColumn, {} as any);
		} else {
			this.addFilterItem(dtColumn, filterValue);
		}

		if (this.preserveFilter == true) {
			if (data == null || data.length == 0) {
				StorageHelper.storeFilter(this.id, null);
			} else {
				StorageHelper.storeFilter(this.id, filterValue);
			}
		}

		if (this.handleClientsideFilter(false, true)) {
			return;
		}

		// Immediate reload if changed, delay handled by the Dropdown component
		this.reloadDataAfterFilterChange();
	}

	performDateRangeFilter(dtColumn: TableColumn, data: DaterangeChangedArgs): void {
		this.currentAdvancedFilterState[dtColumn.id] = data;

		let startDate = data?.startTime;
		let endDate = data?.endTime;
		if (data == null || data.startTime == null || data.endTime == null) {
			this.addFilterItem(dtColumn, {} as any);
		}

		if (data != null && this.timeFilterShiftTimezone) {
			let dstOffset = this.timezoneDstOffset;
			let gmtOffset = this.timezoneGmtOffset;

			if (dstOffset == null && this.rows != null) {
				dstOffset = (this.rows.filter(p => p.TimezoneDstOffset != null && p.TimezoneDstOffset != 0)[0] || {}).TimezoneDstOffset;
				if (dstOffset == null) {
					dstOffset = 0;
				}
			}

			if (gmtOffset == null && this.rows != null) {
				gmtOffset = (this.rows.filter(p => p.TimezoneGmtOffset != null && p.TimezoneGmtOffset != 0)[0] || {}).TimezoneGmtOffset;
				if (gmtOffset == null) {
					gmtOffset = 0;
				}
			}

			startDate = TimezoneHelper.getUTCFromLocalDate(
				startDate,
				dstOffset,
				gmtOffset,
			);
			endDate = TimezoneHelper.getUTCFromLocalDate(
				endDate,
				dstOffset,
				gmtOffset,
			);
		}

		const filterValue: DataTablePostBackFilterItem = {
			PropertyName: dtColumn.id,
			DateFrom: startDate,
			DateTo: endDate,
			FilterType: dtColumn.filterType === DataTableFilterItemType.DateRangeWithTime
				? DataTableFilterItemType.DateRangeWithTime
				: DataTableFilterItemType.DateRange,
		};

		if (!this.filterDataHaveChanged(dtColumn, filterValue)) {
			return;
		}

		if (data == null || data.startTime == null || data.endTime == null) {
			this.addFilterItem(dtColumn, {} as any);
		} else {
			this.addFilterItem(dtColumn, filterValue);
		}

		if (this.preserveFilter == true) {
			if (data == null || data.startTime == null || data.endTime == null) {
				StorageHelper.storeFilter(this.id, null);
			} else {
				StorageHelper.storeFilter(this.id, filterValue);
			}
		}

		if (this.handleClientsideFilter(false, true)) {
			return;
		}

		// Immediate reload if changed, delay handled by the Dropdown component
		this.reloadDataAfterFilterChange();
	}

	performNumericRangeFilter(dtColumn: TableColumn, data: { from: number | null; to: number | null } | null): void {
		this.currentAdvancedFilterState[dtColumn.id] = data;

		// A one-sided range (only from, or only to) is valid; empty means both bounds are missing.
		const isEmpty = data == null || (data.from == null && data.to == null);
		if (isEmpty) {
			this.addFilterItem(dtColumn, {} as any);
		}

		const filterValue: DataTablePostBackFilterItem = {
			PropertyName: dtColumn.id,
			NumFrom: data?.from ?? undefined,
			NumTo: data?.to ?? undefined,
			FilterType: DataTableFilterItemType.NumericRange,
		};

		if (!this.filterDataHaveChanged(dtColumn, filterValue)) {
			return;
		}

		if (isEmpty) {
			this.addFilterItem(dtColumn, {} as any);
		} else {
			this.addFilterItem(dtColumn, filterValue);
		}

		if (this.preserveFilter == true) {
			if (isEmpty) {
				StorageHelper.storeFilter(this.id, null);
			} else {
				StorageHelper.storeFilter(this.id, filterValue);
			}
		}

		if (this.handleClientsideFilter(false, true)) {
			return;
		}

		// Immediate reload if changed, delay handled by the Dropdown component
		this.reloadDataAfterFilterChange();
	}

	private parseNumericFilterValue(raw: string): number | null {
		if (isNullOrEmpty(raw)) {
			return null;
		}

		const parsed = Number(raw);
		return Number.isNaN(parsed) ? null : parsed;
	}

	performClientsideFilter(): void {
		this.rows = this.getClientsideFilteredAndSortedRows(this.loadedRows);
	}

	getClientsideFilteredAndSortedRows(rowArr: any[]): any[] {
		let newRowArr = [...rowArr];
		for (const filterData of this.filterArr) {
			const column = this.columns.find(p => p.id == filterData.PropertyName);
			const filterDef = column?.clientsideFilter;

			if (filterDef != null) {
				if (!isNullOrEmpty(filterData.ContainsValue)) {
					filterData.ContainsValue = this.normalizeStringForSearch(filterData.ContainsValue);
				}

				newRowArr = newRowArr.filter((p) => {
					return filterDef(p, filterData);
				});

				continue;
			}

			if (filterData.FilterType == DataTableFilterItemType.Text && !isNullOrEmpty(filterData.ContainsValue)) {
				const containsVal = this.normalizeStringForSearch(filterData.ContainsValue);
				newRowArr = newRowArr.filter((p) => {
					let val: any;
					if (column?.customFilterValue != null) {
						val = column.customFilterValue(p);
					} else if (column?.customValue != null) {
						val = column.customValue(p);
					} else {
						val = p[filterData.PropertyName];
					}

					if (val == null) {
						return false;
					}

					if (typeof val === 'string' || val instanceof String) {
						return this.normalizeStringForSearch(val as any).includes(containsVal);
					}

					return this.normalizeStringForSearch((p[filterData.PropertyName] || '').toString()).includes(containsVal);
				});
			} else if (filterData.FilterType == DataTableFilterItemType.Dropdown && !isNullOrEmpty(filterData.ValueArr)) {
				newRowArr = newRowArr.filter((p) => {
					let propVal: any;
					if (column?.customFilterValue != null) {
						propVal = column.customFilterValue(p);
					} else if (column?.customValue != null) {
						propVal = column.customValue(p);
					} else {
						propVal = p[filterData.PropertyName];
					}

					if (propVal == null) {
						return false;
					}

					if (filterData.ValueArr.length == 1) {
						return propVal == filterData.ValueArr[0];
					} else if (filterData.ValueArrStrategy == MultiSelectExclusivity.Inclusive) {
						return filterData.ValueArr.includes(propVal);
					}

					return true;
				});
			} else if (filterData.FilterType == DataTableFilterItemType.DateRange) {
				const fromMs = filterData.DateFrom?.toZonedDateTime('UTC')?.epochMilliseconds;
				const toMs = filterData.DateTo?.toZonedDateTime('UTC')?.epochMilliseconds;

				newRowArr = newRowArr.filter((p) => {
					let propVal: any;
					if (column?.customFilterValue != null) {
						propVal = column.customFilterValue(p);
					} else if (column?.customValue != null) {
						propVal = column.customValue(p);
					} else {
						propVal = p[filterData.PropertyName];
					}

					if (propVal == null) {
						return false;
					}

					try {
						if (typeof propVal == 'string') {
							propVal = TemporalUtils.fromString(propVal);
						}

						const timeMs = (propVal as Temporal.PlainDateTime)?.toZonedDateTime('UTC')?.epochMilliseconds;
						return fromMs <= timeMs && toMs >= timeMs;
					} catch (error) {
						return false;
					}
				});
			} else if (filterData.FilterType == DataTableFilterItemType.NumericRange) {
				newRowArr = newRowArr.filter((p) => {
					let propVal: any;
					if (column?.customFilterValue != null) {
						propVal = column.customFilterValue(p);
					} else if (column?.customValue != null) {
						propVal = column.customValue(p);
					} else {
						propVal = p[filterData.PropertyName];
					}

					if (propVal == null) {
						return false;
					}

					const numVal = typeof propVal == 'number' ? propVal : Number(propVal);
					if (Number.isNaN(numVal)) {
						return false;
					}

					if (filterData.NumFrom != null && numVal < filterData.NumFrom) {
						return false;
					}

					if (filterData.NumTo != null && numVal > filterData.NumTo) {
						return false;
					}

					return true;
				});
			}
		}

		if (!isNullOrEmpty(this.fullTextQuery)) {
			const fulltextArr: any[] = [];
			const normalizedQuery = this.normalizeStringForSearch(this.fullTextQuery);

			for (const row of newRowArr) {
				for (const propName in row) {
					const column = this.columns.find(p => p.id == propName);
					let propVal: any;

					if (column?.customFilterValue != null) {
						propVal = column.customFilterValue(row);
					} else if (column?.customValue != null) {
						propVal = column.customValue(row);
					} else {
						propVal = row[propName];
					}

					if (propVal != null) {
						if (this.normalizeStringForSearch(propVal.toString()).includes(normalizedQuery)) {
							fulltextArr.push(row);
							break;
						}
					}
				}
			}

			newRowArr = fulltextArr;
		}

		if (this.sortDefinition != null) {
			const colId = this.sortDefinition.columnId;
            type SORT_TYPE_OPTS = 'string' | 'date' | 'unknown';
            let sortType: SORT_TYPE_OPTS = 'unknown';

            for (const row of newRowArr) {
            	const item = row[colId];
            	if (item == null) {
            		continue;
            	}

            	if (typeof item === 'string' || item instanceof String) {
            		sortType = 'string';
            	} else if (item.getTime != null && item.getSeconds != null) {
            		sortType = 'date';
            	} else {
            		sortType = 'unknown';
            	}

            	break;
            }

            if (sortType == 'string') {
            	const collator = new Intl.Collator(PowerduckState.getCurrentLanguage(), {
            		numeric: true,
            		sensitivity: 'base',
            	});
            	newRowArr = newRowArr.sort((a, b) => collator.compare(a[colId], b[colId]));
            } else if (sortType == 'date') {
            	newRowArr = newRowArr.sort((a, b) => {
            		return (b[colId]?.getTime() || 0) - (a[colId]?.getTime() || 0);
            	});
            } else {
            	try {
            		newRowArr = newRowArr[sortBy](this.sortDefinition.columnId);
            	} catch (error) { }
            }

            if (this.sortDefinition.direction == DataTableSortDirection.Descending) {
            	newRowArr = newRowArr.reverse();
            }
		}

		return newRowArr;
	}

	normalizeStringForSearch(str: string): string {
		return str.toLowerCase()[latinize]().trim();
	}

	reloadData(): void {
		this.reloadDataPromise().then((_data) => {
			this.handleClientsideFilter(false);
		}).catch((_err) => {

		});
	}

	// Reset to the first page after a filter change to avoid requesting an out-of-range page.
	reloadDataAfterFilterChange(): void {
		this.paginationPosition = 1;
		this.reloadData();
	}

	reloadDataPromise(paginationPosition?: number, paginationLength?: number): Promise<any> {
		const mySelf = this;
		this.isLoading = true;

		return new Promise((resolve, reject) => {
			let loadPromise: Promise<any>;
			if (mySelf.customAjaxCall == null) {
				loadPromise = mySelf.apiClient[mySelf.apiMethod](mySelf.getAjaxArgs(paginationPosition, paginationLength));
			} else {
				loadPromise = mySelf.customAjaxCall(mySelf.getAjaxArgs(paginationPosition, paginationLength));
			}

			loadPromise.then((data: any) => {
				mySelf.unmarkSelection(() => {
					mySelf.enforceBodyRedraw = true;
					mySelf.$nextTick(() => {
						mySelf.enforceBodyRedraw = false;
						mySelf.isLoading = false;

						if (mySelf.parseLoadedRowset != null) {
							const rowset = mySelf.parseLoadedRowset(data);
							mySelf.totalCount = rowset.totalCount;
							mySelf.totalFilteredCount = rowset.totalFilteredCount;
							mySelf.hasMore = rowset.hasMore == true;
							mySelf.rows = rowset.rows;
						} else {
							mySelf.totalCount = data.totalCount ?? data.TotalCount;
							mySelf.totalFilteredCount = data.totalFilteredCount ?? data.TotalFilteredCount;
							mySelf.hasMore = (data.hasMore ?? data.HasMore) == true;
							mySelf.rows = data.rows ?? data.Rows ?? data.data;
						}

						mySelf.loadedRows = mySelf.rows;
						mySelf.$forceUpdate();
						mySelf.$nextTick(() => mySelf.markSelection());
						resolve(data);
					});
				});
			})
				.catch((err) => {
					mySelf.isLoading = false;
					NotificationProvider.showErrorMessage(PowerduckState.getResourceValue('errorFetchingData'));
					reject(err);
				});
		});
	}

	getAjaxArgs(paginationPosition?: number, paginationLength?: number): DataTablePostBackData {
		if (this.sessionId == null) {
			this.sessionId = Math.floor(Math.random() * 2147483647) + 2147483647 * -1;
		}

		const dataArgs = {} as any as DataTablePostBackData;
		if (this.apiArgs != null) {
			for (const key in this.apiArgs) {
				const kv = this.apiArgs[key];
				if (kv && {}.toString.call(kv) === '[object Function]') {
					dataArgs[key] = kv.call(this);
				} else {
					dataArgs[key] = kv;
				}
			}
		}

		dataArgs.SessionId = this.sessionId;
		dataArgs.PaginationPosition = paginationPosition || this.paginationPosition;
		dataArgs.PaginationLength = paginationLength || this.getPaginationLength();
		dataArgs.Filter = {
			FullText: this.fullTextQuery,
			FilterItems: this.filterArr,
		};

		if (this.sortDefinition != null) {
			dataArgs.Sort = {
				PropertyName: this.sortDefinition.columnId,
				Direction: this.sortDefinition.direction,
			};
		}

		if (dataArgs.PaginationLength == -1) {
			dataArgs.PaginationPosition = 1;
		}

		return dataArgs;
	}

	isMobileModeRenderType(): boolean {
		return this.activeBreakPoint == 'mobile' && this.getMobileBehavior() == DataTableMobileBehavior.MobileLayout;
	}

	forceHeaderRedraw(): void {
		this.enforceHeaderRedraw = true;
		this.$nextTick(() => (this.enforceHeaderRedraw = false));
	}

	forceBodyRedraw(): void {
		this.enforceBodyRedraw = true;
		this.$nextTick(() => (this.enforceBodyRedraw = false));
	}

	updateRows(rowArr: any[]): void {
		this.rows = rowArr;
		this.loadedRows = rowArr;
		this.forceBodyRedraw();
	}

	getTotalCount(): number {
		return this.totalCount;
	}

	setSkin(skin: DataTableSkin): void {
		StorageHelper.storeSkin(this.id, skin);
		this.currentSkin = skin;
		this.handleMobileBreakpointChanged();
	}

	setMobileBehavior(mobileBehavior: DataTableMobileBehavior): void {
		StorageHelper.storeMobileBehavior(this.id, mobileBehavior);
		this.currentMobileBehavior = mobileBehavior;
		this.handleMobileBreakpointChanged();
	}

	handleColumnsReordered(sortOrder: string[]): void {
		sortOrder = sortOrder.filter(p => p != '_sysIndexCol');
		StorageHelper.storeSortOrder(this.id, sortOrder);
		this.colSortOrder = sortOrder;
		this.reorderColumns(sortOrder, this.getVisibleColumns());
		this.forceHeaderRedraw();
	}

	async handleColumnHeaderClicked(dtColumn: TableColumn) {
		if (dtColumn.sortable == false) {
			return;
		}

		const oldDef = this.sortDefinition;
		let newDirection = DataTableSortDirection.Ascending;
		if (this.sortDefinition != null && this.sortDefinition.columnId == dtColumn.id && this.sortDefinition.direction == DataTableSortDirection.Ascending) {
			newDirection = DataTableSortDirection.Descending;
		}

		this.sortDefinition = {
			columnId: dtColumn.id,
			direction: newDirection,
		};

		if (this.preserveOrderBy == true) {
			StorageHelper.storeOrderBy(this.id, this.sortDefinition);
		}

		if (this.handleClientsideFilter(false)) {
			return;
		}

		try {
			await this.reloadDataPromise();
		} catch (e) {
			this.sortDefinition = oldDef;
		}
	}

	handleColumnVisibilityChanged(columns: TableColumn[]): void {
		StorageHelper.storeColVis(this.id, columns);
		this.forceHeaderRedraw();
	}

	handleColumnModalFilterChanged(e: ColFilterModalShowResponse): void {
		if (e.changed) {
			this.filterArr = e.filterArr;
			this.reloadDataAfterFilterChange();
		}
	}

	handleMobileRowClicked(e: any, row: any) {
		if (this.checkboxesShown) {
			let target = e.target as HTMLElement | null;
			if (target != null && !target.classList.contains('dt-mobile-row-inner')) {
				target = target.closest<HTMLElement>('.dt-mobile-row-inner');
			}

			const input = target?.querySelector<HTMLInputElement>('.dt-selection-checkbox input');
			if (input != null) {
				input.checked = !input.checked;
			}
		}

		if (this.rowClicked) {
			this.rowClicked(row);
		}
	}

	handleRowCheckboxChanged(row: any, checked: boolean) {
		if (this.rowCheckstateChanged != null) {
			this.rowCheckstateChanged(
				row,
				checked,
				this.getSelectedRows(),
			);
		}
	}

	/**
	 * Count-free mode: the server sent `totalCount: -1` + `hasMore` instead of
	 * a real total (skipping the count query). Page numbers are unknowable —
	 * the pager renders prev/next only and navigation is bounded by `hasMore`.
	 */
	isCountlessMode(): boolean {
		return this.totalCount < 0;
	}

	getLastPaginationIndex(): number {
		if (this.isCountlessMode()) {
			return this.hasMore ? this.paginationPosition + 1 : this.paginationPosition;
		}

		let retVal = this.totalFilteredCount / this.getPaginationLength();
		if (retVal % this.getPaginationLength() != 0) {
			retVal = Math.floor(retVal) + 1;
		}

		if (retVal == 0) {
			retVal = 1;
		}

		return retVal;
	}

	async handlePaginationPositionChanged(newValue: number): Promise<any> {
		if (this.paginationPosition == newValue || newValue < 1 || newValue > this.getLastPaginationIndex()) {
			return;
		}

		await this.reloadDataPromise(newValue);
		this.paginationPosition = newValue;
	}

	async handlePaginationLengthChanged(newValue: number) {
		if (this.getPaginationLength() == newValue) {
			return;
		}

		// A page-size change can leave the current page out of range — reload from page 1.
		await this.reloadDataPromise(1, newValue);
		this.paginationPosition = 1;
		StorageHelper.storePaginationLength(this.id, newValue);
		this.paginationLength = newValue;
	}

	handleCheckAllChanged(checked: boolean): void {
		document.querySelectorAll('.dt-col-indexcheckbox').forEach((wrap) => {
			wrap.querySelectorAll<HTMLInputElement>('input').forEach((input) => {
				input.checked = checked;
			});
		});

		this.handleRowCheckboxChanged(null, null);
	}

	getNameAndSurnameIndex(name: string, surname: string): string {
		let retVal: string;
		if ((name != null && name.length > 0) || (surname != null && surname.length > 0)) {
			name = name || '';
			surname = surname || '';

			if (name.length > 0) {
				if (surname.length > 0) {
					retVal = name.substring(0, 1).toUpperCase() + surname.substring(0, 1).toUpperCase();
				} else {
					retVal = name;
				}
			} else {
				retVal = surname;
			}
		}

		return retVal;
	}

	getRowIdentifier(
		columns: TableColumn[],
		row: any,
		includeColumns: boolean,
	): RowIdentifier {
		let namingData: RowIdentifier;
		if (row.CustomFields != null) {
			const nameCf = (row.CustomFields.filter(p => p.MappingType == 1)[0] || {}) as any;
			const surnameCf = (row.CustomFields.filter(p => p.MappingType == 2)[0] || {}) as any;
			namingData = new RowIdentifier(nameCf.Value, surnameCf.Value);

			if (includeColumns && namingData.fullName.length > 1) {
				namingData.dtColumns = [];

				const nameCol = columns.filter(p => p.id.includes(`cf:map:${1}`) || p.id.includes(`cf:id:${nameCf.Id}`))[0];
				if (nameCol != null) {
					namingData.dtColumns.push(nameCol);
				}

				const surnameCol = columns.filter(p => p.id.includes(`cf:map:${2}`) || p.id.includes(`cf:id:${surnameCf.Id}`))[0];
				if (surnameCol != null) {
					namingData.dtColumns.push(surnameCol);
				}
			}
		}

		if (namingData == null || (namingData.fullName.length < 2 && row.Name != null)) {
			namingData = new RowIdentifier(row.Name, row.Surname);

			if (includeColumns) {
				namingData.dtColumns = [];

				const nameCol = columns.filter(p => p.id == 'Name')[0];
				if (nameCol != null) {
					namingData.dtColumns.push(nameCol);
				}

				const surnameCol = columns.filter(p => p.id == 'Surname')[0];
				if (surnameCol != null) {
					namingData.dtColumns.push(surnameCol);
				}
			}
		}

		if (namingData == null || (namingData.fullName.length < 2 && row.Email != null)) {
			if (row.Email != null) {
				namingData = new RowIdentifier(row.Email, '');

				if (includeColumns) {
					const emailCol = columns.filter(p => p.id == 'Email')[0];
					if (emailCol != null) {
						namingData.dtColumns = [emailCol];
					}
				}
			}
		}

		if (namingData == null || (namingData.fullName.length < 2 && row.TicketName != null)) {
			if (row.TicketName != null) {
				namingData = new RowIdentifier(row.TicketName, '');

				if (includeColumns) {
					const emailCol = columns.filter(p => p.id == 'TicketName')[0];
					if (emailCol != null) {
						namingData.dtColumns = [emailCol];
					}
				}
			}
		}

		return namingData;
	}

	// Return the row number relative to the entire result set.
	private getAbsoluteRowIndex(pageRowIndex: number): string {
		const paginationLength = this.getPaginationLength();
		const offset = paginationLength < 0 ? 0 : (this.paginationPosition - 1) * paginationLength;
		return (offset + pageRowIndex + 1).toString();
	}

	getHeaderColumnIndex(
		columns: TableColumn[],
		row,
		i,
	): string {
		if (this.rowIndexMode == RowIndexMode.Index) {
			return this.getAbsoluteRowIndex(i);
		} else {
			let indexText: string;
			const rowIdentifier = this.getRowIdentifier(
				columns,
				row,
				false,
			);
			if (rowIdentifier != null) {
				indexText = this.getNameAndSurnameIndex(rowIdentifier.name, rowIdentifier.surname);
			}

			if (row.CustomFields != null) {
				const nameCf = row.CustomFields.filter(p => p.MappingType == 1)[0];
				const surnameCf = row.CustomFields.filter(p => p.MappingType == 2)[0];
				indexText = this.getNameAndSurnameIndex(nameCf != null ? nameCf.Value : '', surnameCf != null ? surnameCf.Value : '');
			}

			if (indexText != null && indexText.length > 1) {
				if (indexText.length > 2) {
					const splitArr = indexText.split(' ');
					if (splitArr.length > 1) {
						indexText = (splitArr[0].substring(0, 1) + splitArr[1].substring(0, 1)).toUpperCase();
					} else {
						indexText = indexText.substring(0, 2).toUpperCase();
					}
				}
			} else {
				indexText = this.getAbsoluteRowIndex(i);
			}

			return indexText;
		}
	}

	showColVis(): void {
		(this.$refs.colVisModal as typeof ColVisModal.prototype).show();
	}

	showFilterModal(): void {
		(this.$refs.colFilterModal as typeof ColFilterModal.prototype).show({
			filterArr: this.filterArr,
		});
	}

	exportExcel(): void {
		(this.$refs.tableExportModal as typeof TableExportModal.prototype).show({
			apiClient: this.apiClient,
			apiMethod: this.apiMethod,
			apiArgs: this.getAjaxArgs(),
			customAjaxCall: this.customAjaxCall,
			columns: this.columns,
			rows: this.rows,
			exportConfig: this.exportConfig,
			paginationOffset: this.paginationPosition - 1,
			paginationLength: this.getPaginationLength(),
			totalFilteredCount: this.filterMode == DataTableFilterMode.Clientside
				? (this.rows || []).length
				: this.totalFilteredCount,
		});
	}

	toggleCheckboxes(visible?: boolean): void {
		if (visible == null) {
			visible = !this.checkboxesShown;
		}

		this.checkboxesShown = visible;
	}

	getSkin(): DataTableSkin {
		return this.currentSkin || DataTableSkin.Default;
	}

	getMobileBehavior(): DataTableMobileBehavior {
		return this.currentMobileBehavior ?? (DataTableConfig.defaultMobileBehavior as DataTableMobileBehavior);
	}

	getVisibleColumns(): TableColumn[] {
		const state = StorageHelper.getStoredState(this.id);
		const colArr = this.columns.filter(p => p.visible != false);
		this.reorderColumns(this.colSortOrder, colArr);
		this.handleColumnsVisibility(state, colArr);
		return colArr;

		// return this.columns.filter(p => p.visible != false);
	}

	isDesiredCustomFieldColumn(
		dtCol: TableColumn,
		row: any,
		mappingType: any,
	): boolean {
		const nameCf = row.CustomFields.filter(p => p.MappingType == mappingType)[0];
		if (nameCf != null && ((dtCol.id.includes(`id:${nameCf.Id}`)) || (dtCol.id.includes(`map:${nameCf.MappingType}`)))) {
			return true;
		}

		return false;
	}

	getChangeEventDelay() {
		if (this.filterMode == DataTableFilterMode.Clientside) {
			return 0;
		} else {
			return 650;
		}
	}

	getTableColumnOrder(dtCol: TableColumn, row: any): number {
		if (dtCol.mobileOrder != null) {
			return dtCol.mobileOrder;
		}

		if (dtCol.id.includes('cf:') && row.CustomFields != null) {
			if (this.isDesiredCustomFieldColumn(
				dtCol,
				row,
				1,
			)) {
				return -10100;
			}

			if (this.isDesiredCustomFieldColumn(
				dtCol,
				row,
				1,
			)) {
				return -10000;
			}
		}

		if (dtCol.id == 'Name') {
			return -9000;
		}

		if (dtCol.id == 'TicketName') {
			return -8900;
		}

		return null;
	}

	getTableColumnOrderStyle(dtCol: TableColumn, row: any): string {
		const retVal = this.getTableColumnOrder(dtCol, row);
		if (retVal != null) {
			return `order:${retVal}`;
		}

		return null;
	}

	getMassOperationItems(): MassOperationItem[] {
		return this.massOperationOptions || [];
	}

	resolveMassOperationsTarget(): Element | null {
		if (!DataTableConfig.massOperationsButtonsTarget || !this.$el) {
			return null;
		}

		const target = DataTableConfig.massOperationsButtonsTarget(this.$el);

		if (target == null) {
			return null;
		}

		return target instanceof Element ? target : target.$el;
	}

	/**
	 * Caption of the mass-operations dropdown. Flows through the app resources
	 * like `dtMassOperationWarningText` (register `dtMassOperationsButton` in the
	 * consuming app); apps that have not registered the key keep the historical
	 * hardcoded label (`getResourceValue` returns the `{{key}}` placeholder for
	 * unregistered keys).
	 */
	getMassOperationsButtonText(): string {
		const resourceValue = PowerduckState.getResourceValue('dtMassOperationsButton');

		return resourceValue === '{{dtMassOperationsButton}}' ? 'Hromadne operacie' : resourceValue;
	}

	renderMassOpButtons(h) {
		if (!this.checkboxesShown || this.checkboxButtonsVisible == false) {
			return null;
		}

		return (
			<div class="dt-pagination-length dt-massop-btn-wrap">
				<DropdownButton text={this.getMassOperationsButtonText()} layout={ButtonLayout.Secondary}>
					{this.getMassOperationItems().map(mi =>
						(mi as any).isSeparator != true
							? (
									<DropdownButtonItem
										icon={mi.icon}
										img={mi.img}
										text={mi.text}
										href={mi.href}
										disabled={mi.disabled}
										hrefBlank={mi.hrefBlank}
										clicked={mi.clicked}
										isSelected={mi.isSelected}
										emptySpace={mi.emptySpace}
									/>
								)
							: <div class="dropdown-divider" />)}

					<DropdownButtonItem icon="icon icon-close" text={PowerduckState.getResourceValue('cancel')} clicked={() => this.handleMassOperationsCancelClicked()} />
				</DropdownButton>
				<Button text={PowerduckState.getResourceValue('cancel')} layout={ButtonLayout.Default} clicked={() => this.handleMassOperationsCancelClicked()} cssClass="dt-massop-cancelbtn" />
			</div>
		);
	}

	shouldRenderMobileRowColumn(row, dtColumn: TableColumn): boolean {
		if (dtColumn.customRender == null) {
			if (dtColumn.customValue != null) {
				return dtColumn.customValue(row) != null;
			}

			return row[dtColumn.id] != null;
		}

		if (dtColumn.mobileShouldRenderRow) {
			return dtColumn.mobileShouldRenderRow(row);
		}

		return true;
	}

	clearSelection() {
		(this.$el as HTMLElement)
			?.querySelectorAll<HTMLInputElement>('.dt-col-index input')
			.forEach((input) => {
				input.checked = false;
			});
	}

	/**
	 * Wired to both built-in Cancel triggers inside `renderMassOpButtons`. Runs
	 * the baseline reset first so the consumer's callback sees a clean table,
	 * then invokes `cancelClicked` for any consumer-specific cleanup (warning
	 * alerts, derived selection arrays, etc.).
	 */
	private handleMassOperationsCancelClicked() {
		this.clearSelection();
		this.toggleCheckboxes(false);

		if (this.massOperationsCancelClicked != null) {
			this.massOperationsCancelClicked();
		}
	}

	getFilterValue(dtColumn: TableColumn): any {
		if (this.filterArr == null) {
			return null;
		}

		const filterItem = this.filterArr.filter(p => p.PropertyName == dtColumn.id)[0];
		if (filterItem == null) {
			return null;
		}

		if (filterItem.FilterType == DataTableFilterItemType.Text) {
			return filterItem.ContainsValue;
		} else if (filterItem.FilterType == DataTableFilterItemType.Dropdown) {
			return filterItem.ValueArr;
		} else if (filterItem.FilterType == DataTableFilterItemType.DateRange || filterItem.FilterType == DataTableFilterItemType.DateRangeWithTime) {
			return {
				startTime: filterItem.DateFrom,
				endTime: filterItem.DateTo,
			};
		} else if (filterItem.FilterType == DataTableFilterItemType.NumericRange) {
			return {
				from: filterItem.NumFrom,
				to: filterItem.NumTo,
			};
		} else {
			return null;
		}
	}

	render(h) {
		const columns = this.getVisibleColumns();
		let paginationStart = (this.paginationPosition - 1) * this.getPaginationLength() + 1;
		let paginationEnd = this.isCountlessMode()
			? paginationStart + (this.rows?.length || 0) - 1
			: Math.min(paginationStart + this.getPaginationLength(), this.totalFilteredCount) - 1;
		const settingsUUID = `ddl-${PortalUtils.randomString(10)}`;
		const isMobileBreakpoint = this.activeBreakPoint == DataTableBreakpoint.Mobile;
		const hasMobLayout = this.getMobileBehavior() == DataTableMobileBehavior.MobileLayout;
		const hasTableSkin = this.getSkin() == DataTableSkin.Default;
		const massOpTarget = this.resolveMassOperationsTarget();
		const massOpContent = this.renderMassOpButtons(h);

		if (!this.isCountlessMode() && this.paginationPosition == this.getLastPaginationIndex()) {
			paginationEnd += 1;
		}

		if (this.totalFilteredCount == 0 || (this.isCountlessMode() && (this.rows?.length || 0) == 0)) {
			paginationStart = 0;
			paginationEnd = 0;
		}

		if (this.getPaginationLength() == -1) {
			paginationEnd = this.isCountlessMode() ? (this.rows?.length || 0) : this.totalFilteredCount;
		}

		return (
			<div
				class={
					`dt-root dt-mobile-mobile dt-device-${PortalUtils.treatAsMobileDevice() ? 'mobile' : 'desktop'
					}${PortalUtils.isIOS() ? ' dt-ios' : ''
					} dt-skin-${this.getSkin()
					} dt-mobbehavior-${this.getMobileBehavior()
					}${this.checkboxesShown ? ' dt-checkboxes-visible' : ''
					} dt-breakpoint-${this.activeBreakPoint
					}${this.fullSizeHasButtonBelow ? ' dt-fs-buttonsbelow' : ''
					}${this.insetTop ? ' dt-inset-top' : ''
					}${this.fullSizeTable == true ? ' dt-fullsize-table' : ''
					} ${this.cssClass || ''}`
				}
			>
				{this.topVisible != false && (
					<div class="dt-top">
						<div class="dt-top-buttonsrow">
							<div class="dt-buttons">
								<TableButtonComponent title={PowerduckState.getResourceValue('colVisLabel')} icon="fa fa-eye-slash" clicked={() => this.showColVis()} />

								{this.buttons
								&& this.buttons.map(btn => (
									<TableButtonComponent title={btn.title} icon={btn.icon} childItems={btn.childItems} customRender={btn.customRender} clicked={() => btn.clicked()} />
								))}

								{this.allowExport != false && <TableButtonComponent title="Excel export" icon="far fa-file-excel" clicked={() => this.exportExcel()} />}

								{this.allowMassOperations != false && <TableButtonComponent title={this.checkboxesTitle || ''} icon="far fa-check-square" clicked={() => this.toggleCheckboxes()} />}

								{this.activeBreakPoint == DataTableBreakpoint.Mobile && this.getMobileBehavior() == DataTableMobileBehavior.MobileLayout && (
									<TableButtonComponent title="Filter" icon="fas fa-filter" clicked={() => this.showFilterModal()} />
								)}

								<span class="nav-item dt-button dropdown" title={PowerduckState.getResourceValue('dtScreenLayout')}>
									<span class="dropdown-toggle" id={settingsUUID} data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
										<i class="fas fa-desktop"></i>
									</span>
									<div class="dropdown-menu dropdown-menu-right" aria-labelledby={settingsUUID}>
										{isMobileBreakpoint && (
											<a class="dropdown-item dropdown-selectable-section" href="javascript:" onClick={() => this.setMobileBehavior(DataTableMobileBehavior.MobileLayout)}>
												<span>{PowerduckState.getResourceValue('dtLayoutMobile')}</span>
												{hasMobLayout && <i class="fas fa-check"></i>}
											</a>
										)}

										{!isMobileBreakpoint && (
											<a class="dropdown-item dropdown-selectable-section" href="javascript:" onClick={() => this.setSkin(DataTableSkin.Default)}>
												<span>{PowerduckState.getResourceValue('dtLayoutTable')}</span>
												{hasTableSkin && <i class="fas fa-check"></i>}
											</a>
										)}

										<a
											class="dropdown-item dropdown-selectable-section"
											href="javascript:"
											onClick={() => (isMobileBreakpoint ? this.setMobileBehavior(DataTableMobileBehavior.Compact) : this.setSkin(DataTableSkin.Compact))}
										>
											<span>{isMobileBreakpoint ? PowerduckState.getResourceValue('dtLayoutTable') : PowerduckState.getResourceValue('dtLayoutCompact')}</span>
											{((isMobileBreakpoint && !hasMobLayout) || (!isMobileBreakpoint && !hasTableSkin)) && <i class="fas fa-check"></i>}
										</a>
									</div>
								</span>
							</div>
						</div>
						{this.hidePagination != true
						&& (
							<div class="dt-top-paginationrow">
								{(!this.checkboxesShown || this.checkboxButtonsVisible == false) && (
									<div class="dt-pagination-length">
										<select onChange={e => this.handlePaginationLengthChanged(Number(e.target.value))}>
											{this.getEffectivePaginations().map(item => (
												<option value={item} selected={this.getPaginationLength() == item}>
													{item != -1 ? item.toString() : PowerduckState.getResourceValue('all')}
												</option>
											))}
										</select>

										<span>{PowerduckState.getResourceValue('recordsDtLabel')}</span>
									</div>
								)}

								{!massOpTarget && massOpContent}

								<div class="dt-fulltext-search">
									<input
										type="search"
										placeholder={`${PowerduckState.getResourceValue('search')[capitalize]()}...`}
										onKeyup={e => this.performFullTextSearch(e)}
										onSearch={e => this.performFullTextClear(e)}
									/>
								</div>
							</div>
						)}
					</div>
				)}

				{this.renderTableByMode(h, columns)}

				{this.bottomVisible != false && (
					<div class="dt-bottom">
						<div class="dt-summary-wrap">
							{this.isCountlessMode()
								? `${paginationStart} - ${paginationEnd}`
								: PowerduckState.getResourceValue('dtCountText').replace('{0}', paginationStart.toString()).replace('{1}', paginationEnd.toString()).replace('{2}', this.totalFilteredCount.toString())}
							{!this.isCountlessMode() && this.totalFilteredCount != this.totalCount && (
								<span class="dt-summary-totalcount">
                                    &nbsp;
									{PowerduckState.getResourceValue('dtCountFilteredOutOf').replace('{0}', this.totalCount.toString())}
								</span>
							)}
						</div>

						<div class="dt-pagination">{this.renderPagination(h)}</div>
					</div>
				)}

				<ColVisModal ref="colVisModal" columns={this.columns} changed={e => this.handleColumnVisibilityChanged(e)} />
				<ColFilterModal ref="colFilterModal" columns={this.columns} changed={e => this.handleColumnModalFilterChanged(e)} />
				<TableExportModal ref="tableExportModal" />

				{massOpTarget && massOpContent && (
					<Teleport to={massOpTarget}>
						{massOpContent}
					</Teleport>
				)}
			</div>
		);
	}

	protected renderTableByMode(h, columns: TableColumn[]) {
		if (this.activeBreakPoint == DataTableBreakpoint.Mobile) {
			if (this.getMobileBehavior() == DataTableMobileBehavior.MobileLayout) {
				return this.renderMobileMode(h, columns);
			} else if (this.getMobileBehavior() == DataTableMobileBehavior.VerticalTransform) {
				return this.renderVerticallyTransformedTable(h, columns);
			}
		}

		return this.renderStandardTable(h, columns);
	}

	protected renderStandardTable(h, columns: TableColumn[]) {
		return (
			<div class="dt-table dt-table-mode">
				<table>
					<LoadingIndicator visible={!this.initialized} />

					{this.enforceHeaderRedraw == false && (
						<thead>
							<tr class="dt-header">
								<th data-col-id="_sysIndexCol" class="dt-header-index">
									{!this.isLoading && (
										<span>
											<span class="dt-header-indexlabel" style={this.checkboxesShown ? 'display:none' : ''}>
												#
											</span>
											<span class="dt-header-indexcheckbox" style={this.checkboxesShown ? '' : 'display:none'}>
												<CheckBox
													skin={CheckBoxSkin.Material}
													label={null}
													wrap={false}
													value={false}
													changed={(e) => {
														this.handleCheckAllChanged(e);
													}}
												/>
											</span>
										</span>
									)}

									{this.isLoading && <img src={loadingGif} />}
								</th>

								{this.renderHeaderRow(h, columns)}
							</tr>

							{this.autoFilter != false && (
								<tr class="dt-header-filter">
									<th class="dt-filter-index"></th>

									{columns.map((dtColumn, i) => (
										<th class={this.getTableColumnHeaderFilterCssClass(dtColumn, i)}>{this.renderHeaderFilter(h, dtColumn)}</th>
									))}
								</tr>
							)}
						</thead>
					)}
					<tbody>
						{this.rowClicked == null
						&& this.enforceBodyRedraw == false
						&& (this.rows || []).map((row, i) => (
							<tr class={`dt-item${this.rowCssClass == null ? '' : ` ${this.rowCssClass(row)}`}`}>
								<td class={`dt-col-index${this.sortableRows == true ? ' dt-has-sortable-rows' : ''}`}>
									<span class="dt-col-indexlabel" style={this.checkboxesShown ? 'display:none' : ''}>
										{this.getHeaderColumnIndex(
											columns,
											row,
											i,
										)}
									</span>
									<span class="dt-col-indexcheckbox dt-selection-checkbox" data-id={this.getRowId(row)} style={this.checkboxesShown ? '' : 'display:none'}>
										<CheckBox skin={CheckBoxSkin.Material} label={null} value={false} wrap={false} changed={e => this.handleRowCheckboxChanged(row, e)} />
									</span>

									{this.sortableRows == true && <i class="fas fa-sort dt-sort-handle" />}
								</td>

								{columns.map((dtColumn, j) => (
									<td class={this.getTableItemCssClass(dtColumn, j)}>{dtColumn.customRender == null ? (dtColumn.customValue == null ? row[dtColumn.id] : dtColumn.customValue(row)) : dtColumn.customRender(h, row)}</td>
								))}
							</tr>
						))}

						{this.rowClicked != null
						&& this.enforceBodyRedraw == false
						&& (this.rows || []).map((row, i) => (
							<tr
								class={`dt-item${this.rowCssClass == null ? '' : ` ${this.rowCssClass(row)}`}`}
								onClick={() => {
									this.rowClicked(row);
								}}
							>
								<td class={`dt-col-index${this.sortableRows == true ? ' dt-has-sortable-rows' : ''}`}>
									<span class="dt-col-indexlabel" style={this.checkboxesShown ? 'display:none' : ''}>
										{this.getHeaderColumnIndex(
											columns,
											row,
											i,
										)}
									</span>
									<span class="dt-col-indexcheckbox dt-selection-checkbox" data-id={this.getRowId(row)} style={this.checkboxesShown ? '' : 'display:none'}>
										<CheckBox skin={CheckBoxSkin.Material} label={null} value={false} wrap={false} changed={e => this.handleRowCheckboxChanged(row, e)} />
									</span>

									{this.sortableRows == true && <i class="fas fa-sort dt-sort-handle" />}
								</td>

								{columns.map((dtColumn, j) => (
									<td class={this.getTableItemCssClass(dtColumn, j)}>{dtColumn.customRender == null ? (dtColumn.customValue == null ? row[dtColumn.id] : dtColumn.customValue(row)) : dtColumn.customRender(h, row)}</td>
								))}
							</tr>
						))}

						{this.isLoading && (
							<tr>
								<td colspan="99999">
									<LoadingIndicator visible={true} ensureMinHeight={true} />
								</td>
							</tr>
						)}

						{!this.isLoading && (this.totalFilteredCount == 0 || (this.isCountlessMode() && (this.rows?.length || 0) == 0)) && (
							<tr>
								<td class="dt-no-results" colspan="99999">
									<div class="dt-no-results-inner">
										<div>{PowerduckState.getResourceValue('noResultsFound')}</div>
									</div>
								</td>
							</tr>
						)}
					</tbody>
				</table>
			</div>
		);
	}

	protected renderHeaderRow(h, columns: TableColumn[]) {
		return columns.map((dtColumn, i) => (
			<th
				data-col-id={dtColumn.id}
				onMousedown={e =>
					ReorderProvider.handleMouseDown(
						e,
						(sortOrder) => {
							this.handleColumnsReordered(sortOrder);
						},
						() => {
							this.handleColumnHeaderClicked(dtColumn);
						},
					)}
				onTouchstart={e =>
					ReorderProvider.handleMouseDown(
						e,
						(sortOrder) => {
							this.handleColumnsReordered(sortOrder);
						},
						() => {
							this.handleColumnHeaderClicked(dtColumn);
						},
					)}
				class={this.getTableColumnHeaderCssClass(dtColumn, i)}
			>
				{dtColumn.caption}
				{this.sortDefinition && this.sortDefinition.columnId == dtColumn.id && (
					<span class="dt-sort">
						<i class={`fas ${this.sortDefinition.direction == DataTableSortDirection.Ascending ? 'fa-sort-up' : 'fa-sort-down'} float-right`} aria-hidden="true"></i>
					</span>
				)}
			</th>
		));
	}

	protected renderHeaderFilter(h, dtColumn: TableColumn) {
		const filterType = dtColumn.filterType;
		const filterValue = this.getFilterValue(dtColumn);
		const closeIcon = !isNullOrEmpty(filterValue) ? 'icon icon-close' : null;
		const appendIconClicked = () => {
			if (this.currentAdvancedFilterState[dtColumn.id] != null) {
				this.currentAdvancedFilterState[dtColumn.id] = null;
			}

			if (this.filterArr != null) {
				this.filterArr = this.filterArr.filter(p => p.PropertyName != dtColumn.id);
			}

			if (filterType == DataTableFilterItemType.Text || filterType == null) {
				this.performInputFilter(dtColumn, null);
			} else if (filterType == DataTableFilterItemType.DateRange || filterType == DataTableFilterItemType.DateRangeWithTime) {
				this.performDateRangeFilter(dtColumn, null);
			} else if (filterType == DataTableFilterItemType.Dropdown) {
				this.performSelectionFilter(
					dtColumn,
					[],
					null,
				);
			} else if (filterType == DataTableFilterItemType.NumericRange) {
				this.performNumericRangeFilter(dtColumn, null);
			}
		};

		if (filterType == DataTableFilterItemType.None) {
			return '';
		} else if (filterType == DataTableFilterItemType.Text || filterType == null) {
			return (<TextBox updateMode="input" cssClass="dt-filter-input" wrap={false} label={null} autoCompleteEnabled={false} placeholder={`${PowerduckState.getResourceValue('search')[capitalize]()}...`} value={this.currentAdvancedFilterState[dtColumn.id]} changed={(e) => { this.performInputFilter(dtColumn, e); }} appendIcon={closeIcon} appendIconClicked={appendIconClicked} />);
		} else if (filterType == DataTableFilterItemType.Dropdown) {
			return (
				<div class="dt-filter-dropdown">
					<DropdownList
						label={null}
						containerCssClass="dt-filter-dropdown-container"
						options={dtColumn.filterItems}
						wrap={false}
						disableSearch={false} /* search option list; not gated by column.searchable (global search) */
						dropdownAutoWidth={true}
						multiselect={true}
						multiselectMode={MultiselectMode.Checkboxes}
						mobileShortMode={true}
						allowExclusiveSearch={dtColumn.filterAllowExclusivity}
						changedEventDelay={this.getChangeEventDelay()}
						selected={this.currentAdvancedFilterState[dtColumn.id]}
						changed={(changeArr, exclusivity) => {
							this.performSelectionFilter(
								dtColumn,
								changeArr,
								exclusivity,
							);
						}}
						appendIcon={closeIcon}
						appendIconClicked={appendIconClicked}
					/>
				</div>
			);
		} else if (filterType == DataTableFilterItemType.DateRange) {
			return (<DaterangePicker label={null} cssClass="dt-filter-input" wrap={false} placeholder={`${PowerduckState.getResourceValue('search')[capitalize]()}...`} value={this.currentAdvancedFilterState[dtColumn.id]} changed={(e) => { this.performDateRangeFilter(dtColumn, e); }} appendIcon={closeIcon} appendIconClicked={appendIconClicked} />);
		} else if (filterType == DataTableFilterItemType.DateRangeWithTime) {
			return (<DaterangePicker label={null} cssClass="dt-filter-input" wrap={false} placeholder={`${PowerduckState.getResourceValue('search')[capitalize]()}...`} value={this.currentAdvancedFilterState[dtColumn.id]} changed={(e) => { this.performDateRangeFilter(dtColumn, e); }} appendIcon={closeIcon} appendIconClicked={appendIconClicked} enableTime={true} autoClose={false} />);
		} else if (filterType == DataTableFilterItemType.NumericRange) {
			const numRange = this.currentAdvancedFilterState[dtColumn.id];
			const fromRes = PowerduckState.getResourceValue('from');
			const toRes = PowerduckState.getResourceValue('to');
			const searchPlaceholder = PowerduckState.getResourceValue('search')[capitalize]();
			const fromPlaceholder = isNullOrEmpty(fromRes) || fromRes.startsWith('{{') ? searchPlaceholder : fromRes[capitalize]();
			const toPlaceholder = isNullOrEmpty(toRes) || toRes.startsWith('{{') ? searchPlaceholder : toRes[capitalize]();

			return (
				<div class="dt-filter-input dt-filter-numrange">
					<TextBox updateMode="change" cssClass="dt-filter-numrange-from" wrap={false} label={null} autoCompleteEnabled={false} placeholder={fromPlaceholder} value={numRange?.from != null ? String(numRange.from) : ''} changed={(e) => { this.performNumericRangeFilter(dtColumn, { from: this.parseNumericFilterValue(e), to: numRange?.to ?? null }); }} />
					<TextBox updateMode="change" cssClass="dt-filter-numrange-to" wrap={false} label={null} autoCompleteEnabled={false} placeholder={toPlaceholder} value={numRange?.to != null ? String(numRange.to) : ''} changed={(e) => { this.performNumericRangeFilter(dtColumn, { from: numRange?.from ?? null, to: this.parseNumericFilterValue(e) }); }} appendIcon={closeIcon} appendIconClicked={appendIconClicked} />
				</div>
			);
		}
	}

	protected renderVerticallyTransformedTable(h, columns: TableColumn[]) {
		return (this.rows || []).map((row, _i) => (
			<div class="dt-vert-row">
				{columns.map((dtColumn, _j) => (
					<div class="dt-vert-item" style={this.getTableColumnOrderStyle(dtColumn, row)}>
						<div class="dt-vert-caption">{dtColumn.caption}</div>
						<div class="dt-vert-value">{dtColumn.customRender == null ? (dtColumn.customValue == null ? row[dtColumn.id] : dtColumn.customValue(row)) : dtColumn.customRender(h, row)}</div>
					</div>
				))}
			</div>
		));
	}

	protected renderMobileMode(h, columns: TableColumn[]) {
		if (this.mobileModeCustomRender != null) {
			return this.mobileModeCustomRender(
				h,
				columns,
				this.rows,
			);
		}

		return (
			<div class="dt-mobile-rows">
				{(this.isLoading || this.enforceBodyRedraw || this.enforceHeaderRedraw) && (
					<div style="min-height:150px;">
						<LoadingIndicator visible={true} />
					</div>
				)}

				{this.enforceHeaderRedraw == false
				&& this.enforceBodyRedraw == false
				&& (this.rows || []).map((row, i) => (
					<div class={`dt-mobile-row${this.rowCssClass == null ? '' : ` ${this.rowCssClass(row)}`}`}>
						{this.renderMobileModeRow(
							h,
							columns,
							row,
							i,
						)}
					</div>
				))}
			</div>
		);
	}

	protected renderMobileModeRow(
		h,
		columns: TableColumn[],
		row: any,
		_i: number,
	) {
		const identifier = this.getRowIdentifier(
			columns,
			row,
			true,
		);
		let colArr = this.getVisibleColumns().filter(p => p.mobileVisible != false);
		const cssClass = `dt-mobile-row-inner${this.checkboxesShown ? ' dt-mobile-chbvisible' : ''}`;

		// Exclude identifier columns if they are already picked by the engine
		if (identifier != null && identifier.dtColumns != null && identifier.dtColumns.length > 0) {
			colArr = colArr.filter(p => !identifier.dtColumns.map(c => c.id).includes(p.id));
		}

		return (
			<div
				class={cssClass}
				onClick={(e) => {
					this.handleMobileRowClicked(e, row);
				}}
			>
				{this.checkboxesShown && (
					<div class="dt-mobile-checkbox dt-selection-checkbox" data-id={this.getRowId(row)}>
						<CheckBox skin={CheckBoxSkin.Material} label={null} wrap={false} value={null} changed={e => this.handleRowCheckboxChanged(row, e)} />
					</div>
				)}

				{identifier && <div class="dt-mobile-identifier">{identifier.fullName}</div>}

				{colArr.map((dtColumn, _j) => (
					<div class={`dt-mobile-item dt-mobile-${dtColumn.id.replaceAll(':', '_').toLowerCase()}`} style={this.getTableColumnOrderStyle(dtColumn, row)}>
						{dtColumn.mobileRender == null && this.shouldRenderMobileRowColumn(row, dtColumn) && (
							<div class="dt-mobile-item-inner">
								{dtColumn.mobileCaption != false && <div class="dt-mobile-caption">{dtColumn.caption}</div>}

								<div class="dt-mobile-value">{dtColumn.customRender == null ? (dtColumn.customValue == null ? row[dtColumn.id] : dtColumn.customValue(row)) : dtColumn.customRender(h, row)}</div>
							</div>
						)}
						{dtColumn.mobileRender && dtColumn.mobileRender(h, row)}
					</div>
				))}

				<i class={`${this.mobileModeRowIcon || 'icon icon-arrow-right'} dt-mobile-row-icon`}></i>
			</div>
		);
	}

	protected renderPagination(h) {
		if (this.isCountlessMode()) {
			const isFirst = this.paginationPosition <= 1;
			const noNext = !this.hasMore;

			return (
				<ul class="pagination">
					<li class={`page-item${isFirst ? ' disabled' : ''}`}>
						<a class="page-link" href="javascript:" aria-label="Previous" onClick={_e => this.handlePaginationPositionChanged(this.paginationPosition - 1)}>
							<span aria-hidden="true">
								<i class="fa fa-angle-left" aria-hidden="true"></i>
							</span>
						</a>
					</li>

					<li class="page-item active">
						<a class="page-link" href="javascript:">
							{this.paginationPosition}
						</a>
					</li>

					<li class={`page-item${noNext ? ' disabled' : ''}`}>
						<a class="page-link" href="javascript:" aria-label="Next" onClick={_e => this.handlePaginationPositionChanged(this.paginationPosition + 1)}>
							<span aria-hidden="true">
								<i class="fa fa-angle-right" aria-hidden="true"></i>
							</span>
						</a>
					</li>
				</ul>
			);
		}

		let paginationLength = 5;
		let firstIndex = Math.max(this.paginationPosition - 2, 1);
		let paginationEnd = firstIndex + paginationLength - 1;
		let indexArr = [];

		if (this.getPaginationLength() > -1) {
			if (paginationEnd > this.getLastPaginationIndex()) {
				paginationEnd = this.getLastPaginationIndex();
				firstIndex = paginationEnd - paginationLength + 1;
			}

			if (firstIndex < 1) {
				firstIndex = 1;
				paginationLength = paginationEnd - firstIndex + 1;
			}

			for (let i = firstIndex, _len = paginationLength; i <= paginationEnd; i++) {
				indexArr.push(i);
			}
		} else {
			indexArr = [1];
			this.paginationPosition = 1;
		}

		const lastPageIndex = this.getLastPaginationIndex();
		const isFirstPage = this.paginationPosition <= 1;
		const isLastPage = this.paginationPosition >= lastPageIndex;

		return (
			<ul class="pagination">
				<li class={`page-item${isFirstPage ? ' disabled' : ''}`}>
					<a class="page-link" href="javascript:" aria-label="First" onClick={_e => this.handlePaginationPositionChanged(1)}>
						<span aria-hidden="true">
							<i class="fa fa-angle-double-left" aria-hidden="true"></i>
						</span>
					</a>
				</li>

				<li class={`page-item${isFirstPage ? ' disabled' : ''}`}>
					<a class="page-link" href="javascript:" aria-label="Previous" onClick={_e => this.handlePaginationPositionChanged(this.paginationPosition - 1)}>
						<span aria-hidden="true">
							<i class="fa fa-angle-left" aria-hidden="true"></i>
						</span>
					</a>
				</li>

				{indexArr.map(pagIndex => (
					<li class={`page-item${pagIndex == this.paginationPosition ? ' active' : ''}`} onClick={_e => this.handlePaginationPositionChanged(pagIndex)}>
						<a class="page-link" href="javascript:">
							{pagIndex}
						</a>
					</li>
				))}

				<li class={`page-item${isLastPage ? ' disabled' : ''}`}>
					<a class="page-link" href="javascript:" aria-label="Next" onClick={_e => this.handlePaginationPositionChanged(this.paginationPosition + 1)}>
						<span aria-hidden="true">
							<i class="fa fa-angle-right" aria-hidden="true"></i>
						</span>
					</a>
				</li>

				<li class={`page-item${isLastPage ? ' disabled' : ''}`}>
					<a class="page-link" href="javascript:" aria-label="Last" onClick={_e => this.handlePaginationPositionChanged(lastPageIndex)}>
						<span aria-hidden="true">
							<i class="fa fa-angle-double-right" aria-hidden="true"></i>
						</span>
					</a>
				</li>
			</ul>
		);
	}

	protected renderColumnValue(
		h,
		dtColumn: TableColumn,
		row,
	) {
		if (dtColumn.customRender == null) {
			return (dtColumn.customValue == null ? row[dtColumn.id] : dtColumn.customValue(row));
		} else {
			return dtColumn.customRender(h, row);
		}
	}
}

(function () {
	CheckboxUtils.bindTableShiftClick();
	DropdownUtils.bindDropdownOverflowHack(
		'.dt-root.dt-breakpoint-desktop tbody .dropdown',
		'dataTableDropdown',
		'dt-dropdown-button-cloned',
		false,
		() => DataTableConfig.dropdownOverflowCloningEnabled,
	);
})();

const DataTable = toNative(DataTableComponent);
export type DataTableType = typeof DataTable.prototype;
export default DataTable;
