/* eslint-disable react/jsx-props-no-spreading */
import FocusTrap from 'focus-trap-react';
import { ReactNode, useCallback, useMemo, useRef, useState } from 'react';

import { Dropdown, DropdownProps } from '../../components/Dropdown/Dropdown';
import { TriggerButtonContextProvider } from '../../components/TriggerButton/contexts/TriggerButtonContext/TriggerButtonContext';
import { useId } from '../../hooks/useId';
import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { makeTestId } from '../../utils/makeTestId';
import { GeneralFormControlProps } from '../types';

import { Menu, MenuProps } from './components/Menu/Menu';
import { SelectFieldTrigger } from './components/SelectFieldTrigger/SelectFieldTrigger';
import { defaultOptionComponent } from './optionComponent';
import { StyledSelectContainer } from './styled';
import { SelectOption } from './types';

/** General {@link SelectField} props that not depends on any optional parameters */
export type GeneralSelectFieldProps<Option extends SelectOption> = Omit<
  GeneralFormControlProps<any, any>,
  'value' | 'onChange'
> & {
  /**
   * Custom trigger button that will replace default.
   *
   * Trigger button should extend {@link TriggerButton} component.
   *
   * ```tsx
   * const StyledTriggerButton = styled(TriggerButton)`
   *   background-color: blue;
   * `;
   *
   * <SelectField trigger={<StyledTriggerButton />} />
   * ```
   */
  trigger?: ReactNode;
  /** CSS classes that will be appended to select container */
  className?: string;
  /** If `true` that search input in select will be hidden */
  hideSearch?: boolean;
  /** Text that will be shown in default trigger button when {@link SelectFieldProps.value} is `undefined` */
  placeholder?: string;
  /** Text that will be shown in search input when it is empty */
  searchPlaceholder?: string;
  /**
   * Options of a custom option component
   *
   * ```tsx
   * // Example option component that can be used in this prop.
   * const Option = (props: { option: CountryOption }) => {
   *   return (
   *     <>
   *       <OptionFlag country={props.option.value} />
   *       <OptionText>{props.option.label}</OptionText>
   *     </>
   *   );
   * };
   *
   * <StyledSelect
   *   optionComponent={{ renderer: Option, height: 32 }}
   * />
   * ```
   */
  optionComponent?: MenuProps<Option>['optionComponent'];
  /**
   * Set options component with a specific width.
   */
  optionComponentWidth?: number;
  /** Message that will be shown in select when no options should be visible */
  noOptionsMessage?: string;
};

/**
 * Variant of {@link SelectField} props that describe static (non async) behaviour,
 * when all options are exists when {@link SelectField} was created.
 */
export interface SelectFieldNonAsyncOptionsVariant<Option extends SelectOption> {
  async?: false;
  /** List of options that should be displayed in {@link SelectField} dropdown menu */
  options: Option[];
  /**
   * Function that used for searching option in options list. This function receive
   * `option` and should return any string that should be used for searching this
   * option.
   *
   * ```tsx
   * <SelectField
   *   optionToSearchString={(option) => `${option.value}-${option.label}`}
   * />
   * ```
   *
   * Default: `(option) => `${option.label} ${option.value}``
   */
  optionToSearchString?: (option: Option) => string;
}

/**
 * Variant of {@link SelectField} props that describe async behaviour, when
 * list of options can be loaded dynamically.
 */
export interface SelectFieldAsyncOptionsVariant<Option extends SelectOption> {
  async: true;
  /** Total number of options that should be displayed in dropdown menu */
  totalOptions: number;
  /**
   * List of options of the previous options page.
   *
   * If this list is empty and user scrolls to this the previous page,
   * then {@link Skeleton} will be displayed instead options.
   */
  optionsPrevPage: Option[];
  /**
   * List of options of the current options page
   *
   * If this list is empty and user scrolls to this the current page,
   * then {@link Skeleton} will be displayed instead options.
   */
  optionsCurrentPage: Option[];
  /**
   * List of options of the next options page
   *
   * If this list is empty and user scrolls to this the next page,
   * then {@link Skeleton} will be displayed instead options.
   */
  optionsNextPage: Option[];
  /**
   * List of options that was selected (displayed in trigger button).
   *
   * The {@link SelectField} will try to match all {@link SelectFieldProps.value}
   * to value from this list, if this value is not exist the {@link Skeleton}
   * will be displayed instead.
   */
  selectedOptions: Option[];
  /**
   * Function that should be called when user scroll {@link SelectField} menu
   * to the next page, or when they change filter value.
   *
   * Component that use async {@link SelectField} should it this prop
   * handler returns new set of:
   * - {@link SelectFieldAsyncOptionsVariant.optionsPrevPage}
   * - {@link SelectFieldAsyncOptionsVariant.optionsCurrentPage}
   * - {@link SelectFieldAsyncOptionsVariant.optionsNextPage}
   *
   * @param page Current page that displayed in {@link SelectField} menu
   * @param filter Current {@link SelectField} menu filter
   */
  onOptionsPageChanged: (page: number, filter: string) => void;
  /**
   * Optional value that allow to configure how many options should be displayed
   * per one {@link SelectField} menu page.
   *
   * @default 20
   */
  optionsPerPage?: number;
}

