import { ExportModuleId } from '../Utilities/Constants/ModuleConstants';
import { waitForTimeout } from '../Utilities/waitForTimeout';
import { convertCSSAbsoluteFontSizeToPt, getVariableColor, sanitizeStyle, } from '../Utilities/Helpers/StyleHelper';
import FormatHelper, { DateFormatter } from '../Utilities/Helpers/FormatHelper';
import tinycolor from 'tinycolor2';
import StringExtensions from '../Utilities/Extensions/StringExtensions';
import { createUuid } from '../AdaptableState/Uuid';
import { inferSchema, initParser } from 'udsv';
import { AG_GRID_GROUPED_COLUMN, AG_GRID_SELECTION_COLUMN, } from '../Utilities/Constants/GeneralConstants';
export class AgGridExportAdapter {
    _adaptableInstance;
    /**
     * !!!
     * do NOT mutate this array reference, this is passed only initially to AG Grid and we can only change it's internal state
     */
    DANGER_excelStyles = [];
    originalExcelStyles = [];
    excelStylesCache = {};
    cellOrHeaderClassKey2ExcelStyleIdMap = {};
    excelStylesWithFormattedDate = {};
    constructor(_adaptableInstance) {
        this._adaptableInstance = _adaptableInstance;
    }
    get agGridApi() {
        return this._adaptableInstance.agGridAdapter.getAgGridApi();
    }
    get adaptableApi() {
        return this._adaptableInstance.api;
    }
    get exportOptions() {
        return this._adaptableInstance.api.optionsApi.getExportOptions();
    }
    get logger() {
        return this._adaptableInstance.logger;
    }
    static getExcelClassNameForCell(colId, primaryKeyValue, userDefinedCellClass) {
        let excelClassName = `--excel-cell-${colId}-${primaryKeyValue}`;
        if (excelClassName.indexOf(' ') > 0) {
            excelClassName = excelClassName.replace(/\s/g, '_');
        }
        return userDefinedCellClass != null
            ? `${excelClassName}-${Array.isArray(userDefinedCellClass)
                ? userDefinedCellClass.join('-')
                : userDefinedCellClass}`
            : excelClassName;
    }
    static getExcelClassNameForHeader(colId, userDefinedHeaderClass) {
        let excelClassName = `--excel-header-${colId}`;
        if (excelClassName.indexOf(' ') > 0) {
            excelClassName = excelClassName.replace(/\s/g, '_');
        }
        return userDefinedHeaderClass != null
            ? `${excelClassName}-${Array.isArray(userDefinedHeaderClass)
                ? userDefinedHeaderClass.join('-')
                : userDefinedHeaderClass}`
            : excelClassName;
    }
    destroy() {
        this.excelStylesCache = null;
        this.cellOrHeaderClassKey2ExcelStyleIdMap = null;
        this.excelStylesWithFormattedDate = null;
    }
    async exportData(config) {
        const { report, format, showProgressIndicator } = config;
        let excelStylesWerePatched = false;
        try {
            if (showProgressIndicator) {
                this.adaptableApi.userInterfaceApi.showProgressIndicator({
                    text: `Exporting ${report.Name}...`,
                });
                // waitForTimeout required to give the ProgressIndicator rendering a head-start (see rAF in ProgressIndicator implementation)
                // see #raf_progress_indicator
                await waitForTimeout(16);
            }
            this.adaptableApi.exportApi.internalApi.setExportInProgress(config.report.Name, config.format, config.destination);
            const exportProcessData = this.buildExportProcessData(config);
            const { exportContext, exportParams } = exportProcessData;
            if (this.adaptableApi.layoutApi.isCurrentLayoutPivot() &&
                config.report.ReportRowScope === 'AllRows') {
                // if in pivot mode but the user wants to export all rows (aka the table row data), we cannot use AG Grids row model
                // we have to use an intermediary transient AG Grid instance
                return this.adaptableApi.exportApi.internalApi.exportAllDataInPivotMode(exportProcessData);
            }
            if (exportContext.isVisualExcelReport) {
                // FIXME AFL patch styles only for exported columns!
                // or even better, only cells
                this.patchExcelStyles(exportContext);
                excelStylesWerePatched = true;
            }
            // 1. easiest case, we download the file using AG Grid
            // these methods will automatically handle the file download
            if (exportContext.destination === 'Download' && exportContext.isExcelReport) {
                this.agGridApi.exportDataAsExcel(exportParams);
                return;
            }
            if (exportContext.destination === 'Download' && format === 'CSV') {
                this.agGridApi.exportDataAsCsv(exportParams);
                return;
            }
            // 2. we have to handle the export ourselves
            // we get the report data and return it, so that the caller can handle it
            if (exportContext.isExcelReport) {
                return {
                    type: 'excel',
                    data: this.agGridApi.getDataAsExcel(exportParams),
                };
            }
            const csvReportData = this.agGridApi.getDataAsCsv(exportParams);
            if (exportContext.format === 'CSV') {
                return {
                    type: 'csv',
                    data: csvReportData,
                };
            }
            // convert the CSV data to JSON
            const jsonReportData = this.mapCsvToJson(csvReportData, exportContext);
            return {
                type: 'json',
                data: jsonReportData,
            };
        }
        catch (error) {
            this.logger.consoleError(`Export failed: report="${report.Name}", format="${format}", destination="${config.destination}".`, error.stack);
        }
        finally {
            /**
             * Cleanup export process
             */
            this.adaptableApi.exportApi.internalApi.setExportComplete();
            if (excelStylesWerePatched) {
                this.resetExcelStyles();
            }
            if (showProgressIndicator) {
                this.adaptableApi.userInterfaceApi.hideProgressIndicator();
            }
        }
    }
    /**
     * Creates export context and parameters for a given export configuration
     */
    buildExportProcessData(config) {
        const exportContext = this.buildExportProcessContext(config);
        let exportParams = this.buildExportParams(exportContext);
        if (typeof config?.customExportParams === 'function') {
            this.logger.info(`Applying custom export params: report="${config.report.Name}", format="${config.format}".`);
            exportParams = config.customExportParams(exportParams);
        }
        exportContext.exportedColumnIds = exportParams.columnKeys;
        return {
            exportContext,
            exportParams,
        };
    }
    buildExportParams(exportContext) {
        const baseExportParams = this.buildBaseExportParams(exportContext);
        if (exportContext.format === 'Excel' || exportContext.format === 'VisualExcel') {
            const excelExportParams = baseExportParams;
            const sheetName = typeof this.exportOptions.excelSheetName === 'function'
                ? this.exportOptions.excelSheetName(this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportContext.report.Name, exportContext.format, exportContext.destination))
                : this.exportOptions.excelSheetName;
            excelExportParams.sheetName = sheetName;
            return excelExportParams;
        }
        // for everything else we use the CSV export
        const csvExportParams = baseExportParams;
        csvExportParams.columnSeparator = this.getCsvSeparator(exportContext);
        return csvExportParams;
    }
    buildBaseExportParams(exportContext) {
        // TODO AFL: get custom info from user/ExportOptions for ExcelExportParams?
        const params = {};
        const { columnKeys, exportedRows, shouldRowBeSkipped, skipRowGroups, rowGroupExpandState } = this.getReportColsAndRows(exportContext);
        params.columnKeys = columnKeys;
        params.exportedRows = exportedRows;
        params.shouldRowBeSkipped = shouldRowBeSkipped;
        params.skipRowGroups = skipRowGroups;
        // only Excel supports expanded state configuration, but setting it for all formats won't hurt, it will just be ignored
        params.rowGroupExpandState = rowGroupExpandState;
        params.fileName = this.adaptableApi.exportApi.internalApi.getReportFileName(exportContext.report.Name, exportContext.format, exportContext.destination);
        params.skipColumnHeaders = this.computeSkipColumnHeaders(exportContext);
        if (!exportContext.isExcelReport && exportContext.format !== 'CSV') {
            // if we map CSV to JSON, we don't want to skip the column headers
            params.skipColumnHeaders = false;
        }
        params.getCustomContentBelowRow = this.computeGetCustomContentBelowRow(exportContext);
        params.processCellCallback = this.computeProcessCellCallback(exportContext);
        params.processRowGroupCallback = this.computeProcessRowGroupCallback(exportContext);
        return params;
    }
    computeProcessRowGroupCallback(exportContext) {
        return (params) => {
            // recreating the standard AG Grid styling for row groups: 'Parent -> Child'
            // additionally the values are formatted
            let rowGroupNode = params.node;
            const agColumn = params.column;
            const rowGroupSummary = [
                this.processRowGroupForExcelExport(rowGroupNode, exportContext, agColumn) ?? '',
            ];
            while (rowGroupNode.parent) {
                rowGroupNode = rowGroupNode.parent;
                const formattedParentNode = this.processRowGroupForExcelExport(rowGroupNode, exportContext, agColumn);
                if (formattedParentNode) {
                    rowGroupSummary.push(formattedParentNode);
                }
            }
            let summary = rowGroupSummary.reverse().join(' -> ');
            if (agColumn?.getColId() === AG_GRID_GROUPED_COLUMN && rowGroupNode.footer) {
                summary = `Total: ${summary}`;
            }
            return summary;
        };
    }
    computeProcessCellCallback(exportContext) {
        return (params) => {
            const { node, column, value } = params;
            const colId = column.getColId();
            if (exportContext.report.ReportRowScope === 'SelectedRows') {
                // handle case when this cell(column) is not selected
                if (
                // see #isCellPartOfSelection
                !exportContext.isCellPartOfSelection(params.node.id, colId))
                    return '';
            }
            const columnId = column.getColId();
            if (node?.group &&
                (this.adaptableApi.columnApi.isAutoRowGroupColumn(columnId) ||
                    // we would still need to process the cell if this is a group row with an aggregated value
                    node?.aggData?.[columnId] == undefined)) {
                // skip processing of row groups, this was already handled in processRowGroupCallback()
                return value;
            }
            return this.processCellForExcelExport(node, columnId, exportContext);
        };
    }
    getReportColsAndRows(exportContext) {
        const { report, exportedColumnIds, isExportingVisibleColumnToJSON } = exportContext;
        /**
         *        1. columnKeys
         */
        const columnKeys = exportedColumnIds;
        /**
         *        2. exportedRows
         */
        let exportedRows = 'all'; // 'AllRows' or 'ExpressionRows'
        if (report.ReportRowScope === 'VisibleRows' || report.ReportRowScope === 'SelectedRows') {
            exportedRows = 'filteredAndSorted';
        }
        /**
         *        3. shouldRowBeSkipped
         */
        let shouldRowBeSkipped;
        // 'AllRows' or 'VisibleRows': no need to skip rows as we are exporting all rows
        if (report.ReportRowScope === 'SelectedRows') {
            // mapping of row ids to column ids which contain selected cells
            // or true if the whole row is selected
            const selectedRowColumnMapping = new Map();
            // ids of selected rows
            this.adaptableApi.gridApi.getSelectedRowInfo().gridRows?.forEach((row) => {
                if (row?.rowNode?.id) {
                    selectedRowColumnMapping.set(row.rowNode.id, true);
                }
            });
            // ids of rows which contain selected cells
            this.adaptableApi.gridApi.getSelectedCellInfo().gridCells?.map((gridCell) => {
                if (gridCell.rowNode?.id) {
                    const selectedColumns = selectedRowColumnMapping.get(gridCell.rowNode.id);
                    if (selectedColumns == null) {
                        selectedRowColumnMapping.set(gridCell.rowNode.id, new Set([gridCell.column.columnId]));
                    }
                    else if (selectedColumns instanceof Set) {
                        selectedColumns.add(gridCell.column.columnId);
                    }
                    // else no nothing, the entire row is selected
                }
            });
            shouldRowBeSkipped = (params) => {
                return !selectedRowColumnMapping.has(params.node.id);
            };
            // #isCellPartOfSelection
            exportContext.isCellPartOfSelection = (rowId, columnId) => {
                const selectedRow = selectedRowColumnMapping.get(rowId);
                if (selectedRow === true) {
                    return true;
                }
                if (selectedRow instanceof Set) {
                    return selectedRow.has(columnId);
                }
                return false;
            };
        }
        if (report.ReportRowScope === 'ExpressionRows') {
            shouldRowBeSkipped = (params) => {
                const rowNodeMatchesExpression = this.adaptableApi.internalApi
                    .getQueryLanguageService()
                    .evaluateBooleanExpression(report?.Query?.BooleanExpression, ExportModuleId, params.node);
                return !rowNodeMatchesExpression;
            };
        }
        /**
         *        4. skipRowGroups
         */
        let skipRowGroups;
        if (report.ReportRowScope === 'AllRows' || isExportingVisibleColumnToJSON) {
            // we want to return the entire data, without the groups
            skipRowGroups = true;
        }
        if (this.isTreeDataGrid()) {
            // we need the group rows for TreeData
            skipRowGroups = false;
        }
        /**
         *        5. rowGroupExpandState
         */
        let rowGroupExpandState = 'expanded';
        if (report.ReportRowScope === 'VisibleRows') {
            rowGroupExpandState = 'match';
        }
        return { columnKeys, exportedRows, shouldRowBeSkipped, skipRowGroups, rowGroupExpandState };
    }
    isTreeDataGrid() {
        return this.adaptableApi.gridApi.isTreeDataGrid();
    }
    buildExportProcessContext(config) {
        const { report } = config;
        const currentLayout = this.adaptableApi.layoutApi.getCurrentLayout();
        const groupColumnIds = this.isTreeDataGrid()
            ? [AG_GRID_GROUPED_COLUMN]
            : currentLayout.RowGroupedColumns ?? currentLayout.PivotGroupedColumns;
        const pivotColumnIds = currentLayout.PivotColumns;
        const isExportingVisibleColumnToJSON = config.format === 'JSON' && report.ReportColumnScope === 'VisibleColumns';
        const exportableColumnIds = this.adaptableApi.columnApi
            .getExportableColumns()
            .map((col) => col.columnId);
        const exportableColumnIdsSet = new Set(exportableColumnIds);
        // we compute the exported column keys early, as we need them for other parts of the export process
        const onlyExportableColumnIds = (columnId) => exportableColumnIdsSet.has(columnId);
        const getVisibleColumnIds = () => {
            return this.adaptableApi.layoutApi.isCurrentLayoutPivot()
                ? this.adaptableApi.layoutApi
                    .getCurrentVisibleColumnIdsForPivotLayout()
                    .filter(onlyExportableColumnIds)
                : this.adaptableApi.layoutApi
                    .getCurrentVisibleColumnIdsForTableLayout()
                    .filter(onlyExportableColumnIds);
        };
        let exportedColumnIds = [];
        switch (report.ReportColumnScope) {
            case 'AllColumns':
                exportedColumnIds = exportableColumnIds.filter((colId) => this.isTreeDataGrid() || !this.adaptableApi.columnApi.isAutoRowGroupColumn(colId));
                break;
            case 'VisibleColumns':
                exportedColumnIds = getVisibleColumnIds();
                if (isExportingVisibleColumnToJSON) {
                    // for JSON we will export CSV and then convert to JSON
                    // in this case we have to map the auto-group columns to the real keys
                    exportedColumnIds = [
                        ...groupColumnIds,
                        ...exportedColumnIds.filter((columnId) => this.isTreeDataGrid() || !this.adaptableApi.columnApi.isAutoRowGroupColumn(columnId)),
                    ];
                }
                break;
            case 'SelectedColumns':
                const selectedRowInfo = this.adaptableApi.gridApi.getSelectedRowInfo();
                if (selectedRowInfo?.gridRows?.length) {
                    // if there is at least one selected row, we have to export all visible columns
                    exportedColumnIds = getVisibleColumnIds();
                }
                else {
                    // otherwise, we can export only columns which contain selected cells
                    const selectedColumnIds = this.adaptableApi.gridApi
                        .getSelectedCellInfo()
                        .columns?.map((column) => column.columnId);
                    const selectedColumnIdsSet = new Set(selectedColumnIds.filter(onlyExportableColumnIds));
                    // ensure that column order is preserved
                    exportedColumnIds = getVisibleColumnIds().filter((colId) => selectedColumnIdsSet.has(colId));
                }
                break;
            case 'ScopeColumns':
                if ('ColumnIds' in report.Scope) {
                    exportedColumnIds = report.Scope.ColumnIds?.filter(onlyExportableColumnIds);
                }
                else {
                    exportedColumnIds = this.adaptableApi.columnScopeApi
                        .getColumnsInScope(report.Scope)
                        .map((col) => col.columnId)
                        .filter(onlyExportableColumnIds);
                }
                break;
            default:
                this.adaptableApi.logError('Invalid ReportColumnScope: ', report.ReportColumnScope);
                // fallback to all columns
                exportedColumnIds = exportableColumnIds;
        }
        return {
            ...config,
            exportedColumnIds,
            isExcelReport: config.format === 'Excel' || config.format === 'VisualExcel',
            isVisualExcelReport: config.format === 'VisualExcel',
            isExportingVisibleColumnToJSON,
            // will be overriden later, see #isCellPartOfSelection
            isCellPartOfSelection: () => false,
            getCurrent: () => {
                return {
                    layout: currentLayout,
                    groupColumnIds,
                    pivotColumnIds,
                };
            },
        };
    }
    processCellForExcelExport(rowNode, columnId, exportContext) {
        if (this.adaptableApi.exportApi.internalApi.isVisualDataExportInProgress()) {
            const cellKey = AgGridExportAdapter.getExcelClassNameForCell(columnId, this.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(rowNode));
            const isoFormattedDate = this.getExcelStyleWithFormattedDate(cellKey);
            if (isoFormattedDate) {
                // this is a Date cell which will be formatted by Excel
                return isoFormattedDate;
            }
        }
        return this.getCellExportValueFromRawValue(rowNode, this.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, columnId), columnId, exportContext);
    }
    processRowGroupForExcelExport(rowNode, exportContext, agColumn) {
        if (this.isTreeDataGrid()) {
            return rowNode.key;
        }
        const getColIdAndRawValue = () => {
            if (this.adaptableApi.columnApi.isAutoRowGroupColumn(agColumn?.getColId())) {
                const columnId = rowNode.rowGroupColumn?.getColId() ??
                    rowNode.rowGroupColumn?.getColDef()?.field ??
                    rowNode.field;
                let rawValue = rowNode.key;
                return { columnId, rawValue };
            }
            if (!!rowNode.aggData) {
                const columnId = agColumn.getColId();
                const rawValue = rowNode.aggData?.[columnId];
                return { columnId, rawValue };
            }
            return {};
        };
        let { columnId, rawValue } = getColIdAndRawValue();
        if (!columnId || rawValue == null) {
            return;
        }
        const columnDataType = this.adaptableApi.columnApi.getColumnDataTypeForColumnId(columnId);
        if (columnDataType === 'date' &&
            typeof rawValue === 'string' &&
            // rawValue is composed only of digits
            /^\d+$/.test(rawValue)) {
            // AG-Grid converts the value to string, we have to reconvert it back
            const dateRawValue = parseInt(rawValue);
            if (dateRawValue != undefined) {
                // @ts-ignore
                rawValue = dateRawValue;
            }
        }
        return this.getCellExportValueFromRawValue(rowNode, rawValue, columnId, exportContext);
    }
    patchExcelStyles(exportContext) {
        const exportExcelStyles = this.buildExcelStyles(exportContext);
        // Concatenate exportExcelStyles to DANGER_excelStyles without changing the array reference
        this.DANGER_excelStyles.push(...exportExcelStyles);
    }
    resetExcelStyles() {
        // Revert to original state without changing the array reference
        this.DANGER_excelStyles.splice(0, this.DANGER_excelStyles.length, ...this.originalExcelStyles);
        this.excelStylesCache = {};
        this.cellOrHeaderClassKey2ExcelStyleIdMap = {};
        this.excelStylesWithFormattedDate = {};
    }
    buildExcelStyles(exportContext) {
        // we make sure that we start with a clean slate
        // theoretically this should have happened at the end of the export process, but just in case
        this.resetExcelStyles();
        // we memoize as much as possible, as this is called quite A LOT
        const adaptableColumnMemo = {};
        const getAdaptableColumnWithColumnId = (columnId) => {
            const memoizedColumn = adaptableColumnMemo[columnId];
            if (memoizedColumn) {
                return memoizedColumn;
            }
            const abColumn = this.adaptableApi.columnApi.getColumnWithColumnId(columnId);
            adaptableColumnMemo[columnId] = abColumn;
            return abColumn;
        };
        const formatColumnsWithDisplayFormatMemo = {};
        const getFormatColumnsWithDisplayFormatForColumn = (columnId) => {
            const memoizedFormatColumns = formatColumnsWithDisplayFormatMemo[columnId];
            if (memoizedFormatColumns) {
                return memoizedFormatColumns;
            }
            const abColumn = getAdaptableColumnWithColumnId(columnId);
            const formatColumns = this.adaptableApi.formatColumnApi.internalApi.getFormatColumnsWithDisplayFormatForColumn(abColumn, { target: 'cell' });
            formatColumnsWithDisplayFormatMemo[columnId] = formatColumns;
            return formatColumns;
        };
        const agGridDisplayedColumns = this.agGridApi
            .getAllDisplayedColumns()
            .filter((agCol) => agCol.getId() !== AG_GRID_SELECTION_COLUMN);
        const colDefs = agGridDisplayedColumns.map((column) => {
            return column.getColDef();
        });
        const forAllVisibleRowNodesDoConfig = { includeGroupRows: true };
        const userExcelStyles = this.DANGER_excelStyles;
        // 1. Header Styles
        agGridDisplayedColumns.forEach((column, columnIndex) => {
            const colDef = colDefs[columnIndex];
            const columnId = column.getId();
            const adaptableColumn = getAdaptableColumnWithColumnId(columnId);
            if (!adaptableColumn) {
                this.logger.warn(`Export styling: column "${columnId}" not found in Adaptable.`);
                return;
            }
            let headerClassParams = {
                colDef,
                column,
                floatingFilter: false,
                api: this.agGridApi,
                context: this.agGridApi.getGridOption('context'),
            };
            const headerStyle = typeof colDef.headerStyle === 'function' ? colDef.headerStyle(headerClassParams) : {};
            const excelHeaderStyles = [];
            // add user defined excel styles
            let userColDefHeaderClass = this._adaptableInstance.agGridColumnAdapter.getUserColDefProperty(column.getColId(), 'headerClass');
            const userDefinedHeaderClass = typeof userColDefHeaderClass === 'function'
                ? userColDefHeaderClass(headerClassParams)
                : userColDefHeaderClass;
            const userDefinedExcelHeaderStyle = userDefinedHeaderClass &&
                userExcelStyles.find((excelStyle) => {
                    return typeof userDefinedHeaderClass === 'string'
                        ? userDefinedHeaderClass === excelStyle.id
                        : userDefinedHeaderClass?.includes?.(excelStyle.id);
                });
            if (userDefinedExcelHeaderStyle) {
                excelHeaderStyles.push(userDefinedExcelHeaderStyle);
            }
            // add adaptable derived styles (format column etc.)
            const adaptableHeaderStyle = {
                ...Object.keys(headerStyle).reduce((result, key) => {
                    if (headerStyle[key] !== null) {
                        result[key] = headerStyle[key];
                    }
                    return result;
                }, {}),
            };
            const sanitizedAdaptableHeaderStyle = sanitizeStyle(adaptableHeaderStyle);
            if (Object.values(sanitizedAdaptableHeaderStyle).some((style) => style != null)) {
                excelHeaderStyles.push(this.convertCSSToExcelStyle(sanitizedAdaptableHeaderStyle));
            }
            // don't add the header style if it has no adaptable custom styles
            if (!excelHeaderStyles.length) {
                return;
            }
            const headerClassId = AgGridExportAdapter.getExcelClassNameForHeader(column.getId(), userDefinedHeaderClass);
            const finalHeaderExcelStyle = Object.assign({}, ...excelHeaderStyles);
            this.registerExcelStyle(finalHeaderExcelStyle, headerClassId);
        });
        // 2. Cell Styles
        this.adaptableApi.internalApi.forAllVisibleRowNodesDo((node, rowIndex) => {
            const rowParams = {
                node,
                data: node.data,
                rowIndex,
                api: this.agGridApi,
                context: this.agGridApi.getGridOption('context') || {},
            };
            const getRowStyleFn = this.agGridApi.getGridOption('getRowStyle');
            const rowStyle = getRowStyleFn ? getRowStyleFn(rowParams) : {};
            agGridDisplayedColumns.forEach((column, columnIndex) => {
                const colDef = colDefs[columnIndex];
                const columnId = column.getId();
                const adaptableColumn = getAdaptableColumnWithColumnId(columnId);
                if (!adaptableColumn) {
                    this.logger.warn(`Export styling: column "${columnId}" not found in Adaptable.`);
                    return;
                }
                const isDateCellExportedAsFormattedValue = this.isDateCellExportedAsFormattedValue(adaptableColumn, exportContext);
                let cellClassParams;
                const getLazyCellClassParams = () => {
                    if (!cellClassParams) {
                        cellClassParams = {
                            colDef,
                            node,
                            column,
                            data: node.data,
                            value: this.adaptableApi.gridApi.getRawValueFromRowNode(node, columnId),
                            rowIndex,
                            api: this.agGridApi,
                            context: {},
                        };
                    }
                    return cellClassParams;
                };
                const cellStyle = typeof colDef.cellStyle === 'function' ? colDef.cellStyle(getLazyCellClassParams()) : {};
                const excelCellStyles = [];
                // add user defined excel styles
                let userColDefCellClass = this._adaptableInstance.agGridColumnAdapter.getUserColDefProperty(column.getColId(), 'cellClass');
                const userDefinedCellClass = typeof userColDefCellClass === 'function'
                    ? userColDefCellClass(getLazyCellClassParams())
                    : userColDefCellClass;
                const userDefinedExcelStyle = userDefinedCellClass &&
                    userExcelStyles.find((excelStyle) => {
                        return typeof userDefinedCellClass === 'string'
                            ? userDefinedCellClass === excelStyle.id
                            : userDefinedCellClass?.includes?.(excelStyle.id);
                    });
                if (userDefinedExcelStyle) {
                    excelCellStyles.push(userDefinedExcelStyle);
                }
                // add adaptable derived styles (format column etc.)
                const adaptableCellStyle = {
                    ...rowStyle,
                    ...Object.keys(cellStyle).reduce((result, key) => {
                        if (cellStyle[key] !== null) {
                            result[key] = cellStyle[key];
                        }
                        return result;
                    }, {}),
                };
                const sanitizedAdaptableCellStyle = sanitizeStyle(adaptableCellStyle);
                if (Object.values(sanitizedAdaptableCellStyle).some((style) => style != null)) {
                    excelCellStyles.push(this.convertCSSToExcelStyle(sanitizedAdaptableCellStyle));
                }
                const excelDataType = this.getExcelDataType(adaptableColumn.dataType);
                const rawValue = this.adaptableApi.gridApi.getRawValueFromRowNode(node, column.getId());
                // don't add the cell style if it has no adaptable custom styles
                if (!excelCellStyles.length &&
                    // if this is a formatted Date value, we still need to add the AG GRID specific type & numberFormat below
                    !(excelDataType === 'DateTime' && isDateCellExportedAsFormattedValue)) {
                    return;
                }
                const cellClassId = AgGridExportAdapter.getExcelClassNameForCell(column.getId(), this.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(node), userDefinedCellClass);
                const finalCellExcelStyle = Object.assign({}, ...excelCellStyles);
                if (excelDataType === 'DateTime' && isDateCellExportedAsFormattedValue) {
                    let dateFormatPattern = this.exportOptions.exportDateFormat;
                    const abColumn = getAdaptableColumnWithColumnId(column.getColId());
                    if (!dateFormatPattern) {
                        const mostRelevantFormatColumn = this.adaptableApi.formatColumnApi.internalApi.getMostRelevantFormatColumnForColumn(getFormatColumnsWithDisplayFormatForColumn(column.getColId()), abColumn, { node, value: rawValue });
                        dateFormatPattern =
                            mostRelevantFormatColumn?.DisplayFormat?.Formatter === 'DateFormatter' &&
                                mostRelevantFormatColumn?.DisplayFormat?.Options?.Pattern;
                    }
                    if (dateFormatPattern) {
                        const customDateFormatPattern = typeof dateFormatPattern === 'function'
                            ? dateFormatPattern({
                                ...this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportContext.report.Name, exportContext.format, exportContext.destination),
                                column: adaptableColumn,
                            })
                            : dateFormatPattern;
                        const normalisedValue = this._adaptableInstance.getNormalisedValueFromRawValue(rawValue, abColumn);
                        if (normalisedValue) {
                            // we have to pass the date in the ISO format to Excel
                            // see https://www.ag-grid.com/javascript-data-grid/excel-export-data-types/#dates
                            // we can NOT use Date.toISOString() because we don't want the timezone corrections to kick in
                            const isoFormattedValue = DateFormatter(normalisedValue, {
                                Pattern: `yyyy-MM-dd'T'HH:mm:ss.SSS`,
                            });
                            if (isoFormattedValue) {
                                finalCellExcelStyle.dataType = 'DateTime';
                                finalCellExcelStyle.numberFormat = { format: customDateFormatPattern };
                                // create a new cell key to ensure any user provided className does not interfere
                                const cellKey = AgGridExportAdapter.getExcelClassNameForCell(column.getColId(), this.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(node));
                                // we need to register so that later the cellProcessor will put the isoFormattedValue through (thus giving the formatting responsability to Excel)
                                this.registerExcelStyleWithFormattedDate(cellKey, isoFormattedValue);
                            }
                        }
                    }
                }
                this.registerExcelStyle(finalCellExcelStyle, cellClassId);
            });
        }, forAllVisibleRowNodesDoConfig);
        return Object.values(this.excelStylesCache);
    }
    registerExcelStyle(excelStyle, cellClassKey) {
        const excelStyleKey = JSON.stringify(excelStyle);
        if (!this.excelStylesCache[excelStyleKey]) {
            const excelStyleId = createUuid();
            const excelStyleWithId = { ...excelStyle, id: excelStyleId };
            this.excelStylesCache[excelStyleKey] = excelStyleWithId;
        }
        this.cellOrHeaderClassKey2ExcelStyleIdMap[cellClassKey] =
            this.excelStylesCache[excelStyleKey].id;
    }
    registerExcelStyleWithFormattedDate(cellClassId, isoFormattedValue) {
        this.excelStylesWithFormattedDate[cellClassId] = isoFormattedValue;
    }
    isDateCellExportedAsFormattedValue(abColumn, exportContext) {
        return (!!this.exportOptions.exportDateFormat ||
            // FIXME AFL move this method here
            this.getCellExportFormatType(abColumn, 'date', exportContext) === 'formattedValue');
    }
    convertCSSToExcelStyle(style) {
        const getHexColor = (color) => {
            const preparedColor = getVariableColor(color);
            const t = tinycolor(preparedColor);
            const a = t.getAlpha();
            return tinycolor.mix(tinycolor('white'), t, a * 100).toHexString();
        };
        let result = {};
        if (style.backgroundColor != null) {
            result.interior = {
                color: getHexColor(style.backgroundColor),
                pattern: 'Solid',
            };
        }
        if (style.borderColor != null) {
            const excelBorder = {
                color: style.borderColor,
                lineStyle: 'Continuous',
                weight: 1,
            };
            result.borders = {
                borderBottom: excelBorder,
                borderLeft: excelBorder,
                borderRight: excelBorder,
                borderTop: excelBorder,
            };
        }
        if (style.textAlign) {
            result.alignment = {
                horizontal: StringExtensions.CapitaliseFirstLetter(style.textAlign),
            };
        }
        if (style.color != null) {
            if (!result.font) {
                result.font = {};
            }
            result.font = {
                color: getHexColor(style.color),
            };
        }
        if (style.fontStyle === 'italic') {
            if (!result.font) {
                result.font = {};
            }
            result.font.italic = true;
        }
        if (style.fontWeight != null &&
            (style.fontWeight === 'bold' || Number(style.fontWeight) >= 600)) {
            if (!result.font) {
                result.font = {};
            }
            result.font.bold = true;
        }
        if (style.fontSize != null) {
            if (!result.font) {
                result.font = {};
            }
            result.font.size = convertCSSAbsoluteFontSizeToPt(style.fontSize);
        }
        return result;
    }
    getExcelDataType(adaptableColumnDataType) {
        switch (adaptableColumnDataType) {
            case 'number':
                return 'Number';
            case 'boolean':
                return 'Boolean';
            case 'date':
                return 'DateTime';
            case 'text':
            default:
                return 'String';
        }
    }
    getExcelStyleIdForClassKey(cellOrHeaderClassKey) {
        return this.cellOrHeaderClassKey2ExcelStyleIdMap[cellOrHeaderClassKey];
    }
    getExcelStyleWithFormattedDate(cellClassId) {
        return this.excelStylesWithFormattedDate[cellClassId];
    }
    mapCsvToJson(csvReportData, exportContext) {
        const columns = exportContext.exportedColumnIds
            .map((columnId) => this.adaptableApi.columnApi.getColumnWithColumnId(columnId))
            .map((column) => ({
            columnId: column.columnId,
            field: column.field ?? column.columnId,
            friendlyName: column.friendlyName,
            dataType: column.dataType,
        }));
        const reportData = {
            columns,
            rows: [],
        };
        if (exportContext.report.ReportColumnScope === 'VisibleColumns') {
            if (exportContext.getCurrent().groupColumnIds?.length) {
                reportData.groupColumnIds = exportContext.getCurrent().groupColumnIds;
            }
            if (exportContext.getCurrent().pivotColumnIds?.length) {
                reportData.pivotColumnIds = exportContext.getCurrent().pivotColumnIds;
            }
        }
        // map Row Data
        const schema = inferSchema(csvReportData, {
            col: this.getCsvSeparator(exportContext),
            header: (rows) => {
                const headerRow = rows[0];
                // replace headerNames with columnIds
                headerRow.forEach((headerName, index) => {
                    const column = columns.find((col) => col.friendlyName === headerName);
                    if (column) {
                        headerRow[index] = column.columnId;
                    }
                });
                return [headerRow];
            },
        });
        const parser = initParser(schema);
        const jsonData = parser.typedObjs(csvReportData);
        reportData.rows = jsonData;
        return reportData;
    }
    getCsvSeparator(exportProcessContext) {
        if (typeof this.exportOptions.csvSeparator === 'function') {
            return this.exportOptions.csvSeparator(this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportProcessContext.report.Name, exportProcessContext.format, exportProcessContext.destination));
        }
        else {
            return this.exportOptions.csvSeparator;
        }
    }
    computeSkipColumnHeaders(exportProcessContext) {
        if (typeof this.exportOptions.skipColumnHeaders === 'function') {
            return this.exportOptions.skipColumnHeaders(this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportProcessContext.report.Name, exportProcessContext.format, exportProcessContext.destination));
        }
        else {
            return this.exportOptions.skipColumnHeaders;
        }
    }
    computeGetCustomContentBelowRow(exportContext) {
        if (!this.adaptableApi.gridApi.isMasterDetailGrid()) {
            return;
        }
        const getDetailsRowsFn = this.exportOptions.getDetailRows;
        if (typeof getDetailsRowsFn !== 'function') {
            this.adaptableApi.logInfo('ExportOptions.getDetailRows() is not defined, skipping detail rows export');
            return;
        }
        return (params) => {
            const { node } = params;
            const shouldExportDetailRows = exportContext.report.ReportRowScope === 'AllRows' ||
                (exportContext.report.ReportRowScope === 'VisibleRows' && node.expanded);
            if (!shouldExportDetailRows) {
                return;
            }
            const context = {
                ...this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportContext.report.Name, exportContext.format, exportContext.destination),
                adaptableColumn: this.adaptableApi.columnApi.getColumnWithColumnId(params.column?.getId()),
                masterRowNode: node,
                masterRowData: node?.data,
                isExpanded: node.expanded,
                createCellCsv: (cellContent) => this.adaptableApi.exportApi.internalApi.createCellCsv(cellContent),
                createCellExcel: (cellContent, cellType) => this.adaptableApi.exportApi.internalApi.createCellExcel(cellContent, cellType),
                createCellHeader: (cellContent) => {
                    return {
                        ...this.adaptableApi.exportApi.internalApi.createCellHeader(cellContent),
                        // see #masterDetailHeader
                        styleId: '_masterDetailHeader',
                    };
                },
            };
            const detailRows = getDetailsRowsFn(context);
            if (exportContext.isExcelReport) {
                const anyExcelRow = detailRows[0];
                if (anyExcelRow.cells == undefined) {
                    this.adaptableApi.logWarn(`Export ${exportContext.report.Name}: ExportOptions.getDetailRows() should return an array of ExcelRow objects`);
                    return;
                }
            }
            else {
                const anyCsvCell = detailRows[0]?.[0];
                if (anyCsvCell.data == undefined) {
                    this.adaptableApi.logWarn(`Export ${exportContext.report.Name}: ExportOptions.getDetailRows() should return an array of CsvRow objects`);
                    return;
                }
            }
            return detailRows;
        };
    }
    getCellExportValueFromRawValue(rowNode, cellRawValue, columnId, exportContext) {
        if (StringExtensions.IsNullOrEmpty(cellRawValue)) {
            return cellRawValue;
        }
        const column = this.adaptableApi.columnApi.getColumnWithColumnId(columnId);
        const columnDataType = column.dataType;
        // 1. if it is a VisualExcel report format, we always ONLY send the formatted value and ignore all other properties
        if (exportContext.isVisualExcelReport) {
            return this.getCellExportValueFromRawValueByType(rowNode, cellRawValue, columnId, 'formattedValue');
        }
        const isDateColumn = columnDataType === 'date';
        // 2. if this is a date column and there is a custom export date format provided, that will next take precedence
        if (isDateColumn) {
            const customExportDateFormat = this.getCustomExportDateFormat(column, exportContext);
            if (!!customExportDateFormat) {
                return FormatHelper.DateFormatter(cellRawValue, {
                    Pattern: customExportDateFormat,
                });
            }
        }
        // 3. in all other cases check the general export format types
        const cellExportFormat = this.getCellExportFormatType(column, columnDataType, exportContext);
        return this.getCellExportValueFromRawValueByType(rowNode, cellRawValue, columnId, cellExportFormat);
    }
    getCustomExportDateFormat(column, exportContext) {
        const exportDateFormatOption = this.adaptableApi.optionsApi.getExportOptions().exportDateFormat;
        return typeof exportDateFormatOption === 'function'
            ? exportDateFormatOption({
                ...this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportContext.report.Name, exportContext.format, exportContext.destination),
                column,
            })
            : exportDateFormatOption;
    }
    getCellExportValueFromRawValueByType(rowNode, cellRawValue, columnId, 
    // default to rawValue if, for some reason, the configs provide invalid values
    type = 'rawValue') {
        return type === 'rawValue'
            ? cellRawValue
            : // type === formattedValue
                this.adaptableApi.gridApi.getDisplayValueFromRawValue(rowNode, columnId, cellRawValue);
    }
    getCellExportFormatType(column, columnDataType, exportContext) {
        const exportDataFormat = this.exportOptions.exportDataFormat;
        // First check if a function was provided and return the result
        if (exportDataFormat != null && typeof exportDataFormat === 'function') {
            const context = {
                ...this.adaptableApi.exportApi.internalApi.buildBaseExportContext(exportContext.report.Name, exportContext.format, exportContext.destination),
                column: column,
            };
            return exportDataFormat(context);
        }
        // Next Check if a "hard-coded" value has been provided and return that
        if (exportDataFormat === 'rawValue') {
            return 'rawValue';
        }
        if (exportDataFormat === 'formattedValue') {
            return 'formattedValue';
        }
        // Finally test if a DataType object has been provided and return the relevant property
        const dataFormatDataType = exportDataFormat;
        if (dataFormatDataType) {
            // format is customized based on column data type
            switch (columnDataType) {
                case 'text':
                    return dataFormatDataType.text;
                case 'number':
                    return dataFormatDataType.number;
                case 'date':
                    return dataFormatDataType.date;
                default:
                    // default to rawValue for all other column types
                    return 'rawValue';
            }
        }
        return 'rawValue';
    }
}
