/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { createPredicate, getEventPath, isNumber, isObject, ItemManager, matchesPredicate } from '@tylertech/forge-core';
import { MultiSortManager } from './multi-sort-manager';
import { TABLE_CONSTANTS } from './table-constants';
import { TableRow } from './table-row';
import { TableUtils } from './table-utils';
import { SortDirection, CellAlign } from './types';
/**
 * The state class behind the `TableComponent` custom element.
 */
export class TableFoundation {
    constructor(_adapter) {
        this._adapter = _adapter;
        // Backing model values
        this._tableRows = [];
        this._data = [];
        this._columnConfigurations = [];
        this._select = false;
        this._multiselect = true;
        this._dense = false;
        this._roomy = false;
        this._filter = false;
        this._fixedHeaders = false;
        this._layoutType = TABLE_CONSTANTS.strings.DEFAULT_LAYOUT_TYPE;
        this._wrapContent = true;
        this._resizable = false;
        this._minResizeWidth = TABLE_CONSTANTS.numbers.MIN_RESIZE_WIDTH;
        this._multiColumnSort = false;
        this._selectCheckboxAlignment = CellAlign.Center;
        // State variables
        this._isAllSelected = false;
        this._sortedColumnIndex = -1;
        this._rendered = false;
        this._allowRowClick = false;
        this._multiSortManager = new MultiSortManager();
        // Create all listeners
        this._rowClickListener = evt => this._onRowClick(evt);
        this._rowDoubleClickListener = evt => this._onRowDoubleClick(evt);
        this._selectRowListener = evt => this._onRowSelected(evt);
        this._selectAllListener = evt => this._onSelectAll(evt);
        this._sortableHeadCellKeydownListener = (evt) => this._onSortableHeadCellKeydown(evt);
        this._headRowMouseDownListener = evt => this._onHeadRowMouseDown(evt);
        this._headRowContextMenuListener = evt => this._onHeadRowContextMenu(evt);
        this._documentMouseMoveListener = evt => this._onMouseMove(evt);
        this._documentMouseUpListener = evt => this._onMouseUp(evt);
        this._filterListener = (value, columnIndex) => this._onFilter(value, columnIndex);
        // Create helpers
        this._selectionManager = new ItemManager(this._selectKey);
        this._hiddenColumnManager = new ItemManager();
    }
    /** Called when the component is connected to the DOM. */
    initialize() {
        this.connect();
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.SELECT, this._select.toString());
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.MULTISELECT, this._multiselect.toString());
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.DENSE, this._dense.toString());
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.ROOMY, this._roomy.toString());
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.FILTER, this._filter.toString());
        this._adapter.emitHostEvent(TABLE_CONSTANTS.events.INITIALIZED, undefined, false);
    }
    connect() {
        this._adapter.initialize();
    }
    disconnect() {
        this._resizeDescriptor = undefined;
        this._adapter.removeDocumentListener('mousemove', this._documentMouseMoveListener);
        this._adapter.removeDocumentListener('mouseup', this._documentMouseUpListener);
    }
    /** The data to be displayed in the table body. */
    set data(value) {
        if (Array.isArray(value) && value.length > 0) {
            if (isObject(value[0])) {
                this._data = value.map(v => (Object.assign({}, v)));
            }
            else {
                this._data = [...value];
            }
        }
        else {
            this._data = [];
        }
        this._tableRows = this._data.map(data => new TableRow(data, this._selectionManager.exists(data)));
        this._renderBody();
    }
    get data() {
        return this._data.map(o => (Object.assign({}, o))); // Shallow clone
    }
    /** The column configuration options. */
    set columnConfigurations(value) {
        // Intentional shallow copy of member properties. These member objects have properties that are references to functions.
        this._columnConfigurations = value.map(cc => (Object.assign({}, cc)));
        // Update hidden column manager
        this._hiddenColumnManager.clear();
        this._columnConfigurations.forEach(cc => {
            if (cc.hidden) {
                this._hiddenColumnManager.add(cc);
            }
        });
        this._refreshTableSort(); // Must be called after updating the hidden columns
        this.render();
    }
    get columnConfigurations() {
        // Intentional shallow copy of member properties. These member objects have properties that are references to functions.
        return this._columnConfigurations.map(cc => (Object.assign({}, cc)));
    }
    /** Gets the visible column configurations. */
    get _visibleColumnConfigurations() {
        return this._columnConfigurations.filter(cc => !this._hiddenColumnManager.exists(cc));
    }
    /** Gets the table configuration. */
    get _tableConfiguration() {
        return {
            tableElement: this._adapter.getTableElement(),
            columnConfigurations: this._visibleColumnConfigurations,
            data: this._tableRows,
            clickListener: this._allowRowClick ? this._rowClickListener : null,
            doubleClickListener: this._allowRowClick ? this._rowDoubleClickListener : null,
            selectListener: this._select ? this._selectRowListener : null,
            selectAllListener: this._multiselect ? this._selectAllListener : null,
            sortableHeadCellKeydownListener: this._sortableHeadCellKeydownListener,
            headRowMouseDownListener: this._headRowMouseDownListener,
            headRowContextMenuListener: this._headRowContextMenuListener,
            filterListener: this._filter ? this._filterListener : null,
            sortDirection: this._sortDirection,
            sortedColumn: this._sortedColumnIndex,
            multiColumnSort: this._multiColumnSort,
            filter: this._filter,
            wrapContent: this._wrapContent,
            fixedHeaders: this._fixedHeaders,
            layoutType: this._layoutType,
            dense: this._dense,
            resizable: this._resizable,
            rowCreatedCallback: this._rowCreatedCallback,
            cellCreatedCallback: this._cellCreatedCallback,
            selectAllTemplate: this._selectAllTemplate,
            selectCheckboxAlignment: this._selectCheckboxAlignment,
            tooltipSelect: this._tooltipSelect,
            tooltipSelectAll: this._tooltipSelectAll
        };
    }
    /** Controls the visibility of the select column. */
    set select(value) {
        if (this._select !== value) {
            this._select = value;
            // Now we can add/remove the column
            this._adapter.setSelectColumnVisibility(this._adapter.getTableElement(), this._select, this._selectRowListener, this._multiselect ? this._selectAllListener : undefined, this._multiselect ? this._selectAllTemplate : null, this._selectCheckboxAlignment, this._tableRows, this._tooltipSelect, this._tooltipSelectAll);
            this._updateSelections(true);
            // Update the attribute on the host element
            if (this._select) {
                this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.SELECT, this._select.toString());
            }
            else {
                this._adapter.removeHostAttribute(TABLE_CONSTANTS.attributes.SELECT);
            }
        }
    }
    get select() {
        return this._select;
    }
    /** Controls the visibility of the select all checkbox. */
    set multiselect(value) {
        if (this._multiselect !== value) {
            this._multiselect = value;
            if (this._select) {
                this._adapter.setSelectAllVisibility(this._adapter.getTableElement(), this._multiselect, this._multiselect ? this._selectAllListener : null, this._multiselect ? this.selectAllTemplate : null, this._selectCheckboxAlignment, this._tooltipSelectAll);
                this._updateSelections(true);
            }
            this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.MULTISELECT, this._multiselect.toString());
        }
    }
    get multiselect() {
        return this._multiselect;
    }
    /** The row key for matching data to selections. */
    get selectKey() {
        if (Array.isArray(this._selectKey)) {
            return [...this._selectKey];
        }
        return this._selectKey;
    }
    set selectKey(value) {
        if (this._originalSelectKey === value) {
            return;
        }
        if (Array.isArray(value)) {
            this._originalSelectKey = [...value];
            this._selectKey = [...value];
        }
        else {
            this._originalSelectKey = value;
            this._selectKey = [value];
        }
        this._selectionManager.setKey(this._selectKey);
        this._updateSelections(false);
    }
    /** Controls whether the table is dense or not. False by default. */
    get dense() {
        return this._dense;
    }
    set dense(value) {
        if (this._dense !== value) {
            this._dense = value;
            this._adapter.setDense(this._adapter.getTableElement(), this._dense);
            this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.DENSE, this._dense.toString());
        }
    }
    get roomy() {
        return this._roomy;
    }
    set roomy(value) {
        if (value !== this._roomy) {
            this._roomy = value;
            this._applyRoomy();
        }
    }
    /** Controls whether the table filters are visible or not. */
    set filter(value) {
        if (this._filter !== value) {
            this._filter = value;
            this._adapter.setFilterRow(this._tableConfiguration);
            this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.FILTER, this._filter.toString());
        }
    }
    get filter() {
        return this._filter;
    }
    /** Controls whether the table has fixed headers or not. */
    set fixedHeaders(value) {
        if (this._fixedHeaders !== value) {
            this._fixedHeaders = value;
            this._adapter.setFixedHeaders(this._tableConfiguration);
        }
    }
    get fixedHeaders() {
        return this._fixedHeaders;
    }
    /** Controls the table layout algorithm */
    get layoutType() {
        return this._layoutType;
    }
    set layoutType(value) {
        if (this._layoutType !== value) {
            this._layoutType = value;
            this._adapter.setTableLayoutType(this._tableConfiguration);
        }
    }
    /** Controls whether the content in each cell wraps or not */
    get wrapContent() {
        return this._wrapContent;
    }
    set wrapContent(value) {
        if (this._wrapContent !== value) {
            this._wrapContent = value;
            this._adapter.setWrapContentState(this._tableConfiguration);
        }
    }
    /** Controls whether the columns are resizable or not. */
    get resizable() {
        return this._resizable;
    }
    set resizable(value) {
        if (this._resizable !== value) {
            this._resizable = value;
            this._adapter.setResizable(this._tableConfiguration);
        }
    }
    /** Gets/sets the minimum width that a column can be resized to by the user dragging the resize handle. */
    get minResizeWidth() {
        return this._minResizeWidth;
    }
    set minResizeWidth(value) {
        if (this._minResizeWidth !== value) {
            this._minResizeWidth = value;
            if (!isNumber(this._minResizeWidth)) {
                this._minResizeWidth = TABLE_CONSTANTS.numbers.MIN_RESIZE_WIDTH;
            }
            else if (this._minResizeWidth < 0) {
                this._minResizeWidth = 0;
            }
        }
    }
    /** Gets/sets whether the rows respond to click events or not. */
    get allowRowClick() {
        return this._allowRowClick;
    }
    set allowRowClick(value) {
        if (this._allowRowClick !== value) {
            this._allowRowClick = value;
            if (this._rendered) {
                this._adapter.setRowClickListeners(this._adapter.getTableElement(), this._allowRowClick, this._rowClickListener, this._rowDoubleClickListener);
                this._adapter.setRowClickAttributes(this._adapter.getTableElement(), this._allowRowClick);
            }
        }
    }
    /** Gets/sets whether the table can sort multiple columns. */
    get multiColumnSort() {
        return this._multiColumnSort;
    }
    set multiColumnSort(value) {
        if (value !== this._multiColumnSort) {
            this._multiColumnSort = value;
            this._refreshTableSort();
            this.render();
        }
    }
    get selectCheckboxAlignment() {
        return this._selectCheckboxAlignment;
    }
    set selectCheckboxAlignment(value) {
        if (value !== this._selectCheckboxAlignment) {
            this._selectCheckboxAlignment = value;
            this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.SELECT_CHECKBOX_ALIGNMENT, value);
            this.render();
        }
    }
    get tooltipSelect() {
        return this._tooltipSelect;
    }
    set tooltipSelect(value) {
        if (value !== this._tooltipSelect) {
            this._tooltipSelect = value;
            if (typeof value === 'string') {
                this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.TOOLTIP_SELECT, value);
            }
            this.render();
        }
    }
    get tooltipSelectAll() {
        return this._tooltipSelectAll;
    }
    set tooltipSelectAll(value) {
        if (value !== this._tooltipSelectAll) {
            this._tooltipSelectAll = value;
            this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.TOOLTIP_SELECT_ALL, value);
            this.render();
        }
    }
    get selectAllTemplate() {
        return this._selectAllTemplate;
    }
    set selectAllTemplate(value) {
        if (value !== this._selectAllTemplate) {
            this._selectAllTemplate = value;
            this.render();
        }
    }
    get rowCreated() {
        return this._rowCreatedCallback;
    }
    set rowCreated(value) {
        this._rowCreatedCallback = value;
    }
    get cellCreated() {
        return this._cellCreatedCallback;
    }
    set cellCreated(value) {
        this._cellCreatedCallback = value;
    }
    isRowSelected(rowData) {
        const predicate = createPredicate(Array.isArray(this._selectKey) ? this._selectKey : [this._selectKey], rowData);
        return this.selectKey ? !!this._selectionManager.getItems().find(item => matchesPredicate(item, predicate)) : this._selectionManager.exists(rowData);
    }
    /** Returns the selected row instances. */
    getSelectedRows() {
        return this._selectionManager.getItems();
    }
    /**
     * Sets the selected items in the table.
     * @param {any[]} data The data to select.
     * @param {boolean} clearCurrentSelections True to clear the current selections before setting the new selections.
     */
    selectRows(data, clearCurrentSelections) {
        if (!this._selectKey) {
            throw new Error('You must set a selectKey to select rows.');
        }
        if (!this._multiselect || clearCurrentSelections) {
            this.clearSelections();
        }
        if (data && data.length) {
            this._selectionManager.add(data);
            this._updateSelections(!clearCurrentSelections);
        }
    }
    /**
     * Deselects rows from in the table data.
     * @param {any[]} data The array of items to deselect.
     */
    deselectRows(data) {
        if (data && data.length) {
            this._selectionManager.remove(data);
            this._updateSelections(false);
        }
    }
    /**
     * Clears all selections from the table.
     */
    clearSelections() {
        const tableElement = this._adapter.getTableElement();
        this._selectionManager.clear();
        this._tableRows.forEach(tableRow => tableRow.selected = false);
        this._isAllSelected = false;
        this._adapter.clearSelectedRows(tableElement);
        if (this._select && this._multiselect) {
            this._adapter.updateSelectAllState(tableElement, false);
        }
    }
    /**
     * Hides a table column.
     * @param {number} columnIndex The index of the column to hide.
     */
    hideColumn(columnIndex) {
        const columnConfig = this._columnConfigurations[columnIndex];
        if (this._sortedColumnIndex === columnIndex) {
            this._sortedColumnIndex = -1;
        }
        if (!this._hiddenColumnManager.exists(columnConfig)) {
            this._hiddenColumnManager.add(columnConfig);
            this.render();
        }
    }
    /**
     * Shows a column that is currently hidden.
     * @param {number} columnIndex The index of the column to show.
     */
    showColumn(columnIndex) {
        const columnConfig = this._columnConfigurations[columnIndex];
        if (this._hiddenColumnManager.exists(columnConfig)) {
            this._hiddenColumnManager.remove(columnConfig);
            this.render();
        }
    }
    /**
     * Determines if a column at the given index is hidden or not.
     * @param {number} columnIndex The index of the column.
     * @returns {boolean} `true` if the column is hidden, `false` otherwise.
     */
    isColumnHidden(columnIndex) {
        const columnConfig = this._columnConfigurations[columnIndex];
        return !!columnConfig && this._hiddenColumnManager.exists(columnConfig);
    }
    /**
     * Creates and renders the table with the current column configuration and data.
     */
    render() {
        this._adapter.createTable(this._tableConfiguration);
        this._renderSelections();
        this._rendered = true;
    }
    /**
     * Renders the table body only.
     */
    _renderBody() {
        if (!this._rendered) {
            return;
        }
        this._adapter.recreateTableBody(this._tableConfiguration);
        this._renderSelections();
    }
    _renderSelections() {
        if (this.select) {
            this._isAllSelected = this._getSelectAllState();
            if (this._multiselect) {
                this._adapter.updateSelectAllState(this._adapter.getTableElement(), this._isAllSelected, this._isIndeterminate(this._isAllSelected));
            }
        }
        const selections = this._selectionManager.getItems();
        if (selections.length) {
            this._adapter.setSelectedRows(this._adapter.getTableElement(), this._selectKey, this._data, selections, false);
        }
    }
    /**
     * Expands a collapsed row.
     * @param rowIndex The index of the row to expand.
     */
    expandRow(rowIndex, template) {
        if (!this._rendered) {
            throw new Error('Cannot expand a row before the table has rendered.');
        }
        if (rowIndex < 0 || rowIndex > this._tableRows.length) {
            throw new Error(`Invalid row index: ${rowIndex}.`);
        }
        return this._adapter.expandRow(this._tableConfiguration, rowIndex, template);
    }
    /**
     * Collapses an expanded row.
     * @param rowIndex The index of the row to expand.
     */
    collapseRow(rowIndex) {
        if (!this._rendered) {
            return Promise.resolve();
        }
        if (rowIndex < 0 || rowIndex > this._tableRows.length) {
            return Promise.resolve();
        }
        return this._adapter.collapseRow(this._tableConfiguration, rowIndex);
    }
    /**
     * Check if a row is expanded or not.
     * @param rowIndex The row index to check.
     */
    isRowExpanded(rowIndex) {
        return this._adapter.isRowExpanded(this._tableConfiguration, rowIndex);
    }
    /**
     * Selects the rows by the index or indexes.
     * @param indexes The row indexes to select
     * @param preserveExisting Whether or not the existing selections should be preserved
     */
    selectRowsByIndex(indexes, preserveExisting) {
        const rowsToSelect = indexes.map(i => this._data[i]).filter(obj => !!obj);
        this.selectRows(rowsToSelect, !preserveExisting);
    }
    /**
     * Selects the rows by the index or indexes.
     * @param indexes The row indexes to deselect
     */
    deselectRowsByIndex(indexes) {
        const rowsToDeselect = indexes.map(i => this._data[i]).filter(obj => !!obj);
        this.deselectRows(rowsToDeselect);
    }
    /**
     * Determines if every row is selected or not.
     */
    _getSelectAllState() {
        return this._tableRows.length > 0 && this._tableRows.every(tr => tr.selected);
    }
    /**
     * Updates the internal selection state as well as UI state.
     * @param {boolean} preserveExisting Should existing selections be preserved when updating selection state.
     */
    _updateSelections(preserveExisting) {
        this._tableRows.forEach(tableRow => tableRow.selected = this._selectionManager.exists(tableRow.data));
        const isAllSelected = this._getSelectAllState();
        this._adapter.setSelectedRows(this._adapter.getTableElement(), this._selectKey, this._data, this._selectionManager.getItems(), preserveExisting);
        if (isAllSelected !== this._isAllSelected || this._isIndeterminate(isAllSelected)) {
            this._isAllSelected = isAllSelected;
            // (derek.moss): while writting test it seemed like this could never be false.
            // maybe an unneeded check
            if (this._multiselect) {
                this._adapter.updateSelectAllState(this._adapter.getTableElement(), this._isAllSelected, this._isIndeterminate(this._isAllSelected));
            }
        }
    }
    /** Handles a row being single clicked. */
    _onRowClick(evt) {
        evt.stopImmediatePropagation();
        // Ignore if the element being clicked is the checkbox element
        if (evt.target.matches(`[${TABLE_CONSTANTS.attributes.SELECT_CHECKBOX}]`)) {
            return;
        }
        const composedPath = getEventPath(evt);
        const composedElements = composedPath.filter(node => node.nodeType === 1);
        const customCellTemplateElement = composedElements.find(el => el.hasAttribute(TABLE_CONSTANTS.attributes.CUSTOM_CELL_TEMPLATE));
        const stopClickPropagation = customCellTemplateElement && customCellTemplateElement.hasAttribute(TABLE_CONSTANTS.attributes.CUSTOM_CELL_TEMPLATE_STOP_PROPAGATION);
        if (stopClickPropagation) {
            return; // We ignore click events that bubble from custom templates if they were configured to stop propagation
        }
        const { index, data } = this._getSelectedRowFromEvent(evt);
        const evtData = { index, data };
        this._adapter.emitHostEvent(TABLE_CONSTANTS.events.ROW_CLICK, evtData, true, false);
    }
    /** Handles a row being double clicked. */
    _onRowDoubleClick(evt) {
        evt.stopPropagation();
        // Ignore if the element being clicked is the checkbox element
        if (evt.target.matches(`[${TABLE_CONSTANTS.attributes.SELECT_CHECKBOX}]`)) {
            return;
        }
        const rowSelectedInfo = this._getSelectedRowFromEvent(evt);
        const detail = {
            index: rowSelectedInfo.index,
            data: rowSelectedInfo.data
        };
        this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SELECT_DOUBLE, detail, true, false);
    }
    /**
     * Handles a row being selected/deselected.
     */
    _onRowSelected(evt) {
        const rowSelectedInfo = this._getSelectedRowFromEvent(evt);
        // Deselect the previously selected row(s) when not in multiselect mode (only if it's different than the newly selected row)
        if (!this._multiselect) {
            const existingSelections = this._tableRows.filter(item => item.selected);
            existingSelections.forEach(selection => {
                if (!matchesPredicate(selection.data, createPredicate(this._selectKey, rowSelectedInfo.data))) {
                    this.deselectRows([selection.data]);
                }
            });
        }
        // Determine what the new selected state should be for this row (opposite of current state)
        const isSelected = !this._selectionManager.exists(rowSelectedInfo.data);
        // Update the table row based on the new selected state
        this._adapter.updateSelectedState(rowSelectedInfo.rowElement, isSelected);
        // Update the selection manager
        if (isSelected) {
            this._selectionManager.add(rowSelectedInfo.data);
        }
        else {
            this._selectionManager.remove(rowSelectedInfo.data);
        }
        // Update the state for this row
        this._tableRows[rowSelectedInfo.index].selected = isSelected;
        // The indexes that we need to emit select events for
        let selectionIndexes = [rowSelectedInfo.index];
        let isAllSelected = false;
        // If we have multiselect on, then we need to update the select all state
        if (this._multiselect) {
            const shift = evt.shiftKey;
            if (shift && this._previouslyClickedRow) {
                selectionIndexes = this._shiftSelectRows(rowSelectedInfo);
                if (selectionIndexes.includes(this._previouslyClickedRow.index)) {
                    const idx = selectionIndexes.indexOf(this._previouslyClickedRow.index);
                    selectionIndexes.splice(idx, 1);
                }
            }
            // Update the state of the select all checkbox
            isAllSelected = this._getSelectAllState();
            this._adapter.updateSelectAllState(this._adapter.getTableElement(), isAllSelected, this._isIndeterminate(isAllSelected));
        }
        // Emit a table row selection changed event
        for (const index of selectionIndexes) {
            const data = this._tableRows[index].data;
            const selected = this._selectionManager.exists(data);
            const detail = { selected, index, data };
            this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SELECT, detail, true, false);
        }
        // If selecting/deselecting this row causes the select all state to be changed, then emit the select all changed event.
        if (this._isAllSelected !== isAllSelected) {
            this._isAllSelected = isAllSelected;
            const data = {
                status: this._isAllSelected,
                count: this._tableRows.filter(row => row.selected).length,
                data: [...this._data],
                targetType: 'row'
            };
            this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SELECT_ALL, data, true, false);
        }
        this._previouslyClickedRow = Object.assign({}, rowSelectedInfo);
    }
    /**
     * Determines which row was chosen from the given event.
     * @param {Event} evt The click event.
     */
    _getSelectedRowFromEvent(evt) {
        const composedPath = getEventPath(evt);
        const rowElement = composedPath.find(el => el.tagName === 'TR');
        // We need to get the index of the row element from its parent tbody in case the
        // element has been removed from the DOM (table re-rendered) between when this event
        // was dispatched and when this method is executed
        const tbody = rowElement.parentElement;
        const index = this._getNonExpandedRows(tbody).findIndex(el => el === rowElement);
        const data = this._tableRows[index].data;
        return { rowElement, index, data };
    }
    _getNonExpandedRows(tbodyElement) {
        return Array.from(tbodyElement.rows)
            .filter(row => !row.classList.contains(TABLE_CONSTANTS.classes.TABLE_ROW_EXPANDABLE_CONTENT));
    }
    /**
     * Handles the select all checkbox being checked/unchecked.
     */
    _onSelectAll(evt) {
        evt.stopPropagation();
        // Toggle the status of our select all state
        this._isAllSelected = !this._isAllSelected;
        const tableElement = this._adapter.getTableElement();
        // Update the select all checkbox in the table based on the new state
        this._adapter.updateSelectAllState(tableElement, this._isAllSelected);
        // Select/deselect every row based on our new select all state
        this._tableRows.forEach((tableRow, index) => {
            // Update the state for each row
            tableRow.selected = this._isAllSelected;
            // Update the selection manager
            if (this._isAllSelected) {
                this._selectionManager.add(tableRow.data);
            }
            else {
                this._selectionManager.remove(tableRow.data);
            }
            // Update the table row selected state
            const rowElement = TableUtils.getRowByIndex(tableElement, index);
            if (rowElement) {
                this._adapter.updateSelectedState(rowElement, this._isAllSelected);
            }
        });
        // Emit the select all changed event
        const data = {
            status: this._isAllSelected,
            count: this._tableRows.filter(row => row.selected).length,
            data: [...this._data],
            targetType: 'select-all'
        };
        this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SELECT_ALL, data, true, false);
    }
    _onHeadRowContextMenu(evt) {
        // We only handle this event on MacOS due to the `ctrl+click` combination triggering the contextmenu event...
        // So we only detect that scenario here to still allow for the default context menu on Mac when right-clicking
        if (evt.ctrlKey) {
            evt.preventDefault();
        }
    }
    _onHeadRowMouseDown(evt) {
        if (evt.button !== 0) {
            return;
        }
        const composedPath = getEventPath(evt);
        const target = evt.target;
        // Find the row and cell elements that the event originated within
        const rowElement = composedPath.find(el => el.tagName === 'TR');
        const thElement = composedPath.find(el => el.tagName === 'TH');
        const cellIndex = Array.from(rowElement.cells).findIndex(c => c === thElement);
        this._headCellMouseDownIndex = cellIndex;
        if (target !== thElement && target.classList.contains(TABLE_CONSTANTS.classes.TABLE_RESIZE_HANDLE)) {
            this._resizeDescriptor = {
                element: thElement,
                startWidth: thElement.offsetWidth,
                startX: evt.pageX,
                width: thElement.offsetWidth,
                index: this._select ? cellIndex - 1 : cellIndex
            };
            this._adapter.addDocumentListener('mousemove', this._documentMouseMoveListener);
            this._adapter.setResizeColumnVisibility(this._tableConfiguration, cellIndex, true);
        }
        this._adapter.addDocumentListener('mouseup', this._documentMouseUpListener);
    }
    _onMouseMove(evt) {
        if (this._resizeDescriptor) {
            let newWidth = this._resizeDescriptor.startWidth + (evt.pageX - this._resizeDescriptor.startX);
            // Ensure that the user can't drag the column past the minimum column width
            if (newWidth < this._minResizeWidth) {
                newWidth = this._minResizeWidth;
            }
            this._resizeDescriptor.width = newWidth;
            this._resizeDescriptor.element.style.width = `${newWidth}px`;
            this._adapter.addTableClass(TABLE_CONSTANTS.classes.TABLE_RESIZING);
        }
    }
    _onMouseUp(evt) {
        this._adapter.removeDocumentListener('mousemove', this._documentMouseMoveListener);
        this._adapter.removeDocumentListener('mouseup', this._documentMouseUpListener);
        this._adapter.removeTableClass(TABLE_CONSTANTS.classes.TABLE_RESIZING);
        if (this._headCellMouseDownIndex === undefined) {
            return;
        }
        if (this._resizeDescriptor) {
            this._adapter.normalizeColumnWidths(this._tableConfiguration);
            this._adapter.setResizeColumnVisibility(this._tableConfiguration, this._headCellMouseDownIndex, false);
            const resizeEventData = {
                index: this._resizeDescriptor.index,
                width: this._resizeDescriptor.width
            };
            this._adapter.emitHostEvent(TABLE_CONSTANTS.events.COLUMN_RESIZE, resizeEventData);
            this._resizeDescriptor = undefined;
        }
        else if (this._tableConfiguration.tableElement.tHead.contains(evt.target)) {
            if (this._multiColumnSort) {
                this._onMultiSort(this._headCellMouseDownIndex, evt.ctrlKey);
            }
            else {
                this._onSort(this._headCellMouseDownIndex);
            }
        }
        this._headCellMouseDownIndex = undefined;
    }
    _onSortableHeadCellKeydown(evt) {
        if (evt.key === ' ' || evt.key === 'Enter') {
            const composedPath = getEventPath(evt);
            const rowElement = composedPath.find(el => el.tagName === 'TR');
            const thElement = composedPath.find(el => el.tagName === 'TH');
            const cellIndex = Array.from(rowElement.cells).findIndex(c => c === thElement);
            this._onSort(cellIndex);
        }
    }
    /**
     * Called when a click event is triggered on the table header row.
     * We use this to capture all click events on the row, and determine which
     * header cell was clicked.
     */
    _onSort(cellIndex) {
        // Get the column index so we can access the config for that column (this depends on whether there is a select column or not)
        const columnIndex = this._select ? cellIndex - 1 : cellIndex;
        // Get the column config
        const columnConfig = columnIndex >= 0 ? this._visibleColumnConfigurations[columnIndex] : null;
        // If this column was not sortable, or was the select column then we just return
        if (!columnConfig || !columnConfig.sortable) {
            return;
        }
        // Always default to ascending sort on a newly sorted column
        let desiredSortDirection = SortDirection.Ascending;
        // Toggle the sort direction class
        const isTogglingSortOnSameColumn = columnIndex === this._sortedColumnIndex;
        const currentSortDirectionIsAscending = this._sortDirection === SortDirection.Ascending || this._sortDirection === undefined;
        const isUnsorting = isTogglingSortOnSameColumn && this._sortDirection === SortDirection.Descending;
        if (isTogglingSortOnSameColumn && currentSortDirectionIsAscending) {
            desiredSortDirection = SortDirection.Descending;
        }
        if (isUnsorting && !columnConfig.preventUnsort) {
            desiredSortDirection = SortDirection.Unset;
        }
        // Emit the sort event for this column
        const data = {
            direction: desiredSortDirection,
            columnIndex
        };
        if (!this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SORT, data, true, true)) {
            // If defaultPrevented is true, we emit the event without updating internal state.
            return;
        }
        if (isTogglingSortOnSameColumn && this._sortDirection === SortDirection.Descending && !columnConfig.preventUnsort) {
            this._adapter.removeColumnSort(this._adapter.getTableElement(), this._select ? this._sortedColumnIndex + 1 : this._sortedColumnIndex);
            this._sortedColumnIndex = -1;
            return;
        }
        // Update internal state
        this._sortDirection = desiredSortDirection;
        // Toggle the sort direction class
        if (columnIndex === this._sortedColumnIndex) {
            // Update the table with the new sort direction
            this._adapter.setSortDirection(this._adapter.getTableElement(), cellIndex, this._sortDirection);
        }
        else {
            // If we have a column this is currently sorted, remove the sort from that column
            if (this._sortedColumnIndex >= 0) {
                this._adapter.removeColumnSort(this._adapter.getTableElement(), this._select ? this._sortedColumnIndex + 1 : this._sortedColumnIndex);
            }
            // Set this column as the sorted column
            this._sortedColumnIndex = columnIndex;
            // Set the new sort direction on the newly sorted column
            this._adapter.setSortedColumn(this._adapter.getTableElement(), cellIndex, this._sortDirection);
        }
    }
    _onMultiSort(cellIndex, ctrlKey) {
        // Get the column index so we can access the config for that column (this depends on whether there is a select column or not)
        const columnIndex = this._select ? cellIndex - 1 : cellIndex;
        // Get the column config
        const columnConfig = columnIndex >= 0 ? this._visibleColumnConfigurations[columnIndex] : null;
        // If this column was not sortable, or was the select column then we just return
        if (!columnConfig || !columnConfig.sortable) {
            return;
        }
        const revertColumns = this._multiSortManager.sortedColumns;
        const sortColumn = this._multiSortManager.getSortColumn(columnIndex);
        if (sortColumn) {
            this._multiSortManager.updateSortColumn(sortColumn);
        }
        else if (ctrlKey) {
            this._multiSortManager.addSortColumn({ direction: SortDirection.Descending, columnIndex, propertyName: columnConfig.property });
        }
        else {
            this._multiSortManager.clearMultiSort();
            this._multiSortManager.addSortColumn({ direction: SortDirection.Descending, columnIndex, propertyName: columnConfig.property });
        }
        const data = this._multiSortManager.sortedColumns.map(col => (Object.assign({}, col)));
        if (!this._adapter.emitHostEvent(TABLE_CONSTANTS.events.SORT, data, true, true)) {
            // If defaultPrevented is true, we emit the event without updating internal state.
            this._multiSortManager.setSortColumns(revertColumns);
            return;
        }
        const newSortedColumns = this._multiSortManager.sortedColumns;
        const hasMultipleColumnsSorted = newSortedColumns.length > 1;
        // clear any unsorted columns in the UI
        for (let virtualIndex = 0; virtualIndex < this._visibleColumnConfigurations.length; virtualIndex++) {
            // actualIndex takes into account if there is a select column, virtualIndex is 0 -> visible data columns length
            const actualIndex = this._select ? virtualIndex + 1 : virtualIndex;
            if (!this._multiSortManager.isSortColumn(virtualIndex)) {
                this._adapter.removeColumnSort(this._adapter.getTableElement(), actualIndex);
                TableUtils.setMultiSortColumnSortNumber(this._adapter.getTableElement(), hasMultipleColumnsSorted, actualIndex, null);
            }
            else {
                const column = this._multiSortManager.getSortColumn(virtualIndex);
                if (column) {
                    this._adapter.setSortedColumn(this._adapter.getTableElement(), actualIndex, column.direction);
                    TableUtils.setMultiSortColumnSortNumber(this._adapter.getTableElement(), hasMultipleColumnsSorted, actualIndex, column.sortOrder || null);
                }
            }
        }
    }
    /**
     * Handles filter events and emits the filter information event.
     * @param evt The event.
     */
    _onFilter(value, columnIndex) {
        const data = { value, columnIndex };
        this._adapter.emitHostEvent(TABLE_CONSTANTS.events.FILTER, data, true, false);
    }
    _shiftSelectRows(selectedRow) {
        const indexes = [];
        if (this._previouslyClickedRow) {
            const prevIndex = this._previouslyClickedRow.index;
            const currIndex = selectedRow.index;
            let startIndex = currIndex >= prevIndex ? prevIndex : currIndex;
            let endIndex = currIndex >= prevIndex ? currIndex : prevIndex;
            endIndex += endIndex > startIndex ? 1 : 0;
            startIndex += startIndex > endIndex ? 1 : 0;
            let action = (data) => this.selectRows(data, false);
            const existingSelection = this._tableRows.find(item => item.selected && item.data === selectedRow.data);
            const shouldDeselect = this._tableRows.find(item => { var _a; return item.data === ((_a = this._previouslyClickedRow) === null || _a === void 0 ? void 0 : _a.data) && !item.selected; });
            // If you are clicking a checkbox that is already selected, deselect all the rows
            if (!existingSelection || shouldDeselect) {
                action = (data) => this.deselectRows(data);
            }
            for (let i = startIndex; i < endIndex; i++) {
                indexes.push(i);
            }
            action(this.data.filter((v, i) => indexes.includes(i)));
        }
        return indexes;
    }
    _isIndeterminate(isAllSelected) {
        return !isAllSelected && this.getSelectedRows().length > 0 && this._data.length !== this.getSelectedRows().length;
    }
    _applyRoomy() {
        this._adapter.setRoomy(this._adapter.getTableElement(), this._roomy);
        this._adapter.setHostAttribute(TABLE_CONSTANTS.attributes.ROOMY, this._roomy.toString());
    }
    _refreshTableSort() {
        this._sortedColumnIndex = this._visibleColumnConfigurations.findIndex(c => c.initialSort);
        // We only update the sort direction if it was not already set
        if (this._sortedColumnIndex >= 0 && this._sortDirection === undefined) {
            const sortedColumn = this._visibleColumnConfigurations[this._sortedColumnIndex];
            if (sortedColumn.initialSort === true && sortedColumn.sortDirection) {
                this._sortDirection = sortedColumn.sortDirection;
            }
            else if (typeof sortedColumn.initialSort === 'object' && sortedColumn.initialSort.direction) {
                this._sortDirection = sortedColumn.initialSort.direction;
            }
        }
        if (this._multiColumnSort) {
            const sortedColumns = [];
            this._visibleColumnConfigurations.forEach((col, index) => {
                if (typeof col.initialSort === 'object') {
                    sortedColumns.push({
                        columnIndex: index,
                        direction: col.initialSort.direction,
                        sortOrder: col.initialSort.sortOrder,
                        propertyName: col.property
                    });
                }
            });
            this._multiSortManager.setSortColumns(sortedColumns);
        }
    }
}
