'use strict';

import {findDOMNode} from 'react-dom';
import Store from '../../data/Store';
import {FormGroup} from './FormGroup';
import {FormControl} from './FormControl';
import {Checkbox} from './Checkbox';
import {Filter} from '../Filter';
import {Icon} from '../Icon';
import {Tooltip} from '../Tooltip';
import React from 'react';
import PropTypes from 'prop-types';
import Dropdown from 'react-bootstrap/lib/Dropdown';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import _ from 'lodash';
import classnames from 'classnames';
import {highlight} from '../list/utils';
import {translate} from '../../../utils';
import {FocusHelper} from '../../utils/FocusHelper';
import {KeyNames} from '../../../utils/constants';

const FC = FormControl;
const doNothing = () => {};

class MultiSelectField extends FormGroup {
    static propTypes = {
        ...FormGroup.propTypes
    };

    static defaultProps = {
        ...FormGroup.defaultProps,
        testAutomationId: 'multiSelect'
    };

    renderInput() {
        let {children, innerLabel, ...props} = this.getPropsFromContext();
        return (
            <MultiSelect {...props} label={innerLabel} ref="control" hasFormGroupContainer>
                {children}
            </MultiSelect>
        );
    }
}

let multiSelectInstanceCounter = 0;

class MultiSelectOption extends React.Component {
    static contextTypes = {
        multiSelect: PropTypes.object,
        multiSelectGroup: PropTypes.object,
        getOptionIdPrefix: PropTypes.func,
        setAriaActivedescendant: PropTypes.func
    };

    static childContextTypes = {
        getOptionIdPrefix: PropTypes.func
    };

    static propTypes = {
        className: PropTypes.string,
        disabled: PropTypes.bool,
        groupMaster: PropTypes.bool,
        icon: PropTypes.string,
        label: PropTypes.string.isRequired,
        labelClassName: PropTypes.string,
        onKeyDown: PropTypes.func,
        onSelect: PropTypes.func,
        testAutomationId: PropTypes.string,
        value: PropTypes.any.isRequired,
        ariaPosition: PropTypes.number,
        ariaLevel: PropTypes.number,
        ariaSetSize: PropTypes.number,
        hint: PropTypes.string,
        customLabel: PropTypes.any,
        tagRenderer: PropTypes.func,
        description: PropTypes.string
    };

    static defaultProps = {
        className: null,
        disabled: false,
        groupMaster: false,
        icon: '',
        labelClassName: '',
        onKeyDown: doNothing,
        onSelect: doNothing,
        testAutomationId: null,
        ariaPosition: null,
        ariaLevel: null,
        ariaSetSize: null,
        hint: null,
        customLabel: null,
        tagRenderer: null,
        description: null
    };

    static compareOptionValue(source) {
        if (_.isObject(source)) {
            return _.matches(source);
        }
        return function(value) {
            return value === source;
        };
    }

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

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

    getChildContext() {
        return {
            getOptionIdPrefix: () => this.getId()
        };
    }

    handleKeyDown(event) {
        switch (event.keyCode) {
            case 13: // enter
            case 32: // space
                this.refs.control.click();
                event.preventDefault();
                break;
            default:
                this.props.onKeyDown(event);
        }
    }

    getId() {
        if (!_.isFunction(this.context.getOptionIdPrefix)) {
            return null;
        }

        return `${this.context.getOptionIdPrefix()}_${
            this.props.groupMaster ? 'masterOption' : 'option'
        }_${this.props.ariaPosition}`;
    }

    finalRender(props, content) {
        if (_.isFunction(this.props.tagRenderer)) {
            return this.props.tagRenderer(props, content);
        }
        return content;
    }

