import React, { useState, useRef, useEffect } from 'react';

import lively from '@jlongster/lively';
import Downshift from 'downshift';
import { css } from 'glamor';

import { colors } from '../style';
import Remove from '../svg/v2/Remove';

import { View, Input, Tooltip, Button } from './common';

function findItem(strict, suggestions, value) {
  if (strict) {
    let idx = suggestions.findIndex(item => item.id === value);
    return idx === -1 ? null : suggestions[idx];
  }

  return value;
}

function getItemName(item) {
  if (item == null) {
    return '';
  } else if (typeof item === 'string') {
    return item;
  }
  return item.name || '';
}

function getItemId(item) {
  if (typeof item === 'string') {
    return item;
  }
  return item ? item.id : null;
}

function getInitialState({
  props: {
    value,
    suggestions,
    embedded,
    isOpen = false,
    strict,
    initialFilterSuggestions
  }
}) {
  let selectedItem = findItem(strict, suggestions, value);
  let filteredSuggestions = initialFilterSuggestions
    ? initialFilterSuggestions(suggestions, value)
    : null;

  return {
    selectedItem,
    value: selectedItem ? getItemName(selectedItem) : '',
    originalItem: selectedItem,
    filteredSuggestions,
    highlightedIndex: null,
    isOpen: embedded || isOpen
  };
}

function componentWillReceiveProps(bag, nextProps) {
  let {
    strict,
    suggestions,
    filterSuggestions = defaultFilterSuggestions,
    initialFilterSuggestions,
    value,
    itemToString = defaultItemToString
  } = nextProps;
  let { value: currValue } = bag.state;
  let updates = null;

  function updateValue() {
    let selectedItem = findItem(strict, suggestions, value);
    if (selectedItem) {
      updates = updates || {};
      updates.value = itemToString(selectedItem);
      updates.selectedItem = selectedItem;
    }
  }

  if (bag.props.value !== value) {
    updateValue();
  }

  // TODO: Something is causing a rerender immediately after first
  // render, and this condition is true, causing items to be filtered
  // twice. This shouldn't effect functionality (I think), but look
  // into this later
  if (bag.props.suggestions !== suggestions) {
    let filteredSuggestions = null;

    if (bag.state.highlightedIndex != null) {
      filteredSuggestions = filterSuggestions(suggestions, currValue);
    } else {
      filteredSuggestions = initialFilterSuggestions
        ? initialFilterSuggestions(suggestions, currValue)
        : null;
    }

    updates = updates || {};
    updateValue();
    updates.filteredSuggestions = filteredSuggestions;
  }

  return updates;
}

export function defaultFilterSuggestion(suggestion, value) {
  return getItemName(suggestion).toLowerCase().includes(value.toLowerCase());
}

export function defaultFilterSuggestions(suggestions, value) {
  return suggestions.filter(suggestion =>
    defaultFilterSuggestion(suggestion, value)
  );
}

function fireUpdate(onUpdate, strict, suggestions, index, value) {
  // If the index is null, look up the id in the suggestions. If the
  // value is empty it will select nothing (as expected). If it's not
  // empty but nothing is selected, it still resolves to an id. It
  // would very confusing otherwise: the menu could be in a state
  // where nothing is highlighted but there is a valid value.

  let selected = null;
  if (!strict) {
    selected = value;
  } else {
    if (index == null) {
      // If passing in a value directly, validate the id
      let sug = suggestions.find(sug => sug.id === value);
      if (sug) {
        selected = sug.id;
      }
    } else if (index < suggestions.length) {
      selected = suggestions[index].id;
    }
  }

  onUpdate && onUpdate(selected);
}

function onInputValueChange(
  {
    props: {
      suggestions,
      onUpdate,
      multi,
      highlightFirst,
      strict,
      filterSuggestions = defaultFilterSuggestions,
      getHighlightedIndex
    },
    state: { isOpen }
  },
  value,
  changes
) {
  // OMG this is the dumbest thing ever. I need to remove Downshift
  // and build my own component. For some reason this is fired on blur
  // with an empty value which clears out the input when the app blurs
  if (!document.hasFocus()) {
    return;
  }

  // Do nothing if it's simply updating the selected item
  if (
    changes.type ===
    Downshift.stateChangeTypes.controlledPropUpdatedSelectedItem
  ) {
    return;
  }

  // Otherwise, filter the items and always the first item if
  // desired
  const filteredSuggestions = filterSuggestions(suggestions, value);

  if (value === '') {
    // A blank value shouldn't highlight any item so that the field
    // can be left blank if desired

    if (changes.type !== Downshift.stateChangeTypes.clickItem) {
      fireUpdate(onUpdate, strict, filteredSuggestions, null, null);
    }

    return {
      value,
      filteredSuggestions,
      highlightedIndex: null
    };
  } else {
    let defaultGetHighlightedIndex = filteredSuggestions => {
      return highlightFirst && filteredSuggestions.length ? 0 : null;
    };
    let highlightedIndex = (getHighlightedIndex || defaultGetHighlightedIndex)(
      filteredSuggestions
    );

    if (changes.type !== Downshift.stateChangeTypes.clickItem) {
      fireUpdate(
        onUpdate,
        strict,
        filteredSuggestions,
        highlightedIndex,
        value
      );
    }

    return {
      value,
      filteredSuggestions,
      highlightedIndex
    };
  }
}

