import React from 'react';
import Overlay from '../../components/Overlay';
import Checkbox from '../../components/Checkbox';
import PopOver from '../../components/PopOver';
import {Icon} from '../../components/icons';
import {
  autobind,
  flattenTree,
  filterTree,
  string2regExp,
  getTreeAncestors,
  getTreeParent,
  ucFirst
} from '../../utils/helper';
import {
  FormOptionsControl,
  OptionsControl,
  OptionsControlProps
} from '../Form/Options';
import {Option, Options} from '../../components/Select';
import {findDOMNode} from 'react-dom';
import {ResultBox, Spinner} from '../../components';
import xor from 'lodash/xor';
import union from 'lodash/union';
import compact from 'lodash/compact';
import {RootClose} from '../../utils/RootClose';

/**
 * Nested Select
 * 文档：https://baidu.gitee.io/amis/docs/components/form/nested-select
 */
export interface NestedSelectControlSchema extends FormOptionsControl {
  type: 'nested-select';
  /**
   * 边框模式，全边框，还是半边框，或者没边框。
   */
  borderMode?: 'full' | 'half' | 'none';

  /**
   * 弹框的 css 类
   */
  menuClassName?: string;

  /**
   * 父子之间是否完全独立。
   */
  cascade?: boolean;

  /**
   * 选父级的时候是否把子节点的值也包含在内。
   */
  withChildren?: boolean;

  /**
   * 选父级的时候，是否只把子节点的值包含在内
   */
  onlyChildren?: boolean;

  /**
   * 是否隐藏选择框中已选中节点的祖先节点的文本信息
   */
  hideNodePathLabel?: boolean;
}

export interface NestedSelectProps extends OptionsControlProps {
  cascade?: boolean;
  noResultsText?: string;
  withChildren?: boolean;
  onlyChildren?: boolean;
  hideNodePathLabel?: boolean;
}

export interface NestedSelectState {
  isOpened?: boolean;
  isFocused?: boolean;
  inputValue?: string;
  stack: Array<Array<Option>>;
}

export default class NestedSelectControl extends React.Component<
  NestedSelectProps,
  NestedSelectState
