import cx from 'classnames';
import { Component, createRef } from 'react';

import Popover from '../popover';
import { I18nReceiver as Receiver, II18nLocaleCascader } from '../i18n';
import MenuContent from './components/MenuContent';
import {
  union,
  difference,
  getPathValue,
  getPathLabel,
  getPathToNode,
} from './path-fns';
import { getNodeKey } from './node-fns';
import {
  ICascaderItem,
  CascaderValue,
  CascaderSearchClickHandler,
  CascaderChangeAction,
  CascaderLoadAction,
  ICascaderBaseProps,
  ICascaderChangeMeta,
  ICascaderLoadMeta,
  CascaderMenuClickHandler,
  CascaderMenuHoverHandler,
  CascaderItemSelectionState,
  ICascaderMultipleChangeMeta,
  CascaderSimplifySelectionMode,
} from './types';
import SearchContent from './components/SearchContent';
import debounce from '../utils/debounce';
import TextMark from '../text-mark';
import { DisabledContext, IDisabledContext } from '../disabled';
import shallowEqual from '../utils/shallowEqual';
import { TagsTrigger } from './trigger/TagsTrigger';
import { SingleTrigger } from './trigger/SingleTrigger';
import { Forest } from './forest';
import noop from '../utils/noop';
import memorizeOne from '../utils/memorize-one';
import { ICascaderTagsProps } from './trigger/Tags';
import { simplify } from './simplify';

export { ICascaderTagsProps };

export interface IMenuCascaderCommonProps extends ICascaderBaseProps {
  loadOptions?: (
    selectedOptions: ICascaderItem[] | null,
    meta: ICascaderLoadMeta
  ) => Promise<void>;
  expandTrigger?: 'click' | 'hover';
  scrollable?: boolean;
  /**
   * 滚动加载开启时，指定第一级数据是否还有更多数据
   */
  loadChildrenOnScroll?: boolean;
  searchable?: boolean;
  async?: boolean;
  asyncFilter?: (
    keyword: string,
    limit: number
  ) => Promise<Array<ICascaderItem[]>>;
  filter?: (keyword: string, path: ICascaderItem[]) => boolean;
  highlight?: (keyword: string, path: ICascaderItem[]) => React.ReactNode;
  limit?: number;
  multipleType?: 'normal' | 'checkbox';
  maxLine?: number | null;
  lineHeight?: number;
}

export interface IMenuCascaderSingleProps extends IMenuCascaderCommonProps {
  multiple?: false;
  value?: CascaderValue[];
  onChange: (
    value: CascaderValue[],
    selectedOptions: ICascaderItem[],
    meta: ICascaderChangeMeta
  ) => void;
  changeOnSelect?: boolean;
}

export interface IMenuCascaderMultipleProps extends IMenuCascaderCommonProps {
  multiple?: true;
  value?: Array<CascaderValue[]>;
  onChange: (
    value: Array<CascaderValue[]>,
    selectedOptions: Array<ICascaderItem[]>,
    meta: ICascaderMultipleChangeMeta
  ) => void;
  renderTags?: (props: ICascaderTagsProps) => React.ReactNode;
  simplifySelection?: boolean;
  simplifySelectionMode?: CascaderSimplifySelectionMode;
}

export type IMenuCascaderProps =
  | IMenuCascaderMultipleProps
  | IMenuCascaderSingleProps;

interface IMenuCascaderState {
  options: Forest;

  // Value to highlight
  activeValue: CascaderValue[];

  // 节点选中状态根据这个数据实时计算
  selectedPaths: Array<ICascaderItem[]>;
  visible: boolean;
  prevProps: IMenuCascaderProps;
  keyword: string;
  isSearching: boolean;
  searchResultList: Array<ICascaderItem[]>;

  // 当前正在加载状态的节点，e.g. 1-11-112
  loading: string[];
}

function isMultiple(
  props: IMenuCascaderProps
): props is IMenuCascaderMultipleProps {
  return props.multiple;
}

function isSingle(
  props: IMenuCascaderProps
): props is IMenuCascaderSingleProps {
  return !props.multiple;
}

const FILTER_DEBOUNCE_TIME = 200; // ms