/** Combination of supported by {@link SelectField} async/non async behaviours */
export type SelectFieldOptions<Option extends SelectOption> =
  | SelectFieldNonAsyncOptionsVariant<Option>
  | SelectFieldAsyncOptionsVariant<Option>;

/**
 * Variant of single option select {@link SelectField} behaviour.
 *
 * In this variant of {@link SelectField} user can select only zero or one
 * option.
 */
export interface SelectFieldSingleValueVariant<Option extends SelectOption> {
  multiple?: false;
  /**
   * If this prop is `true` then {@link SelectField} will contain clear button
   * that allow to reset value to the `null`.
   */
  cleanable?: boolean;
  /**
   * Selected option value.
   *
   * If `null` given then {@link GeneralSelectFieldProps.placeholder} text will
   * be displayed on trigger button.
   */
  value?: Option['value'] | null;
  /** Function that will be invoked when user selects other option */
  onChange?: (value: Option['value'] | null) => void;
}

/**
 * Variant of multiple options select {@link SelectField} behaviour.
 *
 * In this variant of {@link SelectField} user can select zero or more
 * options in the same time.
 */
export interface SelectFieldMultipleValueVariant<Option extends SelectOption> {
  multiple: true;
  /**
   * List of selected options values.
   *
   * If empty list given then {@link GeneralSelectFieldProps.placeholder} text will
   * be displayed on trigger button.
   */
  value?: Option['value'][];
  /** Function that will be invoked when user selects other options */
  onChange?: (value: Option['value'][]) => void;
}

/** Combination of supported by {@link SelectField} selection behaviours */
export type SelectFieldValue<Option extends SelectOption> =
  | SelectFieldSingleValueVariant<Option>
  | SelectFieldMultipleValueVariant<Option>;

/** Props of {@link SelectField} */
export type SelectFieldProps<Option extends SelectOption> = GeneralSelectFieldProps<Option> &
  SelectFieldOptions<Option> &
  SelectFieldValue<Option>;

