import { debounce, DeepMap } from '@infinite-table/infinite-react';
import { isPivotLayoutModel } from './isPivotLayoutModel';
import { LMEmitter } from './LMEmitter';
import { AUTO_GROUP_COLUMN_ID__MULTI_PREFIX, AUTO_GROUP_COLUMN_ID__SINGLE, normalizeLayoutModel, normalizePivotLayoutModel, normalizeTableLayoutModel, } from './normalizeLayoutModel';
import { getChanges, isLayoutEqual } from './isLayoutEqual';
import { simplifyLayoutModel, simplifyPivotLayoutModel, simplifyTableLayoutModel, } from './simplifyLayoutModel';
import { sortColumnIdsByOrder } from './sortColumnIdsByOrder';
import { destructurePivotColumnId } from './destructurePivotColumnId';
import { isPivotAggTotalColumn } from './isPivotAggTotalColumn';
import { isPivotGrandTotal } from './isPivotGrandTotal';
import { isPivotColumnTotal } from './isPivotColumnTotal';
export const PIVOT_ANY_TOTAL_COL_TYPE = 'pivotAnyTotal';
export const PIVOT_GRAND_TOTAL_COL_TYPE = 'pivotGrandTotal';
export const PIVOT_COLUMN_TOTAL_COL_TYPE = 'pivotColumnTotal';
export const PIVOT_AGGREGATION_TOTAL_COL_TYPE = 'pivotAggregationTotal';
function mapColDefs(colDefs, mapFn) {
    const iteration = (c) => {
        if (c.children) {
            return { ...c, children: mapColDefs(c.children, mapFn) };
        }
        const colDef = c;
        return mapFn(colDef);
    };
    return colDefs.map(iteration);
}
function flattenColDefs(colDefs) {
    const res = [];
    const iteration = (c) => {
        if (c.children) {
            c.children.forEach(iteration);
            return;
        }
        const colDef = c;
        res.push(colDef);
    };
    colDefs.forEach(iteration);
    return res;
}
function findInColDefs(colDefs, callback) {
    function iteration(colDefs) {
        for (const c of colDefs) {
            if (c.children) {
                const res = iteration(c.children);
                if (res) {
                    return res;
                }
            }
            else {
                const colDef = c;
                if (callback(colDef)) {
                    return colDef;
                }
            }
        }
    }
    return iteration(colDefs);
}
const DEFAULT_COLUMN_WIDTH = 200;
const DEFAULT_AGG_FUNC = 'sum';
/**
 *
 * @param columnIds the column ids that we want to be in the order
 * @param colDefs all the colDefs in the grid
 * @returns the array of the given column ids, but also any other column id that is not in the given column ids, but
 * has a column definition
 */
