import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { MouseEvent } from 'react';
import clsx from 'clsx';

import { DataGridPro as MiuDataGrid, LicenseInfo, useGridApiRef } from '@mui/x-data-grid-pro';
import type { GridColumnHeaderParams, GridEventsStr } from '@mui/x-data-grid-pro';

import MuiGrid from '@mui/material/Grid';

import { FilterArea } from '../filter-area';
import { Filters } from '../filters';
import { Pagination } from '../@navigation/pagination';
import { throttleEvent, useWindowEventListener } from '../../utils';

import {
  getGridColumnTypes,
  gridCheckboxSelectionColDef,
  gridDetailPanelToggleColDef,
  isHoverableColumn,
  useBaseFilterItem,
  useGridColDef
} from './column-types';

import {
  ArrowIcon,
  BackToTop,
  CustomGridTreeDataGroupingCell,
  DeferredGridCell,
  LazyLoadingGridCell,
  LazyLoadingGridRow,
  NoRowsOverlay,
  SkeletonizedGridCell,
  SkeletonizedGridHeader,
  Toolbar
} from './components';
import { getDetailPanelContent } from './detail-panel/utils';

import createClasses from './styles';
import { GridFeatureModes, GridSortDirections } from './types';
import type { DataGridProps, FieldKeys, GridColDef } from './types';
import {
  convertChildsToTreeData,
  createColumnHeaderRenderer,
  skeletonizedTableData,
  useStickyColumnHeader,
  useStickyHorizontalScrollerHandler,
  useStickyMasterDetailPanel,
  GRID_COLUMN_HEADER_HEIGHT,
  GRID_LICENSE_KEY,
  GRID_ROW_HEIGHT
} from './utils';

LicenseInfo.setLicenseKey(GRID_LICENSE_KEY);

