import React, { PureComponent } from 'react';
import { debounce, merge, get } from 'lodash-es';
import AsyncSelect from 'react-select/async';
import AsyncCreatable from 'react-select/async-creatable';
import noop from '../../utils/noop';
import AsyncLoader from './asyncLoader';
import AsyncMultiRemove from './asyncMultiRemove';
import type { MultiValue, SelectInstance } from 'react-select';

const CLASS_NAME = 'AsyncDropdown';

interface DropdownItem {
    name: string;
}

export interface DropdownOption {
    label: string;
    data: DropdownItem;
    value: string;
}

export interface Props {
    onFetch?: (value: string) => Promise<DropdownItem[]>;
    onCreate?: (value: string) => void;
    onValueClick?: () => void;
    onChange?: (value: DropdownOption[]) => void;
    onInputChange?: (value: string) => void;
    multi?: boolean;
    placeholder?: string;
    noOptionsMessage?: () => string | null | JSX.Element;
    searchWait?: number;
    value: DropdownItem[];
    isCreatable?: boolean;
    formatCreateLabel?: (value: string) => string;
    selectProps?: object;
}

export default class AsyncDropdown extends PureComponent<Props, { inputValue: string }> {
    select: SelectInstance<DropdownOption> | null | undefined;

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

        this.state = {
            inputValue: '',
        };
        const { searchWait = 350 } = props;
        this.handleLoadOptions = debounce(this.handleLoadOptions, searchWait);
    }

    getComponents = () => {
        const { selectProps } = this.props;
        const components = {
            DropdownIndicator: undefined,
            LoadingIndicator: undefined,
            LoadingMessage: AsyncLoader as any,
            MultiValueRemove: AsyncMultiRemove as any,
        };
        const overriddenComponents = merge(
            components,
            get(selectProps, 'components')
        ) as typeof components;
        return {
            components: {
                ...components,
                ...overriddenComponents,
            },
        };
    };

    getFormattedValue = (value: DropdownItem[]): DropdownOption[] => {
        if (!Array.isArray(value)) return [];

        return value.map((data) => ({
            data,
            label: data.name,
            value: data.name,
        }));
    };

    getFormatCreateLabel = (inputValue: string) => {
        const { formatCreateLabel } = this.props;
        if (!formatCreateLabel) return undefined;
        return <div className={`${CLASS_NAME}__option-new`}>{formatCreateLabel(inputValue)}</div>;
    };

    getCreatableProps = () => {
        const { isCreatable = true } = this.props;
        if (!isCreatable) return null;

        return {
            formatCreateLabel: this.getFormatCreateLabel,
            onCreateOption: this.handleOnCreateOption,
        };
    };

    setRef = (ref: SelectInstance<DropdownOption> | null) => {
        this.select = ref;
    };

    handleOnCreateOption = (inputValue: string) => {
        const { onCreate } = this.props;
        onCreate?.(inputValue);
    };

    handleOnChange = (newValue: DropdownOption | MultiValue<DropdownOption> | null) => {
        const { onChange } = this.props;

        if (Array.isArray(newValue)) onChange?.(newValue as DropdownOption[]);
        else if (newValue) onChange?.([newValue as DropdownOption]);
        else onChange?.([]);
    };

    handleLoadOptions = (value: string, callback: (options: DropdownOption[]) => void) => {
        const { onFetch } = this.props;
        onFetch?.(value)
            .then((results) => {
                const mappedResults = this.getFormattedValue(results);
                callback(mappedResults);
            })
            .catch(noop);
    };

    handleOnFocus = () => {
        const { inputValue } = this.state;
        if (inputValue && this.select) {
            this.select.onMenuOpen();
            this.select.handleInputChange({
                currentTarget: { value: inputValue },
            } as React.FormEvent<HTMLInputElement>);
        }
    };

    handleOnInputChange = (inputValue: string, { action }: { action: string }) => {
        const { onInputChange } = this.props;
        if (action !== 'input-blur' && action !== 'menu-close') {
            this.setState({ inputValue });
            if (onInputChange) onInputChange(inputValue);
        }
    };

    render() {
        const {
            placeholder = '',
            multi = true,
            isCreatable = true,
            value,
            selectProps,
            noOptionsMessage = () => null,
        } = this.props;
        const { inputValue } = this.state;

        const props = {
            ref: this.setRef as any, // Using any here because of intermittent ts errors.
            className: CLASS_NAME,
            classNamePrefix: CLASS_NAME,
            cacheOptions: false,
            isMulti: multi,
            isClearable: !multi,
            placeholder,
            noOptionsMessage,
            openMenuOnClick: false,
            loadOptions: this.handleLoadOptions,
            onChange: this.handleOnChange,
            value: this.getFormattedValue(value),
            onFocus: this.handleOnFocus,
            inputValue,
            onInputChange: this.handleOnInputChange,
            ...this.getCreatableProps(),
            ...selectProps,
            ...this.getComponents(),
        };

        if (isCreatable) return <AsyncCreatable {...props} menuPlacement="auto" />;
        return <AsyncSelect {...props} menuPlacement="auto" />;
    }
}
