/**
 * TEAM: frontend_infra
 *
 * @flow
 */
import * as React from "react";
import {FixedSizeList} from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import {StyleSheet, css} from "aphrodite";
import AutoSizer from "react-virtualized-auto-sizer";
import PopupWithClickAway from "../popup/PopupWithClickAway";
import IconButton from "../button/IconButton";
import Tooltip from "../Tooltip";
import Checkbox from "../Checkbox";
import Group from "../Group";
import TextInput from "../TextInput";
import CheckboxList from "../CheckboxList";
import invariant from "../tools/invariant";
import Text from "../Text";
import Icon from "../Icon";
import Loader from "../Loader";
import colors from "../colors";
import {whitespaceSizeConstants} from "../styles/whitespace";
import Clickable from "../base_candidate/Clickable";
import RowContext from "./RowContext";
import typeof StaticLoadingRow from "./StaticLoadingRow";

export type Style = {[string]: string | number, ...};

const TableContext: React.Context<TableContextType<any>> = React.createContext({
  headers: [],
  columns: [],
  rowMinWidth: 0,
  largeHeader: false,
  hasSubColumns: false,
});
type TableContextType<T> = {|
  +rowMinWidth: number,
  +headers: $ReadOnlyArray<HeaderColumn>,
  +columns: $ReadOnlyArray<Column<T>>,
  +largeHeader: boolean,
  +hasSubColumns: boolean,
|};

type WidthSpecification =
  | number
  | {
      weight: number,
      min?: number,
      ...
    };

export type ParentColumnDefinition<T> = {|
  +id: string,
  +header: string,
  +subColumns: $ReadOnlyArray<ColumnDefinition<T>>,
  +width?: WidthSpecification,
  +headerAlignment?: "left" | "center" | "right",
  +tooltipText?: string,
|};

export type ColumnDefinition<T> = {|
  +id: string,
  +header: string,
  +render: T => React.Node,
  +renderAggregate?: ($ReadOnlyArray<T>) => React.Node,
  +renderExpanded?: T => React.Node,
  +width: WidthSpecification,
  +comparator?: (T, T, "asc" | "desc") => number,
  +aggregateComparator?: ($ReadOnlyArray<T>, $ReadOnlyArray<T>) => number,
  +headerAlignment?: "left" | "center" | "right",
  +tooltipText?: string,
  +allowContentOverflow?: boolean,
|};

export type ColumnDefinitionV2<T> =
  | ParentColumnDefinition<T>
  | ColumnDefinition<T>;

export type Sort = {|
  +columnId: string,
  +direction: "asc" | "desc",
|};

type Pin = {|
  +columnId: string,
  +align: "left" | "right",
|};

export type Hide = {|
  +columnId: string,
|};

type HeaderColumn = {
  id: string,
  render: () => React.Node,
  pinned: "left" | "right" | null,
  ...
};

type Column<T> = {
  id: string,
  Cell: React.ComponentType<{|
    +row: T,
  |}>,
  AggregateCell: React.ComponentType<{|
    +rows: $ReadOnlyArray<T>,
  |}>,
  ExpandedCell: React.ComponentType<{|
    +row: T,
  |}>,
  pinned: "left" | "right" | null,
  width: WidthSpecification,
  ...
};

type FlattenedRow<T> =
  | {
      id: string,
      type: "aggregate",
      rows: $ReadOnlyArray<T>,
      ...
    }
  | {
      id: string,
      type: "default" | "expanded",
      row: T,
      ...
    };

export type RenderColumnCustomization<T> = (
  ColumnCustomizationProps<T>
) => React.Node;

export const HEADER_HEIGHT = 32;
export const LARGE_HEADER_HEIGHT = 48;
export const DEFAULT_ROW_HEIGHT = 44;
export const COLUMN_PADDING = 20;
export const COLUMN_PADDING_COMPACT = 10;
export const FIRST_COLUMN_PADDING = 12;
export const PINNED_COLUMN_BORDER_WIDTH = 2;
export const ROW_SELECTION_COLUMN_WIDTH = 48;
export const MIN_SCROLLABLE_COLUMN_COUNT = 12;
const ROW_EXPANSION_WIDTH = 8; // width for row expansion caret
const MIN_TABLE_WIDTH = 320;
const PARENT_COLUMN_MARGIN = 4; // Vertical gap between parent column and sub-columns

const noop = () => {};
function defaultRenderColumnCustomization<T>(
  props: ColumnCustomizationProps<T>
) {
  return <ColumnCustomization {...props} />;
}

function handleCellClick(e: SyntheticEvent<HTMLDivElement>) {
  e.stopPropagation();
}

// IE11 Sticky scrolling implementation
const StickyScrollContext = React.createContext();

function createStickyScrollPolyfill() {
  const isPositionStickySupported = (() => {
    const testStyle = document.createElement("a").style;

    testStyle.cssText =
      "position:sticky;position:-webkit-sticky;position:-ms-sticky;";

    return testStyle.position.includes("sticky");
  })();
  const context = {
    addRef,
    pinElements: {
      top: [],
      left: [],
      right: [],
    },
    scrollLeft: 0,
    rightOffset: 0,
    scrollTop: 0,
    isPositionStickySupported,
  };

  // This position updater applies for recreating sticky behavior on IE11
  function processScrollPosition(targetElement: HTMLElement) {
    context.scrollLeft = targetElement.scrollLeft;

    context.pinElements.left.forEach((element: HTMLElement) => {
      // eslint-disable-next-line no-param-reassign
      element.style.transform = `translateX(${context.scrollLeft}px)`;
    });

    context.rightOffset =
      targetElement.offsetWidth -
      targetElement.scrollWidth +
      targetElement.scrollLeft;

    context.pinElements.right.forEach((element: HTMLElement) => {
      // eslint-disable-next-line no-param-reassign
      element.style.transform = `translateX(${context.rightOffset}px)`;
    });

    context.scrollTop = targetElement.scrollTop;

    context.pinElements.top.forEach((element: HTMLElement) => {
      // eslint-disable-next-line no-param-reassign
      element.style.transform = `translateY(${context.scrollTop}px)`;
    });
  }

  function addRef(type: "top" | "left" | "right") {
    if (context.isPositionStickySupported) return;

    return (element: ?HTMLElement) => {
      if (!element) return;

      if (!context.pinElements[type].includes(element))
        context.pinElements[type].push(element);
    };
  }

  return {context, processScrollPosition, addRef, isPositionStickySupported};
}

/**
 * @short Displays tabular data
 * @brandStatus V2
 * @status Beta
 * @category Data Display
 *
 * **DISCLAIMER: The table component is still under active development**
 *
 * `<Table />` efficiently displays large amounts of data, and supports column pinning,
 * row selection, sorting, and scrolling. Expandable rows, cell selection, drag-and-drop
 * column reordering, and other features are coming soon.
 *
 * **Note: Table takes the full height of its container so the container needs to have an
 * explicit height set**
 */