    render() {
        const {hint, description} = this.props;

        let values = this.context.multiSelect.values;
        let filterCallback = this.context.multiSelect.filterCallback;

        let isSelected = false;
        let isMaster = this.props.groupMaster;
        let isOptionDisabled =
            !!(
                isMaster &&
                this.context.multiSelect.isSingleSelectMode &&
                this.props.value === undefined
            ) || this.props.disabled;

        let optionValues = [];

        if (!isMaster) {
            optionValues = [this.props.value];

            let index = _.findIndex(values, MultiSelectOption.compareOptionValue(this.props.value));
            isSelected = index > -1;

            // Skip not matched options
            if (!filterCallback(this.props.label, this.props.value)) {
                return null;
            }
        } else {
            optionValues = this.context.multiSelectGroup.values;

            let selectedValuesCount = 0;
            let groupFilteredLabels = this.context.multiSelectGroup.filteredLabels;

            optionValues.forEach(value => {
                let index = _.findIndex(values, MultiSelectOption.compareOptionValue(value));
                if (index > -1) {
                    selectedValuesCount++;
                }
            });

            if (selectedValuesCount > 0) {
                isSelected = selectedValuesCount === optionValues.length ? true : null;
            }

            // Skip not matched groups
            if (
                !filterCallback(this.props.label, this.props.value) &&
                !groupFilteredLabels.length
            ) {
                return null;
            }
        }

        const highlightedLabelText =
            this.props.customLabel ||
            highlight(this.context.multiSelect.filterRegExp, this.props.label);
        let icon = this.props.icon;
        let content;
        let labelTitle = !description ? this.props.label : `${this.props.label} - ${description}`;

        const label = (
            <span title={hint ? null : labelTitle}>
                {highlightedLabelText}
                {description ? <span className="select-description"> - {description}</span> : null}
            </span>
        );

        if (this.context.multiSelect.isSingleSelectMode) {
            isSelected =
                this.props.value !== undefined &&
                MultiSelectOption.compareOptionValue(this.props.value)(values[0]);

            let onClick = event => {
                if (this.props.value !== undefined) {
                    this.context.multiSelect.setValues([this.props.value]);
                }
                this.context.multiSelect.close();
            };

            let labelProps = {
                className: classnames('option-label', this.props.labelClassName, {
                    'selected-option': isSelected
                }),
                ref: 'control'
            };
            if (!isOptionDisabled) {
                labelProps.onClick = onClick;
            }

            content = (
                <span {...labelProps}>
                    {isSelected ? (
                        <Icon aria-hidden="true" name="checkbox-selected-readonly" />
                    ) : null}
                    {label}
                    <div className="icon-wrapper" aria-hidden="true">
                        {icon ? <Icon name={icon} /> : null}
                    </div>
                </span>
            );
        } else {
            content = (
                <Checkbox
                    ref="control"
                    label={label}
                    labelClassName={this.props.labelClassName}
                    checked={isSelected}
                    aria-hidden={true}
                    aria-checked={isSelected === null ? 'mixed' : isSelected}
                    disabled={isOptionDisabled}
                    onChange={value => {
                        let isSelected = this.refs.control.getValue();
                        let newValues = values.filter(
                            value =>
                                _.findIndex(
                                    optionValues,
                                    MultiSelectOption.compareOptionValue(value)
                                ) === -1
                        );

                        if (isSelected) {
                            newValues = values.concat(optionValues);
                        }

                        this.context.multiSelect.setValues(newValues);
                    }}
                />
            );
        }

        const optionId = this.getId();

        let liProps = {
            id: optionId,
            'data-test-automation-id':
                this.props.testAutomationId || 'option' + this.context.multiSelect.uid(),
            className: classnames(
                `multi-select-option multi-select-option-level-${this.props.ariaLevel}`,
                this.props.className,
                {
                    'master-option': isMaster,
                    'option-disabled': isOptionDisabled
                }
            ),
            role: 'treeitem',
            'aria-level': this.props.ariaLevel,
            'aria-setsize': this.props.ariaSetSize,
            'aria-posinset': this.props.ariaPosition
        };

        if (!isOptionDisabled) {
            liProps.tabIndex = '-1';
            liProps.onKeyDown = this.handleKeyDown;
            liProps.onSelect = this.props.onSelect;
            liProps['aria-selected'] = isSelected === true;
            liProps.onFocus = () => {
                if (_.isFunction(this.context.setAriaActivedescendant)) {
                    this.context.setAriaActivedescendant(optionId);
                }
            };
        }

        const finalContent = hint ? (
            <OverlayTrigger overlay={<Tooltip>{hint}</Tooltip>}>{content}</OverlayTrigger>
        ) : (
            this.finalRender(liProps, content)
        );

        return <li {...liProps}>{finalContent}</li>;
    }
}

