import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { components as libraryComponents, GroupBase } from 'react-select';
import { ControlProps } from 'react-select/dist/declarations/src/components/Control';
import { MenuPortalProps } from 'react-select/dist/declarations/src/components/Menu';
import { BEM_CLASS, SSelect, SSelectStyledDiv } from './s-select';
import { SingleSelectItem } from './components';
import { TreeItemProps, TreeItemType, TreeList } from 'common/components/tree-list';
import { mapOptionsToNode } from '../multiselect/transducers';
import { bem } from 'utils/bem';
import { filterBySearchTextInOptionLabel } from 'common/transducers';
import { BaseOption, MenuListProps, MultiSelectProps, OptionComponentProps, OptionIdType, SelectProps } from './types';
import { filterOptionsByValue, flattenOptions, getCombinedOptions } from './transducers';
import { isMultiProps } from './guards';

const classes = bem(BEM_CLASS);

export const Select = <T extends BaseOption>(props: SelectProps<T> | MultiSelectProps<T>) => {
  const {
    className,
    options = [],
    value,
    defaultValue,
    onChange,
    isSearchable = false,
    isMulti = false,
    isRequired = false,
    menuWidthFromChildren = false,
    disabled,
    NoOptionsMessage = libraryComponents.NoOptionsMessage,
    dataSelector,
    components,
    tree,
    horizontalAlignment = 'left',
    inputValue,
    allOption,
    backspaceRemovesValue,
  } = props;
  const combinedOptions = useMemo(() => getCombinedOptions(allOption, options), [allOption, options]);

  const values = new Set(Array.isArray(value) ? value : [value]);
  const defaultValues = new Set(Array.isArray(defaultValue) ? defaultValue : [defaultValue]);

  const flattenedOptions = flattenOptions(combinedOptions);
  const cleanValue = filterOptionsByValue(flattenedOptions, values);
  const cleanDefaultValue = filterOptionsByValue(flattenedOptions, defaultValues);

  const isDefaultEmpty = !cleanDefaultValue || cleanDefaultValue.length <= 0;
  const isValueEmpty = !cleanValue || cleanValue.length <= 0;

  const shouldUseDefaults = isValueEmpty && !isDefaultEmpty;
  const normalizedValue = !shouldUseDefaults || isMulti ? cleanValue : cleanDefaultValue;

  if (shouldUseDefaults && cleanDefaultValue?.[0]?.id && !isMultiProps(props)) {
    props.onChange(cleanDefaultValue[0].id);
  }

  const filteredOptions = useMemo(
    () => combinedOptions.filter(({ name }) => filterBySearchTextInOptionLabel({ label: name }, inputValue)),
    [combinedOptions, inputValue]
  );

  const renderOption = useCallback(
    (
      {
        name,
        depth,
        onClick,
        isChecked,
        isHalfChecked,
        onExpand,
        isExpanded,
        isLeaf,
        isDisabled,
        isDivided = false,
      }: TreeItemProps,
      shouldExpandOnClick = false,
      tooltip?: string,
      disabledTooltip?: string
    ) => {
      const selectItem = () => onClick(!isChecked);
      const isSelected = isChecked || !!isHalfChecked;
      const shouldShowDivider = isDivided && depth === 0 && filteredOptions.length > 1;

      return (
        <>
          <div
            data-selector={`${dataSelector}-option`}
            className={classes('option', {
              'is-selected ': isSelected,
              'is-disabled': !!isDisabled,
            })}
          >
            <SingleSelectItem
              label={name}
              onExpand={onExpand}
              onClick={shouldExpandOnClick ? onExpand : selectItem}
              isExpandable={!isLeaf}
              depth={depth}
              isExpanded={isExpanded}
              tooltip={tooltip}
              disabledTooltip={disabledTooltip}
              isDisabled={isDisabled}
              isTree={tree}
              data-selector={dataSelector}
            />
          </div>
          {shouldShowDivider && <div className={classes('divider')} />}
        </>
      );
    },
    [dataSelector, filteredOptions.length, tree]
  );

  const treeList = useCallback(
    (menuProps: MenuListProps<T>) => {
      const {
        setValue,
        selectProps: { value, inputValue, defaultExpandedKeys },
        options,
      } = menuProps;
      const defaultExpandedIds = defaultExpandedKeys?.map(key => parseInt(key, 10)) || [];
      const mapValueToId = (value: T): OptionIdType[] => {
        const normalizeValue = (value: T | BaseOption): OptionIdType => (value?.id !== undefined ? value.id : -1);
        if (value.options && value.options.length > 0) {
          return value.options.map(normalizeValue);
        }
        return [normalizeValue(value)];
      };
      const onChangeTree = (values: number[]) => setValue(values[0], 'set-value');

      return (
        <TreeList
          onChange={onChangeTree}
          className={classes('tree')}
          value={value.flatMap(mapValueToId)}
          nodes={mapOptionsToNode(options)}
          searchQuery={inputValue}
          isSingle
          renderTreeItem={props =>
            renderOption(
              { ...props, isLeaf: props.isLeaf || !!inputValue },
              false,
              props.description,
              props.disabledDescription
            )
          }
          defaultExpandedIds={defaultExpandedIds}
          renderEmptyPlaceholder={() => <div>{menuProps.children}</div>}
          isExpandable
        />
      );
    },
    [renderOption]
  );

  const selectComponents = useMemo(
    () => ({
      NoOptionsMessage,
      Option: ({ data, isDisabled, isSelected, innerProps }: OptionComponentProps<T>) =>
        renderOption(
          {
            name: data.name || '',
            depth: 0,
            onClick: innerProps.onClick,
            isChecked: isSelected,
            isDisabled,
            isLeaf: true,
            isDivided: !!data.isDivided,
            type: TreeItemType.CHECKBOX,
          },
          false,
          data.description,
          data.disabledDescription
        ),
      Menu: props => (
        <libraryComponents.Menu
          {...props}
          innerProps={{
            ...props.innerProps,
            'data-selector': `${dataSelector}-menu`,
          }}
        />
      ),
      MenuList: tree ? treeList : libraryComponents.MenuList,
      MenuPortal: ({ children, ...rest }: MenuPortalProps<T, boolean, GroupBase<T>>) => (
        <libraryComponents.MenuPortal {...rest}>
          <SSelectStyledDiv {...rest.selectProps}>{children}</SSelectStyledDiv>
        </libraryComponents.MenuPortal>
      ),
      Control: (props: ControlProps<T>) => {
        const innerProps = { ...props.innerProps, 'data-selector': dataSelector };
        return (
          <libraryComponents.Control {...props} innerProps={innerProps}>
            {props.children}
          </libraryComponents.Control>
        );
      },
      ...components,
    }),
    [renderOption, tree, treeList, NoOptionsMessage, components, dataSelector]
  );

  const onSelectChange = useCallback(
    val => {
      const normalizeOnChangeValue = (value: T | OptionIdType): OptionIdType => {
        if (typeof value == 'number' || typeof value == 'string') return value;
        return value?.id !== undefined ? value.id : -1;
      };

      if (isMultiProps(props)) {
        props.onChange(Array.isArray(val) ? val.map(normalizeOnChangeValue) : []);
      } else if (!Array.isArray(val)) {
        props.onChange(normalizeOnChangeValue(val));
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [onChange, isMulti]
  );

  const onSelectBlur = useCallback(() => {
    isMultiProps(props) ? props.onBlur?.(props.value) : props.onBlur?.(props.value);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [props.onBlur, props.value, props.isMulti]);

  const getOptionLabel = useCallback(({ name }) => name, []);
  const getOptionValue = useCallback(({ id }) => id, []);

  return (
    <SSelect
      {...props}
      components={selectComponents}
      isDisabled={disabled}
      onChange={onSelectChange}
      options={combinedOptions}
      value={normalizedValue}
      onBlur={onSelectBlur}
      getOptionLabel={getOptionLabel}
      getOptionValue={getOptionValue}
      classNamePrefix="react-select"
      className={`react-select' ${className}`}
      filterOption={filterBySearchTextInOptionLabel}
      defaultValue={cleanDefaultValue}
      isSearchable={isSearchable}
      backspaceRemovesValue={!isSearchable && backspaceRemovesValue}
      horizontalAlignment={horizontalAlignment}
      isRequired={isRequired}
      menuWidthFromChildren={menuWidthFromChildren}
    />
  );
};