export default function DeprecatedTable<T>({
  data,
  columnDefinitions,
  getUniqueRowId,
  rowSelectionEnabled = false,
  rowSelectionPinned = true,
  onSelectedRowsChange = noop,
  selectedRows = new Set(),
  disabledRows = new Set(),
  hideSelectAllCheckbox = false,
  hideExpandedCheckboxes = false,
  rowHeight = DEFAULT_ROW_HEIGHT,
  pinnedColumns = null,
  columnCustomizationEnabled = false,
  renderColumnCustomization = defaultRenderColumnCustomization,
  hiddenColumns = null,
  onHiddenColumnsChange = noop,
  sortBy,
  onSortByChange = noop,
  rowAggregationEnabled = false,
  rowAggregationPinned = true,
  getRowGroupId,
  expandSingleAggregateRow = false,
  expandedRows = new Set(),
  onExpandedRowsChange = noop,
  hasNextPage = false,
  isNextPageLoading = false,
  loadNextPage = noop,
  rowClickingEnabled = false,
  clickedRow,
  onRowClick = noop,
  rowGroupClickingEnabled = false,
  clickedRowGroup,
  onRowGroupClick = noop,
  toolBar = null,
  isLoading = false,
  useFullWidth = false,
  staticLoadingRow = null,
  compactCells = false,
  largeHeader = false,
  dataQaId,
  dataQaIdCheckboxLabel,
}: {
  /** An array of data for each row in the table */
  +data: $ReadOnlyArray<T>,
  /** Defines how each column of data is rendered. Each column definition needs an id, a header, and a render function. Either width or subColumns must be specified, depending on whether sub-columns are used. */
  +columnDefinitions: $ReadOnlyArray<ColumnDefinitionV2<T>>,
  /** A function to uniquely identify each row, this ID is what is returned in the row selections set */
  +getUniqueRowId: T => string,
  /** Whether or not checkboxes for row selection will be shown */
  +rowSelectionEnabled?: boolean,
  /** Whether or not the checkboxes for row selection will be pinned to the left of the table */
  +rowSelectionPinned?: boolean,
  /** Callback for when the user changes the selected rows */
  +onSelectedRowsChange?: ($ReadOnlySet<string>) => void,
  /** Which rows are selected, based on IDs returned from getUniqueRowId */
  +selectedRows?: $ReadOnlySet<string>,
  /** Which rows cannot be selected by the checkbox, based on IDs returned from getUniqueRowId */
  +disabledRows?: $ReadOnlySet<string>,
  /** Hides the select all checkbox */
  +hideSelectAllCheckbox?: boolean,
  /** Hides the expanded checkboxes */
  +hideExpandedCheckboxes?: boolean,
  /** Controls the height of each row in the table */
  +rowHeight?: number,
  /** Which columns are pinned to either the left or right side of the table (other columns will scroll underneath) */
  +pinnedColumns?: ?$ReadOnlyArray<Pin>,
  /** Callback for when the user changes pinned columns */
  +onPinnedColumnsChange?: ($ReadOnlyArray<Pin>) => void,
  /** Whether or not to show the + for changing column visibility */
  +columnCustomizationEnabled?: boolean,
  /** Render function to override the default column customization component. Use as your own risk. */
  +renderColumnCustomization?: RenderColumnCustomization<T>,
  /** Which columns are hidden */
  +hiddenColumns?: ?$ReadOnlyArray<Hide>,
  /** Callback for when the user changes which columns are hidden */
  +onHiddenColumnsChange?: ($ReadOnlyArray<Hide>) => void,
  /** Which column to sort by, and what direction to sort */
  +sortBy?: ?Sort,
  /** Callback for when the user changes which column and direction to sort */
  +onSortByChange?: (?Sort) => void,
  /** Whether or not rows will be grouped and an aggregation row is present */
  +rowAggregationEnabled?: boolean,
  /** Whether or not the arrows for row aggregation will be pinned to the left of the table */
  +rowAggregationPinned?: boolean,
  /** A function to determine which row group the row belongs to, this ID is what is returned in the row expansions set  */
  +getRowGroupId?: T => string,
  /** Automatically or based on a function to dertermine if expand single aggregate row with no children  */
  +expandSingleAggregateRow?: boolean | (T => boolean),
  /** Which row groups are expanded, based on IDs returned from getRowGroupId */
  +expandedRows?: $ReadOnlySet<string>,
  /** Callback for when the user expands a row group */
  +onExpandedRowsChange?: ($ReadOnlySet<string>) => void,
  /** Whether additional data can be loaded from the server. Used for scrolling pagination */
  +hasNextPage?: boolean,
  /** Whether additional data is currently being loaded. Used for scrolling pagination */
  +isNextPageLoading?: boolean,
  /** Callback to load additional data when a user nears the end of the existing list of data. Used for scrolling pagination */
  +loadNextPage?: () => mixed,
  +rowClickingEnabled?: boolean,
  +clickedRow?: ?string,
  +onRowClick?: string => void,
  +rowGroupClickingEnabled?: boolean,
  +clickedRowGroup?: ?string,
  +onRowGroupClick?: string => void,
  /** A react node to place above the table that will be sized based off the table.  */
  +toolBar?: React.Node,
  /** Initial results are loading */
  +isLoading?: boolean,
  /** Table size expands to the full width of the container */
  +useFullWidth?: boolean,
  /** Enables static loading row component */
  +staticLoadingRow?: ?React.Element<StaticLoadingRow>,
  /** Enables compact spacing between cells */
  +compactCells?: boolean,
  /** Enables a header with larger spacing */
  +largeHeader?: boolean,
  +dataQaId?: string,
  /** $Hide The label for the data-qa-id for accessing checkbox elements */
  +dataQaIdCheckboxLabel?: string,
  ...
}): React.Node {
  const visibleParentColumnDefs = React.useMemo(
    () =>
      columnDefinitions.filter(
        cd => !hiddenColumns?.find(c => c.columnId === cd.id)
      ),
    [columnDefinitions, hiddenColumns]
  );
  const visibleColumnDefinitions = React.useMemo(
    () =>
      visibleParentColumnDefs.flatMap(cd =>
        cd.subColumns ? cd.subColumns : [cd]
      ),
    [visibleParentColumnDefs]
  );
  const hasSubColumns = columnDefinitions.some(
    // eslint-disable-next-line no-unneeded-ternary
    cd => (cd.subColumns ? true : false)
  );
  const StickyScrollPolyfill = createStickyScrollPolyfill();

  function sortRows(rows: $ReadOnlyArray<T>, sortBy: ?Sort) {
    if (!sortBy) {
      return rows;
    }
    const columnDefinition = columnDefinitions
      .flatMap(cd => (cd.subColumns ? cd.subColumns : [cd]))
      .find(cd => cd.id === sortBy.columnId);
    invariant(
      columnDefinition != null,
      `column definition with id ${sortBy.columnId} does not exist`
    );
    const {comparator} = columnDefinition;
    invariant(
      comparator != null,
      `comparator does not exist for column definition with id ${sortBy.columnId}`
    );
    const multiplier = sortBy.direction === "asc" ? 1 : -1;
    return [...rows].sort(
      (a, b) => multiplier * comparator(a, b, sortBy.direction)
    );
  }

  const sortedRows = sortRows(data, sortBy);
  const disabledGroups = new Set();
  if (rowAggregationEnabled && getRowGroupId) {
    data.forEach(row => {
      if (disabledRows.has(getUniqueRowId(row))) {
        disabledGroups.add(getRowGroupId(row));
      }
    });
  }

  function groupRows(
    rows: $ReadOnlyArray<T>
  ): $ReadOnlyArray<$ReadOnlyArray<T>> {
    invariant(
      getRowGroupId,
      "getRowGroupId must be provided to aggregate rows"
    );
    const rowGroupIdToRows = new Map<string, $ReadOnlyArray<T>>();
    rows.forEach(row => {
      const rowGroupId = getRowGroupId(row);
      const rows = rowGroupIdToRows.get(rowGroupId);
      rowGroupIdToRows.set(rowGroupId, rows ? [...rows, row] : [row]);
    });
    return [...rowGroupIdToRows.values()];
  }

  function sortRowGroups(rowGroups: $ReadOnlyArray<$ReadOnlyArray<T>>) {
    if (!sortBy) {
      return rowGroups;
    }
    const columnDefinition = columnDefinitions
      .flatMap(cd => (cd.subColumns ? cd.subColumns : [cd]))
      .find(cd => cd.id === sortBy.columnId);
    invariant(
      columnDefinition != null,
      `column definition with id ${sortBy.columnId} does not exist`
    );
    const {comparator, aggregateComparator} = columnDefinition;

    const multiplier = sortBy.direction === "asc" ? 1 : -1;
    return [...rowGroups].sort((as, bs) => {
      if (aggregateComparator) {
        return multiplier * aggregateComparator(as, bs);
      }
      invariant(
        comparator != null,
        `comparator must exist if aggregateComparator does not exist for column definition with id ${sortBy.columnId}`
      );
      return multiplier * comparator(as[0], bs[0], sortBy.direction);
    });
  }

  const flattenRow = (row: T): FlattenedRow<T> => ({
    id: getUniqueRowId(row),
    type: "default",
    row,
  });

  const flattenExpandedRow = (row: T): FlattenedRow<T> => ({
    id: getUniqueRowId(row),
    type: "expanded",
    row,
  });

  const flattenRowGroup = (rowGroup: $ReadOnlyArray<T>): FlattenedRow<T> => {
    invariant(getRowGroupId);
    return {
      id: getRowGroupId(rowGroup[0]),
      type: "aggregate",
      rows: rowGroup,
    };
  };

  function flattenRows(
    rows: $ReadOnlyArray<T>
  ): $ReadOnlyArray<FlattenedRow<T>> {
    return rows.map(flattenRow);
  }

  function flattenRowGroups(
    rowGroups: $ReadOnlyArray<$ReadOnlyArray<T>>
  ): $ReadOnlyArray<FlattenedRow<T>> {
    invariant(getRowGroupId);
    return rowGroups.reduce(
      (rows, rowGroup) => [
        ...rows,
        rowGroup.length === 1 &&
        (expandSingleAggregateRow === true ||
          (typeof expandSingleAggregateRow === "function" &&
            expandSingleAggregateRow(rowGroup[0])))
          ? flattenRow(rowGroup[0])
          : flattenRowGroup(rowGroup),
        ...(expandedRows.has(getRowGroupId(rowGroup[0]))
          ? rowGroup.map(flattenExpandedRow)
          : []),
      ],
      []
    );
  }

  function renderHeader(
    columnDef: ColumnDefinitionV2<T>,
    opts: {
      compactCells: boolean,
      dataQaId: ?string,
      ...
    }
  ): React.Node {
    if (columnDef.subColumns) {
      return renderParentHeader(columnDef, opts);
    }

    return renderChildHeader(columnDef, opts);
  }

  function renderParentHeader(
    columnDef: ParentColumnDefinition<T>,
    opts: {
      compactCells: boolean,
      dataQaId: ?string,
      ...
    }
  ): React.Node {
    const children = columnDef.subColumns.map(subColumn =>
      renderChildHeader(subColumn, opts)
    );
    const {headerAlignment, id, header, tooltipText} = columnDef;

    return (
      <Group key={id} flexDirection="column" gap={0} containerFlex="none">
        <Header
          style={{
            ...getWidthStyle(
              totalWidth(columnDef, opts.compactCells),
              opts.compactCells
            ),
            margin: `${PARENT_COLUMN_MARGIN}px 0`,
          }}
          header={header}
          align={headerAlignment || "left"}
          tooltipText={tooltipText}
          compactCells={opts.compactCells}
        />
        <Group flexDirection="row" gap={0}>
          {children}
        </Group>
      </Group>
    );
  }

  function renderChildHeader(
    columnDef: ColumnDefinition<T>,
    opts: {
      compactCells: boolean,
      dataQaId: ?string,
      ...
    }
  ): React.Node {
    const {
      aggregateComparator,
      comparator,
      header,
      headerAlignment,
      id,
      tooltipText,
      width,
    } = columnDef;
    return aggregateComparator || comparator ? (
      <SortableHeader
        key={id}
        style={getWidthStyle(width, opts.compactCells)}
        onSortDirectionChange={direction =>
          onSortByChange(direction ? {columnId: id, direction} : null)
        }
        sortDirection={
          sortBy && sortBy.columnId === id ? sortBy.direction : null
        }
        header={header}
        align={headerAlignment || "left"}
        tooltipText={tooltipText}
        compactCells={opts.compactCells}
        dataQaId={opts.dataQaId}
      />
    ) : (
      <Header
        key={id}
        style={getWidthStyle(width, compactCells)}
        header={header}
        align={headerAlignment || "left"}
        tooltipText={tooltipText}
        compactCells={compactCells}
      />
    );
  }

  function totalWidth(
    columnDef: ParentColumnDefinition<T>,
    compactCells: boolean
  ): WidthSpecification {
    if (columnDef.width !== undefined) {
      return columnDef.width;
    }

    invariant(
      columnDef.subColumns.every(
        subColumn => typeof subColumn.width === "number"
      ),
      "Parent column width must be specified if any of the children have flexible width"
    );

    const subColumnWidth = columnDef.subColumns.reduce(
      (total, subColumn) =>
        total + (typeof subColumn.width === "number" ? subColumn.width : 0),
      0
    );

    // The parent header itself will have padding, so we only need to add padding for (n-1) sub-columns.
    return (
      subColumnWidth +
      (columnDef.subColumns.length - 1) * getPadding(compactCells)
    );
  }

  const flattenedRows = rowAggregationEnabled
    ? flattenRowGroups(sortRowGroups(groupRows(sortedRows)))
    : flattenRows(sortedRows);

  const NonaggregatedSelectionCell = ({row, expandedCell}) => (
    <div
      className={css(styles.rowSelectionCell)}
      onClick={handleCellClick}
      role="presentation"
    >
      {hideExpandedCheckboxes && expandedCell ? null : (
        <Checkbox
          checked={selectedRows.has(getUniqueRowId(row))}
          disabled={isSingleRowDisabled(row)}
          onChange={isSelected => {
            const newSelection = new Set(selectedRows);
            if (isSelected) {
              newSelection.add(getUniqueRowId(row));
            } else {
              newSelection.delete(getUniqueRowId(row));
            }
            onSelectedRowsChange(newSelection);
          }}
          dataQaId={dataQaIdCheckboxLabel != null ? dataQaIdCheckboxLabel : ""}
        />
      )}
    </div>
  );
  const SelectionCell = ({row}) => (
    <NonaggregatedSelectionCell row={row} expandedCell={false} />
  );

  const ExpandedSelectionCell = ({row}) => (
    <NonaggregatedSelectionCell row={row} expandedCell={true} />
  );

  const rowSelectionColumn = {
    id: "row_selection",
    pinned:
      rowSelectionPinned ||
      pinnedColumns?.find(({align}) => align === "left") != null
        ? "left"
        : null,
    Cell: SelectionCell,
    ExpandedCell: ExpandedSelectionCell,
    AggregateCell: ({rows}) => (
      <div
        className={css(styles.rowSelectionCell)}
        onClick={handleCellClick}
        role="presentation"
      >
        <Checkbox
          checked={rows.every(row => selectedRows.has(getUniqueRowId(row)))}
          disabled={isAggregateRowDisabled(rows)}
          onChange={isSelected => {
            const newSelection = new Set(selectedRows);
            if (isSelected) {
              rows.forEach(row => newSelection.add(getUniqueRowId(row)));
            } else {
              rows.forEach(row => newSelection.delete(getUniqueRowId(row)));
            }
            onSelectedRowsChange(newSelection);
          }}
        />
      </div>
    ),
    width: 52,
  };
  const rowSelectionHeader = {
    id: rowSelectionColumn.id,
    pinned: rowSelectionColumn.pinned,
    render: () => (
      <div
        id="row_selection"
        key={rowSelectionColumn.id}
        className={css(styles.rowSelectionCell)}
      >
        {hideSelectAllCheckbox || disabledRows.size > 0 ? null : (
          <Checkbox
            checked={selectedRows.size === data.length}
            indeterminate={selectedRows.size > 0}
            onChange={isSelected => {
              if (isSelected) {
                onSelectedRowsChange(new Set(data.map(getUniqueRowId)));
              } else {
                onSelectedRowsChange(new Set());
              }
            }}
          />
        )}
      </div>
    ),
  };

  const rowExpansionWidth = ROW_EXPANSION_WIDTH + getPadding(compactCells);
  const rowExpansionColumn = {
    id: "row_expansion",
    pinned:
      rowAggregationPinned ||
      pinnedColumns?.find(({align}) => align === "left") != null
        ? "left"
        : null,
    Cell: () => <BlankExpansionCell width={rowExpansionWidth} />,
    ExpandedCell: () => <BlankExpansionCell width={rowExpansionWidth} />,
    AggregateCell: ({rows}) => {
      invariant(getRowGroupId);
      return (
        <div
          className={css(styles.rowExpansionCell)}
          style={{width: rowExpansionWidth}}
          onClick={handleCellClick}
          role="presentation"
        >
          <IconButton
            size="s"
            kind="blank"
            type="button"
            iconName={
              expandedRows.has(getRowGroupId(rows[0]))
                ? "downOpen"
                : "rightOpen"
            }
            onClick={() => {
              const rowGroupId = getRowGroupId(rows[0]);
              const newExpandedRows = new Set(expandedRows);
              if (expandedRows.has(rowGroupId)) {
                newExpandedRows.delete(rowGroupId);
              } else {
                newExpandedRows.add(rowGroupId);
              }
              onExpandedRowsChange(newExpandedRows);
            }}
          />
        </div>
      );
    },
    width: 52,
  };
  const rowExpansionHeader = {
    id: rowExpansionColumn.id,
    pinned: rowExpansionColumn.pinned,
    render: () => (
      <BlankExpansionCell
        key={rowExpansionColumn.id}
        id="row_expansion"
        width={rowExpansionWidth}
      />
    ),
  };

  const pinnedChildColumns = React.useMemo(
    () =>
      (pinnedColumns || []).flatMap(pin => {
        const parent = visibleParentColumnDefs.find(
          ({id}) => pin.columnId === id
        );
        if (!parent) {
          return [];
        } else if (!parent.subColumns) {
          return [pin];
        }

        return parent.subColumns.map(({id}) => ({...pin, columnId: id}));
      }),
    [pinnedColumns, visibleParentColumnDefs]
  );

  // Parent columns may sometimes have a width that is larger than the total width of their
  // sub-columns, which means we have to add spacers in between groups of sub-columns.
  const [spacers, subColumnSpacersWidth] = React.useMemo(() => {
    const spacers = {};
    let subColumnSpacersWidth = 0;

    visibleParentColumnDefs.forEach(columnDef => {
      if (
        !columnDef.subColumns ||
        columnDef.subColumns.length === 0 ||
        columnDef.width == null
      ) {
        return;
      }

      if (typeof columnDef.width !== "number" && columnDef.width.min == null) {
        return;
      }

      const parentWidth =
        typeof columnDef.width === "number"
          ? columnDef.width
          : columnDef.width.min || 0;
      const subColumnWidth =
        columnDef.subColumns.reduce(
          (total, subColumn) =>
            total + (typeof subColumn.width === "number" ? subColumn.width : 0),
          0
        ) +
        (columnDef.subColumns.length - 1) * getPadding(compactCells);

      const lastSubColumnId =
        columnDef.subColumns[columnDef.subColumns.length - 1].id;
      const extra = parentWidth - subColumnWidth;
      if (extra <= 0) {
        return;
      }

      spacers[lastSubColumnId] = <BlankExpansionCell width={extra} />;
      subColumnSpacersWidth += extra;
    });
    return [spacers, subColumnSpacersWidth];
  }, [compactCells, visibleParentColumnDefs]);

  const headers = [
    ...(rowAggregationEnabled ? [rowExpansionHeader] : []),
    ...(rowSelectionEnabled ? [rowSelectionHeader] : []),
    ...visibleParentColumnDefs.map(columnDef => ({
      id: columnDef.id,
      render: () => renderHeader(columnDef, {compactCells, dataQaId}),
      pinned:
        pinnedColumns?.find(({columnId}) => columnId === columnDef.id)?.align ||
        null,
    })),
  ];

  const visibleColumnDefinitionsMemo = React.useMemo(
    () =>
      visibleColumnDefinitions.map(columnDef => {
        const {
          id,
          render,
          renderAggregate,
          renderExpanded,
          width,
          allowContentOverflow,
        } = columnDef;
        const pin = pinnedChildColumns.find(({columnId}) => columnId === id);
        invariant(
          !pin || typeof width === "number",
          "Pinned column can not be flexible"
        );

        const cellStyles = [styles.cell, compactCells && styles.cellCompact];
        if (allowContentOverflow === true) {
          cellStyles.push(styles.cellAllowContentOverflow);
        }

        const CellDiv = ({children}) => (
          <>
            <div
              key={id}
              className={css(...cellStyles)}
              style={getWidthStyle(width, compactCells)}
            >
              {children}
            </div>
            {spacers[id]}
          </>
        );

        return {
          id,
          Cell: ({row}) => <CellDiv>{render(row)}</CellDiv>,
          AggregateCell: ({rows}) => (
            <CellDiv>
              {renderAggregate ? renderAggregate(rows) : render(rows[0])}
            </CellDiv>
          ),
          ExpandedCell: ({row}) => (
            <CellDiv>
              {renderExpanded ? renderExpanded(row) : render(row)}
            </CellDiv>
          ),
          pinned: pin ? pin.align : null,
          width: columnDef.width,
        };
      }),
    [compactCells, pinnedChildColumns, spacers, visibleColumnDefinitions]
  );

  const columns = [
    ...(rowAggregationEnabled ? [rowExpansionColumn] : []),
    ...(rowSelectionEnabled ? [rowSelectionColumn] : []),
    ...visibleColumnDefinitionsMemo,
  ];

  const rowMinWidth = getRowMinWidth(
    columns,
    subColumnSpacersWidth,
    compactCells
  );
  const outerElementType = React.forwardRef((props, ref) => (
    <div
      ref={element => {
        if (element) StickyScrollPolyfill.processScrollPosition(element);

        if (typeof ref === "function") return ref(element);

        return ref;
      }}
      {...props}
      onScroll={event => {
        onScroll(event);
        props.onScroll(event);
      }}
    />
  ));

  function isRowExpanded(flattenedRow: ?FlattenedRow<T>) {
    if (!flattenedRow || !getRowGroupId) {
      return false;
    }
    if (flattenedRow.type === "aggregate") {
      return expandedRows.has(flattenedRow.id);
    }
    return expandedRows.has(getRowGroupId(flattenedRow.row));
  }

  function isRowSelected(flattenedRow: ?FlattenedRow<T>) {
    if (!flattenedRow) {
      return false;
    }
    if (flattenedRow.type === "aggregate") {
      return flattenedRow.rows.every(row =>
        selectedRows.has(getUniqueRowId(row))
      );
    }
    return selectedRows.has(getUniqueRowId(flattenedRow.row));
  }

  function isRowClicked(flattenedRow: ?FlattenedRow<T>) {
    if (!flattenedRow) {
      return false;
    }
    if (flattenedRow.type === "aggregate") {
      return clickedRowGroup === flattenedRow.id;
    }
    return clickedRow === flattenedRow.id;
  }

  function isSingleRowDisabled(row: T) {
    if (rowAggregationEnabled && getRowGroupId) {
      return disabledGroups.has(getRowGroupId(row));
    }
    return disabledRows.has(getUniqueRowId(row));
  }

  function isAggregateRowDisabled(rows: $ReadOnlyArray<T>) {
    if (rows.length > 0 && getRowGroupId) {
      return disabledGroups.has(getRowGroupId(rows[0]));
    }
    return false;
  }

  function isRowDisabled(flattenedRow: ?FlattenedRow<T>) {
    if (!flattenedRow) {
      return false;
    }

    if (flattenedRow.type === "aggregate") {
      return isAggregateRowDisabled(flattenedRow.rows);
    }
    return isSingleRowDisabled(flattenedRow.row);
  }

  // This handler applies for recreating sticky behavior on IE11
  function onScroll(event: SyntheticEvent<HTMLDivElement>) {
    const {currentTarget} = event;
    requestAnimationFrame(() => {
      StickyScrollPolyfill.processScrollPosition(currentTarget);
    });
  }

  /* Scrolling Pagination */
  // If more results are available to fetch, or we are still loading initial results,
  // add an extra row to hold a loading indicator
  const rowCount = hasNextPage
    ? flattenedRows.length + 1
    : flattenedRows.length;
  // If we are already fetching more rows, do not try to re-fetch
  // If we are using static loading row, do not try to re-fetch
  const loadMoreItems =
    staticLoadingRow || isNextPageLoading ? noop : loadNextPage;

  // Every row is considered loaded except for the loading indicator
  const isItemLoaded = (index: number) =>
    !hasNextPage || index < flattenedRows.length;

  const itemData = {
    isItemLoaded,
    flattenedRows,
    isRowExpanded,
    isRowSelected,
    isRowClicked,
    isRowDisabled,
    rowGroupClickingEnabled,
    onRowGroupClick,
    rowClickingEnabled,
    onRowClick,
    staticLoadingRow,
    largeHeader,
    hasSubColumns,
  };

  const pinnedColumnIds = (pinnedColumns || []).map(({columnId}) => columnId);
  const customizableColumns = columnDefinitions.filter(
    ({id}) => !pinnedColumnIds.includes(id)
  );

  return (
    <TableContext.Provider
      value={{columns, headers, rowMinWidth, largeHeader, hasSubColumns}}
    >
      <div
        className={css(styles.tableContainer)}
        style={{maxWidth: useFullWidth ? null : rowMinWidth}}
      >
        {toolBar}
        <StickyScrollContext.Provider value={StickyScrollPolyfill.context}>
          <AutoSizer>
            {({width, height}: {|+width: number, +height: number|}) => {
              const tableWidth = useFullWidth
                ? width
                : Math.max(MIN_TABLE_WIDTH, Math.min(width, rowMinWidth));
              return (
                <div style={{width: tableWidth}}>
                  <InfiniteLoader
                    isItemLoaded={isItemLoaded}
                    itemCount={rowCount}
                    loadMoreItems={loadMoreItems}
                  >
                    {({onItemsRendered, ref}) =>
                      isLoading ? (
                        <InitialLoading
                          height={height}
                          width={tableWidth}
                          rowHeight={rowHeight}
                          outerElementType={
                            !StickyScrollPolyfill.isPositionStickySupported &&
                            outerElementType
                          }
                          ref={ref}
                        />
                      ) : (
                        <FixedSizeList
                          itemData={itemData}
                          height={height}
                          itemSize={rowHeight}
                          itemCount={rowCount}
                          width={tableWidth}
                          innerElementType={innerElementType}
                          outerElementType={
                            !StickyScrollPolyfill.isPositionStickySupported &&
                            outerElementType
                          }
                          onItemsRendered={onItemsRendered}
                          ref={ref}
                          // Overscan by roughly one page
                          overscanCount={Math.ceil(height / rowHeight)}
                        >
                          {ItemRenderer}
                        </FixedSizeList>
                      )
                    }
                  </InfiniteLoader>
                  {columnCustomizationEnabled
                    ? renderColumnCustomization({
                        largeHeader,
                        columnDefinitions: customizableColumns,
                        visibleColumnIds: visibleParentColumnDefs.map(
                          cd => cd.id
                        ),
                        onVisibleColumnIdsChange: visibleColumnIds => {
                          onHiddenColumnsChange(
                            customizableColumns
                              .filter(cd => !visibleColumnIds.includes(cd.id))
                              .map(cd => ({columnId: cd.id}))
                          );
                        },
                      })
                    : null}
                </div>
              );
            }}
          </AutoSizer>
        </StickyScrollContext.Provider>
      </div>
    </TableContext.Provider>
  );
}