class MultiSelectMasterOptionWrapper extends React.Component {
    static propTypes = {
        ...MultiSelectOption.propTypes,
        testAutomationId: PropTypes.string,
        groupLabelId: PropTypes.string.isRequired
    };

    static defaultProps = {
        ...MultiSelectOption.defaultProps,
        testAutomationId: null
    };

    static contextTypes = {
        multiSelect: PropTypes.object
    };

    getTestAutomationId() {
        const {testAutomationId} = this.props;
        const {multiSelect} = this.context;

        if (testAutomationId) {
            return testAutomationId;
        }

        return `optionMaster${multiSelect.uid()}`;
    }

    tagRenderer = (props, content) => {
        return <div id={this.props.groupLabelId}>{content}</div>;
    };

    render() {
        return (
            <MultiSelectOption
                {...this.props}
                testAutomationId={this.getTestAutomationId()}
                tagRenderer={this.tagRenderer}
                groupMaster
            />
        );
    }
}

class MultiSelectMasterOption extends React.Component {
    static propTypes = {
        ...MultiSelectOption.propTypes
    };

    render() {
        return null;
    }
}

function getChildrenValues(children) {
    return _.flatten(
        React.Children.toArray(children)
            .filter(child => {
                return (
                    (child.type === MultiSelectOption && !child.props.groupMaster) ||
                    child.type === MultiSelectOptionGroup
                );
            })
            .map(child => {
                if (child.type === MultiSelectOption) {
                    return child.props.value;
                } else {
                    return getChildrenValues(child.props.children);
                }
            })
    );
}

function hasValueChild(children) {
    const items = React.Children.toArray(children);
    const hasValue = items.some(o => o && o.props && o.props.value !== undefined);

    if (hasValue) {
        return true;
    }

    for (let child of items) {
        if (child && child.props && child.props.children && hasValueChild(child.props.children)) {
            return true;
        }
    }

    return false;
}

function getChildrenLabels(children, filterCallback) {
    return _.flatten(
        React.Children.toArray(children)
            .filter(child => {
                if (child.type === MultiSelectOption || child.type === MultiSelectMasterOption) {
                    if (filterCallback && !filterCallback(child.props.label, child.props.value)) {
                        return false;
                    }

                    return true;
                } else if (child.type === MultiSelectOptionGroup) {
                    return true;
                }
            })
            .map(child => {
                if (child.type === MultiSelectOption || child.type === MultiSelectMasterOption) {
                    return child.props.label;
                } else {
                    return getChildrenLabels(child.props.children, filterCallback);
                }
            })
    );
}

function getGroupValueOptionSize(children) {
    return _.filter(React.Children.toArray(children), o => !isMasterOption(o)).map(o => o.key)
        .length;
}

function isMasterOption(node) {
    return node && node.type === MultiSelectMasterOption;
}

const TreeItemAriaProps = ['aria-selected', 'aria-level', 'aria-setsize', 'aria-posinset', 'role'];

class MultiSelectOptionGroup extends React.Component {
    static contextTypes = {
        multiSelect: PropTypes.object,
        multiSelectGroup: PropTypes.object,
        getOptionIdPrefix: PropTypes.func
    };

    static childContextTypes = {
        multiSelectGroup: PropTypes.object,
        getOptionIdPrefix: PropTypes.func
    };

    static propTypes = {
        children: PropTypes.node,
        className: PropTypes.string,
        onKeyDown: PropTypes.func,
        onSelect: PropTypes.func,
        testAutomationId: PropTypes.string,
        ariaPosition: PropTypes.number,
        ariaLevel: PropTypes.number,
        ariaSetSize: PropTypes.number
    };

    static defaultProps = {
        children: null,
        className: null,
        onKeyDown: doNothing,
        onSelect: doNothing,
        testAutomationId: null,
        ariaPosition: 1,
        ariaLevel: 1,
        ariaSetSize: 1
    };

