import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from 'react';
import type { ElementType, HTMLAttributes, KeyboardEvent, ReactNode, SyntheticEvent } from 'react';
import { useToggle } from 'react-use';
import clsx from 'clsx';

import ClickAwayListener from '@mui/base/ClickAwayListener';
import MuiSelect from '@mui/material/Select';
import type { SelectProps as MuiSelectProps } from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import type { InputLabelProps } from '@mui/material/InputLabel';
import type { PaperProps } from '@mui/material/Paper';

import { ASSETS_URL } from '../../../consts/common';
import { CustomIcon } from '../../custom-icon';
import { MenuContent } from '../../dropdown-menu';
import { TextWithTooltip } from '../../text-with-tooltip';
import type { DropdownMenuItem, DropDownMenuProps, MenuContentProps } from '../../dropdown-menu';

import createClasses from './styles';
import { unstable_useControlled as useControlled } from '@mui/utils';

const textSeparator = ', ';

export interface SelectProps extends Omit<MuiSelectProps, 'defaultValue' | 'classes'> {
  /**
   * Props which will be passed into the menu content component.
   */
  menuContentProps: MenuContentProps;
  /**
   * The default element value. Use when the component is not controlled.
   */
  defaultValue?: string;
  classes?: Partial<ReturnType<typeof createClasses>>;
  chips?: boolean;
  /**
   * Whether to show a tooltip with a list of limited items.
   */
  showLimitItemsTooltip?: boolean;
  /**
   * Whether to show a select field clear button.
   */
  showSelectClearButton?: boolean;
  /**
   * A callback function to handle a select field click.
   */
  handleSelectClickCB?: () => void;
  /**
   * If `true`, the dropdown menu will be opened initially.
   * @default false
   */
  openOnce?: boolean;
  /**
   * This will set the maximum width of the dropdown menu.
   */
  menuMaxWidth?: number;
  /**
   * This will override the width of the dropdown menu.
   */
  menuWidth?: number;
}

interface PaperComponentProps extends PaperProps {
  selectProps: MuiSelectProps & Pick<SelectProps, 'menuMaxWidth' | 'menuWidth'>;
  menuContentProps: SelectProps['menuContentProps'];
}

interface SelectBaseProps extends SelectProps {
  selectOpen: boolean;
  toggleSelect: (nextValue?: unknown) => void;
}

let onClickAwayFlag = false;

const PaperComponent = forwardRef<HTMLElement, PaperComponentProps>((props, ref) => {
  const { selectProps, menuContentProps, style, ...paperProps } = props;

  const onClickAway = useCallback(() => {
    onClickAwayFlag = true;
  }, []);

  const menuMaxWidth = selectProps.menuMaxWidth ? selectProps.menuMaxWidth : undefined;
  const menuWidth = selectProps.menuWidth ? selectProps.menuWidth : undefined;

  return (
    <ClickAwayListener onClickAway={onClickAway}>
      <Box ref={ref} style={{ ...style, maxWidth: menuMaxWidth }} width={menuWidth} {...paperProps}>
        <MenuContent multiple={selectProps.multiple} {...menuContentProps} />
      </Box>
    </ClickAwayListener>
  );
}) as unknown as ElementType<HTMLAttributes<HTMLElement>>;