const getPadding = (compactCells: boolean) =>
  2 * (compactCells ? COLUMN_PADDING_COMPACT : COLUMN_PADDING);

const getRowMinWidth = <T>(
  columns: $ReadOnlyArray<Column<T>>,
  subColumnSpacersWidth: number,
  compactCells: boolean
) => {
  const initValue =
    FIRST_COLUMN_PADDING +
    (columns.find(({pinned}) => pinned === "left")
      ? FIRST_COLUMN_PADDING + PINNED_COLUMN_BORDER_WIDTH
      : 0) +
    (columns.find(({pinned}) => pinned === "right")
      ? FIRST_COLUMN_PADDING + PINNED_COLUMN_BORDER_WIDTH
      : 0);

  return columns.reduce((sum, cell) => {
    const padding = getPadding(compactCells);
    const minWidth =
      typeof cell.width === "number"
        ? cell.width + padding
        : (cell.width.min || 0) + padding;
    return sum + minWidth;
  }, initValue + subColumnSpacersWidth);
};

const getWidthStyle = (width: WidthSpecification, compactCells: boolean) => {
  const padding = getPadding(compactCells);
  return typeof width === "number"
    ? {
        width: width + padding,
      }
    : {
        flex: `${width.weight} 0 ${padding}px`,
        minWidth: (width.min || 0) + padding,
      };
};

