import {
    indexOf,
    isEmpty,
    isEqual,
    isFunction,
    isNil,
} from 'lodash';
import ClassNames from 'classnames';
import moment from 'moment-timezone';
import MomentPropTypes from 'react-moment-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import TetherComponent from 'react-tether';
import DatePickerUtils from '../../utils/datePickerUtils';
import DateUtils from '../../utils/dateUtils';
import Icon from '../../dataDisplay/icon';
import Input from '../input';
import DatePickerCalendarOnClickOutside from '../datePickerCalendar/datePickerCalendarOnClickOutside';
import withStyles from '../../styles/withStyles';

const propTypes = {
    /**
     * Override or extend the styles applied to DatePickerInput.
     */
    classes: PropTypes.shape({
        isFluid: PropTypes.string,
        root: PropTypes.string,
    }),

    /**
    * Assign additional class names to DatePickerInput.
    */
    className: PropTypes.string,

    /**
     * Used for DOM testing. https://testing-library.com/docs/queries/bytestid/
     */
    dataTestId: PropTypes.string,

    /**
     * Single date value.  Moment object.
     */
    date: MomentPropTypes.momentObj,

    /**
     * 'From' ('Start') Date for a Date Range.  Moment object.
     */
    dateFrom: MomentPropTypes.momentObj,

    /**
     * 'To' ('End') Date for a Date Range.  Moment object.
     */
    dateTo: MomentPropTypes.momentObj,

    /**
     * Deprecated prop. Please use `disable` instead.
     */
    disable: PropTypes.bool,

    /**
     * A DatePickerInput can be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * String specifying the way the date value will be formatted.
     */
    displayFormat: PropTypes.string,

    /**
     * Indicate that there is a validation error for this DatePickerInput control.
     */
    errorMessage: PropTypes.string,

    /**
     * Indicates dates that should be highlighted on the DatePickerInput's calendar control.  Array of Moment objects.
     */
    events: PropTypes.arrayOf(MomentPropTypes.momentObj),

    /**
     * Specifies a range of dates that are not selectable.  Array of Moment objects.
     */
    excludeDates: PropTypes.arrayOf(MomentPropTypes.momentObj),

    /**
     * Function that is used to filter out dates that are to not be selectable.
     */
    filterDates: PropTypes.func,

    /**
     * The DatePickerInput will be resized to its parent container's width.
     */
    fluid: PropTypes.bool,

    /**
     * Forwarded Ref
     */
    forwardedRef: PropTypes.oneOfType([
        // Either a function
        PropTypes.func,
        // Or the instance of a DOM native element (see the note about SSR)
        PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
    ]),

    /**
     * Specify an element ID this DatePickerInput control.
     */
    id: PropTypes.oneOfType([
        PropTypes.number,
        PropTypes.string,
    ]),

    /**
     * Specifies a range of dates that are selectable.  Array of Moment objects.
     */
    includeDates: PropTypes.arrayOf(MomentPropTypes.momentObj),

    /**
     * Specifies a label for the DatePickerInput control.
     */
    label: PropTypes.string,

    /**
     * Specifies which locale will be used when formatting date values.
     */
    locale: PropTypes.string,

    /**
     * Specifies maximum selectable date.  Moment object.
     */
    maxDate: MomentPropTypes.momentObj,

    /**
     * Specifies minimum selectable date.   Moment object.
     */
    minDate: MomentPropTypes.momentObj,

    /**
     * DatePickerInput can handle an onBlur event from parent.
     */
    onBlur: PropTypes.func,

    /**
     * Event handler for consumer to control state outside of the DatePickerInput.
     */
    onChange: PropTypes.func,

    /**
     * Event handler called when the month in the DatePickerInput's calendar control changes.
     */
    onMonthChange: PropTypes.func,

    /**
     * If true, specifies that the DatePickerInput represents the 'From' (or 'Start') date of a date range.
     */
    rangeFrom: PropTypes.bool,

    /**
     * If true, specifies that the DatePickerInput represents the 'To' (or 'End') date of a date range.
     */
    rangeTo: PropTypes.bool,

    /**
     * Indicates whether the DatePickerInput control represents a required field.
     */
    required: PropTypes.bool,

    /**
     * Supply any inline styles to the DatePickerInput's container. Mainly used for padding and margins.
     */
    style: PropTypes.shape({}),

    /**
     * Allows the DatePickerInput to be focused via the Tab key.
     */
    tabIndex: PropTypes.number,
};

