/**
 * Options list with arrow pagination
 *
 * @author: Denis Makarov
 * @date: 2020-10-07
 */

import * as React from 'react';
import * as styles from './optionList.m.scss';
import {IOption, Value} from '..';
import {joinClassNames} from '../..';
import {OptionItem} from './OptionItem';

interface IProps {
    options: IOption [];
    onSelect: (option: IOption | null) => void;
    value?: Value;
    showUnsetOption?: boolean;
    forwardRef?: React.RefObject<HTMLDivElement>;
    maxHeight?: number;
    unsetText?: string;
    optionRenderer?: (option?: IOption) => React.ReactNode;
}

interface IState {
    cursorItemIndex: number | undefined;
    isNavigationDisabled: boolean;
}

export class OptionList extends React.PureComponent<IProps, IState> {
    override state: IState = {
        cursorItemIndex: undefined,
        isNavigationDisabled: true
    };

    containerElement = React.createRef<HTMLDivElement>();
    navigatedElement = React.createRef<HTMLDivElement>();

    createRef () {
        if (this.props.forwardRef) {
            this.containerElement = this.props.forwardRef;
        }

        return this.containerElement;
    }

    handleKeyDown = (e: KeyboardEvent) => {
        if (this.state.isNavigationDisabled) {
            return;
        }

        if (e.key === 'ArrowUp') {
            this.setState((prevState) => {
                return {
                    cursorItemIndex: this.getPrevAvailableOption(prevState.cursorItemIndex)
                };
            });
        }

        if (e.key === 'ArrowDown') {
            this.setState((prevState) => {
                return {
                    cursorItemIndex: this.getNextAvailableOption(prevState.cursorItemIndex)
                };
            });
        }

        const cursorItemIndex = this.state.cursorItemIndex;

        if (e.key === 'Enter' && cursorItemIndex !== undefined) {
            e.preventDefault();

            let el: IOption | null;

            if (this.props.showUnsetOption) {
                if (cursorItemIndex === 0) {
                    el = null;
                } else {
                    el = this.props.options[cursorItemIndex - 1];
                }

            } else {
                el = this.props.options[cursorItemIndex];
            }

            this.props.onSelect(el || null);
        }
    };

    getNextAvailableOption = (currentIndex: number = -1): number => {
        const showUnsetOption = this.props.showUnsetOption || false;
        const optionsLength = showUnsetOption ? this.props.options.length : this.props.options.length - 1;
        const nextIndex = currentIndex < optionsLength ? currentIndex + 1 : 0;
        const nextOption = showUnsetOption && nextIndex > 0 ? this.props.options[nextIndex - 1] : this.props.options[nextIndex];

        if (nextOption?.isDisabled === true) {
            return this.getNextAvailableOption(nextIndex);
        }

        return nextIndex;
    };

    getPrevAvailableOption = (currentIndex: number = -1): number => {
        const showUnsetOption = this.props.showUnsetOption || false;
        const optionsLength = showUnsetOption ? this.props.options.length : this.props.options.length - 1;
        const nextIndex = currentIndex > 0 ? currentIndex - 1 : optionsLength;
        const nextOption = showUnsetOption ? this.props.options[nextIndex - 1] : this.props.options[nextIndex];

        if (nextOption?.isDisabled === true) {
            return this.getPrevAvailableOption(nextIndex);
        }

        return nextIndex;

    };

    handleUnset = () => {
        this.props.onSelect(null);
    };

    isNavigationDisabled = (): boolean => {
        return this.props.options.every((option) => option.isDisabled);
    }

    override componentDidUpdate (prevProps: Readonly<IProps>, prevState: Readonly<IState>, snapshot?: any) {
        if (prevProps.options !== this.props.options) {
            this.setState({cursorItemIndex: undefined, isNavigationDisabled: this.isNavigationDisabled()});
        } else {
            if (prevState.cursorItemIndex !== this.state.cursorItemIndex && this.state.cursorItemIndex !== undefined) {
                if (this.navigatedElement.current && this.containerElement.current) {
                    const element = this.navigatedElement.current;
                    const container = this.containerElement.current;
                    const elementTop = element.offsetTop;
                    const elementBottom = elementTop + element.clientHeight;
                    const containerTop = container.scrollTop;

                    let containerBottom = containerTop;

                    if (this.props.maxHeight === undefined) {
                        containerBottom += container.clientHeight;
                    } else {
                        containerBottom += Math.min(container.clientHeight, this.props.maxHeight);
                    }

                    if (elementTop > containerBottom || elementBottom < containerTop) {
                        element.scrollIntoView({block: 'center'});
                    } else if (elementTop < containerTop) {
                        element.scrollIntoView({block: 'start'});
                    } else if (elementBottom > containerBottom) {
                        element.scrollIntoView({block: 'end'});
                    }
                }
            }
        }
    }

    override componentDidMount () {
        const value = this.props.value;

        const isOnlyOneValueSelected = value !== undefined &&
            (Array.isArray(value) && value.length === 1 || !Array.isArray(value));

        if (isOnlyOneValueSelected) {
            const selectedValue = Array.isArray(value) ? value[0] : value;
            const index = this.props.options.findIndex((option) => option.value === selectedValue);

            if (index > -1) {
                this.setState({cursorItemIndex: index});
            }
        }

        this.setState({isNavigationDisabled: this.isNavigationDisabled()});
        document.addEventListener('keydown', this.handleKeyDown);
    }

    override componentWillUnmount () {
        document.removeEventListener('keydown', this.handleKeyDown);
    }

    handleClick = (option: IOption | null) => (e: React.SyntheticEvent<HTMLDivElement>) => {
        this.props.onSelect(option);
    };

    renderOption = (option: IOption) => {
        if (this.props.optionRenderer !== undefined) {
            return this.props.optionRenderer(option);
        }

        return <OptionItem option={option}/>;
    }

    override render () {
        let cursorOptionIndex: number | undefined = undefined;

        if (this.state.cursorItemIndex !== undefined) {
            cursorOptionIndex = this.props.showUnsetOption ? this.state.cursorItemIndex - 1 : this.state.cursorItemIndex;
        }

        const style = this.props.maxHeight ? {maxHeight: `${this.props.maxHeight}px`} : undefined;
        const value = this.props.value;

        return (
            <div
                style={style}
                className={styles.optionListContainer}
                ref={this.createRef()}
                data-component-id={'OptionList'}
            >
                {this.props.showUnsetOption && (
                    <div
                        key={'_unset'}
                        tabIndex={0}
                        className={joinClassNames(
                            styles.optionContainer,
                            styles.valueUnset,
                            [styles.hasCursor, this.state.cursorItemIndex === 0]
                        )}
                        onClick={this.handleUnset}
                    >
                        --{this.props.unsetText || ''}--
                    </div>
                )}
                {this.props.options.map((option, index) => {
                    const highlighted = value && (value instanceof Array) ?
                        value.includes(option.value) : value === option.value;

                    const hasCursor = index === cursorOptionIndex;
                    const ref = hasCursor ? this.navigatedElement : undefined;
                    const onClick = option.isDisabled ? undefined : this.handleClick(option);

                    const classNames = joinClassNames(
                        styles.optionContainer,
                        [styles.highlighted, highlighted],
                        [styles.hasCursor, hasCursor],
                        [styles.isDisabled, option.isDisabled === true]
                    );

                    return (
                        <div
                            key={option.value}
                            tabIndex={0}
                            className={classNames}
                            onClick={onClick}
                            ref={ref}
                        >
                            {this.renderOption(option)}
                        </div>
                    );
                })}
            </div>
        );
    }
}