const getHeaderHeight = (largeHeader, hasSubColumns) =>
  largeHeader || hasSubColumns ? LARGE_HEADER_HEIGHT : HEADER_HEIGHT;

const InitialLoading = React.forwardRef(
  ({height, rowHeight, width, outerElementType}, ref) => (
    <FixedSizeList
      height={height}
      itemSize={rowHeight}
      itemCount={1}
      width={width}
      innerElementType={innerElementType}
      outerElementType={outerElementType}
      ref={ref}
      style={{overflow: "hidden"}}
    >
      {LoadingRow}
    </FixedSizeList>
  )
);

const LoadingRow = ({style}) => (
  <div
    className={css(styles.loadingIndicator)}
    style={{
      ...style,
      top: style.top + HEADER_HEIGHT,
    }}
  >
    <Loader loaded={false} />
  </div>
);

const innerElementType = React.forwardRef(({children, ...rest}, ref) => (
  <TableContext.Consumer>
    {({headers, rowMinWidth, largeHeader, hasSubColumns}) => (
      <div ref={ref} {...rest}>
        <HeaderRow
          headers={headers}
          style={{
            minWidth: rowMinWidth,
            height: getHeaderHeight(largeHeader, hasSubColumns),
          }}
        />
        {children}
      </div>
    )}
  </TableContext.Consumer>
));