function onStateChange({ props, state, inst }, changes, stateAndHelpers) {
  if (
    props.tableBehavior &&
    changes.type === Downshift.stateChangeTypes.mouseUp
  ) {
    return;
  }

  const newState = {};
  if ('highlightedIndex' in changes) {
    newState.highlightedIndex = changes.highlightedIndex;
  }
  if ('isOpen' in changes) {
    newState.isOpen = props.embedded ? true : changes.isOpen;
  }
  if ('selectedItem' in changes) {
    newState.selectedItem = changes.selectedItem;
  }

  // We only ever want to update the value if the user explicitly
  // highlighted an item via the keyboard. It shouldn't change with
  // mouseover; otherwise the user could accidentally hover over an
  // item without realizing it and change the value.
  if (
    state.isOpen &&
    (changes.type === Downshift.stateChangeTypes.keyDownArrowUp ||
      changes.type === Downshift.stateChangeTypes.keyDownArrowDown)
  ) {
    fireUpdate(
      props.onUpdate,
      props.strict,
      state.filteredSuggestions || props.suggestions,
      newState.highlightedIndex != null
        ? newState.highlightedIndex
        : state.highlightedIndex,
      state.value
    );
  }

  inst.lastChangeType = changes.type;
  return newState;
}

function onSelect(
  { props: { onSelect, clearAfterSelect, suggestions }, inst },
  item
) {
  if (onSelect) {
    // I AM NOT PROUD OF THIS OK??
    // This WHOLE FILE is a mess anyway
    // OK SIT DOWN AND I WILL EXPLAIN
    // This component uses `componentWillReceiveProps` and in there
    // it will re-filter suggestions if the suggestions change and
    // a `highlightedIndex` exists. When we select something,
    // we clear `highlightedIndex` so it should show all suggestions
    // again. HOWEVER, in the case of a multi-autocomplete, it's
    // changing the suggestions every time something is selected.
    // In that case, cWRP is running *before* our state setting that
    // cleared `highlightedIndex`. Forcing this to run later assures
    // us that we will clear out local state before cWRP runs.
    // YEAH THAT'S ALL OK I JUST WANT TO SHIP THIS
    setTimeout(() => {
      onSelect(getItemId(item));
    }, 0);
  }
  return onSelectAfter(suggestions, clearAfterSelect, inst);
}

function onSelectAfter(suggestions, clearAfterSelect, inst) {
  if (clearAfterSelect) {
    return {
      value: '',
      selectedItem: null,
      highlightedIndex: null,
      filteredSuggestions: suggestions
    };
  } else if (inst.input) {
    inst.input.setSelectionRange(0, 10000);
  }
}

function onChange({ props: { inputProps } }, e) {
  const { onChange } = inputProps || {};
  onChange && onChange(e.target.value);
}

