import { ReactNode } from 'react';

import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { CommonProps } from '../../types';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';

import { SelectedBanner } from './components/SelectedBanner/SelectedBanner';
import { TableContainer } from './components/TableContainer/TableContainer';
import { TableGrid } from './components/TableGrid/TableGrid';
import { TableHorizontalScroll } from './components/TableHorizontalScroll/TableHorizontalScroll';
import { TableLoader } from './components/TableLoader/TableLoader';
import { TableNoColumnsBodyDefault } from './components/TableNoColumnsBodyDefault/TableNoColumnsBodyDefault';
import { TableContexts } from './contexts/TableContexts';
import { CaptionRenderArea } from './renderAreas/CaptionRenderArea';
import { FooterRenderArea } from './renderAreas/FooterRenderArea';
import { TableRenderArea } from './renderAreas/TableRenderArea';
import { StyledFooterContainer, StyledGeneralContainer, StyledTableSizeContainer } from './styled';
import { TableColumnVisibility, TableSelection, TableSort } from './types';

/** Props for the {@link Table} component */
export type TableProps<C extends string, D extends object> = CommonProps & {
  /** The data to display in the table */
  rows: D[];
  /** Function that should return unique key for given row */
  rowIdProvider: (row: D) => string;
  /** List of columns to display in the table */
  columns: TableColumnVisibility<C>[] | C[];
  /** Count of all rows that can be rendered on table */
  totalRows: number;
  /** Current table selection */
  selection?: TableSelection;
  /** Function that should be called when table selection is changed */
  onSelectionChanged?: (selection: TableSelection) => void;
  /** Current table sort */
  sort?: TableSort<C>;
  /** Function that should be called when table sort is changed */
  onSortChanged?: (sort: TableSort<C>) => void;
  /** If `true` than loader indicator will be shown */
  loading?: boolean;
  /** Limit the number of table selections */
  rowSelectionIncludeLimit?: number;
  /**
   * If `true` than user can use row actions.
   *
   * @default true
   */
  rowActionsEnabled?: boolean;
  /**
   * If `true`, the "actions" and "selection" columns are always visible
   */
  stickyColumns?: boolean;
  children: ReactNode;
} & (
    | {
        /**
         * If `true` than user can select rows.
         *
         * @default true
         */
        rowSelectionEnabled: true;
        /** Item name to show in the selection banner */
        rowSelectionName: string;
      }
    | {
        rowSelectionEnabled?: false;
        rowSelectionName?: string;
      }
  );