/**
 * Dropdown select that allow to select one value from given list.
 *
 * ```tsx
 * import { SelectField, SelectOption } from 'ui-kit';
 *
 * const options: SelectOption[] = [
 *   {
 *     label: 'Fist option',
 *     value: 'one'
 *   },
 *   {
 *     label: 'Second option',
 *     value: 'two'
 *   },
 * ];
 *
 * <SelectField value="one" onChange={console.log} options={options} />
 * ```
 *
 * ## Option selection behaviours
 *
 * The {@link SelectField} support single and multiple selection behaviour
 * by changing `multiple` prop.
 *
 * ### Single option selection
 *
 * The {@link SelectField} support selection only zero or one option if
 * `multiple` parameter is `false.
 *
 * In this behaviour {@link SelectField} accepts  only zero (`null`) or one
 * option value in the same time. When user selects other option the previous
 * will be replaced.
 *
 * ```tsx
 * <SelectField
 *   value="ST"
 *   options={[
 *     {value: 'MM', label: 'MessageMedia'},
 *     {value: 'ST', label: 'SimpleTexting'},
 *   ]}
 * />
 * ```
 *
 * ### Cleanable
 *
 * The {@link SelectField} support `Clear selection` button if `cleanable`
 * parameter is `true`.
 *
 * <Story id="form-selectfield--cleanable" />
 *
 * ```tsx
 * <SelectField
 *   value="ST"
 *   options={[
 *     {value: 'MM', label: 'MessageMedia'},
 *     {value: 'ST', label: 'SimpleTexting'},
 *   ]}
 *   cleanable
 * />
 * ```
 *
 * ### Multiple options selection
 *
 * The {@link SelectField} supports selection zero or more options in the same
 * time if `multiple` parameter is `true`.
 *
 * In this behaviour {@link SelectField} accepts zero (when value list is empty)
 * or more options values in the same time. When user selects other option it
 * will be added (if it is not list) to options list or removed (if it in list)
 * from options list.
 *
 * ```tsx
 * <SelectField
 *   multiple
 *   value={['ST', 'MM']}
 *   options={[
 *     {value: 'MM', label: 'MessageMedia'},
 *     {value: 'ST', label: 'SimpleTexting'},
 *   ]}
 * />
 * ```
 *
 * ## Async and not async options behaviours
 *
 * The {@link SelectField} supports async options loading by passing `async`
 * props.
 *
 * ### Non async behaviour
 *
 * If `async` prop is `false` then {@link SelectField} expects list of available
 * options immediately. That's mean that you should provide all available options
 * when you use {@link SelectField}.
 *
 * ```tsx
 * <SelectField
 *   options={[
 *     {value: 'MM', label: 'MessageMedia'},
 *     {value: 'ST', label: 'SimpleTexting'},
 *   ]}
 * />
 * ```
 *
 * #### Filtering
 *
 * In non async behaviour {@link SelectField} supports options filtering.
 * User can pass any string in menu filter field and only options that pass
 * this filter will be displayed.
 *
 * User can configure filtering behaviour by
 * {@link SelectFieldNonAsyncOptionsVariant.optionToSearchString}
 * props. User can pass any function that should return filter string for given
 * option and this when user type filter query to filter field this string will
 * be used for finding matched options.
 *
 * ```tsx
 * <SelectField
 *   options={[
 *     {value: 'MM', label: 'MessageMedia'},
 *     {value: 'ST', label: 'SimpleTexting'},
 *   ]}
 *   optionToSearchString={(option) => option.value}
 * />
 * ```
 *
 * ### Async behaviour
 *
 * The {@link SelectField} supports async option loading by passing `true` to
 * `async` prop. This is much more complex than non async behaviour and
 * should be used carefully.
 *
 * When user uses async behaviour {@link SelectField} options should be loaded
 * dynamically and until some option is not loaded {@link Skeleton} component
 * will be displayed.
 *
 * To allow {@link SelectField} works in async mode user should pass set of
 * props:
 * - {@link SelectFieldAsyncOptionsVariant.selectedOptions}
 * - {@link SelectFieldAsyncOptionsVariant.optionsPrevPage}
 * - {@link SelectFieldAsyncOptionsVariant.optionsCurrentPage}
 * - {@link SelectFieldAsyncOptionsVariant.optionsNextPage}
 * - {@link SelectFieldAsyncOptionsVariant.onOptionsPageChanged}
 *
 * The first prop {@link SelectFieldAsyncOptionsVariant.selectedOptions} should
 * contain list of option that should be displayed in trigger button. If some of
 * given {@link SelectFieldProps.value} is not exist in given
 * {@link SelectFieldAsyncOptionsVariant.selectedOptions} the
 * {@link Skeleton} will be displayed instead this value.
 *
 * The second three props {@link SelectFieldAsyncOptionsVariant.optionsPrevPage},
 * {@link SelectFieldAsyncOptionsVariant.optionsCurrentPage} and
 * {@link SelectFieldAsyncOptionsVariant.optionsNextPage} should contains
 * list of options for previous, current and next {@link SelectField} menu page.
 * If some page list is not passed, then {@link Skeleton} will be displayed
 * instead options of this list.
 *
 * The last prop {@link SelectFieldAsyncOptionsVariant.onOptionsPageChanged}
 * should contain function that will be invoked when user scroll
 * {@link SelectField} menu and showed page is changed. Also, this function will
 * be invoked when user type some filter sting. This function should set new
 * set of this prop for current page:
 * - {@link SelectFieldAsyncOptionsVariant.optionsPrevPage}
 * - {@link SelectFieldAsyncOptionsVariant.optionsCurrentPage}
 * - {@link SelectFieldAsyncOptionsVariant.optionsNextPage}
 *
 * ```tsx
 * // Returns some page of options
 * function useOptionsQuery(page: number, filter: string): Option[] {
 *   // ...
 * }
 *
 * // Returns selected options
 * function useCurrentSelectedOptions(): Option[] {
 *   // ...
 * }
 *
 * const [currentPage, setCurrentPage] = useState(0);
 * const [filter, setFilter] = useState('');
 *
 * const selectedOptions = useCurrentSelectedOptions();
 *
 * const prevPage = useOptionsQuery(currentPage - 1, filter);
 * const currentPage = useOptionsQuery(currentPage, filter);
 * const nextPage = useOptionsQuery(currentPage + 1, filter);
 *
 * const handleOptionsPageChanged = (page: number, filter: string) => {
 *   setCurrentPage(page);
 *   setFilter(filter);
 * }
 *
 * <SelectField
 *   selectedOptions={selectedOptions}
 *   optionsPrevPage={prevPage}
 *   optionsCurrentPage={currentPage}
 *   optionsNextPage={nextPage}
 *   onOptionsPageChanged={handleOptionsPageChanged}
 * />
 * ```
 */
