/* *
 *
 *  Highcharts Grid class
 *
 *  (c) 2020-2026 Highsoft AS
 *
 *  A commercial license may be required depending on use.
 *  See www.highcharts.com/license
 *
 *
 *  Authors:
 *  - Dawid Dragula
 *  - Sebastian Bochan
 *
 * */
'use strict';
import Accessibility from './Accessibility/Accessibility.js';
import AST from '../../Core/Renderer/HTML/AST.js';
import DataProviderRegistry from './Data/DataProviderRegistry.js';
import DataTable from '../../Data/DataTable.js';
import { defaultOptions } from './Defaults.js';
import { makeHTMLElement, setHTMLContent, createOptionsProxy } from './GridUtils.js';
import Table from './Table/Table.js';
import QueryingController from './Querying/QueryingController.js';
import Globals from './Globals.js';
import TimeBase from '../../Shared/TimeBase.js';
import Pagination from './Pagination/Pagination.js';
import { defined, diffObjects, extend, fireEvent, merge, pick } from '../../Shared/Utilities.js';
import { uniqueKey } from '../../Core/Utilities.js';
/* *
 *
 *  Class
 *
 * */
/**
 * A base class for the Grid.
 */
export class Grid {
    // Implementation
    static grid(renderTo, options, async) {
        if (async) {
            return new Promise((resolve) => {
                void new Grid(renderTo, options, (grid) => {
                    resolve(grid);
                });
            });
        }
        return new Grid(renderTo, options);
    }
    /* *
    *
    *  Constructor
    *
    * */
    /**
     * Constructs a new Grid.
     *
     * @param renderTo
     * The render target (container) of the Grid.
     *
     * @param options
     * The options of the Grid.
     *
     * @param afterLoadCallback
     * The callback that is called after the Grid is loaded.
     */
    constructor(renderTo, options, afterLoadCallback) {
        /**
         * The user options declared for the columns as an object of column ID to
         * column options.
         * @internal
         */
        this.columnOptionsMap = {};
        /**
         * The options that were declared by the user when creating the Grid
         * or when updating it.
         */
        this.userOptions = {};
        /**
         * The initial height of the container. Can be 0 also if not set.
         * @internal
         */
        this.initialContainerHeight = 0;
        /**
         * The list of currently shown popups.
         */
        this.popups = new Set();
        /**
         * Whether the Grid is rendered.
         */
        this.isRendered = false;
        /**
         * The flags that indicate which parts of the Grid are dirty and need to be
         * re-rendered.
         * @internal
         */
        this.dirtyFlags = new Set();
        /**
         * Internal redraw queue used to prevent concurrent `redraw()` calls from
         * interleaving async DOM work and corrupting the state (for example
         * rendering duplicate pagination controls when `update()` is called
         * multiple times without awaiting).
         */
        this.redrawQueue = Promise.resolve();
        this.renderTo = renderTo;
        this.loadUserOptions(options);
        this.id = this.options?.id || uniqueKey();
        this.querying = new QueryingController(this);
        this.locale = this.options?.lang?.locale || (this.container?.closest('[lang]')?.lang);
        this.time = new TimeBase(extend(this.options?.time, { locale: this.locale }), this.options?.lang);
        fireEvent(this, 'beforeLoad');
        Grid.grids.push(this);
        void this.render().then(() => {
            afterLoadCallback?.(this);
            fireEvent(this, 'afterLoad');
        });
    }
    /* *
     *
     *  Methods
     *
     * */
    /**
     * The data source of the Grid. It contains the original data table
     * that was passed to the Grid.
     *
     * @deprecated Use `dataProvider` instead.
     */
    get dataTable() {
        const dp = this.dataProvider;
        if (dp && 'getDataTable' in dp) {
            return dp.getDataTable();
        }
    }
    /**
     * The presentation table of the Grid. It contains a modified version
     * of the data table that is used for rendering the Grid content. If
     * not modified, just a reference to the original data table.
     *
     * @deprecated Use `dataProvider` instead.
     */
    get presentationTable() {
        const dp = this.dataProvider;
        if (dp && 'getDataTable' in dp) {
            return dp.getDataTable(true);
        }
    }
    /*
     * Initializes the accessibility controller.
     */
    initAccessibility() {
        this.accessibility?.destroy();
        delete this.accessibility;
        if (this.options?.accessibility?.enabled) {
            this.accessibility = new Accessibility(this);
        }
    }
    /*
     * Initializes the pagination.
     */
    initPagination() {
        this.pagination?.destroy();
        delete this.pagination;
        if (this.options?.pagination?.enabled) {
            this.pagination = new Pagination(this);
        }
    }
    /**
     * Initializes the container of the Grid.
     *
     * @param renderTo
     * The render target (html element or id) of the Grid.
     *
     */
    initContainer(renderTo) {
        const container = (typeof renderTo === 'string') ?
            Globals.win.document.getElementById(renderTo) : renderTo;
        // Display an error if the renderTo is wrong
        if (!container) {
            throw new Error('Rendering div not found. It is unable to find the HTML ' +
                'element to render the Grid in.');
        }
        this.container = container;
        this.container.style.minHeight = 0 + 'px';
        this.container.innerHTML = AST.emptyHTML;
        this.contentWrapper = makeHTMLElement('div', {
            className: Globals.getClassName('container')
        }, this.container);
    }
    /**
     * Loads the new user options to all the important fields (`userOptions`,
     * `options` and `columnOptionsMap`).
     *
     * @param newOptions
     * The options that were declared by the user.
     *
     * @param oneToOne
     * When `false` (default), the existing column options will be merged with
     * the ones that are currently defined in the user options. When `true`,
     * the columns not defined in the new options will be removed.
     *
     * @returns
     * An object of the changed options.
     */
    loadUserOptions(newOptions, oneToOne = false) {
        // Operate on a copy of the options argument
        newOptions = merge(newOptions);
        const diff = {};
        if (newOptions.columns) {
            if (oneToOne) {
                diff.columns = this.setColumnOptionsOneToOne(newOptions.columns);
            }
            else {
                diff.columns = this.setColumnOptions(newOptions.columns);
            }
            delete newOptions.columns;
        }
        if (diff.columns && Object.keys(diff.columns).length < 1) {
            // Remove the columns property if it is empty object
            delete diff.columns;
        }
        merge(true, diff, diffObjects(newOptions, this.userOptions));
        this.userOptions = merge(this.userOptions, newOptions);
        this.options = merge(this.options ?? defaultOptions, this.userOptions);
        this.viewport?.columns.forEach((column) => {
            column.options = createOptionsProxy(this.columnOptionsMap?.[column.id]?.options ?? {}, this.options?.columnDefaults);
        });
        return diff;
    }
    /**
     * Cleans up and reloads the column options from the `userOptions.columns`.
     * Generates the internal column options map from the options.columns array.
     */
    reloadColumnOptions() {
        const colOptions = this.userOptions.columns;
        if (!colOptions) {
            this.columnOptionsMap = {};
            return;
        }
        if (colOptions.length < 1) {
            delete this.userOptions.columns;
            this.columnOptionsMap = {};
            return;
        }
        const columnOptionsMap = {};
        for (let i = 0, iEnd = colOptions.length; i < iEnd; ++i) {
            columnOptionsMap[colOptions[i].id] = {
                index: i,
                options: colOptions[i]
            };
        }
        this.columnOptionsMap = columnOptionsMap;
    }
    /**
     * Sets the new column options to the userOptions field.
     *
     * @param newColumnOptions
     * The new column options that should be loaded.
     *
     * @param overwrite
     * Whether to overwrite the existing column options with the new ones.
     * Default is `false`.
     *
     * @returns
     * An object of the changed column options in form of a record of
     * `[column.id]: column.options`.
     *
     * @internal
     */
    setColumnOptions(newColumnOptions, overwrite = false) {
        const columnDiffOptions = {};
        if (!this.userOptions.columns) {
            this.userOptions.columns = this.options?.columns ?? [];
        }
        const columnOptions = this.userOptions.columns;
        for (let i = 0, iEnd = newColumnOptions.length; i < iEnd; ++i) {
            const newOptions = newColumnOptions[i];
            const colOptionsIndex = this.columnOptionsMap?.[newOptions.id]?.index ?? -1;
            // If the new column options contain only the id.
            if (Object.keys(newOptions).length < 2) {
                if (overwrite && colOptionsIndex !== -1) {
                    columnDiffOptions[newOptions.id] = diffObjects(columnOptions[colOptionsIndex], { id: newOptions.id }, true);
                    columnOptions.splice(colOptionsIndex, 1);
                }
                continue;
            }
            let diff;
            if (colOptionsIndex === -1) {
                diff = merge(newOptions);
                columnOptions.push(newOptions);
            }
            else if (overwrite) {
                const prevOptions = columnOptions[colOptionsIndex];
                diff = merge(diffObjects(prevOptions, newOptions, true), diffObjects(newOptions, prevOptions));
                columnOptions[colOptionsIndex] = newOptions;
            }
            else {
                const prevOptions = columnOptions[colOptionsIndex];
                diff = diffObjects(newOptions, prevOptions);
                merge(true, prevOptions, newOptions);
            }
            delete diff.id;
            if (Object.keys(diff).length > 0) {
                columnDiffOptions[newOptions.id] = diff;
            }
        }
        this.reloadColumnOptions();
        return columnDiffOptions;
    }
    /**
     * Loads the new column options to the userOptions field in a one-to-one
     * manner. It means that all the columns that are not defined in the new
     * options will be removed.
     *
     * @param newColumnOptions
     * The new column options that should be loaded.
     *
     * @returns
     * The difference between the previous and the new column options in form
     * of a record of `[column.id]: column.options`.
     */
    setColumnOptionsOneToOne(newColumnOptions) {
        const prevColumnOptions = this.userOptions.columns;
        const columnOptions = [];
        const columnDiffOptions = {};
        let prevOptions;
        for (let i = 0, iEnd = newColumnOptions.length; i < iEnd; ++i) {
            const newOptions = newColumnOptions[i];
            const indexInPrevOptions = prevColumnOptions?.findIndex((prev) => prev.id === newOptions.id);
            if (indexInPrevOptions !== void 0 && indexInPrevOptions !== -1) {
                prevOptions = prevColumnOptions?.[indexInPrevOptions];
            }
            const diffOptions = diffObjects(newOptions, prevOptions ?? {});
            if (Object.keys(diffOptions).length > 0) {
                delete diffOptions.id;
                columnDiffOptions[newOptions.id] = diffOptions;
            }
            const resultOptions = merge(prevOptions ?? {}, newOptions);
            if (Object.keys(resultOptions).length > 1) {
                columnOptions.push(resultOptions);
            }
        }
        this.userOptions.columns = columnOptions;
        this.reloadColumnOptions();
        return columnDiffOptions;
    }
    /**
     * Updates the Grid with new options.
     *
     * @param options
     * The options of the Grid that should be updated. If not provided,
     * the update will be proceeded based on the `this.userOptions` property.
     * The `column` options are merged using the `id` property as a key.
     *
     * @param redraw
     * Whether to redraw the Grid after updating the options.
     *
     * @param oneToOne
     * When `false` (default), the existing column options will be merged with
     * the ones that are currently defined in the user options. When `true`,
     * the columns not defined in the new options will be removed.
     */
    update(options = {}, redraw = true, oneToOne = false) {
        fireEvent(this, 'beforeUpdate', {
            scope: 'grid',
            options,
            redraw,
            oneToOne
        });
        const { viewport } = this;
        const diff = this.loadUserOptions(options, oneToOne);
        const flags = this.dirtyFlags;
        if (viewport) {
            if (!this.dataProvider ||
                ('data' in diff) ||
                ('dataTable' in diff)) {
                if ( // Handle backward compatibility
                diff.dataTable &&
                    this.options?.dataTable &&
                    this.options?.data?.providerType === 'local') {
                    const userDT = this.options.dataTable;
                    this.options.data.dataTable = 'clone' in userDT ?
                        userDT : new DataTable(userDT);
                }
                this.loadDataProvider(); // Rebuild the data provider
                // TODO(update): Sometimes it can be too much, so we need to
                // check if the columns have changed or just their data. If
                // just their data, we can just mark the grid.table as dirty
                // instead of the whole grid.
                flags.add('grid');
            }
            if ('columns' in diff) {
                const ids = Object.keys(diff.columns ?? {});
                for (const id of ids) {
                    // TODO(update): Move this to the column update method.
                    this.loadColumnOptionDiffs(viewport, id, diff.columns?.[id]);
                    delete diff.columns?.[id];
                }
                delete diff.columns;
            }
            if ('columnDefaults' in diff) {
                this.loadColumnOptionDiffs(viewport, null, diff.columnDefaults);
                delete diff.columnDefaults;
            }
            if (diff.lang) {
                const langDiff = diff.lang;
                if ('locale' in langDiff) {
                    this.locale = langDiff.locale;
                    this.time.update({ locale: this.locale });
                }
                delete langDiff.locale;
                // TODO(update): Add more lang diff checks here.
                if (Object.keys(langDiff).length > 0) {
                    flags.add('grid');
                }
            }
            delete diff.lang;
            if ('time' in diff) {
                this.time.update(diff.time);
                delete diff.time;
            }
            if (diff.pagination) {
                const paginationDiff = diff.pagination;
                if ('enabled' in paginationDiff) {
                    if (!this.pagination && paginationDiff.enabled) {
                        this.pagination = new Pagination(this);
                    }
                }
                this.pagination?.update(paginationDiff);
            }
            delete diff.pagination;
            // TODO(update): Add more options that can be optimized here.
            if (Object.keys(diff).length > 0) {
                flags.add('grid');
            }
        }
        else {
            flags.add('grid');
        }
        const finish = () => {
            fireEvent(this, 'afterUpdate', {
                scope: 'grid',
                options,
                redraw,
                oneToOne
            });
        };
        if (redraw) {
            return this.redraw().then(finish);
        }
        finish();
    }
    /**
     * Loads the column option diffs by updating the dirty flags.
     *
     * @param vp
     * The viewport that the column option diffs should be loaded for.
     *
     * @param columnId
     * The ID of the column that should be updated.
     *
     * @param columnDiff
     * The difference between the previous and the new column options in form
     * of a record of `[column.id]: column.options`. If `null`, assume that
     * it refers to the column defaults.
     */
    loadColumnOptionDiffs(vp, columnId, columnDiff = {}) {
        if (Object.keys(columnDiff).length < 1) {
            return;
        }
        const flags = this.dirtyFlags;
        const column = columnId ? this.viewport?.getColumn(columnId) : null;
        if (column !== null && ( // Column null = column defaults
        (!column && columnDiff.enabled !== false) ||
            (column && columnDiff.enabled === false))) {
            flags.add('grid');
        }
        delete columnDiff.enabled;
        if ('cells' in columnDiff) {
            const cellsDiff = columnDiff.cells ?? {};
            if ('format' in cellsDiff ||
                'formatter' in cellsDiff ||
                'className' in cellsDiff) {
                // Optimization idea: list of columns to update
                flags.add('rows');
            }
            delete cellsDiff.format;
            delete cellsDiff.formatter;
            delete cellsDiff.className;
            if (Object.keys(cellsDiff).length > 0) {
                flags.add('rows');
            }
        }
        delete columnDiff.cells;
        if ('width' in columnDiff) {
            vp.columnResizing.isDirty = true;
        }
        delete columnDiff.width;
        if ('sorting' in columnDiff) {
            const sortingDiff = columnDiff.sorting ?? {};
            if ('compare' in sortingDiff ||
                'order' in sortingDiff) {
                flags.add('sorting');
            }
            delete sortingDiff.compare;
            delete sortingDiff.order;
            delete sortingDiff.orderSequence;
            // Idea: sortable - redraw only header cell
            if (Object.keys(sortingDiff).length > 0) {
                flags.add('grid');
            }
        }
        delete columnDiff.sorting;
        if ('filtering' in columnDiff) {
            const filteringDiff = columnDiff.filtering ?? {};
            if ('condition' in filteringDiff ||
                'value' in filteringDiff) {
                flags.add('filtering');
            }
            delete filteringDiff.condition;
            delete filteringDiff.value;
            if (Object.keys(filteringDiff).length > 0) {
                flags.add('grid');
            }
        }
        delete columnDiff.filtering;
        if (Object.keys(columnDiff).length > 0) {
            flags.add('grid');
        }
    }
    /**
     * Redraws the Grid in more optimized way than the regular render method.
     * It checks what parts of the Grid are marked as dirty and redraws only
     * them minimizing the number of DOM operations.
     */
    async redraw() {
        const run = async () => {
            fireEvent(this, 'beforeRedraw');
            const flags = this.dirtyFlags;
            const flagsToProcess = new Set(flags);
            const { viewport: vp, pagination } = this;
            const colResizing = vp?.columnResizing;
            const paginationWasDirty = !!pagination?.isDirtyQuerying;
            const colResizingWasDirty = !!colResizing?.isDirty;
            if (flagsToProcess.has('grid')) {
                await this.render(false);
                for (const flag of flagsToProcess) {
                    flags.delete(flag);
                }
                fireEvent(this, 'afterRedraw');
                return;
            }
            await this.dataProvider?.init();
            if (flagsToProcess.has('sorting') ||
                flagsToProcess.has('filtering') ||
                paginationWasDirty) {
                this.querying.loadOptions();
            }
            if (colResizingWasDirty) {
                colResizing?.loadColumns();
            }
            if (flagsToProcess.has('rows') ||
                flagsToProcess.has('sorting') ||
                flagsToProcess.has('filtering') ||
                paginationWasDirty) {
                await vp?.updateRows();
            }
            else if (flagsToProcess.has('reflow') ||
                colResizingWasDirty) {
                vp?.reflow();
            }
            const columns = vp?.columns ?? [];
            if (flagsToProcess.has('sorting') ||
                flagsToProcess.has('filtering')) {
                for (const column of columns) {
                    column.header?.toolbar?.refreshState();
                }
            }
            if (flagsToProcess.has('filtering')) {
                for (const column of columns) {
                    column.filtering?.refreshState();
                }
            }
            pagination?.redraw();
            delete colResizing?.isDirty;
            for (const flag of ['sorting', 'filtering']) {
                flags.delete(flag);
            }
            fireEvent(this, 'afterRedraw');
        };
        const queued = this.redrawQueue.then(run, run);
        // Keep the queue progressing even if one redraw fails.
        this.redrawQueue = queued['catch'](() => void 0);
        return queued;
    }
    /**
     * Updates the column of the Grid with new options.
     *
     * @param columnId
     * The ID of the column that should be updated.
     *
     * @param options
     * The options of the columns that should be updated. If null,
     * column options for this column ID will be removed.
     *
     * @param redraw
     * Whether to redraw the Grid after updating the columns.
     *
     * @param overwrite
     * If true, the column options will be updated by replacing the existing
     * options with the new ones instead of merging them.
     */
    async updateColumn(columnId, options, redraw = true, overwrite = false) {
        fireEvent(this, 'beforeUpdate', {
            scope: 'column',
            options,
            redraw,
            overwrite,
            columnId
        });
        const vp = this.viewport;
        const diffs = this.setColumnOptions([{
                id: columnId,
                ...options
            }], overwrite);
        const diff = diffs?.[columnId];
        if (diff && vp) {
            this.loadColumnOptionDiffs(vp, columnId, diff);
        }
        if (redraw) {
            await this.redraw();
        }
        fireEvent(this, 'afterUpdate', {
            scope: 'column',
            options,
            redraw,
            overwrite,
            columnId
        });
    }
    /**
     * Sets the sorting order for one or more columns. Provide the sortings
     * in priority order. Use `null` to clear sorting.
     *
     * @param sortings
     * The sorting definition in priority order.
     */
    async setSorting(sortings) {
        const viewport = this.viewport;
        if (!viewport) {
            return;
        }
        if (viewport.validator?.errorCell) {
            return;
        }
        const normalized = (sortings || []).filter((sorting) => !!(sorting.columnId && sorting.order));
        const sortingController = this.querying.sorting;
        const previousSortings = sortingController.currentSortings || [];
        const eventColumnIds = new Set();
        for (const sorting of previousSortings) {
            if (sorting.columnId) {
                eventColumnIds.add(sorting.columnId);
            }
        }
        for (const sorting of normalized) {
            eventColumnIds.add(sorting.columnId);
        }
        const eventColumns = Array.from(eventColumnIds)
            .map((columnId) => {
            const column = viewport.getColumn(columnId);
            if (!column) {
                return null;
            }
            const order = normalized.find((sorting) => sorting.columnId === columnId)?.order || null;
            return { column, order };
        })
            .filter((item) => !!item);
        for (const { column, order } of eventColumns) {
            [column, this].forEach((source) => {
                fireEvent(source, 'beforeSort', {
                    target: column,
                    order
                });
            });
        }
        sortingController.setSorting(normalized);
        await viewport.updateRows();
        const currentSortings = sortingController.currentSortings || [];
        const hasMultiple = currentSortings.length > 1;
        for (const column of viewport.columns) {
            const sortingIndex = currentSortings.findIndex((sorting) => sorting.columnId === column.id);
            if (sortingIndex !== -1 && currentSortings[sortingIndex].order) {
                const sorting = currentSortings[sortingIndex];
                const sortingOptions = {
                    order: sorting.order
                };
                if (hasMultiple) {
                    sortingOptions.priority = sortingIndex + 1;
                }
                column.setOptions({ sorting: sortingOptions });
                if (!hasMultiple) {
                    delete column.options.sorting?.priority;
                }
            }
            else {
                delete column.options.sorting?.order;
                delete column.options.sorting?.priority;
                if (column.options.sorting &&
                    Object.keys(column.options.sorting).length < 1) {
                    delete column.options.sorting;
                }
            }
            column.sorting?.refreshHeaderAttributes();
        }
        this.accessibility?.userSortedColumn(currentSortings[0]?.order || null);
        for (const { column, order } of eventColumns) {
            [column, this].forEach((source) => {
                fireEvent(source, 'afterSort', {
                    target: column,
                    order
                });
            });
        }
    }
    async render(clearDirtyFlags = true) {
        if (this.isRendered) {
            this.destroy(true);
        }
        await this.loadDataProvider().init();
        this.initContainer(this.renderTo);
        this.initAccessibility();
        this.initPagination();
        this.querying.loadOptions();
        await this.querying.proceed();
        await this.renderViewport();
        this.isRendered = true;
        if (clearDirtyFlags) {
            this.dirtyFlags.clear();
        }
    }
    /**
     * Hovers the row with the provided index. It removes the hover effect from
     * the previously hovered row.
     *
     * @param rowIndex
     * The index of the row.
     */
    hoverRow(rowIndex) {
        const rows = this.viewport?.rows;
        if (!rows) {
            return;
        }
        const firstRowIndex = this.viewport?.rows[0]?.index ?? 0;
        if (this.hoveredRowIndex !== void 0) {
            rows[this.hoveredRowIndex - firstRowIndex]?.setHoveredState(false);
        }
        if (rowIndex !== void 0) {
            rows[rowIndex - firstRowIndex]?.setHoveredState(true);
        }
        this.hoveredRowIndex = rowIndex;
    }
    /**
     * Hovers the column with the provided ID. It removes the hover effect from
     * the previously hovered column.
     *
     * @param columnId
     * The ID of the column.
     */
    hoverColumn(columnId) {
        const vp = this.viewport;
        if (!vp) {
            return;
        }
        if (this.hoveredColumnId) {
            vp.getColumn(this.hoveredColumnId)?.setHoveredState(false);
        }
        if (columnId) {
            vp.getColumn(columnId)?.setHoveredState(true);
        }
        this.hoveredColumnId = columnId;
    }
    /**
     * Sets the sync state to the row with the provided index. It removes the
     * synced effect from the previously synced row.
     *
     * @param rowIndex
     * The index of the row.
     */
    syncRow(rowIndex) {
        const rows = this.viewport?.rows;
        if (!rows) {
            return;
        }
        const firstRowIndex = this.viewport?.rows[0]?.index ?? 0;
        if (this.syncedRowIndex !== void 0) {
            rows[this.syncedRowIndex - firstRowIndex]?.setSyncedState(false);
        }
        if (rowIndex !== void 0) {
            rows[rowIndex - firstRowIndex]?.setSyncedState(true);
        }
        this.syncedRowIndex = rowIndex;
    }
    /**
     * Sets the sync state to the column with the provided ID. It removes the
     * synced effect from the previously synced column.
     *
     * @param columnId
     * The ID of the column.
     */
    syncColumn(columnId) {
        const vp = this.viewport;
        if (!vp) {
            return;
        }
        if (this.syncedColumnId) {
            vp.getColumn(this.syncedColumnId)?.setSyncedState(false);
        }
        if (columnId) {
            vp.getColumn(columnId)?.setSyncedState(true);
        }
        this.syncedColumnId = columnId;
    }
    /**
     * Render caption above the grid.
     * @internal
     */
    renderCaption() {
        const captionOptions = this.options?.caption;
        if (!captionOptions?.text || !this.contentWrapper) {
            return;
        }
        const tag = captionOptions.htmlTag?.toLowerCase();
        const tagName = tag && AST.allowedTags.includes(tag) ? tag : 'div';
        const defaultClass = Globals.getClassName('captionElement');
        const className = captionOptions.className ?
            `${defaultClass} ${captionOptions.className}` : defaultClass;
        this.captionElement = new AST([{
                tagName,
                attributes: { 'class': className, id: this.id + '-caption' },
                textContent: captionOptions.text
            }]).addToDOM(this.contentWrapper);
    }
    /**
     * Render description under the grid.
     *
     * @internal
     */
    renderDescription() {
        const descriptionOptions = this.options?.description;
        const descriptionText = descriptionOptions?.text;
        if (!descriptionText) {
            return;
        }
        // Create a description element.
        this.descriptionElement = makeHTMLElement('div', {
            className: Globals.getClassName('descriptionElement'),
            id: this.id + '-description'
        }, this.contentWrapper);
        // Render the description element content.
        setHTMLContent(this.descriptionElement, descriptionText);
        if (descriptionOptions.className) {
            this.descriptionElement.classList.add(...descriptionOptions.className.split(/\s+/g));
        }
    }
    /**
     * Resets the content wrapper of the Grid. It clears the content and
     * resets the class names.
     * @internal
     */
    resetContentWrapper() {
        if (!this.contentWrapper) {
            return;
        }
        this.contentWrapper.innerHTML = AST.emptyHTML;
        this.contentWrapper.className =
            Globals.getClassName('container') + ' ' +
                this.options?.rendering?.theme || '';
    }
    /**
     * Renders the viewport of the Grid. If the Grid is already
     * rendered, it will be destroyed and re-rendered with the new data.
     * @internal
     */
    async renderViewport() {
        const viewportMeta = this.viewport?.getStateMeta();
        const pagination = this.pagination;
        const paginationPosition = pagination?.options?.position;
        this.enabledColumns = await this.getEnabledColumnIDs();
        this.credits?.destroy();
        this.viewport?.destroy();
        delete this.viewport;
        this.resetContentWrapper();
        fireEvent(this, 'beforeRenderViewport');
        this.renderCaption();
        // Render top pagination if enabled (before table)
        if (paginationPosition === 'top') {
            pagination?.render();
        }
        if (this.enabledColumns.length > 0) {
            this.viewport = await this.renderTable();
            this.viewport.tableElement.setAttribute('id', this.id);
            if (viewportMeta && this.viewport) {
                this.viewport.applyStateMeta(viewportMeta);
            }
        }
        else {
            this.renderNoData();
        }
        await this.renderAccessibility();
        // Render bottom pagination, footer pagination,
        // or custom container pagination (after table).
        if (paginationPosition !== 'top') {
            pagination?.render();
        }
        this.renderDescription();
        fireEvent(this, 'afterRenderViewport');
        this.viewport?.reflow();
    }
    /**
     * Renders the Grid accessibility.
     * @internal
     */
    async renderAccessibility() {
        const accessibility = this.accessibility;
        if (!accessibility) {
            return;
        }
        await accessibility.setA11yOptions();
        accessibility.addScreenReaderSection('before');
        accessibility.addScreenReaderSection('after');
    }
    /**
     * Renders the table (viewport) of the Grid.
     *
     * @returns
     * The newly rendered table (viewport) of the Grid.
     */
    async renderTable() {
        this.tableElement = makeHTMLElement('table', {
            className: Globals.getClassName('tableElement')
        }, this.contentWrapper);
        this.tableElement.setAttribute('role', 'grid');
        const table = new Table(this, this.tableElement);
        await table.init();
        return table;
    }
    /**
     * Renders a message that there is no data to display.
     */
    renderNoData() {
        makeHTMLElement('div', {
            className: Globals.getClassName('noData'),
            innerText: this.options?.lang?.noData
        }, this.contentWrapper);
    }
    /**
     * Returns the array of IDs of columns that should be displayed in the data
     * grid, in the correct order.
     */
    async getEnabledColumnIDs() {
        const { columnOptionsMap } = this;
        const header = this.options?.header;
        const headerColumns = this.getColumnIds(header || [], false);
        const columnsIncluded = this.options?.rendering?.columns?.included || (headerColumns && headerColumns.length > 0 ?
            headerColumns : await this.dataProvider?.getColumnIds());
        if (!columnsIncluded?.length) {
            return [];
        }
        if (!columnOptionsMap) {
            return columnsIncluded;
        }
        let columnId;
        const result = [];
        for (let i = 0, iEnd = columnsIncluded.length; i < iEnd; ++i) {
            columnId = columnsIncluded[i];
            if (columnOptionsMap?.[columnId]?.options?.enabled !== false) {
                result.push(columnId);
            }
        }
        return result;
    }
    loadDataProvider() {
        this.dataProvider?.destroy();
        this.querying.shouldBeUpdated = true;
        const userDT = this.options?.dataTable;
        const dataOptions = this.options?.data ?? {
            providerType: 'local',
            dataTable: userDT ?? {}
        };
        // Just for the backward compatibility, remove in the future
        if (dataOptions.providerType === 'local' &&
            !dataOptions.dataTable && userDT) {
            dataOptions.dataTable = 'clone' in userDT ?
                userDT : new DataTable(userDT);
        }
        // End of backward compatibility snippet
        const DataProviderConstructor = DataProviderRegistry.types[dataOptions.providerType ?? 'local'] ??
            DataProviderRegistry.types.local;
        this.dataProvider = new DataProviderConstructor(this.querying, dataOptions);
        return this.dataProvider;
    }
    /**
     * Extracts all references to columnIds on all levels below defined level
     * in the settings.header structure.
     *
     * @param columnsTree
     * Structure that we start calculation
     *
     * @param [onlyEnabledColumns=true]
     * Extract all columns from header or columns filtered by enabled param
     * @returns
     */
    getColumnIds(columnsTree, onlyEnabledColumns = true) {
        let columnIds = [];
        const { enabledColumns } = this;
        for (const column of columnsTree) {
            const columnId = typeof column === 'string' ? column : column.columnId;
            if (columnId &&
                (!onlyEnabledColumns || (enabledColumns?.includes(columnId)))) {
                columnIds.push(columnId);
            }
            if (typeof column !== 'string' && column.columns) {
                columnIds = columnIds.concat(this.getColumnIds(column.columns, onlyEnabledColumns));
            }
        }
        return columnIds;
    }
    /**
     * Destroys the Grid.
     *
     * @param onlyDOM
     * Whether to destroy the Grid instance completely (`false` - default) or
     * just the DOM elements (`true`). If `true`, the Grid can be re-rendered
     * after destruction by calling the `render` method.
     */
    destroy(onlyDOM = false) {
        fireEvent(this, 'beforeDestroy');
        this.isRendered = false;
        const dgIndex = Grid.grids.findIndex((dg) => dg === this);
        this.dataProvider?.destroy();
        this.accessibility?.destroy();
        this.pagination?.destroy();
        this.viewport?.destroy();
        if (this.container) {
            this.container.innerHTML = AST.emptyHTML;
            this.container.classList.remove(Globals.getClassName('container'));
        }
        if (onlyDOM) {
            return;
        }
        // Clear all properties
        Object.keys(this).forEach((key) => {
            delete this[key];
        });
        Grid.grids.splice(dgIndex, 1);
    }
    /**
     * Grey out the Grid and show a loading indicator.
     *
     * @param message
     * The message to display in the loading indicator.
     */
    showLoading(message) {
        if (this.loadingWrapper) {
            return;
        }
        // Create loading wrapper.
        this.loadingWrapper = makeHTMLElement('div', {
            className: Globals.getClassName('loadingWrapper')
        }, this.contentWrapper);
        // Create spinner element.
        makeHTMLElement('div', {
            className: Globals.getClassName('loadingSpinner')
        }, this.loadingWrapper);
        // Create loading message span element.
        const loadingSpan = makeHTMLElement('span', {
            className: Globals.getClassName('loadingMessage')
        }, this.loadingWrapper);
        setHTMLContent(loadingSpan, pick(message, this.options?.lang?.loading, ''));
    }
    /**
     * Removes the loading indicator.
     */
    hideLoading() {
        this.loadingWrapper?.remove();
        delete this.loadingWrapper;
    }
    /**
     * Returns the grid data as a JSON string.
     *
     * **Note:** This method only works with `LocalDataProvider`.
     * For other data providers, use your data source directly.
     *
     * @deprecated
     *
     * @param modified
     * Whether to return the modified data table (after filtering/sorting/etc.)
     * or the unmodified, original one. Default value is set to `true`.
     *
     * @return
     * JSON representation of the data
     */
    getData(modified = true) {
        if (!this.dataProvider || !('getDataTable' in this.dataProvider)) {
            // eslint-disable-next-line no-console
            console.warn('getData() works only with LocalDataProvider.');
            return JSON.stringify({
                error: 'getData() works only with LocalDataProvider.'
            }, null, 2);
        }
        const dataTable = this.dataProvider.getDataTable(modified);
        const tableColumns = dataTable?.columns;
        const outputColumns = {};
        if (!this.enabledColumns || !tableColumns) {
            return '{}';
        }
        const typeParser = (type) => {
            const TypeMap = {
                number: Number,
                datetime: Number,
                string: String,
                'boolean': Boolean
            };
            return (value) => (defined(value) ? TypeMap[type](value) : null);
        };
        for (const columnId of Object.keys(tableColumns)) {
            const column = this.viewport?.getColumn(columnId);
            if (column) {
                const columnData = tableColumns[columnId];
                const parser = typeParser(column.dataType);
                outputColumns[columnId] = (() => {
                    const result = [];
                    for (let i = 0, iEnd = columnData.length; i < iEnd; ++i) {
                        result.push(parser(columnData[i]));
                    }
                    return result;
                })();
            }
        }
        return JSON.stringify(outputColumns, null, 2);
    }
    /**
     * Returns the current Grid options.
     *
     * @param onlyUserOptions
     * Whether to return only the user options or all options (user options
     * merged with the default ones). Default is `true`.
     *
     * @returns
     * Grid options.
     */
    getOptions(onlyUserOptions = true) {
        const options = onlyUserOptions ?
            merge(this.userOptions) :
            merge(this.options);
        // Keep `getOptions()` serializable:
        if (options.dataTable && 'clone' in options.dataTable) {
            options.dataTable = {
                columns: options.dataTable.columns
            };
        }
        if (options.data?.providerType === 'local') {
            if (options.data?.dataTable && 'clone' in options.data.dataTable) {
                options.data.columns = options.data.dataTable.columns;
            }
            if (options.data?.connector &&
                'initConverters' in options.data.connector) {
                options.data.connector = options.data.connector.options;
            }
        }
        // Clean up the column options by removing the ones that have no other
        // options than `id`:
        const oldColumnOptions = options.columns;
        if (oldColumnOptions) {
            options.columns = [];
            for (const columnOption of oldColumnOptions) {
                if (Object.keys(columnOption).length > 1) {
                    options.columns.push(columnOption);
                }
            }
        }
        return options;
    }
}
/* *
*
*  Properties
*
* */
/**
 * An array containing the current Grid objects in the page.
 * @private
 */
Grid.grids = [];
/* *
 *
 *  Default Export
 *
 * */
export default Grid;
