import { forwardRef, LegacyRef, memo, Ref, useEffect, useMemo, useRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';

import { ListOnItemsRenderedProps, VariableSizeList } from 'react-window';
import { SortableElement as sortableElement } from 'react-sortable-hoc';
import InfiniteScroll from 'react-infinite-scroll-component';
import InfiniteLoader from 'react-window-infinite-loader';

import clsx from 'clsx';

import MuiList from '@mui/material/List';
import MuiListSubheader from '@mui/material/ListSubheader';

import MenuItem from './MenuItem';
import type { InfinityScrollProps, ListItemProps, MenuItemProps, SortableProps } from './types';

const LIST_ITEM_RECURSION_DEPTH_FACTOR = 1000000;
const VARIABLE_SIZE_LIST_ITEM_SIZE = 42;
const VARIABLE_SIZE_LIST_ITEM_MARGIN = 2;

const variableSizeListItemSize = (listItems: MenuItemProps[]) => (index: number) => {
  const optionsToRender = listItems[index]?.options?.filter(item => !item.hidden).length || 0;
  return VARIABLE_SIZE_LIST_ITEM_SIZE * (optionsToRender + 1);
};

const SortableItem = sortableElement(({ children }: SortableProps) => children);

const ListItems =
  // eslint-disable-next-line
  forwardRef<HTMLUListElement, ListItemProps>((props, ref) => {
    const {
      listItems,
      modifyMenuItem,
      InsertBeforeList,
      InsertAfterList,
      styles,
      classes,
      handleClick,
      multiple,
      sortable,
      deletable,
      handleDelete,
      showSelectedIcon,
      ariaLabel,
      noOptionsText,
      noResultsText,
      loadingText,
      variableSizeList,
      variableSizeListProps,
      infinityScrollProps
    } = props;

    const visibleItemsCount = useMemo(
      () => listItems.filter(item => !item.hidden).length,
      [listItems]
    );

    const renderNoResultsText = () =>
      noResultsText ? (
        <MuiListSubheader className={clsx(styles.menuListSubheader, classes?.menuListSubheader)}>
          {noResultsText}
        </MuiListSubheader>
      ) : null;

    const renderNoOptionsText = () =>
      noOptionsText ? (
        <MuiListSubheader className={clsx(styles.menuListSubheader, classes?.menuListSubheader)}>
          {noOptionsText}
        </MuiListSubheader>
      ) : null;

    const renderLoadingText = () =>
      loadingText ? (
        <MuiListSubheader className={clsx(styles.menuListSubheader, classes?.menuListSubheader)}>
          {loadingText}
        </MuiListSubheader>
      ) : null;

    const renderListItem = (item: MenuItemProps, index: number) => {
      const { id, name, disabled } = item;
      const key = `key_${id || name}${index}`;

      if (sortable) {
        return (
          <SortableItem index={index} key={key}>
            <MenuItem
              styles={styles}
              classes={classes}
              handleClick={handleClick}
              multiple={multiple}
              sortable={sortable}
              deletable={deletable}
              disabled={disabled}
              handleDelete={handleDelete}
              index={index}
              showSelectedIcon={showSelectedIcon}
              modifyMenuItem={modifyMenuItem}
              // eslint-disable-next-line react/jsx-props-no-spreading
              {...item}
            />
          </SortableItem>
        );
      }

      return (
        <MenuItem
          key={key}
          styles={styles}
          classes={classes}
          handleClick={handleClick}
          multiple={multiple}
          sortable={sortable}
          deletable={deletable}
          disabled={disabled}
          handleDelete={handleDelete}
          index={index}
          showSelectedIcon={showSelectedIcon}
          modifyMenuItem={modifyMenuItem}
          // eslint-disable-next-line react/jsx-props-no-spreading
          {...item}
        />
      );
    };

    const renderListItemRecursive =
      (recursionDepth = 0) =>
      (listItem: MenuItemProps, index: number): JSX.Element => {
        const { options, ...listItemProps } = listItem;

        return renderListItem(
          options
            ? {
                ...listItemProps,
                nestedListItems: options?.map(renderListItemRecursive(recursionDepth + 1)),
                options
              }
            : listItem,
          // avoiding index collisions
          index + LIST_ITEM_RECURSION_DEPTH_FACTOR * recursionDepth
        );
      };

    const renderListChild = (props: {
      index: number;
      style: CSSProperties;
      data: ListItemProps['listItems'];
    }) => {
      const { index, style, data } = props;
      const listItem = data[index];
      const styleHeight = parseFloat(style.height as string) - VARIABLE_SIZE_LIST_ITEM_MARGIN;

      if (infinityScrollProps && data[index] === undefined) {
        return (
          <li style={{ ...style, height: styleHeight }}>
            <div className={clsx(styles.menuListSubheader, classes?.menuListSubheader)}>
              Loading...
            </div>
          </li>
        );
      }
      return renderListItemRecursive()(
        { ...listItem, style: { ...style, height: styleHeight } },
        index
      );
    };

    function useResetCache(data: number) {
      const ref = useRef<VariableSizeList>(null);
      useEffect(() => {
        ref.current?.resetAfterIndex(0, true);
      }, [data]);
      return ref;
    }

    const vslRef = useResetCache(visibleItemsCount);

    if (variableSizeList) {
      const innerElementType = forwardRef((props, ref: LegacyRef<HTMLUListElement>) => (
        <ul
          ref={ref}
          className={clsx(styles.variableSizeListInner, classes?.variableSizeListInner)}
          {...props}
        />
      ));

      const VariableSizeListWrapper = (props: {
        // eslint-disable-next-line
        refValue: Ref<any>;
        itemCount: number;
        // eslint-disable-next-line
        onItemsRendered?: (props: ListOnItemsRenderedProps) => any;
      }) => {
        return (
          <>
            {InsertBeforeList?.()}
            <VariableSizeList
              {...props}
              ref={props.refValue}
              width="100%"
              children={
                listItems.length < 1 && noOptionsText ? renderNoOptionsText : renderListChild
              }
              innerElementType={innerElementType}
              itemData={listItems}
              itemSize={variableSizeListItemSize(listItems)}
              className={clsx(
                'variable-size-list-wrapper',
                styles.variableSizeList,
                classes?.variableSizeList
              )}
              style={{
                height: 'auto',
                maxHeight: variableSizeListHeight
              }}
              {...variableSizeListProps}
              height={variableSizeListHeight}
            />
            {InsertAfterList?.()}
          </>
        );
      };

      const variableSizeListHeight =
        variableSizeListProps?.height || VARIABLE_SIZE_LIST_ITEM_SIZE * 6;

      if (infinityScrollProps) {
        const { allItemsCount, fetchFunction, threshold = 2 } = infinityScrollProps;
        const nextPortionExists = listItems.length < allItemsCount;

        const itemCount =
          listItems.length < 1 && noOptionsText
            ? 1
            : nextPortionExists
            ? listItems.length + 1
            : listItems.length;
        const isItemLoaded = (index: number) => !nextPortionExists || index < listItems.length;

        return (
          <InfiniteLoader
            isItemLoaded={isItemLoaded}
            itemCount={itemCount}
            loadMoreItems={fetchFunction}
            threshold={threshold}
          >
            {({ onItemsRendered, ref }) =>
              VariableSizeListWrapper({ itemCount, refValue: ref, onItemsRendered })
            }
          </InfiniteLoader>
        );
      }

      const itemCount = listItems.length < 1 && noOptionsText ? 1 : listItems.length;
      return <VariableSizeListWrapper itemCount={itemCount} refValue={vslRef} />;
    }

    const InfiniteScrollWrapper = (props: {
      infinityScrollProps: InfinityScrollProps;
      listItemsLength: number;
      children: ReactNode;
    }) => {
      const { allItemsCount, fetchFunction, threshold = 2 } = props.infinityScrollProps;
      const nextPortionExists = listItems.length < allItemsCount;

      return (
        <InfiniteScroll
          dataLength={props.listItemsLength}
          next={fetchFunction}
          hasMore={nextPortionExists}
          loader={renderLoadingText()}
          scrollableTarget="scrollableDiv"
          scrollThreshold={(listItems.length - threshold) / listItems.length}
        >
          {props.children}
        </InfiniteScroll>
      );
    };

    return (
      <MuiList
        ref={ref}
        component="ul"
        tabIndex={-1}
        aria-label={ariaLabel}
        role="menubar"
        className={clsx(styles.menuList, classes?.menuList)}
        id="scrollableDiv"
      >
        {InsertBeforeList?.()}
        {listItems.length > 0 ? (
          <>
            {visibleItemsCount < 1 ? renderNoResultsText() : null}
            {infinityScrollProps ? (
              <InfiniteScrollWrapper
                infinityScrollProps={infinityScrollProps}
                listItemsLength={listItems.length}
              >
                {listItems.map(renderListItemRecursive())}
              </InfiniteScrollWrapper>
            ) : (
              listItems.map(renderListItemRecursive())
            )}
          </>
        ) : (
          renderNoOptionsText()
        )}
        {InsertAfterList?.()}
      </MuiList>
    );
  });

export default memo(ListItems);
