import * as React from 'react';

import { getTransformsDataPipeline } from 'mmviz';

import {
    COMPONENT_MODE_INITIAL,
    COMPONENT_MODE_EMPTY,
    COMPONENT_MODE_ERROR,
    COMPONENT_MODE_LOADING,
    COMPONENT_MODE_OK,
    ComponentModeWindow,
    MmuiProps,
    MmuiState,
    MmuiTableComponent,
} from '../common';

import { QueryData } from '../../mmui-data';

import { formatValue } from '../../mmui-util';
import { SummaryColumnComponent } from './refiner/SummaryColumnComponent';
import { ColumnHeadComponent } from './refiner/ColumnHeadComponent';

const ROW_MODE_CLOSED = 'closed';
const ROW_MODE_OPEN = 'open';
const ROW_MODE_LOADING = 'loading';

export interface MmuiTableProps extends MmuiProps {
    hasColumnAction?: boolean;
    hasColumnSelection?: boolean;
    hasLevelColumns?: boolean; //whether levels are expanded to dedicated columns
    transforms?: any;
    colOrderIgnoreCase?: boolean; //whether column sort ignores case
    colOrderEmptyLocation?: string; //sort location of empty values (bottom, always_top, always_bottom)
    reportIssueUrl?: string; // url link to report issue when something goes wrong
    hasPipeline?: boolean;
    transformsDelay?: number;
    tableClassNameList?: [];
    tableContainerClassNameList?: [];
    displayDefaultLabel?: boolean;
}

export interface MmuiTableState extends MmuiState {
    // contains transforms objects that are supported by mmviz.getTransformsDataPipeline function
    transforms?: any;
    rows?: any;
    hasPipeline?: boolean;
}

/**
 * Crafted Table Component for rendering different types of tables (regular, nested, pivoted)
 * The data payload is passed in the props. Either this.props.payload or this.props.store.data (store)
 * Create a base class that inherits from MmuiCraftedTableComponent
 *  to override properties like this.name or this.tableClassNameList
 *  as well as override methods such as this.getColumnSummaryRender and this.getColumnSummaryRender
 */
export class MmuiCraftedTableComponent<
    P extends MmuiTableProps,
    S extends MmuiTableState