const defaultProps = {
    classes: null,
    className: null,
    dataTestId: undefined,
    date: null,
    dateFrom: null,
    dateTo: null,
    disable: false,
    disabled: false,
    displayFormat: 'MM/DD/YYYY',
    errorMessage: null,
    events: null,
    excludeDates: null,
    filterDates: null,
    fluid: false,
    forwardedRef: undefined,
    id: null,
    includeDates: null,
    label: null,
    locale: 'en-US',
    maxDate: null,
    minDate: null,
    onBlur: null,
    onChange: null,
    onMonthChange: null,
    rangeFrom: false,
    rangeTo: false,
    required: false,
    style: null,
    tabIndex: -1,
};

const styles = (theme) => ({
    root: {
        display: 'inline-block',
        '&$isFluid': {
            width: '100%',
        },
    },
    isFluid: {},
    '@global': {
        '.date-picker-tether-element': {
            zIndex: theme.zIndex.datePickerInputCalendar,
        },
    },
});

/**
 * The DatePickerInput represents an input for storing a date value.
 *
 * When clicked, it can display an interactive calendar control for selecting
 * the date.
 *
 * A date value can also be typed into the input manually.
 */
class DatePickerInput extends React.PureComponent {
    constructor(props) {
        super(props);

        const isDateRange = props.rangeFrom || props.rangeTo;
        const newDate = isDateRange ? undefined : props.date;
        const newDateFrom = !isDateRange ? undefined : props.dateFrom;
        const newDateTo = !isDateRange ? undefined : props.dateTo;

        let inputValue;

        if (props.rangeFrom) {
            inputValue = this.safeDateFormat(newDateFrom, props.locale);
        } else if (props.rangeTo) {
            inputValue = this.safeDateFormat(newDateTo, props.locale);
        } else {
            inputValue = this.safeDateFormat(newDate, props.locale);
        }

        this.state = {
            date: newDate,
            dateFrom: newDateFrom,
            dateTo: newDateTo,
            isCalendarOpen: false,
            inputValue,
            isFocused: false,
        };

        this.dateFormats = this.getAllowedDateFormats('MM/DD/YYYY');

        this.getMaxDate = this.getMaxDate.bind(this);
        this.getMinDate = this.getMinDate.bind(this);
        this.onCalendarChange = this.onCalendarChange.bind(this);
        this.onCalendarClickOutside = this.onCalendarClickOutside.bind(this);
        this.onIconClick = this.onIconClick.bind(this);
        this.onInputBlur = this.onInputBlur.bind(this);
        this.onInputChange = this.onInputChange.bind(this);
        this.onInputClick = this.onInputClick.bind(this);
        this.onInputFocus = this.onInputFocus.bind(this);
        this.onInputKeyDown = this.onInputKeyDown.bind(this);
        this.onMonthChange = this.onMonthChange.bind(this);

        this.datePickerInput = props.forwardedRef ?? React.createRef();
    }

    componentDidMount() {
        const { onChange, rangeFrom, rangeTo } = this.props;

        if ((rangeFrom || rangeTo) && !isFunction(onChange)) {
            // eslint-disable-next-line no-console
            console.error('The onChange prop is required when using the rangeFrom or rangeTo props');
        }
    }