> {
  static defaultProps: Partial<NestedSelectProps> = {
    cascade: false,
    withChildren: false,
    onlyChildren: false,
    searchPromptText: 'Select.searchPromptText',
    noResultsText: 'noResult',
    checkAll: true,
    checkAllLabel: '全选',
    hideNodePathLabel: false
  };
  target: any;
  input: HTMLInputElement;
  state: NestedSelectState = {
    isOpened: false,
    isFocused: false,
    inputValue: '',
    stack: [this.props.options]
  };

  @autobind
  domRef(ref: any) {
    this.target = ref;
  }

  componentDidUpdate(prevProps: NestedSelectProps) {
    if (prevProps.options !== this.props.options) {
      this.setState({
        stack: [this.props.options]
      });
    }
  }

  @autobind
  handleOutClick(e: React.MouseEvent<any>) {
    const {options} = this.props;
    e.defaultPrevented ||
      this.setState({
        isOpened: true
      });
  }

  @autobind
  close() {
    this.setState({
      isOpened: false
    });
  }

  removeItem(index: number, e?: React.MouseEvent<HTMLElement>) {
    let {
      onChange,
      selectedOptions,
      joinValues,
      valueField,
      extractValue,
      delimiter,
      value
    } = this.props;

    e && e.stopPropagation();

    selectedOptions.splice(index, 1);

    if (joinValues) {
      value = (selectedOptions as Options)
        .map(item => item[valueField || 'value'])
        .join(delimiter || ',');
    } else if (extractValue) {
      value = (selectedOptions as Options).map(
        item => item[valueField || 'value']
      );
    }

    onChange(value);
  }

  @autobind
  renderValue(item: Option, key?: any) {
    const {classnames: cx, labelField, options, hideNodePathLabel} = this.props;

    if (hideNodePathLabel) {
      return item[labelField || 'label'];
    }
    const ancestors = getTreeAncestors(options, item, true);

    return (
      <span className={cx('Select-valueLabel')} key={key}>
        {`${
          ancestors
            ? ancestors
                .map(item => `${item[labelField || 'label']}`)
                .join(' / ')
            : item[labelField || 'label']
        }`}
      </span>
    );
  }

  @autobind
  handleOptionClick(option: Option) {
    const {multiple, onChange, joinValues, extractValue, valueField} =
      this.props;

    if (multiple) {
      return;
    }

    onChange(
      joinValues
        ? option[valueField || 'value']
        : extractValue
        ? option[valueField || 'value']
        : option
    );
    !multiple && this.close();
  }

  @autobind
  handleCheck(option: Option | Options, index?: number) {
    const {
      onChange,
      selectedOptions,
      joinValues,
      delimiter,
      extractValue,
      withChildren,
      onlyChildren,
      cascade,
      options
    } = this.props;
    const {stack} = this.state;

    let valueField = this.props.valueField || 'value';

    if (
      !Array.isArray(option) &&
      option.children &&
      option.children.length &&
      typeof index === 'number'
    ) {
      if (stack[index]) {
        stack.splice(index + 1, 1, option.children);
      } else {
        stack.push(option.children);
      }
    }

    const items = selectedOptions;
    let value: any[];
    /**
     * 打平树且只保留叶节点
     */
    const flattenTreeWithLeafNodes = (option: Option | Options) =>
      compact(
        flattenTree(Array.isArray(option) ? option : [option], node =>
          node.children && node.children.length ? null : node
        )
      );

    // 三种情况：
    // 1.全选，option为数组
    // 2.单个选中，且有children
    // 3.单个选中，没有children

    if (Array.isArray(option)) {
      if (withChildren) {
        option = flattenTree(option);
      } else if (onlyChildren) {
        option = flattenTreeWithLeafNodes(option);
      }
      value = items.length === option.length ? [] : (option as Options);
    } else if (Array.isArray(option.children)) {
      if (cascade) {
        value = xor(items, [option]);
      } else if (withChildren) {
        option = flattenTree([option]);
        const isEvery = (option as Options).every(opt => !!~items.indexOf(opt));
        value = (isEvery ? xor : union)(items, option as any);
      } else if (onlyChildren) {
        option = flattenTreeWithLeafNodes(option);
        const isEvery = (option as Options).every(opt => !!~items.indexOf(opt));
        value = (isEvery ? xor : union)(items, option as any);
      } else {
        value = items.filter(item => !~flattenTree([option]).indexOf(item));
        !~items.indexOf(option) && value.push(option);
      }
    } else {
      value = xor(items, [option]);
    }

    if (!cascade) {
      let toCheck = option;

      while (true) {
        const parent = getTreeParent(options, toCheck as any);
        if (parent?.value) {
          // 如果所有孩子节点都勾选了，应该自动勾选父级。

          if (parent.children.every((child: any) => ~value.indexOf(child))) {
            parent.children.forEach((child: any) => {
              const index = value.indexOf(child);
              if (~index && !withChildren && !onlyChildren) {
                value.splice(index, 1);
              }
            });

            if (!onlyChildren) {
              value.push(parent);
            }

            toCheck = parent;
            continue;
          }
        }
        break;
      }
    }

    onChange(
      joinValues
        ? value.map(item => item[valueField as string]).join(delimiter)
        : extractValue
        ? value.map(item => item[valueField as string])
        : value
    );
  }

  allChecked(options: Options): boolean {
    const {selectedOptions, withChildren, onlyChildren} = this.props;
    return options.every(option => {
      if ((withChildren || onlyChildren) && option.children) {
        return this.allChecked(option.children);
      }
      return selectedOptions.some(item => item === option);
    });
  }

  partialChecked(options: Options): boolean {
    return options.some(option => {
      const childrenPartialChecked =
        option.children && this.partialChecked(option.children);
      return (
        childrenPartialChecked ||
        this.props.selectedOptions.some(item => item === option)
      );
    });
  }

  reload() {
    const reload = this.props.reloadOptions;
    reload && reload();
  }

  @autobind
  onFocus(e: any) {
    this.props.disabled ||
      this.state.isOpened ||
      this.setState({
        isFocused: true
      });
  }

  @autobind
  onBlur(e: any) {
    this.setState({
      isFocused: false
    });
  }

  @autobind
  getTarget() {
    if (!this.target) {
      this.target = findDOMNode(this) as HTMLElement;
    }
    return this.target as HTMLElement;
  }

  @autobind
  handleKeyPress(e: React.KeyboardEvent) {
    if (e.key === ' ') {
      this.handleOutClick(e as any);
      e.preventDefault();
    }
  }

  @autobind
  handleInputKeyDown(event: React.KeyboardEvent) {
    const inputValue = this.state.inputValue;
    const {multiple, selectedOptions} = this.props;

    if (
      event.key === 'Backspace' &&
      !inputValue &&
      selectedOptions.length &&
      multiple
    ) {
      this.removeItem(selectedOptions.length - 1);
    }
  }

  @autobind
  handleInputChange(inputValue: string) {
    const {options, labelField, valueField} = this.props;

    const regexp = string2regExp(inputValue);

    let filtedOptions =
      inputValue && this.state.isOpened
        ? filterTree(
            options,
            option =>
              regexp.test(option[labelField || 'label']) ||
              regexp.test(option[valueField || 'value']) ||
              !!(option.children && option.children.length),
            1,
            true
          )
        : options.concat();

    this.setState({
      inputValue,
      stack: [filtedOptions]
    });
  }

  @autobind
  handleResultChange(value: Array<Option>) {
    const {
      joinValues,
      extractValue,
      delimiter,
      valueField,
      onChange,
      multiple
    } = this.props;

    let newValue: any = Array.isArray(value) ? value.concat() : [];

    if (!multiple && !newValue.length) {
      onChange('');
      return;
    }

    if (joinValues || extractValue) {
      newValue = value.map(item => item[valueField || 'value']);
    }

    if (joinValues) {
      newValue = newValue.join(delimiter || ',');
    }

    onChange(newValue);
  }

  renderOptions() {
    const {
      multiple,
      selectedOptions,
      classnames: cx,
      options: propOptions,
      disabled,
      checkAll,
      checkAllLabel,
      translate: __,
      labelField,
      menuClassName,
      cascade,
      onlyChildren
    } = this.props;
    const valueField = this.props.valueField || 'value';

    const stack = this.state.stack;

    let partialChecked = this.partialChecked(propOptions);
    let allChecked = this.allChecked(propOptions);

    return (
      <>
        {stack.map((options, index) => (
          <div key={index} className={cx('NestedSelect-menu', menuClassName)}>
            {multiple && checkAll && index === 0 ? (
              <div className={cx('NestedSelect-option', 'checkall')}>
                <Checkbox
                  size="sm"
                  onChange={this.handleCheck.bind(this, options)}
                  checked={partialChecked}
                  partial={partialChecked && !allChecked}
                ></Checkbox>
                <span onClick={this.handleCheck.bind(this, options)}>
                  {__(checkAllLabel)}
                </span>
              </div>
            ) : null}

            {options.map((option: Option, idx: number) => {
              const ancestors = getTreeAncestors(propOptions, option as any);
              const parentChecked = ancestors?.some(
                item => !!~selectedOptions.indexOf(item)
              );
              const uncheckable = cascade ? false : multiple && parentChecked;
              const parentDisabled = ancestors?.some(item => !!item.disabled);

              let nodeDisabled =
                uncheckable || option.disabled || parentDisabled || !!disabled;
              let selfChildrenChecked = !!(
                option.children && this.partialChecked(option.children)
              );
              let selfChecked =
                uncheckable || !!~selectedOptions.indexOf(option);

              if (
                !selfChecked &&
                onlyChildren &&
                option.children &&
                this.allChecked(option.children)
              ) {
                selfChecked = true;
              }

              return (
                <div
                  key={idx}
                  className={cx('NestedSelect-option', {
                    'is-active':
                      !nodeDisabled &&
                      (selfChecked || (!cascade && selfChildrenChecked))
                  })}
                  onMouseEnter={this.onMouseEnter.bind(this, option, index)}
                >
                  {multiple ? (
                    <Checkbox
                      size="sm"
                      onChange={this.handleCheck.bind(this, option, index)}
                      trueValue={option[valueField]}
                      checked={selfChecked || (!cascade && selfChildrenChecked)}
                      partial={!selfChecked}
                      disabled={nodeDisabled}
                    ></Checkbox>
                  ) : null}

                  <div
                    className={cx('NestedSelect-optionLabel', {
                      'is-disabled': nodeDisabled
                    })}
                    onClick={() =>
                      !nodeDisabled &&
                      (multiple
                        ? this.handleCheck(option, index)
                        : this.handleOptionClick(option))
                    }
                  >
                    {option[labelField || 'label']}
                  </div>

                  {option.children && option.children.length ? (
                    <div className={cx('NestedSelect-optionArrowRight')}>
                      <Icon icon="right-arrow" className="icon" />
                    </div>
                  ) : null}
                </div>
              );
            })}
          </div>
        ))}
      </>
    );
  }

  onMouseEnter(option: Option, index: number, e: MouseEvent) {
    let {stack} = this.state;
    index = index + 1;

    const children = option.children;
    if (children && children.length) {
      if (stack[index]) {
        stack.splice(index, 1, children);
      } else {
        stack.push(children);
      }
    } else {
      stack[index] && stack.splice(index, 1);
    }

    this.setState({stack: stack.slice(0, index + 1)});
  }

  renderOuter() {
    const {
      popOverContainer,
      translate: __,
      classnames: cx,
      options,
      render
    } = this.props;
    let noResultsText: any = this.props.noResultsText;

    if (noResultsText) {
      noResultsText = render('noResultText', __(noResultsText));
    }

    let body = (
      <RootClose disabled={!this.state.isOpened} onRootClose={this.close}>
        {(ref: any) => {
          return (
            <div className={cx('NestedSelect-menuOuter')} ref={ref}>
              {options.length ? (
                this.renderOptions()
              ) : (
                <div className={cx('NestedSelect-noResult')}>
                  {noResultsText}
                </div>
              )}
            </div>
          );
        }}
      </RootClose>
    );

    return (
      <Overlay
        target={this.getTarget}
        container={popOverContainer || findDOMNode(this)?.parentElement}
        placement={'auto'}
        show
      >
        <PopOver className={cx('NestedSelect-popover')}>{body}</PopOver>
      </Overlay>
    );
  }

  render() {
    const {
      className,
      disabled,
      classnames: cx,
      multiple,
      placeholder,
      translate: __,
      inline,
      searchable,
      autoComplete,
      selectedOptions,
      clearable,
      loading,
      borderMode
    } = this.props;

    return (
      <div className={cx('NestedSelectControl', className)}>
        <ResultBox
          disabled={disabled}
          ref={this.domRef}
          placeholder={__(placeholder || '空')}
          className={cx(`NestedSelect`, {
            'NestedSelect--inline': inline,
            'NestedSelect--single': !multiple,
            'NestedSelect--multi': multiple,
            'NestedSelect--searchable': searchable,
            'is-opened': this.state.isOpened,
            'is-focused': this.state.isFocused,
            [`NestedSelect--border${ucFirst(borderMode)}`]: borderMode
          })}
          result={
            multiple
              ? selectedOptions
              : selectedOptions.length
              ? this.renderValue(selectedOptions[0])
              : ''
          }
          onResultClick={this.handleOutClick}
          value={this.state.inputValue}
          onChange={this.handleInputChange}
          onResultChange={this.handleResultChange}
          itemRender={this.renderValue}
          onKeyPress={this.handleKeyPress}
          onFocus={this.onFocus}
          onBlur={this.onBlur}
          onKeyDown={this.handleInputKeyDown}
          clearable={clearable}
          allowInput={searchable}
          inputPlaceholder={''}
        >
          {loading ? <Spinner size="sm" /> : undefined}
        </ResultBox>
        {this.state.isOpened ? this.renderOuter() : null}
      </div>
    );
  }
}

@OptionsControl({
  type: 'nested-select'
})
export class NestedSelectControlRenderer extends NestedSelectControl {}
