import React, { useState, useRef, useEffect, useCallback } from "react";
import { ListOnScrollProps } from "react-window";
import { Popover, TriggerProps } from "../popover";
import { DropdownBox, CommonDropdownProps } from "../dropdown";
import { StyledProps } from "../_type";
import { SelectOptionWithGroup } from "../select";
import {
  VirtualizedList as List,
  VirtualizedListItem,
} from "../list/VirtualizedList";
import { ControlledProps, useDefaultValue } from "../form";
import { EmptyTip } from "../tips";
import { useDefault } from "../_util/use-default";
import { KeyMap } from "../_util/key-map";
import { useConfig } from "../util";
import { useIsomorphicLayoutEffect } from "../_util/use-isomorphic-layout-effect";
import { highlight } from "../_util/text-highlight";

export interface AutoCompleteProps
  extends ControlledProps<string>,
    CommonDropdownProps,
    StyledProps {
  /**
   * 下拉选项列表
   */
  options?: SelectOptionWithGroup[];

  /**
   * 分组
   */
  groups?: {
    [groupKey: string]: React.ReactNode;
  };

  /**
   * `options` 为空时展示，可使用字符串或 [StatusTip](/component/tips) 相关组件
   */
  tips?: React.ReactNode;

  /**
   * 展示为高亮的关键词
   */
  keyword?: string;

  /**
   * 要包裹的输入组件
   */
  children?: (
    ref: React.Ref<any>,
    context: { close: () => void }
  ) => React.ReactNode;

  /**
   * `options` 滚动至底部的回调
   */
  onScrollBottom?: (props: ListOnScrollProps) => void;
}

export function AutoComplete(props: AutoCompleteProps) {
  const {
    className,
    style = {},
    onChange,
    children,
    options = [],
    groups = {},
    keyword,
    onScrollBottom,
    closeOnScroll = true,
    open,
    defaultOpen,
    onOpenChange,
    ...popoverProps
  } = useDefaultValue(props);
  const { classPrefix } = useConfig();
  const [isSelected, setIsSelected] = useState<boolean>(false);
  const inputRef = useRef<HTMLElement>(null);
  const [isOpened, setIsOpened] = useDefault(open, defaultOpen, onOpenChange);
  const [dropdownWidth, setDropdownWidth] = useState<number>(undefined);
  const [currentIndex, setCurrentIndex] = useState<number>(-1);
  const listRef = useRef(null);

  // 内容变化时更新位置
  const scheduleUpdateRef = useRef(null);
  useIsomorphicLayoutEffect(
    () => () => {
      if (scheduleUpdateRef.current) {
        scheduleUpdateRef.current();
      }
    },
    [options]
  );

  const hasGroup = !!options.find(opt => !!opt.groupKey);

  let { tips = <EmptyTip /> } = props;
  if (options.length === 0) {
    tips = typeof tips === "string" ? <EmptyTip emptyText={tips} /> : tips;
  } else {
    tips = null;
  }

  const items = getListItems({
    tips,
    groups,
    options,
    keyword,
  });
  const count = items.length;

  const getOptionIndex = useCallback(
    (current: number, step: number = 1) => {
      let flag = 1;
      let index = (current + step + count) % count;
      let item = items[index % count];
      while (flag < count && (!item.option || item.option.disabled)) {
        flag += 1;
        index = (index + step + count) % count;
        item = items[index % count];
      }
      return index;
    },
    [items, count]
  );

  useEffect(() => {
    if (!inputRef.current) return null;
    function handleKeyDown(event: KeyboardEvent) {
      const { option } = items[currentIndex % count] || {};

      switch (event.key) {
        case KeyMap.Up:
          event.preventDefault();
          setCurrentIndex(c => {
            const index = getOptionIndex(c, -1);
            if (listRef.current && count > 0) {
              listRef.current.scrollToItem(index);
            }
            return index;
          });
          break;
        case KeyMap.Down:
          event.preventDefault();
          setCurrentIndex(c => {
            const index = getOptionIndex(c);
            if (listRef.current && count > 0) {
              listRef.current.scrollToItem(index);
            }
            return index;
          });
          break;
        case KeyMap.Enter:
          if (isOpened && option) {
            if (option.disabled) break;
            setIsOpened(false);
            onChange(option.value, { event: event as any });
          } else {
            setIsOpened(true);
          }
          break;
        case KeyMap.Esc:
          setIsOpened(false);
          inputRef.current.blur();
          break;
        default:
          setCurrentIndex(-1);
          break;
      }
    }
    inputRef.current.addEventListener("keydown", handleKeyDown);
    return () => {
      if (inputRef.current) {
        inputRef.current.removeEventListener("keydown", handleKeyDown);
      }
    };
  }, [
    currentIndex,
    inputRef,
    isOpened,
    onChange,
    options,
    setIsOpened,
    items,
    count,
    getOptionIndex,
  ]);

  const boxStyle = { width: dropdownWidth, ...style };

  return (
    <Popover
      escapeWithReference
      animationScaleFrom={1}
      trigger={[AutoCompleteTrigger, { setIsSelected }]}
      placement="bottom-start"
      visible={isOpened && !isSelected}
      onVisibleChange={setIsOpened}
      placementOffset={5}
      closeOnScroll={closeOnScroll}
      {...popoverProps}
      overlay={({ scheduleUpdate }) => {
        scheduleUpdateRef.current = scheduleUpdate;
        return (
          <DropdownBox preventMouseDown className={className} style={boxStyle}>
            <List
              containerStyle={boxStyle}
              containerClassName={className}
              virtualizedRef={listRef}
              type={hasGroup ? "option-group" : "option"}
              items={items.map((item, index) => {
                if (item.type === "option") {
                  const { option } = item;
                  return {
                    ...item,
                    props: {
                      disabled: option.disabled,
                      current: currentIndex === index,
                      onClick: event => {
                        event.stopPropagation();
                        inputRef.current.focus();
                        onChange(option.value, { event });
                        setIsSelected(true);
                        setCurrentIndex(index);
                      },
                      tooltip: option.tooltip,
                      className:
                        hasGroup && !option.groupKey
                          ? `${classPrefix}-list__item--single`
                          : "",
                    },
                  };
                }
                return item;
              })}
              onScrollBottom={onScrollBottom}
            />
          </DropdownBox>
        );
      }}
    >
      {children(
        dom => {
          inputRef.current = dom;
          if (dom) {
            setDropdownWidth(dom.offsetWidth);
          }
        },
        { close: () => setIsOpened(false) }
      )}
    </Popover>
  );
}

