import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import cls from 'classnames';
import PropTypes from 'prop-types';
import { isEqual, isString, isEmpty, noop, get, isFunction, isUndefined, isNull, pick } from 'lodash';
import TreeSelectFoundation, {
    Size,
    BasicTriggerRenderProps,
    /* Corresponding props */
    BasicTreeSelectProps,
    /* Corresponding state */
    BasicTreeSelectInnerData,
    TreeSelectAdapter
} from '@douyinfe/semi-foundation/treeSelect/foundation';
import {
    convertDataToEntities,
    flattenTreeData,
    calcExpandedKeysForValues,
    calcMotionKeys,
    findKeysForValues,
    calcCheckedKeys,
    calcExpandedKeys,
    getValueOrKey,
    normalizeKeyList,
    calcDisabledKeys,
    normalizeValue,
    updateKeys,
    filterTreeData
} from '@douyinfe/semi-foundation/tree/treeUtil';
import { cssClasses, strings } from '@douyinfe/semi-foundation/treeSelect/constants';
import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
import { FixedSizeList as VirtualList, ListItemKeySelector } from 'react-window';
import '@douyinfe/semi-foundation/tree/tree.scss';
import '@douyinfe/semi-foundation/treeSelect/treeSelect.scss';
import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
import ConfigContext, { ContextValue } from '../configProvider/context';
import TagGroup from '../tag/group';
import Tag, { TagProps } from '../tag/index';
import Input, { InputProps } from '../input/index';
import AutoSizer from '../tree/autoSizer';
import TreeContext from '../tree/treeContext';
import TreeNode from '../tree/treeNode';
import NodeList from '../tree/nodeList';
import { cloneDeep } from '../tree/treeUtil';
import LocaleConsumer from '../locale/localeConsumer';
import { Locale } from '../locale/interface';
import Trigger from '../trigger';
import TagInput from '../tagInput';
import { isSemiIcon } from '../_utils';
import { OptionProps, TreeProps, TreeState, FlattenNode, TreeNodeData, TreeNodeProps } from '../tree/interface';
import { IconChevronDown, IconClear, IconSearch } from '@douyinfe/semi-icons';
import CheckboxGroup from '../checkbox/checkboxGroup';
import Popover, { PopoverProps } from '../popover/index';
import VirtualRow from '../select/virtualRow';

export type ExpandAction = false | 'click' | 'doubleClick';

export interface TriggerRenderProps extends Omit<BasicTriggerRenderProps, 'componentProps'> {
    [x: string]: any;
    componentProps: TreeSelectProps;
    value: TreeNodeData[];
    onClear: (e: React.MouseEvent) => void
}

export interface OnChange {
    /* onChangeWithObject is false */
    (
        value: TreeNodeData['value'] | Array<TreeNodeData['value']>,
        node: TreeNodeData[] | TreeNodeData,
        e: React.MouseEvent
    ): void;
    /* onChangeWithObject is true */
    (node: TreeNodeData[] | TreeNodeData, e: React.MouseEvent): void
}

export type RenderSelectedItemInSingle = (treeNode: TreeNodeData) => React.ReactNode;

export type RenderSelectedItemInMultiple = (
    treeNode: TreeNodeData,
    otherProps: { index: number | string; onClose: (tagContent: any, e: React.MouseEvent) => void }
) => {
    isRenderInTag: boolean;
    content: React.ReactNode
};

export type RenderSelectedItem = RenderSelectedItemInSingle | RenderSelectedItemInMultiple;

export type OverrideCommonProps =
    'renderFullLabel'
    | 'renderLabel'
    | 'defaultValue'
    | 'emptyContent'
    | 'filterTreeNode'
    | 'style'
    | 'treeData'
    | 'value'
    | 'onExpand'
    | 'keyMaps'
    | 'showLine';

/**
* Type definition description:
* TreeSelectProps inherits some properties from BasicTreeSelectProps (from foundation) and TreeProps (from semi-ui-react).
*/
export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideCommonProps | 'validateStatus' | 'searchRender'>, Pick<TreeProps, OverrideCommonProps> {
    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
    'aria-required'?: React.AriaAttributes['aria-required'];
    motion?: boolean;
    mouseEnterDelay?: number;
    mouseLeaveDelay?: number;
    arrowIcon?: React.ReactNode;
    autoAdjustOverflow?: boolean;
    clickToHide?: boolean;
    clearIcon?: React.ReactNode;
    defaultOpen?: boolean;
    dropdownClassName?: string;
    dropdownMatchSelectWidth?: boolean;
    dropdownStyle?: React.CSSProperties;
    dropdownMargin?: PopoverProps['margin'];
    insetLabel?: React.ReactNode;
    insetLabelId?: string;
    maxTagCount?: number;
    motionExpand?: boolean;
    optionListStyle?: React.CSSProperties;
    outerBottomSlot?: React.ReactNode;
    outerTopSlot?: React.ReactNode;
    placeholder?: string;
    prefix?: React.ReactNode;
    position?: PopoverProps['position'];
    searchAutoFocus?: boolean;
    searchPlaceholder?: string;
    showSearchClear?: boolean;
    size?: Size;
    suffix?: React.ReactNode;
    treeNodeLabelProp?: string;
    validateStatus?: ValidateStatus;
    zIndex?: number;
    searchPosition?: string;
    stopPropagation?: boolean | string;
    restTagsPopoverProps?: PopoverProps;
    searchRender?: boolean | ((inputProps: InputProps) => React.ReactNode);
    onSelect?: (selectedKey: string, selected: boolean, selectedNode: TreeNodeData) => void;
    renderSelectedItem?: RenderSelectedItem;
    getPopupContainer?: () => HTMLElement;
    triggerRender?: (props: TriggerRenderProps) => React.ReactNode;
    onBlur?: (e: React.MouseEvent) => void;
    onChange?: OnChange;
    onFocus?: (e: React.MouseEvent) => void;
    onVisibleChange?: (isVisible: boolean) => void;
    onClear?: (e: React.MouseEvent | React.KeyboardEvent<HTMLDivElement>) => void
}

export type OverrideCommonState =
    'keyEntities'
    | 'treeData'
    | 'disabledKeys'
    | 'flattenNodes';

export interface TreeSelectState extends Omit<BasicTreeSelectInnerData, OverrideCommonState | 'prevProps'>, Pick<TreeState, OverrideCommonState> {
    inputTriggerFocus: boolean;
    isOpen: boolean;
    // isInput: boolean;
    rePosKey: number;
    dropdownMinWidth: null | number;
    isHovering: boolean;
    prevProps: TreeSelectProps;
    isFocus: boolean
}

const prefixcls = cssClasses.PREFIX;
const prefixTree = cssClasses.PREFIX_TREE;

const key = 0;