/**
 * Component is used for show data in table view.
 *
 * It supports flexible data cell rendering, sort data, pagination and column order and visibility control.
 *
 * To create table you must set {@link TableProps.rows} with data, configure columns by {@link TableProps.columns}
 * and set total rows count by {@link TableProps.totalRows}.
 *
 * Also, you should provide {@link TableProps.rowIdProvider} function that should return unique key for given row.
 *
 * ## Columns configuration
 *
 * To configure columns define list of available columns in {@link TableProps.columns} and define
 * column render in {@link TableHeader} and {@link TableBody}.
 *
 * Pay attention that {@link TableRow} should be memoized for performance issue.
 *
 * ```tsx
 * <Table columns={[Columns.Name]}>
 *   <TableHeader>
 *     <TableHeaderCell column={Columns.Name}>Name</TableHeaderCell>
 *   </TableHeader>
 *
 *   <TableBody<RowData>>{(row) => <MyTableRowMemo row={row} />}</TableBody>
 * </Table>
 *
 * // Somewhere in different place
 * function MyTableRow({ row }: { row: RowData }) {
 *   return (
 *     <TableRow linkTo="dashboard">
 *       <TableCell column={Columns.Name}>{row.name}</TableCell>
 *     </TableRow>
 *   );
 * }
 *
 * const MyTableRowMemo = memo(MyTableRow);
 * ```
 *
 * ## Rows per page
 *
 * {@link Table} supports rows per change feature. To enable it you must use {@link TableRowsPerPage} component
 * in {@link TableFooter}.
 *
 * ```tsx
 * <TableFooter>
 *   <TableRowsPerPage rowsPerPage={rowsPerPage} onRowsPerPageChange={setRowsPerPage} />
 * </TableFooter>
 * ```
 *
 * ## Pagination
 *
 * {@link Table} supports pagination. To enable it you must use {@link TablePagination} component
 * in {@link TableFooter}.
 *
 * ```tsx
 * <TableFooter>
 *   <TablePagination
 *      rowsPerPage={rowsPerPage}
 *      currentPage={currentPage}
 *      linkToBuilder={(page) => `/?page=${page}`}
 *    />
 * </TableFooter>
 * ```
 *
 * Pagination should be implemented by some URL search query params for best user experience.
 *
 * ```tsx
 * const { search } = useLocation();
 * const currentPage = Number(new URLSearchParams(search).get('page') || '0');
 * ```
 *
 * ## Column sort
 *
 * {@link Table} supports column sort by {@link TableProps.sort} and {@link TableProps.onSortChanged}
 * {@link Table} props and {@link TableHeaderCellProps.sortable} {@link TableHeaderCell} props.
 *
 * Set current sort in {@link TableProps.sort} and define header that should be sortable by
 * {@link TableHeaderCellProps.sortable}.
 *
 * ```tsx
 * <Table
 *   sort={currentSort}
 *   onSortChanged={handleSortChanged}
 *  >
 *    <TableHeader>
 *      <TableHeaderCell column={Columns.Name} sortable>Name</TableHeaderCell>
 *    </TableHeader>
 *  </Table>
 * ```
 *
 * ## Row actions
 *
 * {@link Table} supports row actions that will be shown on each row at rightTo enable it you must use
 * {@link TableActionsCell} component in {@link TableRow}.
 *
 * ```tsx
 * <TableRow>
 *   <TableActionsCell>
 *     <TableAction icon={IconGlyph.Edit} onClick={editClickHandler}>Edit</TableAction>
 *     <TableAction icon={IconGlyph.Delete} onClick={deleteClickHandler}>Delete</TableAction>
 *   </TableActionsCell>
 * </TableRow>
 * ```
 *
 * ## Rows selection
 *
 * Fot select some rows in {@link Table} provide {@link TableProps.selection} property with selection mode
 * and {@link TableSelection.selectedIds} or {@link TableSelection.unselectedIds} row IDs.
 * And provide {@link TableProps.onSelectionChanged} property to process selection change events.
 *
 * ```tsx
 * <Table
 *   selection={currentSelection}
 *   onSelectionChanged={handleSelectionChanged}
 * >
 *   // ...
 * </Table>
 * ```
 *
 * ### Disable selection
 *
 * To disable rows selection set `false` to {@link TableProps.rowSelectionEnabled}.
 *
 * ```tsx
 * <Table rowSelectionEnabled={false}>
 *   // ...
 * </Table>
 * ```
 *
 * ## Interaction
 *
 * If you need to open some URL or run some code when a row is clicked, you should use
 * {@link TableRowProps.linkTo} or {@link TableRowProps.onClick} props.
 *
 * ```tsx
 * <TableRow linkTo="some_url">
 *   // ...
 * </TableRow>
 * ```
 *
 * ## States
 *
 * {@link Table} provide some special states.
 *
 * ### Empty state
 *
 * If there are no rows to display, you can set specific content that should be displayed by
 * {@link TableNoRowsBody} component.
 *
 * ```tsx
 * <Table>
 *   <TableNoRowsBody>
 *     <EmptyState>
 *       <EmptyStateHeader>There are no items</EmptyStateHeader>
 *       <EmptyStateBody>Click "Add item" button to add one</EmptyStateBody>
 *       <EmptyStateButton>Add item</EmptyStateButton>
 *     </EmptyState>
 *   </TableNoRowsBody>
 * </Table>
 * ```
 *
 * ### Loading state
 *
 * You can pass {@link TableProps.loading} props to indicate that table is in loading.
 *
 * If table has no data and state is loading then preload indicator will be shown.
 *
 * ```tsx
 * <Table loading>
 *   // ...
 * </Table>
 * ```
 *
 * ## Complex example
 *
 * ```tsx
 * interface RowData {
 *   id: string;
 *   name: string;
 *   phone: string;
 * }
 *
 * enum Columns {
 *   Name,
 *   Phone
 * }
 *
 * function Users() {
 *   const rows: RowData[] = [
 *     // Some data
 *   ];
 *   const totalRows = 1000;
 *   const [ rowsPerPage, setRowsPerPage ] = useState(25);
 *   const rowIdProvider = useCallback((row: RowData) => row.id, []);
 *
 *   const { search } = useLocation();
 *   const currentPage = Number(new URLSearchParams(search).get('page') || '0');
 *
 *   return (
 *     <Table
 *       columns={[Columns.Name, Columns.Phone]}
 *       rows={rows}
 *       rowIdProvider={rowIdProvider}
 *       totalRows={totalRows}
 *     >
 *       <TableCaption>
 *         <TableCaptionTitle>Table title</TableCaptionTitle>
 *         <TableCaptionSummary>Table sub title</TableCaptionSummary>
 *       </TableCaption>
 *
 *       <TableHeader>
 *         <TableHeaderCell column={Columns.Name} sortable>
 *          Name
 *         </TableHeaderCell>
 *         <TableHeaderCell column={Columns.Phone}>
 *           Phone
 *         </TableHeaderCell>
 *       </TableHeader>
 *
 *       <TableBody<RowData>>{(row) => <MyTableRowMemo row={row} />}</TableBody>
 *
 *       <TableNoRowsBody>
 *         <EmptyState>
 *           <EmptyStateHeader>There are no items</EmptyStateHeader>
 *           <EmptyStateBody>Click "Add item" button to add one</EmptyStateBody>
 *           <EmptyStateButton>Add item</EmptyStateButton>
 *         </EmptyState>
 *       </TableNoRowsBody>
 *
 *       <TableNoColumnsBody>
 *         <EmptyState>
 *           <EmptyStateHeader>All table columns are hidden</EmptyStateHeader>
 *           <EmptyStateBody>Click the "Adjust columns" button to choose columns to show</EmptyStateBody>
 *         </EmptyState>
 *       </TableNoColumnsBody>
 *
 *      <TableFooter>
 *        <TableRowsPerPage rowsPerPage={rowsPerPage} onRowsPerPageChange={setRowsPerPage} />
 *        <TablePagination
 *           rowsPerPage={rowsPerPage}
 *           currentPage={currentPage}
 *           linkToBuilder={(page) => `/?page=${page}`}
 *         />
 *      </TableFooter>
 *    </Table>
 *  );
 * }
 *
 * function MyTableRow({ row }: { row: RowData }) {
 *   const editClickHandler = useCallback(() => {}, []);
 *   const deleteClickHandler = useCallback(() => {}, []);
 *
 *   return (
 *     <TableRow>
 *       <TableCell column={Columns.Name}>{row.name}</TableCell>
 *       <TableCell column={Columns.Phone}>{row.phone}</TableCell>
 *       <TableActionsCell>
 *         <TableAction icon={IconGlyph.Edit} onClick={editClickHandler}>Edit</TableAction>
 *         <TableAction icon={IconGlyph.Delete} onClick={deleteClickHandler}>Delete</TableAction>
 *       </TableActionsCell>
 *     </TableRow>
 *   );
 * }
 *
 * const MyTableRowMemo = memo(MyTableRow);
 * ```
 */
