import React, { HTMLAttributes, ReactNode } from "react";
import classNames from "classnames";
import { useSelect } from "downshift";
import { SingleSelectContext } from "./single/SingleSelectContext";
import {
  SelectOption,
  SelectOptionsByCategory,
  SingleSelectedOption,
} from "./shared/types";
import { Box } from "../Box";
import { SingleSelectOption } from "./single/SingleSelectOption";
import { SelectOptionCategory } from "./shared/SelectOptionCategory";
import { useSelectLayout } from "./hooks/useSelectLayout";
import { SelectStatus } from "./shared/SelectStatus";
import { STATUS_VARIANT } from "../../types";
import { useSelectCreatable } from "./hooks/useSelectCreatable";
import { useTransformSelectOptions } from "./hooks/useTransformSelectOptions";
import { useDownshiftConfig } from "./hooks/useDownshiftConfig";
import { SingleSelectSelected } from "./single/SingleSelectSelected";
import { SelectControl } from "./shared/SelectControl";
import { SelectMenu } from "./shared/SelectMenu";

interface SingleSelectBaseProps<T extends SelectOption>
  extends Pick<HTMLAttributes<HTMLDivElement>, "className" | "style"> {
  /**
   * The currently selected item (`T`) or `null`.
   *
   * Generic `T` extends the base options shape: `{ value: string; label: string;}`
   */
  selected: SingleSelectedOption<T>;
  /**
   * Called whenever the `selected` item should change
   */
  onChange: (newSelected: SingleSelectedOption<T>) => void;
  /**
   * `children` is what gets displayed in the dropdown when opened. Since `children`
   * are rendered as a direct descendent of a `ul` element, every child must render
   * an `li` as its outer-most element. The `SingleSelect.Option` and
   * `SingleSelect.Status` components do this automatically. You'll need to make sure
   * any other elements you pass to `children` do as well.
   *
   * **SUPER IMPORTANT NOTE**: `children` need to be rendered in the exact
   * same order as `options`. If you need to do any sorting, make sure you are using
   * the sorted array for both `options` and `children`.
   */
  children: ReactNode;
  /**
   * The current value of the search input
   */
  inputValue?: string;
  /**
   * If `true`, an 'X' button will appear within the component when an item is selected
   * that allows the user to clear the selection, setting `selected` to `null`.
   */
  isClearable?: boolean;
  /**
   * If `true`, the component gets disabled styles and cannot be interacted with.
   */
  isDisabled?: boolean;
  /**
   * If `true`, a loading indicator will be displayed within the component. This is useful
   * when fetching results or when saving a user's selection.
   */
  isLoading?: boolean;
  /**
   * **IMPORTANT: This prop is technically optional but should always be used unless this component
   * is being used within a `FormGroup`!**
   *
   * Providing a `label` will make this component accessible to those using a
   * screen reader. However, when this component is used inside a `FormGroup` with a `Label`, that
   * label will be used automatically. See the "FormGroup" demo for details.
   */
  label?: string;
  /**
   * If a function is provided to `onCreateOption` and the search field is enabled (by
   * providing a function to `onInputChange`), users will be able to create new options
   * based on their search query. This function will be called when the new option is
   * created.
   */
  onCreateOption?: (newOption: string) => void;
  /**
   * When a function is provided to `onInputChange`, it will enable the search field in
   * the dropdown.
   *
   * **NOTE**: This component does not handle any actual filtering. It's up to you to
   * filter options based on the query, provide them to the `options` prop, and use
   * them to render `children`.
   */
  onInputChange?: (newValue: string) => void;
  /**
   * If a string/element is provided to `placeholder`, it will be displayed when `selected`
   * is `null`.
   */
  placeholder?: ReactNode;
  /**
   * If provided, this function will replace the default render function that displays the
   * currently selected item.
   *
   * **NOTE**: The default render function accounts for labels that are too long. You should
   * probably do the same when using this function (perhaps using Arrow's `Truncate` component)
   * unless you are sure none of your selected items will exceed the allotted space.
   */
  renderSelected?: ({
    selectedItem,
  }: {
    selectedItem: SingleSelectedOption<T>;
  }) => ReactNode;
  /**
   * Useful for indicating success, warning, or failure status
   */
  variant?: STATUS_VARIANT;
}

export interface SingleSelectBasicProps<T extends SelectOption>
  extends SingleSelectBaseProps<T> {
  /**
   * This prop will define the shape of the `options` passed in to the component.
   * The default, `basic`, will be used the vast majority of the time and will only
   * need to be changed if the display of options within the dropdown should be
   * anything other than a flat list.
   */
  optionsDisplay?: "basic";
  /**
   * When `optionsDisplay` is `basic` (default), the `options` prop should be a flat
   * array of options.
   *
   * Generic `T` extends the base options shape: `{ value: string; label: string;}`
   */
  options: T[];
}

