/**
 * The new select element
 *
 * @author Platon Fedorov
 * @date 2021-01-12
 */

import * as React from 'react';
import {IOption, ISelectProps, Value} from './Select.types';
import {
    Button,
    DropDown,
    Icon,
    joinClassNames,
    PLACEMENT,
    POPOVER_BOUNDARIES,
    safeInvoke,
    SIZE,
    Skeleton,
    Tooltip
} from '../..';
import {OptionList} from '..';
import * as styles from './select.m.scss';
import {getSizeThemeKey} from '../../utils/getSizeThemeKey';
import {SizeClassNames} from '../wrapper/Wrapper.types';
import {Wrapper, WrapperProps} from '../wrapper/Wrapper';
import debounce from 'lodash/debounce';
import {Extra} from './component/extra/Extra';

interface IState {
    isDropDownOpen: boolean;
    inputText: string;
    isFocused: boolean;
    isHovered: boolean;
    selectedOptions: IOption[];
    dropDownWidth: number;
}

export {IOption, IGroupOption, Value, ISelectProps, Mode} from './Select.types';

export class Select extends React.PureComponent<ISelectProps, IState> {
    inputRef = React.createRef<HTMLInputElement>();
    dropDownTargetRef = React.createRef<HTMLDivElement>();
    dropDownRef = React.createRef<HTMLDivElement>();
    isFocusOnInputFromOptionList: boolean = false;

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

        let inputText = '';
        let selectedOptions: IOption[] = [];

        const defaultValue = this.props.defaultValue;

        if (defaultValue) {
            if (defaultValue instanceof Array && props.mode === 'default' && defaultValue.length > 1) {
                // Check for bad input. If we give more then one default option to 'mode: default' select
                selectedOptions = this.getOptionsByValue(defaultValue[0]);
            } else {
                selectedOptions = this.getOptionsByValue(defaultValue);
            }

            if (props.mode === 'default' && selectedOptions.length > 0) {
                inputText = selectedOptions[0].title;
            }
        }

        this.state = {
            isDropDownOpen: Boolean(props.isDefaultOpen),
            inputText: inputText,
            isFocused: false, // will be updated in override componentDidMount
            isHovered: Boolean(this.props.autoFocus), // to show extra controls
            selectedOptions: selectedOptions,
            dropDownWidth: 0
        };

