import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import {
  Box,
  IconButton,
  Input,
  InputGroup,
  InputLeftAddon,
  InputLeftElement,
  InputRightElement,
  Tooltip,
  useColorModeValue,
} from "@chakra-ui/react";
import {
  ChevronDownIcon,
  ChevronUpIcon,
  CloseIcon,
  SearchIcon,
} from "@chakra-ui/icons";
import { ClassNames } from "@emotion/react";
import Autosuggest from "react-autosuggest";

import useCurrentUser from "../components/useCurrentUser";
import { logAndCapture } from "../util";

export const emptySearchQuery = {
  value: "",
  filterToZoneLabel: null,
  filterToItemKind: null,
  filterToCurrentUserOwnsOrWants: null,
};

export function searchQueryIsEmpty(query) {
  return Object.values(query).every((value) => !value);
}

const SUGGESTIONS_PLACEMENT_PROPS = {
  inline: {
    borderBottomRadius: "md",
  },
  top: {
    position: "absolute",
    bottom: "100%",
    borderTopRadius: "md",
  },
};

/**
 * SearchToolbar is rendered above both the ItemsPanel and the SearchPanel,
 * and contains the search field where the user types their query.
 *
 * It has some subtle keyboard interaction support, like DownArrow to go to the
 * first search result, and Escape to clear the search and go back to the
 * ItemsPanel. (The SearchPanel can also send focus back to here, with Escape
 * from anywhere, or UpArrow from the first result!)
 */