    getChildContext() {
        return {
            multiSelectGroup: {
                values: getChildrenValues(this.props.children),
                filteredLabels: getChildrenLabels(
                    this.props.children,
                    this.context.multiSelect.filterCallback
                )
            },
            getOptionIdPrefix: this.getOptionId
        };
    }

    getOptionId = () => {
        if (_.isFunction(this.context.getOptionIdPrefix)) {
            return `${this.context.getOptionIdPrefix()}_${this.props.ariaPosition}`;
        }
        return null;
    };

    getSiblingOptionSize() {
        return getGroupValueOptionSize(this.props.children);
    }

    getMasterOptionWrapperProps = props => {
        return _.omit(props, TreeItemAriaProps);
    };

    getGroupProps(props) {
        if (_.isEmpty(props)) {
            return null;
        }

        return _.pick(props, TreeItemAriaProps);
    }

    render() {
        const children = React.Children.toArray(this.props.children);

        let hasMaster = false;

        const items = children.map((child, index) => {
            let childProps = child.props || {};
            const isMasterNode = isMasterOption(child);
            if (isMasterNode) {
                hasMaster = true;
            }

            const ariaPosition = hasMaster ? index : index + 1;

            return React.cloneElement(
                child,
                {
                    ariaPosition: isMasterNode ? this.props.ariaPosition : ariaPosition,
                    ariaLevel: this.props.ariaLevel + (isMasterNode ? 0 : 1),
                    ariaSetSize: isMasterNode
                        ? this.props.ariaSetSize
                        : this.getSiblingOptionSize(),
                    onKeyDown: this.props.onKeyDown,
                    onSelect: this.props.onSelect
                },
                childProps.children
            );
        });

        const masterItem = _.find(items, isMasterOption);

        const groupLabelId = `${this.getOptionId()}_group_node`;

        const masterOptionNode = masterItem ? (
            <MultiSelectMasterOptionWrapper
                groupLabelId={groupLabelId}
                {...this.getMasterOptionWrapperProps(masterItem.props)}
            />
        ) : null;

        const valueItems = items.filter(node => !isMasterOption(node));
        const hasAvailableValue = hasValueChild(valueItems);

        const groupProps = {
            id: this.getOptionId(),
            'data-test-automation-id':
                this.props.testAutomationId || 'groupOption' + this.context.multiSelect.uid(),
            role: 'treeitem',
            'aria-expanded': true,
            'aria-selected': false,
            ...this.getGroupProps(masterItem && masterItem.props)
        };

        groupProps.className = classnames(groupProps.className, {
            'multi-select-has-children': !_.isEmpty(valueItems)
        });

        return (
            <div {...groupProps}>
                {masterOptionNode}
                <ul
                    className={classnames('multi-select-group', this.props.className)}
                    role={hasAvailableValue ? 'group' : null}
                    aria-labelledby={hasAvailableValue && masterItem ? groupLabelId : null}>
                    {valueItems}
                </ul>
            </div>
        );
    }
}

const MultiSelectDefaultAutomationId = 'multiSelect';

export class MultiSelect extends FormControl {
    static propTypes = {
        ...FormControl.propTypes,
        id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        isFilterable: PropTypes.bool,
        searchText: PropTypes.string,
        noResultsText: PropTypes.string,
        label: PropTypes.string,
        value: PropTypes.any,
        testAutomationId: PropTypes.string,
        className: PropTypes.string,
        block: PropTypes.bool,
        stretch: PropTypes.bool,
        listHeader: PropTypes.any,
        listItemsLimit: PropTypes.number,
        labelItemsLimit: PropTypes.number,
        onChange: PropTypes.func,
        isSingleSelect: PropTypes.bool,
        showMoreCountTextTemplate: PropTypes.string,
        toggleAriaLabel: PropTypes.string,
        selectedValuesPrefix: PropTypes.string,
        applyLabelPrefixForSingleMode: PropTypes.bool,
        applyLabelPrefixForMultiMode: PropTypes.bool,
        ariaLabelledById: PropTypes.string,
        stretchDropdownMenu: PropTypes.bool
    };