        this.calculateDropDownWidth = debounce(this.calculateDropDownWidth.bind(this), 800, {
            leading: true,
            trailing: false
        });
        this.onKeyDown = this.onKeyDown.bind(this);
    }

    static defaultProps = {
        mode: 'default',
        size: SIZE.MIDDLE,
        showSearch: false,
        showArrow: true,
        defaultActiveFirstOption: false,
        notFoundText: 'Not found',
        selectAllText: 'Select all',
        selectNoneText: 'Select none',
        addText: 'Add'
    };

    getOptionsByValue = (value: Value): IOption[] => {
        if (value instanceof Array) {
            return this.props.options.filter((option) => {
                return value.indexOf(option.value) > -1;
            });
        }

        for (let i = 0; i < this.props.options.length; i++) {
            if (this.props.options[i].value === value) {
                return [this.props.options[i]];
            }
        }

        return [];
    }

    getValueList = (options: IOption[]): Array<string | number> => {
        return options.map((option) => {
            return option.value;
        });
    }

    getSelectedOptions = (): IOption[] => {
        if (this.props.value !== undefined) {
            return this.getOptionsByValue(this.props.value);
        }

        return this.state.selectedOptions;
    }

    openDropDown = () => {
        this.setState({
            isDropDownOpen: true
        });

        safeInvoke(this.props.onDropdownVisibleChange, true);
    };

    closeDropDown = () => {
        this.setState({
            isDropDownOpen: false,
            inputText: ''
        });

        safeInvoke(this.props.onDropdownVisibleChange, false);
    };

    addSelectedOption = (option: IOption): IOption[] => {
        const newOptions = this.getSelectedOptions().slice();

        newOptions.push(option);

        this.setState({
            selectedOptions: newOptions
        });

        return newOptions;
    }

    deleteOption = (option: IOption): IOption[] => {
        const newOptions = this.getSelectedOptions().slice();
        const index = newOptions.findIndex((optionItem) => option.value === optionItem.value);

        newOptions.splice(index, 1);

        this.setState({
            selectedOptions: newOptions
        });

        return newOptions;
    }

    deleteLastOption = (): IOption[] => {
        if (this.getSelectedOptions().length === 0) {
            return [];
        }

        const newOptions = this.getSelectedOptions().slice();

        newOptions.splice(newOptions.length - 1, 1);

        this.setState({
            selectedOptions: newOptions
        });

        return newOptions;
    }

    deleteAllOptions = (): IOption[] => {
        this.setState({
            selectedOptions: []
        });

        return [];
    }

    filterOptions = (text: string): IOption[] => {
        let filteredOptions: IOption[] = this.props.options;

        const searchText = text.toLowerCase();

        if (text.length > 0) {
            filteredOptions = this.props.options.filter((option) => {
                return option.title.toLowerCase().indexOf(searchText) > -1 ||
                    option.value.toString().toLowerCase().indexOf(searchText) > -1;
            });
        }

        if (text.length > 0 && this.props.canCreate === true) {
            const valuesList = this.props.options.map((option) => option.value.toString());

            if (!valuesList.includes(this.state.inputText)) {
                filteredOptions.push({
                    value: this.state.inputText,
                    title: `+ ${this.state.inputText}`
                });
            }
        }

        return filteredOptions;
    }

    handleWrapperClick = (e: React.MouseEvent<HTMLDivElement>) => {
        if (!this.state.isDropDownOpen || this.props.isOpen === false) {
            if (this.inputRef.current !== null && e.target !== this.inputRef.current) {
                this.inputRef.current.focus();
            } else if (this.inputRef.current === null) {
                this.setState({
                    isFocused: true
                });
                safeInvoke(this.props.onFocus);
            }

            this.openDropDown();
        } else if (this.state.inputText === '') {
            this.closeDropDown();
        }
    }

    handleWrapperClickOutside = (e: MouseEvent) => {
        let isBlur = true;

        if (this.dropDownRef.current && e.target instanceof Node) {
            isBlur = !this.dropDownRef.current.contains(e.target);
        }

        if (isBlur) {
            this.blur();
        }
    }

    handleInputChange = (e: React.FormEvent<HTMLInputElement>) => {
        this.doInputChange(e.currentTarget.value);
    }

    doInputChange = (value: string) => {
        this.setState({
            inputText: value
        });

        if (!this.state.isDropDownOpen) {
            this.openDropDown();
        }

        safeInvoke(this.props.onSearch, value);
    }

    handleInputKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Backspace' &&
            this.state.inputText === '' &&
            this.props.mode !== 'default' &&
            (this.props.showSearch === true || this.props.canCreate === true) &&
            this.getSelectedOptions().length > 0
        ) {
            this.onChange(this.deleteLastOption());
        }

        if (e.key === 'Enter' &&
            this.state.inputText !== '' &&
            this.props.mode === 'multiple' &&
            this.props.canCreate === true
        ) {
            const newOptionValue = e.currentTarget.value;
            const hasSame = this.props.options.map((option) => option.value).includes(newOptionValue);

            if (!hasSame) {
                this.onSelect({title: newOptionValue, value: newOptionValue});
            }
        }
    }

    handleInputFocus = () => {
        if (!this.isFocusOnInputFromOptionList) {
            this.setState({
                isFocused: true
            });
            safeInvoke(this.props.onFocus);
        }
    }

    handleWrapperFocus = (e: FocusEvent) => {
        this.focus();
        this.openDropDown();
    }

    handleWrapperMouseEnter = () => {
        this.setState({
            isHovered: true
        });
    }

    handleWrapperMouseLeave = () => {
        this.setState({
            isHovered: false
        });
    }

    focus = () => {
        if (this.inputRef.current !== null) {
            this.inputRef.current.focus();
        } else {
            this.setState({
                isFocused: true
            });
            safeInvoke(this.props.onFocus);
        }
    }

    blur = () => {
        this.setState({
            inputText: '',
            isFocused: false
        });
        this.closeDropDown();

        safeInvoke(this.props.onBlur);
    }

    onSelect = (option: IOption | null) => {
        if (option) {
            const selectedOptions = this.getSelectedOptions();

            if (this.props.mode === 'default') {
                const isChanged = (selectedOptions.length > 0) ?
                    (option.value !== selectedOptions[0].value) : true;

                this.setState({
                    selectedOptions: [option]
                });

                safeInvoke(this.props.onSelect, option.value);

                if (isChanged) {
                    this.onChange([option]);
                }

                this.closeDropDown();
            } else {
                let newOptions = selectedOptions;

                if (newOptions.some((optionItem) => optionItem.value === option.value)) {
                    newOptions = this.deleteOption(option);

                    safeInvoke(this.props.onDeselect, option.value);
                } else {
                    newOptions = this.addSelectedOption(option);

                    safeInvoke(this.props.onSelect, option.value);
                }

                this.onChange(newOptions);
            }

            this.setState({
                inputText: ''
            });

            this.isFocusOnInputFromOptionList = true;
            this.inputRef.current?.focus();
            this.isFocusOnInputFromOptionList = false;
        }
    }

    onChange = (newOptions: IOption[]) => {
        const value: Value | undefined = this.props.mode === 'default' ? this.getValueList(newOptions)[0] : this.getValueList(newOptions);

        safeInvoke(
            this.props.onChange,
            value,
            newOptions.slice()
        );
    }

    override componentDidMount (): void {
        this.calculateDropDownWidth();
        window.addEventListener('resize', this.calculateDropDownWidth);
        window.addEventListener('keydown', this.onKeyDown);

        if (this.props.autoFocus) {
            this.focus();

            if (this.props.isDefaultOpen !== false) {
                this.openDropDown();
            }
        }
    }

    override componentWillUnmount (): void {
        window.removeEventListener('resize', this.calculateDropDownWidth);
        window.removeEventListener('keydown', this.onKeyDown);
    }

    calculateDropDownWidth () {
        if (this.dropDownTargetRef.current) {
            const width = this.dropDownTargetRef.current.clientWidth;

            this.setState({
                dropDownWidth: width
            });
        }
    }

    onKeyDown (e: KeyboardEvent) {
        if (this.state.isFocused) {
            if (e.key === 'Tab') {
                this.blur();
            } else if (e.key === 'Enter' && this.state.inputText === '' &&  this.state.isDropDownOpen === false) {
                this.openDropDown();
            }
        }
    }

    onDropDownClose = (e: React.SyntheticEvent<Element> | Event) => {
        if (e instanceof KeyboardEvent) {
            this.setState({
                inputText: '',
                isDropDownOpen: false
            });

            safeInvoke(this.props.onDropdownVisibleChange, false);
        }
    }

    renderSelect () {
        const isOpen = (this.props.isOpen !== undefined) ? this.props.isOpen : this.state.isDropDownOpen;

        const style = {
            width: 'auto'
        };

        if (this.state.dropDownWidth && this.props.dropdownMatchSelectWidth === undefined) {
            style.width = `${this.state.dropDownWidth}px`;
        }

        if (this.state.dropDownWidth && this.props.dropdownMatchSelectWidth === '1.5:1') {
            style.width = `${this.state.dropDownWidth * (3 / 2)}px`;
        }

        const udSelectClassName = joinClassNames(
            'ud-select',
            styles.udSelect
        );

        return (
            <DropDown
                isOpen={isOpen}
                onClose={this.onDropDownClose}
                target={(
                    <div className={udSelectClassName} data-component-id={'Select'}>
                        {this.props.targetRenderer !== undefined ?
                            this.props.targetRenderer(this.handleWrapperClick, this.props.value) : this.renderWrapper()}
                    </div>
                )}
                placement={PLACEMENT.BOTTOM_START}
                hasArrow={false}
                hasPaddings={false}
                boundaries={POPOVER_BOUNDARIES.DOCUMENT}
                isDisabled={this.props.isDisabled || this.props.isReadOnly}
            >
                {isOpen ? (
                    <div className={styles.dropdownContainer} style={style} ref={this.dropDownRef}>
                        {this.renderDropDownContent()}
                    </div>
                ) : null}
            </DropDown>
        );
    }

    onExtraClear = (e: React.MouseEvent) => {
        e.stopPropagation();

        if (!(this.props.isDisabled || this.props.isReadOnly)) {
            if (this.state.inputText.trim()) {
                this.doInputChange('');

                return;
            }

            if (this.getSelectedOptions().length !== 0) {
                this.onChange(this.deleteAllOptions());
            }
        }
    }

    onDeleteSelectedOption = (option: IOption) => (e: React.MouseEvent) => {
        e.stopPropagation();

        if (!(this.props.isDisabled || this.props.isReadOnly)) {
            const newOptions = this.deleteOption(option);

            safeInvoke(this.props.onDeselect, option.value);

            this.onChange(newOptions);
        }
    }

    renderCustomValueRenderer = (value: Value) => {
        return (
            <div className={styles.rendererValueContainer}>
                {this.props.valueRenderer !== undefined ? this.props.valueRenderer(value) : null}
            </div>
        );
    }

    renderWrapper = () => {
        const isLogicDisabled = this.props.isDisabled || this.props.isReadOnly;
        const handleWrapperClick = isLogicDisabled ? undefined : this.handleWrapperClick;
        const handleWrapperClickOutside = isLogicDisabled ? undefined : this.handleWrapperClickOutside;

        const wrapperProps: WrapperProps = {
            size: this.props.size,
            prefix: this.props.prefix,
            suffix: this.props.suffix,
            hasError: this.props.hasError,
            isDisabled: this.props.isDisabled,
            isReadOnly: this.props.isReadOnly,
            errorMessage: this.props.errorMessage,
            onPrefixClick: this.props.onPrefixClick,
            onSuffixClick: this.props.onSuffixClick,
            isFocused: this.state.isFocused,
            forwardRef: this.dropDownTargetRef,
            onClick: handleWrapperClick,
            onClickOutside: handleWrapperClickOutside,
            tabIndex: isLogicDisabled ? -1 : 0,
            onWrapperFocus: this.handleWrapperFocus,
            onMouseEnter: this.handleWrapperMouseEnter,
            onMouseLeave: this.handleWrapperMouseLeave
        };

        const size = this.props.size ? this.props.size : SIZE.MIDDLE;
        const sizeThemeKey = getSizeThemeKey<SizeClassNames>('', size);
        const selectedOptions = this.getSelectedOptions();
        const searchText = this.props.showSearch || this.props.canCreate ? this.state.inputText : '';

        let isInputReadonly = true;

        if (this.props.showSearch === true || this.props.canCreate === true) {
            isInputReadonly = false;
        }

        const containerClassName = joinClassNames(
            styles.selectContainer,
            styles[sizeThemeKey],
            [styles.inputRowsModeScroll, Boolean(this.props.inputRowsMode === 'scroll')],
            [styles.isReadOnly, Boolean(this.props.isReadOnly)],
            [styles.isDisabled, Boolean(this.props.isDisabled)]
        );

        return (
            <Wrapper
                {...wrapperProps}
            >
                <div
                    className={containerClassName}
                >
                    {
                        (this.props.valueRenderer !== undefined && this.props.value) ? (
                            this.renderCustomValueRenderer(this.props.value)
                        ) : (
                            <div className={styles.content}>
                                {this.props.mode !== 'default' ? (
                                    selectedOptions.map((option) => {
                                        return this.renderSelectedMultipleValue(option, size);
                                    })
                                ) : null}
                                <div className={styles.inputValue}>
                                    {this.renderValueDefault()}
                                    <input
                                        size={searchText.length + 1}
                                        ref={this.inputRef}
                                        value={searchText}
                                        onChange={this.handleInputChange}
                                        onFocus={this.handleInputFocus}
                                        onKeyUp={this.handleInputKeyUp}
                                        readOnly={isInputReadonly}
                                        disabled={this.props.isDisabled || this.props.isReadOnly}
                                    />
                                </div>
                            </div>
                        )
                    }
                    <Extra
                        isWaiting={this.props.isWaiting}
                        showArrow={this.props.showArrow}
                        allowClear={this.props.allowClear}
                        clearIcon={this.state.inputText ? 'backspace' : 'cross'}
                        onClear={this.onExtraClear}
                        isInputEmpty={!Boolean(this.state.inputText) && Boolean(!this.props.value)}
                        isHovered={this.state.isHovered}
                        size={size}
                    />
                </div>
            </Wrapper>
        );
    }

    renderValueDefault = () => {
        const mode = this.props.mode;
        const selectedOptions = this.getSelectedOptions();
        const isOpen = (this.props.isOpen !== undefined) ? this.props.isOpen : this.state.isDropDownOpen;

        const isPlaceholder = selectedOptions.length === 0 ||
            mode === 'default' && this.state.isFocused && isOpen;

        const isShowValueDefault = this.state.inputText === '' &&
            (mode === 'default' || selectedOptions.length === 0);

        const valueDefaultClassName = joinClassNames(
            styles.displayValue,
            [styles.placeholder, isPlaceholder]
        );

        let valueDefault = '';

        if (mode === 'default' && selectedOptions.length !== 0) {
            valueDefault = selectedOptions[0].title;
        } else if (this.props.placeholder !== undefined) {
            valueDefault = this.props.placeholder;
        }

        return isShowValueDefault ? (
            <div className={valueDefaultClassName}>
                {valueDefault}
            </div>
        ) : null;
    }

    renderDropDownContent = () => {
        const searchText = this.state.inputText;

        let dropDownContent: React.ReactNode | null;
        let showLastButton = true;

        if (this.props.dropdownRender) {
            dropDownContent = this.props.dropdownRender(this.getSelectedOptions(), this.closeDropDown);
        } else if (this.props.isWaiting) {
            showLastButton = false;
            dropDownContent = <Skeleton isLoading={true}/>;
        } else {
            const options = this.filterOptions(searchText);

            if (this.props.sortOrder !== undefined) {
                options.sort((option1, option2) => {
                    if (this.props.sortOrder === 'ASC') {
                        return option1.title.localeCompare(option2.title);
                    }

                    return option2.title.localeCompare(option1.title);
                });
            }

            if (options.length === 0) {
                showLastButton = Boolean(this.props.canAdd);

                if (this.props.notFoundContent) {
                    return this.props.notFoundContent;
                }

                const style = {
                    width: `${this.state.dropDownWidth}px`
                };

                dropDownContent = (
                    <div
                        style={style}
                        className={styles.noContent}
                    >
                        {this.props.notFoundText}
                    </div>
                );
            } else {
                dropDownContent = (
                    <div>
                        <OptionList
                            options={options}
                            value={this.getSelectedOptions().map((option) => option.value)}
                            onSelect={this.onSelect}
                            maxHeight={this.props.optionsMaxHeight}
                        />
                    </div>
                );
            }
        }

        return (
            <>
                {dropDownContent}
                {showLastButton && (this.props.canSelectAll || this.props.canAdd) && (
                    <>
                        <DropDown.Splitter/>
                        {this.props.canSelectAll && this.renderSelectAllButton()}
                        {this.props.canAdd && this.renderAddButton()}
                    </>
                )}
            </>
        );
    }

    renderSelectAllButton = () => {
        const isAllSelected = this.props.isAllSelected;
        const onSelectAllNoneClick = isAllSelected ? this.props.onDeselectAll : this.props.onSelectAll;
        const selectAllNoneText = isAllSelected ? this.props.selectNoneText : this.props.selectAllText;

        return (
            <DropDown.Item>
                <Button
                    isMinimal={true}
                    onClick={onSelectAllNoneClick}
                    isFilled={true}
                    size={SIZE.SMALL}
                >
                    {selectAllNoneText}
                </Button>
            </DropDown.Item>
        );
    }

    renderAddButton = () => {
        return (
            <DropDown.Item>
                <Button
                    isMinimal={true}
                    onClick={this.props.onAdd}
                    isFilled={true}
                    size={SIZE.SMALL}
                    leftIcon={'plus-circle'}
                >
                    {this.props.addText}
                </Button>
            </DropDown.Item>
        );
    }

    renderSelectedMultipleValue = (option: IOption, size: SIZE) => {
        return (
            <div
                className={styles.option}
                title={option.title}
                key={option.value}
            >
                {
                    this.props.multipleValueRenderer ? (
                        this.props.multipleValueRenderer(option, this.onDeleteSelectedOption)
                    ) : (
                        <>
                            <span>{option.title}</span>
                            {!this.props.isReadOnly && (
                                <Icon
                                    name={'cross'}
                                    size={size}
                                    onClick={this.onDeleteSelectedOption(option)}
                                />
                            )}
                        </>
                    )
                }
            </div>
        );
    }

    override render () {
        const title = this.props.title;
        const tooltipPlacement = this.props.tooltipPlacement || PLACEMENT.TOP;

        if (!title) {
            return this.renderSelect();
        }

        return (
            <Tooltip
                overlay={title}
                placement={tooltipPlacement}
                mouseEnterDelay={300}
            >
                <span>
                    {this.renderSelect()}
                </span>
            </Tooltip>
        );
    }
}
