import React, {
  ChangeEvent,
  KeyboardEvent,
  forwardRef,
  HTMLAttributes,
  InputHTMLAttributes,
  useCallback,
  useMemo,
  useState,
  FocusEvent,
  ReactNode,
  useRef,
  useEffect,
} from 'react';
import { classNames } from 'utils/class-names';

type StringKeys<T, KT = string> = {
  [K in keyof T]: T[K] extends KT ? K : never;
}[keyof T];

export interface RenderSuggestionItemContext<TCommand> {
  // empty
  command: TCommand;
  isHighlighted: boolean;
}

export interface CommandCompleteProps<
  TCommand extends { [C in TCommandLabelKey]: string },
  TCommandLabelKey extends StringKeys<TCommand>,
> {
  availableCommands: Array<TCommand>;
  commandLabelKey: TCommandLabelKey;
  renderSuggestionItem?: (
    renderSuggestionItemContext: RenderSuggestionItemContext<TCommand>,
  ) => ReactNode;
  maxCommandHistory?: boolean;
  minSuggestions?: number;
  maxSuggestions?: number;
  minCharactersToSuggest?: number;
  keysToConfirmSelection?: Array<string>;
  appendAfterSelection?: string;
  keysToSubmit?: Array<string>;
  clearAfterSubmit?: boolean;
  onSubmit?: (command: string) => void;
  containerProps?: HTMLAttributes<HTMLDivElement>;
  inputProps?: InputHTMLAttributes<HTMLInputElement>;
  dropdownProps?: HTMLAttributes<HTMLDivElement>;
}

