/**
 * Date picker component
 *
 * @author Fedorov Platon
 * @date 2021-06-25
 */

import * as React from 'react';
import {POPOVER_BOUNDARIES} from '../../constants';
import {Calendar} from '../calendar/Calendar';
import {CalendarProps} from '../calendar/Calendar.types';
import {DropDown} from '../dropdown/DropDown';
import {safeInvoke} from '../../utils/safeInvoke';
import moment from 'moment';
import * as styles from './datePicker.m.scss';
import {Input} from '../input/Input';
import {AdditionalTemplates, IDateTemplate, TemplateDate} from './AdditionalTemplates';
import {parseNoSeparatedDate} from '../../utils/parseNoSeparatedDate';

const defaultProps = {
    mode: 'month',
    pickerType: 'date',
    showSuffix: true
};

export {
    IDateTemplate
};

export type DatePickerProps = Omit<CalendarProps,
    'value' | 'fullscreen' | 'onSelect' | 'onChange' | 'mode' | 'defaultValue'> & {
    autoFocus?: boolean;
    allowClear?: boolean;
    errorMessage?: React.ReactNode;
    viewFormat: string;
    valueFormat: string;
    isReadOnly?: boolean;
    isDisabled?: boolean;
    placeholderInvalidDate?: string;
    placeholder?: string;
    mode?: 'month' | 'year';
    showSuffix?: boolean;
    onBlur?: () => void;
    isOpen?: boolean;
    isUtc?: boolean;
    isDefaultOpen?: boolean;
    onVisibleChange?: (visible: boolean) => void;
    value?: string;
    defaultValue?: string;
    onChange?: (date: string) => void;
    pickerType: 'date' | 'dateTime';
    additionalDateTemplates?: IDateTemplate[];
    errorTimeMessage: string;
    errorDateMessage: string;
}

interface IState {
    isOpen: boolean;
    text: string;
    date: moment.Moment | undefined;
    time: string;
    dateIsValid: boolean;
}

export class DatePicker extends React.Component<DatePickerProps, IState> {
    static defaultProps = defaultProps;

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