const defaultFilter = (keyword: string, path: ICascaderItem[]): boolean => {
  return path.some(node =>
    node.label.toLowerCase().includes(keyword.toLowerCase())
  );
};

const defaultHighlight = (
  keyword: string,
  path: ICascaderItem[]
): React.ReactNode => {
  return path.map((node, index) => {
    return (
      <span key={getPathValue(path.slice(0, index + 1))}>
        <TextMark
          searchWords={[keyword]}
          textToHighlight={node.label}
          highlightClassName="zent-cascader-v2--highlight"
          autoEscape
        />
        {index !== path.length - 1 && ' / '}
      </span>
    );
  });
};

function getActiveValue(props: IMenuCascaderProps) {
  let activeValue: CascaderValue[] = [];
  if (isMultiple(props) && props.value.length > 0) {
    activeValue = props.value[0];
  }
  if (isSingle(props)) {
    activeValue = props.value;
  }

  return activeValue;
}

function getSelectedPaths(props: IMenuCascaderProps, options: Forest) {
  const selectedPaths = isMultiple(props)
    ? props.value.map(x => options.getPathByValue(x))
    : [options.getPathByValue(props.value)];

  // Filter out nested empty arrays
  // This can happen if `options` and `value` are set one by one
  return selectedPaths.filter(p => p.length !== 0);
}

function toggleLoading(
  loading: string[],
  val: string,
  isLoading: boolean
): string[] {
  const contains = loading.indexOf(val) !== -1;
  if (isLoading && !contains) {
    return loading.concat(val);
  }

  if (!isLoading && contains) {
    return loading.filter(v => v !== val);
  }

  return loading;
}

function isControlled(props: IMenuCascaderProps): boolean {
  return (
    'visible' in props &&
    'onVisibleChange' in props &&
    typeof props.onVisibleChange === 'function'
  );
}

function getVisible(
  props: IMenuCascaderProps,
  state: IMenuCascaderState
): boolean {
  if (isControlled(props)) {
    return !!props.visible;
  }

  return state.visible;
}

export class MenuCascader extends Component<
  IMenuCascaderProps,
  IMenuCascaderState