function SearchToolbar({
  query,
  searchQueryRef,
  firstSearchResultRef = null,
  onChange,
  autoFocus,
  showItemsLabel = false,
  background = null,
  boxShadow = null,
  suggestionsPlacement = "inline",
  ...props
}) {
  const [suggestions, setSuggestions] = React.useState([]);
  const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false);
  const { isLoggedIn } = useCurrentUser();

  // NOTE: This query should always load ~instantly, from the client cache.
  const { data } = useQuery(gql`
    query SearchToolbarZones {
      allZones {
        id
        label
        depth
        isCommonlyUsedByItems
      }
    }
  `);
  const zones = data?.allZones || [];
  const itemZones = zones.filter((z) => z.isCommonlyUsedByItems);

  let zoneLabels = itemZones.map((z) => z.label);
  zoneLabels = [...new Set(zoneLabels)];
  zoneLabels.sort();

  const onMoveFocusDownToResults = (e) => {
    if (firstSearchResultRef && firstSearchResultRef.current) {
      firstSearchResultRef.current.focus();
      e.preventDefault();
    }
  };

  const suggestionBgColor = useColorModeValue("white", "whiteAlpha.100");
  const highlightedBgColor = useColorModeValue("gray.100", "whiteAlpha.300");

  const renderSuggestion = React.useCallback(
    ({ text }, { isHighlighted }) => (
      <Box
        fontWeight={isHighlighted ? "bold" : "normal"}
        background={isHighlighted ? highlightedBgColor : suggestionBgColor}
        padding="2"
        paddingLeft="2.5rem"
        fontSize="sm"
      >
        {text}
      </Box>
    ),
    [suggestionBgColor, highlightedBgColor],
  );

  const renderSuggestionsContainer = React.useCallback(
    ({ containerProps, children }) => {
      const { className, ...otherContainerProps } = containerProps;
      return (
        <ClassNames>
          {({ css, cx }) => (
            <Box
              {...otherContainerProps}
              boxShadow="md"
              overflow="auto"
              transition="all 0.4s"
              maxHeight="48"
              width="100%"
              className={cx(
                className,
                css`
                  li {
                    list-style: none;
                  }
                `,
              )}
              {...SUGGESTIONS_PLACEMENT_PROPS[suggestionsPlacement]}
            >
              {children}
              {!children && advancedSearchIsOpen && (
                <Box
                  padding="4"
                  fontSize="sm"
                  fontStyle="italic"
                  textAlign="center"
                >
                  No more filters available!
                </Box>
              )}
            </Box>
          )}
        </ClassNames>
      );
    },
    [advancedSearchIsOpen, suggestionsPlacement],
  );

  // When we change the query filters, clear out the suggestions.
  React.useEffect(() => {
    setSuggestions([]);
  }, [
    query.filterToItemKind,
    query.filterToZoneLabel,
    query.filterToCurrentUserOwnsOrWants,
  ]);

  let queryFilterText = getQueryFilterText(query);
  if (showItemsLabel) {
    queryFilterText = queryFilterText ? (
      <>
        <Box as="span" fontWeight="600">
          Items:
        </Box>{" "}
        {queryFilterText}
      </>
    ) : (
      <Box as="span" fontWeight="600">
        Items
      </Box>
    );
  }

  const allSuggestions = getSuggestions(null, query, zoneLabels, isLoggedIn, {
    showAll: true,
  });

  // Once you remove the final suggestion available, close Advanced Search. We
  // have placeholder text available, sure, but this feels more natural!
  React.useEffect(() => {
    if (allSuggestions.length === 0) {
      setAdvancedSearchIsOpen(false);
    }
  }, [allSuggestions.length]);

  const focusBorderColor = useColorModeValue("green.600", "green.400");

  return (
    <Box position="relative" {...props}>
      <Autosuggest
        suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
        onSuggestionsFetchRequested={({ value }) => {
          // HACK: I'm not sure why, but apparently this gets called with value
          //       set to the _chosen suggestion_ after choosing it? Has that
          //       always happened? Idk? Let's just, gate around it, I guess?
          if (typeof value === "string") {
            setSuggestions(
              getSuggestions(value, query, zoneLabels, isLoggedIn),
            );
          }
        }}
        onSuggestionSelected={(e, { suggestion }) => {
          onChange({
            ...query,
            // If the suggestion was from typing, remove the last word of the
            // query value. Or, if it was from Advanced Search, leave it alone!
            value: advancedSearchIsOpen
              ? query.value
              : removeLastWord(query.value),
            filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
            filterToItemKind: suggestion.itemKind || query.filterToItemKind,
            filterToCurrentUserOwnsOrWants:
              suggestion.userOwnsOrWants ||
              query.filterToCurrentUserOwnsOrWants,
          });
        }}
        getSuggestionValue={(zl) => zl}
        alwaysRenderSuggestions={true}
        renderSuggestion={renderSuggestion}
        renderSuggestionsContainer={renderSuggestionsContainer}
        renderInputComponent={(inputProps) => (
          <InputGroup boxShadow={boxShadow} borderRadius="md">
            {queryFilterText ? (
              <InputLeftAddon>
                <SearchIcon color="gray.400" marginRight="3" />
                <Box fontSize="sm">{queryFilterText}</Box>
              </InputLeftAddon>
            ) : (
              <InputLeftElement>
                <SearchIcon color="gray.400" />
              </InputLeftElement>
            )}
            <Input
              background={background}
              // TODO: How to improve a11y here?
              // eslint-disable-next-line jsx-a11y/no-autofocus
              autoFocus={autoFocus}
              {...inputProps}
            />
            <InputRightElement
              width="auto"
              justifyContent="flex-end"
              paddingRight="2px"
              paddingY="2px"
            >
              {!searchQueryIsEmpty(query) && (
                <Tooltip label="Clear">
                  <IconButton
                    icon={<CloseIcon fontSize="0.6em" />}
                    color="gray.400"
                    variant="ghost"
                    height="100%"
                    marginLeft="1"
                    aria-label="Clear search"
                    onClick={() => {
                      setSuggestions([]);
                      onChange(emptySearchQuery);
                    }}
                  />
                </Tooltip>
              )}
              <Tooltip label="Advanced search">
                <IconButton
                  icon={
                    advancedSearchIsOpen ? (
                      <ChevronUpIcon fontSize="1.5em" />
                    ) : (
                      <ChevronDownIcon fontSize="1.5em" />
                    )
                  }
                  color="gray.400"
                  variant="ghost"
                  height="100%"
                  aria-label="Open advanced search"
                  onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
                />
              </Tooltip>
            </InputRightElement>
          </InputGroup>
        )}
        inputProps={{
          placeholder: "Search all items…",
          focusBorderColor: focusBorderColor,
          value: query.value || "",
          ref: searchQueryRef,
          minWidth: 0,
          "data-test-id": "item-search-input",
          onChange: (e, { newValue, method }) => {
            // The Autosuggest tries to change the _entire_ value of the element
            // when navigating suggestions, which isn't actually what we want.
            // Only accept value changes that are typed by the user!
            if (method === "type") {
              onChange({ ...query, value: newValue });
            }
          },
          onKeyDown: (e) => {
            if (e.key === "Escape") {
              if (suggestions.length > 0) {
                setSuggestions([]);
                return;
              }
              onChange(emptySearchQuery);
              e.target.blur();
            } else if (e.key === "Enter") {
              // Pressing Enter doesn't actually submit because it's all on
              // debounce, but it can be a declaration that the query is done, so
              // filter suggestions should go away!
              if (suggestions.length > 0) {
                setSuggestions([]);
                return;
              }
            } else if (e.key === "ArrowDown") {
              if (suggestions.length > 0) {
                return;
              }
              onMoveFocusDownToResults(e);
            } else if (e.key === "Backspace" && e.target.selectionStart === 0) {
              onChange({
                ...query,
                filterToItemKind: null,
                filterToZoneLabel: null,
                filterToCurrentUserOwnsOrWants: null,
              });
            }
          },
        }}
      />
    </Box>
  );
}

