import { debounce, DeepMap } from '@infinite-table/infinite-react';
import { isPivotLayoutModel } from './isPivotLayoutModel';
import { LMEmitter } from './LMEmitter';
import { GROUP_COLUMN_ID__MULTI_PREFIX, GROUP_COLUMN_ID__SINGLE, normalizeLayoutModel, normalizePivotLayoutModel, normalizeTableLayoutModel, } from './normalizeLayoutModel';
import { isLayoutEqual } from './isLayoutEqual';
import { simplifyTableLayoutModel, simplifyPivotLayoutModel, simplifyLayoutModel, } from './simplifyLayoutModel';
import { sortColumnIdsByOrder } from './sortColumnIdsByOrder';
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;
}
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);
}
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) => {
            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.setupEvents();
        this.indexColumns();
        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);
    }
    getColDefById(colId) {
        return this.idsToColDefs[colId] ?? null;
    }
    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) {
        layout = simplifyLayoutModel(layout);
        const prevLayout = this._prevFiredLayout;
        this._prevFiredLayout = layout;
        this.currentLayout = layout;
        // emit an event that the layout has changed from the grid
        this.emitSync('gridLayoutChanged', layout);
        const log = this.debugger.extend('gridLayoutChanged');
        log('current grid layout:', JSON.stringify(layout, null, 2), '\nprev layout:\n', prevLayout ? JSON.stringify(prevLayout, null, 2) : 'no prev layout');
    }
    onChange(fn) {
        return this.on('gridLayoutChanged', fn);
    }
    onColumnDefsChanged(fn) {
        return this.on('columnDefsChanged', fn);
    }
    setOptions(options) {
        this.gridApi = options.gridApi;
    }
    getLayoutModelFromGrid() {
        return this.gridApi.isPivotMode()
            ? this.getPivotLayoutModelFromGrid()
            : this.getTableLayoutModelFromGrid();
    }
    getPivotLayoutModelFromGrid() {
        const pivotResultColumnsSet = new Set((this.gridApi.getPivotResultColumns() || []).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);
        delete layout.TableColumns;
        const pivotLayout = {
            PivotColumns,
            ColumnPinning: layout.ColumnPinning,
            ColumnSorts: layout.ColumnSorts,
            ColumnWidths: layout.ColumnWidths,
            ColumnVisibility: layout.ColumnVisibility,
            RowGroupValues: layout.RowGroupValues,
            PivotGroupedColumns: layout.RowGroupedColumns,
            PivotAggregationColumns: layout.TableAggregationColumns,
            PivotExpandLevel: prevLayout?.PivotExpandLevel ?? -1,
        };
        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 ColumnWidths = {};
        let ColumnSorts = [];
        let RowGroupedColumns = [];
        let RowGroupValues = undefined;
        let TableAggregationColumns = [];
        let ColumnPinning = {};
        const gridState = this.gridApi.getState();
        const prevLayout = this.currentLayout;
        const prevAggColumns = prevLayout?.TableAggregationColumns;
        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 === GROUP_COLUMN_ID__SINGLE) {
                    const groupByColId = this.gridApi.getRowGroupColumns()[0]?.getColId();
                    initialWidth =
                        this.initialColumnWidths[colId] || this.initialColumnWidths[groupByColId];
                }
                else if (colId.startsWith(GROUP_COLUMN_ID__MULTI_PREFIX)) {
                    initialWidth =
                        this.initialColumnWidths[colId.replace(GROUP_COLUMN_ID__MULTI_PREFIX, '')];
                }
            }
            // and only add the col width to the layout ColumnWidths
            // if it's different from the initial default column width
            if (colState.width && initialWidth != colState.width) {
                ColumnWidths[colId] = colState.width;
            }
            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.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(GROUP_COLUMN_ID__SINGLE);
                }
                else {
                    this.currentLayout.RowGroupedColumns.forEach((colId) => {
                        colsInLayout.add(`${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(`${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(ColumnWidths).forEach((colId) => {
                if (!columnOrderSet.has(colId)) {
                    delete ColumnWidths[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) {
            if (this.currentLayout?.RowGroupValues) {
                const currentRowGroupValues = this.currentLayout.RowGroupValues;
                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 = this.getRowGroupNodePathsAs({
                        expanded: true,
                    });
                    RowGroupValues = {
                        RowGroupDisplay: 'collapsed',
                        Values: ExpandedValues,
                    };
                }
                else if (currentRowGroupValues.RowGroupDisplay === 'expanded') {
                    const CollapsedValues = this.getRowGroupNodePathsAs({
                        expanded: false,
                    });
                    RowGroupValues = {
                        RowGroupDisplay: 'expanded',
                        Values: CollapsedValues,
                    };
                }
            }
        }
        const layout = simplifyTableLayoutModel({
            TableColumns: TableColumns,
            ColumnVisibility,
            ColumnWidths: ColumnWidths,
            ColumnSorts,
            RowGroupedColumns,
            TableAggregationColumns,
            ColumnPinning: ColumnPinning,
            RowGroupValues,
        });
        return layout;
    }
    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, _options);
        const agGridState = {};
        const columnIds = getColumnOrderIdsForAllColDefs(layout.TableColumns, colDefs);
        agGridState.columnOrder = {
            orderedColIds: columnIds,
        };
        const flatColDefs = flattenColDefs(colDefs);
        const getColDef = (colId) => flatColDefs.find((colDef) => colDef.colId === colId);
        agGridState.columnVisibility = {
            hiddenColIds: flatColDefs
                .filter((colDef) => {
                const colId = colDef.colId ?? colDef.field;
                return (layout.ColumnVisibility?.[colId] === false ||
                    !layout.TableColumns.includes(colId) ||
                    colDef.hide ||
                    colDef.initialHide);
            })
                .map((colDef) => colDef.colId ?? colDef.field),
        };
        if (layout.ColumnWidths) {
            agGridState.columnSizing = {
                columnSizingModel: Object.keys(layout.ColumnWidths).map((colId) => {
                    const width = layout.ColumnWidths[colId];
                    return {
                        colId,
                        width,
                    };
                }),
            };
        }
        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 getColDef = (colId) => flatColDefs.find((colDef) => colDef.colId === 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.ColumnWidths) {
            agGridState.columnSizing = {
                columnSizingModel: Object.keys(layout.ColumnWidths).map((colId) => {
                    const width = layout.ColumnWidths[colId];
                    return {
                        colId,
                        width,
                    };
                }),
            };
        }
        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 shouldSkipEqualityCheck = options?.force === true;
        if (!shouldSkipEqualityCheck && isLayoutEqual(this.currentLayout, layout)) {
            return false;
        }
        this.silentSetCurrentLayout(layout);
        this.applyLayout(layout, options);
        this.triggerGridLayoutChange(layout);
        return true;
    }
    isCurrentLayoutPivot() {
        return this.currentLayout && isPivotLayoutModel(this.currentLayout);
    }
    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
        if (this.supressGlobalAgGridEventTimeoutId) {
            clearTimeout(this.supressGlobalAgGridEventTimeoutId);
            this.supressGlobalAgGridEventTimeoutId = null;
        }
        const unsuppress = () => {
            this.supressGlobalAgGridEventTimeoutId = setTimeout(() => {
                this.suppressGlobalAgGridEventListener = false;
            }, 0);
        };
        this.suppressGlobalAgGridEventListener = true;
        const pivotMode = this.gridApi.isPivotMode();
        if (layout.SuppressAggFuncInHeader !== undefined &&
            layout.SuppressAggFuncInHeader !== this.gridApi.getGridOption('suppressAggFuncInHeader')) {
            // @ts-ignore
            this.gridApi.setGridOption('suppressAggFuncInHeader', layout.SuppressAggFuncInHeader);
        }
        if (isPivotLayoutModel(layout)) {
            try {
                const perfApplyPivot = this.beginPerf('applyLayout:pivot');
                this.gridApi.setGridOption('pivotMode', true);
                this.applyPivotLayout(layout, options);
                perfApplyPivot.end();
            }
            finally {
                unsuppress();
            }
            return;
        }
        try {
            const perfApplyTable = this.beginPerf('applyLayout:table');
            if (pivotMode) {
                this.gridApi.setGridOption('pivotMode', false);
            }
            this.applyTableLayout(layout, options);
            perfApplyTable.end();
        }
        finally {
            unsuppress();
        }
    }
    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);
            }
        });
    }
    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) {
        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';
        if (RowGroupValues.Values) {
            const deepMap = new DeepMap();
            RowGroupValues.Values.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;
        }, {});
        columnState.applyOrder = true;
        const columnIds = getColumnOrderIdsForAllColDefs([...layout.PivotColumns, ...layout.PivotAggregationColumns.map((col) => col.ColumnId)], this.gridApi.getColumnDefs());
        const pivotAggsToIndexes = layout.PivotAggregationColumns.reduce((acc, { ColumnId }, index) => {
            acc[ColumnId] = index;
            return acc;
        }, {});
        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.computeColumnWidths(layout, columnState);
        this.computeColumnSorts(layout, columnState);
        this.computePinnedColumns(layout, columnState);
        this.computePivotAggregations(layout, columnState);
        return columnState;
    }
    computePivotAggregations(layout, columnState) {
        const pivotAggsToIndexes = layout.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 ? layout.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.computeColumnWidths(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];
            const colSort = sorts[colSortIndex];
            if (colSort != undefined) {
                colState.sort = colSort.SortOrder === 'Asc' ? 'asc' : 'desc';
                colState.sortIndex = colSortIndex;
            }
            else {
                colState.sort = null;
                colState.sortIndex = null;
            }
        });
    }
    getDefaultColumnWidthForCol(colId, colDef) {
        colDef = colDef ?? this.gridApi.getColumnDef(colId);
        return colDef?.width ?? DEFAULT_COLUMN_WIDTH;
    }
    computeColumnWidths(layout, columnState) {
        const widthMap = layout.ColumnWidths;
        if (!widthMap || !Object.keys(widthMap).length) {
            columnState.state.forEach((colState) => {
                colState.width = this.getDefaultColumnWidthForCol(colState.colId);
            });
            return;
        }
        columnState.state.forEach((colState) => {
            if (widthMap[colState.colId]) {
                colState.width = widthMap[colState.colId];
            }
            else {
                colState.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.ColumnWidths) {
                // dont auto size columns with a set width
                tableColumns = tableColumns.filter((colId) => !currentLayout.ColumnWidths[colId]);
            }
        }
        columnIds = columnIds ?? tableColumns;
        if (!columnIds.length) {
            return false;
        }
        this.gridApi.autoSizeColumns(columnIds);
        return columnIds;
    }
    applyPivotLayout(layout, options) {
        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);
        this.applyPivotExpandLevel(layout);
        const hasGroupedColumns = layout.PivotGroupedColumns && layout.PivotGroupedColumns.length;
        // but also let's not forget to apply the row group values
        if (hasGroupedColumns && layout.RowGroupValues && !options?.skipApplyRowGroupsExpandedState) {
            this.applyRowGroupValues(layout.RowGroupValues);
        }
    }
    applyPivotExpandLevel(layout) {
        const PivotExpandLevel = layout.PivotExpandLevel ?? -1;
        const allDisplayedColumnGroups = this.gridApi.getAllDisplayedColumnGroups();
        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 processColGroupDefs = (colGroupDefs) => {
            colGroupDefs.forEach((colGroupDef) => {
                if ('children' in colGroupDef && colGroupDef.children) {
                    colGroupState.push({
                        groupId: colGroupDef.groupId,
                        open: PivotExpandLevel === -1 ? true : colGroupDef.pivotKeys?.length <= PivotExpandLevel,
                    });
                    processColGroupDefs(colGroupDef.children);
                }
            });
        };
        processColGroupDefs(colGroupDefs);
        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;
    }
}