const SelectBase = (props: SelectBaseProps) => {
  const {
    defaultValue = '',
    placeholder,
    menuContentProps,
    MenuProps,
    label,
    selectOpen,
    menuWidth,
    showLimitItemsTooltip,
    showSelectClearButton,
    toggleSelect,
    handleSelectClickCB,
    onClick,
    classes,
    className,
    ...selectProps
  } = props;

  const [value, setValue] = useControlled({
    controlled: selectProps.value,
    default: defaultValue,
    name: selectProps.id || 'select'
  });

  const { listItems, defaultListItems, search } = menuContentProps;

  const { labelRoot, ...restClasses } = classes || {};

  const styles = createClasses({
    isValue: Array.isArray(selectProps.value) ? !!selectProps.value.length : !!selectProps.value
  });

  const withValue = useMemo(() => listItems?.every(item => 'value' in item), [listItems]);

  const handleSelectClear = useCallback(
    (event?: React.SyntheticEvent) => {
      event?.stopPropagation();
      menuContentProps?.clearSelectItems?.();
      setValue(void 0);
    },
    [menuContentProps, setValue]
  );

  const getSelectedValues: (value: unknown) => ReactNode = useCallback(
    (value: unknown) => {
      const findListItem = (value: DropdownMenuItem['value'] | boolean | null) => {
        const listItem = listItems.find(item => item.value === value);
        return listItem?.textNode || listItem?.name;
      };

      // single selection
      if (
        (typeof value === 'string' && value) ||
        typeof value === 'boolean' ||
        typeof value === 'number' ||
        value === null
      ) {
        if (withValue) {
          return findListItem(value);
        }

        return value;
      }

      // multiple selection
      if ((value as unknown[])?.length && listItems.length) {
        if (withValue) {
          return (value as DropdownMenuItem['value'][]).map(value => findListItem(value));
        }

        return value as string[];
      }

      return null;
    },
    [listItems, withValue]
  );

  const renderValue: MuiSelectProps['renderValue'] = useCallback(
    value => {
      const values = getSelectedValues(value);

      if (null === values) return placeholder;

      return (
        <>
          {showSelectClearButton && (
            <CustomIcon
              data-testid="clearButton"
              color="inherit"
              src={`${ASSETS_URL}/icons2/icon_closeCircle.svg`}
              className={clsx(styles.clearButton, classes?.clearButton)}
              onClick={handleSelectClear}
            />
          )}
          {showLimitItemsTooltip ? (
            <TextWithTooltip
              text={values || []}
              textSeparator={textSeparator}
              numOfLines={1}
              showLimitedItemsOnly
              textClassName={clsx(styles.truncatedText, classes?.truncatedText)}
            />
          ) : Array.isArray(values) ? (
            values.join(textSeparator)
          ) : (
            values
          )}
        </>
      );
    },
    [
      classes,
      getSelectedValues,
      handleSelectClear,
      placeholder,
      showLimitItemsTooltip,
      showSelectClearButton,
      styles.clearButton,
      styles.truncatedText
    ]
  );

  const handleClick = useCallback<NonNullable<DropDownMenuProps['handleClick']>>(
    (name, value) => e => {
      setValue(value);
      menuContentProps.handleClick?.(name, value)(e);
      if (!selectProps.multiple) {
        toggleSelect();
      }
    },
    [menuContentProps, selectProps, setValue, toggleSelect]
  );

  const processedListItems = useMemo(() => {
    const shouldBeSelected = Array.isArray(value)
      ? (item: DropdownMenuItem) => value.includes(item.value)
      : (item: DropdownMenuItem) => value === item.value;
    return withValue
      ? listItems.map(item => ({
          ...item,
          selected: shouldBeSelected(item)
        }))
      : listItems;
  }, [listItems, value, withValue]);

  // For accessibility
  const handleKeyPress = useCallback(
    (e: KeyboardEvent<HTMLDivElement>) => {
      switch (e.code) {
        case 'Enter':
        case 'Space':
          e.preventDefault();
          toggleSelect();
          break;
        default:
          onClickAwayFlag = false;
      }
    },
    [toggleSelect]
  );

  return (
    <MuiSelect
      tabIndex={0}
      displayEmpty
      data-testid="select"
      className={clsx(className, classes?.inputBaseRoot)}
      classes={{
        ...restClasses,
        select: clsx(styles.select, classes?.select)
      }}
      defaultValue={defaultValue}
      renderValue={renderValue}
      open={selectOpen}
      onClick={label ? undefined : onClick}
      onKeyPress={handleKeyPress}
      MenuProps={{
        ...MenuProps,
        anchorOrigin: {
          horizontal: 'left',
          vertical: 'bottom',
          ...MenuProps?.anchorOrigin
        },
        transformOrigin: {
          horizontal: 'left',
          vertical: -10,
          ...MenuProps?.transformOrigin
        },
        PaperProps: {
          className: clsx([styles.selectMenu, classes?.selectMenu]),
          // @ts-expect-error: https://github.com/mui/material-ui/issues/27703
          component: PaperComponent,
          defaultValue,
          selectProps: {
            menuWidth,
            ...selectProps
          },
          menuContentProps: {
            ...menuContentProps,
            listItems: onClickAwayFlag && search ? defaultListItems : processedListItems,
            handleClick
          },
          ...MenuProps?.PaperProps
        }
      }}
      {...selectProps}
      SelectDisplayProps={{
        tabIndex: -1,
        ...selectProps.SelectDisplayProps
      }}
      value={value}
    >
      {/* This part is only for material-ui's select values support */}
      {menuContentProps.listItems.map(item => {
        const { name, value } = item;
        return (
          <MenuItem
            key={`menuItem_${name}`}
            value={value !== undefined ? value : name}
            className={styles.menuItem}
          />
        );
      })}
    </MuiSelect>
  );
};

const Select = (props: SelectProps) => {
  const { openOnce = false, ...selectBaseProps } = props;
  const styles = createClasses({});

  const onOpenRef = useRef(props.onOpen);
  const [selectOpen, toggleSelect] = useToggle(openOnce);

  const handleSelectClick = useCallback(() => {
    if (!selectBaseProps.disabled) toggleSelect();
    selectBaseProps.handleSelectClickCB?.();
  }, [selectBaseProps, toggleSelect]);

  const handleLabelClick: InputLabelProps['onClick'] = useCallback(
    e => {
      e.preventDefault();
      // Using a label cause the menu to stay open even after selection, this is how we prevent this behavior.
      if (e.target.tagName !== 'INPUT') {
        handleSelectClick();
      }
    },
    [handleSelectClick]
  );

  useEffect(() => {
    openOnce && onOpenRef.current?.(new Event('click') as unknown as SyntheticEvent);
  }, [openOnce]);

  if (selectBaseProps.label) {
    return (
      <InputLabel
        classes={{
          root: clsx(styles.labelRoot, props.classes?.labelRoot)
        }}
        onClick={handleLabelClick}
      >
        {selectBaseProps.label}
        <SelectBase {...selectBaseProps} selectOpen={selectOpen} toggleSelect={toggleSelect} />
      </InputLabel>
    );
  }

  return (
    <SelectBase
      {...selectBaseProps}
      selectOpen={selectOpen}
      onClick={handleSelectClick}
      toggleSelect={toggleSelect}
    />
  );
};

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