> extends MmuiTableComponent<P, S> {
    // css classes to apply to the rendered table
    tableClassNameList;
    tableContainerClassNameList;
    numberOfItems;

    defaultColumnConfig: any;

    constructor(props: MmuiTableProps) {
        super(props);

        // css classes to apply to the rendered table
        this.tableClassNameList = [
            'table',
            'table-sm',
            'table-hover',
            'mmui-table',
            'mmui-table-sticky',
            'mmui-table-sortable',
        ];
        this.tableContainerClassNameList = [
            'table-responsive',
            'table-responsive-short',
            'mmui-table-sticky-wrapper',
            'mmui-hmi-30',
        ];

        if (props.tableClassNameList) {
            for (const tableClassName of props.tableClassNameList) {
                this.tableClassNameList.push(tableClassName);
            }
        }

        if (props.tableContainerClassNameList) {
            this.tableContainerClassNameList = props.tableContainerClassNameList;
        }

        this.defaultColumnConfig = {
            orderEmptyLocation: props.colOrderEmptyLocation,
            orderIgnoreCase: props.colOrderIgnoreCase === false ? false : true,
        };
    }

    /**
     * @return this component's initial state.
     */
    getInitialComponentState(): any {
        const state: any = {
            // contains transforms objects that are supported by mmviz.getTransformsDataPipeline function
            transforms: {},
            rows: {},
            isVisible: true,
            hasPipeline: true,
        };

        if (this.props.payload) {
            state.payload = this.props.payload;
        }

        if (this.props.transforms) {
            state.transforms = this.props.transforms;
        }

        if (this.props.hasPipeline !== undefined) {
            state.hasPipeline = this.props.hasPipeline;
        }

        return state;
    }

    /**
     * Get the dataModel from the payload.
     * The dataModel should match the structure returned by the following mmvizutil functions:
     *  mmvizutil.data.shape.shape_data
     *  mmvizutil.data.shape.shape_data_model
     *  mmvizutil.data.shape.shape_data_model_levels
     *  mmvizutil.data.shape.shape_data_model_pivot
     */
    getDataModel() {
        let dataModel,
            payload = this.getPayloadData();
        if (payload) {
            dataModel = payload.data;
        }

        return dataModel;
    }

    /**
     * Extract the dataArray portion of the payload received from the server.
     * @param payload
     */
    extractDataArray(payload) {
        let dataModel,
            dataArray = [];
        if (payload) {
            dataModel = payload.data;
            if (dataModel && dataModel.rows) {
                dataArray = dataModel.rows;
            }
        }

        return dataArray;
    }

    /**
     * Get the query data object that details how the data was queried
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    getQueryDataObj(fromStore = false) {
        let state = this.getComponentState(),
            payload = this.getPayloadData(),
            queryDataObj = {
                dim: [],
                dim_level: [],
                dim_pivot: [],
            };
        if (state.queryDataObj) {
            queryDataObj = state.queryDataObj;
        } else if (payload && payload.data && payload.data.query) {
            queryDataObj = payload.data.query;
        }

        return queryDataObj;
    }

    /**
     * Get a copy of the rows states in the Component state.
     */
    getStateRowsCopy(): any {
        const newRows: any = {},
            state = this.getComponentState();

        for (const rowId in state.rows) {
            newRows[rowId] = Object.assign({}, state.rows[rowId]);
        }

        return newRows;
    }

    /**
     * Create a default row state
     */
    createStateRowDefault(): any {
        return {
            mode: ROW_MODE_CLOSED,
            isSelected: false,
            isVisibleAction: false,
        };
    }

    /**
     * Get configuration details for the provided column
     *  such as name, data type, if sortable, if metric, if distinct.
     *  Override this method in a child class to customize configuration of each column (isSortable, isMetric, hasFilter)
     * @param column
     * @param config
     */
    getColumnConfig(field, config: any = {}) {
        let columnConfig,
            defaultConfig = {
                name: field.toUpperCase(),
                columnName: field,
                dataType: 'categorical',
                isSortable: true,
                isDistinct: false,
                isNumeric: false,
                hasFilter: true,
                hasLevels: false,
            },
            dataModel = this.getDataModel(),
            columnData;

        // check if column is in data or if column is datum attribute prefix

        if (Object.prototype.hasOwnProperty.call(dataModel.columns, field)) {
            columnData = dataModel.columns[field];
        } else {
            const columnName = field + '_name';
            if (
                Object.prototype.hasOwnProperty.call(
                    dataModel.columns,
                    columnName
                )
            ) {
                defaultConfig.columnName = columnName;
                columnData = dataModel.columns[columnName];
            }
        }

        columnConfig = Object.assign(defaultConfig, config);
        if (columnData) {
            columnConfig.dataType = columnData.dataType;
            columnConfig.isNumeric = columnData.dataType == 'numerical';
            columnConfig.isDistinct = columnData.is_distinct;
            columnConfig.isSortable = !columnConfig.isDistinct;
            if (dataModel.columns.hasSummary) {
                columnConfig.hasSummary = true;
            }

            columnConfig.hasFilter = !(
                columnData.extent?.length == 1 && columnData.extent[0] == ''
            );
        }

        switch (field) {
            case 'level':
                columnConfig.name = 'Level';
                columnConfig.columnName = 'level_name';
                columnConfig.hasLevels = true;
                break;
            case 'pivot':
                columnConfig.dataType = 'numerical';
                columnConfig.isNumeric = true;
                columnConfig.columnName = columnConfig.rowKey;
                break;
            default:
                break;
        }

        return columnConfig;
    }

    /**
     * Handles column row action. To be overriden by inheriting table component class.
     * @param e
     * @param data
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    onColumnAction(e, data) {
        e.preventDefault();
    }

    /**
     *
     * @param data
     * @param newState
     */
    updateRowState(row_id, newState) {
        let rowState: any = {},
            newRows = this.getStateRowsCopy(),
            currentRowState = newRows[row_id];
        if (currentRowState) {
            rowState = Object.assign(currentRowState, newState);
        } else {
            rowState = Object.assign(this.createStateRowDefault(), newState);
        }

        newRows[row_id] = rowState;

        this.setComponentStateValue('rows', newRows);
    }

    onColumnActionOver(e, data) {
        this.updateRowState(data.row_id, { isVisibleAction: true });
    }

    onColumnActionOut(e, data) {
        this.updateRowState(data.row_id, { isVisibleAction: false });
    }

    getColumnActionRender(data) {
        let onColumnActionClick = (e) => {
                this.onColumnAction(e, data);
            },
            onColumnActionMouseOver = (e) => {
                this.onColumnActionOver(e, data);
            },
            onColumnActionMouseOut = (e) => {
                this.onColumnActionOut(e, data);
            },
            state = this.getComponentState(),
            rowState = state.rows[data.row_id],
            classArray = [],
            actionRender;

        if (rowState && rowState.isVisibleAction) {
            classArray.push('visible');
        } else {
            classArray.push('invisible');
        }

        actionRender = (
            <a
                href="#"
                className={classArray.join(' ')}
                onClick={onColumnActionClick}
            >
                Filter
            </a>
        );

        return (
            <td
                key={`td-action-${data.row_id}`}
                className="mmui-column-action"
                onMouseOver={onColumnActionMouseOver}
                onMouseOut={onColumnActionMouseOut}
            >
                {actionRender}
            </td>
        );
    }

    /**
     * Get the header summary render of the provided data of the provided columnName.
     * @param columnName
     * @return (<div class="...">{value}</div>)
     */
    getColumnSummaryRender(field, index) {
        let valueFormatted,
            dataModel = this.getDataModel(),
            columnData = dataModel.columns[field],
            isMetric = false,
            classNameArray = [],
            summaryValue,
            summaryValueRender = <span>&nbsp;</span>;
        if (columnData) {
            isMetric = columnData.dataType == 'numerical';

            summaryValue = columnData.summaryValue;
            if (summaryValue) {
                if (isMetric) {
                    valueFormatted = summaryValue.toLocaleString('en-US');
                    summaryValueRender = valueFormatted;
                } else {
                    summaryValueRender = summaryValue;
                }
            }
        }

        const key = `th-summary-${index}`;
        return (
            <span key={key} className={classNameArray.join(' ')}>
                {summaryValueRender}
            </span>
        );
    }

    /**
     * Request row handler to request the children of provided rowId.
     * Called in onToggleLevel, override and implement in inheriting class.
     * @param rowId
     * @param onRowRequestCallback
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
    requestRowData(rowId, onRowRequestCallback) {}

    /**
     * Called when the a nexted level's toggle button is clicked to toggle the visibility of the level's child rows.
     * @param e
     * @param data
     */
    onToggleLevel = (e, data) => {
        e.preventDefault();
        e.stopPropagation();

        const newRowState: any = {},
            state = this.getComponentState(),
            currentRowState = state.rows[data.row_id];
        if (data.children != null || data.child_data != null) {
            if (
                currentRowState === undefined ||
                currentRowState.mode === ROW_MODE_CLOSED
            ) {
                newRowState.mode = ROW_MODE_OPEN;
            } else {
                newRowState.mode = ROW_MODE_CLOSED;
            }
        } else {
            newRowState.mode = ROW_MODE_LOADING;
        }

        this.updateRowState(data.row_id, newRowState);
        if (newRowState.mode == ROW_MODE_LOADING) {
            this.requestRowData(data.row_id, (rowId) => {
                let componentState = this.getComponentState(),
                    currentRowState = componentState.rows[rowId];
                if (currentRowState === undefined) {
                    currentRowState = this.createStateRowDefault();
                }

                currentRowState.mode = 'open';

                this.updateRowState(rowId, currentRowState);
            });
        }
    };

    getLevelColumnRender(
        columnData,
        data,
        levelDisplay,
        currentRowState,
        classNameArray,
        config: any = {}
    ) {
        let columnRender,
            levelBtn,
            levelIcon,
            columnSelectionRender = null,
            tLevel = (e) => {
                this.onToggleLevel(e, data);
            };

        const hasChildrenProperty = Object.prototype.hasOwnProperty.call(
                data,
                'children'
            ),
            hasChildDataProperty = Object.prototype.hasOwnProperty.call(
                data,
                'child_data'
            );
        if (hasChildrenProperty || hasChildDataProperty) {
            const hasChildren =
                    hasChildrenProperty &&
                    (data.children == null || data.children.length > 0),
                hasChildData =
                    hasChildDataProperty &&
                    (data.child_data == null || data.child_data);
            if (hasChildren || hasChildData) {
                levelIcon = <i className="fa fa-chevron-circle-down"></i>;
                if (currentRowState) {
                    if (currentRowState.mode === ROW_MODE_OPEN) {
                        levelIcon = <i className="fa fa-chevron-circle-up"></i>;
                    } else if (currentRowState.mode === ROW_MODE_LOADING) {
                        levelIcon = <i className="fa fa-spinner fa-spin"></i>;
                    }
                }

                levelBtn = (
                    <div>
                        <a
                            href="#"
                            className="btn mmui-level-btn"
                            onClick={tLevel}
                        >
                            {levelIcon}
                        </a>
                    </div>
                );
            }
        }

        if (this.props.hasColumnSelection) {
            const isSelected =
                currentRowState !== undefined && currentRowState.isSelected;
            const isIndeterminate =
                currentRowState !== undefined &&
                currentRowState.isIndeterminate;
            columnSelectionRender = (
                <div className="mmui-selection">
                    <input
                        className="form-check-input"
                        type="checkbox"
                        value={data.row_id}
                        checked={isSelected}
                        ref={(el) => el && (el.indeterminate = isIndeterminate)}
                        onClick={this.onRowSelectionClick}
                        onChange={this.onRowSelectionChange}
                    />
                </div>
            );
        }

        const levelItemsClassNameArray = ['d-flex', 'flex-row'];
        if (data.children && data.children.length == 0) {
            levelItemsClassNameArray.push('mmui-row-empty-nest');
        }

        const isHead = columnData && columnData.is_head;
        if (isHead) {
            columnRender = (
                <td
                    key={config.key}
                    className={classNameArray.join(' ')}
                    style={config.style}
                >
                    <div className={levelItemsClassNameArray.join(' ')}>
                        {levelBtn}
                        {columnSelectionRender}
                        <div>{levelDisplay}</div>
                    </div>
                </td>
            );
        } else {
            columnRender = (
                <th
                    key={config.key}
                    className={classNameArray.join(' ')}
                    style={config.style}
                >
                    <div className={levelItemsClassNameArray.join(' ')}>
                        {levelBtn}
                        {columnSelectionRender}
                        <div>{levelDisplay}</div>
                    </div>
                </th>
            );
        }

        return columnRender;
    }

    getColumnRenderValue(field, data, isMetric) {
        let valueRender = data[field];
        if (valueRender === undefined) {
            valueRender = data[field + '_name'];
        }

        if (isMetric) {
            valueRender = formatValue(valueRender);
        }

        return valueRender;
    }

    /**
     * Get the render of the provided data of the provided columnName.
     * Override this in a child class to customize the render of each table data element
     * @param columnName
     * @param data
     * @param config
     * @return (<th class="...">{value}</th>) or (<td class="...">{value}</td>)
     */
    getColumnRender(field, data, config: any = {}) {
        let columnRender,
            valueDisplay,
            dataModel = this.getDataModel(),
            columnData = dataModel.columns[field],
            isHead = false,
            isParent = false,
            isMetric = false,
            state = this.getComponentState(),
            currentRowState = state.rows[data.row_id],
            classNameArray = [];
        if (columnData) {
            isHead = columnData.is_head;
            isParent = columnData.is_parent;
            isMetric = columnData.dataType == 'numerical';

            if (isMetric) {
                classNameArray.push('text-right');
            }
        }

        switch (field) {
            case 'level':
                classNameArray.push('mmui-level');

                valueDisplay = this.getColumnRenderValue(field, data, false);
                columnRender = this.getLevelColumnRender(
                    columnData,
                    data,
                    valueDisplay,
                    currentRowState,
                    classNameArray,
                    config
                );
                break;
            case 'pivot':
                valueDisplay = this.getColumnRenderValue(
                    config.dataKey,
                    data,
                    true
                );
                columnRender = (
                    <td
                        key={config.key}
                        className={classNameArray.join(' ')}
                        style={config.style}
                    >
                        {valueDisplay}
                    </td>
                );
                break;
            default:
                if (isParent) {
                    classNameArray.push('mmui-level');
                    valueDisplay = this.getColumnRenderValue(
                        field,
                        data,
                        false
                    );
                    columnRender = this.getLevelColumnRender(
                        columnData,
                        data,
                        valueDisplay,
                        currentRowState,
                        classNameArray,
                        config
                    );
                } else if (isHead) {
                    let columnSelectionRender;
                    if (this.props.hasColumnSelection) {
                        const isSelected =
                            currentRowState !== undefined &&
                            currentRowState.isSelected;
                        columnSelectionRender = (
                            <div className="mmui-selection">
                                <input
                                    className="form-check-input"
                                    type="checkbox"
                                    value={data.row_id}
                                    checked={isSelected}
                                    onClick={this.onRowSelectionClick}
                                    onChange={this.onRowSelectionChange}
                                />
                            </div>
                        );
                    }

                    valueDisplay = this.getColumnRenderValue(
                        field,
                        data,
                        false
                    );
                    columnRender = (
                        <th
                            key={config.key}
                            className={classNameArray.join(' ')}
                            style={config.style}
                        >
                            <div className="d-flex flex-row">
                                {columnSelectionRender}
                                <div>{valueDisplay}</div>
                            </div>
                        </th>
                    );
                } else {
                    valueDisplay = this.getColumnRenderValue(
                        field,
                        data,
                        isMetric
                    );
                    columnRender = (
                        <td
                            key={config.key}
                            className={classNameArray.join(' ')}
                            style={config.style}
                        >
                            {valueDisplay}
                        </td>
                    );
                }
                break;
        }

        return columnRender;
    }

    /**
     * Save the transforms into the state
     * @param transforms
     */
    setTransforms = (transforms) => {
        transforms.updatedAt = new Date();
        this.setComponentStateValue('transforms', transforms);
    };

    /**
     * Update the selection state of the Table header based on the number of selected rows.
     */
    getRowSelectionCount() {
        let row,
            selectionCount = 0,
            state = this.getComponentState();
        for (const rowId in state.rows) {
            if (
                rowId !== 'head' &&
                Object.prototype.hasOwnProperty.call(state.rows, rowId)
            ) {
                row = state.rows[rowId];
                if (row.isSelected) {
                    selectionCount++;
                }
            }
        }

        return selectionCount;
    }

    onRowSelectionClick = (e) => {
        e.stopPropagation();
    };

    /**
     * Handles row selection state changes.
     * @param e
     */
    onRowSelectionChange = (e) => {
        const input = e.target as HTMLInputElement,
            rowId = input.value;

        let newRowState,
            state = this.getComponentState(),
            rowState = state.rows[rowId];
        if (rowState === undefined) {
            newRowState = this.createStateRowDefault();
            newRowState.isSelected = true;
        } else {
            newRowState = Object.assign({}, rowState);
            newRowState.isSelected = !newRowState.isSelected;
        }

        this.updateRowState(rowId, newRowState);

        const dataModel = this.getDataModel(),
            newRowsState = this.getStateRowsCopy();
        const selectedRow = this.findSelectedRow(dataModel.rows, rowId);
        newRowsState[selectedRow.row_id] = newRowState;
        if (selectedRow.children) {
            this.updateSelectionEachRecursive(
                selectedRow.children,
                newRowState.isSelected,
                newRowsState
            );
        }
        this.updateParentSelections(dataModel.rows, newRowsState);
        this.numberOfItems = this.countRows(dataModel.rows);
        this.setComponentStateValue('rows', newRowsState);
    };

    /**
     * Counts the number of rows
     * @param rows
     */
    countRows(rows) {
        let rowCount = 0;
        for (const row of rows) {
            rowCount++;
            if (row.children) {
                rowCount = rowCount + this.countRows(row.children);
            }
        }
        return rowCount;
    }

    /**
     * This function loops through the rows and updates the selection states based on selection state of children.
     * @param rows
     * @param newRowsState
     */
    updateParentSelections(rows, newRowsState) {
        let rowState, childRowState;

        for (const r of rows) {
            if (r.children) {
                this.updateParentSelections(r.children, newRowsState);

                let isAllSelected = true,
                    isSomeSelected = false;
                for (const c of r.children) {
                    childRowState = newRowsState[c.row_id];
                    if (childRowState) {
                        isAllSelected =
                            isAllSelected && childRowState.isSelected;
                        isSomeSelected =
                            isSomeSelected ||
                            childRowState.isSelected ||
                            childRowState.isIndeterminate;
                    } else {
                        isAllSelected = false;
                        break;
                    }
                }

                rowState = newRowsState[r.row_id];
                if (rowState === undefined) {
                    rowState = this.createStateRowDefault();
                }

                rowState.isSelected = isAllSelected;
                rowState.isIndeterminate =
                    !isAllSelected && isSomeSelected ? true : false;

                newRowsState[r.row_id] = rowState;
            }
        }
    }

    /**
     * Finds a distinct row given a set of rows and a rowId
     * @param rows
     * @param rowId
     */
    findSelectedRow(rows, rowId) {
        let selectedRow;
        for (const row of rows) {
            if (row.row_id == rowId) {
                selectedRow = row;
                break;
            }
            if (row.children) {
                selectedRow = this.findSelectedRow(row.children, rowId);
                if (selectedRow) {
                    return selectedRow;
                }
            }
        }
        return selectedRow;
    }

    /**
     * Set the selection state of all rows.
     * @param isSelected
     */
    setRowsSelection(isSelected: boolean) {
        const dataModel = this.getDataModel(),
            newRowState = this.getStateRowsCopy();

        this.numberOfItems = this.countRows(dataModel.rows);
        this.updateSelectionEachRecursive(
            dataModel.rows,
            isSelected,
            newRowState
        );
        this.setComponentStateValue('rows', newRowState);
    }

    /**
     * Updates the row state of every row and counts the number of selected items
     * @param rows
     * @param isSelected
     * @param newRows
     */
    updateSelectionEachRecursive(rows, isSelected, newRowsState) {
        let rowState;
        if (rows == undefined || rows.length < 1) {
            return;
        }
        for (const row of rows) {
            rowState = newRowsState[row.row_id];
            if (rowState === undefined) {
                rowState = this.createStateRowDefault();
            }
            rowState.isSelected = isSelected;
            newRowsState[row.row_id] = rowState;
            if (row.children) {
                this.updateSelectionEachRecursive(
                    row.children,
                    isSelected,
                    newRowsState
                );
            }
        }
    }

    /**
     *
     * @param e
     */
    onSelectionClick = (e) => {
        this.setRowsSelection(e.target.checked);
    };

    /**
     * Get Component mode based on data payload
     */
    getComponentMode(payload) {
        let mode = COMPONENT_MODE_INITIAL,
            dataArray,
            hasData = false;
        if (payload) {
            dataArray = this.extractDataArray(payload);
            hasData = dataArray.length > 0;

            if (payload.hasError) {
                mode = COMPONENT_MODE_ERROR;
            } else if (payload.isInvalidated) {
                mode = COMPONENT_MODE_LOADING;
            } else if (hasData) {
                mode = COMPONENT_MODE_OK;
            } else {
                mode = COMPONENT_MODE_EMPTY;
            }
        }

        return mode;
    }

    /**
     * On update to Component props or state
     * @param prevProps
     * @param prevState
     */
    componentDidUpdate() {
        const state = this.getComponentState();

        if (state.operation === 'selection_clear') {
            this.setRowsSelection(false);
            this.setOperation('');
        }
    }

    /**
     * Parse out the table header renders of pivot table into tHeadRows.
     * @param tHeadRows - renders of the table header rows
     * @param columnsData - data for all the columns
     * @param columnName - name of the column
     * @param rowStart
     * @param transforms - transforms applied on the data
     */
    parsePivotTableHeaders(
        tHeadRows,
        columnsArray,
        columnName,
        rowStart,
        transforms
    ) {
        /**
         * Parse pivot table header render into tHeadRows.
         * @param columnsData - data for all the columns
         * @param dataKey - key in the data that the column corresponds with
         * @param rowDepth - current depth, into the header, of the row
         */
        const parseTableHeader = (cArray, dataKey, rowDepth = 0) => {
            let totalColSpan = 0;
            if (cArray && cArray.length > 0) {
                let tColumns = [];
                if (rowDepth >= tHeadRows.length) {
                    tHeadRows.push(tColumns);
                } else {
                    tColumns = tHeadRows[rowDepth];
                }
                for (let i = 0; i < cArray.length; i++) {
                    let colSpan = 0,
                        cData = cArray[i],
                        dKey = `${dataKey}_${cData.key}`;
                    // Recursively apply function to children.
                    if (cData.children) {
                        colSpan = parseTableHeader(
                            cData.children,
                            dKey,
                            rowDepth + 1
                        );
                    }

                    colSpan = colSpan === 0 ? 1 : colSpan;
                    totalColSpan = totalColSpan + colSpan;

                    const key = `th-${rowDepth}-${tColumns.length}`,
                        columnConfig = this.getColumnConfig('pivot', {
                            rowKey: cData.row_key,
                            id: cData.id,
                            name: cData.name,
                            columnName: dKey,
                            colSpan: colSpan,
                        });
                    if (cData.children) {
                        tColumns.push(
                            <th key={key} colSpan={colSpan}>
                                {cData.name}
                            </th>
                        );
                    } else {
                        const completeColumnConfig = Object.assign(
                            columnConfig,
                            this.defaultColumnConfig
                        );
                        tColumns.push(
                            <ColumnHeadComponent
                                key={key}
                                config={completeColumnConfig}
                                data={cData}
                                transforms={transforms}
                                setTransforms={this.setTransforms}
                                transformsDelay={this.props.transformsDelay}
                                displayDefaultLabel={this.props.displayDefaultLabel}
                            />
                        );
                    }
                }

                tHeadRows[rowDepth] = tColumns;
            }

            return totalColSpan;
        };

        return parseTableHeader(columnsArray, columnName, rowStart);
    }

    /**
     * Row Click event handler class method. Called by this.onRowClick. Can be overridden child class.
     * @param evt
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
    rowClick(evt) {}

    /**
     * Row Click event handler.
     * @param evt
     */
    onRowClick = (evt) => {
        this.rowClick(evt);
    };

    /**
     * Parse out the row renders of table with dimension levels into tBodyRows.
     * @param queryData - details of the data query (dimension, levels, pivots, facts, filters)
     * @param tBodyRows - renders of the table body rows
     * @param factColumns - column names that are facts
     * @param rData - current row data
     * @param lDepth - current level depth
     */
    parseTableLevelsRows(queryData, tBodyRows, factColumns, rData) {
        /**
         * Compose dimension level row render with fact row render into tBodyRows.
         * @param rowData - renders of the table body rows
         * @param levelDepth - current level depth
         */
        const dimensionLevel = (rowData, levelDepth = 0) => {
            if (levelDepth > queryData.dimension_levels.length) {
                return;
            }

            let tBodyColumns = [],
                fact,
                dimension,
                rowRender,
                rowId = `${rowData.row_id}`;
            if (this.props.hasLevelColumns) {
                // let dimension_level = queryData.dimension_levels[levelDepth];
                rowRender = this.getColumnRender('level', rowData, {
                    key: `td-${tBodyRows.length}-${tBodyColumns.length}`,
                    style: { paddingLeft: '0px' },
                });

                for (let i = 0; i < levelDepth; i++) {
                    tBodyColumns.push(<td></td>);
                }
            } else {
                rowRender = this.getColumnRender('level', rowData, {
                    key: `td-${tBodyRows.length}-${tBodyColumns.length}`,
                    style: { paddingLeft: `${levelDepth * 30}px` },
                });
            }

            tBodyColumns.push(rowRender);

            // add dimension renders
            if (queryData.dimensions) {
                for (let i = 0; i < queryData.dimensions.length; i++) {
                    dimension = queryData.dimensions[i];
                    rowRender = this.getColumnRender(dimension, rowData, {
                        key: `td-${tBodyRows.length}-${tBodyColumns.length}`,
                    });
                    tBodyColumns.push(rowRender);

                    const dimId = rowData[`${dimension}_id`];
                    rowId = rowId + `_${dimId}`;
                }
            }

            if (this.props.hasLevelColumns) {
                for (
                    let i = levelDepth;
                    i < queryData.dimension_levels.length - 1;
                    i++
                ) {
                    tBodyColumns.push(<td></td>);
                }
            }

            // add fact renders
            for (let i = 0; i < factColumns.length; i++) {
                fact = factColumns[i];
                rowRender = this.getColumnRender(fact, rowData, {
                    key: `td-${tBodyRows.length}-${tBodyColumns.length}`,
                });
                tBodyColumns.push(rowRender);
            }

            if (this.props.hasColumnAction) {
                tBodyColumns.push(this.getColumnActionRender(rowData));
            }

            tBodyRows.push(
                <tr
                    key={`tbody-tr-${rowId}`}
                    data-levelindex={levelDepth}
                    data-rowid={rowId}
                    onClick={this.onRowClick}
                >
                    {tBodyColumns}
                </tr>
            );

            // Recursively apply function to children.
            const children = rowData['children'];
            if (children) {
                const state = this.getComponentState(),
                    rowState = state.rows[rowData.row_id];

                // only render the children if the row is open
                if (rowState && rowState.mode == ROW_MODE_OPEN) {
                    for (let i = 0; i < children.length; i++) {
                        const child = children[i];
                        dimensionLevel(child, levelDepth + 1);
                    }
                }
            }
        };

        dimensionLevel(rData);
    }

    /**
     * Parse out the row renders of a pivot table, with dimension levels, into tBodyRows.
     * @param queryData - details of the data query (dimension, levels, pivots, facts, filters)
     * @param dataModel - details of the data (columns, rows, pivots)
     * @param tBodyRows - renders of the table body rows
     * @param factColumns - column names that are facts
     * @param rData - current row data
     * @param lDepth - current level depth
     */
    parseTablePivotLevelsRows(
        queryData,
        dataModel,
        tBodyRows,
        factColumns,
        rData,
        lDepth = 0
    ) {
        /**
         * Compose dimension level row render with pivot data row render into tBodyRows.
         * @param rowData - current row data
         * @param levelIndex - current level depth
         */
        const dimensionLevel = (rowData, levelDepth = 0) => {
            if (levelDepth > queryData.dimension_levels.length) {
                return;
            }

            const tBodyColumns = [],
                style = { paddingLeft: `${levelDepth * 30}px` },
                indexColumn = this.getColumnRender('level', rowData, {
                    key: `td-${tBodyRows.length}-${tBodyColumns.length}`,
                    style,
                });

            for (let c = 0; c < dataModel.columns.length; c++) {
                const factData = dataModel.columns[c],
                    columnName = factData.key;
                for (const columnData of factData.children) {
                    this.parsePivotDataRow(
                        columnName,
                        columnData,
                        tBodyColumns,
                        rowData
                    );
                }
            }

            tBodyRows.push(
                <tr
                    key={`tbody-tr-${tBodyRows.length}`}
                    onClick={this.onRowClick}
                >
                    {indexColumn}
                    {tBodyColumns}
                </tr>
            );

            // Recursively apply function to children.
            if (rowData.children) {
                const state = this.getComponentState(),
                    rowState = state.rows[rowData.row_id];

                // only render the children if the row is open
                if (rowState && rowState.mode == ROW_MODE_OPEN) {
                    let child;
                    for (let i = 0; i < rowData.children.length; i++) {
                        child = rowData.children[i];
                        dimensionLevel(child, levelDepth + 1);
                    }
                }
            }
        };

        dimensionLevel(rData, lDepth);
    }

    /**
     * Parse out the row renders of a pivot table, for columnName, into dataColumnRenders.
     * @param columnName - Name of the column
     * @param columnData - Data associated with the column
     * @param dataColumnRenders - renders of the column data
     * @param data - data for the current row
     */
    parsePivotDataRow(columnName, columnData, dataColumnRenders, data) {
        /**
         * Compose pivot data column render using columnData and dataKey based on columnName.
         * @param cData
         * @param dataKey
         */
        const renderPivotDataColumn = (cData, dataKey) => {
            const newDataKey = `${dataKey}_${cData.key}`;

            // Recursively apply function to children.
            if (cData.children) {
                // apply renderPivotDataColumn to each child
                for (let i = 0; i < cData.children.length; i++) {
                    const childData = cData.children[i];
                    renderPivotDataColumn(childData, newDataKey);
                }
            } else {
                // Get data columnRender based on data key lookup into data.
                const columnRender = this.getColumnRender('pivot', data, {
                    dataKey: newDataKey,
                });

                dataColumnRenders.push(
                    <React.Fragment key={`td-${newDataKey}`}>
                        {columnRender}
                    </React.Fragment>
                );
            }
        };

        renderPivotDataColumn(columnData, columnName);
    }

    getChildRender(childData) {
        const childPayload = {
            data: childData,
            hasError: false,
            isInvalidated: false,
            updatedAt: new Date(),
        };

        return (
            <div className="mmui-table-child">
                <MmuiCraftedTableComponent
                    initializeState={true}
                    payload={childPayload}
                />
            </div>
        );
    }

    /**
     * Render the Table Component
     */
    render() {
        let state = this.getComponentState(),
            payload = this.getPayloadData(),
            mode = this.getComponentMode(payload),
            queryData,
            isOkMode,
            isEmptyMode,
            transforms,
            preSortColumn,
            preSortMethod,
            tHead,
            tBody,
            dataRows,
            dataPipeline,
            dataModel,
            tBodyRows,
            columnCount = 0;
        if (state === undefined || state === null || !state.isVisible) {
            return null;
        }

        isOkMode = mode === COMPONENT_MODE_OK;
        isEmptyMode = mode === COMPONENT_MODE_EMPTY;
        // isErrorMode = mode === COMPONENT_MODE_ERROR;
        transforms = state.transforms;

        if (isOkMode || isEmptyMode) {
            dataModel = this.getDataModel();
            if (state.hasPipeline) {
                dataPipeline = getTransformsDataPipeline(transforms);
                dataRows = dataPipeline.transform(dataModel.rows);
            } else {
                dataRows = dataModel.rows;
            }

            queryData = new QueryData(this.getQueryDataObj());

            if (transforms === undefined || transforms.order === undefined) {
                preSortColumn = dataModel.order_by_column;
                preSortMethod = dataModel.order_by_method
                    ? dataModel.order_by_method
                    : 'ASC';
            }

            const rowSelectionCount = this.getRowSelectionCount();
            this.numberOfItems = this.countRows(dataModel.rows);
            let isHeadSelected = false,
                isHeadIndeterminate = false;
            if (this.numberOfItems != 0 && rowSelectionCount != 0) {
                isHeadSelected = rowSelectionCount == this.numberOfItems;
                isHeadIndeterminate = !isHeadSelected && rowSelectionCount > 0;
            }
            /**
             * Data is the result of rollup of columns with a pivot.
             */
            if (queryData.hasPivot && queryData.hasDimensionLevels) {
                const tHeadRows = [],
                    tHeadColumns = [],
                    factColumns = queryData.factColumns,
                    key = `th-level`,
                    columnConfig = this.getColumnConfig('level'),
                    completeColumnConfig = Object.assign(
                        columnConfig,
                        this.defaultColumnConfig
                    );

                //determine if the dimension level is pre-sorted
                let preSort, levelColumnHead;
                if (preSortColumn === 'level') {
                    preSort = preSortMethod;
                }

                levelColumnHead = (
                    <ColumnHeadComponent
                        key={key}
                        config={completeColumnConfig}
                        isSelected={isHeadSelected}
                        isIndeterminate={isHeadIndeterminate}
                        transforms={transforms}
                        setTransforms={this.setTransforms}
                        transformsDelay={this.props.transformsDelay}
                        preSort={preSort}
                        displayDefaultLabel={this.props.displayDefaultLabel}
                    />
                );

                tHeadRows.push(tHeadColumns);

                for (let c = 0; c < dataModel.columns.length; c++) {
                    const factData = dataModel.columns[c],
                        columnName = factData.key,
                        columnConfig = this.getColumnConfig(columnName);
                    let colSpan = this.parsePivotTableHeaders(
                        tHeadRows,
                        factData.children,
                        columnName,
                        1,
                        transforms
                    );
                    colSpan = colSpan === 0 ? 1 : colSpan;
                    tHeadColumns.push(
                        <th key={`th-${0}-${c}`} colSpan={colSpan}>
                            {columnConfig.name}
                        </th>
                    );

                    // add up column count for possible empty row
                    columnCount = columnCount + colSpan;
                }

                const tHeadRowRenders = tHeadRows.map(
                    (columnRenderArray, index) => {
                        let rowRender = null;
                        if (index === tHeadRows.length - 1) {
                            rowRender = levelColumnHead;
                        } else {
                            rowRender = <th></th>;
                        }

                        return (
                            <tr key={`thead-tr-${index}`}>
                                {rowRender}
                                {columnRenderArray}
                            </tr>
                        );
                    }
                );

                // add up column count for possible empty row, add one for levels column
                columnCount = columnCount + 1;

                tHead = <thead>{tHeadRowRenders}</thead>;

                tBodyRows = [];

                for (let i = 0; i < dataModel.rows.length; i++) {
                    const rowData = dataModel.rows[i];
                    this.parseTablePivotLevelsRows(
                        queryData,
                        dataModel,
                        tBodyRows,
                        factColumns,
                        rowData
                    );
                }

                tBody = <tbody>{tBodyRows}</tbody>;
            } else if (queryData.hasDimensionLevels) {
                /**
                 * Data is the result of rollup of columns.
                 */
                let tHeadColumns = [],
                    tSummaryColumns = [],
                    levelColumnHead,
                    levelColumnSummary,
                    columnData,
                    columnConfig,
                    fact,
                    dimension,
                    dimensionLevel,
                    factColumns = queryData.factColumns;
                if (this.props.hasLevelColumns) {
                    levelColumnHead = [];

                    let isSelectionColumn = false;
                    for (
                        let i = 0;
                        i < queryData.dimension_levels.length;
                        i++
                    ) {
                        dimensionLevel = queryData.dimension_levels[i];
                        columnConfig = this.getColumnConfig(dimensionLevel);

                        isSelectionColumn = false;
                        if (i == 0) {
                            columnConfig.hasLevels = true;
                            isSelectionColumn = this.props.hasColumnSelection;
                        }

                        //determine if the dimension level is pre-sorted
                        let preSort;
                        if (preSortColumn === dimensionLevel) {
                            preSort = preSortMethod;
                        }

                        if (dataModel.columns.hasSummary) {
                            columnConfig.hasSummary = true;
                            columnConfig.summaryValueRender =
                                this.getColumnSummaryRender(dimensionLevel, i);
                        }

                        const key = `th-level-${i}`;
                        const completeColumnConfig = Object.assign(
                            columnConfig,
                            this.defaultColumnConfig
                        );
                        levelColumnHead.push(
                            <ColumnHeadComponent
                                key={key}
                                config={completeColumnConfig}
                                isSelected={isHeadSelected}
                                isIndeterminate={isHeadIndeterminate}
                                transforms={transforms}
                                setTransforms={this.setTransforms}
                                transformsDelay={this.props.transformsDelay}
                                isSelectionColumn={isSelectionColumn}
                                onSelectionClick={this.onSelectionClick}
                                preSort={preSort}
                                displayDefaultLabel={this.props.displayDefaultLabel}
                            />
                        );
                        if (dataModel.columns.hasSummary) {
                            tSummaryColumns.push(
                                <SummaryColumnComponent
                                    key={key}
                                    config={completeColumnConfig}
                                />
                            );
                        }
                    }
                } else {
                    const columnName = 'level';
                    columnConfig = this.getColumnConfig(columnName);

                    if (dataModel.columns.hasSummary) {
                        columnConfig.hasSummary = true;
                        columnConfig.summaryValueRender =
                            this.getColumnSummaryRender(columnName, 0);
                    }

                    const key = `th-level`;
                    const completeColumnConfig = Object.assign(
                        columnConfig,
                        this.defaultColumnConfig
                    );
                    levelColumnHead = (
                        <ColumnHeadComponent
                            key={key}
                            config={completeColumnConfig}
                            isSelected={isHeadSelected}
                            isIndeterminate={isHeadIndeterminate}
                            transforms={transforms}
                            setTransforms={this.setTransforms}
                            transformsDelay={this.props.transformsDelay}
                            isSelectionColumn={this.props.hasColumnSelection}
                            onSelectionClick={this.onSelectionClick}
                            displayDefaultLabel={this.props.displayDefaultLabel}
                        />
                    );
                    if (dataModel.columns.hasSummary) {
                        levelColumnSummary = (
                            <SummaryColumnComponent
                                key={key}
                                config={completeColumnConfig}
                            />
                        );
                    }
                }

                if (queryData.dimensions) {
                    for (let i = 0; i < queryData.dimensions.length; i++) {
                        dimension = queryData.dimensions[i];

                        columnConfig = this.getColumnConfig(dimension);

                        if (columnConfig.hasSummary) {
                            columnConfig.summaryValueRender =
                                this.getColumnSummaryRender(dimension, i);
                        }

                        const completeColumnConfig = Object.assign(
                            columnConfig,
                            this.defaultColumnConfig
                        );
                        const key = `th-dim-${i}`;
                        tHeadColumns.push(
                            <ColumnHeadComponent
                                key={key}
                                config={completeColumnConfig}
                                isSelected={isHeadSelected}
                                isIndeterminate={isHeadIndeterminate}
                                transforms={transforms}
                                setTransforms={this.setTransforms}
                                transformsDelay={this.props.transformsDelay}
                                isSelectionColumn={
                                    this.props.hasColumnSelection
                                }
                                onSelectionClick={this.onSelectionClick}
                                displayDefaultLabel={this.props.displayDefaultLabel}
                            />
                        );
                        if (columnConfig.hasSummary) {
                            tSummaryColumns.push(
                                <SummaryColumnComponent
                                    key={key}
                                    config={completeColumnConfig}
                                />
                            );
                        }
                    }
                }

                for (let i = 0; i < factColumns.length; i++) {
                    const key = `th-fact-${i}`;
                    fact = factColumns[i];
                    columnConfig = this.getColumnConfig(fact);

                    //determine if the dimension level is pre-sorted
                    let preSort;
                    if (preSortColumn === fact) {
                        preSort = preSortMethod;
                    }

                    if (dataModel.columns.hasSummary) {
                        columnConfig.hasSummary = true;
                        columnConfig.summaryValueRender =
                            this.getColumnSummaryRender(fact, i);
                    }

                    const completeColumnConfig = Object.assign(
                        columnConfig,
                        this.defaultColumnConfig
                    );
                    columnData = dataModel.columns[fact];

                    tHeadColumns.push(
                        <ColumnHeadComponent
                            key={key}
                            config={completeColumnConfig}
                            data={columnData}
                            isSelected={isHeadSelected}
                            isIndeterminate={isHeadIndeterminate}
                            transforms={transforms}
                            setTransforms={this.setTransforms}
                            transformsDelay={this.props.transformsDelay}
                            hasRowLevels={true}
                            preSort={preSort}
                            displayDefaultLabel={this.props.displayDefaultLabel}
                        />
                    );
                    if (dataModel.columns.hasSummary) {
                        tSummaryColumns.push(
                            <SummaryColumnComponent
                                key={key}
                                config={completeColumnConfig}
                            />
                        );
                    }
                }

                if (this.props.hasColumnAction) {
                    tHeadColumns.push(
                        <th key="th-action" className="mmui-column-action"></th>
                    );
                }

                // set column count for possible empty row
                if (this.props.hasLevelColumns) {
                    columnCount =
                        queryData.dimension_levels.length + factColumns.length;
                } else {
                    columnCount = queryData.columns.length;
                }

                tHead = (
                    <thead>
                        <tr>
                            {levelColumnHead}
                            {tHeadColumns}
                        </tr>
                        <tr>
                            {levelColumnSummary}
                            {tSummaryColumns}
                        </tr>
                    </thead>
                );

                tBodyRows = [];

                for (let i = 0; i < dataRows.length; i++) {
                    const rowData = dataRows[i];
                    this.parseTableLevelsRows(
                        queryData,
                        tBodyRows,
                        factColumns,
                        rowData
                    );
                }

                tBody = <tbody>{tBodyRows}</tbody>;
            } else if (queryData.hasPivot) {
                /**
                 * Data is the result of a pivot.
                 */
                const tHeadRows = [],
                    tHeadColumns = [];
                tHeadRows.push(tHeadColumns);

                const pivotRowCount = queryData.dimensions.length,
                    pivotRowRender = queryData.dimensions.map(
                        (columnName, index) => {
                            const key = `th-${index}`,
                                columnConfig = this.getColumnConfig(columnName),
                                completeColumnConfig = Object.assign(
                                    columnConfig,
                                    this.defaultColumnConfig
                                );

                            //determine if columnName is pre-sorted
                            let preSort;
                            if (preSortColumn === columnName) {
                                preSort = preSortMethod;
                            }

                            return (
                                <ColumnHeadComponent
                                    key={key}
                                    config={completeColumnConfig}
                                    isSelected={isHeadSelected}
                                    isIndeterminate={isHeadIndeterminate}
                                    transforms={transforms}
                                    setTransforms={this.setTransforms}
                                    transformsDelay={this.props.transformsDelay}
                                    preSort={preSort}
                                    displayDefaultLabel={this.props.displayDefaultLabel}
                                />
                            );
                        }
                    );

                // add up column count for possible empty row
                columnCount = columnCount + pivotRowCount;

                const pivotRowRenderEmpty = <th colSpan={pivotRowCount}></th>;

                for (let i = 0; i < dataModel.columns.length; i++) {
                    const factData = dataModel.columns[i],
                        columnName = factData.key,
                        columnConfig = this.getColumnConfig(columnName);
                    let colSpan = this.parsePivotTableHeaders(
                        tHeadRows,
                        factData.children,
                        columnName,
                        1,
                        transforms
                    );
                    colSpan = colSpan === 0 ? 1 : colSpan;
                    tHeadColumns.push(
                        <th key={`th-${0}-${i}`} colSpan={colSpan}>
                            {columnConfig.name}
                        </th>
                    );

                    // add up column count for possible empty row
                    columnCount = columnCount + colSpan;
                }

                const tHeadRowRenders = tHeadRows.map(
                    (columnRenderArray, index) => {
                        let rowRender = null;
                        if (index === tHeadRows.length - 1) {
                            rowRender = pivotRowRender;
                        } else {
                            rowRender = pivotRowRenderEmpty;
                        }

                        return (
                            <tr key={`thead-tr-${index}`}>
                                {rowRender}
                                {columnRenderArray}
                            </tr>
                        );
                    }
                );

                tHead = <thead>{tHeadRowRenders}</thead>;

                //render body
                const dataRowRenders = [];
                dataRows.map((data, index) => {
                    let dataColumnRenders = [],
                        pivotRowName,
                        dataColumnRender;

                    for (let i = 0; i < queryData.dimensions.length; i++) {
                        pivotRowName = queryData.dimensions[i];
                        dataColumnRender = this.getColumnRender(
                            pivotRowName,
                            data,
                            {
                                key: `td-${index}-${i}`,
                            }
                        );
                        dataColumnRenders.push(dataColumnRender);
                    }

                    for (let c = 0; c < dataModel.columns.length; c++) {
                        const factData = dataModel.columns[c],
                            columnName = factData.key;
                        for (const columnData of factData.children) {
                            this.parsePivotDataRow(
                                columnName,
                                columnData,
                                dataColumnRenders,
                                data
                            );
                        }
                    }

                    dataRowRenders.push(
                        <tr key={`tbody-tr-${index}`} onClick={this.onRowClick}>
                            {dataColumnRenders}
                        </tr>
                    );
                });

                tBody = <tbody>{dataRowRenders}</tbody>;
            } else {
                /**
                 * Data is the result of a group by of columns.
                 */
                const tHeadColumns = queryData.columns.map(
                    (columnName, index) => {
                        const key = `th-${index}`,
                            // isDistinct = dataDistinct[columnName],
                            isDistinct = false,
                            columnConfig = this.getColumnConfig(columnName, {
                                isDistinct: isDistinct,
                            }),
                            columnData = dataModel.columns[columnName],
                            isSelectionColumn =
                                this.props.hasColumnSelection && index == 0;
                        if (dataModel.columns.hasSummary) {
                            columnConfig.hasSummary = true;
                            columnConfig.summaryValueRender =
                                this.getColumnSummaryRender(columnName, index);
                        }

                        let preSort,
                            completeColumnConfig = Object.assign(
                                columnConfig,
                                this.defaultColumnConfig
                            );

                        //determine if columnName is pre-sorted
                        if (preSortColumn === columnName) {
                            preSort = preSortMethod;
                        }

                        return (
                            <ColumnHeadComponent
                                key={key}
                                config={completeColumnConfig}
                                isSelected={isHeadSelected}
                                isIndeterminate={isHeadIndeterminate}
                                data={columnData}
                                transforms={transforms}
                                setTransforms={this.setTransforms}
                                transformsDelay={this.props.transformsDelay}
                                isSelectionColumn={isSelectionColumn}
                                onSelectionClick={this.onSelectionClick}
                                preSort={preSort}
                                displayDefaultLabel={this.props.displayDefaultLabel}
                            />
                        );
                    }
                );

                if (this.props.hasColumnAction) {
                    tHeadColumns.push(
                        <th key="th-action" className="mmui-column-action"></th>
                    );
                }

                let tSummaryColumns = null;
                if (dataModel.columns.hasSummary) {
                    tSummaryColumns = queryData.columns.map(
                        (columnName, index) => {
                            const key = `th-${index}`,
                                columnConfig = this.getColumnConfig(
                                    columnName,
                                    {
                                        isDistinct: false,
                                    }
                                );

                            columnConfig.hasSummary = true;
                            columnConfig.summaryValueRender =
                                this.getColumnSummaryRender(columnName, index);

                            const completeColumnConfig = Object.assign(
                                columnConfig,
                                this.defaultColumnConfig
                            );

                            return (
                                <SummaryColumnComponent
                                    key={key}
                                    config={completeColumnConfig}
                                />
                            );
                        }
                    );
                }

                tHead = (
                    <thead>
                        <tr>{tHeadColumns}</tr>
                        <tr>{tSummaryColumns}</tr>
                    </thead>
                );

                // set column count for possible empty row
                columnCount = queryData.columns.length;

                tBodyRows = dataRows.map((data, index) => {
                    const hasChildRow = Object.prototype.hasOwnProperty.call(
                            data,
                            'child_data'
                        ),
                        tBodyColumns = queryData.columns.map(
                            (columnName, index) => {
                                const columnRender = this.getColumnRender(
                                    columnName,
                                    data
                                );
                                return (
                                    <React.Fragment key={`td-${index}`}>
                                        {columnRender}
                                    </React.Fragment>
                                );
                            }
                        );

                    if (this.props.hasColumnAction) {
                        tBodyColumns.push(this.getColumnActionRender(data));
                    }

                    let childRenderRow,
                        currentRowState = state.rows[data.row_id];
                    if (
                        hasChildRow &&
                        currentRowState !== undefined &&
                        currentRowState.mode === ROW_MODE_OPEN
                    ) {
                        const childRender = this.getChildRender(
                            data.child_data
                        );
                        childRenderRow = (
                            <tr key="tr-child-row">
                                <td
                                    className="mmui-td-child"
                                    colSpan={columnCount}
                                >
                                    {childRender}
                                </td>
                            </tr>
                        );
                    }

                    return (
                        <React.Fragment key={`tbody-tr-${index}`}>
                            <tr
                                onClick={this.onRowClick}
                                data-rowid={data.row_id}
                            >
                                {tBodyColumns}
                            </tr>
                            {childRenderRow}
                        </React.Fragment>
                    );
                });

                tBody = <tbody>{tBodyRows}</tbody>;
            }

            // create an empty row if data has been refined to no results
            if (dataRows.length <= 0) {
                tBodyRows = this.createEmptyRow(columnCount);
                tBody = <tbody>{tBodyRows}</tbody>;
            }
        }

        return (
            <React.Fragment>
                <div className={this.tableContainerClassNameList.join(' ')}>
                    <table className={this.tableClassNameList.join(' ')}>
                        {tHead}
                        {tBody}
                    </table>
                </div>
                <ComponentModeWindow
                    mode={mode}
                    reportIssueUrl={this.props.reportIssueUrl}
                    loadingGraphicUrl={this.loadingImage}
                    hasErrorMode={true}
                />
            </React.Fragment>
        );
    }
}
