import clsx from 'clsx';
import { LocationDescriptor } from 'history';
import React, {
  PropsWithChildren,
  ReactElement,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { ActionData } from '..';
import { noop } from '../../helpers/utils';
import { Data } from '../../types/data';
import { ButtonContext } from '../Buttons/Button.model';
import { TextButton } from '../Buttons/TextButton/TextButton';
import {
  Column,
  ItemSelectEventArgs,
  ListElement,
  ListItem,
  ListSelectMode,
  SortData,
} from './List.model';
import classes from './List.scss';
import { ListHeader } from './ListHeader/ListHeader';
import { ListRowProps } from './ListRow/ListRow';
import { ListRowLoader } from './ListRow/ListRowLoader';
import { ListRowRenderer } from './ListRowRenderer/ListRowRenderer';
import { getActionButtonVisibility, isTrigger, useSort } from './helpers';
import { useColumnsSize } from './useColumnsSize';

export interface ListProps<T extends Data> {
  /**
   * Column definitions
   * The order of this array determines the order of columns
   */
  columns: Column<T>[];
  /** The name of the property on the data object that should be used to identify objects (default: 'id')  */
  keyProperty?: keyof T;
  /** List data */
  data: T[];
  /** Defines whether data is being loaded (default: false) */
  isLoading?: boolean;
  /** Defines whether there was an error with the data. If true a button will be shown that will trigger `onRetry` when hit. */
  isError?: boolean;
  /** Optional error message to display if an error occurs. (default: 'There was an error.) */
  errorMsg?: string;
  /** Determines whether the component should render the error message and the 'onRetry' button. Set this to false if error handling will be done outside this component. (default: true) */
  handleRetry?: boolean;
  /** Minimum width of this component */
  minimumWidth?: string;
  /** Spacing between columns */
  columnGap?: string;
  /** Header row height */
  headerRowHeight?: string;
  /** List row height */
  listRowHeight?: string;
  /** List row action button and checkbox size (default: '50px') */
  listRowActionSize?: string;
  /** Header checkbox size (default: '28px') */
  headerRowActionSize?: string;
  /** Spacing between rows */
  rowGap?: string;
  /** Horizontal alignment of text */
  horizontalTextAlign?: 'left' | 'right' | 'center';
  /** Vertical alignment of text */
  verticalTextAlign?: 'start' | 'center' | 'end';
  /** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
  textWrap?: boolean;
  /**
   * Determines which select mode the list is in. (default: ListSelectMode.None)
   * If 'Single' is selected, a check mark will be rendered for each row of data
   * If 'Multi' is selected, a checkbox will be rendered for the header and each row of data
   */
  selectionMode?: ListSelectMode;
  /** In multiselect mode, whether the Select All button will be displayed. When Select All button is clicked the onItemClicked button will return SELECT_ALL instead of an array of items. (default: true) */
  enableSelectAll?: boolean;
  /** Sets whether deselection should be allowed in multiselect mode when select all is enabled. This would allow the user to deselect items after select all checkbox is checked. Once an item is deselected, the list will switch to a SINGLE_ITEMS selection mode. */
  enableSelectAllDeselect?: boolean;
  /**
   * Defines whether an action button will be rendered (default: true)
   * Will not render if selectMode is not 'None'
   */
  showActionButton?: boolean | ((data: T) => boolean);
  /** Defines when the loading of the next page is triggered. The number represents the number of row left, before a load is triggered. (default: 10) */
  loadingTriggerOffset?: number;
  /** Defines how the list data should be sorted */
  defaultSortOrder?: SortData<T>;
  /**
   * When set, this function is used to generate the link that the user should be navigated to for each item.
   *
   * This property takes precedence over `onItemClicked`, which will not be executed in case `generateItemLink` is set!
   */
  generateItemLink?: (data: T) => LocationDescriptor<unknown>;
  /**
   * Raised when an data item is clicked.
   *
   * This is not being used, if generateItemLink is set!
   */
  onItemClicked?: (data: T) => void | Promise<void>;
  /** Raised when item selection has changed */
  onItemSelected?: (itemSelectEvent: ItemSelectEventArgs<T>) => void;
  /** Raised when list has scrolled down to the item indicated by loadingTriggerOffset */
  onRequestMoreData?: () => void;
  /** Callback to emit when a sort event occurs. Name of the property and order of the direction are sent back. */
  onSortChanged?: (sort: SortData<T>) => void;
  /** Callback emitted if the user clicks on the retry button which is shown when `isError` is set to true */
  onRetry?: () => void;
  /** CSS Class name for additional styles */
  className?: string;
  /** Provide inline actions which are available through '...' context menu */
  inlineMenuActions?: (data: T) => ActionData[];
  /** Function to determine if a specific row should be disabled */
  isRowDisabled?: (data: T, index: number) => boolean;
  /** Custom row renderer - if it returns null, the default ListRow will be used */
  customRowRenderer?: (props: ListRowProps<T>) => ReactElement | null;
}

const ListRenderer = <T extends Data>(
  {
    columns,
    data = [],
    isLoading = false,
    isError = false,
    errorMsg = 'There was an error.',
    handleRetry = true,
    minimumWidth = '500px',
    columnGap = '5px',
    rowGap = '0px',
    headerRowHeight = '44px',
    listRowHeight,
    listRowActionSize = '50px',
    headerRowActionSize = '28px',
    horizontalTextAlign = 'left',
    verticalTextAlign = 'center',
    keyProperty = 'id' as keyof T,
    showActionButton = true,
    loadingTriggerOffset = 10,
    defaultSortOrder,
    selectionMode = ListSelectMode.None,
    enableSelectAll = true,
    enableSelectAllDeselect = false,
    textWrap,
    onItemClicked = noop,
    onItemSelected = noop,
    onRequestMoreData = noop,
    onSortChanged = noop,
    onRetry = noop,
    generateItemLink,
    className = '',
    inlineMenuActions,
    isRowDisabled,
    customRowRenderer,
  }: PropsWithChildren<ListProps<T>>,
  ref?: React.ForwardedRef<ListElement>,
): JSX.Element => {
  const [listItems, setListItems] = useState<ListItem<T>[]>([]);
  const isAllItemsChecked = useRef<boolean>(false);

  useEffect(() => {
    setListItems((prevState: ListItem<T>[]) => {
      const newListItems: ListItem<T>[] = data.map((i, index) => ({
        selected: isAllItemsChecked.current || prevState[index]?.selected,
        data: i,
      }));
      return newListItems;
    });
  }, [data]);

  const { columnSizes, resetColumnSizes, setColumnSizes, hasActionColumn } =
    useColumnsSize(
      columns,
      Boolean(showActionButton),
      selectionMode,
      Boolean(inlineMenuActions),
    );

  const customStyles = {
    gridRowGap: rowGap,
    minWidth: minimumWidth,
    width: '100%',
  };

  const itemSelectionHandler = useCallback(
    (items: ListItem<T>[]) => {
      const selectedData: T[] = items.map(({ data }) => data);
      if (enableSelectAllDeselect) {
        if (items.length === listItems.length) {
          isAllItemsChecked.current = true;
        } else {
          isAllItemsChecked.current = false;
        }
      }
      onItemSelected &&
        onItemSelected({
          mode: 'SINGLE_ITEMS',
          items: selectedData,
        });
    },
    [enableSelectAllDeselect, listItems.length, onItemSelected],
  );

  const headerCheckboxHandler = useCallback(
    (checked: boolean): void => {
      setListItems((prevState) =>
        prevState.map((i) => ({
          selected: checked,
          data: i.data,
        })),
      );

      isAllItemsChecked.current = checked;

      if (checked) {
        onItemSelected({
          mode: 'SELECT_ALL',
        });
      } else {
        onItemSelected({
          mode: 'SINGLE_ITEMS',
          items: [],
        });
      }
    },
    [onItemSelected],
  );

  const itemSelectedHandler = useCallback(
    (selected: boolean, index: number): void => {
      const items = [...listItems];
      items[index].selected = selected;
      setListItems(items);
      itemSelectionHandler(items.filter((item) => item.selected === true));
    },
    [itemSelectionHandler, listItems],
  );

  const onTriggeredHandler = (): void => {
    if (!isLoading && !isError) {
      onRequestMoreData();
    }
  };

  const itemClickedHandler = async (data: T, index: number): Promise<void> => {
    try {
      await onItemClicked(data);

      if (selectionMode === ListSelectMode.Single) {
        setListItems((prevState) =>
          prevState.map((item, i) => ({
            selected: i === index,
            data: item.data,
          })),
        );
      }
    } catch (error) {
      return;
    }
  };

  const { sort, sortChangedHandler } = useSort(defaultSortOrder, onSortChanged);

  useImperativeHandle(ref, () => ({
    resetSelection: () => {
      setListItems((prevState) =>
        prevState.map((i) => ({
          selected: false,
          data: i.data,
        })),
      );

      isAllItemsChecked.current = false;

      onItemSelected({
        mode: 'SINGLE_ITEMS',
        items: [],
      });
    },
    selectIndex: (index: number) => {
      if (selectionMode === ListSelectMode.Single) {
        itemClickedHandler(listItems[index].data, index);
      } else {
        itemSelectedHandler(true, index);
      }
    },
  }));

  useEffect(() => {
    if (selectionMode !== ListSelectMode.Multi) {
      setListItems((prevState) =>
        prevState.map((i) => ({
          selected: false,
          data: i.data,
        })),
      );
    }
  }, [selectionMode]);

  const getRowDisabledState = useCallback(
    (item: ListItem<T>, index: number): boolean => {
      const isDisabledDueToSelectAll =
        isAllItemsChecked.current &&
        !enableSelectAllDeselect &&
        selectionMode === ListSelectMode.Multi;

      const isDisabledByCustomLogic =
        isRowDisabled?.(item.data, index) ?? false;

      return isDisabledDueToSelectAll || isDisabledByCustomLogic;
    },
    [enableSelectAllDeselect, selectionMode, isRowDisabled],
  );

  return (
    <div
      className={clsx(classes.wrapper, 'list-wrapper', className)}
      style={customStyles}
      data-test-id="list"
    >
      {/* Header */}
      <ListHeader
        columns={columns}
        itemSelected={isAllItemsChecked.current}
        sortData={sort}
        columnSizes={columnSizes}
        columnGap={columnGap}
        rowHeight={headerRowHeight}
        actionSize={headerRowActionSize}
        horizontalTextAlign={horizontalTextAlign}
        verticalTextAlign={verticalTextAlign}
        showItemCheckbox={
          enableSelectAll &&
          selectionMode === ListSelectMode.Multi &&
          listItems.length > 0
        }
        isCheckboxDisabled={listItems.length === 0}
        onCheckboxToggled={headerCheckboxHandler}
        onSortChanged={sortChangedHandler}
        onResetColumnSizes={resetColumnSizes}
        onColumnSizesChanged={setColumnSizes}
        hasActionColumn={hasActionColumn}
      />

      {/* Rows */}
      {listItems.map((item: ListItem<T>, index) => (
        <ListRowRenderer<T>
          key={String(item.data[keyProperty] || index)}
          customRowRenderer={customRowRenderer}
          isTrigger={isTrigger<T>(index, listItems, loadingTriggerOffset)}
          onTriggered={onTriggeredHandler}
          listRowProps={{
            columns,
            data: item.data,
            itemSelected: item.selected,
            isRowDisabled: getRowDisabledState(item, index),
            columnSizes,
            columnGap,
            rowHeight: listRowHeight,
            actionSize: listRowActionSize,
            horizontalTextAlign,
            verticalTextAlign,
            textWrap,
            selectionMode,
            showActionButton:
              selectionMode === ListSelectMode.None &&
              getActionButtonVisibility(item.data, showActionButton),
            showCheckMark: selectionMode === ListSelectMode.Single,
            showItemCheckbox: selectionMode === ListSelectMode.Multi,
            onItemClicked: generateItemLink
              ? generateItemLink(item.data)
              : (data: T) => itemClickedHandler(data, index),
            onItemSelected: (checked: boolean) =>
              itemSelectedHandler(checked, index),
            inlineMenuActions,
          }}
        />
      ))}

      {isLoading && (
        <ListRowLoader
          columnSizes={columnSizes}
          columnGap={columnGap}
          rowHeight={listRowHeight}
          columns={columns}
        />
      )}
      {noItemsMessage(listItems.length, isLoading, isError)}
      {isError && handleRetry && (
        <div className={clsx(classes.error)}>
          <p>{errorMsg}</p>
          <TextButton
            text={'Retry'}
            height={listRowActionSize}
            width={'100px'}
            buttonContext={ButtonContext.Context}
            onButtonClicked={() => onRetry()}
          />
        </div>
      )}
    </div>
  );
};

/**
 * Renders various sets of data in a tabular format
 * @example
 * <List<DataInterface>
 *  columns={[{propertyName: 'id', size: '1fr', label: 'Id'}]}
 *  data={[{id: '1',desc: 'Description 1',title: 'Item 1'}]}
 *  itemClicked={(item)=> {console.log(item)}}
 * />
 */
export const List = React.forwardRef(ListRenderer);

const noItemsMessage = (
  itemsCount: number,
  isLoading: boolean,
  isError: boolean,
): ReactElement | undefined => {
  if (!isLoading && !isError && itemsCount === 0) {
    return (
      <div className={clsx(classes.NoData)} data-test-id="list-empty">
        <p>No items found</p>
      </div>
    );
  }
};