class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
    static contextType = ConfigContext;

    static propTypes = {
        'aria-describedby': PropTypes.string,
        'aria-errormessage': PropTypes.string,
        'aria-invalid': PropTypes.bool,
        'aria-labelledby': PropTypes.string,
        'aria-required': PropTypes.bool,
        borderless: PropTypes.bool,
        loadedKeys: PropTypes.arrayOf(PropTypes.string),
        loadData: PropTypes.func,
        onLoad: PropTypes.func,
        arrowIcon: PropTypes.node,
        clearIcon: PropTypes.node,
        defaultOpen: PropTypes.bool,
        defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
        defaultExpandAll: PropTypes.bool,
        defaultExpandedKeys: PropTypes.array,
        expandAll: PropTypes.bool,
        disabled: PropTypes.bool,
        disableStrictly: PropTypes.bool,
        // Whether to turn on the input box filtering function, when it is a function, it represents a custom filtering function
        filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
        multiple: PropTypes.bool,
        searchPlaceholder: PropTypes.string,
        searchAutoFocus: PropTypes.bool,
        virtualize: PropTypes.object,
        treeNodeFilterProp: PropTypes.string,
        onChange: PropTypes.func,
        onClear: PropTypes.func,
        onSearch: PropTypes.func,
        onSelect: PropTypes.func,
        onExpand: PropTypes.func,
        onChangeWithObject: PropTypes.bool,
        onBlur: PropTypes.func,
        onFocus: PropTypes.func,
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
        expandedKeys: PropTypes.array,
        autoExpandParent: PropTypes.bool,
        showClear: PropTypes.bool,
        showSearchClear: PropTypes.bool,
        autoAdjustOverflow: PropTypes.bool,
        showFilteredOnly: PropTypes.bool,
        showLine: PropTypes.bool,
        motionExpand: PropTypes.bool,
        emptyContent: PropTypes.node,
        keyMaps: PropTypes.object,
        leafOnly: PropTypes.bool,
        treeData: PropTypes.arrayOf(
            PropTypes.shape({
                key: PropTypes.string,
                value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
                label: PropTypes.any,
            })
        ),
        dropdownClassName: PropTypes.string,
        dropdownStyle: PropTypes.object,
        dropdownMargin: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
        motion: PropTypes.bool,
        placeholder: PropTypes.string,
        maxTagCount: PropTypes.number,
        size: PropTypes.oneOf<TreeSelectProps['size']>(strings.SIZE_SET),
        className: PropTypes.string,
        style: PropTypes.object,
        treeNodeLabelProp: PropTypes.string,
        suffix: PropTypes.node,
        prefix: PropTypes.node,
        insetLabel: PropTypes.node,
        insetLabelId: PropTypes.string,
        zIndex: PropTypes.number,
        getPopupContainer: PropTypes.func,
        dropdownMatchSelectWidth: PropTypes.bool,
        validateStatus: PropTypes.oneOf(strings.STATUS),
        mouseEnterDelay: PropTypes.number,
        mouseLeaveDelay: PropTypes.number,
        triggerRender: PropTypes.func,
        stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
        outerBottomSlot: PropTypes.node,
        outerTopSlot: PropTypes.node,
        onVisibleChange: PropTypes.func,
        expandAction: PropTypes.oneOf(['click' as const, 'doubleClick' as const, false as const]),
        searchPosition: PropTypes.oneOf([strings.SEARCH_POSITION_DROPDOWN, strings.SEARCH_POSITION_TRIGGER]),
        clickToHide: PropTypes.bool,
        renderLabel: PropTypes.func,
        renderFullLabel: PropTypes.func,
        labelEllipsis: PropTypes.bool,
        optionListStyle: PropTypes.object,
        searchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
        renderSelectedItem: PropTypes.func,
        checkRelation: PropTypes.string,
        'aria-label': PropTypes.string,
        showRestTagsPopover: PropTypes.bool,
        restTagsPopoverProps: PropTypes.object,
        preventScroll: PropTypes.bool,
        clickTriggerToHide: PropTypes.bool,
    };

    static defaultProps: Partial<TreeSelectProps> = {
        borderless: false,
        searchPosition: strings.SEARCH_POSITION_DROPDOWN,
        arrowIcon: <IconChevronDown />,
        autoExpandParent: false,
        autoAdjustOverflow: true,
        stopPropagation: true,
        motion: true,
        motionExpand: true,
        expandAll: false,
        zIndex: popoverNumbers.DEFAULT_Z_INDEX,
        disableStrictly: false,
        multiple: false,
        filterTreeNode: false,
        size: 'default' as const,
        treeNodeFilterProp: 'label' as const,
        onChangeWithObject: false,
        treeNodeLabelProp: 'label' as const,
        dropdownMatchSelectWidth: true,
        defaultOpen: false,
        showSearchClear: true,
        showClear: false,
        onVisibleChange: noop,
        expandAction: false,
        clickToHide: true,
        searchAutoFocus: false,
        checkRelation: 'related',
        'aria-label': 'TreeSelect',
        showRestTagsPopover: false,
        restTagsPopoverProps: {},
        clickTriggerToHide: true,
    };
    inputRef: React.RefObject<typeof Input>;
    tagInputRef: React.RefObject<TagInput>;
    triggerRef: React.RefObject<HTMLDivElement>;
    optionsRef: React.RefObject<any>;
    clickOutsideHandler: any;
    // _flattenNodes: TreeState['flattenNodes'];
    onNodeClick: any;
    onNodeDoubleClick: any;
    onMotionEnd: any;
    treeSelectID: string;
    context: ContextValue;

    constructor(props: TreeSelectProps) {
        super(props);
        this.state = {
            inputTriggerFocus: false,
            isOpen: false,
            isFocus: false,
            // isInput: false,
            rePosKey: key,
            dropdownMinWidth: null,
            inputValue: '',
            keyEntities: {},
            treeData: [],
            flattenNodes: [],
            cachedFlattenNodes: undefined,
            selectedKeys: [],
            checkedKeys: new Set(),
            halfCheckedKeys: new Set(),
            realCheckedKeys: new Set([]),
            disabledKeys: new Set(),
            motionKeys: new Set([]),
            motionType: 'hide',
            expandedKeys: new Set(props.expandedKeys),
            filteredKeys: new Set(),
            filteredExpandedKeys: new Set(),
            filteredShownKeys: new Set(),
            prevProps: null,
            isHovering: false,
            cachedKeyValuePairs: {},
            loadedKeys: new Set(),
            loadingKeys: new Set(),
        };
        this.inputRef = React.createRef();
        this.tagInputRef = React.createRef();
        this.triggerRef = React.createRef();
        this.optionsRef = React.createRef();
        this.clickOutsideHandler = null;
        this.foundation = new TreeSelectFoundation(this.adapter);
        this.treeSelectID = Math.random().toString(36).slice(2);
        this.onMotionEnd = () => {
            this.adapter.rePositionDropdown();
        };
    }

    static getDerivedStateFromProps(props: TreeSelectProps, prevState: TreeSelectState) {
        const { prevProps, rePosKey } = prevState;
        const { keyMaps } = props;
        const needUpdate = (name: string) => (
            (!prevProps && name in props) ||
            (prevProps && !isEqual(prevProps[name], props[name]))
        );

        let treeData;
        const withObject = props.onChangeWithObject;
        let keyEntities = prevState.keyEntities || {};
        let valueEntities = prevState.cachedKeyValuePairs || {};
        const newState: Partial<TreeSelectState> = {
            prevProps: props,
        };
        const needUpdateTreeData = needUpdate('treeData');
        const needUpdateExpandedKeys = needUpdate('expandedKeys');
        const isExpandControlled = 'expandedKeys' in props;
        const isSearching = Boolean(props.filterTreeNode && prevState.inputValue && prevState.inputValue.length);
        // TreeNode
        if (needUpdateTreeData) {
            treeData = props.treeData;
            newState.treeData = treeData;
            const entitiesMap = convertDataToEntities(treeData, keyMaps);
            newState.keyEntities = {
                ...entitiesMap.keyEntities,
            };
            keyEntities = newState.keyEntities;
            newState.cachedKeyValuePairs = { ...entitiesMap.valueEntities };
            valueEntities = newState.cachedKeyValuePairs;
        }

        // if treeData keys changes, we won't show animation
        if (
            treeData &&
            props.motion &&
            !isEqual(Object.keys(newState.keyEntities), Object.keys(prevState.keyEntities))
        ) {
            if (prevProps && props.motion) {
                newState.motionKeys = new Set([]);
                newState.motionType = null;
            }
        }
        const expandAllWhenDataChange = needUpdateTreeData && props.expandAll;
        if (!isSearching) {
            // expandedKeys
            if (needUpdateExpandedKeys || (prevProps && needUpdate('autoExpandParent'))) {
                newState.expandedKeys = calcExpandedKeys(
                    props.expandedKeys,
                    keyEntities,
                    props.autoExpandParent || !prevProps
                );
                // only show animation when treeData does not change
                if (prevProps && props.motion && !treeData) {
                    const { motionKeys, motionType } = calcMotionKeys(
                        prevState.expandedKeys,
                        newState.expandedKeys,
                        keyEntities
                    );
                    newState.motionKeys = new Set(motionKeys);
                    newState.motionType = motionType;
                    if (motionType === 'hide') {
                        // cache flatten nodes: expandedKeys changed may not be triggered by interaction
                        newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
                    }
                }
            } else if ((!prevProps && (props.defaultExpandAll || props.expandAll)) || expandAllWhenDataChange) {
                newState.expandedKeys = new Set(Object.keys(keyEntities));
            } else if (!prevProps && props.defaultExpandedKeys) {
                newState.expandedKeys = calcExpandedKeys(props.defaultExpandedKeys, keyEntities);
            } else if (!prevProps && props.defaultValue) {
                newState.expandedKeys = calcExpandedKeysForValues(
                    normalizeValue(props.defaultValue, withObject, keyMaps),
                    keyEntities,
                    props.multiple,
                    valueEntities
                );
            } else if (!prevProps && props.value) {
                newState.expandedKeys = calcExpandedKeysForValues(
                    normalizeValue(props.value, withObject, keyMaps),
                    keyEntities,
                    props.multiple,
                    valueEntities
                );
            } else if ((!isExpandControlled && needUpdateTreeData) && props.value) {
                // 当 treeData 已经设置具体的值，并且设置了 props.loadData ，则认为 treeData 的更新是因为 loadData 导致的
                // 如果是因为 loadData 导致 treeData改变， 此时在这里重新计算 key 会导致为未选中的展开项目被收起
                // 所以此时不需要重新计算 expandedKeys，因为在点击展开按钮时候已经把被展开的项添加到 expandedKeys 中
                // When treeData has a specific value and props.loadData is set, it is considered that the update of treeData is caused by loadData
                // If the treeData is changed because of loadData, recalculating the key here will cause the unselected expanded items to be collapsed
                // So there is no need to recalculate expandedKeys at this time, because the expanded item has been added to expandedKeys when the expand button is clicked
                if (!(prevState.treeData && prevState.treeData?.length > 0 && props.loadData)) {
                    newState.expandedKeys = calcExpandedKeysForValues(
                        props.value,
                        keyEntities,
                        props.multiple,
                        valueEntities
                    );
                }
            }

            if (!newState.expandedKeys) {
                delete newState.expandedKeys;
            }

            if (treeData || newState.expandedKeys) {
                const flattenNodes = flattenTreeData(
                    treeData || prevState.treeData,
                    newState.expandedKeys || prevState.expandedKeys,
                    keyMaps
                );
                newState.flattenNodes = flattenNodes;
            }
        } else {
            let filteredState;
            // treeData changed while searching
            if (treeData) {
                // Get filter data
                filteredState = filterTreeData({
                    treeData,
                    inputValue: prevState.inputValue,
                    filterTreeNode: props.filterTreeNode,
                    filterProps: props.treeNodeFilterProp,
                    showFilteredOnly: props.showFilteredOnly,
                    keyEntities: newState.keyEntities,
                    prevExpandedKeys: [...prevState.filteredExpandedKeys],
                    keyMaps: keyMaps
                });
                newState.flattenNodes = filteredState.flattenNodes;
                newState.motionKeys = new Set([]);
                newState.filteredKeys = filteredState.filteredKeys;
                newState.filteredShownKeys = filteredState.filteredShownKeys;
                newState.filteredExpandedKeys = filteredState.filteredExpandedKeys;
            }

            // expandedKeys changed while searching
            if (props.expandedKeys) {
                newState.filteredExpandedKeys = calcExpandedKeys(
                    props.expandedKeys,
                    keyEntities,
                    props.autoExpandParent || !prevProps
                );

                if (prevProps && props.motion) {
                    const prevKeys = prevState ? prevState.filteredExpandedKeys : new Set([]);
                    // only show animation when treeData does not change
                    if (!treeData) {
                        const motionResult = calcMotionKeys(
                            prevKeys,
                            newState.filteredExpandedKeys,
                            keyEntities
                        );

                        let { motionKeys } = motionResult;
                        const { motionType } = motionResult;
                        if (props.showFilteredOnly) {
                            motionKeys = motionKeys.filter(key => prevState.filteredShownKeys.has(key));
                        }
                        if (motionType === 'hide') {
                            // cache flatten nodes: expandedKeys changed may not be triggered by interaction
                            newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
                        }
                        newState.motionKeys = new Set(motionKeys);
                        newState.motionType = motionType;
                    }
                }
                newState.flattenNodes = flattenTreeData(
                    treeData || prevState.treeData,
                    newState.filteredExpandedKeys || prevState.filteredExpandedKeys,
                    keyMaps,
                    props.showFilteredOnly && prevState.filteredShownKeys
                );
            }
        }

        // selectedKeys: single mode controlled
        const isMultiple = props.multiple;
        if (!isMultiple) {
            if (needUpdate('value')) {
                newState.selectedKeys = findKeysForValues(
                    normalizeValue(props.value, withObject, keyMaps),
                    valueEntities,
                    isMultiple
                );
            } else if (!prevProps && props.defaultValue) {
                newState.selectedKeys = findKeysForValues(
                    normalizeValue(props.defaultValue, withObject, keyMaps),
                    valueEntities,
                    isMultiple
                );
            } else if (treeData) {
                // If `treeData` changed, we also need check it
                if (props.value) {
                    newState.selectedKeys = findKeysForValues(
                        normalizeValue(props.value, withObject, keyMaps) || '',
                        valueEntities,
                        isMultiple
                    );
                } else {
                    newState.selectedKeys = updateKeys(prevState.selectedKeys, keyEntities);
                }
            }
        } else {
            // checkedKeys: multiple mode controlled || data changed
            let checkedKeyValues;

            if (needUpdate('value')) {
                checkedKeyValues = findKeysForValues(
                    normalizeValue(props.value, withObject, keyMaps),
                    valueEntities,
                    isMultiple
                );
            } else if (!prevProps && props.defaultValue) {
                checkedKeyValues = findKeysForValues(
                    normalizeValue(props.defaultValue, withObject, keyMaps),
                    valueEntities,
                    isMultiple
                );
            } else if (treeData) {
                // If `treeData` changed, we also need check it
                if (props.value) {
                    checkedKeyValues = findKeysForValues(
                        normalizeValue(props.value, withObject, keyMaps) || [],
                        valueEntities,
                        isMultiple
                    );
                } else {
                    checkedKeyValues = updateKeys(props.checkRelation === 'related' ? prevState.checkedKeys : prevState.realCheckedKeys, keyEntities);
                }
            }

            if (checkedKeyValues) {
                if (props.checkRelation === 'unRelated') {
                    newState.realCheckedKeys = new Set(checkedKeyValues);
                } else if (props.checkRelation === 'related') {
                    const { checkedKeys, halfCheckedKeys } = calcCheckedKeys(checkedKeyValues, keyEntities);

                    newState.checkedKeys = checkedKeys;
                    newState.halfCheckedKeys = halfCheckedKeys;
                }
            }
        }

        // loadedKeys
        if (needUpdate('loadedKeys')) {
            newState.loadedKeys = new Set(props.loadedKeys);
        }

        // ================== rePosKey ==================
        if (needUpdateTreeData || needUpdate('value')) {
            newState.rePosKey = rePosKey + 1;
        }

        // ================ disableStrictly =================
        if (treeData && props.disableStrictly && props.checkRelation === 'related') {
            newState.disabledKeys = calcDisabledKeys(keyEntities, keyMaps);
        }

        return newState;
    }

    get adapter(): TreeSelectAdapter<TreeSelectProps, TreeSelectState> {
        const filterAdapter: Pick<TreeSelectAdapter, 'updateInputValue'> = {
            updateInputValue: value => {
                this.setState({ inputValue: value });
            }
        };
        const treeSelectAdapter: Pick<TreeSelectAdapter,
        'registerClickOutsideHandler'
        | 'unregisterClickOutsideHandler'
        | 'rePositionDropdown'
        > = {
            registerClickOutsideHandler: cb => {
                const clickOutsideHandler = (e: Event) => {
                    const optionInstance = this.optionsRef && this.optionsRef.current as React.ReactInstance;
                    const triggerDom = this.triggerRef && this.triggerRef.current;
                    const optionsDom = ReactDOM.findDOMNode(optionInstance);
                    const target = e.target as Element;
                    if (
                        optionsDom &&
                        (
                            !optionsDom.contains(target) ||
                            !optionsDom.contains(target.parentNode)
                        ) &&
                        triggerDom &&
                        !triggerDom.contains(target)
                    ) {
                        cb(e);
                    }
                };
                this.clickOutsideHandler = clickOutsideHandler;
                document.addEventListener('mousedown', clickOutsideHandler, false);
            },
            unregisterClickOutsideHandler: () => {
                document.removeEventListener('mousedown', this.clickOutsideHandler, false);
                this.clickOutsideHandler = null;
            },
            rePositionDropdown: () => {
                let { rePosKey } = this.state;
                rePosKey = rePosKey + 1;
                this.setState({ rePosKey });
            },
        };
        const treeAdapter: Pick<TreeSelectAdapter,
        'updateState'
        | 'notifySelect'
        | 'notifySearch'
        | 'cacheFlattenNodes'
        | 'notifyLoad'
        | 'notifyClear'
        > = {
            updateState: states => {
                this.setState({ ...states } as TreeSelectState);
            },
            notifySelect: ((selectKey, bool, node) => {
                this.props.onSelect && this.props.onSelect(selectKey, bool, node);
            }),
            notifySearch: (input, filteredExpandedKeys) => {
                this.props.onSearch && this.props.onSearch(input, filteredExpandedKeys);
            },
            cacheFlattenNodes: bool => {
                this.setState({ cachedFlattenNodes: bool ? cloneDeep(this.state.flattenNodes) : undefined });
            },
            notifyLoad: (newLoadedKeys, data) => {
                const { onLoad } = this.props;
                isFunction(onLoad) && onLoad(newLoadedKeys, data);
            },
            notifyClear: (e: React.MouseEvent | React.KeyboardEvent<HTMLDivElement>) => {
                this.props.onClear && this.props.onClear(e);
            }
        };
        return {
            ...super.adapter,
            ...filterAdapter,
            ...treeSelectAdapter,
            ...treeAdapter,
            updateLoadKeys: (data, resolve) => {
                this.setState(({ loadedKeys, loadingKeys }) =>
                    this.foundation.handleNodeLoad(loadedKeys, loadingKeys, data, resolve));
            },
            updateState: states => {
                this.setState({ ...states } as TreeSelectState);
            },
            openMenu: () => {
                this.setState({ isOpen: true }, () => {
                    this.props.onVisibleChange(true);
                });
            },
            closeMenu: cb => {
                this.setState({ isOpen: false }, () => {
                    cb && cb();
                    this.props.onVisibleChange(false);
                });
            },
            getTriggerWidth: () => {
                const el = this.triggerRef.current;
                return el && el.getBoundingClientRect().width;
            },
            setOptionWrapperWidth: width => {
                this.setState({ dropdownMinWidth: width });
            },
            notifyChange: (value, node, e) => {
                this.props.onChange && this.props.onChange(value, node, e);
            },
            notifyChangeWithObject: (node, e) => {
                this.props.onChange && this.props.onChange(node, e);
            },
            notifyExpand: (expandedKeys, { expanded: bool, node }) => {
                this.props.onExpand && this.props.onExpand([...expandedKeys], { expanded: bool, node });
                if (bool && this.props.loadData) {
                    this.onNodeLoad(node);
                }
            },
            notifyFocus: (...v) => {
                this.props.onFocus && this.props.onFocus(...v);
            },
            notifyBlur: (...v) => {
                this.props.onBlur && this.props.onBlur(...v);
            },
            toggleHovering: bool => {
                this.setState({ isHovering: bool });
            },
            updateInputFocus: bool => {
                if (bool) {
                    if (this.inputRef && this.inputRef.current) {
                        const { preventScroll } = this.props;
                        (this.inputRef.current as any).focus({ preventScroll });
                    }
                    if (this.tagInputRef && this.tagInputRef.current) {
                        this.tagInputRef.current.focus();
                    }
                } else {
                    if (this.inputRef && this.inputRef.current) {
                        (this.inputRef.current as any).blur();
                    }
                    if (this.tagInputRef && this.tagInputRef.current) {
                        this.tagInputRef.current.blur();
                    }
                }
            },
            updateIsFocus: bool => {
                this.setState({ isFocus: bool });
            }
        };
    }

    componentDidMount() {
        this.foundation.init();
    }

    componentWillUnmount() {
        this.foundation.destroy();
    }

    renderSuffix = () => {
        const { suffix }: any = this.props;
        const suffixWrapperCls = cls({
            [`${prefixcls}-suffix`]: true,
            [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
            [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
        });
        return (
            <div className={suffixWrapperCls} x-semi-prop="suffix">
                {suffix}
            </div>
        );
    };

    renderPrefix = () => {
        const { prefix, insetLabel, insetLabelId }: any = this.props;
        const labelNode = prefix || insetLabel;
        const prefixWrapperCls = cls({
            [`${prefixcls}-prefix`]: true,
            // to be doublechecked
            [`${prefixcls}-inset-label`]: insetLabel,
            [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
            [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
        });

        return (
            <div className={prefixWrapperCls} id={insetLabelId} x-semi-prop="prefix,insetLabel">
                {labelNode}
            </div>
        );
    };

    renderContent = () => {
        const { dropdownMinWidth } = this.state;
        const { dropdownStyle, dropdownClassName } = this.props;
        const style = { minWidth: dropdownMinWidth, ...dropdownStyle };
        const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
        return (
            <div className={popoverCls} style={style}>
                {this.renderTree()}
            </div>
        );
    };

    removeTag = (removedKey: TreeNodeData['key']) => {
        this.foundation.removeTag(removedKey);
    };

    handleClick = (e: React.MouseEvent) => {
        this.foundation.handleClick(e);
    };

    getDataForKeyNotInKeyEntities = (key: string) => {
        return this.foundation.getDataForKeyNotInKeyEntities(key);
    }

    /* istanbul ignore next */
    handleSelectionEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
        this.foundation.handleSelectionEnterPress(e);
    };

    hasValue = (): boolean => {
        const { multiple, checkRelation } = this.props;
        const { realCheckedKeys, checkedKeys, selectedKeys } = this.state;
        let hasValue = false;
        if (multiple) {
            if (checkRelation === 'related') {
                hasValue = Boolean(checkedKeys.size);
            } else if (checkRelation === 'unRelated') {
                hasValue = Boolean(realCheckedKeys.size);
            }
        } else {
            hasValue = Boolean(selectedKeys.length);
        }
        return hasValue;
    }

    showClearBtn = () => {
        const { showClear, disabled, searchPosition } = this.props;
        const { inputValue, isOpen, isHovering } = this.state;
        const triggerSearchHasInputValue = searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue;

        return showClear && (this.hasValue() || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering);
    };

    renderTagList = () => {
        const { checkedKeys, keyEntities, disabledKeys, realCheckedKeys } = this.state;
        const {
            treeNodeLabelProp,
            leafOnly,
            disabled,
            disableStrictly,
            size,
            checkRelation,
            renderSelectedItem: propRenderSelectedItem,
            keyMaps
        } = this.props;
        const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
        const renderSelectedItem = isFunction(propRenderSelectedItem) ?
            propRenderSelectedItem :
            (item: TreeNodeData) => ({
                isRenderInTag: true,
                content: get(item, realLabelName, null)
            });
        let renderKeys = [];
        if (checkRelation === 'related') {
            renderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true);
        } else if (checkRelation === 'unRelated' && Object.keys(keyEntities).length > 0) {
            renderKeys = [...realCheckedKeys];
        }
        const tagList: Array<React.ReactNode> = [];
        renderKeys.forEach((key: TreeNodeData['key'], index) => {
            const item = (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
            const onClose = (tagContent: any, e: React.MouseEvent) => {
                if (e && typeof e.preventDefault === 'function') {
                    // make sure that tag will not hidden immediately in controlled mode
                    e.preventDefault();
                }
                this.removeTag(key);
            };
            const { content, isRenderInTag } = item ?
                (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index, onClose }) :
                ({} as any);
            if (isNull(content) || isUndefined(content)) {
                return;
            }
            const isDisabled = disabled || item.disabled || (disableStrictly && disabledKeys.has(item.key));
            const tag: Partial<TagProps> & React.Attributes = {
                closable: !isDisabled,
                color: 'white',
                visible: true,
                onClose,
                key: `tag-${key}-${index}`,
                size: size === 'small' ? 'small' : 'large'
            };
            if (isRenderInTag) {
                // pass ReactNode list to tagList when using tagGroup custom mode
                tagList.push(<Tag {...tag}>{content}</Tag>);
            } else {
                tagList.push(content);
            }
        });
        return tagList;
    };

    /**
     * When single selection and the search box is on trigger, the items displayed in the rendered search box
     */
    renderSingleTriggerSearchItem = () => {
        const { placeholder, disabled } = this.props;
        const { inputTriggerFocus } = this.state;
        const renderText = this.foundation.getRenderTextInSingle();
        const spanCls = cls(`${prefixcls}-selection-TriggerSearchItem`, {
            [`${prefixcls}-selection-TriggerSearchItem-placeholder`]: (inputTriggerFocus || !renderText) && !disabled,
            [`${prefixcls}-selection-TriggerSearchItem-disabled`]: disabled,
        });
        return (
            <span className={spanCls}>
                {renderText ? renderText : placeholder}
            </span>
        );
    };

    /**
     * Single selection and the search box content rendered when the search box is on trigger
     */
    renderSingleTriggerSearch = () => {
        const { inputValue } = this.state;
        return (
            <>
                {!inputValue && this.renderSingleTriggerSearchItem()}
                {this.renderInput()}
            </>
        );
    };

    renderSelectContent = () => {
        const {
            multiple,
            placeholder,
            maxTagCount,
            searchPosition,
            filterTreeNode,
            showRestTagsPopover,
            restTagsPopoverProps
        } = this.props;
        const isTriggerPositionSearch = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER;
        // searchPosition = trigger
        if (isTriggerPositionSearch) {
            return multiple ? this.renderTagInput() : this.renderSingleTriggerSearch();
        }
        // searchPosition = dropdown and single seleciton
        if (!multiple || !this.hasValue()) {
            const renderText = this.foundation.getRenderTextInSingle();
            const spanCls = cls(`${prefixcls}-selection-content`, {
                [`${prefixcls}-selection-placeholder`]: !renderText,
            });
            return <span className={spanCls}>{renderText ? renderText : placeholder}</span>;
        }
        // searchPosition = dropdown and multiple seleciton
        const tagList = this.renderTagList();
        // mode=custom to return tagList directly
        return (
            <TagGroup<'custom'>
                maxTagCount={maxTagCount}
                tagList={tagList}
                size="large"
                mode="custom"
                showPopover={showRestTagsPopover}
                popoverProps={restTagsPopoverProps}
            />
        );
    };

    handleClear = (e: React.MouseEvent) => {
        e && e.stopPropagation();
        this.foundation.handleClear(e);
    };

    /* istanbul ignore next */
    handleClearEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
        e && e.stopPropagation();
        this.foundation.handleClearEnterPress(e);
    };

    handleMouseOver = (e: React.MouseEvent) => {
        this.foundation.toggleHoverState(true);
    };

    handleMouseLeave = (e: React.MouseEvent) => {
        this.foundation.toggleHoverState(false);
    };

    search = (value: string) => {
        const { isOpen } = this.state;
        if (!isOpen) {
            this.foundation.open();
        }
        this.foundation.handleInputChange(value);
    };

    close = () => {
        this.foundation.close(null);
    };

    renderArrow = () => {
        const showClearBtn = this.showClearBtn();
        const { arrowIcon } = this.props;
        if (showClearBtn) {
            return null;
        }
        return arrowIcon ? (
            <div className={cls(`${prefixcls}-arrow`)} x-semi-prop="arrowIcon">
                {arrowIcon}
            </div>
        ) : null;
    };

    renderClearBtn = () => {
        const showClearBtn = this.showClearBtn();
        const { clearIcon } = this.props;
        const clearCls = cls(`${prefixcls}-clearbtn`);
        if (showClearBtn) {
            return (
                <div
                    role='button'
                    tabIndex={0}
                    aria-label="Clear TreeSelect value"
                    className={clearCls}
                    onClick={this.handleClear}
                    onKeyPress={this.handleClearEnterPress}
                >
                    {clearIcon ? clearIcon : <IconClear />}
                </div>
            );
        }
        return null;
    };

    renderSelection = () => {
        const {
            disabled,
            multiple,
            filterTreeNode,
            validateStatus,
            prefix,
            suffix,
            style,
            size,
            insetLabel,
            className,
            placeholder,
            showClear,
            leafOnly,
            searchPosition,
            triggerRender,
            borderless,
            ...rest
        } = this.props;
        const { inputValue, selectedKeys, checkedKeys, keyEntities, isFocus } = this.state;
        const filterable = Boolean(filterTreeNode);
        const useCustomTrigger = typeof triggerRender === 'function';
        const mouseEvent = showClear ?
            {
                onMouseEnter: (e: React.MouseEvent) => this.handleMouseOver(e),
                onMouseLeave: (e: React.MouseEvent) => this.handleMouseLeave(e),
            } :
            {};
        const isTriggerPositionSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterable;
        const isEmptyTriggerSearch = isTriggerPositionSearch && isEmpty(checkedKeys);
        const isValueTriggerSearch = isTriggerPositionSearch && !isEmpty(checkedKeys);
        const classNames = useCustomTrigger ?
            cls(className) :
            cls(
                prefixcls,
                {
                    [`${prefixcls}-borderless`]: borderless,
                    [`${prefixcls}-focus`]: isFocus,
                    [`${prefixcls}-disabled`]: disabled,
                    [`${prefixcls}-single`]: !multiple,
                    [`${prefixcls}-multiple`]: multiple,
                    [`${prefixcls}-multiple-tagInput-empty`]: multiple && isEmptyTriggerSearch,
                    [`${prefixcls}-multiple-tagInput-notEmpty`]: multiple && isValueTriggerSearch,
                    [`${prefixcls}-filterable`]: filterable,
                    [`${prefixcls}-error`]: validateStatus === 'error',
                    [`${prefixcls}-warning`]: validateStatus === 'warning',
                    [`${prefixcls}-small`]: size === 'small',
                    [`${prefixcls}-large`]: size === 'large',
                    [`${prefixcls}-with-prefix`]: prefix || insetLabel,
                    [`${prefixcls}-with-suffix`]: suffix,
                    [`${prefixcls}-with-suffix`]: suffix,
                },
                className
            );
        const triggerRenderKeys = multiple ? normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true) : selectedKeys;
        const inner = useCustomTrigger ? (
            <Trigger
                inputValue={inputValue}
                value={triggerRenderKeys.map((key: string) => get(keyEntities, [key, 'data']))}
                disabled={disabled}
                placeholder={placeholder}
                onClear={this.handleClear}
                componentName={'TreeSelect'}
                triggerRender={triggerRender}
                componentProps={{ ...this.props }}
                onSearch={this.search}
                onRemove={this.removeTag}
            />
        ) : (
            [
                <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
                <Fragment key={'selection'}>
                    <div className={`${prefixcls}-selection`}>{this.renderSelectContent()}</div>
                </Fragment>,
                <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,
                <Fragment key={'clearBtn'}>
                    {
                        (showClear || (isTriggerPositionSearch && inputValue)) ?
                            this.renderClearBtn() :
                            null
                    }
                </Fragment>,
                <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
            ]
        );
        const tabIndex = disabled ? null : 0;
        /**
         * Reasons for disabling the a11y eslint rule:
         * The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
         */
        return (
            <div
                // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
                role='combobox'
                aria-disabled={disabled}
                aria-haspopup="tree"
                tabIndex={tabIndex}
                className={classNames}
                style={style}
                ref={this.triggerRef}
                onClick={this.handleClick}
                onKeyPress={this.handleSelectionEnterPress}
                aria-invalid={this.props['aria-invalid']}
                aria-errormessage={this.props['aria-errormessage']}
                aria-label={this.props['aria-label']}
                aria-labelledby={this.props['aria-labelledby']}
                aria-describedby={this.props['aria-describedby']}
                aria-required={this.props['aria-required']}
                {...mouseEvent}
                {...this.getDataAttr(rest)}
            >
                {inner}
            </div>
        );
    };

    renderTagItem = (key: string, idx: number) => {
        const { keyEntities, disabledKeys } = this.state;
        const {
            size,
            leafOnly,
            disabled,
            disableStrictly,
            renderSelectedItem: propRenderSelectedItem,
            treeNodeLabelProp,
            keyMaps
        } = this.props;
        const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
        const keyList = normalizeKeyList([key], keyEntities, leafOnly, true);
        const nodes = keyList.map(i => (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
        const value = getValueOrKey(nodes, keyMaps);
        const tagCls = cls(`${prefixcls}-selection-tag`, {
            [`${prefixcls}-selection-tag-disabled`]: disabled,
        });
        const nodeHaveData = !isEmpty(nodes) && !isEmpty(nodes[0]);
        const isDisableStrictlyNode = disableStrictly && nodeHaveData && disabledKeys.has(nodes[0].key);
        const closable = nodeHaveData && !nodes[0].disabled && !disabled && !isDisableStrictlyNode;
        const onClose = (tagChildren: React.ReactNode, e: React.MouseEvent) => {
            // When value has not changed, prevent clicking tag closeBtn to close tag
            e.preventDefault();
            this.removeTag(key);
        };
        const tagProps: Partial<TagProps> & React.Attributes = {
            size: size === 'small' ? 'small' : 'large',
            key: `tag-${value}-${idx}`,
            color: 'white',
            className: tagCls,
            closable,
            onClose,
        };
        const item = nodes[0];
        const renderSelectedItem = isFunction(propRenderSelectedItem) ? propRenderSelectedItem :
            (selectedItem: TreeNodeData) => ({
                isRenderInTag: true,
                content: get(selectedItem, realLabelName, null)
            });
        if (isFunction(renderSelectedItem)) {
            const { content, isRenderInTag } = item ?
                (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: idx, onClose }) :
                ({} as any);
            if (isRenderInTag) {
                return <Tag {...tagProps}>{content}</Tag>;
            } else {
                return content;
            }
        }
        return (
            <Tag {...tagProps}>
                {value}
            </Tag>
        );
    };

    renderTagInput = () => {
        const {
            leafOnly,
            disabled,
            size,
            searchAutoFocus,
            placeholder,
            maxTagCount,
            checkRelation,
            showRestTagsPopover,
            restTagsPopoverProps,
            searchPosition,
            filterTreeNode,
            preventScroll
        } = this.props;
        const {
            keyEntities,
            checkedKeys,
            inputValue,
            realCheckedKeys,
        } = this.state;
        let keyList = [];
        if (checkRelation === 'related') {
            keyList = normalizeKeyList(checkedKeys, keyEntities, leafOnly, true);
        } else if (checkRelation === 'unRelated') {
            keyList = [...realCheckedKeys];
        }
        // auto focus search input divide into two parts
        // 1. filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER
        //    Implemented by passing autofocus to the underlying input's autofocus
        // 2. filterTreeNode && searchPosition === strings.SEARCH_POSITION_DROPDOWN
        //    Due to the off-screen rendering in the tooltip implementation mechanism, if it is implemented through the 
        //    autofocus of the input, when the option panel is opened, the page will scroll to top, so it is necessary 
        //    to call the focus method through ref in the onVisibleChange callback of the Popover to achieve focus
        const autoFocus = (filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER) ? searchAutoFocus : undefined;
        return (
            <TagInput
                maxTagCount={maxTagCount}
                disabled={disabled}
                onInputChange={v => this.search(v)}
                ref={this.tagInputRef}
                placeholder={placeholder}
                value={keyList}
                inputValue={inputValue}
                size={size}
                showRestTagsPopover={showRestTagsPopover}
                restTagsPopoverProps={restTagsPopoverProps}
                // eslint-disable-next-line jsx-a11y/no-autofocus
                autoFocus={autoFocus}
                renderTagItem={(itemKey, index) => this.renderTagItem(itemKey, index)}
                onRemove={itemKey => this.removeTag(itemKey)}
                expandRestTagsOnClick={false}
                preventScroll={preventScroll}
            />
        );
    };

    // render Tree
    renderInput = () => {
        const {
            searchPlaceholder,
            searchRender,
            showSearchClear,
            searchPosition,
            searchAutoFocus,
            multiple,
            disabled,
            preventScroll,
        } = this.props;
        const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
        const inputcls = cls({
            [`${prefixTree}-input`]: isDropdownPositionSearch,
            [`${prefixcls}-inputTrigger`]: !isDropdownPositionSearch
        });
        const { inputValue } = this.state;
        const baseInputProps = {
            value: inputValue,
            className: inputcls,
            preventScroll,
            onChange: (value: string) => this.search(value),
        };
        const inputDropdownProps = {
            showClear: showSearchClear,
            prefix: <IconSearch />,
        };
        const inputTriggerProps = {
            autofocus: searchAutoFocus,
            onFocus: (e: React.FocusEvent) => this.foundation.handleInputTriggerFocus(),
            onBlur: (e: React.FocusEvent) => this.foundation.handleInputTriggerBlur(),
            disabled,
        };
        const realInputProps = isDropdownPositionSearch ? inputDropdownProps : inputTriggerProps;
        const wrapperCls = cls({
            [`${prefixTree}-search-wrapper`]: isDropdownPositionSearch,
            [`${prefixcls}-triggerSingleSearch-wrapper`]: !isDropdownPositionSearch && !multiple,
        });
        const useCusSearch = typeof searchRender === 'function' || typeof searchRender === 'boolean';
        if (useCusSearch && !searchRender) {
            return null;
        }
        return (
            <div className={wrapperCls}>
                <LocaleConsumer componentName="TreeSelect">
                    {(locale: Locale['TreeSelect']) => {
                        const placeholder = isDropdownPositionSearch ?
                            searchPlaceholder || locale.searchPlaceholder :
                            '';
                        if (useCusSearch) {
                            return (searchRender as any)({ ...realInputProps, ...baseInputProps, placeholder });
                        }
                        return (
                            <Input
                                aria-label='Filter TreeSelect item'
                                ref={this.inputRef as any}
                                placeholder={placeholder}
                                {...baseInputProps}
                                {...realInputProps}
                            />
                        );
                    }}
                </LocaleConsumer>
            </div>
        );
    };

    renderEmpty = () => {
        const { emptyContent } = this.props;
        if (emptyContent) {
            return <TreeNode empty emptyContent={this.props.emptyContent} />;
        } else {
            return (
                <LocaleConsumer componentName="Tree">
                    {(locale: Locale['Tree']) => <TreeNode empty emptyContent={locale.emptyText} />}
                </LocaleConsumer>
            );
        }
    };

    onNodeLoad = (data: TreeNodeData) => new Promise(resolve => this.foundation.setLoadKeys(data, resolve));

    onNodeSelect = (e: React.MouseEvent | React.KeyboardEvent, treeNode: TreeNodeProps) => {
        this.foundation.handleNodeSelect(e, treeNode);
    };

    onNodeCheck = (e: React.MouseEvent | React.KeyboardEvent, treeNode: TreeNodeProps) => {
        this.foundation.handleNodeSelect(e, treeNode);
    };

    onNodeExpand = (e: React.MouseEvent | React.KeyboardEvent, treeNode: TreeNodeProps) => {
        this.foundation.handleNodeExpand(e, treeNode);
    };

    getTreeNodeRequiredProps = () => {
        const { expandedKeys, selectedKeys, checkedKeys, halfCheckedKeys, keyEntities, filteredKeys } = this.state;
        return {
            expandedKeys: expandedKeys || new Set(),
            selectedKeys: selectedKeys || [],
            checkedKeys: checkedKeys || new Set(),
            halfCheckedKeys: halfCheckedKeys || new Set(),
            filteredKeys: filteredKeys || new Set(),
            keyEntities,
        };
    };

    getTreeNodeKey = (treeNode: TreeNodeData) => {
        const { data } = treeNode;
        const { key }: { key: string } = data;
        return key;
    };

    /* Event handler function after popover is closed */
    handlePopoverClose = isVisible => {
        const { filterTreeNode, searchAutoFocus, searchPosition } = this.props;
        // 将 inputValue 清空，如果有选中值的话，选中项能够快速回显
        // Clear the inputValue. If there is a selected value, the selected item can be quickly echoed.
        if (isVisible === false && filterTreeNode) {
            this.foundation.clearInputValue();
        }
        if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_DROPDOWN && isVisible && searchAutoFocus) {
            this.foundation.focusInput(true);
        }
    }

    afterClose = () => {
        // flattenNode 的变化将导致弹出层面板中的选项数目变化
        // 在弹层完全收起之后，再通过 clearInput 重新计算 state 中的 expandedKey， flattenNode
        // 防止在弹出层未收起时弹层面板中选项数目变化导致视觉上出现弹层闪动问题
        // Changes to flattenNode will cause the number of options in the popup panel to change
        // After the pop-up layer is completely closed, recalculate the expandedKey and flattenNode in the state through clearInput.
        // Prevent the pop-up layer from flickering visually due to changes in the number of options in the pop-up panel when the pop-up layer is not collapsed.
        const { filterTreeNode } = this.props;
        filterTreeNode && this.foundation.clearInput();
    }

    renderTreeNode = (treeNode: FlattenNode, ind: number, style: React.CSSProperties) => {
        const { data, key } = treeNode;
        const treeNodeProps = this.foundation.getTreeNodeProps(key);
        const { showLine } = this.props;
        if (!treeNodeProps) {
            return null;
        }
        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
        const { keyMaps } = this.props;
        const children = data[get(keyMaps, 'children', 'children')];
        !isUndefined(children) && (props.children = children);
        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={style} showLine={showLine}/>;
    };

    itemKey = (index: number, data: Record<string, any>) => {
        const { visibleOptions } = data;
        // Find the item at the specified index.
        const item = visibleOptions[index];
        // Return a value that uniquely identifies this item.
        return item.key;
    };

    renderNodeList = () => {
        const { flattenNodes, cachedFlattenNodes, motionKeys, motionType, filteredKeys } = this.state;
        const { direction } = this.context;
        const { virtualize, motionExpand } = this.props;
        const isExpandControlled = 'expandedKeys' in this.props;
        if (!virtualize || isEmpty(virtualize)) {
            return (
                <NodeList
                    flattenNodes={flattenNodes}
                    flattenList={cachedFlattenNodes}
                    motionKeys={motionExpand ? motionKeys : new Set([])}
                    motionType={motionType}
                    // When motionKeys is empty, but filteredKeys is not empty (that is, the search hits), this situation should be distinguished from ordinary motionKeys
                    searchTargetIsDeep={
                        isExpandControlled &&
                        motionExpand &&
                        isEmpty(motionKeys) &&
                        !isEmpty(filteredKeys)
                    }
                    onMotionEnd={this.onMotionEnd}
                    renderTreeNode={this.renderTreeNode}
                />
            );
        }

        const data = {
            visibleOptions: flattenNodes,
            renderOption: this.renderTreeNode
        };

        return (
            <AutoSizer defaultHeight={virtualize.height} defaultWidth={virtualize.width}>
                {({ height, width }) => (
                    <VirtualList
                        itemCount={flattenNodes.length}
                        itemSize={virtualize.itemSize}
                        height={height}
                        width={width}
                        // @ts-ignore avoid strict check of itemKey
                        itemKey={this.itemKey}
                        itemData={data}
                        className={`${prefixTree}-virtual-list`}
                        style={{ direction }}
                    >
                        {VirtualRow}
                    </VirtualList>
                )}
            </AutoSizer>
        );
    };

    renderTree = () => {
        const { keyEntities, motionKeys, motionType, inputValue, filteredKeys, flattenNodes, checkedKeys, realCheckedKeys } = this.state;
        const {
            loadData,
            filterTreeNode,
            disabled,
            multiple,
            showFilteredOnly,
            motionExpand,
            outerBottomSlot,
            outerTopSlot,
            expandAction,
            labelEllipsis,
            virtualize,
            optionListStyle,
            searchPosition,
            renderLabel,
            renderFullLabel,
            checkRelation,
        } = this.props;
        const wrapperCls = cls(`${prefixTree}-wrapper`);
        const listCls = cls(`${prefixTree}-option-list`, {
            [`${prefixTree}-option-list-block`]: true,
        });
        const searchNoRes = Boolean(inputValue) && !filteredKeys.size;
        const noData = isEmpty(flattenNodes) || (showFilteredOnly && searchNoRes);
        const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
        return (
            <TreeContext.Provider
                value={{
                    loadData,
                    treeDisabled: disabled,
                    motion: motionExpand,
                    motionKeys,
                    motionType,
                    expandAction,
                    filterTreeNode,
                    keyEntities,
                    onNodeClick: this.onNodeClick,
                    onNodeDoubleClick: this.onNodeDoubleClick,
                    // tree node will call this function when treeNode is right clicked
                    onNodeRightClick: noop,
                    onNodeExpand: this.onNodeExpand,
                    onNodeSelect: this.onNodeSelect,
                    onNodeCheck: this.onNodeCheck,
                    renderTreeNode: this.renderTreeNode,
                    multiple,
                    showFilteredOnly,
                    isSearching: Boolean(inputValue),
                    renderLabel,
                    renderFullLabel,
                    labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis,
                }}
            >
                <div className={wrapperCls}>
                    {outerTopSlot}
                    {
                        !outerTopSlot &&
                        filterTreeNode &&
                        isDropdownPositionSearch &&
                        this.renderInput()
                    }
                    <div className={listCls} role="tree" aria-multiselectable={multiple ? true : false} style={optionListStyle}>
                        {noData ? this.renderEmpty() : (multiple ?
                            (<CheckboxGroup value={Array.from(checkRelation === 'related' ? checkedKeys : realCheckedKeys)}>
                                {this.renderNodeList()}
                            </CheckboxGroup>) :
                            this.renderNodeList()
                        )}
                    </div>
                    {outerBottomSlot}
                </div>
            </TreeContext.Provider>
        );
    };

    render() {
        const content = this.renderContent();
        const {
            motion,
            zIndex,
            mouseLeaveDelay,
            mouseEnterDelay,
            autoAdjustOverflow,
            stopPropagation,
            getPopupContainer,
            dropdownMargin,
            position,
        } = this.props;
        const { isOpen, rePosKey } = this.state;
        const selection = this.renderSelection();
        const pos = position ? position : 'bottomLeft';
        return (
            <Popover
                stopPropagation={stopPropagation}
                getPopupContainer={getPopupContainer}
                zIndex={zIndex}
                motion={motion}
                margin={dropdownMargin}
                ref={this.optionsRef}
                content={content}
                visible={isOpen}
                trigger="custom"
                rePosKey={rePosKey}
                position={pos}
                autoAdjustOverflow={autoAdjustOverflow}
                mouseLeaveDelay={mouseLeaveDelay}
                mouseEnterDelay={mouseEnterDelay}
                onVisibleChange={this.handlePopoverClose}
                afterClose={this.afterClose}
            >
                {selection}
            </Popover>
        );
    }
}

export default TreeSelect;