    componentDidUpdate(prevProps) {
        const {
            date,
            dateFrom,
            dateTo,
            locale,
            rangeFrom,
            rangeTo,
        } = this.props;

        if (!DatePickerUtils.isSameDay(date, prevProps.date) ||
            !DatePickerUtils.isSameDay(dateFrom, prevProps.dateFrom) ||
            !DatePickerUtils.isSameDay(dateTo, prevProps.dateTo) ||
            !isEqual(locale, prevProps.locale)
        ) {
            const isDateRange = rangeFrom || rangeTo;
            const newDate = isDateRange ? undefined : date;
            const newDateFrom = !isDateRange ? undefined : dateFrom;
            const newDateTo = !isDateRange ? undefined : dateTo;
            let newInputValue;

            if (rangeFrom) {
                newInputValue = this.safeDateFormat(newDateFrom, locale);
            } else if (rangeTo) {
                newInputValue = this.safeDateFormat(newDateTo, locale);
            } else {
                newInputValue = this.safeDateFormat(newDate, locale);
            }

            this.setState({
                date: newDate,
                dateFrom: newDateFrom,
                dateTo: newDateTo,
                inputValue: newInputValue,
            });
        }
    }

    onCalendarChange({ date, dateFrom, dateTo }) {
        const {
            disable,
            disabled,
            onChange,
        } = this.props;

        const isNotDisabled = !disable && !disabled;
        const isOnChangeFunc = isFunction(onChange);

        if (isNotDisabled && isOnChangeFunc) {
            onChange({ date, dateFrom, dateTo });
        }

        if (isNotDisabled && !isOnChangeFunc) {
            const { locale } = this.props;

            this.setState({
                date,
                inputValue: this.safeDateFormat(date, locale),
            });
        }

        this.setOpen(false);
    }

    onCalendarClickOutside() {
        this.setOpen(false);
    }

    onIconClick() {
        const {
            disable,
            disabled,
        } = this.props;

        const isNotDisabled = !disable && !disabled;

        if (isNotDisabled) {
            this.datePickerInput.current.inputElement.focus();
        }
    }

    onInputBlur(event) {
        const { onBlur } = this.props;

        if (isFunction(onBlur)) {
            onBlur(event);
        }

        this.setState({ isFocused: false });
    }

    onInputChange(value) {
        const {
            disable,
            disabled,
            locale,
            rangeFrom,
            rangeTo,
        } = this.props;

        const { dateFrom, dateTo } = this.state;
        const date = moment(value, this.dateFormats, locale || moment.locale(), true);
        const isNotDisabled = !disable && !disabled;

        const isValidValueChange = isNotDisabled &&
            date.isValid() &&
            !DatePickerUtils.isDayDisabled(date, this.props);

        const isValidUpdateToNull = !isValidValueChange &&
            isNotDisabled &&
            (value === '' || value === '__/__/____');

        const onChangeParam = {};
        const updatedDateValue = isValidValueChange ? date : null;

        if (rangeFrom) {
            onChangeParam.dateFrom = updatedDateValue;
            onChangeParam.dateTo = dateTo;
        } else if (rangeTo) {
            onChangeParam.dateFrom = dateFrom;
            onChangeParam.dateTo = updatedDateValue;
        } else {
            onChangeParam.date = updatedDateValue;
        }

        if (isValidValueChange || isValidUpdateToNull) {
            this.onCalendarChange(onChangeParam);
        }
    }

    onInputClick() {
        this.setState({ isFocused: true });
    }

    onInputFocus() {
        this.setOpen(true);
        this.setState({ isFocused: true });
    }

    onInputKeyDown(event) {
        if (event.keyCode === 9 || event.keyCode === 13) {
            this.setOpen(false);
        }
    }

    onMonthChange(month, year) {
        const { onMonthChange } = this.props;

        if (isFunction(onMonthChange)) {
            onMonthChange(month, year);
        }
    }

    getAllowedDateFormats(specifiedFormat) {
        let formats = DateUtils.getAllowedDateFormats();

        if (indexOf(formats, specifiedFormat) < 0) {
            formats = [specifiedFormat, ...formats];
        }

        return formats;
    }

    getMaxDate() {
        const { maxDate } = this.props;

        return maxDate;
    }

    getMinDate() {
        const { minDate } = this.props;

        return minDate;
    }

    setOpen(open) {
        this.setState({ isCalendarOpen: open });
    }

    safeDateFormat(date, locale, format = 'MM/DD/YYYY') {
        if (date && date.isValid()) {
            return date.clone().locale(locale || moment.locale()).format(format);
        }

        if (isNil(date)) {
            return '';
        }

        return null;
    }

