import React from 'react';
import map from 'lodash/map';
import filter from 'lodash/filter';
import orderBy from 'lodash/orderBy';

/**
 * Interface for selecting a completion based on a list and a user's input.
 */
class SearchSingle extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: '',
      isOpen: false,
      highlightIndex: 0,
      dropdownTop: -100,
      dropdownLeft: -100,
    };

    // Used to ensure that when you click on one of the autocomplete results the
    // dropdown doesn't close before the click is completed.
    this.ignoreBlur = false;
    this.preventCompletion = false;
  }

  componentDidMount() {
    this.initialSizeCalculations();
    if (this.props.bindClearSearchCallback) {
      this.props.bindClearSearchCallback(this.clearSearch.bind(this));
    }
  }

  componentDidUpdate() {
    if (this.props.isSelected) {
      this.refs.input.focus();
    }
    if (this.props.bindClearSearchCallback) {
      this.props.bindClearSearchCallback(this.clearSearch.bind(this));
    }
  }

  /**
   * Returns a list of autocomplete possibilities after they are filtered and sorted.
   */
  getFilteredAutocomplete() {
    let completions = this.props.completions;
    if (this.props.itemFilter) {
      completions = filter(completions, (item) => this.props.itemFilter(this.state.input, item));
    }

    if (this.props.itemSortValue) {
      completions = orderBy(completions, (item) => this.props.itemSortValue(this.state.input, item), this.props.itemSortDirection);
    }
    return completions;
  }

  getDropdownPosition(element) {
    const compStyle = window.getComputedStyle(element);
    const textWidth = this.calculateTextWidth(element.value || this.props.placeholder, compStyle.font);
    const newLeft = textWidth;
    const newTop = parseInt(compStyle.height.substring(0, compStyle.height.length - 2), 10) + 3;
    return {dropdownTop: newTop, dropdownLeft: newLeft};
  }

  clearSearch() {
    const newState = {input: '', highlightIndex: 0, isOpen: false};
    this.ignoreBlur = false;
    this.setState(newState);
  }

  initialSizeCalculations() {
    this.setState(this.getDropdownPosition(this.refs.input));
  }

  // Thank you http://stackoverflow.com/a/21015393
  calculateTextWidth(text, font) {
    // re-use canvas object for better performance
    const canvas = this.textCanvas || (this.textCanvas = document.createElement('canvas'));
    const context = canvas.getContext('2d');
    context.font = font;
    const metrics = context.measureText(text);
    return metrics.width;
  }

  handleMouseEnter(index) {
    this.setState({highlightIndex: index});
  }

  handleSelect(index) {
    const item = this.getFilteredAutocomplete()[index];
    const input = this.state.input;
    const newState = {highlightIndex: 0, isOpen: false};
    if (!this.props.keepOnCompletion) {
      newState.input = '';
    } else {
      if (this.props.getItemValue) {
        newState.input = this.props.getItemValue(item);
      } else {
        newState.input = item;
      }
    }

    this.setState(newState, () => {
      this.props.onSelect(item, input);
      if (!this.props.keepOnCompletion) {
        this.props.onChange(null, '');
        const newState = this.getDropdownPosition(this.refs.input);
        this.setState(newState);
      }
      this.ignoreBlur = false;
    });
  }

  handleChange(e) {
    const newState = this.getDropdownPosition(e.target);
    newState.input = e.target.value;
    this.setState(newState, () => {
      this.props.onChange(e, this.state.input);
    });
  }

  handleKeyPress(e) {
    e.persist();
    const doTransparentKeyPress = function(event) {
      if (this.props.onKeyPress) {
        this.props.onKeyPress(e);
      }
    }.bind(this, e);

    const hi = this.state.highlightIndex;

    if (this.props.completionKeys.indexOf(e.key) !== -1) {
      if (hi !== null && this.getFilteredAutocomplete().length !== 0) {
        // Only prevent default if we are selecting an autocomplete option.
        e.preventDefault();
        this.preventCompletion = true;
        this.handleSelect(this.state.highlightIndex);
      } else {
        doTransparentKeyPress();
      }
      return;
    }

    this.setState({isOpen: true}, doTransparentKeyPress);
  }

  handleKeyDown(e) {
    e.persist();
    const doTransparentKeyDown = function(event) {
      if (this.props.onKeyDown) {
        this.props.onKeyDown(e);
      }
    }.bind(this, e);

    let hi = this.state.highlightIndex;

    switch (e.key) {
    case 'ArrowDown':
      if (hi === null || hi === this.getFilteredAutocomplete().length - 1) {
        hi = 0;
      } else {
        hi = hi + 1;
      }
      this.setState({highlightIndex: hi, isOpen: true}, doTransparentKeyDown);
      break;

    case 'ArrowUp':
      if (hi === null || hi === 0) {
        hi = this.getFilteredAutocomplete().length - 1;
      } else {
        hi = hi - 1;
      }
      this.setState({highlightIndex: hi, isOpen: true}, doTransparentKeyDown);
      break;

    case 'Backspace':
      if (this.state.input.length <= 1) {
        this.setState({isOpen: false}, doTransparentKeyDown);
      } else {
        doTransparentKeyDown();
      }
      break;
    default:
      if (this.props.completionKeys.indexOf(e.key) !== -1) {
        if (this.preventCompletion) {
          // e.preventDefault();
        }
      } else {
        doTransparentKeyDown();
      }
    }
  }

  handleFocus(e) {
    this.setState({isOpen: true});
  }

  handleBlur(e) {
    if (!this.ignoreBlur) {
      this.setState({isOpen: false});
    }
  }

  renderItem(value, index) {
    const element = this.props.renderItem(value, this.state.highlightIndex === index);
    return React.cloneElement(element, {
      onMouseDown: (e) => this.ignoreBlur = true,
      onMouseEnter: (e) => this.handleMouseEnter(index),
      onClick: (e) => this.handleSelect(index),
      ref: 'itme-' + index,
      key: index
    });
  }

  render() {
    const {keepOnCompletion, renderItem, itemFilter, itemSortValue,
      itemSortDirection, completions, isSelected, dynamicSizing,
      minimumSize, bindClearSearchCallback, ...childProps} = this.props;
    let inputStyle = {};
    if ('style' in childProps) {
      inputStyle = Object.assign({}, childProps.style);
    }
    if (dynamicSizing) {
      inputStyle.width = this.state.dropdownLeft > minimumSize ? this.state.dropdownLeft : minimumSize;
      // 6 is the true "minimum" minumum width. You need to have some width in order to actually see
      //   the carot when typing and to give some wiggle room for the characters.
      inputStyle.width += 6;
    }
    return (
      <span>
        <input
          {...childProps}
          style={inputStyle}
          onSelect={function(e) {}}
          onChange={(e) => this.handleChange(e)}
          onKeyDown={(e) => this.handleKeyDown(e)}
          onKeyPress={(e) => this.handleKeyPress(e)}
          value={this.state.input}
          onFocus={(e) => this.handleFocus(e)}
          onBlur={(e) => this.handleBlur(e)}
          ref="input"
        />
        <ul ref="dropdown" className="Search-dropdown">
          {this.state.isOpen && map(this.getFilteredAutocomplete(), (value, index) => this.renderItem(value, index), this)}
        </ul>
      </span>
    );
  }
}