type ItemRendererData<T> = {|
  isItemLoaded: number => boolean,
  flattenedRows: $ReadOnlyArray<FlattenedRow<T>>,
  isRowExpanded: (FlattenedRow<T>) => boolean,
  isRowSelected: (FlattenedRow<T>) => boolean,
  isRowClicked: (FlattenedRow<T>) => boolean,
  isRowDisabled: (FlattenedRow<T>) => boolean,
  rowGroupClickingEnabled: boolean,
  onRowGroupClick: string => void,
  rowClickingEnabled: boolean,
  onRowClick: string => void,
  staticLoadingRow: React.Element<StaticLoadingRow>,
  largeHeader: boolean,
  hasSubColumns: boolean,
|};

const ItemRenderer = ({
  index,
  data,
  style,
}: {|
  +index: number,
  +data: ItemRendererData<any>,
  +style: Style,
|}) => {
  const [highlightedRowIndex, setHighlightedRowIndex] = React.useState(null);
  const {
    isItemLoaded,
    flattenedRows,
    isRowExpanded,
    isRowSelected,
    isRowClicked,
    isRowDisabled,
    rowGroupClickingEnabled,
    onRowGroupClick,
    rowClickingEnabled,
    onRowClick,
    staticLoadingRow,
    largeHeader,
    hasSubColumns,
  } = data;

  if (!isItemLoaded(index)) {
    return (
      <div
        className={css(styles.row, styles.infiniteLoader)}
        style={{
          ...style,
          top: style.top + getHeaderHeight(largeHeader, hasSubColumns),
        }}
      >
        {staticLoadingRow || <Loader loaded={false} />}
      </div>
    );
  }

  const flattenedRow = flattenedRows[index];
  const nextFlattenedRow = flattenedRows[index + 1];
  const isExpanded = isRowExpanded(flattenedRow);
  const isSelected = isRowSelected(flattenedRow);
  const isNextSelected = isRowSelected(nextFlattenedRow);
  const isClicked = isRowClicked(flattenedRow);
  const isNextClicked = isRowClicked(nextFlattenedRow);
  const isDisabled = isRowDisabled(flattenedRow);

  const isHighlighted = index === highlightedRowIndex;

  const NonaggregatedRow = flattenedRow.type === "default" ? Row : ExpandedRow;
  return (
    <TableContext.Consumer>
      {({columns, rowMinWidth}) => (
        <RowContext.Provider value={{isHighlighted, isSelected, isDisabled}}>
          {flattenedRow.type === "aggregate" ? (
            <AggregateRow
              className={css(
                styles.row,
                isHighlighted && styles.isHighlighted,
                isExpanded && styles.isExpandedAggregateRow,
                (isSelected || isClicked) && styles.isSelected,
                (isNextSelected || isNextClicked) && styles.isNextSelected,
                isDisabled && styles.disabledRow
              )}
              style={{
                ...style,
                top: style.top + getHeaderHeight(largeHeader, hasSubColumns),
                minWidth: rowMinWidth,
              }}
              columns={columns}
              data={flattenedRow.rows}
              onHighlightedChange={isHighlighted =>
                setHighlightedRowIndex(isHighlighted ? index : null)
              }
              onClick={
                rowGroupClickingEnabled
                  ? () => onRowGroupClick(flattenedRow.id)
                  : null
              }
            />
          ) : (
            <NonaggregatedRow
              className={css(
                styles.row,
                isHighlighted && styles.isHighlighted,
                isExpanded && styles.isExpandedChildRow,
                isSelected && styles.isSelected,
                isNextSelected && styles.isNextSelected,
                (isSelected || isClicked) && styles.isSelected,
                (isNextSelected || isNextClicked) && styles.isNextSelected,
                isDisabled && styles.disabledRow
              )}
              style={{
                ...style,
                top: style.top + getHeaderHeight(largeHeader, hasSubColumns),
                minWidth: rowMinWidth,
              }}
              columns={columns}
              data={flattenedRow.row}
              onHighlightedChange={isHighlighted =>
                setHighlightedRowIndex(isHighlighted ? index : null)
              }
              onClick={
                rowClickingEnabled ? () => onRowClick(flattenedRow.id) : null
              }
            />
          )}
        </RowContext.Provider>
      )}
    </TableContext.Consumer>
  );
};

