/**
 * TEAM: frontend_infra
 * @flow strict
 */

import * as React from "react";
import {StyleSheet, css} from "aphrodite";
import latitudeColors from "../colors";
import Highlighter from "../Highlighter";
import {
  UP,
  DOWN,
  ENTER,
  PAGE_UP,
  PAGE_DOWN,
} from "../constants/interactions/KeyCodes";

/** Internal Latitude component used for keyboard enabled lists such as DropdownList */

type Props = {|
  /** The content for the list to display */
  +children: React.Node,
  /** The value of the selected list option */
  +selected?: ?string,
  /** called when a dropdown option is clicked */
  +onSelection: (selected: string) => void,
|};

export type OptionRepresentation = {|
  +value: string,
  +elementRef: {|current: ?HTMLElement|},
|};

export const SelectionListContext: React.Context<{|
  +selected: ?string,
  +select: string => void,
  +addOption: OptionRepresentation => void,
  +removeOption: OptionRepresentation => void,
  +highlightOption: OptionRepresentation => void,
|}> = React.createContext({
  selected: null,
  select: () => {},
  addOption: () => {},
  removeOption: () => {},
  highlightOption: () => {},
});

export type SelectionListInstance = {|
  +parseKeyEvent: KeyboardEvent => void,
|};

/**
 * @category Data Entry
 * @short The list that displays options with keyboard support
 * @brandStatus V2
 * @status In Review
 */
