import 'react-data-grid/lib/styles.css';
import { css } from '@emotion/css';
import { useMemo, useState, useLayoutEffect, useCallback, useRef, useEffect } from 'react';
import DataGrid, { RenderCellProps, RenderRowProps, Row, SortColumn, DataGridHandle } from 'react-data-grid';
import { useMeasure } from 'react-use';

import {
  DataFrame,
  DataHoverClearEvent,
  DataHoverEvent,
  Field,
  fieldReducers,
  FieldType,
  formattedValueToString,
  getDefaultTimeRange,
  GrafanaTheme2,
  ReducerID,
} from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema';

import { useStyles2, useTheme2 } from '../../../themes';
import { t, Trans } from '../../../utils/i18n';
import { ContextMenu } from '../../ContextMenu/ContextMenu';
import { MenuItem } from '../../Menu/MenuItem';
import { Pagination } from '../../Pagination/Pagination';
import { PanelContext, usePanelContext } from '../../PanelChrome';
import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector';

import { HeaderCell } from './Cells/HeaderCell';
import { RowExpander } from './Cells/RowExpander';
import { TableCellNG } from './Cells/TableCellNG';
import { COLUMN, TABLE } from './constants';
import {
  TableNGProps,
  FilterType,
  TableRow,
  TableSummaryRow,
  ColumnTypes,
  TableColumnResizeActionCallback,
  TableColumn,
  TableFieldOptionsType,
  ScrollPosition,
  CellColors,
} from './types';
import {
  frameToRecords,
  getCellColors,
  getCellHeightCalculator,
  getComparator,
  getDefaultRowHeight,
  getFooterItemNG,
  getFooterStyles,
  getIsNestedTable,
  getRowHeight,
  getTextAlign,
  handleSort,
  MapFrameToGridOptions,
  shouldTextOverflow,
} from './utils';