function getSuggestions(
  value,
  query,
  zoneLabels,
  isLoggedIn,
  { showAll = false } = {},
) {
  if (!value && !showAll) {
    return [];
  }

  const words = (value || "").split(/\s+/);
  const lastWord = words[words.length - 1];
  if (lastWord.length < 2 && !showAll) {
    return [];
  }

  const suggestions = [];

  if (query.filterToItemKind == null) {
    if (
      wordMatches("NC", lastWord) ||
      wordMatches("Neocash", lastWord) ||
      showAll
    ) {
      suggestions.push({ itemKind: "NC", text: "Neocash items" });
    }

    if (
      wordMatches("NP", lastWord) ||
      wordMatches("Neopoints", lastWord) ||
      showAll
    ) {
      suggestions.push({ itemKind: "NP", text: "Neopoint items" });
    }

    if (
      wordMatches("PB", lastWord) ||
      wordMatches("Paintbrush", lastWord) ||
      showAll
    ) {
      suggestions.push({ itemKind: "PB", text: "Paintbrush items" });
    }
  }

  if (isLoggedIn && query.filterToCurrentUserOwnsOrWants == null) {
    if (wordMatches("Items you own", lastWord) || showAll) {
      suggestions.push({ userOwnsOrWants: "OWNS", text: "Items you own" });
    }

    if (wordMatches("Items you want", lastWord) || showAll) {
      suggestions.push({ userOwnsOrWants: "WANTS", text: "Items you want" });
    }
  }

  if (query.filterToZoneLabel == null) {
    for (const zoneLabel of zoneLabels) {
      if (wordMatches(zoneLabel, lastWord) || showAll) {
        suggestions.push({ zoneLabel, text: `Zone: ${zoneLabel}` });
      }
    }
  }

  return suggestions;
}

function wordMatches(target, word) {
  return target.toLowerCase().includes(word.toLowerCase());
}

function getQueryFilterText(query) {
  const textWords = [];

  if (query.filterToItemKind) {
    textWords.push(query.filterToItemKind);
  }

  if (query.filterToZoneLabel) {
    textWords.push(pluralizeZoneLabel(query.filterToZoneLabel));
  }

  if (query.filterToCurrentUserOwnsOrWants === "OWNS") {
    if (!query.filterToItemKind && !query.filterToZoneLabel) {
      textWords.push("Items");
    } else if (query.filterToItemKind && !query.filterToZoneLabel) {
      textWords.push("items");
    }
    textWords.push("you own");
  } else if (query.filterToCurrentUserOwnsOrWants === "WANTS") {
    if (!query.filterToItemKind && !query.filterToZoneLabel) {
      textWords.push("Items");
    } else if (query.filterToItemKind && !query.filterToZoneLabel) {
      textWords.push("items");
    }
    textWords.push("you want");
  }

  return textWords.join(" ");
}

/**
 * pluralizeZoneLabel hackily tries to convert a zone name to a plural noun!
 *
 * HACK: It'd be more reliable and more translatable to do this by just
 *       manually creating the plural for each zone. But, ehh! ¯\_ (ツ)_/¯
 */
function pluralizeZoneLabel(zoneLabel) {
  if (zoneLabel.endsWith("ss")) {
    return zoneLabel + "es";
  } else if (zoneLabel.endsWith("s")) {
    return zoneLabel;
  } else {
    return zoneLabel + "s";
  }
}

/**
 * removeLastWord returns a copy of the text, with the last word and any
 * preceding space removed.
 */
function removeLastWord(text) {
  // This regex matches the full text, and assigns the last word and any
  // preceding text to subgroup 2, and all preceding text to subgroup 1. If
  // there's no last word, we'll still match, and the full string will be in
  // subgroup 1, including any space - no changes made!
  const match = text.match(/^(.*?)(\s*\S+)?$/);
  if (!match) {
    logAndCapture(
      new Error(
        `Assertion failure: pattern should match any input text, ` +
          `but failed to match ${JSON.stringify(text)}`,
      ),
    );
    return text;
  }

  return match[1];
}

export default SearchToolbar;