function onKeyDown(
  {
    props: {
      suggestions,
      clearAfterSelect,
      initialFilterSuggestions,
      embedded,
      onUpdate,
      onSelect,
      inputProps,
      shouldSaveFromKey = defaultShouldSaveFromKey,
      strict
    },
    state: {
      selectedItem,
      filteredSuggestions,
      highlightedIndex,
      originalItem,
      isNulled,
      isOpen,
      value
    },
    inst
  },
  e
) {
  let ENTER = 13;
  let ESC = 27;
  let { onKeyDown } = inputProps || {};

  // If the dropdown is open, an item is highlighted, and the user
  // pressed enter, always capture that and handle it ourselves
  if (isOpen) {
    if (e.keyCode === ENTER) {
      if (highlightedIndex != null) {
        if (inst.lastChangeType === Downshift.stateChangeTypes.itemMouseEnter) {
          // If the last thing the user did was hover an item, intentionally
          // ignore the default behavior of selecting the item. It's too
          // common to accidentally hover an item and then save it
          e.preventDefault();
        } else {
          // Otherwise, stop propagation so that the table navigator
          // doesn't handle it
          e.stopPropagation();
        }
      } else if (!strict) {
        // Handle it ourselves
        e.stopPropagation();
        onSelect(value);
        return onSelectAfter(suggestions, clearAfterSelect, inst);
      } else {
        // No highlighted item, still allow the table to save the item
        // as `null`, even though we're allowing the table to move
        e.preventDefault();
        onKeyDown && onKeyDown(e);
      }
    } else if (shouldSaveFromKey(e)) {
      e.preventDefault();
      onKeyDown && onKeyDown(e);
    }
  }

  // Handle escape ourselves
  if (e.keyCode === ESC) {
    e.preventDefault();

    if (!embedded) {
      e.stopPropagation();
    }

    let filteredSuggestions = initialFilterSuggestions
      ? initialFilterSuggestions(suggestions, getItemName(originalItem))
      : null;
    fireUpdate(onUpdate, strict, suggestions, null, getItemId(originalItem));
    return {
      value: getItemName(originalItem),
      selectedItem: findItem(strict, suggestions, originalItem),
      filteredSuggestions,
      highlightedIndex: null,
      isOpen: embedded ? true : false
    };
  }
}

function defaultRenderInput(props) {
  return <Input {...props} />;
}

function defaultRenderItems(items, getItemProps, highlightedIndex) {
  return (
    <div>
      {items.map((item, index) => {
        let name = getItemName(item);
        return (
          <div
            {...getItemProps({ item })}
            key={name}
            {...css({
              padding: 5,
              cursor: 'default',
              backgroundColor: highlightedIndex === index ? colors.n4 : null
            })}
          >
            {name}
          </div>
        );
      })}
    </div>
  );
}

function defaultShouldSaveFromKey(e) {
  // Enter
  return e.keyCode === 13;
}

function onFocus({ inst, props: { inputProps = {}, openOnFocus = true } }, e) {
  inputProps.onFocus && inputProps.onFocus(e);

  if (openOnFocus) {
    return { isOpen: true };
  }
}

function onBlur({ inst, props, state: { selectedItem } }, e) {
  let { inputProps = {}, onSelect } = props;

  e.preventDownshiftDefault = true;
  inputProps.onBlur && inputProps.onBlur(e);

  if (!props.tableBehavior) {
    if (e.target.value === '') {
      onSelect && onSelect(null);
      return { selectedItem: null, originalValue: null, isOpen: false };
    }

    // If not using table behavior, reset the input on blur. Tables
    // handle saving the value on blur.
    let value = selectedItem ? getItemId(selectedItem) : null;

    return getInitialState({
      props: {
        ...props,
        value,
        originalValue: value
      }
    });
  } else {
    return { isOpen: false };
  }
}

function defaultItemToString(item) {
  return item ? getItemName(item) : '';
}

function _SingleAutocomplete({
  props: {
    focused,
    embedded,
    containerProps,
    inputProps,
    children,
    suggestions,
    tooltipStyle,
    onItemClick,
    strict,
    tooltipProps,
    renderInput = defaultRenderInput,
    renderItems = defaultRenderItems,
    itemToString = defaultItemToString
  },
  state: { value, selectedItem, filteredSuggestions, highlightedIndex, isOpen },
  updater,
  inst
}) {
  const filtered = filteredSuggestions || suggestions;

  return (
    <Downshift
      onSelect={updater(onSelect)}
      highlightedIndex={highlightedIndex}
      selectedItem={selectedItem || null}
      itemToString={itemToString}
      inputValue={value}
      isOpen={isOpen}
      onInputValueChange={updater(onInputValueChange)}
      onStateChange={updater(onStateChange)}
    >
      {({
        getInputProps,
        getItemProps,
        getRootProps,
        isOpen,
        inputValue,
        selectedItem,
        highlightedIndex
      }) => (
        // Super annoying but it works best to return a div so we
        // can't use a View here, but we can fake it be using the
        // className
        <div
          className={'view ' + css({ display: 'flex' }).toString()}
          {...containerProps}
        >
          {renderInput(
            getInputProps({
              focused,
              ...inputProps,
              onFocus: updater(onFocus),
              onBlur: updater(onBlur),
              onKeyDown: updater(onKeyDown),
              onChange: updater(onChange)
            })
          )}
          {isOpen &&
            filtered.length > 0 &&
            (embedded ? (
              <View style={{ marginTop: 5 }} data-testid="autocomplete">
                {renderItems(
                  filtered,
                  getItemProps,
                  highlightedIndex,
                  inputValue
                )}
              </View>
            ) : (
              <Tooltip
                position="bottom-stretch"
                offset={2}
                style={{
                  padding: 0,
                  backgroundColor: colors.n1,
                  color: 'white',
                  ...tooltipStyle
                }}
                {...tooltipProps}
                data-testid="autocomplete"
              >
                {renderItems(
                  filtered,
                  getItemProps,
                  highlightedIndex,
                  inputValue
                )}
              </Tooltip>
            ))}
        </div>
      )}
    </Downshift>
  );
}