export function TableNG(props: TableNGProps) {
  const {
    cellHeight,
    enablePagination,
    enableVirtualization = true,
    fieldConfig,
    footerOptions,
    height,
    initialSortBy,
    noHeader,
    onCellFilterAdded,
    onColumnResize,
    onSortByChange,
    width,
    data,
    enableSharedCrosshair,
    showTypeIcons,
    replaceVariables,
  } = props;

  const initialSortColumns = useMemo<SortColumn[]>(() => {
    const initialSort = initialSortBy?.map(({ displayName, desc }) => {
      const matchingField = data.fields.find(({ state }) => state?.displayName === displayName);
      const columnKey = matchingField?.name || displayName;

      return {
        columnKey,
        direction: desc ? ('DESC' as const) : ('ASC' as const),
      };
    });
    return initialSort ?? [];
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  /* ------------------------------- Local state ------------------------------ */
  const [revId, setRevId] = useState(0);
  const [contextMenuProps, setContextMenuProps] = useState<{
    rowIdx?: number;
    value: string;
    mode?: TableCellInspectorMode.code | TableCellInspectorMode.text;
    top?: number;
    left?: number;
  } | null>(null);
  const [isInspecting, setIsInspecting] = useState(false);
  const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
  const [filter, setFilter] = useState<FilterType>({});
  const [page, setPage] = useState(0);
  // This state will trigger re-render for recalculating row heights
  const [, setResizeTrigger] = useState(0);
  const [, setReadyForRowHeightCalc] = useState(false);
  const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>(initialSortColumns);
  const [expandedRows, setExpandedRows] = useState<number[]>([]);
  const [isNestedTable, setIsNestedTable] = useState(false);
  const scrollPositionRef = useRef<ScrollPosition>({ x: 0, y: 0 });
  const [hasScroll, setHasScroll] = useState(false);

  /* ------------------------------- Local refs ------------------------------- */
  const crossFilterOrder = useRef<string[]>([]);
  const crossFilterRows = useRef<Record<string, TableRow[]>>({});
  const headerCellRefs = useRef<Record<string, HTMLDivElement>>({});
  // TODO: This ref persists sortColumns between renders. setSortColumns is still used to trigger re-render
  const sortColumnsRef = useRef<SortColumn[]>(initialSortColumns);
  const prevProps = useRef(props);
  const calcsRef = useRef<string[]>([]);
  const [paginationWrapperRef, { height: paginationHeight }] = useMeasure<HTMLDivElement>();

  const theme = useTheme2();
  const panelContext = usePanelContext();

  const isFooterVisible = Boolean(footerOptions?.show && footerOptions.reducer?.length);
  const isCountRowsSet = Boolean(
    footerOptions?.countRows &&
      footerOptions.reducer &&
      footerOptions.reducer.length &&
      footerOptions.reducer[0] === ReducerID.count
  );
  const tableRef = useRef<DataGridHandle | null>(null);

  /* --------------------------------- Effects -------------------------------- */
  useEffect(() => {
    // TODO: there is a use case when adding a new column to the table doesn't update the table
    if (
      prevProps.current.data.fields.length !== props.data.fields.length ||
      prevProps.current.fieldConfig?.overrides !== fieldConfig?.overrides ||
      prevProps.current.fieldConfig?.defaults !== fieldConfig?.defaults
    ) {
      setRevId(revId + 1);
    }
    prevProps.current = props;
  }, [props, revId, fieldConfig?.overrides, fieldConfig?.defaults]); // eslint-disable-line react-hooks/exhaustive-deps

  useLayoutEffect(() => {
    if (!isContextMenuOpen) {
      return;
    }

    function onClick(event: MouseEvent) {
      setIsContextMenuOpen(false);
    }

    addEventListener('click', onClick);

    return () => {
      removeEventListener('click', onClick);
    };
  }, [isContextMenuOpen]);

  useEffect(() => {
    const hasNestedFrames = getIsNestedTable(props.data);
    setIsNestedTable(hasNestedFrames);
  }, [props.data]);

  useEffect(() => {
    const el = tableRef.current;
    if (el) {
      const gridElement = el?.element;
      if (gridElement) {
        setHasScroll(
          gridElement.scrollHeight > gridElement.clientHeight || gridElement.scrollWidth > gridElement.clientWidth
        );
      }
    }
  }, []);

  // TODO: this is a hack to force the column width to update when the fieldConfig changes
  const columnWidth = useMemo(() => {
    setRevId(revId + 1);
    return fieldConfig?.defaults?.custom?.width || 'auto';
  }, [fieldConfig]); // eslint-disable-line react-hooks/exhaustive-deps

  const defaultRowHeight = getDefaultRowHeight(theme, cellHeight);
  const defaultLineHeight = theme.typography.body.lineHeight * theme.typography.fontSize;
  const panelPaddingHeight = theme.components.panel.padding * theme.spacing.gridSize * 2;

  /* ------------------------------ Rows & Columns ----------------------------- */
  const rows = useMemo(() => frameToRecords(props.data), [frameToRecords, props.data]); // eslint-disable-line react-hooks/exhaustive-deps

  // Create a map of column key to column type
  const columnTypes = useMemo(
    () => props.data.fields.reduce<ColumnTypes>((acc, { name, type }) => ({ ...acc, [name]: type }), {}),
    [props.data.fields]
  );

  // Create a map of column key to text wrap
  const textWraps = useMemo(
    () =>
      props.data.fields.reduce<{ [key: string]: boolean }>(
        (acc, { name, config }) => ({ ...acc, [name]: config?.custom?.cellOptions?.wrapText ?? false }),
        {}
      ),
    [props.data.fields]
  );

  const textWrap = useMemo(() => Object.values(textWraps).some(Boolean), [textWraps]);
  const styles = useStyles2(getStyles);

  // Create a function to get column widths for text wrapping calculations
  const getColumnWidths = useCallback(() => {
    const widths: Record<string, number> = {};

    // Set default widths from field config if they exist
    props.data.fields.forEach(({ name, config }) => {
      const configWidth = config?.custom?.width;
      const totalWidth = typeof configWidth === 'number' ? configWidth : COLUMN.DEFAULT_WIDTH;
      // subtract out padding and 1px right border
      const contentWidth = totalWidth - 2 * TABLE.CELL_PADDING - 1;
      widths[name] = contentWidth;
    });

    // Measure actual widths if available
    Object.keys(headerCellRefs.current).forEach((key) => {
      const headerCell = headerCellRefs.current[key];

      if (headerCell.offsetWidth > 0) {
        widths[key] = headerCell.offsetWidth;
      }
    });

    return widths;
  }, [props.data.fields]);

  const headersLength = useMemo(() => {
    return props.data.fields.length;
  }, [props.data.fields]);

  const fieldDisplayType = useMemo(() => {
    return props.data.fields.reduce<Record<string, TableCellDisplayMode>>((acc, { config, name }) => {
      if (config?.custom?.cellOptions?.type) {
        acc[name] = config.custom.cellOptions.type;
      }
      return acc;
    }, {});
  }, [props.data.fields]);

  // Clean up fieldsData to simplify
  const fieldsData = useMemo(
    () => ({
      headersLength,
      textWraps,
      columnTypes,
      fieldDisplayType,
      columnWidths: getColumnWidths(),
    }),
    [textWraps, columnTypes, getColumnWidths, headersLength, fieldDisplayType]
  );

  const getDisplayedValue = (row: TableRow, key: string) => {
    const field = props.data.fields.find((field) => field.name === key)!;
    const displayedValue = formattedValueToString(field.display!(row[key]));
    return displayedValue;
  };

  // Filter rows
  const filteredRows = useMemo(() => {
    const filterValues = Object.entries(filter);
    if (filterValues.length === 0) {
      // reset cross filter order
      crossFilterOrder.current = [];
      return rows;
    }

    // Update crossFilterOrder
    const filterKeys = new Set(filterValues.map(([key]) => key));
    filterKeys.forEach((key) => {
      if (!crossFilterOrder.current.includes(key)) {
        // Each time a filter is added or removed, it is always a single filter.
        // When adding a new filter, it is always appended to the end, maintaining the order.
        crossFilterOrder.current.push(key);
      }
    });
    // Remove keys from crossFilterOrder that are no longer present in the current filter values
    crossFilterOrder.current = crossFilterOrder.current.filter((key) => filterKeys.has(key));

    // reset crossFilterRows
    crossFilterRows.current = {};

    return rows.filter((row) => {
      for (const [key, value] of filterValues) {
        const displayedValue = getDisplayedValue(row, key);
        if (!value.filteredSet.has(displayedValue)) {
          return false;
        }
        // collect rows for crossFilter
        if (!crossFilterRows.current[key]) {
          crossFilterRows.current[key] = [row];
        } else {
          crossFilterRows.current[key].push(row);
        }
      }
      return true;
    });
  }, [rows, filter, props.data.fields]); // eslint-disable-line react-hooks/exhaustive-deps

  // Sort rows
  const sortedRows = useMemo(() => {
    const comparators = sortColumns.map(({ columnKey }) => getComparator(columnTypes[columnKey]));
    const sortDirs = sortColumns.map(({ direction }) => (direction === 'ASC' ? 1 : -1));

    if (sortColumns.length === 0) {
      return filteredRows;
    }

    return filteredRows.slice().sort((a, b) => {
      let result = 0;
      let sortIndex = 0;

      for (const { columnKey } of sortColumns) {
        const compare = comparators[sortIndex];
        result = sortDirs[sortIndex] * compare(a[columnKey], b[columnKey]);

        if (result !== 0) {
          break;
        }

        sortIndex += 1;
      }

      return result;
    });
  }, [filteredRows, sortColumns, columnTypes]);

  // Paginated rows
  // TODO consolidate calculations into pagination wrapper component and only use when needed
  const numRows = sortedRows.length;
  // calculate number of rowsPerPage based on height stack
  let headerCellHeight = TABLE.MAX_CELL_HEIGHT;
  if (noHeader) {
    headerCellHeight = 0;
  } else if (!noHeader && Object.keys(headerCellRefs.current).length > 0) {
    headerCellHeight = headerCellRefs.current[Object.keys(headerCellRefs.current)[0]].getBoundingClientRect().height;
  }
  let rowsPerPage = Math.floor(
    (height - headerCellHeight - TABLE.SCROLL_BAR_WIDTH - paginationHeight - panelPaddingHeight) / defaultRowHeight
  );
  // if footer calcs are on, remove one row per page
  if (isFooterVisible) {
    rowsPerPage -= 1;
  }
  if (rowsPerPage < 1) {
    // avoid 0 or negative rowsPerPage
    rowsPerPage = 1;
  }
  const numberOfPages = Math.ceil(numRows / rowsPerPage);
  if (page > numberOfPages) {
    // resets pagination to end
    setPage(numberOfPages - 1);
  }
  // calculate row range for pagination summary display
  const itemsRangeStart = page * rowsPerPage + 1;
  let displayedEnd = itemsRangeStart + rowsPerPage - 1;
  if (displayedEnd > numRows) {
    displayedEnd = numRows;
  }
  const smallPagination = width < TABLE.PAGINATION_LIMIT;

  const paginatedRows = useMemo(() => {
    const pageOffset = page * rowsPerPage;
    return sortedRows.slice(pageOffset, pageOffset + rowsPerPage);
  }, [rows, sortedRows, page, rowsPerPage]); // eslint-disable-line react-hooks/exhaustive-deps

  useMemo(() => {
    calcsRef.current = props.data.fields.map((field, index) => {
      if (field.state?.calcs) {
        delete field.state?.calcs;
      }
      if (isCountRowsSet) {
        return index === 0 ? `${sortedRows.length}` : '';
      }
      if (index === 0) {
        const footerCalcReducer = footerOptions?.reducer?.[0];
        return footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : '';
      }
      return getFooterItemNG(sortedRows, field, footerOptions);
    });
  }, [sortedRows, props.data.fields, footerOptions, isCountRowsSet]); // eslint-disable-line react-hooks/exhaustive-deps

  const onCellExpand = (rowIdx: number) => {
    if (!expandedRows.includes(rowIdx)) {
      setExpandedRows([...expandedRows, rowIdx]);
    } else {
      const currentExpandedRows = expandedRows;
      const indexToRemove = currentExpandedRows.indexOf(rowIdx);
      if (indexToRemove > -1) {
        currentExpandedRows.splice(indexToRemove, 1);
        setExpandedRows(currentExpandedRows);
      }
    }
    setResizeTrigger((prev) => prev + 1);
  };

  const { ctx, avgCharWidth } = useMemo(() => {
    const font = `${theme.typography.fontSize}px ${theme.typography.fontFamily}`;
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d')!;
    // set in grafana/data in createTypography.ts
    const letterSpacing = 0.15;

    ctx.letterSpacing = `${letterSpacing}px`;
    ctx.font = font;
    let txt =
      "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s";
    const txtWidth = ctx.measureText(txt).width;
    const avgCharWidth = txtWidth / txt.length + letterSpacing;

    return {
      ctx,
      font,
      avgCharWidth,
    };
  }, [theme.typography.fontSize, theme.typography.fontFamily]);

  const columns = useMemo(
    () =>
      mapFrameToDataGrid({
        frame: props.data,
        calcsRef,
        options: {
          columnTypes,
          textWraps,
          columnWidth,
          crossFilterOrder,
          crossFilterRows,
          defaultLineHeight,
          defaultRowHeight,
          expandedRows,
          filter,
          headerCellRefs,
          isCountRowsSet,
          onCellFilterAdded,
          ctx,
          onSortByChange,
          rows,
          // INFO: sortedRows is for correct row indexing for cell background coloring
          sortedRows,
          setContextMenuProps,
          setFilter,
          setIsInspecting,
          setSortColumns,
          sortColumnsRef,
          styles,
          theme,
          showTypeIcons,
          replaceVariables,
          ...props,
        },
        handlers: {
          onCellExpand,
          onColumnResize: onColumnResize!,
        },
        // Adjust table width to account for the scroll bar width
        availableWidth: width - (hasScroll ? TABLE.SCROLL_BAR_WIDTH + TABLE.SCROLL_BAR_MARGIN : 0),
      }),
    [props.data, calcsRef, filter, expandedRows, expandedRows.length, footerOptions, width, hasScroll, sortedRows] // eslint-disable-line react-hooks/exhaustive-deps
  );

  // This effect needed to set header cells refs before row height calculation
  useLayoutEffect(() => {
    setReadyForRowHeightCalc(Object.keys(headerCellRefs.current).length > 0);
  }, [columns]);

  const renderMenuItems = () => {
    return (
      <>
        <MenuItem
          label={t('grafana-ui.table.inspect-menu-label', 'Inspect value')}
          onClick={() => {
            setIsInspecting(true);
          }}
          className={styles.menuItem}
        />
      </>
    );
  };

  const cellHeightCalc = useMemo(() => {
    return getCellHeightCalculator(ctx, defaultLineHeight, defaultRowHeight, TABLE.CELL_PADDING);
  }, [ctx, defaultLineHeight, defaultRowHeight]);

  const calculateRowHeight = useCallback(
    (row: TableRow) => {
      // Logic for sub-tables
      if (Number(row.__depth) === 1 && !expandedRows.includes(Number(row.__index))) {
        return 0;
      } else if (Number(row.__depth) === 1 && expandedRows.includes(Number(row.__index))) {
        const headerCount = row?.data?.meta?.custom?.noHeader ? 0 : 1;
        return defaultRowHeight * (row.data?.length ?? 0 + headerCount); // TODO this probably isn't very robust
      }
      return getRowHeight(row, cellHeightCalc, avgCharWidth, defaultRowHeight, fieldsData);
    },
    [expandedRows, avgCharWidth, defaultRowHeight, fieldsData, cellHeightCalc]
  );

  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    const target = event.currentTarget;
    scrollPositionRef.current = {
      x: target.scrollLeft,
      y: target.scrollTop,
    };
  };

  // Reset sortColumns when initialSortBy changes
  useEffect(() => {
    if (initialSortColumns.length > 0) {
      setSortColumns(initialSortColumns);
    }
  }, [initialSortColumns]);

  // Restore scroll position after re-renders
  useEffect(() => {
    if (tableRef.current?.element) {
      tableRef.current.element.scrollLeft = scrollPositionRef.current.x;
      tableRef.current.element.scrollTop = scrollPositionRef.current.y;
    }
  }, [revId]);

  return (
    <>
      <DataGrid<TableRow, TableSummaryRow>
        ref={tableRef}
        className={styles.dataGrid}
        // Default to true, overridden to false for testing
        enableVirtualization={enableVirtualization}
        key={`DataGrid${revId}`}
        rows={enablePagination ? paginatedRows : sortedRows}
        columns={columns}
        headerRowHeight={noHeader ? 0 : undefined}
        defaultColumnOptions={{
          sortable: true,
          resizable: true,
        }}
        rowHeight={textWrap || isNestedTable ? calculateRowHeight : defaultRowHeight}
        // TODO: This doesn't follow current table behavior
        style={{ width, height: height - (enablePagination ? paginationHeight : 0) }}
        renderers={{
          renderRow: (key, props) =>
            myRowRenderer(key, props, expandedRows, panelContext, data, enableSharedCrosshair ?? false),
        }}
        onScroll={handleScroll}
        onCellContextMenu={({ row, column }, event) => {
          event.preventGridDefault();
          // Do not show the default context menu
          event.preventDefault();

          const cellValue = row[column.key];
          setContextMenuProps({
            // rowIdx: rows.indexOf(row),
            value: String(cellValue ?? ''),
            top: event.clientY,
            left: event.clientX,
          });
          setIsContextMenuOpen(true);
        }}
        // sorting
        sortColumns={sortColumns}
        // footer
        // TODO figure out exactly how this works - some array needs to be here for it to render regardless of renderSummaryCell()
        bottomSummaryRows={isFooterVisible ? [{}] : undefined}
        onColumnResize={() => {
          // NOTE: This method is called continuously during the column resize drag operation,
          // providing the current column width. There is no separate event for the end of the drag operation.
          if (textWrap) {
            // This is needed only when textWrap is enabled
            // TODO: this is a hack to force rowHeight re-calculation
            setResizeTrigger((prev) => prev + 1);
          }
        }}
      />

      {enablePagination && (
        <div className={styles.paginationContainer} ref={paginationWrapperRef}>
          <Pagination
            className="table-ng-pagination"
            currentPage={page + 1}
            numberOfPages={numberOfPages}
            showSmallVersion={smallPagination}
            onNavigate={(toPage) => {
              setPage(toPage - 1);
            }}
          />
          {!smallPagination && (
            <div className={styles.paginationSummary}>
              <Trans i18nKey="grafana-ui.table.pagination-summary">
                {{ itemsRangeStart }} - {{ displayedEnd }} of {{ numRows }} rows
              </Trans>
            </div>
          )}
        </div>
      )}

      {isContextMenuOpen && (
        <ContextMenu
          x={contextMenuProps?.left || 0}
          y={contextMenuProps?.top || 0}
          renderMenuItems={renderMenuItems}
          focusOnOpen={false}
        />
      )}

      {isInspecting && (
        <TableCellInspector
          mode={contextMenuProps?.mode ?? TableCellInspectorMode.text}
          value={contextMenuProps?.value}
          onDismiss={() => {
            setIsInspecting(false);
            setContextMenuProps(null);
          }}
        />
      )}
    </>
  );
}

export function mapFrameToDataGrid({
  frame,
  calcsRef,
  options,
  handlers,
  availableWidth,
}: {
  frame: DataFrame;
  calcsRef: React.MutableRefObject<string[]>;
  options: MapFrameToGridOptions;
  handlers: { onCellExpand: (rowIdx: number) => void; onColumnResize: TableColumnResizeActionCallback };
  availableWidth: number;
}): TableColumn[] {
  const {
    columnTypes,
    textWraps,
    crossFilterOrder,
    crossFilterRows,
    defaultLineHeight,
    defaultRowHeight,
    expandedRows,
    filter,
    headerCellRefs,
    isCountRowsSet,
    onCellFilterAdded,
    ctx,
    onSortByChange,
    rows,
    sortedRows,
    setContextMenuProps,
    setFilter,
    setIsInspecting,
    setSortColumns,
    sortColumnsRef,
    styles,
    theme,
    timeRange,
    getActions,
    showTypeIcons,
    replaceVariables,
  } = options;
  const { onCellExpand, onColumnResize } = handlers;

  const columns: TableColumn[] = [];
  const hasNestedFrames = getIsNestedTable(frame);

  // If nested frames, add expansion control column
  if (hasNestedFrames) {
    const expanderField: Field = {
      name: '',
      type: FieldType.other,
      config: {},
      values: [],
    };
    columns.push({
      key: 'expanded',
      name: '',
      field: expanderField,
      cellClass: styles.cell,
      colSpan(args) {
        return args.type === 'ROW' && Number(args.row.__depth) === 1 ? frame.fields.length : 1;
      },
      renderCell: ({ row }) => {
        // TODO add TableRow type extension to include row depth and optional data
        if (Number(row.__depth) === 0) {
          const rowIdx = Number(row.__index);
          return (
            <RowExpander
              height={defaultRowHeight}
              onCellExpand={() => onCellExpand(rowIdx)}
              isExpanded={expandedRows.includes(rowIdx)}
            />
          );
        }
        // If it's a child, render entire DataGrid at first column position
        let expandedColumns: TableColumn[] = [];
        let expandedRecords: TableRow[] = [];

        // Type guard to check if data exists as it's optional
        if (row.data) {
          expandedColumns = mapFrameToDataGrid({
            frame: row.data,
            calcsRef,
            options: { ...options },
            handlers: { onCellExpand, onColumnResize },
            availableWidth: availableWidth - COLUMN.EXPANDER_WIDTH,
          });
          expandedRecords = frameToRecords(row.data);
        }

        // TODO add renderHeaderCell HeaderCell's here and handle all features
        return (
          <DataGrid<TableRow, TableSummaryRow>
            rows={expandedRecords}
            columns={expandedColumns}
            rowHeight={defaultRowHeight}
            style={{ height: '100%', overflow: 'visible', marginLeft: COLUMN.EXPANDER_WIDTH }}
            headerRowHeight={row.data?.meta?.custom?.noHeader ? 0 : undefined}
          />
        );
      },
      width: COLUMN.EXPANDER_WIDTH,
      minWidth: COLUMN.EXPANDER_WIDTH,
    });

    availableWidth -= COLUMN.EXPANDER_WIDTH;
  }

  // Row background color function
  let rowBg: Function | undefined = undefined;
  for (const field of frame.fields) {
    const fieldOptions = field.config.custom;
    const cellOptionsExist = fieldOptions !== undefined && fieldOptions.cellOptions !== undefined;

    if (
      cellOptionsExist &&
      fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground &&
      fieldOptions.cellOptions.applyToRow
    ) {
      rowBg = (rowIndex: number): CellColors => {
        const display = field.display!(field.values[rowIndex]);
        const colors = getCellColors(theme, fieldOptions.cellOptions, display);
        return colors;
      };
    }
  }

  let fieldCountWithoutWidth = 0;
  frame.fields.map((field, fieldIndex) => {
    if (field.type === FieldType.nestedFrames || field.config.custom?.hidden) {
      // Don't render nestedFrames type field
      return;
    }
    const fieldTableOptions: TableFieldOptionsType = field.config.custom || {};
    const key = field.name;
    const justifyColumnContent = getTextAlign(field);
    const footerStyles = getFooterStyles(justifyColumnContent);

    // current/old table width logic calculations
    if (fieldTableOptions.width) {
      availableWidth -= fieldTableOptions.width;
    } else {
      fieldCountWithoutWidth++;
    }

    // Add a column for each field
    columns.push({
      key,
      name: field.name,
      field,
      cellClass: textWraps[field.name] ? styles.cellWrapped : styles.cell,
      renderCell: (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
        const { row, rowIdx } = props;
        const cellType = field.config?.custom?.cellOptions?.type ?? TableCellDisplayMode.Auto;
        const value = row[key];
        // Cell level rendering here
        return (
          <TableCellNG
            frame={frame}
            key={key}
            value={value}
            field={field}
            theme={theme}
            timeRange={timeRange ?? getDefaultTimeRange()}
            height={defaultRowHeight}
            justifyContent={justifyColumnContent}
            rowIdx={sortedRows[rowIdx].__index}
            shouldTextOverflow={() =>
              shouldTextOverflow(
                key,
                row,
                columnTypes,
                headerCellRefs,
                ctx,
                defaultLineHeight,
                defaultRowHeight,
                TABLE.CELL_PADDING,
                textWraps[field.name],
                field,
                cellType
              )
            }
            setIsInspecting={setIsInspecting}
            setContextMenuProps={setContextMenuProps}
            getActions={getActions}
            rowBg={rowBg}
            onCellFilterAdded={onCellFilterAdded}
            replaceVariables={replaceVariables}
          />
        );
      },
      renderSummaryCell: () => {
        if (isCountRowsSet && fieldIndex === 0) {
          return (
            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
              <span>
                <Trans i18nKey="grafana-ui.table.count">Count</Trans>
              </span>
              <span>{calcsRef.current[fieldIndex]}</span>
            </div>
          );
        }
        return <div className={footerStyles.footerCell}>{calcsRef.current[fieldIndex]}</div>;
      },
      renderHeaderCell: ({ column, sortDirection }): JSX.Element => (
        <HeaderCell
          column={column}
          rows={rows}
          field={field}
          onSort={(columnKey, direction, isMultiSort) => {
            handleSort(columnKey, direction, isMultiSort, setSortColumns, sortColumnsRef);

            // Update panel context with the new sort order
            if (onSortByChange) {
              const sortByFields = sortColumnsRef.current.map(({ columnKey, direction }) => ({
                displayName: columnKey,
                desc: direction === 'DESC',
              }));
              onSortByChange(sortByFields);
            }
          }}
          direction={sortDirection}
          justifyContent={justifyColumnContent}
          filter={filter}
          setFilter={setFilter}
          onColumnResize={onColumnResize}
          headerCellRefs={headerCellRefs}
          crossFilterOrder={crossFilterOrder}
          crossFilterRows={crossFilterRows}
          showTypeIcons={showTypeIcons}
        />
      ),
      width: fieldTableOptions.width,
      minWidth: fieldTableOptions.minWidth || COLUMN.DEFAULT_WIDTH,
    });
  });

  // set columns that are at minimum width
  let sharedWidth = availableWidth / fieldCountWithoutWidth;
  for (let i = fieldCountWithoutWidth; i > 0; i--) {
    for (const column of columns) {
      if (!column.width && column.minWidth! > sharedWidth) {
        column.width = column.minWidth;
        availableWidth -= column.width!;
        fieldCountWithoutWidth -= 1;
        sharedWidth = availableWidth / fieldCountWithoutWidth;
      }
    }
  }

  // divide up the rest of the space
  for (const column of columns) {
    if (!column.width) {
      column.width = sharedWidth;
    }
    column.minWidth = COLUMN.MIN_WIDTH;
  }

  return columns;
}

export function myRowRenderer(
  key: React.Key,
  props: RenderRowProps<TableRow, TableSummaryRow>,
  expandedRows: number[],
  panelContext: PanelContext,
  data: DataFrame,
  enableSharedCrosshair: boolean
): React.ReactNode {
  // Let's render row level things here!
  // i.e. we can look at row styles and such here
  const { row } = props;
  const rowIdx = Number(row.__index);
  const isExpanded = expandedRows.includes(rowIdx);

  // Don't render non expanded child rows
  if (Number(row.__depth) === 1 && !isExpanded) {
    return null;
  }

  // Add aria-expanded to parent rows that have nested data
  if (row.data) {
    return <Row key={key} {...props} aria-expanded={isExpanded} />;
  }

  return (
    <Row
      key={key}
      {...props}
      onMouseEnter={() => onRowHover(rowIdx, panelContext, data, enableSharedCrosshair)}
      onMouseLeave={() => onRowLeave(panelContext, enableSharedCrosshair)}
    />
  );
}

export function onRowHover(idx: number, panelContext: PanelContext, frame: DataFrame, enableSharedCrosshair: boolean) {
  if (!enableSharedCrosshair) {
    return;
  }

  const timeField: Field = frame!.fields.find((f) => f.type === FieldType.time)!;

  if (!timeField) {
    return;
  }

  panelContext.eventBus.publish(
    new DataHoverEvent({
      point: {
        time: timeField.values[idx],
      },
    })
  );
}

export function onRowLeave(panelContext: PanelContext, enableSharedCrosshair: boolean) {
  if (!enableSharedCrosshair) {
    return;
  }

  panelContext.eventBus.publish(new DataHoverClearEvent());
}

const getStyles = (theme: GrafanaTheme2) => ({
  dataGrid: css({
    '--rdg-background-color': theme.colors.background.primary,
    '--rdg-header-background-color': theme.colors.background.primary,
    '--rdg-border-color': 'transparent',
    '--rdg-color': theme.colors.text.primary,
    '&:hover': {
      '--rdg-row-hover-background-color': theme.colors.emphasize(theme.colors.action.hover, 0.6),
    },

    // If we rely solely on borderInlineEnd which is added from data grid, we
    // get a small gap where the gridCell borders meet the column header borders.
    // To avoid this, we can unset borderInlineEnd and set borderRight instead.
    '.rdg-cell': {
      borderInlineEnd: 'unset',
      borderRight: `1px solid ${theme.colors.border.medium}`,

      '&:last-child': {
        borderRight: 'none',
      },
    },

    '.rdg-summary-row': {
      backgroundColor: theme.colors.background.primary,
      '--rdg-summary-border-color': theme.colors.border.medium,

      '.rdg-cell': {
        // Prevent collisions with custom cell components
        zIndex: 2,
        borderRight: 'none',
      },
    },

    // Due to stylistic choices, we do not want borders on the column headers
    // other than the bottom border.
    'div[role=columnheader]': {
      borderBottom: `1px solid ${theme.colors.border.medium}`,
      borderInlineEnd: 'unset',

      '.r1y6ywlx7-0-0-beta-46': {
        '&:hover': {
          borderRight: `3px solid ${theme.colors.text.link}`,
        },
      },
    },

    '::-webkit-scrollbar': {
      width: TABLE.SCROLL_BAR_WIDTH,
      height: TABLE.SCROLL_BAR_WIDTH,
    },
    '::-webkit-scrollbar-thumb': {
      backgroundColor: 'rgba(204, 204, 220, 0.16)',
      borderRadius: '4px',
    },
    '::-webkit-scrollbar-track': {
      background: 'transparent',
    },
    '::-webkit-scrollbar-corner': {
      backgroundColor: 'transparent',
    },
  }),
  menuItem: css({
    maxWidth: '200px',
  }),
  cell: css({
    '--rdg-border-color': theme.colors.border.medium,
    borderLeft: 'none',
    whiteSpace: 'nowrap',
    wordWrap: 'break-word',
    overflow: 'hidden',
    textOverflow: 'ellipsis',

    // Reset default cell styles for custom cell component styling
    paddingInline: '0',
  }),
  cellWrapped: css({
    '--rdg-border-color': theme.colors.border.medium,
    borderLeft: 'none',
    whiteSpace: 'pre-line',
    wordWrap: 'break-word',
    overflow: 'hidden',
    textOverflow: 'ellipsis',

    // Reset default cell styles for custom cell component styling
    paddingInline: '0',
  }),
  paginationContainer: css({
    alignItems: 'center',
    display: 'flex',
    justifyContent: 'center',
    marginTop: '8px',
    width: '100%',
  }),
  paginationSummary: css({
    color: theme.colors.text.secondary,
    fontSize: theme.typography.bodySmall.fontSize,
    display: 'flex',
    justifyContent: 'flex-end',
    padding: theme.spacing(0, 1, 0, 2),
  }),
});
