import classNames from 'classnames';
import debounce from 'lodash.debounce';
import extend from 'extend';
import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';

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

const defaultProps = {
  isChickletNone: false,
  hasInitialValues: false,
  search: {},
  autoComplete: {
    clearTextboxOnSelection: true,
    filters: [],
    infoBar: {
      visible: false,
      leftLabel: '',
    },
  },
};

const propTypes = {
  theme: PropTypes.oneOfType([
    PropTypes.object,
    PropTypes.func,
  ]).isRequired,
  kdFilterModel: PropTypes.object,

  search: PropTypes.shape({
    url: PropTypes.func,
    data: PropTypes.array,
  }),

  autoComplete: PropTypes.shape({
    onSuccess: PropTypes.func,
    onRender: PropTypes.func,
    onSelect: PropTypes.func,
    onFail: PropTypes.func,
    onDone: PropTypes.func,
    placeholderText: PropTypes.string,
    clearTextboxOnSelection: PropTypes.bool,
    filters: PropTypes.arrayOf(PropTypes.object),
    infoBar: PropTypes.shape({
      visible: PropTypes.bool,
      leftLabel: PropTypes.string,
      rightLabel: PropTypes.string,
    }),
  }),

  api: PropTypes.shape({
    url: PropTypes.string.isRequired,
    key: PropTypes.string.isRequired,
  }),

  events: PropTypes.shape({
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    onLoadingStart: PropTypes.func,
    onLoadingEnd: PropTypes.func,
  }),

  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.isOptionSelected = this.isOptionSelected.bind(this);
    this.handleOutsideClick = this.handleOutsideClick.bind(this);
    this.fetchByType = this.fetchByType.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleSelect = this.handleSelect.bind(this);
    this.handleRemove = this.handleRemove.bind(this);
    this.renderItem = this.renderItem.bind(this);
    this.renderOptions = this.renderOptions.bind(this);
    this.setActiveOption = this.setActiveOption.bind(this);
    this.removeActiveOption = this.removeActiveOption.bind(this);
    this.resetDisplayed = this.resetDisplayed.bind(this);
    this.resetActiveOption = this.resetActiveOption.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
    this.renderInput = this.renderInput.bind(this);
    this.addItem = this.addItem.bind(this);

    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);
  }

  isOptionSelected() {
    return (this.state.activeOption !== NON_SELECTED);
  }

  initialInputValue() {
    if (this.props.isChickletNone && this.props.hasInitialValues) {
      return this.props.lastInitialValue.label;
    }

    return null;
  }

  handleOutsideClick(e) {
    if (ReactDOM.findDOMNode(this).contains(e.target)) {
      return;
    }

    this.resetDisplayed();
  }

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

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

  fetchByType({ value, type, source, dataFormatter, itemFormatter }) {
    const {
      autoComplete,
    } = this;
    const parseResult = (elem) => {
      let item = itemFormatter(elem);
      if (autoComplete.onSuccess) {
        item = autoComplete.onSuccess(item);
      }
      return item;
    };
    const onDone = (data) => {
      this.setState({
        isLoading: false,
        displayedOptions: dataFormatter(data).map(elem => (parseResult(elem))),
      }, () => {
        if (this.props.events && this.props.events.onLoadingEnd) {
          this.props.events.onLoadingEnd();
        }
      });
      dispatchCustomEvent(
        this.rootElement,
        'kd:ui:SelectEntity:onDone',
        { data: this.state.displayedOptions },
      );
    };
    const request = new requestHelper({
      type,
      source,
      onDone,
      onFail: autoComplete.onFail,
      filters: autoComplete.filters,
    });
    if (this.props.events && this.props.events.onLoadingStart) {
      this.props.events.onLoadingStart();
    }
    request.getData(value);
  }

  getOptions(event) {
    const { search, kdFilterModel, api } = this.props;
    const value = event.target.value || '';
    let fetchFormat;
    if (value.length) {
      if (search.url) {
        fetchFormat = {
          type: 'url',
          source: {
            url: search.url(),
          },
          dataFormatter: data => data,
          itemFormatter: ({ value, label, entityType }) => ({ value, label, entityType }),
        };
      } else if (search.data) {
        fetchFormat = {
          type: 'data',
          source: search.data,
          dataFormatter: data => data,
          itemFormatter: ({ value, label, entityType }) => ({ value, label, entityType }),
        };
      } else if (kdFilterModel) {
        fetchFormat = {
          type: 'request',
          source: {
            model: kdFilterModel,
            api,
          },
          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.setState({ isLoading: true });
      this.delayedGetOptions(event);
    } else {
      this.resetDisplayed();
    }
  }

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

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

  addItem(value) {
    const { autoComplete } = this;
    const { setSelectedItems, isChickletNone } = this.props;
    const item = this.state.displayedOptions.find(elem => (elem.value === value));

    setSelectedItems && setSelectedItems(item);
    autoComplete.onSelect && autoComplete.onSelect({ data: { ...item } });

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

    this.resetDisplayed();
  }

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

  renderItem(item) {
    const { autoComplete, theme } = this.props;

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

    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}`];
          const value = elem.getAttribute('data-value');
          this.addItem(value);
          break;
        }
        case ESCAPE: {
          this.resetDisplayed();
          break;
        }
        default:
          break;
      }
    }
  }

  renderInput() {
    const { autoComplete } = this;
    const { isLoading } = this.state;
    const {
      isChickletNone,
      kdFilterModel,
      isEntityMultiple,
      isEntityMultipleWithSelector,
      theme,
    } = this.props;
    const placeholder = autoComplete.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,
          '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,
          [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 (this.props.events && this.props.events.onFocus) {
            this.props.events.onFocus(event);
          }
        }}
        onBlur={(event) => {
          if (this.props.events && this.props.events.onBlur) {
            this.props.events.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;