const SingleAutocomplete = lively(_SingleAutocomplete, {
  getInitialState,
  componentWillReceiveProps
});

function MultiItem({ name, onRemove }) {
  return (
    <View
      style={{
        alignItems: 'center',
        flexDirection: 'row',
        backgroundColor: colors.b9,
        padding: '2px 4px',
        margin: '2px',
        borderRadius: 4
      }}
    >
      {name}
      <Button type="button" bare style={{ marginLeft: 1 }} onClick={onRemove}>
        <Remove style={{ width: 8, height: 8 }} />
      </Button>
    </View>
  );
}

export function MultiAutocomplete({
  value: selectedItems,
  onSelect,
  suggestions,
  strict,
  ...props
}) {
  let [focused, setFocused] = useState(false);
  let lastSelectedItems = useRef();

  useEffect(() => {
    lastSelectedItems.current = selectedItems;
  });

  function onRemoveItem(id) {
    let items = selectedItems.filter(i => i !== id);
    onSelect(items);
  }

  function onAddItem(id) {
    if (id) {
      id = id.trim();
      onSelect([...selectedItems, id], id);
    }
  }

  function onKeyDown(e, prevOnKeyDown) {
    if (e.key === 'Backspace' && e.target.value === '') {
      onRemoveItem(selectedItems[selectedItems.length - 1]);
    }

    prevOnKeyDown && prevOnKeyDown(e);
  }

  return (
    <Autocomplete
      {...props}
      value={null}
      suggestions={suggestions.filter(
        item => !selectedItems.includes(getItemId(item))
      )}
      onSelect={onAddItem}
      clearAfterSelect
      highlightFirst
      strict={strict}
      tooltipProps={{
        forceLayout: lastSelectedItems.current !== selectedItems
      }}
      renderInput={props => (
        <View
          style={[
            {
              display: 'flex',
              flexWrap: 'wrap',
              flexDirection: 'row',
              alignItems: 'center',
              backgroundColor: 'white',
              borderRadius: 4,
              border: '1px solid #d0d0d0'
            },
            focused && {
              border: '1px solid ' + colors.b5,
              boxShadow: '0 1px 1px ' + colors.b7
            }
          ]}
        >
          {selectedItems.map((item, idx) => {
            item = findItem(strict, suggestions, item);
            return (
              item && (
                <MultiItem
                  key={getItemId(item) || idx}
                  name={getItemName(item)}
                  onRemove={() => onRemoveItem(getItemId(item))}
                />
              )
            );
          })}
          <Input
            {...props}
            onKeyDown={e => onKeyDown(e, props.onKeyDown)}
            onFocus={e => {
              setFocused(true);
              props.onFocus(e);
            }}
            onBlur={e => {
              setFocused(false);
              props.onBlur(e);
            }}
            style={[
              {
                flex: 1,
                minWidth: 30,
                border: 0,
                ':focus': { border: 0, boxShadow: 'none' }
              },
              props.style
            ]}
          />
        </View>
      )}
    />
  );
}

export function AutocompleteFooterButton({
  title,
  style,
  hoveredStyle,
  onClick
}) {
  return (
    <Button
      style={[
        {
          fontSize: 12,
          color: colors.n10,
          backgroundColor: 'transparent',
          borderColor: colors.n5
        },
        style
      ]}
      hoveredStyle={[
        { backgroundColor: 'rgba(200, 200, 200, .25)' },
        hoveredStyle
      ]}
      onClick={onClick}
    >
      {title}
    </Button>
  );
}

export function AutocompleteFooter({ show = true, embedded, children }) {
  return (
    show && (
      <View
        style={[
          { flexShrink: 0 },
          embedded ? { paddingTop: 5 } : { padding: 5 }
        ]}
        onMouseDown={e => e.preventDefault()}
      >
        {children}
      </View>
    )
  );
}

export default function Autocomplete({ multi, ...props }) {
  if (multi) {
    return <MultiAutocomplete {...props} />;
  } else {
    return <SingleAutocomplete {...props} />;
  }
}