// @flow
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import styled from 'styled-components';
import { v4 } from 'uuid';
import { pick, omit } from 'lodash';
import type { SearchInputProps } from './search-input.proptypes';
import { getPaletteColor } from '../../../mixins/branding';
import { Box } from '../../../primitives';
import { INPUT_ROLE_ARIA_PROPS } from '../../../mixins/aria';
import {
  ClickableSearchResult,
  DefaultSearchResult,
  SearchResultSeparator,
  SearchResultsWrapper,
} from './components';
import { TextInput } from '../text-input';

const NotFoundItem = styled(DefaultSearchResult)`
  color: ${getPaletteColor('greyscale-dark')};
`;

const SeachInputContainer = styled(Box)`
  position: relative;
`;

export const SearchInput = ({
  value = null,
  initialValue = null,
  placeholder,
  searchResults,
  onChange,
  onSearchResultClicked = (_id, _value) => {},
  allowNotFound = false,
  showNotFoundOnlyOnNoResults = false,
  notFoundText,
  clearLabel,
  label,
  focusOnMount,
  renderItem,
  renderNotFoundItem,
  clearSearchOnClick = false,
  onClear,
  spacing,
  grow,
  shrink,
  basis,
  onFocus,
  onBlur,
  className,
  'data-qa': dataQa,
  'data-cy': dataCy,
  id,
  name,
  error = false,
  success,
  description,
  labelStyle,
  ...props
}: SearchInputProps) => {
  const [searchInput, setSearchInput] = useState(value || initialValue || '');
  const [showSearchResults, setShowSearchResults] = useState(false);
  const [selectedSearchResultIndex, setSelectedSearchResultIndex] = useState(
    -1,
  );
  const [timeoutId, setTimeoutId] = useState();
  const resultsListId = useMemo(() => v4(), []);
  const handleChange = useCallback(
    (newSearchInput) => {
      setSelectedSearchResultIndex(-1);
      setSearchInput(newSearchInput ?? '');
      onChange(newSearchInput ?? '');
      setShowSearchResults(!!newSearchInput);
    },
    [onChange],
  );

  useEffect(() => {
    if (value != null && searchInput !== value) {
      setSearchInput(value);
    }
  }, [searchInput, value]);

  const handleSearchResultClicked = useCallback(
    (resultId, newValue) => {
      setSelectedSearchResultIndex(-1);
      setShowSearchResults(false);
      if (clearSearchOnClick) {
        setSearchInput('');
      } else {
        setSearchInput(newValue ?? '');
      }
      onSearchResultClicked(resultId, newValue);
    },
    [onSearchResultClicked, clearSearchOnClick],
  );

  const handleSearchInputFocused = useCallback(
    (e) => {
      setShowSearchResults(!!searchInput.length);
      if (onFocus) onFocus(e);
    },
    [searchInput, onFocus],
  );

  const handleSearchInputBlurred = useCallback(
    (e) => {
      // Wait before closing the drop-down so clicking on an item has time
      // to register before the list is dismissed.
      setTimeoutId(window.setTimeout(() => setShowSearchResults(false), 200));
      if (onBlur) onBlur(e);
    },
    [onBlur],
  );
  // Clear any outstanding timeout when the component is unmounted.
  useEffect(() => () => timeoutId && window.clearTimeout(timeoutId));

  const handleKeyDown = useCallback(
    (evt) => {
      const enterKey = evt.keyCode === 13;

      if (enterKey && selectedSearchResultIndex >= 0) {
        const clickedResult = searchResults[selectedSearchResultIndex];
        if (clickedResult) {
          handleSearchResultClicked(clickedResult.id, clickedResult.text);
        } else {
          handleSearchResultClicked();
        }

        evt.stopPropagation();
        evt.preventDefault();
      }
    },
    [handleSearchResultClicked, searchResults, selectedSearchResultIndex],
  );

  const handleKeyUp = useCallback(
    (evt) => {
      let highlightedIndex = selectedSearchResultIndex;
      const upKey = evt.keyCode === 38;
      const downKey = evt.keyCode === 40;
      const escapeKey = evt.keyCode === 27;

      // Search results should be 1 longer for if the last option is the "Not Found" option
      const searchResultsLength =
        searchResults.length - (allowNotFound ? 0 : 1);

      if (escapeKey) {
        highlightedIndex = -1;
        setShowSearchResults(false);
      } else if (upKey && selectedSearchResultIndex >= 0) {
        let selectedSearchResult;
        do {
          highlightedIndex--;
          selectedSearchResult = searchResults[highlightedIndex];
        } while (selectedSearchResult && selectedSearchResult.isSeparator);
      } else if (downKey && selectedSearchResultIndex < searchResultsLength) {
        let selectedSearchResult;
        do {
          highlightedIndex++;
          selectedSearchResult = searchResults[highlightedIndex];
        } while (selectedSearchResult && selectedSearchResult.isSeparator);
      }

      setSelectedSearchResultIndex(highlightedIndex);
    },
    [allowNotFound, searchResults, selectedSearchResultIndex],
  );

  const handleClick = (resultId, result) => {
    handleSearchResultClicked(resultId, result);
  };

  /**
   * In the handlers, `searchResults` is outdated so we need to check it in the render.
   * Make an exception to show search results regardless if there are any for `allowNotFound`
   */
  const shouldShowSearchResults =
    showSearchResults && (!!searchResults.length || allowNotFound);

  const showNotFoundItem =
    allowNotFound &&
    (!showNotFoundOnlyOnNoResults ||
      (showNotFoundOnlyOnNoResults && !searchResults.length));

  return (
    <SeachInputContainer
      stacked="column"
      onKeyUp={handleKeyUp}
      onKeyDown={handleKeyDown}
      {...{ spacing, grow, shrink, basis }}
      className={className}
      data-cy={dataCy}
      data-qa={dataQa}
      id={id}
      {...omit(props, INPUT_ROLE_ARIA_PROPS)}
    >
      <TextInput
        name={name}
        label={label}
        type="search"
        onChange={handleChange}
        value={searchInput}
        placeholder={placeholder}
        focusOnMount={focusOnMount}
        onBlur={handleSearchInputBlurred}
        onFocus={handleSearchInputFocused}
        error={error}
        success={success}
        description={description}
        clearButtonLabel={clearLabel}
        onClear={onClear}
        labelStyle={labelStyle}
        {...pick(props, INPUT_ROLE_ARIA_PROPS)}
        aria-autocomplete="list"
        aria-expanded={shouldShowSearchResults}
        aria-owns={resultsListId}
        role="combobox"
        autoComplete="off"
        autoCapitalize="none"
        spellCheck={false}
      />
      <SearchResultsWrapper
        isVisible={shouldShowSearchResults}
        role="listbox"
        id={resultsListId}
        spacing="mt1"
      >
        {searchResults.map((result, i) =>
          result.isSeparator ? (
            <SearchResultSeparator role="separator" key={result.id}>
              {result.text}
            </SearchResultSeparator>
          ) : (
            <ClickableSearchResult
              type="button"
              role="option"
              key={result.id}
              onClick={() => handleClick(result.id, result.text)}
              aria-selected={selectedSearchResultIndex === i}
              id={`opt-${resultsListId}-${i}`}
            >
              {renderItem ? (
                renderItem({
                  ...result,
                  selected: selectedSearchResultIndex === i,
                })
              ) : (
                <DefaultSearchResult
                  matchedValue={searchInput}
                  isFirst={i === 0}
                  isSelected={selectedSearchResultIndex === i}
                  text={result.text}
                />
              )}
            </ClickableSearchResult>
          ),
        )}
        {showNotFoundItem && !renderNotFoundItem && (
          <NotFoundItem
            onClick={() => handleClick()}
            isSelected={selectedSearchResultIndex === searchResults.length}
            bold={false}
            isFirst={searchResults.length === 0}
            text={notFoundText}
          />
        )}
        {showNotFoundItem && renderNotFoundItem && renderNotFoundItem()}
      </SearchResultsWrapper>
    </SeachInputContainer>
  );
};

SearchInput.displayName = 'SearchInput';