SearchSingle.propTypes = {
  /**
   * boolean, default: false
   *   Whether or not to clear the input (delete the text) when the user makes a selection.
  */
  keepOnCompletion: React.PropTypes.bool,
  /**
   * function(event event)
   *   Fired whenever the user pressed down a key.
   *   Added for transparency.
   *   Not fired when:
   *     props.completionKeys is used to select a completion
   */
  onKeyDown: React.PropTypes.func,
  /**
   * function(event event)
   *   Fired whenever the user presses a key.
   *   Added for transparency.
   *   Not fired when:
   *     props.completionKeys is used to select a completion
   */
  onKeyPress: React.PropTypes.func,
  /**
   * function(object item, boolean isHighlighted)
   *   Function used to render the items in the autocomplete list.
   *   Returns a JSX component.
   */
  renderItem: React.PropTypes.func,
  /**
   * function(event event)
   *   Fired whenever a change is made to the input.
   *   Added for transparency.
   *   Can be used to trigger a recalculation of the autocomplete list based on
   *     input (input can be accessed with event.target.value).
   */
  onChange: React.PropTypes.func,
  /**
   * function(object item, String input)
   *   Fired when the user selects an item from the autocomplete list.
   *   item is the object found in props.completions that was selected.
   *   input is what was in the input box when the selection was made.
   */
  onSelect: React.PropTypes.func,
  /**
   * function(string input, object item)
   *   Used to filter what completions are shown based on what was inputted.
   *   Return the boolean value of whether to show the item or not
   *   input is what the user typed into the input so far.
   *   item is the item in props.completion we are checking.
   */
  itemFilter: React.PropTypes.func,
  /**
   * function(string input, object item)
   *   Used to sort the completion list based on what was inputted.
   *   Return something that will be used in the sort to figure out the item's new position.
   *   Look at https://lodash.com/docs#sortByOrder for more information.
  */
  itemSortValue: React.PropTypes.func,
  /**
   * String or Array of Strings
   *   String/Array saying whether or not to sort each key by ascending or descenting order.
   *   Strings should be either 'asc' or 'desc'.
   *   Look at https://lodash.com/docs#orderBy for more information.
  */
  itemSortDirection: React.PropTypes.any,
  /**
   * Any object that can be enumerated. Most likely an array or an Immutable.List.
   *   The list of completions to used for autocomletion.
  */
  completions: React.PropTypes.any.isRequired,
  /**
   * String, default: 'search-input'
   *   What to set the input's class as.
   *   Added in order to set a default value.
  */
  className: React.PropTypes.string,
  /**
   * String
   *   What to set the dropdown's calss as.
  */
  // dropdownClassName: React.PropTypes.string
  /**
   * boolean
   *   Whether or not this component is selected.
   */
  isSelected: React.PropTypes.bool,
  /**
   * String[], default ['Enter']
   *   Keys used to signify the selection of a completion.
   */
  completionKeys: React.PropTypes.arrayOf(React.PropTypes.string),
  /**
   * boolean, default false
   *   Whether or not to resize the input dynamically based on what is inputted.
   */
  dynamicSizing: React.PropTypes.bool,
  /**
   * int, default 36
   *   A minimum size the input should be when dynamically resizing.
   */
  minimumSize: React.PropTypes.number,
  /**
   * function(Any item)
   *   Returns the textual value of an item
   */
  getItemValue: React.PropTypes.func,
  /**
   * String
   *   Text to display when nothing is typed
   */
  placeholder: React.PropTypes.string,
  /**
   * function(clearSearchCallback)
   *   A function that takes this component's clearSearch and does something with it
   *   Meant to be a way to give a parent function a reference to clearSearch
   *   An example implementation:
   *    (clearSearchCallback) => this.clearSearch = this.clearSearch.bind(this, clearSearchCallback)
   *    This would allow the parent to define a clearSearch function that takes a function as a
   *     parameter and call it with this.clearSearch() to clear this SearcSingle's search
   */
  bindClearSearchCallback: React.PropTypes.func
};
SearchSingle.defaultProps = {
  dynamicSizing: false,
  minimumSize: 52,
  className: 'Search-input',
  completionKeys: ['Enter'],
  itemSortDirection: 'desc',
  renderItem(item, isHighlighted) {
    return (<div style={isHighlighted ? { color: 'red'} : {color: 'gray'}}>{item}</div>);
  },
  onChange(event, input) {},
  onSelect(selectedItem, input) {},
};

export default SearchSingle;