export function Table<C extends string, D extends object>(props: TableProps<C, D>) {
  const {
    rowActionsEnabled,
    stickyColumns,
    rowSelectionEnabled,
    rowSelectionIncludeLimit,
    rowSelectionName,
    children,
    className,
    columns,
    rows,
    rowIdProvider,
    totalRows,
    sort,
    onSortChanged,
    onSelectionChanged,
    selection,
    loading,
    testId,
    ariaDescribedBy,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();

  return (
    <TableContexts
      columns={columns}
      loading={loading}
      onSelectionChanged={onSelectionChanged}
      onSortChanged={onSortChanged}
      rowActionsEnabled={rowActionsEnabled ?? true}
      rowIdProvider={rowIdProvider}
      rows={rows}
      rowSelectionEnabled={rowSelectionEnabled ?? true}
      rowSelectionIncludeLimit={rowSelectionIncludeLimit}
      rowSelectionName={rowSelectionName}
      selection={selection}
      sort={sort}
      stickyColumns={stickyColumns}
      totalRows={totalRows}
    >
      <StyledGeneralContainer
        aria-describedby={ariaDescribedBy}
        className={className}
        {...{ [testIdAttribute]: testId }}
      >
        <CaptionRenderArea>{children}</CaptionRenderArea>

        <TableContainer>
          <TableRenderArea>
            <StyledTableSizeContainer>
              <TableGrid testId={makeTestId(testId, 'grid')}>
                {children}
                <TableNoColumnsBodyDefault testId={makeTestId(testId, 'no-columns')}>
                  {children}
                </TableNoColumnsBodyDefault>
              </TableGrid>
              <TableLoader testId={makeTestId(testId, 'loader')} />
            </StyledTableSizeContainer>
          </TableRenderArea>

          <StyledFooterContainer>
            {!loading && <TableHorizontalScroll testId={makeTestId(testId, 'horizontal-scroll')} />}
            <SelectedBanner />
            <FooterRenderArea>{children}</FooterRenderArea>
          </StyledFooterContainer>
        </TableContainer>
      </StyledGeneralContainer>
    </TableContexts>
  );
}