AutoComplete.displayName = "AutoComplete";

export function AutoCompleteTrigger({
  visible,
  setVisible,
  openDelay = 50,
  closeDelay = 100,
  render,
  setIsSelected,
}: TriggerProps & { setIsSelected: (s: boolean) => void }) {
  const commonProps = {
    onFocus: () => setVisible(true, openDelay),
    onChange: () => {
      setIsSelected(false);
      if (!visible) {
        setVisible(true, openDelay);
      }
    },
    onClick: () => {
      setIsSelected(false);
      if (!visible) {
        setVisible(true, openDelay);
      }
    },
    onBlur: () => setVisible(false, closeDelay),
  };
  return render({
    overlayProps: { ...commonProps, tabIndex: 1000 },
    childrenProps: commonProps,
  });
}

AutoComplete.defaultLabelAlign = "middle";

function getListItems({
  tips,
  options,
  groups,
  keyword,
}: Pick<
  AutoCompleteProps,
  "tips" | "options" | "groups" | "keyword"
>): VirtualizedListItem<SelectOptionWithGroup>[] {
  const items: VirtualizedListItem<SelectOptionWithGroup>[] = [];
  // tips
  if (tips) {
    items.push({ type: "tips", key: "__auto_complete_tips", text: tips });
  }
  options.forEach((option, index) => {
    if (
      option.groupKey &&
      (index === 0 || option.groupKey !== options[index - 1].groupKey)
    ) {
      items.push({
        type: "group",
        key: `${option.groupKey}-${option.value}`,
        text: groups[option.groupKey],
      });
    }
    items.push({
      type: "option",
      key: option.value,
      text: highlight(
        typeof option.text === "undefined" ? option.value : option.text,
        keyword
      ),
      option,
    });
  });
  return items;
}