    render() {
        const {
            classes,
            className,
            errorMessage,
            dataTestId,
            disable,
            disabled,
            displayFormat,
            events,
            excludeDates,
            filterDates,
            fluid,
            id,
            includeDates,
            label,
            locale,
            rangeFrom,
            rangeTo,
            required,
            style,
            tabIndex,
        } = this.props;

        const {
            date,
            dateFrom,
            dateTo,
            isCalendarOpen,
            inputValue,
            isFocused,
        } = this.state;

        const rootClasses = ClassNames(
            'ui',
            'date-picker-input',
            classes.root,
            className,
            {
                [classes.isFluid]: fluid,
            },
        );

        let iconColor;

        if (isCalendarOpen) {
            iconColor = 'highlight';
        }

        const isDisabled = disable || disabled;

        if (isDisabled) {
            iconColor = 'primary';
        }

        let inputString = inputValue;
        if (!isFocused) {
            const dateFormat = this.safeDateFormat(date, locale, displayFormat);
            if (!isEmpty(dateFormat)) {
                inputString = dateFormat;
            }
        }

        return (
            <div
                className={rootClasses}
                id={id}
                style={style}
            >
                <TetherComponent
                    attachment="top left"
                    classPrefix="date-picker-tether"
                    constraints={[{
                        to: 'window',
                        attachment: 'together',
                    }]}
                    renderElement={(ref) => isCalendarOpen && (
                        <div ref={ref}>
                            <DatePickerCalendarOnClickOutside
                                controls="dropdowns"
                                date={date}
                                dateFrom={dateFrom}
                                dateTo={dateTo}
                                events={events}
                                excludeDates={excludeDates}
                                filterDates={filterDates}
                                includeDates={includeDates}
                                locale={locale}
                                maxDate={this.getMaxDate()}
                                minDate={this.getMinDate()}
                                mode="input"
                                onChange={this.onCalendarChange}
                                onClose={this.onCalendarClickOutside}
                                onMonthChange={this.onMonthChange}
                                rangeFrom={rangeFrom}
                                rangeTo={rangeTo}
                            />
                        </div>
                    )}
                    renderTarget={(ref) => (
                        <div ref={ref}>
                            <Input
                                autoComplete="off"
                                data-parsley-error-message={errorMessage}
                                dataTestId={dataTestId}
                                disable={isDisabled}
                                fluid={fluid}
                                guide
                                icon={(
                                    <Icon
                                        color={iconColor}
                                        compact
                                        disable={isDisabled}
                                        onClick={this.onIconClick}
                                        type="calendar"
                                    />
                                )}
                                keepCharPositions
                                label={label}
                                mask={[/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/]}
                                onBlur={this.onInputBlur}
                                onChange={this.onInputChange}
                                onClick={this.onInputClick}
                                onFocus={this.onInputFocus}
                                onKeyDown={this.onInputKeyDown}
                                placeholder="mm/dd/yyyy"
                                required={required}
                                ref={this.datePickerInput}
                                tabIndex={tabIndex}
                                type="text"
                                value={inputString}
                            />
                        </div>
                    )}
                    targetAttachment="bottom left"
                    targetOffset="11px 0"
                />
            </div>
        );
    }
}

DatePickerInput.propTypes = propTypes;
DatePickerInput.defaultProps = defaultProps;

const DatePickerInputWrapper = React.forwardRef((props, ref) => ((
    // eslint-disable-next-line react/jsx-props-no-spreading
    <DatePickerInput {...props} forwardedRef={ref} />
)));

const wrapperPropTypes = { ...propTypes };
delete wrapperPropTypes.forwardedRef;

const wrapperDefaultProps = { ...defaultProps };
delete wrapperDefaultProps.forwardedRef;

DatePickerInputWrapper.propTypes = wrapperPropTypes;
DatePickerInputWrapper.defaultProps = wrapperDefaultProps;
DatePickerInputWrapper.displayName = 'DatePickerInput';

export default withStyles(styles)(DatePickerInputWrapper);
