/**
 * Virtual selector for react.js
 * 
 * @version 1.1.5
 * @author artisan.
 * @Date(2015-11-06)
 * @example https://code-artisan.github.io/selector2
 * @copyright artisan
 */

import _ from 'underscore';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import SelectorFilter from './components/SelectorFilter.jsx';
import SelectorDropdown from './components/SelectorDropdown.jsx';

class Selector2 extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      group: false,

      loading: false,

      options: [],

      previous: {}, // Pervious selected options.

      // Is opened dropdown.
      dropdown: Boolean(props.autoOpen),

      // Selected options store.
      selected: []
    };

    /**
     * Copy props.options.
     * 
     * @type {Object}
     */
    this.store = {
      options: []
    };

    /**
     * Cache dropdown element.
     * 
     * @type {Object}
     */
    this.$dropdown  = null;

    /**
     * Cache selector container element.
     * 
     * @type {Object}
     */
    this.$container = null;

    /**
     * Component display name in react develop tool.
     * 
     * @type {String}
     */
    this.displayName = 'Selector2';

    this.handleParentScroll   = this.handleParentScroll.bind(this);

    this.handleCloseDropdown  = this.handleCloseDropdown.bind(this);
  }

  /**
   * Fetch data from remote server.
   * 
   * @param  {Object} confingures.
   * @return {Undefined}
   */
  fetch(props) {
    let request = $.getJSON(props.remote.url),
        results = [],

        selected = []; // Save default selected optioins.

    if (!this.state.loading) {
      this.setState({
        loading: true
      });
    }

    request.then((response) => {
      results = props.remote.field ? response[ props.remote.field ] : response;

      if ($.isArray(results)) {
        results = results.map(option => {
          return $.isPlainObject(option) ? option : {label: option, value: option};
        });
  
        results = Object.assign({}, props, {options: results});

        this.cloneAndFilterOptions(results);

        this.setState({
          loading : false
        });
      }
    });
  }

  /**
   * Get jquery element by given ref name.
   * 
   * @param  {string} name      ref name.
   * @return {object}           element.
   */
  getElementByRefName(name) {
    let $element = null,
        element = this.refs[name];

    if ( this.refs && element ) {
      if (_.isElement(element)) {
        $element = $(this.refs[name]);
      } else {
        $element = $(element.getDOMNode());
      }
    }
    return $element;
  }

  /**
   * Clone and set unique key.
   * 
   * @param  {array} properties.options options
   * @return {array} copyed array.
   */
  clonePropsOption({options}) {
    let resouces = [], increment = 0,
        groupOptions;

    if (_.isArray(options)) {
      resouces = $.extend(true, resouces, options);

      resouces.forEach((resouce, unique) => {
        groupOptions = resouce.options;

        // Support group.
        if (_.isArray(groupOptions)) {
          this.state.group = true;
          groupOptions.forEach((option, second) => {
            option.parent = unique;
            option.unique = increment++;
          });
        } else {
          // Normal select type.
          resouce.unique = unique;
        }
      });
    }
    return resouces;
  }

  /**
   * Filter default options.
   * 
   * @param  {string|array} options.defaults  default options.
   * @param  {string} options.separator separator.
   * @return {array}                   filter options.
   */
  filterDefaultOption({defaults, separator}) {
    let temp, values = [], selected = [],
        { options } = this.state;

    // Is array. e.g: ['foo', 'bar', ...] or [{...}, {...}] or ['foo', {...}]
    if ( _.isArray(defaults) ) {
      // If is single mode.
      if ( ! this.props.multiple ) {
        defaults = [_.last(defaults)];
      }
      defaults.forEach((option, index) => {
        // If option is string.
        if ( _.isString(option) ) {
          values.push( option.trim() );
        // Object...
        } else if ( $.isPlainObject(option) ) {
          temp = _.pick(option, 'label');

          if (_.isString(temp)) {
            values.push( temp );
          }
        }
      });

      defaults = values.join(separator);
    }

    // Is string. e.g: 'foo,bar,...'
    if ( _.isString(defaults) ) {
      defaults.split(separator).forEach((value) => {
        if (this.state.group) {
          options.forEach((group) => {
            temp = _.find(group.options, {
              label: value.trim()
            });

            if ($.isPlainObject(temp)) {
              return selected.push(temp);
            }
          });
        } else {
          temp = _.find(options, {value: value.trim()});
        }

        if ($.isPlainObject(temp)) {
          selected.push( temp );
        }
      });
    }

    selected = _.union(selected);

    if (! this.props.nullable &&
          selected.length === 0 &&
        ! this.state.group) {
      selected.push(options[0] || '');
    }

    return selected;
  }

  /**
   * Filter options by keyword.
   * 
   * @param  {string} keyword keyword.
   * @return {array}          options.
   */
  filterOptionByKeyword(keyword) {
    keyword = $.trim(keyword);

    let illegal = /[\^|\$|\.|\*|\+|\-|\?|\=|\!|\:|\||\\|\/|\(|\)|\[|\]|\{|\}]/g;

    // Replace illegal chars. e.g: 'hello world.' '$variable'
    keyword = keyword.replace(new RegExp(illegal), ($0) => `\\${$0}`);

    let matcher = new RegExp(keyword, 'i'),
        options = this.clonePropsOption(this.store),
        results = [], temp;

    // Filter options by keyword.
    if (this.state.group) { // Group.
      _.forEach(options, (group) => {
        temp = _.filter(group.options, (option) => {
          return option.label.match(matcher);
        });

        if (temp.length) {
          results.push({
            group: group.group,
            options: temp
          });
        }
      });
    } else {
      results = _.filter(options, (option) => {
        return option.label.match( matcher );
      });
    }

    this.setState({
      options: results
    });
  }

  /**
   * Stop propagation.
   * 
   * @param  {object} event event.
   * @return {undefined}
   */
  stopPropagation(event){
    event.stopPropagation();

    if ( event.nativeEvent ) {
      event.nativeEvent.stopImmediatePropagation();
    }
  }

  /**
   * Exec callback after selecte some option.
   * 
   * @return {Undefined}
   */
  handleAfterSelected() {
    let {
          selected,
          previous
        } = this.state, // Cache.
        
        { onChange } = this.props,

        results = {
          values: [],   // Cache selected options value.
          labels: [],   // Cache selected options label.
          active: null, // Last selected option.
          selected: []
        };

    selected.forEach((option) => {
      results.values.push(option.value);
      results.labels.push(option.label);
    });

    results.active = _.last(selected);
    results.selected = $.extend(true, results.selected, selected);

    if (_.isEqual(results, previous)) return false;

    previous = $.extend(true, previous, results);

    if (_.isFunction(onChange)) {
      onChange(results);
    }
  }

  initialization(props) {
    if ($.isPlainObject(props.remote)) {
      this.state.loading = true;

      return this.fetch(props);
    }

    this.cloneAndFilterOptions(props);
  }

  componentWillMount() {
    this.initialization(this.props);
  }

  cloneAndFilterOptions(props) {
    this.store.options  = this.clonePropsOption(props);

    // Copy resouces and set unique key.
    this.state.options  = this.clonePropsOption(props);

    // Find defualt selected options by this.props.defaults field.
    this.state.selected = this.filterDefaultOption(props);
  }

  componentDidMount() {
    if (this.props.autoOpen) {
      this.handleToggleDropdown();
    }

    // Scroll handle.
    $(this.props.parent).on('scroll', this.handleParentScroll);

    // Click handle.
    $(document).on('click virtual-selector:undropdown', this.handleCloseDropdown)
               .on('virtual-selector:updatedoption', this.handleParentScroll);
  }

  componentWillUnmount() {
    this.handleToggleDropdown(true); // #5

    // Remove scroll event.
    $(this.props.parent).off('scroll', this.handleParentScroll);

    // Remove click event.
    $(document).off('click virtual-selector:undropdown', this.handleCloseDropdown)
               .off('virtual-selector:updatedoption', this.handleParentScroll);
  }

  componentWillReceiveProps(props) {
    this.state.dropdown = Boolean(props.autoOpen);

    if ($.isPlainObject(props.remote)) {
      if (!_.isEqual(props.remote, this.props.remote)) {
        this.initialization(props);
      }
    } else {
      this.initialization(props);
    }
  }

  componentDidUpdate() {
    this.handleToggleDropdown();
    this.handleParentScroll();
  }

  /**
   * Parent node scroll event.
   * 
   * @return {undefined}
   */
  handleParentScroll() {
    if ( this.$dropdown ) {
      let offset  = this.$container.offset(),
          offsetTop = offset.top + this.$container.height(),
          dropdownHeight = this.$dropdown.height(),
          $window = $(window);

      // Fix position and set dropdown class name.
      if ($window.scrollTop() + $window.height() - offsetTop < dropdownHeight) {
        offsetTop = offset.top - dropdownHeight;
        this.$dropdown.removeClass('selector-dropdown-down').addClass('selector-dropdown-up');
        this.$container.removeClass('selector-dropdown-down').addClass('selector-dropdown-up');
      } else {
        this.$dropdown.removeClass('selector-dropdown-up').addClass('selector-dropdown-down');
        this.$container.removeClass('selector-dropdown-up').addClass('selector-dropdown-down');
      }

      // Set dropdown positon.
      this.$dropdown.css({'top': offsetTop, 'left': offset.left});
    }
  }

  /**
   * Open / close dropdown.
   * 
   * @param  {object} event event.
   * @return {undefined}
   */
  handleOpenDropdown(event) {
    this.stopPropagation(event);

    if ( this.props.disabled || this.state.loading ) {
      return false;
    }

    let isEqual = _.isEqual(this.state.options, this.props.options);

    // Copy options.
    if ($.isPlainObject(this.props.remote)) { // Remote.
      this.state.options = this.clonePropsOption(this.store);
    // Not equal.
    } else if (isEqual === false) {
      this.state.options = this.clonePropsOption(this.props);
    }

    let { dropdown } = this.state;

    if ( dropdown === false ) {
      this.triggerUnDropdown();
    }

    this.setState({
      dropdown: ! dropdown
    });
  }

  /**
   * Trigger undropdown.
   * 
   * @return {undefined}
   */
  triggerUnDropdown() {
    $(document).trigger('virtual-selector:undropdown');
  }

  /**
   * Close dropdown.
   * 
   * @return {undefined}
   */
  handleCloseDropdown(event) {
    if ( this.state.dropdown ) {
      let element = event.target,
          contains = $.contains(this.$dropdown[0], element) || $.contains(this.$container[0], element);

      if (contains === false) {
        this.setState({
          dropdown: false
        });
      }
    }
  }

  /**
   * Append option to selected options.
   * 
   * @param  {object} option target
   * @return {undefined}
   */
  handleAppendActiveOption(option) {
    if ( $.isPlainObject(option) ) {
      
      let { selected } = this.state,
          { onSelectClose, multiple } = this.props;

      if ( _.find(selected, option) && multiple ) {
        this.handleRemoveSelectedOption( option );
      } else {
        // Set selected option.
        if ( multiple ) {
          selected.push(option);
        } else {
          selected = [ option ];
        }

        this.setState({
          selected: selected,
          dropdown: ! onSelectClose
        }, this.handleAfterSelected);
      }
    }
  }

  /**
   * Clear all selected options.
   * 
   * @param  {object} event event.
   * @return {undefined}
   */
  handleClearSelectedOption(event) {
    this.stopPropagation(event);

    if ( this.state.dropdown ) {
      if ( this.$dropdown !== null ) {
        this.$dropdown.css('width', 'auto');
      }
    }

    this.triggerUnDropdown();

    this.setState({
      selected: [],
      dropdown: true
    }, this.handleAfterSelected);
  }

  /**
   * Remove option from selected options.
   * 
   * @param  {object|number} target target.
   * @return {undefined}
   */
  handleRemoveSelectedOption(target, dropdown, event) {
    let { selected } = this.state,
        { onSelectClose } = this.props;

    if ( ! _.isUndefined(event) ) {
      this.stopPropagation(event);
    }

    if ( $.isPlainObject(target) ) {
      target = _.findIndex(selected, target);
    }

    if ( _.isNumber(target) ) {
      if ( target >= 0 ) {
        selected.splice(target, 1);

        this.setState({
          selected: selected,
          dropdown: dropdown || ! onSelectClose
        }, this.handleAfterSelected);
      }
    }
  }

  /**
   * Toggle dropdown component by state's dropdown.
   * 
   * @return {Undefined}
   */
  handleToggleDropdown(unmount = false) {
    if ( this.state.dropdown && unmount === false ) {

      // Get container dom.
      if ( this.$container === null ) {
        this.$container = this.getElementByRefName('container');
      }

      let { searchable, disabled, autoFocus,
            theme, className, size } = this.props,
          position = {
            width: this.$container.outerWidth()
          };

      if ( this.$dropdown === null ) {
        position.height = this.$container.height();
        position.offset = this.$container.offset();
        
        this.$dropdown = $(`<div class="${ classnames('selector-container', `selector-${size}`, 'selector-dropdown', `selector-${theme}`, className.dropdown) }"
          style="left: ${position.offset.left}px; top: ${position.offset.top + position.height}px;"></div>`);

        // Append selector-container to body.
        $('body').append( this.$dropdown );
      }

      // Reset width.
      this.$dropdown.css('width', 'auto');

      ReactDOM.render(
        <SelectorDropdown shortcuts={ this.props.shortcuts } {...this.state} template={ this.props.template.option }
          noResultText={ this.props.noResultText } onSelect={ this.handleAppendActiveOption.bind(this) }>
          {
            searchable && ! disabled ? <SelectorFilter autoFocus={ this.props.autoFocus } onChange={ this.filterOptionByKeyword.bind(this) } /> : null
          }
        </SelectorDropdown>, this.$dropdown[0],

        // Set width.
        () => {          
          let $width = this.$container.outerWidth();
          let _width = ~~this.$dropdown.outerWidth();

          let finalWidth = _width <= $width ? $width : 'auto';

          this.$dropdown.css('width', finalWidth);
        });
    } else {
      if ( this.$dropdown ) {
        // Unmount dropdown component.
        ReactDOM.unmountComponentAtNode(this.$dropdown[0]);

        // Destroy dropdown.
        this.$dropdown.remove();
        this.$dropdown = null;
      }
    }
  }

  /**
   * Render loading component.
   * 
   * @return {Object} loading component.
   */
  loading(message) {
    return (
      <div className="selector-loading">{ message }</div>
    );
  }

  /**
   * Render renderer component.
   * 
   * @param  {Boolean} isEmpty  Is empty.
   * @param  {Boolean}  multiple Is multiple.
   * @return {Object}
   */
  renderer(isEmpty, multiple) {
    let { selected } = this.state,
        {
          clearable, placeholder, disabled, template
        } = this.props, templates;

    return (
      <ul className="selector-renderer">
        { // Display placeholder element if selected option's length equal to 0.
          isEmpty ? <span className="selector-placeholder">{ placeholder }</span> : null
        }
        { // Map selected options.
          selected.map((option, unique) => {

            if ( _.isString(template.selected) ) {
              templates = _.template(template.selected)(option);
            } else {
              templates = option.label;
            }

            return (
              <li className="selector-choice" key={ unique }>
                { multiple ? <span className="selector-choice-remove" onClick={ this.handleRemoveSelectedOption.bind(this, option, true) }>&times;</span> : null }
                <span dangerouslySetInnerHTML={{__html: templates}}></span>
              </li>
            )
          })
        }
        { // Display clear element if clearable equal to true.
          (clearable && ! disabled) && ! isEmpty ? <span className="selector-clearer" onClick={ this.handleClearSelectedOption.bind(this) }>&times;</span> : null
        }
      </ul>
    );
  }

  render() {
    let { selected, dropdown, loading } = this.state,
        { clearable, multiple, theme,
          disabled, remote, size } = this.props,
        isEmpty = selected.length === 0;

    // Set class name by multiple.
    let selectMode = multiple ? 'selector-multiple' : 'selector-single';

    return (
      <div className={classnames('selector-container', `selector-${theme}`, `selector-${size}`, {
        'selector-opened'  : dropdown,
        'selector-disabled': disabled || this.state.loading
      }, this.props.className.container)} onClick={ this.handleOpenDropdown.bind(this) } ref="container">
        <div className={classnames('selector-selection', selectMode, {
          'selector-clearable': (clearable && ! disabled) && ! isEmpty
        })}>
          {
            loading && $.isPlainObject(remote) ? this.loading(remote.loading) : this.renderer(isEmpty, multiple)
          }
        </div>
      </div>
    );
  }
}

