import { getSearchReg } from '@vev/utils';
import { isEqual, isUndefined } from 'lodash';
import React, {
  Children,
  ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useFormField, useInterval, useParentScroll, useWindowResize } from '../../hooks';
import { SilkeBox } from '../silke-box';
import { SilkeDivider } from '../silke-divider';
import { FormValidator } from '../silke-form';
import { SilkeIcon } from '../silke-icon';
import { PopoverOrigin, SilkePopover } from '../silke-popover';
import { SilkeTextSmall } from '../silke-text';
import { SilkeTextField, SilkeTextFieldItem } from '../silke-text-field';
import { useTextFieldContext } from '../silke-text-field/text-field-context';
import AutocompleteItem from './autocomplete-item';
import { Data, Entry } from './autocomplete-types';
import styles from './silke-autocomplete-field.scss';
import { SilkeAutocompleteFieldProps } from './silke-autocomplete-field';
import { SilkeButton } from '../silke-button';

/**
 * Props for autocomplete popup
 */
type SilkeAutocompletePopupProps<T = any> = SilkeAutocompleteFieldProps<T> & {
  containerRef?: React.RefObject<HTMLDivElement>;
  searchPlaceholder?: string;
  onRequestClose?: () => void;
};

const autocompleteValidator: FormValidator<SilkeAutocompletePopupProps> = (props, value) => {
  if (props.multiple) {
    const len = value?.length || 0;
    if (len > (props.maxItems || 1000)) return `To many item, max ${props.maxItems}`;
    if (len < (props.minItems || 0)) return `To few item, min ${props.minItems}`;
  }
  if (props.required && !value && value !== 0) return 'Required';

  return undefined;
};

/**
 * Checks if any of an `Entry`'s fields match the query
 * @param entry
 * @param search Text to search for
 */
const filter = ({ label, subLabel, value }: Entry<any>, search: RegExp): boolean =>
  (value && value.toString && search.test(value.toString() + '')) ||
  (label ? search.test(label) : false) ||
  (subLabel ? search.test(subLabel) : false);

const normalizeString = (str?: string | number): string =>
  str ? str.toString().toLowerCase() : '';

/**
 * Checks if any of an `Entry`'s fields match the query
 * @param entry
 * @param searchString Text string to search for
 */
const filterString = ({ label, subLabel, value }: Entry<any>, searchString: string): boolean => {
  const normalizedSearchString = normalizeString(searchString);

  return (
    normalizeString(label).startsWith(normalizedSearchString) ||
    normalizeString(value).startsWith(normalizedSearchString) ||
    normalizeString(subLabel).startsWith(normalizedSearchString)
  );
};

const sorter: SilkeAutocompletePopupProps<any>['sort'] = () => 0;

const putMatchingItemsOnTop = (filteredItems: Entry<any>[], searchString: string): Entry<any>[] => {
  const itemsWithMatch = filteredItems.filter((item) => filterString(item, searchString));
  const itemsWithNoMatch = filteredItems.filter((item) => !filterString(item, searchString));

  return [...itemsWithMatch, ...itemsWithNoMatch];
};

type GroupedEntries<T> = { groupLabel?: string; items: Entry<T>[] };

function groupEntriesByLabel<T>(entries: Entry<T>[]): GroupedEntries<T>[] {
  const grouped: GroupedEntries<T>[] = [];
  let currentGroup: GroupedEntries<T> | null = null;

  entries.forEach((entry) => {
    const groupLabel = !entry.divider ? (entry as Data<T>).groupLabel : undefined;

    if (!currentGroup || currentGroup.groupLabel !== groupLabel) {
      currentGroup = { groupLabel, items: [] };
      grouped.push(currentGroup);
    }

    currentGroup.items.push(entry);
  });

  return grouped;
}

export const useMatchingItemsOnTop = (
  filteredItems: Entry<any>[],
  searchString?: string,
  selectedLabel?: string,
): Entry<any>[] => {
  return useMemo(
    () =>
      !searchString || searchString === selectedLabel
        ? filteredItems
        : putMatchingItemsOnTop(filteredItems, searchString),
    [searchString, filteredItems, selectedLabel],
  );
};