export const CommandComplete = forwardRef(function CommandCompleteForwarded<
  TCommand extends { [C in TCommandLabelKey]: string },
  TCommandLabelKey extends StringKeys<TCommand>,
>(
  {
    minSuggestions = 1,
    maxSuggestions,
    minCharactersToSuggest = 1,
    keysToConfirmSelection = ['Enter', 'Tab'],
    keysToSubmit = ['Enter'],
    appendAfterSelection = ' ',
    onSubmit,
    availableCommands,
    commandLabelKey,
    renderSuggestionItem,
    clearAfterSubmit = true,
    containerProps = {},
    inputProps = {},
    dropdownProps = {},
  }: CommandCompleteProps<TCommand, TCommandLabelKey>,
  ref: React.Ref<HTMLDivElement>,
) {
  const [highlightedOptionIndex, setHighlightedOptionIndex] = useState<
    number | undefined
  >(undefined);
  const [isHistoryMode, setHistoryMode] = useState(false);
  const [submittedHistory, setSubmittedHistory] = useState<Array<string>>([]);
  const [isFocused, setIsFocused] = useState(false);
  const moveCursorRef = useRef(false);

  const onFocus = useCallback(
    (event: FocusEvent<HTMLDivElement>) => {
      setIsFocused(true);
      containerProps.onFocus?.(event);
    },
    [containerProps],
  );
  const onBlur = useCallback(
    (event: FocusEvent<HTMLDivElement>) => {
      setIsFocused(false);
      setHistoryMode(false);
      setHighlightedOptionIndex(undefined);
      containerProps.onBlur?.(event);
    },
    [containerProps],
  );

  const [inputValue, setInputValue] = useState('');
  const onChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      setInputValue(event.currentTarget.value);
      moveCursorRef.current = true;
      setHistoryMode(false);
      setHighlightedOptionIndex(undefined);
      inputProps.onChange?.(event);
    },
    [inputProps],
  );

  const dropdownDivRef = useRef<HTMLDivElement>(null);

  // todo: debounce to prevent excessive rerenders
  const filteredCommands = useMemo(() => {
    const searchValue = inputValue.toLowerCase();

    return availableCommands
      .filter((command) => command[commandLabelKey].indexOf(searchValue) !== -1)
      .sort(
        (commandA, commandB) =>
          commandB[commandLabelKey].localeCompare(searchValue) -
          commandA[commandLabelKey].localeCompare(searchValue),
      )
      .slice(
        0,
        maxSuggestions === undefined
          ? availableCommands.length
          : maxSuggestions,
      );
  }, [availableCommands, commandLabelKey, inputValue, maxSuggestions]);

  const onKeyDown = useCallback(
    (event: KeyboardEvent<HTMLInputElement>) => {
      const localHistoryMode = isHistoryMode || inputValue.length === 0;
      if (event.key === 'ArrowUp') {
        if (localHistoryMode) {
          setHistoryMode(localHistoryMode);
          const nextIndex = Math.max(
            highlightedOptionIndex === undefined
              ? submittedHistory.length - 1
              : highlightedOptionIndex - 1,
            0,
          );
          setInputValue(submittedHistory[nextIndex]);
          moveCursorRef.current = true;
          setHighlightedOptionIndex(nextIndex);
        } else {
          const nextIndex = Math.max(
            highlightedOptionIndex === undefined
              ? filteredCommands.length - 1
              : highlightedOptionIndex - 1,
            0,
          );
          setHighlightedOptionIndex(nextIndex);
          dropdownDivRef.current?.children[nextIndex].scrollIntoView({
            block: 'center',
          });
        }
        event.preventDefault();
      } else if (event.key === 'ArrowDown') {
        if (localHistoryMode) {
          setHistoryMode(localHistoryMode);
          const nextIndex = Math.min(
            highlightedOptionIndex === undefined
              ? 0
              : highlightedOptionIndex + 1,
            submittedHistory.length - 1,
          );
          setInputValue(submittedHistory[nextIndex]);
          moveCursorRef.current = true;
          setHighlightedOptionIndex(nextIndex);
        } else {
          const nextIndex = Math.min(
            highlightedOptionIndex === undefined
              ? 0
              : highlightedOptionIndex + 1,
            filteredCommands.length - 1,
          );
          setHighlightedOptionIndex(nextIndex);
          dropdownDivRef.current?.children[nextIndex].scrollIntoView({
            block: 'center',
          });
        }
        event.preventDefault();
      } else if (
        keysToConfirmSelection.includes(event.key) &&
        highlightedOptionIndex !== undefined
      ) {
        setInputValue(
          `${filteredCommands[highlightedOptionIndex][commandLabelKey]}${appendAfterSelection}`,
        );
        moveCursorRef.current = true;
        setIsFocused(false);
        setHighlightedOptionIndex(undefined);
      } else if (
        keysToSubmit.includes(event.key) &&
        highlightedOptionIndex === undefined
      ) {
        onSubmit?.(inputValue);
        if (submittedHistory[submittedHistory.length - 1] !== inputValue) {
          setSubmittedHistory([...submittedHistory, inputValue]);
        }
        if (clearAfterSubmit) {
          setInputValue('');
        }
      } else {
        setIsFocused(true);
      }

      if (
        (event.key === 'Tab' && keysToConfirmSelection?.includes(event.key)) ||
        keysToSubmit?.includes(event.key)
      ) {
        // do not leave input
        event.preventDefault();
      }
      inputProps.onKeyDown?.(event);
    },
    [
      appendAfterSelection,
      clearAfterSubmit,
      commandLabelKey,
      filteredCommands,
      highlightedOptionIndex,
      inputProps,
      inputValue,
      isHistoryMode,
      keysToConfirmSelection,
      keysToSubmit,
      onSubmit,
      submittedHistory,
    ],
  );

  const onRenderSuggestionItem = useCallback(
    (renderContext: RenderSuggestionItemContext<TCommand>) => {
      if (renderSuggestionItem) {
        return renderSuggestionItem(renderContext);
      }
      const { command, isHighlighted } = renderContext;
      return (
        <div
          className={classNames(
            'p-2 hover:bg-wu-light pointer hover:bg-ci-500',
            isHighlighted && 'bg-ci-500',
          )}
        >
          {command[commandLabelKey]}
        </div>
      );
    },
    [commandLabelKey, renderSuggestionItem],
  );

  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (moveCursorRef.current) {
      moveCursorRef.current = false;
      if (inputRef.current) {
        inputRef.current.selectionStart = inputRef.current.value.length;
      }
    }
  }, [inputValue]);

  return (
    <div
      ref={ref}
      {...containerProps}
      className={classNames('relative', containerProps.className)}
      onFocus={onFocus}
      onBlur={onBlur}
    >
      <input
        type="text"
        value={inputValue}
        {...inputProps}
        ref={inputRef}
        className={classNames('w-full', inputProps.className)}
        onKeyDown={onKeyDown}
        onChange={onChange}
      />
      {isFocused &&
        !isHistoryMode &&
        filteredCommands.length >= minSuggestions &&
        inputValue.length > minCharactersToSuggest && (
          <div
            {...dropdownProps}
            className={classNames(
              'absolute top-full left-0 w-full bg-wu-dark border border-wu-dark-2 rounded-b max-h-[200px] overflow-y-auto',
              dropdownProps?.className,
            )}
            ref={dropdownDivRef}
          >
            {filteredCommands.map((command, index) => (
              <div
                key={`${command[commandLabelKey]}_${
                  highlightedOptionIndex === index ? 'active' : 'not-active'
                }`}
              >
                {onRenderSuggestionItem({
                  command,
                  isHighlighted: highlightedOptionIndex === index,
                })}
              </div>
            ))}
          </div>
        )}
    </div>
  );
});