export function SelectField<Option extends SelectOption = SelectOption>(props: SelectFieldProps<Option>) {
  const {
    id,
    trigger,
    className,
    hideSearch,
    disabled,
    searchPlaceholder,
    onBlur,
    placeholder,
    optionComponent = defaultOptionComponent,
    optionComponentWidth,
    onChange,
    noOptionsMessage,
    ariaInvalid,
    ariaLabel,
    ariaErrorMessage,
    ariaDescribedBy,
    testId,
    ...rest
  } = props;

  const dropdownId = useId();
  const [isExpanded, setIsExpanded] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const testIdAttribute = useTestIdAttribute();

  const closeMenu = useCallback(() => {
    setIsExpanded(false);
  }, []);

  const handleTriggerButtonClick = useCallback(() => {
    setIsExpanded((v) => !v);
  }, []);

  const handleTriggerBlur = useCallback(() => {
    if (onBlur && !isExpanded) {
      onBlur();
    }
  }, [isExpanded, onBlur]);

  const handleOptionChange = useCallback(
    (value: any) => {
      if (onChange) {
        onChange(value);
      }
    },
    [onChange],
  );

  const focusTrapOptions = useMemo<FocusTrap.Props['focusTrapOptions']>(() => {
    return {
      allowOutsideClick: true,
    };
  }, []);

  const popperOptions = useMemo<DropdownProps['popperOptions']>(() => {
    return {
      placement: 'bottom-start',
      modifiers: [
        {
          name: 'sameWidth',
          enabled: true,
          phase: 'beforeWrite',
          requires: ['computeStyles'],
          fn: ({ state, instance }) => {
            const newPopperWidth = `${Math.max(
              optionComponentWidth ?? state.rects?.reference?.width,
              200,
            )}px`;

            if (state.styles.popper.width !== newPopperWidth) {
              state.styles.popper.width = newPopperWidth;
              instance?.update?.();
            }
          },
        },
        {
          name: 'offset',
          options: {
            offset: [0, 3],
          },
        },
      ],
    };
  }, [optionComponentWidth]);

  return (
    <StyledSelectContainer className={className} {...{ [testIdAttribute]: testId }}>
      <TriggerButtonContextProvider
        ariaControls={dropdownId}
        ariaDescribedBy={ariaDescribedBy}
        ariaErrorMessage={ariaErrorMessage}
        ariaExpanded={isExpanded}
        ariaHasPopup="listbox"
        ariaInvalid={ariaInvalid}
        ariaLabel={ariaLabel}
        disabled={disabled}
        id={id}
        onBlur={handleTriggerBlur}
        onClick={handleTriggerButtonClick}
        role="combobox"
        triggerRef={triggerRef}
      >
        {trigger ?? (
          <SelectFieldTrigger
            disabled={disabled}
            onChange={handleOptionChange}
            placeholder={placeholder}
            testId={makeTestId(testId, 'trigger')}
            {...('multiple' in props && props.multiple
              ? {
                  multiple: true,
                  selectedValues: props.value ?? [],
                }
              : {
                  multiple: false,
                  cleanable: props.cleanable,
                  selectedValue: props.value,
                })}
            {...('async' in props && props.async
              ? {
                  async: true,
                  selectedOptions: props.selectedOptions,
                  optionsPrevPage: props.optionsPrevPage,
                  optionsCurrentPage: props.optionsCurrentPage,
                  optionsNextPage: props.optionsNextPage,
                }
              : {
                  async: false,
                  options: props.options,
                })}
          />
        )}
      </TriggerButtonContextProvider>

      <Dropdown
        focusTrapOptions={focusTrapOptions}
        id={dropdownId}
        onEscButtonPress={closeMenu}
        onOutsideClick={closeMenu}
        popperOptions={popperOptions}
        trigger={triggerRef.current}
        visible={isExpanded}
      >
        <Menu<Option>
          hideSearch={hideSearch}
          noOptionsMessage={noOptionsMessage}
          onChange={handleOptionChange}
          onClose={closeMenu}
          optionComponent={optionComponent}
          searchPlaceholder={searchPlaceholder}
          testId={makeTestId(testId, 'menu')}
          {...('multiple' in rest && rest.multiple
            ? {
                multiple: true,
                selectedValues: rest.value ?? [],
              }
            : {
                multiple: false,
                selectedValue: rest.value,
              })}
          {...('async' in rest && rest.async
            ? {
                async: true,
                totalOptions: rest.totalOptions,
                optionsPrevPage: rest.optionsPrevPage,
                optionsCurrentPage: rest.optionsCurrentPage,
                optionsNextPage: rest.optionsNextPage,
                onOptionsPageChanged: rest.onOptionsPageChanged,
                optionsPerPage: rest.optionsPerPage ?? 20,
              }
            : {
                async: false,
                options: rest.options,
                optionToSearchString: rest.optionToSearchString,
              })}
        />
      </Dropdown>
    </StyledSelectContainer>
  );
}
