import React, { useCallback, useMemo } from 'react';
import {
  array,
  arrayOf,
  bool,
  func,
  node,
  number,
  object,
  oneOf,
  oneOfType,
  shape,
  string,
} from 'prop-types';
import { useExpanded, useGetLatest, useRowSelect, useTable } from 'react-table';

import { CSSTransition } from 'react-transition-group';
import Checkbox from '../Checkbox';
import TableSort from './TableSort';
import cx from 'classnames';
import omit from 'lodash/omit';
import style from './Table.module.scss';
import useControlProp from '../../hooks/useControlProp';
import { usePaginationContext } from '../Pagination/PaginationContainer';

const Table = ({
  columns,
  className,
  rowClassName,
  borderBottom = true,
  noDataIndication,
  loading,
  scrollContainer = true,
  data: dataProp = [],
  keyField,
  pagination: paginationProp,
  initialPlaceholderRows: placeholderRowsProp = 10,
  selected = {},
  disabled = {},
  setSelected,
  renderSelect,
  renderSelectAll,
  expanded: expandedProp,
  setExpanded: setExpandedProp,
  hiddenColumns = [],
  renderExpanded,
}) => {
  const paginationCtx = usePaginationContext();
  const pagination = paginationProp || paginationCtx;
  const placeholderRows =
    (pagination?.totalSize && pagination.itemCountOnPage) || placeholderRowsProp;

  const getRowId = useCallback(
    (row, relativeIndex, parent) =>
      row[keyField] || (parent ? [parent.id, relativeIndex].join('.') : relativeIndex),
    [keyField],
  );
  const [expanded, setExpanded] = useControlProp(expandedProp, setExpandedProp, {});
  const data = pagination?.pageData || dataProp;
  const canExpand = !!renderExpanded;
  const canSelect = setSelected;
  const getSelected = useGetLatest(selected);
  const getSelectDisabled = useGetLatest(disabled);
  const getAllSelectDisabled = useGetLatest(!data.length || loading);

  function getAllSelectedByPage(rowsById, disabled) {
    return Object.keys(
      omit(
        rowsById,
        Object.keys(disabled).filter((k) => disabled[k]),
      ),
    ).every((rowId) => getSelected()[rowId]);
  }

  const instance = useTable(
    {
      initialState: {
        hiddenColumns,
      },
      columns,
      data,
      getRowId,
      useControlledState: (originalState) => {
        return useMemo(
          () => ({
            ...originalState,
            expanded,
            selectedRowIds: selected,
          }),
          // eslint-disable-next-line react-hooks/exhaustive-deps
          [originalState, expanded, selected],
        );
      },
      autoResetExpanded: false,
      autoResetSelectedRows: false,
    },
    useExpanded,
    useRowSelect,
    (hooks) => {
      function toggleSelectAll(checked) {
        const selectedRowIds = {};

        if (checked) {
          const disabled = getSelectDisabled();
          Object.assign(selectedRowIds, getSelected());
          Object.keys(instance.rowsById).forEach((rowId) => {
            if (disabled[rowId]) delete selectedRowIds[rowId];
            else selectedRowIds[rowId] = true;
          });
        }

        setSelected(selectedRowIds);
      }

      function toggleSelect(rowId, checked) {
        const selectedRowIds = Object.assign({}, getSelected());

        if (checked) {
          selectedRowIds[rowId] = true;
        } else {
          delete selectedRowIds[rowId];
        }

        setSelected(selectedRowIds);
      }
      if (canSelect) {
        hooks.getToggleAllRowsSelectedProps = [
          (props, { instance }) => [
            props,
            {
              onChange: function onChange(e) {
                instance.toggleAllRowsSelected(e.target.checked);
              },
              style: {
                cursor: 'pointer',
              },
              title: 'Toggle All Rows Selected',
              indeterminate: Boolean(
                !getAllSelectedByPage(instance.rowsById, getSelectDisabled()) &&
                  Object.keys(instance.state.selectedRowIds).length,
              ),
              checked: getAllSelectedByPage(instance.rowsById, getSelectDisabled()),
            },
          ],
        ];
        hooks.allColumns.push((cols) => [
          {
            id: 'selection',
            headerStyle: { width: '20px' },
            header: (_, { getToggleAllRowsSelectedProps }) => {
              const { checked, indeterminate, title } = getToggleAllRowsSelectedProps();

              function onChange(e) {
                toggleSelectAll(e.target.checked);
              }

              const disabled = getAllSelectDisabled();

              return renderSelectAll ? (
                renderSelectAll({ checked, indeterminate, title, onChange, disabled })
              ) : (
                <Checkbox
                  checked={checked}
                  indeterminate={indeterminate}
                  title={title}
                  onChange={onChange}
                  disabled={disabled}
                />
              );
            },
            cell: (_, __, row) => {
              const { checked, indeterminate, title } = row.getToggleRowSelectedProps();

              function onChange(e) {
                toggleSelect(row.id, e.target.checked);
              }

              const disabled = !!getSelectDisabled()[row.id];

              return (
                <div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
                  {renderSelect ? (
                    renderSelect(row, { checked, indeterminate, title, onChange })
                  ) : (
                    <Checkbox
                      checked={checked}
                      indeterminate={indeterminate}
                      title={title}
                      onChange={onChange}
                      disabled={disabled}
                    />
                  )}
                </div>
              );
            },
          },
          ...cols,
        ]);
      }
    },
  );
  const { headerGroups, rows, prepareRow, visibleColumns, setHiddenColumns } = instance;

  React.useEffect(() => {
    if (hiddenColumns && hiddenColumns.length > 0) setHiddenColumns(hiddenColumns);
  }, [hiddenColumns, setHiddenColumns]);

  return (
    <div className={cx(scrollContainer && style.scrollContainer)}>
      <table className={cx(style.table, className, borderBottom && style.borderBottom)}>
        <thead>
          {headerGroups.map((headerGroup) => (
            <tr className={style.header} key={headerGroup.getHeaderGroupProps().key}>
              {headerGroup.headers.map((column) => (
                <th
                  style={column.headerStyle}
                  className={cx(column.headerClassName)}
                  key={column.getHeaderProps().key}
                >
                  {typeof column.header === 'function'
                    ? column.header(column, instance)
                    : column.header}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {loading ? (
            Array.from({ length: placeholderRows }, (_, i) => (
              <tr className={cx(style.row)} key={i.toString()}>
                {Array.from({ length: visibleColumns.length }, (__, j) => (
                  <td
                    className={cx(style[visibleColumns[j].align], visibleColumns[j].className)}
                    key={j.toString()}
                  >
                    {canSelect && j === 0 ? (
                      <Checkbox disabled />
                    ) : (
                      <div className={style.placeholder} />
                    )}
                  </td>
                ))}
              </tr>
            ))
          ) : rows.length ? (
            rows.map((row) => {
              prepareRow(row);
              const rowProps = row.getRowProps();
              const rowClass =
                typeof rowClassName === 'function' ? rowClassName(row.original, row) : rowClassName;
              return (
                <React.Fragment key={rowProps.key}>
                  <tr
                    className={cx(
                      style.row,
                      rowClass,
                      row.isExpanded && style.expanded,
                      canExpand && style.canExpand,
                    )}
                    onClick={
                      canExpand
                        ? () =>
                            setExpanded(
                              {
                                ...expanded,
                                [row.id]: !row.isExpanded,
                              },
                              row.original,
                              !row.isExpanded,
                            )
                        : undefined
                    }
                    onKeyDown={(e) => {
                      if ((canExpand && e.key === 'Enter') || e.key === ' ')
                        setExpanded(
                          {
                            ...expanded,
                            [row.id]: !row.isExpanded,
                          },
                          row.original,
                          !row.isExpanded,
                        );
                    }}
                    tabIndex={canExpand ? 0 : undefined}
                  >
                    {row.cells.map((cell, j) => (
                      <Cell
                        cell={cell}
                        row={row}
                        key={j.toString()}
                        visibleColumns={visibleColumns}
                        index={j}
                        instance={instance}
                      />
                    ))}
                  </tr>
                  {canExpand && (
                    <CSSTransition
                      in={row.isExpanded}
                      timeout={{ enter: 2000, exit: 400 }}
                      unmountOnExit
                    >
                      <tr className={style.expandRow}>
                        <td colSpan={visibleColumns.length}>
                          <div className={style.expand}>{renderExpanded(row.original, row)}</div>
                        </td>
                      </tr>
                    </CSSTransition>
                  )}
                </React.Fragment>
              );
            })
          ) : (
            <tr className={style.row}>
              <td colSpan={visibleColumns.length}>
                {typeof noDataIndication === 'function' ? (
                  noDataIndication()
                ) : (
                  <div className={style.noData}>{noDataIndication}</div>
                )}
              </td>
            </tr>
          )}
        </tbody>
      </table>
    </div>
  );
};

const Cell = React.memo(
  ({ cell, row, visibleColumns, index, instance }) => {
    if (cell.column.loading) {
      return (
        <td className={cx(style[cell.column.align])}>
          <div className={style.placeholder}></div>
        </td>
      );
    }

    const formatter = visibleColumns[index].cell;
    return (
      <td className={cx(style[cell.column.align])}>
        {typeof formatter === 'function'
          ? formatter(cell.value, row.original, row, instance)
          : formatter || cell.value}
      </td>
    );
  },
  (prev, next) =>
    next.cell.column.memoize !== false &&
    prev.row.original === next.row.original &&
    next.cell.column.id !== 'selection',
);

Table.propTypes = {
  data: array,
  loading: bool,
  initialPlaceholderRows: number,
  scrollContainer: bool,
  columns: arrayOf(
    shape({
      id: string,
      accessor: string,
      header: oneOfType([node, func]),
      cell: oneOfType([node, func]),
      headerStyle: object,
      align: oneOf(['left', 'center', 'right']),
    }),
  ).isRequired,
  pagination: object,
  className: string,
  rowClassName: oneOfType([string, func]),
  borderBottom: bool,
  noDataIndication: oneOfType([func, node]),
  expanded: object,
  setExpanded: func,
  renderExpanded: func,
  selected: object,
  setSelected: func,
  renderSelect: func,
  renderSelectAll: func,
  keyField: string,
};

export default Table;
export { TableSort };