function SelectionList(
  {selected, onSelection, children}: Props,
  ref:
    | {current: SelectionListInstance | null, ...}
    | ((inst: SelectionListInstance | null) => mixed)
): React.Element<typeof SelectionListContext.Provider> {
  const options = React.useRef([]);
  const [highlightedOptionIndex, setHighlightedOptionIndex] =
    React.useState(null);
  const select = React.useCallback(
    value => {
      if (selected === value) return;

      onSelection(value);
    },
    [onSelection, selected]
  );
  const addOption = React.useCallback((option: OptionRepresentation) => {
    options.current.push(option);
  }, []);
  const removeOption = React.useCallback((option: OptionRepresentation) => {
    const index = options.current.indexOf(option);

    if (index > -1) options.current.splice(index, 1);
  }, []);
  // Sort options because they could be not in DOM order based on when there were added/removed
  const sortOptions = React.useCallback(() => {
    options.current = options.current.sort((a, b) => {
      const aElement = a.elementRef.current;
      const bElement = b.elementRef.current;

      if (aElement == null || bElement == null) return 0;

      return aElement.compareDocumentPosition(bElement) ===
        Node.DOCUMENT_POSITION_FOLLOWING
        ? -1
        : 1;
    });
  }, []);
  const highlightOption = React.useCallback(
    (option: OptionRepresentation) => {
      // Sort options to make sure they're in DOM order first
      sortOptions();

      const index = options.current.indexOf(option);

      if (index === -1) throw new Error("Highlighting missing option.");

      setHighlightedOptionIndex(index);
    },
    [sortOptions]
  );
  const getSelectedOption: () => ?OptionRepresentation =
    React.useCallback(() => {
      for (let idx = 0; idx < options.current.length; idx += 1) {
        const option = options.current[idx];

        if (option.value === selected) return option;
      }
    }, [selected]);
  const onKeyDown = React.useCallback(
    (e: KeyboardEvent) => {
      if (options.current.length === 0) {
        return;
      }

      switch (e.keyCode) {
        case UP: {
          e.preventDefault();

          // Sort options to make sure they're in DOM order first
          sortOptions();

          // We do not want to highlight the selected option, so we track it so we can skip over it
          const selectedOptionIndex = options.current.indexOf(
            getSelectedOption()
          );
          const currentIndex =
            highlightedOptionIndex != null
              ? highlightedOptionIndex
              : selectedOptionIndex;

          // Cover wrap around highlight or if no highlight
          let newHighlightIndex =
            currentIndex == null || currentIndex === 0
              ? options.current.length - 1
              : currentIndex - 1;

          if (newHighlightIndex === selectedOptionIndex) {
            newHighlightIndex -= 1;

            if (newHighlightIndex < 0)
              newHighlightIndex = options.current.length - 1;
          }

          setHighlightedOptionIndex(newHighlightIndex);

          break;
        }
        case DOWN: {
          e.preventDefault();

          // Sort options to make sure they're in DOM order first
          sortOptions();

          // We do not want to highlight the selected option, so we track is so we can skip over it
          const selectedOptionIndex = options.current.indexOf(
            getSelectedOption()
          );
          const currentIndex =
            highlightedOptionIndex != null
              ? highlightedOptionIndex
              : selectedOptionIndex;

          // Cover wrap around highlight or if no highlight
          let newHighlightIndex =
            currentIndex === null || currentIndex === options.current.length - 1
              ? 0
              : currentIndex + 1;

          if (newHighlightIndex === selectedOptionIndex) {
            newHighlightIndex += 1;

            if (newHighlightIndex > options.current.length - 1)
              newHighlightIndex = 0;
          }

          setHighlightedOptionIndex(newHighlightIndex);

          break;
        }
        case PAGE_UP: {
          e.preventDefault();

          // Sort options to make sure they're in DOM order first
          sortOptions();

          // Cover wrap around highlight or if no highlight
          let newHighlightIndex = 0;

          // We do not want to highlight the selected option, so we track is so we can skip over it
          const selectedOptionIndex = options.current.indexOf(
            getSelectedOption()
          );

          if (newHighlightIndex === selectedOptionIndex) {
            newHighlightIndex += 1;

            if (newHighlightIndex > options.current.length - 1)
              newHighlightIndex = 0;
          }

          setHighlightedOptionIndex(newHighlightIndex);

          break;
        }
        case PAGE_DOWN: {
          e.preventDefault();

          // Sort options to make sure they're in DOM order first
          sortOptions();

          // Cover wrap around highlight or if no highlight
          let newHighlightIndex = options.current.length - 1;

          // We do not want to highlight the selected option, so we track is so we can skip over it
          const selectedOptionIndex = options.current.indexOf(
            getSelectedOption()
          );

          if (newHighlightIndex === selectedOptionIndex) {
            newHighlightIndex -= 1;

            if (newHighlightIndex < 0) newHighlightIndex = 0;
          }

          setHighlightedOptionIndex(newHighlightIndex);

          break;
        }
        case ENTER: {
          if (highlightedOptionIndex != null) {
            // Sort options to make sure they're in DOM order first
            sortOptions();

            if (options.current[highlightedOptionIndex])
              select(options.current[highlightedOptionIndex].value);
          }

          break;
        }
        default: {
          break;
        }
      }
    },
    [getSelectedOption, highlightedOptionIndex, select, sortOptions]
  );

  // Add autoscroll to highlighted when highlighted option changes
  React.useEffect(() => {
    if (highlightedOptionIndex != null) {
      // Sort options to make sure they're in DOM order first
      sortOptions();

      const highlightedOption = options.current[highlightedOptionIndex];

      if (
        highlightedOption != null &&
        highlightedOption.elementRef.current != null &&
        // We do this check since scrollIntoView may not be available when rendered in tests
        // $FlowIssue[method-unbinding]: This is a Flow bug (related: https://github.com/facebook/flow/issues/8689)
        highlightedOption.elementRef.current.scrollIntoView != null
      ) {
        highlightedOption.elementRef.current.scrollIntoView({
          block: "nearest",
          behavior: "smooth",
        });
      }
    }
  }, [highlightedOptionIndex, sortOptions]);

  // Autoscroll to selected option when selected
  React.useEffect(() => {
    const selectedOption = getSelectedOption();

    if (
      selectedOption &&
      selectedOption.elementRef.current != null &&
      // We do this check since scrollIntoView may not be available when rendered in tests
      // $FlowIssue[method-unbinding]: This is a Flow bug, CY add a link here
      selectedOption.elementRef.current.scrollIntoView != null
    ) {
      selectedOption.elementRef.current.scrollIntoView({
        block: "nearest",
        behavior: "smooth",
      });
    }
  }, [getSelectedOption]);

  React.useImperativeHandle(ref, () => ({
    parseKeyEvent: onKeyDown,
  }));

  return (
    <SelectionListContext.Provider
      value={{
        selected,
        select,
        addOption,
        removeOption,
        highlightOption,
      }}
    >
      <div
        className={css(styles.list)}
        role="listbox"
        tabIndex={0}
        onMouseLeave={() => {
          setHighlightedOptionIndex(null);
        }}
        onKeyDown={onKeyDown}
      >
        <Highlighter
          color="grey20"
          highlightedElement={
            /** Either highlight an option being hovered/arrow-keyed if
             * available, otherwise highlight what's selected in the list */
            highlightedOptionIndex != null
              ? options.current[highlightedOptionIndex].elementRef.current
              : null
          }
          entryExitAnimationDuration={250}
          transitionDuration={180}
        >
          {children}
        </Highlighter>
      </div>
    </SelectionListContext.Provider>
  );
}

const WrappedSelectionList: React.AbstractComponent<
  Props,
  SelectionListInstance
> = React.memo<Props, SelectionListInstance>(
  React.forwardRef<Props, SelectionListInstance>(SelectionList)
);

export default WrappedSelectionList;

const styles = StyleSheet.create({
  list: {
    padding: "0",
    margin: "0",
    backgroundColor: latitudeColors.white,
    outline: "none",
  },
});
