import classNames from 'classnames';
import debounce from 'lodash/debounce';
import _get from 'lodash/get';
import extend from 'extend';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';

import AutocompleteOption from '../AutocompleteOption';
import RequestHelper from '../../utils/request';
import { NON_SELECTED, ARROW_UP, ARROW_DOWN, ENTER, ESCAPE } from '../../constants';

const defaultProps = {
  autoComplete: {
    minLength: 3,
    delay: 350,
    infoBar: {
      visible: false,
      leftLabel: '',
      rightLabel: '',
    },
    events: {
      valueModifier: v => v,
    },
    search: {},
    clearAfterSelection: true,

    filters: [],
    relatedEntities: [],
  },

  // private props
  isChickletNone: false,
  hasInitialValues: false,
};

const propTypes = {
  autoComplete: PropTypes.shape({
    url: PropTypes.string,
    key: PropTypes.string,
    bearerToken: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
    minLength: PropTypes.number,
    delay: PropTypes.number,
    placeholderText: PropTypes.string,
    filters: PropTypes.arrayOf(PropTypes.object),
    relatedEntities: PropTypes.arrayOf(PropTypes.string),
    clearAfterSelection: PropTypes.bool,
    events: PropTypes.shape({
      onSuccess: PropTypes.func,
      onRender: PropTypes.func,
      onSelect: PropTypes.func,
      onFail: PropTypes.func,
      onDone: PropTypes.func,
      valueModifier: PropTypes.func,
      beforeSend: PropTypes.func,
      onFocus: PropTypes.func,
      onBlur: PropTypes.func,
      onLoadingStart: PropTypes.func,
      onLoadingEnd: PropTypes.func,
    }),
    infoBar: PropTypes.shape({
      visible: PropTypes.bool,
      leftLabel: PropTypes.string,
      rightLabel: PropTypes.string,
    }),
    search: PropTypes.shape({
      url: PropTypes.func,
      data: PropTypes.array,
    }),
  }),

  // private props
  setSelectedItems: PropTypes.func,
  isChickletNone: PropTypes.bool,
  hasInitialValues: PropTypes.bool,
  lastInitialValue: PropTypes.object,
  handleRemove: PropTypes.func,
};

class Autocomplete extends React.Component {
  constructor(props) {
    super(props);
    this.autoComplete = extend(true, {}, defaultProps.autoComplete, this.props.autoComplete);
    this.state = {
      displayedOptions: [],
      isLoading: false,
      activeOption: NON_SELECTED,
    };
  }

  componentWillReceiveProps(nextProps) {
    this.autoComplete = extend(true, {}, defaultProps.autoComplete, nextProps.autoComplete);
  }

  handleOutsideClick = e => {
    if (this.rootElement === null || ReactDOM.findDOMNode(this).contains(e.target)) {
      return;
    }
    this.resetDisplayed();
  };