    static childContextTypes = {
        multiSelect: PropTypes.object,
        level: PropTypes.number,
        getOptionIdPrefix: PropTypes.func,
        setAriaActivedescendant: PropTypes.func
    };

    static Option = MultiSelectOption;
    static MasterOption = MultiSelectMasterOption;
    static Group = MultiSelectOptionGroup;
    static MultiSelectField = MultiSelectField;

    // extends defaultProps from superclass
    static defaultProps = {
        ...FC.defaultProps,
        isFilterable: false,
        labelItemsLimit: 3,
        testAutomationId: MultiSelectDefaultAutomationId,
        searchText: 'TRANSLATE:SEARCH',
        noResultsText: 'TRANSLATE:NO_RESULTS',
        disabled: false,
        isSingleSelect: false,
        showMoreCountTextTemplate: 'TRANSLATE:SHOW_MORE_COUNT',

        /** example string format: "{filterName} filter" */
        toggleAriaLabel: 'TRANSLATE:MULTI_SELECT_ARIA_LABEL',

        /**  example string format: "selected {filterName}: {values}" */
        selectedValuesPrefix: 'TRANSLATE:MULTI_SELECT_SELECTED_TITLE_LABEL',
        applyLabelPrefixForSingleMode: false,
        applyLabelPrefixForMultiMode: true,
        stretchDropdownMenu: false
    };

    focusHelper = new FocusHelper();

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