export function SilkeAutocompletePopup<T>(props: SilkeAutocompletePopupProps<T>) {
  const {
    disableSearch,
    disableAddFromList,
    selectedVariableKey,
    containerRef,
    items,
    onAdd,
    renderItem: ItemRender,
    sort = sorter,
    label,
    anchorOrigin,
    targetOrigin,
    maxDisplayedItems = 150,
    dropDownMinWidth,
    dropDownMaxWidth,
    onSearch,
    onClose,
    onRequestClose,
    buttonRight,
    searchPlaceholder,
    ...form
  } = useFormField(props, autocompleteValidator);
  const context = useTextFieldContext();
  const size = form.size || context.size || 'base';

  const inputField = useRef<HTMLDivElement>();
  const isSelected = useCallback(
    (value: T): boolean => {
      if (form.multiple) {
        const values = Array.isArray(form.value) ? form.value : [];
        for (const v of values) {
          if (isEqual(v, value) || isEqual(value, selectedVariableKey)) return true;
        }
        return false;
      }
      return isEqual(value, form.value) || isEqual(value, selectedVariableKey);
    },
    [form.multiple, form.value, selectedVariableKey],
  );

  function getIndexInDir(
    currentIndex: number,
    listLength: number,
    dir: 'ArrowUp' | 'ArrowDown',
  ): number {
    if (dir === 'ArrowUp') {
      return currentIndex === 0 ? listLength - 1 : currentIndex - 1;
    }
    return currentIndex === listLength - 1 ? 0 : currentIndex + 1;
  }

  const [selectedLabel] = useMemo((): [label?: string] => {
    if (form.multiple) return [];
    const entry = items.find((entry) => !entry.divider && isEqual(entry.value, form.value));

    return [entry?.label || (form.value && form.value.toString()) || ''];
  }, [items, form.multiple, form.value]);

  const [searchText, _setSearchText] = useState<undefined | string>();
  const [prevSearchText, setPrevSearchText] = useState(searchText);
  const inputRef = useRef<HTMLInputElement>();
  const dropdownRef = useRef<HTMLElement>();
  const [highlight, setHighlight] = useState<undefined | number>(undefined);

  const before: ReactNode[] = [];
  const after: ReactNode[] = [];
  Children.forEach(form.children, (child) => {
    if (React.isValidElement(child) && child.type === SilkeTextFieldItem && child.props.before) {
      before.push(child);
    } else {
      after.push(child);
    }
  });

  /**
   * Keep track of previous value, so we know when to remove an item in the list
   */
  const setSearchText = useCallback((value: string | undefined) => {
    _setSearchText((previousValue) => {
      setPrevSearchText(previousValue);
      return value;
    });
  }, []);

  /**
   * Set text field to display selected value
   */
  const resetSearchText = useCallback(() => {
    // Update display of selected item
    setSearchText('');
  }, []);

  useEffect(() => {
    if (!form.multiple && !isUndefined(form.value)) {
      resetSearchText();
    }
  }, [form.multiple, resetSearchText, form.value]);

  useEffect(() => {
    if (highlight !== undefined) {
      dropdownRef.current?.children.item(highlight)?.scrollIntoView(false);
    }
  }, [highlight]);

  const handleChange = (nuValue: T | undefined) => {
    if (form.multiple) {
      form.onChange([...(Array.isArray(form.value) ? form.value : []), nuValue]);
    } else {
      form.onChange(nuValue as any);
    }
  };

  const handleRemoveSingle = (remove: T) => {
    if (form.multiple && form.value) {
      const value = Array.isArray(form.value) ? form.value.filter((v: T) => remove !== v) : [];

      if (!isEqual(value, form.value)) form.onChange(value);
    }
  };

  const searchReg = useMemo(() => {
    return searchText && searchText !== selectedLabel ? getSearchReg(searchText) : undefined;
  }, [searchText, selectedLabel]);

  const filteredItems = useMemo(() => {
    return searchReg ? items.filter((entry) => filter(entry, searchReg)) : items;
  }, [searchReg, items]);

  const itemsWithMatchingOnTop = useMatchingItemsOnTop(filteredItems, searchText, selectedLabel);

  const [displayedItems, isHidingResults]: [Entry<T>[], boolean] = useMemo(() => {
    return [
      itemsWithMatchingOnTop.slice(0, maxDisplayedItems),
      itemsWithMatchingOnTop.length > maxDisplayedItems,
    ];
  }, [itemsWithMatchingOnTop, maxDisplayedItems]);

  const sortedItems = useMemo<Entry<T>[]>(
    () => [...displayedItems].sort(sort),
    [displayedItems, sort],
  );
  const groupedItems = useMemo(() => groupEntriesByLabel(sortedItems), [sortedItems]);
  const navigableItems = useMemo(
    () => sortedItems.filter((entry) => !entry.divider) as Data<T>[],
    [sortedItems],
  );

  const isItemSelected = (item: string): boolean => {
    return (
      Array.isArray(form.value) &&
      form.value.map((s) => String(s).toUpperCase()).includes(item.trim().toUpperCase())
    );
  };

  const findItem = (item: string) => {
    return filteredItems.find((listItem) => {
      return (listItem.label || listItem.value).toUpperCase() === item.toUpperCase().trim();
    });
  };

  const [overridePopoverPlacement, setOverridePopoverPlacement] = useState<{
    anchor: PopoverOrigin;
    target: PopoverOrigin;
  }>();

  const updatePlacement = () => {
    if (containerRef && containerRef.current) {
      const rect = containerRef.current.getBoundingClientRect();
      const height = window.innerHeight;
      const spaceUnderElement = height - rect.top - rect.height;
      if (spaceUnderElement < 300) {
        setOverridePopoverPlacement({
          anchor: 'top-left',
          target: 'bottom-left',
        });
      } else {
        setOverridePopoverPlacement(undefined);
      }
    }
  };

  useInterval(updatePlacement, 1000);
  useParentScroll(containerRef, updatePlacement);
  useWindowResize(updatePlacement);

  const getIndexOfFirstSelectedItem = useCallback(() => {
    for (let i = 0; i < navigableItems.length; i++) {
      if (isSelected(navigableItems[i].value)) {
        return i;
      }
    }

    return 0;
  }, [isSelected, navigableItems]);

  useEffect(() => {
    if (highlight === undefined) {
      const indexOfFirstSelectedItem = getIndexOfFirstSelectedItem();
      setHighlight(indexOfFirstSelectedItem);
    }
  }, [getIndexOfFirstSelectedItem, highlight]);

  const handleSelect = (item?: Entry<T>) => {
    inputField?.current?.focus();
    if (!item) {
      resetSearchText();
    } else if (!item.divider) {
      if (form.multiple) {
        if (isSelected(item.value)) handleRemoveSingle(item.value);
        else handleChange(item.value);
        resetSearchText();
      } else {
        resetSearchText();
        handleChange(item.value);
        onRequestClose && onRequestClose();
        onClose && onClose();

        setHighlight(undefined);
      }
    }
  };

  const handleKeyUp = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'Backspace': {
        if (prevSearchText?.length === 1) {
          setPrevSearchText('');
        } else if (form.multiple && form.value && !searchText) {
          if (Array.isArray(form.value)) form.onChange(form.value.slice(0, -1));
        }

        break;
      }
      case 'Enter': {
        if (form.multiple && onAdd && searchText) {
          if (!isItemSelected(searchText)) {
            const item = findItem(searchText);
            if (!item) {
              onAdd(searchText);
            } else {
              handleChange(item.value);
            }
          }
          setSearchText('');
          setPrevSearchText('');
        } else {
          const item = highlight !== undefined && navigableItems[highlight];
          if (item) {
            handleSelect(item);
          } else {
            if (onAdd && searchText) {
              onAdd(searchText);
            }
          }
        }
        inputField.current?.focus();
        onRequestClose && onRequestClose();
        onClose && onClose();
        break;
      }
      case 'ArrowUp':
      case 'ArrowDown': {
        e.preventDefault();
        e.stopPropagation();
        const { key } = e;
        setHighlight((value) => getIndexInDir(value || 0, navigableItems.length, key));
        break;
      }
      case 'Escape': {
        inputField.current?.focus();
        onRequestClose && onRequestClose();
        onClose && onClose();
        break;
      }
    }
  };

  const dataCy = props['data-cy'] || undefined;
  const iconFontSize = size === 's' ? '12px' : undefined;

  // Only match anchor width if dropDownMinWidth not defined or container width is larger than dropDownMinWidth
  const matchAnchorWidth =
    !dropDownMinWidth ||
    (!!containerRef?.current && containerRef?.current?.clientWidth > dropDownMinWidth);

  return (
    <SilkePopover
      offsetY={1}
      contextId="autocomplete-dropdown"
      anchor={containerRef}
      anchorOrigin={overridePopoverPlacement?.anchor || anchorOrigin || 'bottom-left'}
      targetOrigin={overridePopoverPlacement?.target || targetOrigin || 'top-left'}
      matchAnchorWidth={matchAnchorWidth}
      onRequestClose={() => {
        onRequestClose && onRequestClose();
        onClose && onClose();
      }}
    >
      <SilkeBox
        role="listbox"
        rounded={size === 's' ? 'tiny' : 'small'}
        shadow="level3"
        data-cy={dataCy ? dataCy + '-dropdown' : undefined}
        bg="neutral-10"
        column
        style={{
          maxHeight: 300,
          minWidth: dropDownMinWidth,
          maxWidth: dropDownMaxWidth,
          border: '1px solid var(--color-surface-2)',
        }}
        gap={disableSearch ? undefined : 'xs'}
        vPad="xs"
        value={form.value as any}
        onKeyUp={handleKeyUp}
        ref={dropdownRef}
      >
        <SilkeBox hPad="xs">
          <SilkeTextField
            gap={size === 's' ? 'xxs' : 'xs'}
            inline="label-inside"
            label={<SilkeIcon icon="search" />}
            aria-label={'Search for ' + label}
            autoFocus
            size={size}
            className={disableSearch ? styles.hiddenSearch : undefined}
            placeholder={searchPlaceholder || 'Search'}
            kind="ghost"
            onFocus={(e) => {
              inputRef.current = e.currentTarget as HTMLInputElement;
              if (props.onFocus) props.onFocus(e);
            }}
            onBlur={(e) => {
              if (props.onBlur) props.onBlur(e);
            }}
            onChange={(text: string) => {
              onSearch?.(text);
              setHighlight(0);
              setSearchText(text);
            }}
            onKeyUp={handleKeyUp}
            onKeyDown={(e: React.KeyboardEvent) => {
              if (e.key === 'Tab') {
                inputField.current?.focus();
              }

              if (['Enter', 'ArrowUp', 'ArrowDown', 'Escape'].includes(e.key)) {
                e.preventDefault();
                e.stopPropagation();
              }
            }}
            value={searchText}
          >
            {props.showAddIcon && onAdd && (
              <SilkeTextFieldItem hPad="xs">
                <SilkeButton icon="add" kind="ghost" size="s" onClick={() => onAdd('variable')} />
              </SilkeTextFieldItem>
            )}
            {buttonRight && <div onClick={() => onRequestClose?.()}>{buttonRight}</div>}
          </SilkeTextField>
        </SilkeBox>
        {!disableSearch && <SilkeDivider dark />}
        <SilkeBox column scroll flex hPad="xs">
          {displayedItems.length === 0 && (
            <SilkeBox hPad="s" height={size} vAlign="center">
              <SilkeTextSmall color="neutral-60">No results</SilkeTextSmall>
            </SilkeBox>
          )}
          {(() => {
            let navigableIndex = 0;

            return groupedItems.map((group, groupIdx) => (
              <React.Fragment key={`group-${groupIdx}`}>
                {group.groupLabel && (
                  <SilkeBox hPad="s" vPad="s">
                    <SilkeTextSmall color="neutral-60" weight="strong">
                      {group.groupLabel}
                    </SilkeTextSmall>
                  </SilkeBox>
                )}
                {group.items.map((item, itemIdx) => {
                  if (item.divider) {
                    return <SilkeDivider dark key={`group-${groupIdx}-divider-${itemIdx}`} />;
                  }

                  const currentIndex = navigableIndex++;

                  return (
                    <AutocompleteItem
                      ItemRender={ItemRender}
                      key={`group-${groupIdx}-item-${itemIdx}`}
                      size={size}
                      iconFontSize={iconFontSize}
                      item={item}
                      highlight={currentIndex === highlight}
                      selected={isSelected(item.value)}
                      multiple={form.multiple}
                      onMouseDown={(e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        if (!item.disabled) handleSelect(item);
                      }}
                    />
                  );
                })}
              </React.Fragment>
            ));
          })()}
          {onAdd && !disableAddFromList && searchText && (
            <AutocompleteItem
              size={size}
              iconFontSize={iconFontSize}
              highlight={!displayedItems.length}
              item={{
                label: `+ Add item "${searchText}"`,
                value: undefined,
              }}
              onMouseDown={(e) => {
                e.preventDefault();
                onAdd(searchText);
                setSearchText('');
              }}
            />
          )}
          {isHidingResults
            ? props.maxDisplayedItemsLabel || (
                <>
                  <SilkeDivider dark />
                  <SilkeBox pad="l">
                    Currently displaying the {maxDisplayedItems} first results, please enter a more
                    specific query to see the rest
                  </SilkeBox>
                </>
              )
            : null}
        </SilkeBox>
      </SilkeBox>
    </SilkePopover>
  );
}