function getColumnOrderIdsForAllColDefs(columnIds = [], colDefs) {
    const flatColDefs = flattenColDefs(colDefs);
    const flatColIds = flatColDefs.map((c) => c.colId ?? c.field);
    if (columnIds.length === 0) {
        return flatColIds;
    }
    const sortedColIds = sortColumnIdsByOrder(flatColIds, columnIds);
    return sortedColIds;
}
const EXPAND_COLLAPSE_EVENTS = {
    expandOrCollapseAll: true,
    rowGroupOpened: true,
};
const COLUMN_EVENTS_THAT_TRIGGERS_AUTO_LAYOUT_SAVE = {
    displayedColumnsChanged: true,
    columnResized: true,
    sortChanged: true,
    columnRowGroupChanged: true,
    ...EXPAND_COLLAPSE_EVENTS,
    // TODO check if all of those are needed
    // stateUpdated: true,
    // columnPinned: true,
    // columnPivotChanged: true,
    // columnValueChanged: true,
};
function isGridLayoutSame(options) {
    const { layoutFromGrid: gridLayout, layoutFromModel: layout } = options;
    if (!!gridLayout != !!layout) {
        return false;
    }
    return isLayoutEqual(gridLayout, layout);
}
function getDefaultColumnSizeStateForColDef(colId, colDef, options) {
    if (!colDef) {
        colDef = findInColDefs(options?.columnDefs || [], (colDef) => colDef.colId ? colDef.colId === colId : colDef.field === colId);
        if (!colDef) {
            return undefined;
        }
    }
    let propsFromColType = {
        ...options?.defaultColDef,
    };
    if (options?.colTypes) {
        // for col sizing, we want the columns sized as flex via col.type and defaultColDef
        // to be sized as flex, even if they are not in the layout.ColumnSizing object
        let colType = colDef?.type;
        if (colType && !Array.isArray(colType)) {
            colType = colType.split(',');
        }
        if (Array.isArray(colType) && colType.length) {
            propsFromColType = colType.reduce((acc, type) => {
                const props = options.colTypes[type];
                if (props) {
                    return { ...acc, ...props };
                }
                return acc;
            }, {});
        }
    }
    if (propsFromColType.flex) {
        return {
            colId,
            flex: propsFromColType.flex,
        };
    }
    if (propsFromColType.width) {
        return {
            colId,
            width: propsFromColType.width,
        };
    }
    return undefined;
}
export class LayoutManager extends LMEmitter {
    constructor(options) {
        super({ debugId: options.debugId });
        this.fieldsToIds = {};
        this.idsToFields = {};
        this.idsToColDefs = {};
        this.initialColumnWidths = {};
        this.currentLayout = null;
        this.suppressGlobalAgGridEventListener = false;
        this.supressGlobalAgGridEventTimeoutId = null;
        this.globalAgGridEventListener = (type, event) => {
            if (this.destroyed || this.suppressGlobalAgGridEventListener) {
                return;
            }
            if (type in COLUMN_EVENTS_THAT_TRIGGERS_AUTO_LAYOUT_SAVE) {
                if (type === 'columnResized' && !event.finished) {
                    return;
                }
                // called while dragging a column from the sidebar, to the row groups panel
                if (type === 'displayedColumnsChanged' && event.source === 'uiColumnDragged') {
                    return;
                }
                this.warn(`globalAgGridEventListener-${type}`, event);
                if (!this.currentLayout) {
                    const gridLayout = this.getLayoutModelFromGrid();
                    // if there's no layout set currently in AG Grid
                    // then emit the gridLayoutChanged event directly
                    // as we don't have a current layout to compare to
                    this.onGridLayoutChanged(gridLayout);
                    return;
                }
                const gridLayout = this.getLayoutModelFromGrid();
                // but if we do have a current layout set
                // then only emit the event if the layout has changed
                if (isGridLayoutSame({
                    layoutFromGrid: gridLayout,
                    layoutFromModel: this.currentLayout,
                })) {
                    return;
                }
                this.onGridLayoutChanged(gridLayout);
            }
        };
        this.columnDefsChanged = (colDefs) => {
            if (this.suppressGlobalAgGridEventListener) {
                return;
            }
            this.warn('onColumnDefsChanged');
            this.indexColumns();
            if (this.currentLayout) {
                this.applyLayout(this.currentLayout);
            }
            this.emitSync('columnDefsChanged', colDefs);
        };
        this.layoutChangeDebounce = options.layoutChangeDebounce ?? 0;
        if (this.layoutChangeDebounce) {
            this.onGridLayoutChanged = debounce(this.onGridLayoutChanged, {
                wait: this.layoutChangeDebounce,
            }).bind(this);
        }
        this.setOptions(options);
        // this ensures the grand total columns are positioned correctly (before/after) even when changed at runtime
        // see https://www.ag-grid.com/react-data-grid/pivoting-column-groups/#changing-data-filters-and-configurations
        this.gridApi.setGridOption('enableStrictPivotColumnOrder', true);
        this.setupEvents();
        this.indexColumns();
        this.setupPivotTotals();
        globalThis.layoutManager = this;
    }
    destroy() {
        if (this.destroyed) {
            return;
        }
        super.destroy();
        if (this.setGridOption_default) {
            this.gridApi.setGridOption = this.setGridOption_default;
        }
        if (!this.gridApi.isDestroyed()) {
            this.gridApi.removeGlobalListener(this.globalAgGridEventListener);
        }
        this.gridApi = null;
    }
    setupEvents() {
        this.setupSetColumnDefs();
        this.gridApi.addGlobalListener(this.globalAgGridEventListener);
    }
    setupSetColumnDefs() {
        const setGridOption_default = this.gridApi.setGridOption;
        const self = this;
        this.setGridOption_default = setGridOption_default;
        const setGridOption = (key, value) => {
            let defaultSuppressColumnMoveAnimation = this.gridApi.getGridOption('suppressColumnMoveAnimation');
            if (key === 'columnDefs') {
                if (!defaultSuppressColumnMoveAnimation) {
                    setGridOption_default.call(this.gridApi, 'suppressColumnMoveAnimation', true);
                }
            }
            const res = setGridOption_default.call(this.gridApi, key, value);
            if (key === 'columnDefs') {
                if (!defaultSuppressColumnMoveAnimation) {
                    this.gridApi.setGridOption('suppressColumnMoveAnimation', false);
                }
                self.columnDefsChanged(value);
            }
            return res;
        };
        this.gridApi.setGridOption = setGridOption;
    }
    indexColumns() {
        const colDefs = this.gridApi.getColumnDefs();
        this.fieldsToIds = {};
        this.idsToFields = {};
        this.idsToColDefs = {};
        const iteration = (c) => {
            if (c.children) {
                c.children.forEach(iteration);
                return;
            }
            const colDef = c;
            const colId = colDef.colId ?? colDef.field;
            const field = colDef.field;
            if (field) {
                this.fieldsToIds[field] = colId;
                this.idsToFields[colId] = field;
            }
            this.idsToColDefs[colId] = colDef;
            // const col = this.gridApi.getColumn(colId);
            // this.initialColumnWidths[colId] = colDef.width ?? colDef.initialWidth ?? DEFAULT_COLUMN_WIDTH; // this.gridApi.getColumn(colId).getActualWidth();
            // this.initialColumnWidths[colId] = col.getActualWidth();
            this.initialColumnWidths[colId] = this.getDefaultColumnWidthForCol(colId, colDef);
            const rowGroupColumns = this.gridApi.getRowGroupColumns() ?? [];
            rowGroupColumns.forEach((column) => {
                const colId = column.getColId();
                // const col = this.gridApi.getColumn(colId);
                // const colDef = col.getColDef();
                this.initialColumnWidths[colId] = this.getDefaultColumnWidthForCol(colId);
                // colDef.width ?? colDef.initialWidth ?? DEFAULT_COLUMN_WIDTH;
            });
        };
        colDefs.forEach(iteration);
    }
    onGridLayoutChanged(layout) {
        // we want to have this check, since onGridLayoutChanged might be triggered
        // by some AG Grid actions we're not interested - eg: filter changed
        // and the state of which is not captured by the Layout Model
        // so it would result in a layout object that's similar to the previous one
        // that we fired
        // hence we need this check here as well
        if (this._prevFiredLayout && isLayoutEqual(layout, this._prevFiredLayout)) {
            return;
        }
        this.triggerGridLayoutChange(layout);
    }
    triggerGridLayoutChange(layout, prevLayout = this._prevFiredLayout, options) {
        layout = simplifyLayoutModel(layout);
        prevLayout = prevLayout ? simplifyLayoutModel(prevLayout) : undefined;
        const shouldSkipTriggerChange = options?.skipTriggerChange === true;
        this._prevFiredLayout = layout;
        this.currentLayout = layout;
        if (layout.RowGroupedColumns &&
            layout.RowGroupedColumns.length > 0 &&
            (prevLayout?.RowGroupedColumns || []).join(',') !== layout.RowGroupedColumns.join(',') &&
            layout.RowGroupValues) {
            // most likely the user has changed grouping via some AG Grid action
            // and we need to make sure the expand/collapse state is applied
            // but we want to suspend the listener
            // as it would re-trigger another change
            const unsupress = this.suspendAgGridListener();
            this.applyRowGroupValues(layout.RowGroupValues, layout.RowGroupedColumns);
            unsupress();
        }
        if (!shouldSkipTriggerChange) {
            // emit an event that the layout has changed from the grid
            this.emitSync('gridLayoutChanged', layout);
            const log = this.debugger.extend('gridLayoutChanged');
            const changes = getChanges(prevLayout, layout);
            log('current grid layout:', JSON.stringify(layout, null, 2), '\nprev layout:\n', prevLayout ? JSON.stringify(prevLayout, null, 2) : 'no prev layout', '\nchanges from prev to current:\n', changes);
        }
    }
    onChange(fn) {
        return this.on('gridLayoutChanged', fn);
    }
    onColumnDefsChanged(fn) {
        return this.on('columnDefsChanged', fn);
    }
    setOptions(options) {
        this.gridApi = options.gridApi;
    }
    getLayoutModelFromGrid() {
        return this.isInPivotMode()
            ? this.getPivotLayoutModelFromGrid()
            : this.getTableLayoutModelFromGrid();
    }
    getPivotLayoutModelFromGrid() {
        const pivotResultColumns = this.gridApi.getPivotResultColumns() || [];
        const pivotResultColumnsSet = new Set(pivotResultColumns.map((col) => col.getColId()));
        const prevLayout = this.currentLayout;
        const columnState = this.gridApi
            .getColumnState()
            .filter((col) => !pivotResultColumnsSet.has(col.colId));
        let PivotColumns = this.gridApi.getPivotColumns().map((col) => col.getColId());
        const layout = this.getUndecidedLayoutModelFromGrid(columnState);
        let ColumnSizing = layout.ColumnSizing || {};
        //let's also include the column widths of the pivotResult columns
        pivotResultColumns.forEach((col) => {
            ColumnSizing[col.getColId()] = {
                Width: col.getActualWidth(),
            };
        });
        if (!Object.keys(ColumnSizing).length) {
            ColumnSizing = undefined;
        }
        delete layout.TableColumns;
        const pivotLayout = {
            Name: layout.Name,
            Ignore_GridFilter: layout.Ignore_GridFilter,
            Ignore_ColumnFilters: layout.Ignore_ColumnFilters,
            Ignore_ColumnHeaders: layout.Ignore_ColumnHeaders,
            Ignore_AutoSizeColumns: layout.Ignore_AutoSizeColumns,
            Ignore_RowSummaries: layout.Ignore_RowSummaries,
            Ignore_IsReadOnly: layout.Ignore_IsReadOnly,
            Ignore_Tags: layout.Ignore_Tags,
            Ignore_Source: layout.Ignore_Source,
            Ignore_AdaptableVersion: layout.Ignore_AdaptableVersion,
            Ignore_Uuid: layout.Ignore_Uuid,
            PivotColumns,
            ColumnPinning: layout.ColumnPinning,
            ColumnSorts: layout.ColumnSorts,
            ColumnSizing,
            ColumnVisibility: layout.ColumnVisibility,
            RowGroupValues: layout.RowGroupValues,
            ColumnGroupValues: layout.ColumnGroupValues,
            PivotGroupedColumns: layout.RowGroupedColumns,
            PivotAggregationColumns: layout.TableAggregationColumns,
            GrandTotalRow: layout.GrandTotalRow,
            PivotExpandLevel: prevLayout?.PivotExpandLevel ?? -1,
        };
        if (layout.SuppressAggFuncInHeader) {
            pivotLayout.SuppressAggFuncInHeader = layout.SuppressAggFuncInHeader;
        }
        const pivotGrandTotal = this.gridApi.getGridOption('pivotRowTotals');
        if (pivotGrandTotal) {
            pivotLayout.PivotGrandTotal = pivotGrandTotal;
        }
        const pivotColumnTotal = this.gridApi.getGridOption('pivotColumnGroupTotals');
        if (pivotColumnTotal) {
            pivotLayout.PivotColumnTotal = pivotColumnTotal;
        }
        return simplifyPivotLayoutModel(pivotLayout);
    }
    getTableLayoutModelFromGrid() {
        return this.getUndecidedLayoutModelFromGrid(this.gridApi.getColumnState());
    }
    isTreeMode() {
        return this.gridApi.getGridOption('treeData');
    }
    getUndecidedLayoutModelFromGrid(columnState) {
        let TableColumns = columnState.map((c) => c.colId);
        // .filter((colId) => colId !== 'ag-Grid-SelectionColumn');
        let ColumnSorts = [];
        let RowGroupedColumns = [];
        let RowGroupValues = undefined;
        let ColumnGroupValues = undefined;
        let TableAggregationColumns = [];
        let ColumnPinning = {};
        const gridState = this.gridApi.getState();
        const prevLayout = this.currentLayout;
        const prevAggColumns = prevLayout?.TableAggregationColumns ?? prevLayout?.PivotAggregationColumns;
        let ColumnSizing = { ...prevLayout?.ColumnSizing };
        const prevAggColumnsMap = prevAggColumns?.reduce((acc, agg) => {
            acc[agg.ColumnId] = agg;
            return acc;
        }, {});
        let ColumnVisibility = columnState.reduce((acc, colState) => {
            const colId = colState.colId;
            // normally we take the initial width from this object
            let initialWidth = this.initialColumnWidths[colId];
            // but if this is a group column
            // we try to infer it from the default width (when display type is single)
            // or from the respective group column (when display type is multi)
            if (initialWidth === undefined) {
                if (colId === AUTO_GROUP_COLUMN_ID__SINGLE) {
                    const groupByColId = this.gridApi.getRowGroupColumns()[0]?.getColId();
                    initialWidth =
                        this.initialColumnWidths[colId] || this.initialColumnWidths[groupByColId];
                }
                else if (colId.startsWith(AUTO_GROUP_COLUMN_ID__MULTI_PREFIX)) {
                    initialWidth =
                        this.initialColumnWidths[colId.replace(AUTO_GROUP_COLUMN_ID__MULTI_PREFIX, '')];
                }
            }
            if (colState.flex != undefined) {
                const colSizing = { ...ColumnSizing[colId] };
                delete colSizing.Width;
                colSizing.Flex = colState.flex;
                ColumnSizing[colId] = colSizing;
            }
            // and only add the col width to the layout ColumnSizing
            // if it's different from the initial default column width
            else if (colState.width && initialWidth != colState.width) {
                const colSizing = { ...ColumnSizing[colId] };
                delete colSizing.Flex;
                colSizing.Width = colState.width;
                ColumnSizing[colId] = colSizing;
            }
            if (colState.hide) {
                acc[colId] = false;
            }
            if (colState.sort != undefined) {
                ColumnSorts[colState.sortIndex] = {
                    ColumnId: colId,
                    SortOrder: colState.sort === 'asc' ? 'Asc' : 'Desc',
                };
            }
            if (colState.rowGroupIndex != null) {
                RowGroupedColumns[colState.rowGroupIndex] = colId;
            }
            if (colState.pinned) {
                ColumnPinning[colId] = colState.pinned === true ? 'left' : colState.pinned;
            }
            return acc;
        }, {});
        if (gridState.pivot?.pivotMode && gridState.sort?.sortModel) {
            // for pivot mode, we can't simply take the current sort from the ColumnState
            // but we have to go via the sortModel from the grid state
            ColumnSorts = gridState.sort.sortModel.map((sort) => {
                return {
                    ColumnId: sort.colId,
                    SortOrder: sort.sort === 'asc' ? 'Asc' : 'Desc',
                };
            });
        }
        if (gridState.aggregation) {
            TableAggregationColumns = gridState.aggregation.aggregationModel.map((agg) => {
                const prevAggForColumn = prevAggColumnsMap?.[agg.colId];
                if (prevAggForColumn && prevAggForColumn.AggFunc.aggFunc === agg.aggFunc) {
                    return prevAggForColumn;
                }
                return {
                    ColumnId: agg.colId,
                    AggFunc: {
                        aggFunc: agg.aggFunc,
                    },
                };
            });
        }
        // if there's a current layout set
        // we want to only return the AG Grid columns that are also in the current layout
        if (this.currentLayout &&
            this.currentLayout.TableColumns &&
            this.currentLayout.TableColumns.length > 0) {
            const colsInLayout = new Set(this.currentLayout.TableColumns);
            if (this.currentLayout.RowGroupedColumns?.length) {
                const groupDisplayType = this.gridApi.getGridOption('groupDisplayType');
                if (groupDisplayType === 'singleColumn') {
                    colsInLayout.add(AUTO_GROUP_COLUMN_ID__SINGLE);
                }
                else {
                    this.currentLayout.RowGroupedColumns.forEach((colId) => {
                        colsInLayout.add(`${AUTO_GROUP_COLUMN_ID__MULTI_PREFIX}${colId}`);
                    });
                }
            }
            // if (this.isTreeMode()) {
            //   // colsInLayout.add(GROUP_COLUMN_ID__SINGLE);
            // }
            TableColumns = TableColumns.filter((colId) => {
                if (colsInLayout.has(colId)) {
                    return true;
                }
                // we might hit an edge case where we ungroup one of the 2+ group cols
                // in the current layout - in which case in the prev state of the current layout
                // we had the group column
                // but now we will need to include the current column
                if (colsInLayout.has(`${AUTO_GROUP_COLUMN_ID__MULTI_PREFIX}${colId}`)) {
                    // ColumnVisibility[colId] = false;
                    return true;
                }
                const col = this.gridApi.getColumn(colId);
                if (!col) {
                    return false;
                }
                return col.isVisible();
            });
            const columnOrderSet = new Set(TableColumns);
            Object.keys(ColumnSizing).forEach((colId) => {
                if (!columnOrderSet.has(colId)) {
                    delete ColumnSizing[colId];
                }
            });
            Object.keys(ColumnVisibility).forEach((colId) => {
                if (!columnOrderSet.has(colId)) {
                    delete ColumnVisibility[colId];
                }
            });
            TableAggregationColumns = TableAggregationColumns.filter((agg) => {
                if (columnOrderSet.has(agg.ColumnId)) {
                    return true;
                }
                return false;
            });
            Object.keys(ColumnPinning).forEach((colId) => {
                if (!columnOrderSet.has(colId)) {
                    delete ColumnPinning[colId];
                }
            });
        }
        if (RowGroupedColumns && RowGroupedColumns.length) {
            const LayoutRowGroupedColumns = this.currentLayout?.RowGroupedColumns || this.currentLayout?.PivotGroupedColumns || [];
            // if it's a new grouping, try and take it
            const isGroupingNew = RowGroupedColumns && RowGroupedColumns.join(',') !== LayoutRowGroupedColumns.join(',');
            if (this.currentLayout?.RowGroupValues) {
                const currentRowGroupValues = this.currentLayout.RowGroupValues;
                let currentRowGroupValuesGroupKeys = [];
                if (currentRowGroupValues.RowGroupDisplay === 'expanded' ||
                    currentRowGroupValues.RowGroupDisplay === 'collapsed') {
                    if (Array.isArray(currentRowGroupValues.GroupKeys)) {
                        currentRowGroupValuesGroupKeys =
                            currentRowGroupValues.GroupKeys.find((item) => {
                                return ((item.RowGroupedColumns || []).join(',') === (RowGroupedColumns || []).join(','));
                            })?.Values || [];
                    }
                    else {
                        currentRowGroupValuesGroupKeys = currentRowGroupValues.Values || [];
                    }
                }
                if (currentRowGroupValues.RowGroupDisplay === 'always-collapsed') {
                    RowGroupValues = {
                        RowGroupDisplay: 'always-collapsed',
                    };
                }
                else if (currentRowGroupValues.RowGroupDisplay === 'always-expanded') {
                    RowGroupValues = {
                        RowGroupDisplay: 'always-expanded',
                    };
                }
                else if (currentRowGroupValues.RowGroupDisplay === 'collapsed') {
                    const ExpandedValues = isGroupingNew
                        ? currentRowGroupValuesGroupKeys || []
                        : this.getRowGroupNodePathsAs({
                            expanded: true,
                        });
                    if (Array.isArray(currentRowGroupValues.GroupKeys)) {
                        RowGroupValues = {
                            RowGroupDisplay: 'collapsed',
                            GroupKeys: currentRowGroupValues.GroupKeys.map((item) => {
                                if ((item.RowGroupedColumns || []).join(',') === (RowGroupedColumns || []).join(',')) {
                                    return {
                                        RowGroupedColumns: item.RowGroupedColumns,
                                        Values: ExpandedValues,
                                    };
                                }
                                return item;
                            }),
                        };
                    }
                    else {
                        RowGroupValues = {
                            RowGroupDisplay: 'collapsed',
                            Values: ExpandedValues,
                        };
                    }
                }
                else if (currentRowGroupValues.RowGroupDisplay === 'expanded') {
                    const CollapsedValues = isGroupingNew
                        ? currentRowGroupValuesGroupKeys || []
                        : this.getRowGroupNodePathsAs({
                            expanded: false,
                        });
                    if (Array.isArray(currentRowGroupValues.GroupKeys)) {
                        RowGroupValues = {
                            RowGroupDisplay: 'expanded',
                            GroupKeys: currentRowGroupValues.GroupKeys.map((item) => {
                                if ((item.RowGroupedColumns || []).join(',') === (RowGroupedColumns || []).join(',')) {
                                    return {
                                        RowGroupedColumns: item.RowGroupedColumns,
                                        Values: CollapsedValues,
                                    };
                                }
                                return item;
                            }),
                        };
                    }
                    else {
                        RowGroupValues = {
                            RowGroupDisplay: 'expanded',
                            Values: CollapsedValues,
                        };
                    }
                }
            }
        }
        else {
            if (this.currentLayout?.RowGroupValues) {
                RowGroupValues = this.currentLayout.RowGroupValues;
            }
        }
        if (this.currentLayout?.ColumnGroupValues) {
            const currentColumnGroupValues = this.currentLayout.ColumnGroupValues;
            if (currentColumnGroupValues.ColumnGroupDisplay === 'always-expanded') {
                ColumnGroupValues = {
                    ColumnGroupDisplay: 'always-expanded',
                };
            }
            else if (currentColumnGroupValues.ColumnGroupDisplay === 'always-collapsed') {
                ColumnGroupValues = {
                    ColumnGroupDisplay: 'always-collapsed',
                };
            }
            else if (currentColumnGroupValues.ColumnGroupDisplay === 'expanded' ||
                currentColumnGroupValues.ColumnGroupDisplay === 'collapsed') {
                const isPivot = this.isCurrentLayoutPivot();
                const currentColumnGroupState = this.gridApi
                    .getColumnGroupState()
                    // if we're in a pivot layout, only include groups that are pivot groups
                    // as there are other fake groups in the grid as well
                    .filter((state) => (isPivot ? state.groupId.startsWith('pivotGroup_') : true));
                if (currentColumnGroupValues.ColumnGroupDisplay === 'expanded') {
                    const collapsedGroupIds = currentColumnGroupState
                        .filter((state) => {
                        return state.open === false;
                    })
                        .map((state) => state.groupId);
                    ColumnGroupValues = {
                        ColumnGroupDisplay: 'expanded',
                        Values: collapsedGroupIds,
                    };
                }
                else if (currentColumnGroupValues.ColumnGroupDisplay === 'collapsed') {
                    const expandedGroupIds = currentColumnGroupState
                        .filter((state) => {
                        return state.open === true;
                    })
                        .map((state) => state.groupId);
                    ColumnGroupValues = {
                        ColumnGroupDisplay: 'collapsed',
                        Values: expandedGroupIds,
                    };
                }
            }
        }
        const layout = simplifyTableLayoutModel({
            Name: this.currentLayout?.Name || 'Default',
            Ignore_GridFilter: this.currentLayout?.Ignore_GridFilter,
            Ignore_ColumnFilters: this.currentLayout?.Ignore_ColumnFilters,
            Ignore_ColumnHeaders: this.currentLayout?.Ignore_ColumnHeaders,
            Ignore_AutoSizeColumns: this.currentLayout?.Ignore_AutoSizeColumns,
            Ignore_RowSummaries: this.currentLayout?.Ignore_RowSummaries,
            Ignore_IsReadOnly: this.currentLayout?.Ignore_IsReadOnly,
            Ignore_Tags: this.currentLayout?.Ignore_Tags,
            Ignore_Source: this.currentLayout?.Ignore_Source,
            Ignore_AdaptableVersion: this.currentLayout?.Ignore_AdaptableVersion,
            Ignore_Uuid: this.currentLayout?.Ignore_Uuid,
            TableColumns: TableColumns,
            ColumnVisibility,
            ColumnSizing,
            ColumnSorts,
            RowGroupedColumns,
            TableAggregationColumns,
            ColumnPinning: ColumnPinning,
            RowGroupValues,
            ColumnGroupValues,
            GrandTotalRow: this.gridApi.getGridOption('grandTotalRow'),
            SuppressAggFuncInHeader: this.gridApi.getGridOption('suppressAggFuncInHeader'),
        });
        return layout;
    }
    static getColumnDefsChangesForLayout(layout, colDefs) {
        const columnSizing = layout.ColumnSizing || {};
        const flatColDefs = flattenColDefs(colDefs);
        const changedColDefs = new Map();
        flatColDefs.forEach((colDef) => {
            const colId = colDef.colId ?? colDef.field;
            const colSizing = columnSizing[colId] || {};
            if (colDef) {
                let changed = false;
                if (colSizing.MinWidth != undefined && colSizing.MinWidth != colDef.minWidth) {
                    colDef = { ...colDef, minWidth: colSizing.MinWidth };
                    changed = true;
                }
                if (colSizing.MaxWidth != undefined && colSizing.MaxWidth != colDef.maxWidth) {
                    colDef = { ...colDef, maxWidth: colSizing.MaxWidth };
                    changed = true;
                }
                // this (width and flex) shouldn't be needed theoretically, as applyColumnState will apply sizing
                // using the newly computed column state
                // but for whatever reason, this is needed here, for some edge cases
                // eg: when a layout has a column width a width and minWidth
                // and then we create a new layout with a different width and different min width
                // then if we don't have this code, the new layout might wrongly apply the previous
                // minWidth as the current column width
                // see #edge-case-min-width-width-mismatch
                if (colSizing.Width !== undefined && colSizing.Width !== colDef.width) {
                    colDef = { ...colDef, width: colSizing.Width };
                }
                if (colSizing.Flex !== undefined && colSizing.Flex !== colDef.flex) {
                    colDef = { ...colDef, flex: colSizing.Flex };
                    changed = true;
                }
                if (changed) {
                    changedColDefs.set(colId, colDef);
                }
            }
        });
        if (changedColDefs.size) {
            return mapColDefs(colDefs, (colDef) => {
                const changedColDef = changedColDefs.get(colDef.colId);
                if (changedColDef) {
                    return changedColDef;
                }
                return colDef;
            });
        }
        return null;
    }
    static getAGGridInitialStateForLayout(layout, colDefs, options) {
        if (isPivotLayoutModel(layout)) {
            return LayoutManager.getAGGridInitialStateForPivotLayout(layout, colDefs);
        }
        return LayoutManager.getAGGridInitialStateForTableLayout(layout, colDefs, options);
    }
    static getAGGridInitialStateForTableLayout(layout, colDefs, _options) {
        if (!layout.TableColumns) {
            console.error(`TableColumns property is missing in table layout: `, layout);
            console.warn('Setting TableColumns to empty array.');
            layout.TableColumns = [];
        }
        // just in case the enduser provided falsy colIds, it will crash AG Grid internals
        layout.TableColumns = layout.TableColumns.filter((colId) => colId != undefined);
        layout = normalizeTableLayoutModel(layout, { isTree: _options?.isTree ?? false });
        const agGridState = {};
        const columnIds = getColumnOrderIdsForAllColDefs(layout.TableColumns, colDefs);
        agGridState.columnOrder = {
            orderedColIds: columnIds,
        };
        const flatColDefs = flattenColDefs(colDefs);
        const colDefsById = new Map(flatColDefs.map((colDef) => [colDef.colId ?? colDef.field, colDef]));
        const getColDef = (colId) => colDefsById.get(colId);
        agGridState.columnVisibility = {
            hiddenColIds: flatColDefs
                .filter((colDef) => {
                const colId = colDef.colId ?? colDef.field;
                return (layout.ColumnVisibility?.[colId] === false ||
                    !layout.TableColumns.includes(colId) ||
                    colDef.type === 'hiddenColumn' ||
                    (Array.isArray(colDef.type) && colDef.type.includes('hiddenColumn')));
            })
                .map((colDef) => colDef.colId ?? colDef.field),
        };
        if (layout.ColumnSizing) {
            const colsInColumnSizingModel = new Set();
            const columnSizingModel = Object.keys(layout.ColumnSizing)
                .map((colId) => {
                const colSizing = layout.ColumnSizing[colId];
                if (colSizing.Flex != undefined) {
                    colsInColumnSizingModel.add(colId);
                    return {
                        colId,
                        flex: colSizing.Flex,
                    };
                }
                if (colSizing.Width != undefined) {
                    colsInColumnSizingModel.add(colId);
                    return {
                        colId,
                        width: colSizing.Width ?? DEFAULT_COLUMN_WIDTH,
                    };
                }
            })
                .filter(Boolean);
            const options = {
                ..._options,
                columnDefs: colDefs,
            };
            columnIds.forEach((colId) => {
                if (!colsInColumnSizingModel.has(colId)) {
                    const colDef = getColDef(colId);
                    const colSizeState = getDefaultColumnSizeStateForColDef(colId, colDef, options);
                    if (colSizeState) {
                        columnSizingModel.push(colSizeState);
                    }
                }
            });
            agGridState.columnSizing = {
                columnSizingModel,
            };
        }
        if (layout.ColumnSorts) {
            agGridState.sort = {
                sortModel: layout.ColumnSorts.map((columnSort) => {
                    return {
                        colId: columnSort.ColumnId,
                        sort: columnSort.SortOrder === 'Asc' ? 'asc' : 'desc',
                    };
                }),
            };
        }
        if (layout.RowGroupedColumns) {
            agGridState.rowGroup = {
                groupColIds: layout.RowGroupedColumns,
            };
        }
        if (layout.ColumnPinning) {
            const columnPinning = {
                leftColIds: [],
                rightColIds: [],
            };
            Object.keys(layout.ColumnPinning).forEach((colId) => {
                const pinned = layout.ColumnPinning[colId];
                if (pinned === 'left') {
                    columnPinning.leftColIds.push(colId);
                }
                else if (pinned === 'right') {
                    columnPinning.rightColIds.push(colId);
                }
            });
            agGridState.columnPinning = columnPinning;
        }
        const TableAggregationColumns = (layout.TableAggregationColumns ?? []).map(({ ColumnId, AggFunc }) => ({
            colId: ColumnId,
            aggFunc: AggFunc.aggFunc,
        }));
        if (TableAggregationColumns.length) {
            agGridState.aggregation = {
                aggregationModel: TableAggregationColumns.map(({ colId, aggFunc }) => {
                    if (aggFunc === true) {
                        const colDef = getColDef(colId);
                        // fallback to SUM if no defaultAggFunc is defined
                        aggFunc = colDef?.defaultAggFunc || DEFAULT_AGG_FUNC;
                    }
                    return {
                        colId,
                        aggFunc,
                    };
                }),
            };
        }
        return agGridState;
    }
    static getAGGridInitialStateForPivotLayout(layout, colDefs) {
        layout = normalizePivotLayoutModel(layout);
        const agGridState = {};
        // const columnIds = getColumnOrderIdsForAllColDefs([], colDefs);
        // agGridState.columnOrder = {
        //   orderedColIds: columnIds,
        // };
        const flatColDefs = flattenColDefs(colDefs);
        const colDefsById = new Map(flatColDefs.map((colDef) => [colDef.colId ?? colDef.field, colDef]));
        const getColDef = (colId) => colDefsById.get(colId);
        // agGridState.columnVisibility = {
        //   hiddenColIds: flatColDefs
        //     .filter((colDef) => {
        //       const colId = colDef.colId ?? colDef.field;
        //       return (
        //         layout.ColumnVisibility?.[colId] === false ||
        //         !layout.OrderedColumns.includes(colId) ||
        //         colDef.hide ||
        //         colDef.initialHide
        //       );
        //     })
        //     .map((colDef) => colDef.colId ?? colDef.field),
        // };
        agGridState.pivot = {
            pivotMode: true,
            pivotColIds: layout.PivotColumns,
        };
        if (layout.ColumnSizing) {
            const columnSizingModel = Object.keys(layout.ColumnSizing)
                .map((colId) => {
                const colSizing = layout.ColumnSizing[colId];
                if (colSizing.Flex != undefined) {
                    return {
                        colId,
                        flex: colSizing.Flex,
                    };
                }
                return {
                    colId,
                    width: colSizing.Width ?? DEFAULT_COLUMN_WIDTH,
                };
            })
                .filter(Boolean);
            agGridState.columnSizing = {
                columnSizingModel,
            };
        }
        if (layout.ColumnSorts) {
            agGridState.sort = {
                sortModel: layout.ColumnSorts.map((columnSort) => {
                    return {
                        colId: columnSort.ColumnId,
                        sort: columnSort.SortOrder === 'Asc' ? 'asc' : 'desc',
                    };
                }),
            };
        }
        if (layout.PivotGroupedColumns) {
            agGridState.rowGroup = {
                groupColIds: layout.PivotGroupedColumns,
            };
        }
        if (layout.ColumnPinning) {
            const columnPinning = {
                leftColIds: [],
                rightColIds: [],
            };
            Object.keys(layout.ColumnPinning).forEach((colId) => {
                const pinned = layout.ColumnPinning[colId];
                if (pinned === 'left') {
                    columnPinning.leftColIds.push(colId);
                }
                else if (pinned === 'right') {
                    columnPinning.rightColIds.push(colId);
                }
            });
            agGridState.columnPinning = columnPinning;
        }
        const PivotAggregationColumns = (layout.PivotAggregationColumns || []).map(({ ColumnId, AggFunc }) => {
            const aggFunc = AggFunc.aggFunc;
            return {
                colId: ColumnId,
                aggFunc,
            };
        });
        if (PivotAggregationColumns.length) {
            agGridState.aggregation = {
                aggregationModel: PivotAggregationColumns.map(({ colId, aggFunc }) => {
                    if (aggFunc === true) {
                        const colDef = getColDef(colId);
                        // fallback to SUM if no defaultAggFunc is defined
                        aggFunc = colDef?.defaultAggFunc || DEFAULT_AGG_FUNC;
                    }
                    return {
                        colId,
                        aggFunc,
                    };
                }),
            };
        }
        return agGridState;
    }
    silentSetCurrentLayout(layout, options) {
        if (options?.normalize) {
            layout = normalizeLayoutModel(layout, { isTree: this.isTreeMode() });
        }
        this.currentLayout = layout;
        // we do this in order to avoid firing the `gridLayoutChanged` event
        // when actually applying the layout
        this._prevFiredLayout = layout;
    }
    setLayout(layout, options) {
        layout = normalizeLayoutModel(layout, { isTree: this.isTreeMode() });
        const shouldSkipTriggerChange = options?.skipTriggerChange === true;
        const shouldSkipEqualityCheck = options?.force === true;
        if (!shouldSkipEqualityCheck && isLayoutEqual(this.currentLayout, layout)) {
            return false;
        }
        const prevCurrent = this.currentLayout;
        this.silentSetCurrentLayout(layout);
        this.applyLayout(layout, options);
        this.triggerGridLayoutChange(layout, prevCurrent, {
            skipTriggerChange: shouldSkipTriggerChange,
        });
        return true;
    }
    isCurrentLayoutPivot() {
        return this.currentLayout && isPivotLayoutModel(this.currentLayout);
    }
    suspendAgGridListener() {
        if (this.supressGlobalAgGridEventTimeoutId) {
            clearTimeout(this.supressGlobalAgGridEventTimeoutId);
            this.supressGlobalAgGridEventTimeoutId = null;
        }
        this.suppressGlobalAgGridEventListener = true;
        return () => this.resumeAgGridListener();
    }
    resumeAgGridListener(options) {
        if (options?.immediate) {
            this.suppressGlobalAgGridEventListener = false;
            return;
        }
        this.supressGlobalAgGridEventTimeoutId = setTimeout(() => {
            this.suppressGlobalAgGridEventListener = false;
        }, 0);
    }
    applyLayout(layout, options) {
        this.warn('applyLayout', layout);
        // we want to do this supress/unsupress thing
        // as otherwise, our applyLayout will trigger an onChange event
        // which could trigger another setLayout
        // yes, we do check for same layout and bail out if it is the same
        // but we still want to avoid this unnecessary initial loop
        // which may cause more adaptable work
        const resume = this.suspendAgGridListener();
        const pivotMode = this.isInPivotMode();
        if (!!layout.SuppressAggFuncInHeader !== !!this.gridApi.getGridOption('suppressAggFuncInHeader')) {
            this.gridApi.setGridOption('suppressAggFuncInHeader', !!layout.SuppressAggFuncInHeader);
        }
        if (layout.GrandTotalRow) {
            let grandTotalRow = null;
            if (layout.GrandTotalRow === true) {
                grandTotalRow = 'top';
            }
            else if (typeof layout.GrandTotalRow === 'string') {
                switch (layout.GrandTotalRow) {
                    case 'top':
                    case 'bottom':
                    case 'pinnedTop':
                    case 'pinnedBottom':
                        grandTotalRow = layout.GrandTotalRow;
                        break;
                    default:
                        grandTotalRow = null;
                }
            }
            this.gridApi.setGridOption('grandTotalRow', grandTotalRow);
        }
        else {
            this.gridApi.setGridOption('grandTotalRow', null);
        }
        this.applyColumnDefsChanges(layout);
        if (isPivotLayoutModel(layout)) {
            try {
                const perfApplyPivot = this.beginPerf('applyLayout:pivot');
                this.gridApi.setGridOption('pivotMode', true);
                this.applyPivotLayout(layout, options);
                perfApplyPivot.end();
            }
            finally {
                resume();
            }
            return;
        }
        try {
            const perfApplyTable = this.beginPerf('applyLayout:table');
            if (pivotMode) {
                this.gridApi.setGridOption('pivotMode', false);
            }
            this.applyTableLayout(layout, options);
            perfApplyTable.end();
        }
        finally {
            resume();
        }
    }
    applyColumnDefsChanges(layout) {
        return this.applyColumnSizingColumnDefsChanges(layout);
    }
    applyColumnSizingColumnDefsChanges(layout) {
        const newColDefs = LayoutManager.getColumnDefsChangesForLayout(layout, this.gridApi.getColumnDefs());
        if (newColDefs) {
            this.gridApi.setGridOption('columnDefs', newColDefs);
            return true;
        }
        return false;
    }
    applyTableLayout(layout, options) {
        this.withSuppressColumnAnimation(() => {
            // if we apply the state here, before calling setGridOption for groupDisplayType
            // the order is not always correctly applied
            // this.gridApi.applyColumnState(state);
            const hasGroupedColumns = layout.RowGroupedColumns && layout.RowGroupedColumns.length;
            if (hasGroupedColumns) {
                const displayTypeFromLayout = layout.RowGroupDisplayType === 'multi' ? 'multipleColumns' : 'singleColumn';
                const groupDisplayType = this.gridApi.getGridOption('groupDisplayType');
                if (groupDisplayType !== displayTypeFromLayout) {
                    this.gridApi.setGridOption('groupDisplayType', displayTypeFromLayout);
                }
            }
            // so we apply the state at the end
            this.gridApi.applyColumnState(this.computeColumnStateForTableLayout(layout));
            // but also let's not forget to apply the row group values
            if (hasGroupedColumns && layout.RowGroupValues && !options?.skipApplyRowGroupsExpandedState) {
                this.applyRowGroupValues(layout.RowGroupValues, layout.RowGroupedColumns);
            }
            this.applyColumnGroupCollapseExpandState(layout);
        });
    }
    getRowGroupNodePathsAs(options) {
        const expanded = options.expanded === true;
        const result = [];
        this.gridApi.forEachNode((node) => {
            if (node.group) {
                const nodeExpanded = !!node.expanded;
                if (expanded === nodeExpanded) {
                    const nodePath = [];
                    let currentNode = node;
                    do {
                        nodePath.unshift(currentNode.key);
                        currentNode = currentNode.parent;
                    } while (currentNode.parent);
                    result.push(nodePath);
                }
            }
        });
        return result;
    }
    applyRowGroupValues(RowGroupValues, rowGroupedColumns) {
        if (!RowGroupValues) {
            return;
        }
        if (RowGroupValues.RowGroupDisplay === 'always-expanded') {
            this.gridApi.expandAll();
            return;
        }
        if (RowGroupValues.RowGroupDisplay === 'always-collapsed') {
            this.gridApi.collapseAll();
            return;
        }
        const defaultExpanded = RowGroupValues.RowGroupDisplay === 'expanded';
        let currentRowGroupedValues = RowGroupValues.Values;
        if (RowGroupValues.GroupKeys) {
            const matchingRowGroupColumnValues = RowGroupValues.GroupKeys.find((item) => {
                return (item.RowGroupedColumns || []).join(',') === (rowGroupedColumns || []).join(',');
            });
            if (matchingRowGroupColumnValues) {
                currentRowGroupedValues = matchingRowGroupColumnValues.Values;
            }
            else {
                currentRowGroupedValues = [];
            }
        }
        if (currentRowGroupedValues) {
            const deepMap = new DeepMap();
            currentRowGroupedValues.forEach((rowGroupValue) => {
                deepMap.set(rowGroupValue, true);
            });
            this.gridApi.forEachNode((node) => {
                if (!node.group) {
                    return;
                }
                if (node.group) {
                    const nodePath = [];
                    let currentNode = node;
                    do {
                        nodePath.unshift(currentNode.key);
                        currentNode = currentNode.parent;
                    } while (currentNode.parent);
                    const shouldBeExpanded = defaultExpanded
                        ? // by default nodes should be expanded
                            // but this node should be expanded if it is not in the map
                            !deepMap.has(nodePath)
                        : // by default nodes should be collapsed
                            // but this node should be expanded if it is in the map
                            deepMap.has(nodePath);
                    node.expanded = shouldBeExpanded;
                }
            });
            this.gridApi.onGroupExpandedOrCollapsed();
        }
    }
    computeColumnStateForPivotLayout(layout) {
        let columnState = {};
        const pivotColumnsToIndexes = (layout.PivotColumns || []).reduce((acc, colId, index) => {
            acc[colId] = index;
            return acc;
        }, {});
        const rowGroupColumnIndexes = (layout.PivotGroupedColumns || []).reduce((acc, colId, index) => {
            acc[colId] = index;
            return acc;
        }, {});
        // having this set to true will change pivot column order when there is sorting
        // and we don't want that to happen #keep-sorted-columns-in-same-order-as-unsorted
        // columnState.applyOrder = true;
        const columnIds = getColumnOrderIdsForAllColDefs([
            ...layout.PivotColumns,
            ...(layout.PivotAggregationColumns || []).map((col) => col.ColumnId),
        ], this.gridApi.getColumnDefs());
        columnState.state = columnIds.map((columnId) => {
            const pivotIndex = pivotColumnsToIndexes[columnId];
            const rowGroupIndex = rowGroupColumnIndexes[columnId];
            return {
                colId: columnId,
                pivot: pivotIndex != undefined,
                pivotIndex: pivotIndex ?? null,
                rowGroupIndex: rowGroupIndex ?? null,
                rowGroup: rowGroupIndex != null,
            };
        });
        this.computeColumnSizing(layout, columnState);
        this.computeColumnSorts(layout, columnState);
        this.computePinnedColumns(layout, columnState);
        this.computePivotAggregations(layout, columnState);
        return columnState;
    }
    computePivotAggregations(layout, columnState) {
        const PivotAggregationColumns = layout.PivotAggregationColumns || [];
        const pivotAggsToIndexes = PivotAggregationColumns.reduce((acc, { ColumnId }, index) => {
            acc[ColumnId] = index;
            return acc;
        }, {});
        columnState.state = columnState.state.map((colState) => {
            const columnId = colState.colId;
            const aggIndex = pivotAggsToIndexes[columnId];
            const aggFunc = aggIndex != null ? PivotAggregationColumns[aggIndex].AggFunc.aggFunc : null;
            return {
                ...colState,
                aggFunc: aggIndex != null ? (aggFunc === true ? DEFAULT_AGG_FUNC : aggFunc) : null,
            };
        });
    }
    computeColumnStateForTableLayout(layout) {
        const columnState = {};
        this.computeColumnOrderAndVisibility(layout, columnState);
        this.computeColumnSizing(layout, columnState);
        this.computeColumnSorts(layout, columnState);
        this.computeRowGrouping(layout, columnState);
        this.computeAggregations(layout, columnState);
        this.computePinnedColumns(layout, columnState);
        return columnState;
    }
    computePinnedColumns(layout, columnState) {
        const pinnedColumns = layout.ColumnPinning;
        if (!pinnedColumns || !Object.keys(pinnedColumns).length) {
            columnState.state.forEach((colState) => {
                colState.pinned = null;
            });
            return;
        }
        columnState.state.forEach((colState) => {
            const pinnedSide = pinnedColumns[colState.colId];
            if (pinnedSide) {
                colState.pinned = pinnedSide;
            }
            else {
                colState.pinned = null;
            }
        });
    }
    computeAggregations(layout, columnState) {
        const aggregations = layout.TableAggregationColumns;
        if (!aggregations || !aggregations.length) {
            columnState.state.forEach((colState) => {
                colState.aggFunc = null;
            });
            return;
        }
        const aggsMap = new Map(aggregations.map((agg) => [agg.ColumnId, agg]));
        columnState.state.forEach((colState) => {
            const agg = aggsMap.get(colState.colId)?.AggFunc;
            if (agg) {
                colState.aggFunc = agg.aggFunc === true ? 'sum' : agg.aggFunc;
            }
            else {
                colState.aggFunc = null;
            }
        });
    }
    computeRowGrouping(layout, columnState) {
        const rowGroupedColumns = layout.RowGroupedColumns || [];
        if (!rowGroupedColumns.length) {
            columnState.state.forEach((colState) => {
                colState.rowGroupIndex = null;
                colState.rowGroup = false;
            });
            return;
        }
        const rowGroupsToIndexes = rowGroupedColumns.reduce((acc, colId, index) => {
            acc[colId] = index;
            return acc;
        }, {});
        columnState.state.forEach((colState) => {
            if (rowGroupsToIndexes[colState.colId] != undefined) {
                colState.rowGroupIndex = rowGroupsToIndexes[colState.colId];
            }
            else {
                colState.rowGroupIndex = null;
                colState.rowGroup = false;
            }
        });
    }
    computeColumnSorts(layout, columnState) {
        const sorts = layout.ColumnSorts || [];
        if (!sorts.length) {
            columnState.state.forEach((colState) => {
                colState.sort = null;
                colState.sortIndex = null;
            });
            return;
        }
        const sortsToIndexes = sorts.reduce((acc, sort, index) => {
            acc[sort.ColumnId] = index;
            return acc;
        }, {});
        columnState.state.forEach((colState) => {
            const colSortIndex = sortsToIndexes[colState.colId];
            // we delete them just to know if there are any left
            // after the forEach is done
            delete sortsToIndexes[colState.colId];
            const colSort = sorts[colSortIndex];
            if (colSort != undefined) {
                colState.sort = colSort.SortOrder === 'Asc' ? 'asc' : 'desc';
                colState.sortIndex = colSortIndex;
            }
            else {
                colState.sort = null;
                colState.sortIndex = null;
            }
        });
        if (Object.keys(sortsToIndexes).length) {
            // if there are some sorts left which were not applied to the columnState
            // probably those are for pivot result columns, so we need to apply them as well
            Object.keys(sortsToIndexes).forEach((colId) => {
                const colSortIndex = sortsToIndexes[colId];
                const colSort = sorts[colSortIndex];
                if (colSort != undefined) {
                    columnState.state.push({
                        colId,
                        sort: colSort.SortOrder === 'Asc' ? 'asc' : 'desc',
                        sortIndex: colSortIndex,
                    });
                }
            });
        }
    }
    getDefaultColumnWidthForCol(colId, colDef) {
        colDef = colDef ?? this.gridApi.getColumnDef(colId);
        return colDef?.width ?? DEFAULT_COLUMN_WIDTH;
    }
    computeColumnSizing(layout, columnState) {
        const ColumnSizing = layout.ColumnSizing || {};
        const colDefs = this.gridApi.getColumnDefs();
        const options = {
            columnDefs: colDefs,
            colTypes: this.gridApi.getGridOption('columnTypes'),
            defaultColDef: this.gridApi.getGridOption('defaultColDef'),
        };
        columnState.state.forEach((colState) => {
            const colSizing = ColumnSizing[colState.colId];
            if (colSizing) {
                if (colSizing.Width != undefined) {
                    colState.width = colSizing.Width;
                }
                else if (colSizing.Flex != undefined) {
                    colState.flex = colSizing.Flex;
                }
            }
            else {
                let defaultColState;
                if (!colState.flex) {
                    defaultColState = getDefaultColumnSizeStateForColDef(colState.colId, undefined, options);
                    if (defaultColState?.flex) {
                        colState.flex = defaultColState.flex;
                    }
                }
                if (!colState.flex) {
                    colState.width =
                        colState.width ??
                            defaultColState?.width ??
                            this.getDefaultColumnWidthForCol(colState.colId);
                }
            }
        });
    }
    computeColumnOrderAndVisibility(layout, columnState) {
        const visibility = layout.ColumnVisibility || {};
        const columnOrderSet = new Set(layout.TableColumns);
        const isColHidden = (colId) => visibility[colId] === false || !columnOrderSet.has(colId);
        columnState = columnState ?? {};
        columnState.applyOrder = true;
        const columnIds = getColumnOrderIdsForAllColDefs(layout.TableColumns, this.gridApi.getColumnDefs());
        columnState.state = columnIds.map((columnId) => {
            return {
                colId: columnId,
                hide: isColHidden(columnId),
                pivot: false,
                // @ts-ignore
                pivotIndex: null,
            };
        });
        return columnState;
    }
    autoSizeColumns(columnIds) {
        const { currentLayout } = this;
        let tableColumns = [];
        if (currentLayout) {
            tableColumns = currentLayout.TableColumns;
            if (currentLayout.ColumnVisibility) {
                // dont auto size hidden columns
                tableColumns = tableColumns.filter((colId) => !currentLayout.ColumnVisibility[colId]);
            }
            if (currentLayout.ColumnSizing) {
                // dont auto size columns with a set sizing
                tableColumns = tableColumns.filter((colId) => !currentLayout.ColumnSizing[colId]);
            }
        }
        columnIds = columnIds ?? tableColumns;
        if (!columnIds.length) {
            return false;
        }
        this.gridApi.autoSizeColumns(columnIds);
        return columnIds;
    }
    applyPivotLayout(layout, options) {
        this.withSuppressColumnAnimation(() => {
            this.applyPivotTotals(layout);
            const hasGroupedColumns = layout.PivotGroupedColumns && !!layout.PivotGroupedColumns.length;
            if (hasGroupedColumns) {
                const displayTypeFromLayout = layout.RowGroupDisplayType === 'multi' ? 'multipleColumns' : 'singleColumn';
                const groupDisplayType = this.gridApi.getGridOption('groupDisplayType');
                if (groupDisplayType !== displayTypeFromLayout) {
                    this.gridApi.setGridOption('groupDisplayType', displayTypeFromLayout);
                }
            }
            const columnState = this.computeColumnStateForPivotLayout(layout);
            // by simply calling this.gridApi.applyColumnState(columnState)
            // the order of aggregations is not preserved/guaranteed by ag-grid
            // so we want to apply multiple states - adding aggregations one by one
            // first: no aggregation
            // second: [agg[0]]
            // third: [agg[0], agg[1]]
            // fourth: ...etc
            // so we apply the states for all aggregations except the last one
            //
            const aggregationsMapForOrder = (layout.PivotAggregationColumns || []).map((_, index) => layout.PivotAggregationColumns.slice(0, index));
            aggregationsMapForOrder.forEach((agg) => {
                // we're mutating the columnState here
                this.computePivotAggregations({ ...layout, PivotAggregationColumns: agg }, columnState);
                // and apply the state
                this.gridApi.applyColumnState(columnState);
            });
            // now recompute and apply the last one
            this.computePivotAggregations(layout, columnState);
            this.gridApi.applyColumnState(columnState);
            // let's also include the column widths of the pivotResult columns
            // we can't simply include those in the columnState because the columnState will also apply order
            // but we don't want to affect the order of the pivotResult columns
            // so we'll do it manually, and apply the widths after the columnState
            // by using the gridApi.setColumnWidths method
            if (layout.ColumnSizing) {
                const colWidthForPivotResultColumns = [];
                (this.gridApi.getPivotResultColumns() || []).forEach((col) => {
                    const colId = col.getColId();
                    if (layout.ColumnSizing[colId]?.Width != undefined) {
                        colWidthForPivotResultColumns.push({
                            key: colId,
                            newWidth: layout.ColumnSizing[colId].Width ?? DEFAULT_COLUMN_WIDTH,
                        });
                    }
                });
                if (colWidthForPivotResultColumns.length) {
                    this.gridApi.setColumnWidths(colWidthForPivotResultColumns);
                }
            }
            this.applyColumnGroupCollapseExpandState(layout);
            // but also let's not forget to apply the row group values
            if (hasGroupedColumns && layout.RowGroupValues && !options?.skipApplyRowGroupsExpandedState) {
                this.applyRowGroupValues(layout.RowGroupValues, layout.PivotGroupedColumns);
            }
        });
    }
    applyPivotTotals(layout) {
        /**
         * GrandTotalRow
         */
        // is common to both Table and Pivot and is applied in applyLayout()
        /**
         * PivotGrandTotal
         */
        if (layout.PivotGrandTotal) {
            const pivotGrandTotal = layout.PivotGrandTotal === true || layout.PivotGrandTotal === 'before'
                ? 'before'
                : layout.PivotGrandTotal === 'after'
                    ? 'after'
                    : null;
            this.gridApi.setGridOption('pivotRowTotals', pivotGrandTotal);
        }
        else {
            this.gridApi.setGridOption('pivotRowTotals', null);
        }
        /**
         * PivotColumnTotal
         */
        if (layout.PivotColumnTotal) {
            const pivotColumnTotal = layout.PivotColumnTotal === true || layout.PivotColumnTotal === 'before'
                ? 'before'
                : layout.PivotColumnTotal === 'after'
                    ? 'after'
                    : null;
            this.gridApi.setGridOption('pivotColumnGroupTotals', pivotColumnTotal);
        }
        else {
            this.gridApi.setGridOption('pivotColumnGroupTotals', null);
        }
    }
    applyColumnGroupCollapseExpandState(layout) {
        const IsPivotLayout = isPivotLayoutModel(layout);
        const allDisplayedColumnGroups = this.gridApi.getAllDisplayedColumnGroups();
        let PivotExpandLevel = layout.PivotExpandLevel;
        const ColumnGroupValues = layout.ColumnGroupValues;
        if (IsPivotLayout && !ColumnGroupValues && PivotExpandLevel === undefined) {
            PivotExpandLevel = -1;
        }
        const colGroupDefs = allDisplayedColumnGroups
            .map((colGroup) => colGroup.getDefinition())
            // we need to filter because there are some fake groups as well, no idea what for
            .filter(Boolean);
        const colGroupState = [];
        const columnGroupExceptions = ColumnGroupValues?.ColumnGroupDisplay === 'collapsed' ||
            ColumnGroupValues?.ColumnGroupDisplay === 'expanded'
            ? new Set(ColumnGroupValues.Values)
            : new Set();
        const processColGroupDefs = (colGroupDefs, currentLevel) => {
            colGroupDefs.forEach((colGroupDef) => {
                if ('children' in colGroupDef && colGroupDef.children) {
                    let open;
                    if (IsPivotLayout && !ColumnGroupValues) {
                        open =
                            PivotExpandLevel === -1 ? true : colGroupDef.pivotKeys?.length <= PivotExpandLevel;
                    }
                    if (ColumnGroupValues) {
                        if (ColumnGroupValues.ColumnGroupDisplay === 'always-expanded') {
                            open = true;
                        }
                        else if (ColumnGroupValues.ColumnGroupDisplay === 'always-collapsed') {
                            open = false;
                        }
                        else if (ColumnGroupValues.ColumnGroupDisplay === 'expanded') {
                            open = !columnGroupExceptions.has(colGroupDef.groupId);
                        }
                        else if (ColumnGroupValues.ColumnGroupDisplay === 'collapsed') {
                            open = columnGroupExceptions.has(colGroupDef.groupId);
                        }
                    }
                    colGroupState.push({
                        groupId: colGroupDef.groupId,
                        open,
                    });
                    processColGroupDefs(colGroupDef.children, currentLevel + 1);
                }
            });
        };
        processColGroupDefs(colGroupDefs, 1);
        if (colGroupState.length > 0) {
            this.gridApi.setColumnGroupState(colGroupState);
        }
    }
    withSuppressColumnAnimation(fn) {
        const suppressColumnMoveAnimation = this.gridApi.getGridOption('suppressColumnMoveAnimation');
        if (!suppressColumnMoveAnimation) {
            this.gridApi.setGridOption('suppressColumnMoveAnimation', true);
        }
        const res = fn();
        if (!suppressColumnMoveAnimation) {
            this.gridApi.setGridOption('suppressColumnMoveAnimation', false);
        }
        return res;
    }
    patchColDefType(colDef, colTypes) {
        const originalTypes = colDef.type == undefined ? [] : Array.isArray(colDef.type) ? colDef.type : [colDef.type];
        const columnTypes = new Set(originalTypes);
        colTypes.forEach((colType) => {
            columnTypes.add(colType);
        });
        colDef.type = Array.from(columnTypes);
    }
    setupPivotTotals() {
        const _original_processPivotResultColDef = this.gridApi.getGridOption('processPivotResultColDef');
        this.gridApi.setGridOption('processPivotResultColDef', (colDef) => {
            _original_processPivotResultColDef?.(colDef);
            if (!isPivotLayoutModel(this.currentLayout)) {
                return;
            }
            if (isPivotGrandTotal(colDef)) {
                this.patchColDefType(colDef, [PIVOT_ANY_TOTAL_COL_TYPE, PIVOT_GRAND_TOTAL_COL_TYPE]);
            }
            if (isPivotColumnTotal(colDef.colId)) {
                this.patchColDefType(colDef, [PIVOT_ANY_TOTAL_COL_TYPE, PIVOT_COLUMN_TOTAL_COL_TYPE]);
            }
        });
        const _original_processPivotResultColGroupDef = this.gridApi.getGridOption('processPivotResultColGroupDef');
        this.gridApi.setGridOption('processPivotResultColGroupDef', (colGroupDef) => {
            _original_processPivotResultColGroupDef?.(colGroupDef);
            if (!isPivotLayoutModel(this.currentLayout)) {
                return;
            }
            this.patchPivotAggregationTotal(colGroupDef);
        });
    }
    patchPivotAggregationTotal(colGroupDef) {
        const hasPivotAggTotals = (pivotLayout) => {
            return pivotLayout.PivotAggregationColumns?.some((aggCol) => !!aggCol.Total);
        };
        if (!isPivotLayoutModel(this.currentLayout) || !hasPivotAggTotals(this.currentLayout)) {
            return;
        }
        const pivotGrandTotalsBefore = [];
        const pivotGrandTotalsAfter = [];
        const pivotAggTotalsBefore = [];
        const pivotAggTotalsAfter = [];
        const normalColDefs = [];
        colGroupDef.children.forEach((colDef) => {
            if (isPivotGrandTotal(colDef)) {
                if (this.gridApi.getGridOption('pivotRowTotals') === 'after') {
                    pivotGrandTotalsAfter.push(colDef);
                }
                else {
                    pivotGrandTotalsBefore.push(colDef);
                }
                return;
            }
            if (isPivotAggTotalColumn(colDef)) {
                if (!colDef.colId.startsWith('pivot_')) {
                    this.warn(`Pivot total column ${colDef.colId} is not prefixed with 'pivot_', skipping...`);
                    return;
                }
                this.patchColDefType(colDef, [PIVOT_ANY_TOTAL_COL_TYPE, PIVOT_AGGREGATION_TOTAL_COL_TYPE]);
                // we do this for all total cols, but we will hide the ones that are not visible
                colDef.columnGroupShow = undefined;
                const totalColConfig = this.getPivotTotalColumnConfig(colDef, this.currentLayout);
                if (!totalColConfig.visible) {
                    colDef.hide = true;
                }
                else {
                    if (totalColConfig.position === 'after') {
                        pivotAggTotalsAfter.push(colDef);
                    }
                    else {
                        pivotAggTotalsBefore.push(colDef);
                    }
                }
            }
            else {
                normalColDefs.push(colDef);
            }
        });
        colGroupDef.children = [
            ...pivotGrandTotalsBefore,
            ...pivotAggTotalsBefore,
            ...normalColDefs,
            ...pivotAggTotalsAfter,
            ...pivotGrandTotalsAfter,
        ];
    }
    getPivotTotalColumnConfig(colDef, currentPivotLayout) {
        const defaultHiddenConfig = {
            visible: false,
        };
        const colIdInfo = destructurePivotColumnId(colDef, {
            pivotColIds: this.currentLayout.PivotColumns,
            aggColIds: (this.currentLayout.PivotAggregationColumns || []).map((col) => col.ColumnId),
        }, (message) => this.warn(message));
        if (colIdInfo === '!unknown!') {
            return defaultHiddenConfig;
        }
        const { pivotColumnId, aggregationColumnId } = colIdInfo;
        const layoutAggCol = currentPivotLayout.PivotAggregationColumns?.find((col) => col.ColumnId === aggregationColumnId);
        if (!layoutAggCol) {
            this.warn(`Pivot Totals: could NOT find aggregation(value) column with id ${aggregationColumnId}`);
            return defaultHiddenConfig;
        }
        const layoutPivotTotalColumn = layoutAggCol.Total;
        if (!layoutPivotTotalColumn) {
            return defaultHiddenConfig;
        }
        if (Array.isArray(layoutPivotTotalColumn)) {
            const pivotSpecificConfig = layoutPivotTotalColumn.find((config) => config.PivotColumnId === pivotColumnId);
            return {
                visible: pivotSpecificConfig && pivotSpecificConfig.ShowTotal !== false,
                position: pivotSpecificConfig?.ShowTotal === 'after' ? 'after' : 'before',
            };
        }
        else {
            return {
                visible: !!layoutPivotTotalColumn,
                position: layoutPivotTotalColumn === 'after' ? 'after' : 'before',
            };
        }
    }
    isInPivotMode() {
        return this.gridApi.isModuleRegistered('PivotModule') && this.gridApi.isPivotMode();
    }
}