export type ColumnCustomizationProps<T> = {|
  +columnDefinitions: $ReadOnlyArray<ColumnDefinitionV2<T>>,
  +visibleColumnIds: $ReadOnlyArray<string>,
  +onVisibleColumnIdsChange: ($ReadOnlyArray<string>) => void,
  +largeHeader: boolean,
|};

function ColumnCustomization<T>({
  columnDefinitions,
  visibleColumnIds,
  onVisibleColumnIdsChange,
  largeHeader,
}: ColumnCustomizationProps<T>) {
  const [searchText, setSearchText] = React.useState("");
  const filteredColumnDefinitions =
    columnDefinitions.length < MIN_SCROLLABLE_COLUMN_COUNT
      ? columnDefinitions
      : columnDefinitions.filter(cd =>
          cd.header.toLowerCase().includes(searchText.toLocaleLowerCase())
        );
  return (
    <div
      className={css(
        styles.columnCustomizationContainer,
        largeHeader
          ? styles.columnCustomizationLarge
          : styles.columnCustomizationSmall
      )}
    >
      <PopupWithClickAway>
        {(Target, Popup, {openPopup}) => (
          <>
            <Target>
              <IconButton
                size="l"
                kind={largeHeader ? "hollow" : "blank"}
                iconName="cog"
                type="button"
                onClick={openPopup}
              />
            </Target>
            {/* zIndex needed for IE11 */}
            <Popup placement="bottom-end" zIndex={1}>
              <div className={css(styles.columnCustomizationPopup)}>
                <div className={css(styles.columnCustomizationHeader)}>
                  {columnDefinitions.length >= MIN_SCROLLABLE_COLUMN_COUNT ? (
                    <TextInput
                      value={searchText}
                      onChange={setSearchText}
                      prefix={{iconName: "search"}}
                      placeholder="Search"
                    />
                  ) : null}
                </div>
                <div className={css(styles.columnCustomizationOptions)}>
                  <CheckboxList
                    values={visibleColumnIds}
                    options={filteredColumnDefinitions.map(cd => ({
                      label: cd.header,
                      value: cd.id,
                      key: cd.id,
                    }))}
                    onChange={onVisibleColumnIdsChange}
                    gap={8}
                    showSelectAllOption={true}
                  />
                </div>
              </div>
            </Popup>
          </>
        )}
      </PopupWithClickAway>
    </div>
  );
}

