import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Select from '../select';
import Cascader from '../cascader';
import Menu from '../menu';
import { func, obj, dom, KEYCODE } from '../util';

const { bindCtx } = func;
const { pickOthers } = obj;
const { getStyle } = dom;

/**
 * CascaderSelect
 */
export default class CascaderSelect extends Component {
  static propTypes = {
    prefix: PropTypes.string,
    pure: PropTypes.bool,
    className: PropTypes.string,
    /**
         * 选择框大小
         */
    size: PropTypes.oneOf(['small', 'medium', 'large']),
    /**
         * 选择框占位符
         */
    placeholder: PropTypes.string,
    /**
         * 是否禁用
         */
    disabled: PropTypes.bool,
    /**
         * 是否有下拉箭头
         */
    hasArrow: PropTypes.bool,
    /**
         * 是否有边框
         */
    hasBorder: PropTypes.bool,
    /**
         * 是否有清除按钮
         */
    hasClear: PropTypes.bool,
    /**
         * 自定义内联 label
         */
    label: PropTypes.node,
    /**
         * 是否只读，只读模式下可以展开弹层但不能选
         */
    readOnly: PropTypes.bool,
    /**
         * 数据源，结构可参考下方说明
         */
    dataSource: PropTypes.arrayOf(PropTypes.object),
    /**
         * （非受控）默认值
         */
    defaultValue: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.arrayOf(PropTypes.string),
    ]),
    /**
         * （受控）当前值
         */
    value: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.arrayOf(PropTypes.string),
    ]),
    /**
         * 选中值改变时触发的回调函数
         * @param {String|Array} value 选中的值，单选时返回单个值，多选时返回数组
         * @param {Object|Array} data 选中的数据，包括 value 和 label，单选时返回单个值，多选时返回数组，父子节点选中关联时，同时选中，只返回父节点
         * @param {Object} extra 额外参数
         * @param {Array} extra.selectedPath 单选时选中的数据的路径
         * @param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中
         * @param {Object} extra.currentData 多选时当前操作的数据
         * @param {Array} extra.checkedData 多选时所有被选中的数据
         * @param {Array} extra.indeterminateData 多选时半选的数据
         */
    onChange: PropTypes.func,
    /**
         * 默认展开值，如果不设置，组件内部会根据 defaultValue/value 进行自动设置
         */
    defaultExpandedValue: PropTypes.arrayOf(PropTypes.string),
    /**
         * 展开触发的方式
         */
    expandTriggerType: PropTypes.oneOf(['click', 'hover']),
    onExpand: PropTypes.func,
    /**
         * 是否开启虚拟滚动
         */
    useVirtual: PropTypes.bool,
    /**
         * 是否多选
         */
    multiple: PropTypes.bool,
    /**
         * 是否选中即发生改变, 该属性仅在单选模式下有效
         */
    changeOnSelect: PropTypes.bool,
    /**
         * 是否只能勾选叶子项的checkbox，该属性仅在多选模式下有效
         */
    canOnlyCheckLeaf: PropTypes.bool,
    /**
         * 父子节点是否选中不关联
         */
    checkStrictly: PropTypes.bool,
    /**
         * 每列列表样式对象
         */
    listStyle: PropTypes.object,
    /**
         * 每列列表类名
         */
    listClassName: PropTypes.string,
    /**
         * 选择框单选时展示结果的自定义渲染函数
         * @param {Array} label 选中路径的文本数组
         * @return {ReactNode} 渲染在选择框中的内容
         * @default 单选时：labelPath => labelPath.join(' / ')；多选时：labelPath => labelPath[labelPath.length - 1]
         */
    displayRender: PropTypes.func,
    /**
         * 渲染 item 内容的方法
         * @param {Object} item 渲染节点的item
         * @return {ReactNode} item node
         */
    itemRender: PropTypes.func,
    /**
         * 是否显示搜索框
         */
    showSearch: PropTypes.bool,
    /**
         * 自定义搜索函数
         * @param {String} searchValue 搜索的关键字
         * @param {Array} path 节点路径
         * @return {Boolean} 是否匹配
         * @default 根据路径所有节点的文本值模糊匹配
         */
    filter: PropTypes.func,
    /**
         * 搜索结果自定义渲染函数
         * @param {String} searchValue 搜索的关键字
         * @param {Array} path 匹配到的节点路径
         * @return {ReactNode} 渲染的内容
         * @default 按照节点文本 a / b / c 的模式渲染
         */
    resultRender: PropTypes.func,
    /**
         * 搜索结果列表是否和选择框等宽
         */
    resultAutoWidth: PropTypes.bool,
    /**
         * 无数据时显示内容
         */
    notFoundContent: PropTypes.node,
    /**
         * 异步加载数据函数
         * @param {Object} data 当前点击异步加载的数据
         */
    loadData: PropTypes.func,
    /**
         * 自定义下拉框头部
         */
    header: PropTypes.node,
    /**
         * 自定义下拉框底部
         */
    footer: PropTypes.node,
    /**
         * 初始下拉框是否显示
         */
    defaultVisible: PropTypes.bool,
    /**
         * 当前下拉框是否显示
         */
    visible: PropTypes.bool,
    /**
         * 下拉框显示或关闭时触发事件的回调函数
         * @param {Boolean} visible 是否显示
         * @param {String} type 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发； docClick 表示由document的点击触发
         */
    onVisibleChange: PropTypes.func,
    /**
         * 下拉框自定义样式对象
         */
    popupStyle: PropTypes.object,
    /**
         * 下拉框样式自定义类名
         */
    popupClassName: PropTypes.string,
    /**
         * 下拉框挂载的容器节点
         */
    popupContainer: PropTypes.any,
    /**
         * 透传到 Popup 的属性对象
         */
    popupProps: PropTypes.object,
    /**
         * 是否跟随滚动
         */
    followTrigger: PropTypes.bool,
    /**
         * 是否为预览态
         */
    isPreview: PropTypes.bool,
    /**
         * 预览态模式下渲染的内容
         * @param {Array<data>} value 选择值 { label: , value:}
         */
    renderPreview: PropTypes.func,
  };

  static defaultProps = {
    prefix: 'next-',
    pure: false,
    size: 'medium',
    disabled: false,
    hasArrow: true,
    hasBorder: true,
    hasClear: false,
    dataSource: [],
    defaultValue: null,
    expandTriggerType: 'click',
    onExpand: () => {},
    useVirtual: false,
    multiple: false,
    changeOnSelect: false,
    canOnlyCheckLeaf: false,
    checkStrictly: false,
    showSearch: false,
    filter: (searchValue, path) => {
      return path.some(item => item.label.indexOf(searchValue) > -1);
    },
    resultRender: (searchValue, path) => {
      const parts = [];
      path.forEach((item, i) => {
        const others = item.label.split(searchValue);
        others.forEach((other, j) => {
          if (other) {
            parts.push(other);
          }
          if (j < others.length - 1) {
            parts.push(<em key={`${i}-${j}`}>{searchValue}</em>);
          }
        });
        if (i < path.length - 1) {
          parts.push(' / ');
        }
      });
      return <span>{parts}</span>;
    },
    resultAutoWidth: true,
    notFoundContent: 'Not Found',
    defaultVisible: false,
    onVisibleChange: () => {},
    popupProps: {},
  };

  constructor(props, context) {
    super(props, context);

    this.state = {
      value: this.normalizeValue(
        'value' in props ? props.value : props.defaultValue,
      ),
      searchValue: '',
      visible:
                typeof props.visible === 'undefined'
                  ? props.defaultVisible
                  : props.visible,
    };

    bindCtx(this, [
      'handleVisibleChange',
      'handleAfterOpen',
      'handleSelect',
      'handleChange',
      'handleClear',
      'handleRemove',
      'handleSearch',
      'getPopup',
      'saveSelectRef',
      'saveCascaderRef',
      'handleKeyDown',
    ]);
  }

  componentWillReceiveProps(nextProps) {
    const st = {};

    if ('value' in nextProps) {
      st.value = this.normalizeValue(nextProps.value);
    }
    if ('visible' in nextProps) {
      st.visible = nextProps.visible;
    }

    if (Object.keys(st).length) {
      this.setState(st);
    }
  }

  normalizeValue(value) {
    if (value) {
      if (Array.isArray(value)) {
        return value;
      }

      return [value];
    }

    return [];
  }

  updateCache(dataSource) {
    this._v2n = {};
    this._p2n = {};
    const loop = (data, prefix = '0') => data.forEach((item, index) => {
      const { value, children } = item;
      const pos = `${prefix}-${index}`;
      this._v2n[value] = this._p2n[pos] = { ...item, pos };

      if (children && children.length) {
        loop(children, pos);
      }
    });

    loop(dataSource);
  }

  flatValue(value) {
    const getDepth = v => {
      const pos = this.getPos(v);
      if (!pos) {
        return 0;
      }
      return pos.split('-').length;
    };
    const newValue = value.slice(0).sort((prev, next) => {
      return getDepth(prev) - getDepth(next);
    });

    for (let i = 0; i < newValue.length; i++) {
      for (let j = 0; j < newValue.length; j++) {
        if (
          i !== j &&
                    this.isDescendantOrSelf(
                      this.getPos(newValue[i]),
                      this.getPos(newValue[j]),
                    )
        ) {
          newValue.splice(j, 1);
          j--;
        }
      }
    }

    return newValue;
  }

  isDescendantOrSelf(currentPos, targetPos) {
    if (!currentPos || !targetPos) {
      return false;
    }

    const currentNums = currentPos.split('-');
    const targetNums = targetPos.split('-');

    return (
      currentNums.length <= targetNums.length &&
            currentNums.every((num, index) => {
              return num === targetNums[index];
            })
    );
  }

  getValue(pos) {
    return this._p2n[pos] ? this._p2n[pos].value : null;
  }

  getPos(value) {
    return this._v2n[value] ? this._v2n[value].pos : null;
  }

  getData(value) {
    return value.map(v => this._v2n[v]);
  }

  getLabelPath(data) {
    const nums = data.pos.split('-');
    return nums.slice(1).reduce((ret, num, index) => {
      const p = nums.slice(0, index + 2).join('-');
      ret.push(this._p2n[p].label);
      return ret;
    }, []);
  }

  getSignleData(value) {
    if (!value.length) {
      return null;
    }

    if (Array.isArray(value)) value = value[0];

    const data = this._v2n[value];
    if (!data) {
      return {
        value,
      };
    }

    const labelPath = this.getLabelPath(data);
    const displayRender =
            this.props.displayRender || (labels => labels.join(' / '));

    return {
      ...data,
      label: displayRender(labelPath, data),
    };
  }

  getMultipleData(value) {
    if (!value.length) {
      return null;
    }

    const { checkStrictly, canOnlyCheckLeaf, displayRender } = this.props;
    let data = (checkStrictly || canOnlyCheckLeaf
      ? value
      : this.flatValue(value)
    ).map(v => this._v2n[v] || { value: v });

    if (displayRender) {
      data = data.map(item => {
        if (!item.pos) {
          return item;
        }
        const labelPath = this.getLabelPath(item);

        return {
          ...item,
          label: displayRender(labelPath, item),
        };
      });
    }

    return data;
  }

  getIndeterminate(value) {
    const indeterminate = [];

    const positions = value.map(this.getPos.bind(this));
    positions.forEach(pos => {
      if (!pos) {
        return false;
      }
      const nums = pos.split('-');
      for (let i = nums.length; i > 2; i--) {
        const parentPos = nums.slice(0, i - 1).join('-');
        const parentValue = this.getValue(parentPos);
        if (indeterminate.indexOf(parentValue) === -1) {
          indeterminate.push(parentValue);
        }
      }
    });

    return indeterminate;
  }

  saveSelectRef(ref) {
    this.select = ref;
  }

  saveCascaderRef(ref) {
    this.cascader = ref;
  }

  completeValue(value) {
    const newValue = [];

    const flatValue = this.flatValue(value).reverse();
    const ps = Object.keys(this._p2n);
    for (let i = 0; i < ps.length; i++) {
      for (let j = 0; j < flatValue.length; j++) {
        const v = flatValue[j];
        if (this.isDescendantOrSelf(this.getPos(v), ps[i])) {
          newValue.push(this.getValue(ps[i]));
          ps.splice(i, 1);
          i--;
          break;
        }
      }
    }

    return newValue;
  }

  isLeaf(data) {
    return !(
      (data.children && data.children.length) ||
            (!!this.props.loadData && !data.isLeaf)
    );
  }

  handleVisibleChange(visible, type) {
    const { searchValue } = this.state;
    if (!('visible' in this.props)) {
      this.setState({
        visible,
      });
    }

    if (!visible && searchValue) {
      this.setState({
        searchValue: '',
      });
    }

    if (['fromCascader', 'keyboard'].indexOf(type) !== -1 && !visible) {
      this.select.focusInput();
    }

    this.props.onVisibleChange(visible, type);
  }

  handleKeyDown(e) {
    const { onKeyDown } = this.props;
    const { visible } = this.state;

    if (onKeyDown) {
      onKeyDown(e);
    }

    if (!visible) {
      return;
    }

    switch (e.keyCode) {
      case KEYCODE.UP:
      case KEYCODE.DOWN:
        this.cascader.setFocusValue();
        e.preventDefault();
        break;
      default:
        break;
    }
  }

  getPopup(ref) {
    this.popup = ref;
    if (typeof this.props.popupProps.ref === 'function') {
      this.props.popupProps.ref(ref);
    }
  }

  handleAfterOpen() {
    if (!this.popup) {
      return;
    }

    const { prefix, popupProps } = this.props;
    const dropDownNode = this.popup
      .getInstance()
      .overlay.getInstance()
      .getContentNode();
    const cascaderNode = dropDownNode.querySelector(`.${prefix}cascader`);
    if (cascaderNode) {
      this.cascaderHeight = getStyle(cascaderNode, 'height');
    }

    if (typeof popupProps.afterOpen === 'function') {
      popupProps.afterOpen();
    }
  }

  handleSelect(value, data) {
    const { multiple, changeOnSelect } = this.props;
    const { visible, searchValue } = this.state;

    if (
      !multiple &&
            (!changeOnSelect || this.isLeaf(data) || !!searchValue)
    ) {
      this.handleVisibleChange(!visible, 'fromCascader');
    }
  }

  handleChange(value, data, extra) {
    const { multiple, onChange } = this.props;
    const { searchValue, value: stateValue } = this.state;

    const st = {};

    if (multiple && stateValue && Array.isArray(stateValue)) {
      value = [...stateValue.filter(v => !this._v2n[v]), ...value];
    }

    if (!('value' in this.props)) {
      st.value = value;
    }
    if (!multiple && searchValue) {
      st.searchValue = '';
    }
    if (Object.keys(st).length) {
      this.setState(st);
    }

    if (onChange) {
      onChange(value, data, extra);
    }

    if (searchValue && this.select) {
      this.select.handleSearchClear();
    }
  }

  handleClear() {
    // 单选时点击清空按钮
    const { hasClear, multiple, treeCheckable } = this.props;
    if (hasClear && (!multiple || !treeCheckable)) {
      if (!('value' in this.props)) {
        this.setState({
          value: [],
        });
      }

      this.props.onChange(null, null);
    }
  }

  handleRemove(currentData) {
    const { value: currentValue } = currentData;
    let value;

    const { multiple, checkStrictly, onChange } = this.props;
    if (multiple) {
      value = [...this.state.value];
      value.splice(value.indexOf(currentValue), 1);

      if (this.props.onChange) {
        const data = this.getData(value);
        const checked = false;

        if (checkStrictly) {
          this.props.onChange(value, data, {
            checked,
            currentData,
            checkedData: data,
          });
        } else {
          const checkedValue = this.completeValue(value);
          const checkedData = this.getData(checkedValue);
          const indeterminateValue = this.getIndeterminate(value);
          const indeterminateData = this.getData(indeterminateValue);
          this.props.onChange(value, data, {
            checked,
            currentData,
            checkedData,
            indeterminateData,
          });
        }
      }
    } else {
      value = [];
      onChange(null, null);
    }

    if (!('value' in this.props)) {
      this.setState({
        value,
      });
    }
  }

  handleSearch(searchValue) {
    this.setState({
      searchValue,
    });
  }

  getPath(pos) {
    const items = [];

    const nums = pos.split('-');
    if (nums === 2) {
      items.push(this._p2n[pos]);
    } else {
      for (let i = 1; i < nums.length; i++) {
        const p = nums.slice(0, i + 1).join('-');
        items.push(this._p2n[p]);
      }
    }

    return items;
  }

  filterItems() {
    const {
      multiple,
      changeOnSelect,
      canOnlyCheckLeaf,
      filter,
    } = this.props;
    const { searchValue } = this.state;
    let items = Object.keys(this._p2n).map(p => this._p2n[p]);
    if ((!multiple && !changeOnSelect) || (multiple && canOnlyCheckLeaf)) {
      items = items.filter(
        item => !item.children || !item.children.length,
      );
    }

    return items
      .map(item => this.getPath(item.pos))
      .filter(path => filter(searchValue, path));
  }

  renderNotFound() {
    const { prefix, notFoundContent } = this.props;

    return (
      <Menu className={`${prefix}cascader-select-not-found`}>
        <Menu.Item>{notFoundContent}</Menu.Item>
      </Menu>
    );
  }

  renderCascader() {
    const { dataSource } = this.props;
    if (dataSource.length === 0) {
      return this.renderNotFound();
    }

    const { searchValue } = this.state;
    let filteredPaths = [];
    if (searchValue) {
      filteredPaths = this.filterItems();
      if (filteredPaths.length === 0) {
        return this.renderNotFound();
      }
    }

    const {
      multiple,
      useVirtual,
      changeOnSelect,
      checkStrictly,
      canOnlyCheckLeaf,
      defaultExpandedValue,
      expandTriggerType,
      onExpand,
      listStyle,
      listClassName,
      loadData,
      showSearch,
      resultRender,
      readOnly,
      itemRender,
    } = this.props;
    const { value } = this.state;

    const props = {
      dataSource,
      value,
      multiple,
      useVirtual,
      canOnlySelectLeaf: !changeOnSelect,
      checkStrictly,
      canOnlyCheckLeaf,
      defaultExpandedValue,
      expandTriggerType,
      ref: this.saveCascaderRef,
      onExpand,
      listStyle,
      listClassName,
      loadData,
      itemRender,
    };
    if (!readOnly) {
      props.onChange = this.handleChange;
      props.onSelect = this.handleSelect;
    }
    if (showSearch) {
      props.searchValue = searchValue;
      props.filteredPaths = filteredPaths;
      props.resultRender = resultRender;
      props.filteredListStyle = { height: this.cascaderHeight };
    }

    return <Cascader {...props} />;
  }

  renderPopupContent() {
    const { prefix, header, footer } = this.props;
    return (
      <div className={`${prefix}cascader-select-dropdown`}>
        {header}
        {this.renderCascader()}
        {footer}
      </div>
    );
  }

  renderPreview(others) {
    const { prefix, multiple, className, renderPreview } = this.props;
    const { value } = this.state;
    const previewCls = classNames(className, `${prefix}form-preview`);
    let items =
            (multiple
              ? this.getMultipleData(value)
              : this.getSignleData(value)) || [];

    if (!Array.isArray(items)) {
      items = [items];
    }

    if (typeof renderPreview === 'function') {
      return (
        <div {...others} className={previewCls}>
          {renderPreview(items, this.props)}
        </div>
      );
    }

    return (
      <p {...others} className={previewCls}>
        {items.map(({ label }) => label).join(', ')}
      </p>
    );
  }

  render() {
    const {
      prefix,
      size,
      hasArrow,
      hasBorder,
      hasClear,
      label,
      readOnly,
      placeholder,
      dataSource,
      disabled,
      multiple,
      className,
      showSearch,
      popupStyle,
      popupClassName,
      popupContainer,
      popupProps,
      followTrigger,
      isPreview,
    } = this.props;
    const { value, searchValue, visible } = this.state;
    const others = pickOthers(
      Object.keys(CascaderSelect.propTypes),
      this.props,
    );

    this.updateCache(dataSource);

    if (isPreview) {
      return this.renderPreview(others);
    }

    const popupContent = this.renderPopupContent();

    const props = {
      prefix,
      className,
      size,
      placeholder,
      disabled,
      hasArrow,
      hasBorder,
      hasClear,
      label,
      readOnly,
      ref: this.saveSelectRef,
      autoWidth: false,
      mode: multiple ? 'multiple' : 'single',
      value: multiple
        ? this.getMultipleData(value)
        : this.getSignleData(value),
      onChange: this.handleClear,
      onRemove: this.handleRemove,
      visible,
      onVisibleChange: this.handleVisibleChange,
      showSearch,
      // searchValue,
      onSearch: this.handleSearch,
      onKeyDown: this.handleKeyDown,
      popupContent,
      popupStyle,
      popupClassName,
      popupContainer,
      popupProps,
      followTrigger,
    };

    if (showSearch) {
      props.popupProps = {
        ...popupProps,
        ref: this.getPopup,
        afterOpen: this.handleAfterOpen,
      };
      props.autoWidth = showSearch && !!searchValue;
    }

    return <Select {...props} {...others} />;
  }
}
