import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Autocomplete as MuiAutocomplete, NoOptionsText, classes } from './AutocompleteStyles';
import Option from './Option';
import LinearProgress from '@mui/material/LinearProgress';
import Chip from '@mui/material/Chip';
import { is, isNil, equals, isEmpty, any, prop } from 'ramda';
import { filterOptions, getSimpleValue, findFirstNotNil, isStringOrNumber, computeChangedMultiValue, computeChangedSingleValue } from './utils';
import TextField from '../TextField';
/**
 *
 * The autocomplete is a normal text input enhanced by a panel of suggested options.
 *
 * The widget is useful for setting the value of a single-line textbox in one of two types of scenarios:
 *
 * The value for the textbox must be chosen from a predefined set of allowed values, e.g., a location field must contain a valid location name.
 * The textbox may contain any arbitrary value, but it is advantageous to suggest possible values to the user, e.g., a search field may suggest similar or previous searches to save the user time.
 * It's meant to be an improved version of the "react-select" and "downshift" packages.
 */
const Autocomplete = ({ options: receivedOptions, defaultOptions = [], loadOptions, loading: receivedLoading, loadingText, noOptionsText = 'No options', getOptionLabel, onChange, onInputChange, creatable = false, onOpen, onClose, value = null, isMultiSelection = false, withCheckboxes = false, isClearable = false, disabled = false, simpleValue = false, label, valueKey = 'id', labelKey = 'name', error = false, helperText, required = false, createdLabel = 'Add', typographyContentColor = 'textSecondary', inputSelectedColor, isSearchable = true, getOptionDisabled, placeholder, inputTextFieldProps, isPaginated, ...other }) => {
    const [options, setOptions] = useState(receivedOptions ?? []);
    const [asyncOptions, setAsyncOptions] = useState(is(Array, defaultOptions) ? defaultOptions : []);
    const [additionalPageData, setAdditionalPageData] = useState(null);
    const [hasMore, setHasMore] = useState(true);
    const [localLoading, setLocalLoading] = useState(false);
    const loading = receivedLoading || localLoading;
    const [localInput, setLocalInput] = useState();
    const [optionsLoaded, setOptionsLoaded] = useState(false);
    const disabledOptions = useMemo(() => (getOptionDisabled ? options.filter(getOptionDisabled) : []), [getOptionDisabled, options]);
    const disabledValues = disabledOptions.map(prop(valueKey));
    const isValueDisabled = getOptionDisabled ? any(equals(value), disabledValues) : false;
    const handleLoadOptionsPaginated = useCallback((input) => {
        if (hasMore) {
            if (asyncOptions.length === 0)
                setLocalLoading(true);
            loadOptions(input, options, additionalPageData).then(({ loadedOptions, more, additional }) => {
                if (input != localInput)
                    setAsyncOptions(loadedOptions || []);
                else
                    setAsyncOptions(prevAsyncOptions => [...prevAsyncOptions, ...loadedOptions]);
                setAdditionalPageData(additional);
                if (hasMore !== more && input === localInput)
                    setHasMore(more);
                setLocalLoading(false);
                setOptionsLoaded(true);
            });
            return;
        }
        setLocalLoading(false);
        setOptionsLoaded(true);
    }, [additionalPageData, asyncOptions.length, hasMore, loadOptions, localInput, options]);
    const handleLoadOptions = useCallback((input) => {
        if (!loadOptions)
            return;
        if (!isPaginated) {
            setLocalLoading(true);
            loadOptions(input).then(loadedOptions => {
                setAsyncOptions(loadedOptions || []);
                setLocalLoading(false);
                setOptionsLoaded(true);
            });
        }
        else
            handleLoadOptionsPaginated(input);
    }, [handleLoadOptionsPaginated, isPaginated, loadOptions]);
    const handleMenuOpen = useCallback((event) => {
        if (!optionsLoaded)
            handleLoadOptions(localInput);
        if (onOpen)
            onOpen(event);
    }, [handleLoadOptions, localInput, onOpen, optionsLoaded]);
    const handleMenuClose = useCallback((event, reason) => {
        setLocalInput('');
        if (onClose)
            onClose(event, reason);
    }, [onClose]);
    const renderInput = useCallback((params) => {
        params.inputProps.className = `${params.inputProps.className} ${classes.input}`;
        if (inputSelectedColor)
            params.inputProps.style = { color: inputSelectedColor };
        params.inputProps.readOnly = !isSearchable;
        const textFieldProps = {
            label,
            error,
            helperText,
            required,
            placeholder,
            ...inputTextFieldProps
        };
        return (React.createElement(TextField, { fullWidth: true, ...params, startAdornment: params.InputProps.startAdornment, endAdornment: params.InputProps.endAdornment, ...textFieldProps, InputProps: { ...params.InputProps, margin: 'none' }, InputLabelProps: { ...params.InputLabelProps, margin: null } }));
    }, [inputSelectedColor, isSearchable, label, error, helperText, required, placeholder, inputTextFieldProps]);
    const handleOptionLabel = useCallback((option) => {
        if (getOptionLabel)
            return getOptionLabel(option);
        if (isStringOrNumber(option))
            return option.toString();
        const label = option?._primitiveValue ? option?._primitiveValue : findFirstNotNil([labelKey, valueKey], option);
        return label?.toString() ?? '';
    }, [getOptionLabel, labelKey, valueKey]);
    const renderOption = useCallback((props, option, { selected }) => {
        const optionLabel = handleOptionLabel(option);
        return (React.createElement(Option, { createdLabel: option?._createdOption ? createdLabel : '', optionLabel: optionLabel, selected: selected, withCheckboxes: withCheckboxes, option: option, ...props }));
    }, [handleOptionLabel, createdLabel, withCheckboxes]);
    const renderTags = useCallback((value, getTagProps) => value.map((option, index) => (React.createElement(Chip, { key: index, label: handleOptionLabel(option), ...getTagProps({ index }), disabled: option?.isDisabled || disabled || isValueDisabled }))), [handleOptionLabel, disabled, isValueDisabled]);
    const isOptionEqualToValue = useCallback((option, value) => !is(Object, option) && !is(Object, value)
        ? equals(option, value)
        : simpleValue
            ? equals(option[valueKey], value) || equals(option?.[valueKey], value?.[valueKey])
            : equals(option?.[valueKey], value?.[valueKey]), [simpleValue, valueKey]);
    const handleChange = useCallback((event, inputValue, reason) => {
        // when multi-value and clearable, doesn't clear disabled options that have already been selected
        if (reason === 'clear' && getOptionDisabled && isMultiSelection) {
            return onChange(computeChangedMultiValue(disabledOptions, simpleValue, valueKey, labelKey), event, reason);
        }
        setLocalInput(handleOptionLabel(inputValue));
        // for multi-value Autocomplete, options dialog remains open after selection and we do not want to display a loading state
        if (loadOptions && !isMultiSelection)
            setLocalLoading(true);
        if (isNil(inputValue) || isStringOrNumber(inputValue))
            return onChange(inputValue, event, reason);
        if (isMultiSelection) {
            return onChange(computeChangedMultiValue(inputValue, simpleValue, valueKey, labelKey), event, reason);
        }
        return onChange(computeChangedSingleValue(inputValue, simpleValue, valueKey, labelKey), event, reason);
    }, [
        disabledOptions,
        getOptionDisabled,
        handleOptionLabel,
        isMultiSelection,
        labelKey,
        loadOptions,
        onChange,
        simpleValue,
        valueKey
    ]);
    const handleInputChange = useCallback((event, value, reason) => {
        setLocalInput(value ? value : '');
        if (onInputChange)
            onInputChange(event, value, reason);
        // this prevents the component from calling loadOptions again when the user clicks outside it and the menu closes
        if (event?.nativeEvent?.type === 'focusout') {
            setOptionsLoaded(false);
            return;
        }
        if (loadOptions) {
            setLocalLoading(true);
            handleLoadOptions(value);
        }
    }, [handleLoadOptions, loadOptions, onInputChange]);
    useEffect(() => {
        // when simpleValue is false, loadOptions has already been called at this point by handleInputChange
        if (!simpleValue || !loadOptions)
            return;
        if (is(Array, defaultOptions) && !isEmpty(defaultOptions))
            return;
        if (defaultOptions === false)
            return;
        const hasInitialValue = is(Array, value) ? !isEmpty(value) : value;
        // when simpleValue is true, we need to previously load a set of options in order to match the value with one of them
        if (defaultOptions === true || hasInitialValue) {
            handleLoadOptions();
        }
        // this effect should run only at component mount
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    useEffect(() => {
        setOptions(receivedOptions || []);
    }, [receivedOptions]);
    const listBoxProps = useMemo(() => isPaginated
        ? {
            onScroll: (event) => {
                const listboxNode = event.currentTarget;
                if (listboxNode.scrollTop + listboxNode.clientHeight >= listboxNode.scrollHeight - 1 && hasMore) {
                    handleLoadOptions(localInput);
                }
            }
        }
        : undefined, [handleLoadOptions, hasMore, isPaginated, localInput]);
    return (React.createElement(MuiAutocomplete, { noOptionsText: React.createElement(NoOptionsText, { color: typographyContentColor }, noOptionsText), typographyContentColor: typographyContentColor, forcePopupIcon: true, label: label, disabled: disabled || isValueDisabled, loading: loading, loadingText: loadingText ?? React.createElement(LinearProgress, null), onOpen: handleMenuOpen, onClose: handleMenuClose, clearOnBlur: true, freeSolo: creatable, options: loading ? [] : loadOptions ? asyncOptions : options || [], autoHighlight: true, handleHomeEndKeys: true, selectOnFocus: true, disableCloseOnSelect: isMultiSelection, filterSelectedOptions: simpleValue && isMultiSelection && !withCheckboxes, filterOptions: filterOptions(labelKey, valueKey, creatable), getOptionLabel: handleOptionLabel, isOptionEqualToValue: isOptionEqualToValue, getOptionDisabled: getOptionDisabled, value: simpleValue ? getSimpleValue(loadOptions ? asyncOptions : options, value, valueKey, isMultiSelection) : value, multiple: isMultiSelection, onChange: handleChange, onInputChange: handleInputChange, disableClearable: !isClearable, renderOption: renderOption, renderInput: renderInput, renderTags: renderTags, ListboxProps: listBoxProps, ...other }));
};
Autocomplete.propTypes = {
    /**
     * @default []
     * The array of options from which the client can select a value.
     */
    options: PropTypes.array,
    /**
     * Function that returns a promise, which resolves to the set of options to be used once the promise resolves.
     */
    loadOptions: PropTypes.func,
    /**
     * If true, the component is in a loading state.
     * By default, this shows a linear progress instead of options.
     * This can be changed by sending the loadingText prop to Autocomplete.
     */
    loading: PropTypes.bool,
    /**
     * @default '<LinearProgress />'
     * Text/component to display when in a loading state.
     */
    loadingText: PropTypes.node,
    /**
     * @default 'No options'
     * Text to display when there are no options.
     */
    noOptionsText: PropTypes.node,
    /**
     * Used to determine the string value for a given option.
     */
    getOptionLabel: PropTypes.func,
    /**
     * @default null
     * The selected value from list of options.
     */
    value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.number, PropTypes.string, PropTypes.bool]),
    /**
     * Handle change events on the autocomplete.
     */
    onChange: PropTypes.func.isRequired,
    /**
     * Callback fired when the input value changes.
     */
    onInputChange: PropTypes.func,
    /**
     * Handle the menu opening.
     */
    onOpen: PropTypes.func,
    /**
     * Handle the menu closing.
     */
    onClose: PropTypes.func,
    /**
     * @default false
     * If true, the user can select multiple values from list.
     */
    isMultiSelection: PropTypes.bool,
    /**
     * @default false
     * If true, the options list will have checkboxes.
     */
    withCheckboxes: PropTypes.bool,
    /**
     * @default false
     * If true, the user can clear the selected value.
     */
    isClearable: PropTypes.bool,
    /**
     * @default true
     * If false, the user cannot type in Autocomplete, filter options or create new ones.
     */
    isSearchable: PropTypes.bool,
    /**
     * @default false
     * If true, the Autocomplete is free solo, meaning that the user input is not bound to provided options and can add
     * his own values.
     */
    creatable: PropTypes.bool,
    /**
     * @default false
     * If true, the Autocomplete is disabled.
     */
    disabled: PropTypes.bool,
    /**
     * Used to determine the disabled state for a given option.
     */
    getOptionDisabled: PropTypes.func,
    /**
     * @default false
     * If true, options will be an array of simple values, instead of objects.
     */
    simpleValue: PropTypes.bool,
    /**
     * Label to be displayed in the heading component.
     */
    label: PropTypes.string,
    /**
     * @default 'id'
     * The key of values from options.
     */
    valueKey: PropTypes.string,
    /**
     * @default 'name'
     * The key of the displayed label for each option.
     */
    labelKey: PropTypes.string,
    /**
     * The content of the helper under the input.
     */
    helperText: PropTypes.node,
    /**
     * @default false
     * If true, the helper text is displayed when an error pops up.
     */
    error: PropTypes.bool,
    /**
     * Text to be displayed as a placeholder in the text field.
     */
    placeholder: PropTypes.string,
    /**
     * @default false
     * Marks the input field as required (with an *).
     */
    required: PropTypes.bool,
    /**
     * The value of label when a new option is added.
     */
    createdLabel: PropTypes.string,
    /**
     * @default []
     * The default set of options to show before the user starts searching. When set to true, the results for loadOptions('') will be autoloaded.
     */
    defaultOptions: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
    /**
     * @default 'textSecondary'
     * The color of both the text displayed when there are no options and placeholder. It supports those theme colors that make sense for this component.
     */
    typographyContentColor: PropTypes.oneOf([
        'initial',
        'inherit',
        'primary',
        'secondary',
        'textPrimary',
        'textSecondary',
        'error'
    ]),
    /**
     * The color of selected input.
     */
    inputSelectedColor: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.oneOf(['primary', 'secondary', 'error', 'info', 'success', 'warning'])
    ]),
    /**
     *  Properties that will be passed to the rendered input. This is a TextField.
     */
    inputTextFieldProps: PropTypes.object,
    /**
     * @default false
     * If true, the options list will be loaded incrementally using the paginated loadOptions callback
     */
    isPaginated: PropTypes.bool
};
export default Autocomplete;
//# sourceMappingURL=Autocomplete.js.map