function HeaderRow({
  headers,
  style,
}: {|
  +headers: $ReadOnlyArray<HeaderColumn>,
  +style: Style,
|}) {
  const leftPinnedColumns = headers.filter(({pinned}) => pinned === "left");
  const unpinnedColumns = headers.filter(({pinned}) => pinned === null);
  const rightPinnedColumns = headers.filter(({pinned}) => pinned === "right");
  const scrollContext = React.useContext(StickyScrollContext);

  return (
    <div
      className={css(styles.headerRow)}
      ref={scrollContext && scrollContext.addRef("top")}
      style={{
        ...style,
        ...(scrollContext && !scrollContext.isPositionStickySupported
          ? {
              transform: `translateY(${scrollContext.scrollTop}px)`,
              position: "relative",
            }
          : {position: "sticky"}),
      }}
    >
      <PinnedColumns columns={leftPinnedColumns} type="left">
        {header => header.render()}
      </PinnedColumns>
      <div className={css(styles.unpinnedDrawer)}>
        {unpinnedColumns.map(header => header.render())}
      </div>
      <PinnedColumns columns={rightPinnedColumns} type="right">
        {header => header.render()}
      </PinnedColumns>
    </div>
  );
}

function getIcon(sortDirection: "asc" | "desc" | null) {
  const iconNames = {
    asc: "up",
    desc: "down",
  };

  return iconNames[sortDirection || "asc"];
}

function Header({
  style,
  header,
  align,
  tooltipText,
  compactCells,
}: {|
  +style: Style,
  +header: React.Node,
  +align: "left" | "center" | "right",
  +tooltipText: ?string,
  +compactCells: boolean,
|}) {
  return (
    <div
      className={css(
        styles.cell,
        compactCells && styles.cellCompact,
        styles.header,
        align === "center" && styles.alignCenter,
        align === "right" && styles.alignRight
      )}
      style={style}
    >
      {tooltipText != null ? (
        <Tooltip
          placement="bottom"
          overlay={<span className={css(styles.tooltip)}>{tooltipText}</span>}
        >
          <div className={css(styles.tooltipHeader)}>
            <Text scale="subtext" color="grey40">
              {header}
            </Text>
          </div>
        </Tooltip>
      ) : (
        <Text scale="subtext" color="grey50">
          {header}
        </Text>
      )}
    </div>
  );
}

function SortableHeader({
  style,
  onSortDirectionChange,
  sortDirection,
  header,
  align,
  tooltipText,
  compactCells,
  dataQaId,
}: {|
  +style: Style,
  +onSortDirectionChange: ("asc" | "desc" | null) => void,
  +sortDirection: "asc" | "desc" | null,
  +header: string,
  +align: "left" | "center" | "right",
  +tooltipText: ?string,
  +compactCells: boolean,
  +dataQaId: ?string,
|}) {
  const [isHovered, setHovered] = React.useState(false);
  const isSorted = sortDirection !== null;

  return (
    <Clickable
      onClick={() => {
        if (sortDirection === null) {
          onSortDirectionChange("asc");
        } else if (sortDirection === "asc") {
          onSortDirectionChange("desc");
        } else {
          onSortDirectionChange(null);
        }
      }}
    >
      <div
        className={css(
          styles.cell,
          compactCells && styles.cellCompact,
          styles.header,
          styles.clickable,
          align === "center" && styles.alignCenter,
          align === "right" && styles.alignRight
        )}
        style={style}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
      >
        <div className={css(styles.sortableHeader)}>
          {tooltipText != null ? (
            <Tooltip
              placement="bottom"
              overlay={
                <span className={css(styles.tooltip)}>{tooltipText}</span>
              }
            >
              <div className={css(styles.tooltipHeader)}>
                <Text
                  scale="subtext"
                  weight={isSorted ? "bold" : "regular"}
                  color={isSorted ? "grey60" : "grey40"}
                  dataQaId={
                    dataQaId != null ? `${dataQaId}-header-${header}` : ""
                  }
                >
                  {header}
                </Text>
              </div>
            </Tooltip>
          ) : (
            <Text
              scale="subtext"
              weight={isSorted ? "bold" : "regular"}
              color={isSorted ? "grey60" : "grey40"}
              dataQaId={dataQaId != null ? `${dataQaId}-header-${header}` : ""}
            >
              {header}
            </Text>
          )}
          {isSorted || isHovered ? (
            <div className={css(styles.headerSortIcon)}>
              <Icon
                alignment="center"
                color={isSorted ? "grey60" : "grey40"}
                size="xxxs"
                iconName={getIcon(sortDirection)}
              />
            </div>
          ) : null}
        </div>
      </div>
    </Clickable>
  );
}

function Row<T>({
  data,
  ...params
}: {|
  +data: T,
  +columns: $ReadOnlyArray<Column<T>>,
  +className: string,
  +style: Style,
  +onHighlightedChange: boolean => void,
  +onClick: (() => void) | null,
|}) {
  const render = ({id, Cell}) => <Cell key={id} row={data} />;
  return BaseRow({
    render,
    ...params,
  });
}

function ExpandedRow<T>({
  data,
  ...params
}: {|
  +data: T,
  +columns: $ReadOnlyArray<Column<T>>,
  +className: string,
  +style: Style,
  +onHighlightedChange: boolean => void,
  +onClick: (() => void) | null,
|}) {
  const render = ({id, ExpandedCell}) => <ExpandedCell key={id} row={data} />;
  return BaseRow({
    render,
    ...params,
  });
}