Selector2.defaultProps = {
  autoOpen: false,
  theme: 'default',
  parent: document,
  size: 'md',
  remote: null,
  options: [],
  defaults: [],
  nullable: true,
  multiple: false,
  disabled: false,
  onChange: null,
  template: {
    option: null,
    selected: null
  },
  shortcuts: true,
  separator: ',',
  autoFocus: true,
  clearable: true,
  className: {
    dropdown: null,
    container: null
  },
  searchable: true,
  placeholder: 'Please select...',
  noResultText: 'No options to show.',
  onSelectClose: true
};

Selector2.propTypes = {
  autoOpen: React.PropTypes.bool,
  theme: React.PropTypes.string,
  parent: React.PropTypes.oneOfType([
    React.PropTypes.string,
    React.PropTypes.object
  ]),
  size: React.PropTypes.oneOf([
    'sm', 'md', 'lg'
  ]),
  options: React.PropTypes.array.isRequired,
  defaults: React.PropTypes.oneOfType([
    React.PropTypes.array,
    React.PropTypes.string,
  ]),
  nullable: React.PropTypes.bool,
  multiple: React.PropTypes.bool,
  disabled: React.PropTypes.bool,
  onChange: React.PropTypes.func,
  template: React.PropTypes.object,
  shortcuts: React.PropTypes.bool,
  separator: React.PropTypes.string,
  autoFocus: React.PropTypes.bool,
  clearable: React.PropTypes.bool,
  className: React.PropTypes.oneOfType([
    React.PropTypes.object,
    React.PropTypes.string
  ]),
  searchable: React.PropTypes.bool,
  placeholder: React.PropTypes.string,
  noResultText: React.PropTypes.string,
  onSelectClose: React.PropTypes.bool
};

export default Selector2;