  componentWillMount() {
    document.addEventListener('click', this.handleOutsideClick, false);
    this.delayedGetOptions = debounce(this.getOptions, this.autoComplete.delay);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleOutsideClick, false);
  }

  fetchByType = ({ value, type, source, dataFormatter, itemFormatter }) => {
    const {
      autoComplete: {
        delay,
        filters,
        relatedEntities,
        events: { onDone, onSuccess, onLoadingStart, onLoadingEnd, onFail, beforeSend, valueModifier },
      },
    } = this;
    const parseResult = elem => {
      const item = itemFormatter(elem);
      if (onSuccess) {
        onSuccess(item);
      }
      return item;
    };

    const customOnDone = data => {
      onDone && onDone(data);
      this.setState(
        {
          isLoading: false,
          displayedOptions: dataFormatter(data).map(elem => parseResult(elem)),
        },
        () => {
          if (onLoadingEnd) {
            onLoadingEnd();
          }
        }
      );
    };

    const request = new RequestHelper({
      type,
      source,
      filters,
      relatedEntities,
      delay,
      onFail,
      beforeSend,
      onDone: customOnDone,
    });

    if (onLoadingStart) {
      onLoadingStart();
    }

    request.getData(valueModifier(value));
  };

  getOptions(event) {
    const { kdFilterModel } = this.props;
    const { url, bearerToken, key, pageRows, minLength, search } = this.autoComplete;
    const value = event.target.value || '';
    let fetchFormat;

    if (value.length >= minLength) {
      if (search.url) {
        fetchFormat = {
          type: 'url',
          source: {
            url: search.url,
          },
          dataFormatter: d => d,
          itemFormatter: ({ value, label, entityType }) => ({ value, label, entityType }),
        };
      } else if (search.data) {
        fetchFormat = {
          type: 'data',
          source: search.data,
          dataFormatter: d => d,
          itemFormatter: ({ value, label, entityType }) => ({ value, label, entityType }),
        };
      } else if (kdFilterModel) {
        fetchFormat = {
          type: 'request',
          source: {
            model: kdFilterModel,
            api: { url, bearerToken, key, pageRows },
          },
          dataFormatter: data => data.KdResult.Result,
          itemFormatter: item => ({
            value: item.KdId,
            label: item.KdLabel,
            ...item,
          }),
        };
      }
      this.fetchByType({ value, ...fetchFormat });
    } else {
      this.resetDisplayed();
    }
  }

  handleChange = event => {
    event.persist();
    if (event.target.value.length >= this.autoComplete.minLength) {
      this.setState({ isLoading: true });
      this.delayedGetOptions(event);
    } else {
      this.resetDisplayed();
    }
  };

  handleSelect = e => {
    const value = e.currentTarget.getAttribute('data-value');

    this.addItem(value);
    this.resetActiveOption();
  };

  isOptionSelected = () => this.state.activeOption !== NON_SELECTED;

  initialInputValue = () => {
    const { isChickletNone, lastInitialValue, hasInitialValues } = this.props;
    if (isChickletNone && hasInitialValues) {
      return lastInitialValue.label;
    }
    return null;
  };

  addItem = value => {
    const { clearAfterSelection, autoComplete: { events: { onSelect, onChange } } } = this;
    const { setSelectedItems, isChickletNone } = this.props;
    const item = this.state.displayedOptions.find(elem => elem.value === value);
    const event = () => {
      onSelect && onSelect({ data: { ...item } });
      onChange && onChange({ data: { ...item } });
    };

    setSelectedItems && setSelectedItems({ values: [item] }, event);

    if (isChickletNone) {
      this.setInputValue(item.label);
    } else {
      clearAfterSelection && this.setInputValue();
    }

    this.resetDisplayed();
  };

  handleRemove = elem => {
    if (this.props.handleRemove) {
      this.props.handleRemove(elem);
    }
    this.setInputValue();
  };

  renderItem = item => {
    const { autoComplete: { events: { onRender } }, theme } = this.props;

    if (onRender) {
      return (
        <span
          className={classNames('o-options__option-text', theme['o-options__option-text'])}
          dangerouslySetInnerHTML={{ __html: onRender(item) }}
        />
      );
    }

    return <AutocompleteOption theme={theme} item={item} />;
  };

  renderInfoBar() {
    const { autoComplete: { infoBar: { leftLabel, rightLabel } } } = this;
    const { theme } = this.props;

    let rightLabelValue;
    if (rightLabel) {
      rightLabelValue = rightLabel;
    } else {
      rightLabelValue = (
        <div>
          <b>&uarr; Up</b>
          <b>&darr; Down</b> to navigate
          <b className={classNames('u-margin-left-small', theme['u-margin-left-small'])}>&crarr; Enter </b>
          <span> to view</span>
        </div>
      );
    }

    return (
      <li className={classNames('o-options__info-option', theme['o-options__info-option'])}>
        <div className={classNames('o-pack o-pack--middle', theme['o-pack o-pack--middle'])}>
          <div
            className={classNames(
              'o-pack__item',
              'u-padding-small',
              'o-options__left-label',
              theme['o-pack__item'],
              theme['u-padding-small'],
              theme['o-options__left-label']
            )}
          >
            {leftLabel}
          </div>
          <div
            className={classNames(
              'o-pack__item',
              'u-padding-small',
              'o-options__right-label',
              theme['o-pack__item'],
              theme['u-padding-small'],
              theme['o-options__right-label']
            )}
          >
            {rightLabelValue}
          </div>
        </div>
      </li>
    );
  }

  renderOptions = () => {
    const { theme } = this.props;
    const displayedOptions = this.state.displayedOptions.map((item, index) => (
      <li
        ref={ref => (this[`option_${index}`] = ref)}
        className={classNames(
          'o-options__option',
          { 'o-options__option-active': index === this.state.activeOption },
          theme['o-options__option'],
          { [theme['o-options__option-active']]: index === this.state.activeOption }
        )}
        key={item.value}
        data-label={item.label}
        data-value={item.value}
        data-index={index}
        onClick={this.handleSelect}
        onMouseEnter={this.setActiveOption}
        onMouseLeave={this.removeActiveOption}
      >
        {this.renderItem(item)}
      </li>
    ));
    if (displayedOptions.length) {
      return (
        <ul className={classNames('o-options', theme['o-options'])}>
          {this.autoComplete.infoBar.visible && this.renderInfoBar()}
          {displayedOptions}
        </ul>
      );
    }

    return null;
  };

  setActiveOption = event => {
    const index = event.currentTarget.getAttribute('data-index');
    this.setState({ activeOption: parseInt(index, 10) });
  };

  removeActiveOption = () => {
    this.resetActiveOption();
  };

  setInputValue(value) {
    if (this.AutocompleteInput != null) {
      this.AutocompleteInput.value = value || '';
    }
  }

  resetDisplayed = () => {
    this.setState({
      displayedOptions: [],
      activeOption: NON_SELECTED,
      isLoading: false,
    });
  };

  resetActiveOption = () => {
    this.setState({ activeOption: NON_SELECTED });
  };

  handleKeyDown = e => {
    const itemsLength = this.state.displayedOptions.length;
    if (itemsLength) {
      switch (e.keyCode) {
        case ARROW_DOWN: {
          const next = this.isOptionSelected() ? (this.state.activeOption + 1) % itemsLength : 0;
          this.setState({ activeOption: next });
          break;
        }
        case ARROW_UP: {
          const prev = this.isOptionSelected() ? (this.state.activeOption - 1) % itemsLength : itemsLength - 1;
          this.setState({ activeOption: prev });
          break;
        }
        case ENTER: {
          const elem = this[`option_${this.state.activeOption}`];
          if (elem) {
            const value = elem.getAttribute('data-value');
            this.addItem(value);
          }
          break;
        }
        case ESCAPE: {
          this.resetDisplayed();
          break;
        }
        default:
          break;
      }
    }
  };

  renderInput = () => {
    const { events: { onFocus, onBlur }, placeholderText } = this.autoComplete;
    const { isLoading } = this.state;
    const {
      isChickletNone,
      kdFilterModel,
      isEntityMultiple,
      isEntityMultipleWithSelector,
      theme,
      emptyItems,
    } = this.props;
    const removeVisible = _get(this.props, 'chicklets.buttons.remove.visible', false);
    const placeholder = placeholderText || `${kdFilterModel.getEntityType()} Search...`;
    const multipleNoSelector = isEntityMultiple && !isEntityMultipleWithSelector;
    const inputClass = classNames(
      'o-kd-autocomplete-input',
      'o-search-icon',
      'c-kd__input',
      {
        'o-search-icon--with-loading': isLoading && !isChickletNone,
        'o-search-icon--with-loading-for-chicklet-none': isLoading && isChickletNone && !emptyItems && removeVisible,
        'o-search-icon--with-loading-for-chicklet-none-empty':
          isLoading && isChickletNone && (emptyItems || !removeVisible),
        'o-search-icon--entity-multiple-no-selector-with-loading-for-chicklet-none':
          multipleNoSelector && isLoading && isChickletNone,
        'o-search-icon--entity-multiple-no-selector-with-loading': multipleNoSelector && isLoading,
        'o-search-icon--entity-multiple-no-selector': multipleNoSelector,
        'o-kd-autocomplete-input--entity-multiple-no-selector': multipleNoSelector,
        'u-input-chicklet-none': isChickletNone,
      },
      theme['o-kd-autocomplete-input'],
      theme['o-search-icon'],
      theme['c-kd__input'],
      {
        [theme['o-search-icon--with-loading']]: isLoading && !isChickletNone,
        [theme['o-search-icon--with-loading-for-chicklet-none']]:
          isLoading && isChickletNone && !emptyItems && removeVisible,
        [theme['o-search-icon--with-loading-for-chicklet-none-empty']]:
          isLoading && isChickletNone && (emptyItems || !removeVisible),
        [theme['o-search-icon--entity-multiple-no-selector-with-loading-for-chicklet-none']]:
          multipleNoSelector && isLoading && isChickletNone,
        [theme['o-search-icon--entity-multiple-no-selector-with-loading']]: multipleNoSelector && isLoading,
        [theme['o-search-icon--entity-multiple-no-selector']]: multipleNoSelector,
        [theme['o-kd-autocomplete-input--entity-multiple-no-selector']]: multipleNoSelector,
        [theme['u-input-chicklet-none']]: isChickletNone,
      }
    );
    return (
      <input
        onFocus={event => {
          if (onFocus) {
            onFocus(event);
          }
        }}
        onBlur={event => {
          if (onBlur) {
            onBlur(event);
          }
        }}
        className={inputClass}
        onKeyDown={this.handleKeyDown}
        type="text"
        ref={ref => (this.AutocompleteInput = ref)}
        placeholder={placeholder}
        onChange={this.handleChange}
        defaultValue={this.initialInputValue()}
      />
    );
  };

  render() {
    const { theme } = this.props;
    return (
      <div
        ref={ref => (this.rootElement = ref)}
        className={classNames(
          'c-kd-autocomplete',
          {
            'c-kd-autocomplete--entity-multiple-with-selector': this.props.isEntityMultipleWithSelector,
            'c-kd-autocomplete--entity-multiple-no-selector': !this.props.isEntityMultipleWithSelector,
          },
          theme['c-kd-autocomplete'],
          {
            [theme['c-kd-autocomplete--entity-multiple-with-selector']]: this.props.isEntityMultipleWithSelector,
            [theme['c-kd-autocomplete--entity-multiple-no-selector']]: !this.props.isEntityMultipleWithSelector,
          }
        )}
      >
        {this.renderInput()}
        {this.renderOptions()}
      </div>
    );
  }
}

Autocomplete.defaultProps = defaultProps;
Autocomplete.propTypes = propTypes;

export default Autocomplete;