function AggregateRow<T>({
  data,
  ...params
}: {|
  +data: $ReadOnlyArray<T>,
  +columns: $ReadOnlyArray<Column<T>>,
  +className: string,
  +style: Style,
  +onHighlightedChange: boolean => void,
  +onClick: (() => void) | null,
|}) {
  const render = ({id, AggregateCell}) => (
    <AggregateCell key={id} rows={data} />
  );
  return BaseRow({
    render,
    ...params,
  });
}

function BaseRow<T>({
  render,
  columns,
  className,
  style,
  onHighlightedChange,
  onClick,
}: {|
  +render: (Column<T>) => React.Node,
  +columns: $ReadOnlyArray<Column<T>>,
  +className: string,
  +style: Style,
  +onHighlightedChange: boolean => void,
  +onClick: (() => void) | null,
|}) {
  const leftPinnedColumns = columns.filter(({pinned}) => pinned === "left");
  const unpinnedColumns = columns.filter(({pinned}) => pinned === null);
  const rightPinnedColumns = columns.filter(({pinned}) => pinned === "right");

  return (
    <Clickable onClick={onClick}>
      <div
        onMouseEnter={() => onHighlightedChange(true)}
        onMouseLeave={() => onHighlightedChange(false)}
        className={className}
        style={style}
      >
        <PinnedColumns columns={leftPinnedColumns} type="left">
          {render}
        </PinnedColumns>
        <div className={css(styles.unpinnedDrawer)}>
          {unpinnedColumns.map(render)}
        </div>
        <PinnedColumns columns={rightPinnedColumns} type="right">
          {render}
        </PinnedColumns>
      </div>
    </Clickable>
  );
}

function PinnedColumns<C>({
  columns,
  children,
  type,
}: {|
  /* The generic type C could be either Column<T> or Header<T> */
  +columns: Array<C>,
  +children: C => React.Node,
  +type: "left" | "right",
|}) {
  const scrollContext = React.useContext(StickyScrollContext);

  return columns.length ? (
    <div
      className={css(
        type === "left" ? styles.leftPinnedDrawer : styles.rightPinnedDrawer
      )}
      ref={scrollContext && scrollContext.addRef(type)}
      style={
        scrollContext && !scrollContext.isPositionStickySupported
          ? {
              transform: `translateX(${
                type === "left"
                  ? scrollContext.scrollLeft
                  : scrollContext.rightOffset
              }px)`,
              position: "relative",
            }
          : {position: "sticky"}
      }
    >
      {columns.map(column => children(column))}
    </div>
  ) : null;
}

function BlankExpansionCell({width}: {+width: number, ...}) {
  return <div className={css(styles.rowExpansionCell)} style={{width}} />;
}

const styles = StyleSheet.create({
  tableContainer: {
    minHeight: "100px", // hint to users that they need to set a container height to size table
    minWidth: MIN_TABLE_WIDTH,
    height: "100%",
    width: "100%",
    position: "relative",
  },
  leftPinnedDrawer: {
    flex: "none",
    display: "flex",
    left: 0,
    backgroundColor: "inherit",
    borderRight: `2px solid ${colors.grey30}`,
    paddingLeft: 12,
    zIndex: 1,
  },
  unpinnedDrawer: {
    flex: 1,
    minWidth: 0,
    display: "flex",
    paddingLeft: 12,
  },
  rightPinnedDrawer: {
    flex: "none",
    display: "flex",
    right: 0,
    backgroundColor: "inherit",
    borderLeft: `2px solid ${colors.grey30}`,
    paddingLeft: 12,
    zIndex: 1,
  },
  rowExpansionCell: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
  },
  rowSelectionCell: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    width: ROW_SELECTION_COLUMN_WIDTH,
  },
  tooltipHeader: {
    borderBottom: `1px dashed ${colors.grey40}`,
  },
  tooltip: {
    display: "block",
    width: 150,
    wordWrap: "break-word",
  },
  cell: {
    display: "flex",
    textOverflow: "ellipsis",
    overflowX: "hidden",
    whiteSpace: "nowrap",
    paddingLeft: COLUMN_PADDING,
    paddingRight: COLUMN_PADDING,
  },
  cellAllowContentOverflow: {
    overflowX: "inherit",
  },
  cellCompact: {
    paddingLeft: COLUMN_PADDING_COMPACT,
    paddingRight: COLUMN_PADDING_COMPACT,
  },
  alignCenter: {
    justifyContent: "center",
  },
  alignRight: {
    justifyContent: "flex-end",
  },
  headerRow: {
    display: "inline-flex",
    backgroundColor: "white",
    borderTop: `1px solid ${colors.grey30}`,
    borderBottom: `1px solid ${colors.grey30}`,
    zIndex: 2,
    top: 0,
    left: 0,
    width: "100%",
  },
  header: {
    alignItems: "center",
    color: colors.grey40,
    backgroundColor: "inherit",
    ":hover": {
      color: colors.grey60,
    },
  },
  sortableHeader: {
    position: "relative",
    display: "flex",
  },
  headerSortIcon: {
    position: "absolute",
    left: "calc(100% + 6px)",
    height: "100%",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
  },
  clickable: {
    cursor: "pointer",
  },
  row: {
    display: "flex",
    backgroundColor: "white",
    borderBottom: `1px solid ${colors.grey30}`,
  },
  columnCustomizationContainer: {
    position: "absolute",
    display: "flex",
    alignItems: "center",
    top: 0,
    right: 0,
    padding: "0 12px",
  },
  columnCustomizationSmall: {
    borderTop: `1px solid ${colors.grey30}`,
    borderBottom: `1px solid ${colors.grey30}`,
    borderLeft: `1px solid ${colors.grey30}`,
    background: "white",
    height: HEADER_HEIGHT,
  },
  columnCustomizationLarge: {
    height: LARGE_HEADER_HEIGHT,
  },
  columnCustomizationPopup: {
    background: "white",
    boxShadow: "0px 0px 20px rgba(57, 65, 77, 0.15)",
    minWidth: 200,
    marginTop: 4,
    padding: 12,
  },
  columnCustomizationHeader: {
    marginBottom: whitespaceSizeConstants.s,
  },
  columnCustomizationOptions: {
    maxHeight: 470,
    overflowY: "scroll",
  },
  infiniteLoader: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    padding: "12px 0px",
  },
  loadingIndicator: {
    height: "100%",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    padding: "12px 0px",
  },
  isHighlighted: {
    backgroundColor: colors.grey10,
  },
  isExpandedChildRow: {
    backgroundColor: colors.grey10,
  },
  isExpandedAggregateRow: {
    backgroundColor: colors.grey20,
  },
  isSelected: {
    backgroundColor: colors.grey20,
    borderBottom: `1px solid ${colors.grey60}`,
  },
  isNextSelected: {
    borderBottom: `1px solid ${colors.grey60}`,
  },
  disabledRow: {
    backgroundColor: colors.grey10,
    color: `${colors.grey40} !important`,
  },
});