        this.state = {
            ...this.initialState,
            isOpen: props.isDefaultOpen || props.isOpen || false
        };
    }

    input = React.createRef<HTMLInputElement>();
    inputContainer = React.createRef<HTMLDivElement>();
    inputTime = React.createRef<HTMLInputElement>();
    calendar = React.createRef<HTMLDivElement>();
    isPanelChanged = false;

    private getMoment (value?: string, valueFormat?: string): moment.Moment {
        return this.isUTC ? moment.utc(value, valueFormat) : moment(value, valueFormat);
    }

    private get isUTC () {
        return this.props.isUtc || false;
    }

    get viewFormat (): string {
        return this.props.viewFormat;
    }

    get valueFormat (): string {
        return this.props.valueFormat;
    }

    get viewMask (): string {
        return this.viewFormat.replace(/\w/g, '9');
    }

    get viewEmptyMask (): string {
        return this.viewFormat.replace(/\w/g, '_');
    }

    get initialState () {
        const props = this.props;

        let text = '';
        let date: moment.Moment | undefined;

        if (props.value && props.value.length > 0) {
            const parsedDate = this.getMoment(props.value, props.valueFormat);

            if (parsedDate.isValid()) {
                text = parsedDate.format(this.viewFormat);
                date = parsedDate;
            } else {
                text = props.value;
            }
        }

        return {
            text: text,
            date: date,
            time: date ? date.format('HH:mm:ss') : '',
            dateIsValid: Boolean(date?.isValid())
        };
    }

    override componentDidUpdate (prevProps:Readonly<DatePickerProps>, prevState:Readonly<IState>, snapshot?:any) {
        if (this.props.value !== prevProps.value) {
            this.setState({...this.initialState});
        }
    }

    isValidRange = (date: moment.Moment) => {
        if (!this.props.validRange) {
            return true;
        }

        const validRange = this.props.validRange;

        return date.diff(validRange[0]) >= 0 && date.diff(validRange[1]) <= 0;
    }

    closeDropDown = () => {
        const text = this.state.text;
        const isOpen = this.props.isOpen === undefined ? this.state.isOpen : this.props.isOpen;

        if (text === this.viewEmptyMask) {
            this.onChange(undefined);
        } else {
            this.onChange(this.state.date);
        }

        if (isOpen) {
            safeInvoke(this.props.onVisibleChange, false);
        }
    }

    openDropDown = () => {
        this.setState({isOpen: true});
        safeInvoke(this.props.onVisibleChange, true);
    }

    onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        switch (e.key) {
            case 'Tab': {
                this.closeDropDown();
                break;
            }
            case 'Enter': {
                if (this.state.isOpen) {
                    this.closeDropDown();
                } else {
                    this.openDropDown();
                }

                break;
            }
        }
    }

    onInputFocus = () => {
        const isClose = this.props.isOpen === false || this.state.isOpen === false;

        if (isClose) {
            this.openDropDown();
        }
    }

    onInputBlur = () => {
        if  (this.state.isOpen === false) {
            safeInvoke(this.props.onBlur);
        }
    }

    onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.type === 'blur') {
            e.stopPropagation();
            e.preventDefault();

            return;
        }

        const text = e.currentTarget.value;
        const date = parseNoSeparatedDate(text, this.viewFormat, this.isUTC);

        if (date && date.isValid() &&
            (this.props.disabledDate === undefined || !this.props.disabledDate(date) && this.isValidRange(date))
        ) {
            this.setState({
                date: date,
                time: date ? date.format('HH:mm:ss') : '',
                dateIsValid: true,
                isOpen: true,
                text: text
            });
        } else {
            this.setState({
                dateIsValid: false,
                isOpen: true,
                text: text
            });
        }
    }

    onDropDownClose = (e: React.SyntheticEvent<Element> | Event) => {
        const isEsc = e instanceof KeyboardEvent;

        if (isEsc ||
            (this.inputContainer.current &&
                e.target instanceof Node && !this.inputContainer.current.contains(e.target))
        ) {
            this.closeDropDown();

            if (!isEsc) {
                safeInvoke(this.props.onBlur);
            } else {
                this.input.current?.focus();
            }
        }
    }

    onChange = (date?: moment.Moment, closePopup: boolean = true) => {
        if (this.state.time === '') {
            date?.startOf('date');
        }

        this.setState((prevState) => {
            return {
                isOpen: !closePopup ? prevState.isOpen : false,
                date: date,
                text: date ? date.format(this.viewFormat) : '',
                time: date ? date.format('HH:mm:ss') : '',
                dateIsValid: true
            };
        });

        const setDate = date ? date.format(this.valueFormat) : '';

        if (this.props.value !== setDate) {
            safeInvoke(this.props.onChange, date ? date.format(this.valueFormat) : '');
        }
    }

    onCalendarSelect = (date: moment.Moment) => {
        let isDisabled: boolean = false;

        if (this.props.disabledDate) {
            isDisabled = this.props.disabledDate(date);
        }

        if (!isDisabled) {
            this.onChange(date, !this.isPanelChanged);
        }

        if (!this.isPanelChanged) {
            this.input.current?.focus();
            safeInvoke(this.props.onVisibleChange, false);
        }

        this.isPanelChanged = false;
    }

    onCalendarPanelChange = (date: moment.Moment, mode: 'year' | 'month') => {
        this.isPanelChanged = true;

        safeInvoke(this.props.onPanelChange, date, mode);
    }

    onCalendarClose = () => {
        this.input.current?.focus();
        this.closeDropDown();
    }

    onAdditionalTemplateClick = (dateArg: TemplateDate) => {
        let date: moment.Moment | string | undefined;

        if (Array.isArray(dateArg)) {
            date = dateArg[0];
        } else {
            date = dateArg;
        }

        this.input.current?.focus();

        if (typeof date === 'string') {
            this.onChange(this.getMoment(date, this.valueFormat));
        } else {
            this.onChange(date);
        }

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

    onTimeInputBlur = () => {
        const value = this.state.time;

        if (value === '__:__:__' && this.state.date) {
            const date = this.state.date.clone();

            date.hours(0);
            date.minutes(0);
            date.seconds(0);

            this.onChange(date, false);

            return;
        }

        if (/^(([01][0-9])|(2[0-3])):[0-5][0-9]:[0-5][0-9]$/.test(value)) {
            const [h, m, s] = value.split(':').map((item) => parseInt(item));

            if (this.state.date === undefined) {
                this.onChange(this.getMoment(value, 'HH:mm:ss'), false);

                return;
            }

            const date = this.state.date.clone();

            if (date.hours() !== h || date.minutes() !== m || date.seconds() !== s) {
                date.hours(h);
                date.minutes(m);
                date.seconds(s);

                this.onChange(date, false);
            }
        } else {
            const date = this.state.date;

            this.setState({
                time: date ? date.format('HH:mm:ss') : ''
            });
        }
    }

    onTimeInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') {
            this.inputTime.current?.blur();
            this.input.current?.focus();
            this.setState({isOpen: false});
        }
    }

    onTimeInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        this.setState({time: e.target.value});
    }

    override render () {
        const {
            value,
            autoFocus,
            allowClear,
            errorMessage,
            isReadOnly,
            isDisabled,
            placeholder,
            placeholderInvalidDate,
            viewFormat,
            valueFormat,
            onChange,
            errorTimeMessage,
            ...props
        } = this.props;

        const isOpen = this.props.isOpen === undefined ? this.state.isOpen : this.props.isOpen;

        let defaultValue: moment.Moment | undefined = this.props.defaultValue ? this.getMoment(this.props.defaultValue) : undefined;
        let date: moment.Moment | undefined;
        let text: string = '';

        // we need to fixed types in data record attributes (there could be null in value)
        if (value === undefined || value === null) {
            date = this.state.date;
            text = this.state.text;
        } else {
            if (document.activeElement === this.input.current || isOpen) {
                text = this.state.text;
                date = this.state.date;
            } else if (value !== '') {
                date = this.getMoment(value, this.valueFormat);
                text = date.format(this.viewFormat);
            }
        }

        const isValid = date === undefined || (date.isValid() && this.state.dateIsValid);
        const additionalTemplates: IDateTemplate[] = this.props.additionalDateTemplates ? this.props.additionalDateTemplates : [];
        const dateIsInvalid = !isValid ? this.props.errorDateMessage : '';
        const errMessage = errorMessage ? errorMessage : dateIsInvalid;

        return (
            <DropDown
                target={(
                    <div className={styles.inputContainer}>
                        <div ref={this.inputContainer} onClick={this.onInputFocus}>
                            <Input.Mask
                                mask={this.viewMask}
                                value={text}
                                onFocus={this.onInputFocus}
                                onBlur={this.onInputBlur}
                                onChange={this.onInputChange}
                                onKeyDown={this.onKeyDown}
                                autoFocus={autoFocus}
                                allowClear={allowClear}
                                hasError={Boolean(errMessage)}
                                errorMessage={errMessage}
                                readOnly={isReadOnly}
                                isDisabled={isDisabled}
                                forwardRef={this.input}
                                suffix={this.props.showSuffix !== false ? 'calendar-empty' : undefined}
                                placeholder={isValid ? placeholder : placeholderInvalidDate}
                            />
                        </div>
                    </div>
                )}
                isOpen={isOpen}
                boundaries={POPOVER_BOUNDARIES.DOCUMENT}
                onClose={this.onDropDownClose}
                isDisabled={isDisabled || isReadOnly}
            >
                <div
                    className={styles.dateContainer}
                    ref={this.calendar}
                >
                    <Calendar
                        {...props}
                        defaultValue={defaultValue}
                        fullscreen={false}
                        value={date}
                        onClose={this.onCalendarClose}
                        onPanelChange={this.onCalendarPanelChange}
                        onSelect={this.onCalendarSelect}
                        showSearch={true}
                    />
                    {
                        this.props.pickerType === 'dateTime' && (
                            <div className={styles.inputTimeContainer}>
                                <Input.Time
                                    value={this.state.time}
                                    onChange={this.onTimeInputChange}
                                    onBlur={this.onTimeInputBlur}
                                    onKeyDown={this.onTimeInputKeyDown}
                                    forwardRef={this.inputTime}
                                    errorMessage={errorTimeMessage}
                                />
                            </div>
                        )
                    }
                    <AdditionalTemplates
                        dates={additionalTemplates}
                        onClick={this.onAdditionalTemplateClick}
                    />
                </div>
            </DropDown>
        );
    }
}