        this.onToggle = this.onToggle.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);
        this.showMore = this.showMore.bind(this);
        this.filterByLimit = this.filterByLimit.bind(this);
        this.filterByLabelAndLimit = this.filterByLabelAndLimit.bind(this);
        this.listItemsFilteredByLimit = [];
        this.listItemsFiltered = [];

        this.state = {
            filterText: '',
            filterRegExp: null,
            filterCallback: this.filterByLimit,
            isOpen: false,
            isMoreShowed: false,
            hasFilterFocus: false,
            ariaActivedescendant: null
        };
        this.instanceId = ++multiSelectInstanceCounter;
    }

    componentDidMount() {
        this.multiSelectEle.addEventListener('keydown', event => {
            if (this.state.isOpen && event.key === KeyNames.Escape) {
                event.stopPropagation();
            }
        });
    }

    saveMultiSelectRef = ref => {
        this.multiSelectEle = findDOMNode(ref);
    };

    filterByLimit(label, value) {
        if (!(this.props.listItemsLimit > 0)) {
            return true;
        }
        if (this.state.isMoreShowed) {
            return true;
        }
        let validValue = typeof value !== 'undefined';

        // Calculate all visible options
        if (validValue && this.listItemsFiltered.indexOf(value) === -1) {
            this.listItemsFiltered.push(value);
        }
        if (this.listItemsFilteredByLimit.length < this.props.listItemsLimit) {
            if (validValue && this.listItemsFilteredByLimit.indexOf(value) === -1) {
                this.listItemsFilteredByLimit.push(value);
            }
            return true;
        }
        return this.listItemsFilteredByLimit.indexOf(value) !== -1;
    }

    filterByLabelAndLimit(label, value) {
        let filterRegExp = this.state.filterRegExp;
        if (filterRegExp === null) {
            return this.filterByLimit(label, value);
        } else {
            filterRegExp.lastIndex = 0;
            return filterRegExp.test(label) && this.filterByLimit(label, value);
        }
    }

    focus = () => {
        this.focusHelper.focus();
    };

    outputConverter(e) {
        return e;
    }

    getValue() {
        //FIXME Not implemented
        return null;
    }

    getChildContext() {
        let props = this.getPropsFromContext();

        return {
            multiSelect: {
                uid: () => {
                    return ++this.uid;
                },

                values: toArray(props.value),
                setValues: values => {
                    if (this.props.isSingleSelect) {
                        values = values[0];
                    }
                    props.onChange(values);
                },
                filterCallback: this.state.filterCallback,
                filterRegExp: this.state.filterRegExp,
                isSingleSelectMode: this.props.isSingleSelect,
                close: () => {
                    this.setState({isOpen: false});
                }
            },
            level: 0,
            getOptionIdPrefix: () => `select_${this.instanceId}`,
            setAriaActivedescendant: ariaActivedescendant => this.setState({ariaActivedescendant})
        };
    }

    onToggle(isOpen) {
        this.uid = 0;

        if (!isOpen && this.state.hasFilterFocus) {
            isOpen = true;
        }

        this.setState({isOpen});
    }

    onKeyDown(event) {
        switch (event.key) {
            case 'Escape':
            case 'Tab':
                this.setState({isOpen: false});
                break;
        }
    }

    setTextFilter(text) {
        let filterRegExp = Store.searchStringToRegExp(text);
        this.listItemsFiltered = [];
        this.listItemsFilteredByLimit = [];

        this.setState({
            filterText: text,
            filterRegExp: filterRegExp,
            filterCallback: this.filterByLabelAndLimit,
            isMoreShowed: false
        });
    }

    showMore(event) {
        event.preventDefault();
        this.setState({
            isMoreShowed: true
        });
    }

    renderShowMore() {
        if (this.state.isMoreShowed) {
            return null;
        }
        let count = this.listItemsFiltered.length - this.listItemsFilteredByLimit.length;
        if (count <= 0) {
            return null;
        }
        return (
            <li role="menuitem">
                <a className="show-more" onClick={this.showMore}>
                    {translate(this.props.showMoreCountTextTemplate, {count})}
                </a>
            </li>
        );
    }

    /** @param {Array<String>} selectedItemLabels */
    getTitle = selectedItemLabels => {
        if (_.isEmpty(selectedItemLabels)) {
            return null;
        }

        const {label, selectedValuesPrefix} = this.props;

        const valueStr = selectedItemLabels.join(', ');

        if (!this.shouldApplyLabelPrefix()) {
            return valueStr;
        }

        return translate(selectedValuesPrefix, {
            filterName: label,
            values: valueStr
        });
    };

    getAriaLabel = () => {
        const {label, toggleAriaLabel} = this.props;

        if (!this.shouldApplyLabelPrefix()) {
            return label;
        }

        return translate(toggleAriaLabel, {filterName: label});
    };

    /** @param {Array<String>} selectedItems */
    getDisplayLabel = selectedItems => {
        const {label, labelItemsLimit} = this.props;

        if (_.isEmpty(selectedItems)) {
            return label;
        }

        let valueString = selectedItems.join(', ');

        if (selectedItems.length > labelItemsLimit) {
            valueString = selectedItems
                .slice(0, labelItemsLimit)
                .concat(['...'])
                .join(', ');
        }

        return this.shouldApplyLabelPrefix() ? `${label}: ${valueString}` : valueString;
    };

    shouldApplyLabelPrefix() {
        const {
            isSingleSelect,
            applyLabelPrefixForSingleMode,
            applyLabelPrefixForMultiMode
        } = this.props;

        return isSingleSelect ? applyLabelPrefixForSingleMode : applyLabelPrefixForMultiMode;
    }

    /** @returns {Array<String>}*/
    getSelectedItemLabels = () => {
        const {children} = this.props;
        let props = this.getPropsFromContext();
        let values = toArray(props.value);

        return getChildrenLabels(children, (label, value) => {
            return _.findIndex(values, MultiSelectOption.compareOptionValue(value)) > -1;
        });
    };

    render() {
        this.uid = 0;

        let {
            id,
            isFilterable,
            searchText,
            noResultsText,
            label,
            value,
            children,
            labelItemsLimit,
            testAutomationId,
            className,
            block,
            stretch,
            listHeader,
            listItemsLimit,
            showMoreCountTextTemplate,
            onChange,
            ariaLabelledById,
            toggleAriaLabel,
            stretchDropdownMenu,
            ...otherProps
        } = this.props;
        let filter = null;
        let isNoResults = false;
        let hasChildren = children && (!(children instanceof Array) || children.length);

        if (isFilterable && hasChildren) {
            filter = (
                <Filter
                    value={this.state.filterText}
                    testAutomationId="filter"
                    className="multi-select-filter"
                    ref="filter"
                    placeholder={searchText}
                    tabIndex="-1"
                    noOffset
                    onFocus={event => {
                        if (!this.state.hasFilterFocus) {
                            this.refs.filter.focus();
                            this.setState({hasFilterFocus: true});
                        }
                    }}
                    onBlur={event => this.setState({hasFilterFocus: false})}
                    onKeyDown={event => {
                        if (['ArrowUp', 'ArrowDown', 'Escape', 'Tab'].indexOf(event.key) === -1) {
                            event.stopPropagation();
                        }
                    }}
                    onChange={filterText => this.setTextFilter(filterText)}
                />
            );

            isNoResults = getChildrenLabels(children, this.state.filterCallback).length === 0;
        }

        let header = null;
        if (listHeader && filter) {
            header = (
                <li className="multi-select-list-header" role="none">
                    {listHeader}
                    {filter}
                </li>
            );
        } else if (listHeader) {
            header = (
                <li className="multi-select-list-header" role="none">
                    {listHeader}
                </li>
            );
        } else if (filter) {
            header = (
                <li className="multi-select-list-header" role="none">
                    {filter}
                </li>
            );
        }

        const selectedItemLabels = this.getSelectedItemLabels();
        const title = this.getTitle(selectedItemLabels);
        const displayLabel = this.getDisplayLabel(selectedItemLabels);

        id =
            id ||
            (testAutomationId !== MultiSelectDefaultAutomationId ? testAutomationId : Date.now());

        const dropdownMenuClassName = stretchDropdownMenu ? 'stretch' : null;

        return (
            <Dropdown
                id={id}
                onToggle={this.onToggle}
                onKeyDown={this.onKeyDown}
                open={this.state.isOpen}
                ref={this.saveMultiSelectRef}
                data-test-automation-id={
                    this.props.hasFormGroupContainer ? undefined : testAutomationId
                }
                className={classnames('multi-select', className, {
                    'btn-block': stretch || block,
                    stretch: stretch
                })}
                {...this.getSafeProps(otherProps)}>
                <Dropdown.Toggle
                    ref={this.focusHelper.setEl}
                    data-test-automation-id="toggle"
                    aria-labelledby={ariaLabelledById}
                    noCaret
                    block
                    disabled={this.props.disabled}
                    aria-label={this.getAriaLabel()}
                    aria-describedby={otherProps['aria-describedby'] || null}
                    title={title}>
                    <span className="icon-wrapper" aria-hidden="true">
                        <Icon name="chevron-down-small" />
                    </span>
                    <div className="label-wrapper">
                        <div className="label-wrapper-inner">
                            <div className="label-text-overflow">{displayLabel}</div>
                        </div>
                    </div>
                </Dropdown.Toggle>
                <Dropdown.Menu className={dropdownMenuClassName} data-test-automation-id="list">
                    {header}
                    {!hasChildren || isNoResults ? (
                        <li className="no-results" data-test-automation-id="noResult" role="none">
                            {noResultsText}
                        </li>
                    ) : (
                        <li className="multi-select-list-content" role="menuitem">
                            <ul
                                role="tree"
                                aria-multiselectable={!this.props.isSingleSelect}
                                aria-activedescendant={this.state.ariaActivedescendant}>
                                {appendAriaPropsForMultiSelectChildren(children)}
                            </ul>
                        </li>
                    )}
                    {hasChildren && !isNoResults ? this.renderShowMore() : null}
                </Dropdown.Menu>
            </Dropdown>
        );
    }
}

function appendAriaPropsForMultiSelectChildren(children) {
    const arr = _.flatten(toArray(children));
    return _.map(arr, (child, index) => {
        if (_.isEmpty(child)) {
            return child;
        }

        const childProps = child.props || {};

        return React.cloneElement(
            child,
            {
                ariaPosition: index + 1,
                ariaLevel: 1,
                ariaSetSize: arr.length
            },
            childProps.children
        );
    });
}

function toArray(value) {
    return _.isArray(value) ? value : [value];
}