const DataGrid = forwardRef<HTMLDivElement, DataGridProps>((props: DataGridProps, ref) => {
  const {
    tableConfig,
    tableData,
    detailPanelField,
    detailPanelProps,
    treeDataField,
    filters: _filters,
    filterAreaProps,
    filtersProps,
    handleFilterChange,
    toolbarProps,
    numOfRowsOptions = [],
    paginationProps,
    backToTopProps,
    disableColumnHover,
    disableStickyHeader,
    disableStickyPagination,
    stickyHeaderPositionTop,
    selected,
    classes,
    getRowId,
    pushFunc,
    ...otherProps
  } = props;

  const apiRef = useGridApiRef();
  const styles = createClasses({ stickyHeaderPositionTop });

  // COMPONENTS
  const ColumnSortedAscendingIcon = useCallback(
    () => <ArrowIcon className={styles.arrowIcon} rotateDeg={180} />,
    [styles.arrowIcon]
  );
  const ColumnSortedDescendingIcon = useCallback(
    () => <ArrowIcon className={styles.arrowIcon} />,
    [styles.arrowIcon]
  );
  // Lazy & deferred loading only works properly with autoHeight enabled
  const OptimizedGridCell = otherProps.lazyLoading ? LazyLoadingGridCell : DeferredGridCell;
  const GridCell = otherProps.autoHeight ? OptimizedGridCell : undefined;
  const GridRow = otherProps.autoHeight && otherProps.lazyLoading ? LazyLoadingGridRow : undefined;

  // FILTERS
  const [filters, setFilters] = useState({ ..._filters });

  const { createFilterItemsFromValues } = useBaseFilterItem();

  useEffect(() => {
    if (otherProps.filterMode !== GridFeatureModes.SERVER) {
      // reset filters at loading
      if (otherProps.loading) {
        return apiRef.current.setFilterModel({ items: [] });
      }
      // sync filters values with DataGrid's filterModel
      const filtersValues = (filterAreaProps?.values ||
        filtersProps?.values ||
        {}) as DataGridProps['filters'];
      if (filtersValues) {
        const gridFilterItems = createFilterItemsFromValues(filtersValues);
        apiRef.current.setFilterModel({ items: gridFilterItems });
        setFilters(filtersValues);
      }
    }
  }, [
    apiRef,
    filterAreaProps?.values,
    filtersProps?.values,
    otherProps.filterMode,
    otherProps.loading,
    createFilterItemsFromValues
  ]);

  // PAGINATION
  const [paginationCount, setPaginationCount] = useState(paginationProps?.count || 0);
  const [paginationHidden, setPaginationHidden] = useState(
    Boolean(paginationProps?.hideNavigation && paginationProps?.hideRowCount)
  );
  const onStateChange = useCallback(() => {
    if (!paginationProps) return;
    if (otherProps.paginationMode !== GridFeatureModes.SERVER) {
      const paginationState = apiRef.current?.state?.pagination;
      if (paginationState) {
        // override pagination props in 'client' mode
        setPaginationCount(paginationState.rowCount);
        setPaginationHidden(!paginationState.rowCount);
      }
    } else {
      setPaginationCount(otherProps.rowCount || paginationProps.count);
      setPaginationHidden(Boolean(paginationProps.hideNavigation && paginationProps.hideRowCount));
    }
  }, [apiRef, otherProps.paginationMode, otherProps.rowCount, paginationProps]);
  useEffect(() => onStateChange, [onStateChange]);

  // STICKY HEADER
  const toggleStickyHeader = useCallback(
    (isSticky: boolean) => {
      const headersContainerEl = apiRef?.current?.columnHeadersContainerElementRef?.current;
      if (isSticky) {
        headersContainerEl?.classList.add(styles.stickyHeader);
      } else {
        headersContainerEl?.classList.remove(styles.stickyHeader);
      }
    },
    [apiRef, styles.stickyHeader]
  );
  useStickyColumnHeader(apiRef, toggleStickyHeader, {
    autoHeight: otherProps.autoHeight,
    disableStickyHeader
  });

  // COLUMN HOVER
  const [columnNameHover, setColumnNameHover] = useState<string>('');
  const handleColumnMouseEvents = useCallback(
    (field: FieldKeys, hovered?: boolean) => () => {
      // Prevent infinite events on sort icon hover
      if (disableColumnHover || (hovered && field === columnNameHover)) {
        return;
      }
      setColumnNameHover(field === columnNameHover ? '' : field);
    },
    [columnNameHover, disableColumnHover]
  );
  // cancel hover when dragging column heading
  const cancelColumHoverOnEvent = useCallback(
    (eventName: GridEventsStr) =>
      !disableColumnHover
        ? apiRef.current.subscribeEvent(eventName, () => setColumnNameHover(''))
        : undefined,
    [apiRef, disableColumnHover]
  );
  useEffect(() => cancelColumHoverOnEvent('columnHeaderDragEnter'), [cancelColumHoverOnEvent]);
  useEffect(() => cancelColumHoverOnEvent('columnHeaderDragEnd'), [cancelColumHoverOnEvent]);

  // STICKY HORIZONTAL SCROLLER
  const stickyPaginationRef = useRef<HTMLDivElement>(null);
  const stickyHorizontalScrollerHandler = useStickyHorizontalScrollerHandler(
    apiRef,
    !disableStickyPagination ? stickyPaginationRef : undefined,
    otherProps.headerHeight || GRID_COLUMN_HEADER_HEIGHT,
    otherProps.autoHeight
  );
  const handleWindowEventThrottled = throttleEvent(event => {
    if (otherProps.autoHeight) stickyHorizontalScrollerHandler(event);
  });
  useWindowEventListener(['load', 'resize', 'scroll'], handleWindowEventThrottled, true);
  useEffect(() => {
    if (!otherProps.autoHeight) return stickyHorizontalScrollerHandler();
    handleWindowEventThrottled();

    return () => {
      throttleEvent(() => stickyHorizontalScrollerHandler(undefined, true))();
    };
  }, [handleWindowEventThrottled, otherProps.autoHeight, stickyHorizontalScrollerHandler]);

  // STICKY MASTER DETAIL PANEL
  const isDetailPanelEnabled = Boolean(detailPanelField && detailPanelProps);
  useStickyMasterDetailPanel(apiRef, isDetailPanelEnabled);

  // COLUMNS DATA
  const getGridColDef = useGridColDef({ pushFunc });
  const renderColumnHeader = useMemo(
    () =>
      createColumnHeaderRenderer({
        filters,
        setFilters: filters => setFilters(filters || {}),
        handleFilterChange,
        textClassName: styles.columnHeaderTitle
      }),
    [filters, handleFilterChange, styles.columnHeaderTitle]
  );
  const dataGridColDef = useCallback(
    (col: GridColDef, index?: number) => {
      const colDef = getGridColDef(col);
      const colTypeClass = `colType--${colDef.type || 'default'}`;
      const colClasses = {
        [styles.firstColumn]: !index || index === 0,
        [styles.hoverableColumn]: !disableColumnHover && isHoverableColumn(colDef.field),
        [styles.pinnableColumn]: colDef.pinnable,
        [styles.resizableColumn]: !otherProps.disableColumnResize && colDef.resizable !== false,
        [colTypeClass]: true
      };
      return {
        ...colDef,
        cellClassName: clsx(colClasses, colDef.cellClassName),
        headerClassName: clsx(colClasses, colDef.headerClassName),
        renderHeader: colDef.renderHeader || renderColumnHeader
      };
    },
    [disableColumnHover, getGridColDef, otherProps.disableColumnResize, renderColumnHeader, styles]
  );
  const dataGridColumns = useMemo(() => {
    // extend other columns definitions
    let tableColDefs = tableConfig.map(dataGridColDef);
    if (treeDataField) {
      // prevent column duplicating
      tableColDefs = tableColDefs.filter(colDef => colDef.field !== treeDataField);
    }
    // prepend the checkbox and master details column definitions
    // https://mui.com/x/react-data-grid/master-detail/#customizing-the-detail-panel-toggle
    return [
      gridCheckboxSelectionColDef,
      { ...gridDetailPanelToggleColDef, detailPanelField, detailPanelProps },
      ...tableColDefs
    ];
  }, [dataGridColDef, detailPanelField, detailPanelProps, tableConfig, treeDataField]);
  // prevents the MUI's warning on unsupported column type
  const dataGridColumnTypes = useMemo(() => getGridColumnTypes(), []);

  // ROWS DATA
  const dataGridRows = useMemo(() => {
    const rowsData = otherProps.loading ? skeletonizedTableData : tableData.data.slice();
    return treeDataField
      ? // convert rows to switch to tree data
        convertChildsToTreeData(rowsData, '_id')
      : rowsData;
  }, [otherProps.loading, tableData, treeDataField]);

  // TREEDATA GROUPING
  let groupingColDef: DataGridProps['groupingColDef'];
  if (treeDataField) {
    const treeDataColDef = dataGridColDef(
      tableConfig.find(col => col.field === treeDataField) || {
        field: treeDataField,
        type: undefined
      }
    );
    groupingColDef = {
      ...treeDataColDef,
      renderCell: params => (
        <CustomGridTreeDataGroupingCell
          {...params}
          api={null}
          checkboxSelection={otherProps.checkboxSelection}
          renderCell={treeDataColDef.renderCell}
        />
      )
    };
  }

  // PRESELECT ROWS
  useEffect(() => {
    if (selected && selected.length) {
      apiRef.current?.selectRows(selected, true);
    }
  }, [apiRef, selected]);

  return (
    <MuiGrid
      className={clsx(styles.container, classes?.container)}
      container
      flexDirection="column"
      flexWrap="nowrap"
    >
      {filterAreaProps && (
        <MuiGrid
          container
          item
          className={clsx(styles.filtersContainer, classes?.filtersContainer)}
        >
          <FilterArea {...filterAreaProps} />
        </MuiGrid>
      )}

      {filtersProps && (
        <MuiGrid
          container
          item
          className={clsx(styles.filtersContainer, classes?.filtersContainer)}
        >
          <Filters {...filtersProps} />
        </MuiGrid>
      )}

      {toolbarProps && (
        <MuiGrid
          container
          item
          className={clsx([styles.toolbarContainer, classes?.toolbarContainer])}
        >
          <Toolbar
            apiRef={apiRef}
            numOfRowsOptions={numOfRowsOptions}
            rowsPerPage={paginationProps?.rowsPerPage}
            {...toolbarProps}
          />
        </MuiGrid>
      )}

      <MuiGrid
        container
        flex={1}
        item
        className={clsx([styles.dataGridContainer, classes?.dataGridContainer])}
      >
        <MiuDataGrid
          apiRef={apiRef}
          ref={ref}
          className={clsx([
            styles.dataGridRoot,
            otherProps.autoHeight &&
              !disableStickyHeader &&
              styles.dataGridAutoHeightWithStickyHeader,
            otherProps.loading && styles.dataGridSkeletonizedLoading,
            classes?.dataGridRoot
          ])}
          // REQUIRED
          columnTypes={dataGridColumnTypes}
          columns={dataGridColumns}
          rows={dataGridRows}
          // COLUMNS
          disableColumnFilter
          disableColumnMenu
          disableColumnReorder
          // COLUMN HEADERS
          headerHeight={GRID_COLUMN_HEADER_HEIGHT}
          onColumnHeaderEnter={(params: GridColumnHeaderParams) =>
            handleColumnMouseEvents(params.field, true)()
          }
          onColumnHeaderLeave={(params: GridColumnHeaderParams) =>
            handleColumnMouseEvents(params.field, false)()
          }
          // COLUMN HOVER (EXCEPT CHECKBOXES)
          getCellClassName={params => {
            const isHovered = columnNameHover === params.field;
            return clsx({
              [styles.actionsCell]: params.field === 'actions',
              [styles.hoveredCell]: isHovered && isHoverableColumn(params.field)
            });
          }}
          // ROWS
          disableSelectionOnClick
          getRowId={entry => getRowId?.(entry) || entry._id}
          getRowClassName={params => (styles.disabledRow ? params.row.disabled : undefined)}
          rowHeight={GRID_ROW_HEIGHT}
          // TREE (ROW GROUPING)
          disableChildrenSorting
          getTreeDataPath={row => row.path}
          groupingColDef={groupingColDef}
          treeData={!!treeDataField}
          // MASTER DETAIL
          {...(isDetailPanelEnabled && {
            getDetailPanelContent,
            getDetailPanelHeight: () => 'auto'
          })}
          // PAGINATION
          pagination
          page={paginationProps?.page}
          pageSize={paginationProps?.rowsPerPage}
          rowsPerPageOptions={numOfRowsOptions.map(item => item.name as number)}
          onPageChange={newPage => {
            paginationProps?.onChangePage(null, newPage);
          }}
          onPageSizeChange={newPageSize => {
            if (paginationProps) paginationProps.rowsPerPage = newPageSize;
            const event = null as unknown as MouseEvent<HTMLLIElement>;
            toolbarProps?.onChangeNumOfRows?.(newPageSize)(event);
          }}
          onStateChange={onStateChange}
          // VIRTUALIZATION
          columnThreshold={2}
          rowThreshold={2}
          // MISC
          hideFooter
          sortingOrder={[GridSortDirections.DESC, GridSortDirections.ASC]}
          throttleRowsMs={40}
          {...otherProps}
          components={{
            BaseCheckbox: otherProps.loading ? SkeletonizedGridHeader : undefined,
            Cell: otherProps.loading ? SkeletonizedGridCell : GridCell,
            ColumnSortedAscendingIcon,
            ColumnSortedDescendingIcon,
            LoadingOverlay: () => null,
            NoRowsOverlay,
            Row: GridRow,
            ...otherProps.components
          }}
        />
      </MuiGrid>

      <MuiGrid
        container
        item
        hidden={otherProps.loading || paginationHidden || !paginationProps}
        className={clsx([styles.paginationContainer, classes?.paginationContainer])}
      >
        <MuiGrid ref={stickyPaginationRef} container className={styles.paginationContainerInner}>
          <MuiGrid container justifyContent="space-between">
            {paginationProps && (
              <Pagination
                classes={{ root: styles.paginationRoot }}
                {...paginationProps}
                count={paginationCount}
              />
            )}
            <BackToTop className={styles.backToTop} {...backToTopProps} />
          </MuiGrid>
        </MuiGrid>
      </MuiGrid>
    </MuiGrid>
  );
});

const m = memo(DataGrid);
export { m as DataGrid };