export interface SingleSelectCategoriesProps<T extends SelectOption>
  extends SingleSelectBaseProps<T> {
  /**
   * Setting `optionsDisplay` to `categories` makes it so options can be displayed
   * under various category headers within the dropdown.
   */
  optionsDisplay: "categories";
  /**
   * When `optionsDisplay` is `categories`, the `options` prop should be an array
   * of categories with `title` and `options` properties. The `title` is just a
   * string and the `options` should be an array of `T`.
   */
  options: SelectOptionsByCategory<T>[];
}

export type SingleSelectProps<T extends SelectOption> =
  | SingleSelectBasicProps<T>
  | SingleSelectCategoriesProps<T>;

function SingleSelect<T extends SelectOption>({
  selected,
  onChange,
  label,
  renderSelected,
  children,
  className,
  inputValue,
  onInputChange,
  isLoading,
  placeholder,
  isClearable,
  onCreateOption,
  isDisabled,
  variant: variantProp,
  ...props
}: SingleSelectProps<T>) {
  const {
    isCreatable,
    creatableOption,
    inputHasFocus,
    setInputHasFocus,
  } = useSelectCreatable<T>({ onCreateOption, inputValue });

  const transformedOptions = useTransformSelectOptions<T>({
    ...props,
    isCreatable,
    creatableOption,
  });

  const downshiftConfig = useDownshiftConfig({
    transformedOptions,
    selected,
    onChange,
    onInputChange,
    inputHasFocus,
  });

  const {
    isOpen,
    selectedItem,
    selectItem,
    getToggleButtonProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
    getLabelProps,
    closeMenu,
    setHighlightedIndex,
  } = useSelect<SingleSelectedOption<T>>(downshiftConfig);

  const {
    setReferenceElement,
    popperRef,
    controlClasses,
    wrapperClasses,
    callbackElementProps,
    inputRef,
    hasContextLabel,
    optionIndexRef,
    setOptionIndex,
    menuProps,
    createOption,
  } = useSelectLayout({
    isOpen,
    hasSearch: !!onInputChange,
    isDisabled,
    variantProp,
    getLabelProps,
    onCreateOption,
    inputValue,
    closeMenu,
    getMenuProps,
    transformedOptions,
    highlightedIndex,
    onInputChange,
    inputHasFocus,
    isCreatable,
    setHighlightedIndex,
  });

  // Swallow props we don't want to pass to the outer element
  const { options, optionsDisplay, ...rest } = props;

  return (
    <Box className={classNames(wrapperClasses, className)} {...rest}>
      <SelectControl
        setReferenceElement={setReferenceElement}
        label={label}
        hasContextLabel={hasContextLabel}
        getLabelProps={getLabelProps}
        getToggleButtonProps={getToggleButtonProps}
        isDisabled={isDisabled}
        isClearable={isClearable}
        isLoading={isLoading}
        onClear={() => selectItem(null)}
        hasSelection={!!selectedItem}
        className={controlClasses}
      >
        <SingleSelectSelected
          selectedItem={selectedItem}
          placeholder={placeholder}
          renderSelected={renderSelected}
        />
      </SelectControl>

      <SingleSelectContext.Provider
        value={{
          getItemProps,
          selectedItem,
          highlightedIndex,
          optionIndexRef,
          setOptionIndex,
        }}
      >
        <SelectMenu
          onCreateOption={createOption}
          isOpen={isOpen}
          callbackElementProps={callbackElementProps}
          onInputChange={onInputChange}
          inputRef={inputRef}
          inputValue={inputValue}
          setInputHasFocus={setInputHasFocus}
          popperRef={popperRef}
          optionIndexRef={optionIndexRef}
          setOptionIndex={setOptionIndex}
          highlightedIndex={highlightedIndex}
          creatableOption={creatableOption}
          isCreatable={isCreatable}
          getItemProps={getItemProps}
          isLoading={isLoading}
          menuProps={menuProps}
        >
          {children}
        </SelectMenu>
      </SingleSelectContext.Provider>
    </Box>
  );
}

SingleSelect.Option = SingleSelectOption;
SingleSelect.Category = SelectOptionCategory;
SingleSelect.Status = SelectStatus;

SingleSelect.defaultProps = {
  optionsDisplay: "basic",
  variant: STATUS_VARIANT.DEFAULT,
};

export { SingleSelect };