> {
  static defaultProps = {
    value: [],
    options: [],
    clearable: false,
    multiple: false,
    multipleType: 'checkbox',
    maxLine: null,
    lineHeight: 22,
    expandTrigger: 'click',
    scrollable: false,
    loadChildrenOnScroll: false,
    searchable: false,
    async: false,
    limit: 50,
    renderValue: getPathLabel,
    filter: defaultFilter,
    highlight: defaultHighlight,
    simplifySelectionMode: 'excludeDisabled',
  };

  constructor(props: IMenuCascaderProps) {
    super(props);

    const options = new Forest(props.options);
    this.state = {
      options,
      activeValue: getActiveValue(props),
      visible: false,
      prevProps: props,
      selectedPaths: getSelectedPaths(props, options),
      keyword: '',
      isSearching: false,
      searchResultList: [],
      loading: [],
    };
  }

  tagsTriggerRef = createRef<TagsTrigger>();

  static contextType = DisabledContext;
  context!: IDisabledContext;

  static getDerivedStateFromProps(
    props: IMenuCascaderProps,
    state: IMenuCascaderState
  ) {
    const { prevProps, options } = state;
    const newState: Partial<IMenuCascaderState> = {
      prevProps: props,
    };

    let newOptions = options;
    let optionsChanged = false;
    if (prevProps.options !== props.options) {
      newOptions = new Forest(props.options);
      newState.options = newOptions;
      optionsChanged = true;
    }

    if (
      optionsChanged ||
      prevProps.multiple !== props.multiple ||
      !shallowEqual(prevProps.value, props.value)
    ) {
      newState.selectedPaths = getSelectedPaths(props, newOptions);
    }

    // Reset highlighted item when popup closes
    const visible = getVisible(props, state);
    if (!visible) {
      newState.activeValue = getActiveValue(props);
    }

    return newState;
  }

  private get disabled() {
    const { disabled = this.context.value } = this.props;
    return disabled;
  }

  private isControlled(): boolean {
    return isControlled(this.props);
  }

  private getVisible(): boolean {
    return getVisible(this.props, this.state);
  }

  private setVisible(visible: boolean): void {
    if (this.isControlled()) {
      this.props.onVisibleChange(visible);
    } else {
      this.setState({
        visible,
      });
    }
  }

  // 根据选中信息生成所有节点的选中状态表 O(n)
  private getSelectionMapImpl(
    selectedPaths: Array<ICascaderItem[]>,
    mode: CascaderSimplifySelectionMode = 'excludeDisabled'
  ) {
    return this.state.options.reduceNodeDfs((map, node) => {
      const key = getNodeKey(node);
      const { value } = node;

      // 叶子节点，判断是否在选中的路径中；叶子节点不可能是 partial 状态
      if (node.children.length === 0) {
        const selected = selectedPaths.some(
          path => path[path.length - 1].value === value
        );
        map.set(key, selected ? 'on' : 'off');
      } else {
        // 忽略禁用的选项
        const children =
          mode === 'excludeDisabled'
            ? node.children.filter(n => !n.disabled)
            : node.children;
        const childrenState = children.reduce(
          (acc, n) => {
            const k = getNodeKey(n);
            const v = map.get(k);

            if (v === 'on') {
              acc.on += 1;
            } else if (v === 'off') {
              acc.off += 1;
            }

            return acc;
          },
          { on: 0, off: 0 }
        );

        const childrenCount = children.length;
        if (childrenState.on === childrenCount && childrenCount > 0) {
          map.set(key, 'on');
        } else if (childrenState.off === childrenCount) {
          map.set(key, 'off');
        } else {
          map.set(key, 'partial');
        }
      }

      return map;
    }, new Map<string, CascaderItemSelectionState>());
  }

  private getSelectionMap = memorizeOne(
    (selectedPaths: Array<ICascaderItem[]>) => {
      return this.getSelectionMapImpl(selectedPaths);
    }
  );

  // 用来计算不同simplify模式下的selectionMap，mode用来区分全选合并路径时候disabled的options是否作为有效数据
  private getSimplifySelectionMap = memorizeOne(
    (
      selectedPaths: Array<ICascaderItem[]>,
      mode: CascaderSimplifySelectionMode = 'excludeDisabled'
    ) => {
      return this.getSelectionMapImpl(selectedPaths, mode);
    }
  );

  private simplify: (
    options: Array<ICascaderItem[]>,
    mode: CascaderSimplifySelectionMode
  ) => Array<ICascaderItem[]> = (options, mode = 'excludeDisabled') => {
    return simplify(options, this.getSelectionMapImpl(options, mode));
  };

  // 搜索返回的结果列表中可能没有树状结构，这里根据 value 从当前的 options 里换取树结构中的节点
  private getSearchResultList = memorizeOne(
    (options: Forest, resultList: Array<ICascaderItem[]>) => {
      return resultList.map(path => {
        const values = path.map(x => x.value);
        return options.getPathByValue(values);
      });
    }
  );

  private onVisibleChange = (visible: boolean) => {
    const { keyword } = this.state;
    if (this.disabled) {
      return;
    }

    this.setVisible(visible);
    this.setState({
      keyword: visible === false ? '' : keyword,
    });
  };

  private onKeywordChange = (keyword: string) => {
    this.setState({ keyword }, this.filterOptions);
  };

  private filterOptions = debounce(() => {
    const { keyword, options } = this.state;

    if (!keyword) {
      return;
    }

    const { async, asyncFilter, filter, limit } = this.props;
    if (async) {
      this.setState({ isSearching: true });

      asyncFilter(keyword, limit)
        .then(searchList => {
          this.setSearchState(searchList);
        })
        .finally(() => {
          this.setState({ isSearching: false });
        });
    } else {
      const searchList = options
        .reducePath((acc, path) => {
          acc.push(path);
          return acc;
        }, [])
        .filter(path => filter(keyword, path));
      this.setSearchState(searchList);
    }
  }, FILTER_DEBOUNCE_TIME);

  private setSearchState = (searchList: Array<ICascaderItem[]>) => {
    const { limit } = this.props;
    const size = searchList.length;

    this.setState({
      searchResultList: limit <= size ? searchList : searchList.slice(0, limit),
    });
  };

  private onMenuOptionHover: CascaderMenuHoverHandler = node => {
    this.onMenuOptionSelect(node, noop, 'hover');
  };

  private onMenuOptionClick: CascaderMenuClickHandler = (node, closePopup) => {
    this.onMenuOptionSelect(node, closePopup, 'click');
  };

  private onMenuOptionSelect = (
    node: ICascaderItem,
    closePopup: () => void,
    source: 'click' | 'hover'
  ) => {
    const { loadOptions, multiple } = this.props;
    const { loading } = this.state;
    const needLoading = node.loadChildrenOnExpand && loadOptions;

    const selectedOptions = getPathToNode(node);
    const newValue = selectedOptions.map(n => n.value);

    const newState: Partial<IMenuCascaderState> = {
      activeValue: newValue,
      keyword: '',
    };

    const hasChildren = node.children && node.children.length > 0;
    const needClose =
      !node.loadChildrenOnExpand &&
      !hasChildren &&
      !multiple &&
      source === 'click';

    // 设置 loading 状态
    const nodeKey = getNodeKey(node);
    if (needLoading) {
      newState.loading = toggleLoading(loading, nodeKey, true);
    }

    this.setState(newState as IMenuCascaderState, () => {
      if (needLoading) {
        loadOptions(selectedOptions, {
          action: CascaderLoadAction.LoadChildren,
        }).finally(() => {
          // 结束 loading 状态
          this.setState(state => {
            return {
              loading: toggleLoading(state.loading, nodeKey, false),
            };
          });
        });
      }

      if (isSingle(this.props)) {
        const { changeOnSelect = false } = this.props;
        const needTriggerChange =
          needClose || (changeOnSelect && source === 'click');

        if (needTriggerChange) {
          this.props.onChange(
            selectedOptions.map(it => it.value),
            selectedOptions,
            { action: CascaderChangeAction.Change }
          );
        }
      }

      if (needClose) {
        closePopup();
      }
    });
  };

  /**
   * 复选框勾选/取消勾选才会触发，所以仅适用于多选场景
   */
  private toggleMenuOption = (node: ICascaderItem, checked: boolean) => {
    if (isMultiple(this.props)) {
      const { onChange } = this.props;
      const { options, selectedPaths: oldSelectedPaths } = this.state;

      // filter out paths that contain disabled node
      const affectedPaths = options.getPaths(node, path =>
        path.every(node => !node.disabled)
      );
      let selectedPaths = checked
        ? union(oldSelectedPaths, affectedPaths)
        : difference(oldSelectedPaths, affectedPaths);
      selectedPaths = options.sort(selectedPaths);

      const value = selectedPaths.map(list => list.map(n => n.value));

      this.setState({ selectedPaths }, () => {
        onChange(value, selectedPaths, {
          action: CascaderChangeAction.Change,
          simplify: this.simplify,
        });

        if (this.props.searchable) {
          // focus to search input
          this.tagsTriggerRef.current?.focus();
        }
      });
    }
  };

  private onSearchOptionClick: CascaderSearchClickHandler = (
    path,
    closePopup
  ) => {
    const activeValue = path.map(n => n.value);

    this.setState({ activeValue }, () => {
      this.onMenuOptionClick(path[path.length - 1], closePopup);
    });
  };

  private toggleSearchOption = (path: ICascaderItem[], checked: boolean) => {
    this.toggleMenuOption(path[path.length - 1], checked);
  };

  private onClear = () => {
    this.setVisible(false);
    this.setState(
      {
        activeValue: [],
        selectedPaths: [],
      },
      () => {
        if (isSingle(this.props)) {
          this.props.onChange([], [], { action: CascaderChangeAction.Clear });
        } else {
          this.props.onChange([], [], {
            action: CascaderChangeAction.Clear,
            simplify: this.simplify,
          });
        }
      }
    );
  };

  private scrollLoad = (parent: ICascaderItem | null) => {
    const { loadOptions } = this.props;
    // 判断是否要加载更多
    const currentHasMore = parent
      ? parent.loadChildrenOnScroll
      : this.props.loadChildrenOnScroll;

    if (currentHasMore === false) {
      return Promise.resolve();
    }

    const selectedOptions = getPathToNode(parent);
    return loadOptions(selectedOptions, {
      action: CascaderLoadAction.Scroll,
    });
  };

  private onRemove = (node: ICascaderItem) => {
    if (this.disabled) {
      return;
    }

    // 只有多选情况下才存在移除，即取消叶子节点的选中
    this.toggleMenuOption(node, false);
  };

  private renderPopoverContent = (i18n: II18nLocaleCascader) => {
    const {
      expandTrigger,
      scrollable,
      multiple,
      searchable,
      highlight,
      loadChildrenOnScroll,
      renderItemContent,
      getItemTooltip,
      renderList,
      multipleType,
    } = this.props;
    const {
      options,
      activeValue,
      keyword,
      isSearching,
      searchResultList,
      loading,
      selectedPaths,
    } = this.state;
    const visible = this.getVisible();
    const selectionMap = this.getSelectionMap(selectedPaths);

    if (searchable && visible && keyword) {
      return (
        <SearchContent
          i18n={i18n}
          multiple={multiple}
          isSearching={isSearching}
          searchList={this.getSearchResultList(options, searchResultList)}
          keyword={keyword}
          highlight={highlight}
          onOptionToggle={this.toggleSearchOption}
          onOptionClick={this.onSearchOptionClick}
          selectionMap={selectionMap}
        />
      );
    }

    return (
      <MenuContent
        value={activeValue}
        options={options.getTrees()}
        expandTrigger={expandTrigger}
        i18n={i18n}
        scrollable={scrollable}
        loadChildrenOnScroll={loadChildrenOnScroll}
        multiple={multiple}
        multipleType={multipleType}
        onOptionClick={this.onMenuOptionClick}
        onOptionHover={this.onMenuOptionHover}
        scrollLoad={this.scrollLoad}
        onOptionToggle={this.toggleMenuOption}
        loading={loading}
        selectionMap={selectionMap}
        renderItemContent={renderItemContent}
        getItemTooltip={getItemTooltip}
        renderList={renderList}
      />
    );
  };

  render() {
    const {
      className,
      popupClassName,
      placeholder,
      searchable,
      clearable,
      renderValue,
      maxLine,
      lineHeight,
    } = this.props;
    const { selectedPaths, keyword } = this.state;
    const visible = this.getVisible();
    const hasValue = selectedPaths.length > 0;

    return (
      <Receiver componentName="Cascader">
        {i18n => {
          const triggerCommonProps = {
            placeholder,
            disabled: this.disabled,
            className,
            clearable,
            visible,
            keyword,
            searchable,
            i18n,
            renderValue,
            maxLine,
            lineHeight,
            onClear: this.onClear,
            onKeywordChange: this.onKeywordChange,
          };

          return (
            <Popover
              className={cx('zent-cascader-v2__popup', popupClassName)}
              position={Popover.Position.CascaderAutoBottomLeft}
              visible={visible}
              onVisibleChange={this.onVisibleChange}
              cushion={4}
            >
              <Popover.Trigger.Click toggle={!searchable}>
                {isMultiple(this.props) ? (
                  <TagsTrigger
                    {...triggerCommonProps}
                    simplifyPaths={this.props.simplifySelection ?? false}
                    selectedPaths={selectedPaths}
                    selectionMap={this.getSimplifySelectionMap(
                      selectedPaths,
                      this.props.simplifySelectionMode
                    )}
                    onRemove={this.onRemove}
                    renderTags={this.props.renderTags}
                    ref={this.tagsTriggerRef}
                  />
                ) : (
                  <SingleTrigger
                    {...triggerCommonProps}
                    selectedPath={hasValue ? selectedPaths[0] : []}
                  />
                )}
              </Popover.Trigger.Click>
              <Popover.Content>
                {this.renderPopoverContent(i18n)}
              </Popover.Content>
            </Popover>
          );
        }}
      </Receiver>
    );
  }
}

export default MenuCascader;
