import React, {
  useMemo,
  useState,
  useRef,
  useContext,
  createContext,
  useEffect,
  useLayoutEffect
} from 'react';
import PropTypes from 'prop-types';
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
  ComboboxPopover
} from '@reach/combobox';
import '@reach/combobox/styles.css';
import { matchSorter } from 'match-sorter';
import { fire } from '@utils';
import { XIcon } from '@heroicons/react/outline';
import isHotkey from 'is-hotkey';

const Context = createContext();

function ComboMultipleDropdownList({
  options,
  hiddenFieldId,
  selected,
  label,
  acceptNewValues = false
}) {
  if (label == undefined) {
    label = 'Choisir une option';
  }
  if (!Array.isArray(options[0])) {
    options = options.filter((o) => o).map((o) => [o, o]);
  }
  const inputRef = useRef();
  const [term, setTerm] = useState('');
  const [selections, setSelections] = useState(selected);
  const [newValues, setNewValues] = useState([]);

  const optionValueByLabel = (label) => {
    const maybeOption = newValues.includes(label)
      ? [label, label]
      : options.find(([optionLabel]) => optionLabel == label);
    return maybeOption ? maybeOption[1] : undefined;
  };
  const optionLabelByValue = (value) => {
    const maybeOption = newValues.includes(value)
      ? [value, value]
      : options.find(([, optionValue]) => optionValue == value);
    return maybeOption ? maybeOption[0] : undefined;
  };

  const extraOptions = useMemo(
    () =>
      acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
        ? [[term, term]]
        : [],
    [acceptNewValues, term, newValues.join(',')]
  );
  const results = useMemo(
    () =>
      [
        ...extraOptions,
        ...(term
          ? matchSorter(
              options.filter(([label]) => !label.startsWith('--')),
              term
            )
          : options)
      ].filter(([, value]) => !selections.includes(value)),
    [term, selections.join(','), newValues.join(',')]
  );
  const hiddenField = useMemo(
    () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
    [hiddenFieldId]
  );

  const handleChange = (event) => {
    setTerm(event.target.value);
  };

  const onKeyDown = (event) => {
    if (
      isHotkey('enter', event) ||
      isHotkey(' ', event) ||
      isHotkey(',', event) ||
      isHotkey(';', event)
    ) {
      if (
        term &&
        [...extraOptions, ...options].map(([label]) => label).includes(term)
      ) {
        event.preventDefault();
        return onSelect(term);
      }
    }
  };

  const saveSelection = (fn) => {
    setSelections((selections) => {
      selections = fn(selections);
      if (hiddenField) {
        hiddenField.setAttribute('value', JSON.stringify(selections));
        fire(hiddenField, 'autosave:trigger');
      }
      return selections;
    });
  };

  const onSelect = (value) => {
    const maybeValue = [...extraOptions, ...options].find(
      ([val]) => val == value
    );
    const selectedValue = maybeValue && maybeValue[1];
    if (selectedValue) {
      if (
        acceptNewValues &&
        extraOptions[0] &&
        extraOptions[0][0] == selectedValue
      ) {
        setNewValues((newValues) => [...newValues, selectedValue]);
      }
      saveSelection((selections) => [...selections, selectedValue]);
    }
    setTerm('');
  };

  const onRemove = (label) => {
    const optionValue = optionValueByLabel(label);
    if (optionValue) {
      saveSelection((selections) =>
        selections.filter((value) => value != optionValue)
      );
      setNewValues((newValues) =>
        newValues.filter((value) => value != optionValue)
      );
    }
    inputRef.current.focus();
  };

  return (
    <Combobox openOnFocus={true} onSelect={onSelect} aria-label={label}>
      <ComboboxTokenLabel onRemove={onRemove}>
        <ul
          aria-live="polite"
          aria-atomic={true}
          data-reach-combobox-token-list
        >
          {selections.map((selection) => (
            <ComboboxToken
              key={selection}
              value={optionLabelByValue(selection)}
            />
          ))}
        </ul>
        <ComboboxInput
          ref={inputRef}
          value={term}
          onChange={handleChange}
          onKeyDown={onKeyDown}
          autocomplete={false}
        />
      </ComboboxTokenLabel>
      {results && (results.length > 0 || !acceptNewValues) && (
        <ComboboxPopover className="shadow-popup">
          {results.length === 0 && (
            <p>
              Aucun résultat{' '}
              <button onClick={() => setTerm('')}>Effacer</button>
            </p>
          )}
          <ComboboxList>
            {results.map(([label, value], index) => {
              if (label.startsWith('--')) {
                return <ComboboxSeparator key={index} value={label} />;
              }
              return (
                <ComboboxOption
                  key={index}
                  value={label}
                  data-option-value={value}
                />
              );
            })}
          </ComboboxList>
        </ComboboxPopover>
      )}
    </Combobox>
  );
}

function ComboboxTokenLabel({ onRemove, ...props }) {
  const selectionsRef = useRef([]);

  useLayoutEffect(() => {
    selectionsRef.current = [];
    return () => (selectionsRef.current = []);
  });

  const context = {
    onRemove,
    selectionsRef
  };

  return (
    <Context.Provider value={context}>
      <div data-combobox-token-label {...props} />
    </Context.Provider>
  );
}

ComboboxTokenLabel.propTypes = {
  onRemove: PropTypes.func
};

function ComboboxSeparator({ value }) {
  return (
    <li aria-disabled="true" role="option" data-combobox-separator>
      {value.slice(2, -2)}
    </li>
  );
}

ComboboxSeparator.propTypes = {
  value: PropTypes.string
};

function ComboboxToken({ value, ...props }) {
  const { selectionsRef, onRemove } = useContext(Context);
  useEffect(() => {
    selectionsRef.current.push(value);
  });

  return (
    <li
      data-reach-combobox-token
      tabIndex="0"
      onKeyDown={(event) => {
        if (event.key === 'Backspace') {
          onRemove(value);
        }
      }}
      {...props}
    >
      <button
        type="button"
        tabIndex={-1}
        data-combobox-remove-token
        onClick={() => {
          onRemove(value);
        }}
      >
        <XIcon className="icon-size" />
        <span className="screen-reader-text">Désélectionner</span>
      </button>
      {value}
    </li>
  );
}

ComboboxToken.propTypes = {
  value: PropTypes.string,
  label: PropTypes.string
};

ComboMultipleDropdownList.propTypes = {
  options: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.string),
    PropTypes.arrayOf(
      PropTypes.arrayOf(
        PropTypes.oneOfType([PropTypes.string, PropTypes.number])
      )
    )
  ]),
  hiddenFieldId: PropTypes.string,
  selected: PropTypes.arrayOf(PropTypes.string),
  arraySelected: PropTypes.arrayOf(PropTypes.array),
  label: PropTypes.string,
  acceptNewValues: PropTypes.bool
};

export default ComboMultipleDropdownList;
