import {
    forwardRef,
    ForwardedRef,
    useState,
    useMemo,
    useEffect,
    useRef,
} from 'react';
import dayjs, { Dayjs } from 'dayjs';
import PopperJS from '@popperjs/core';
import { usePopper } from 'react-popper';

import { styled } from '../../stitches.config';

import { useValue, useFocusTrap } from '../../hooks';
import {
    BaseFormInputStyles,
    BaseFormInput,
    BaseFormInputProps,
    BaseIcon,
    BaseCalendar,
    BaseCalendarProps,
    BaseCalendarMethods,
    BaseCalendarView,
} from '../internal';

import { Icon } from '../Icon';
import { Portal } from '../Portal';

const StyledFormInput = styled(BaseFormInput, BaseFormInputStyles, {
    position: 'relative',
    cursor: 'pointer',
});

const StyledContainer = styled('div', {
    display: 'flex',
    zIndex: '0',
    position: 'relative',
});

const StyledContainerInner = styled('div', {
    display: 'grid',
    flex: '1',
});

const StyledValueContainer = styled('div', {
    alignItems: 'center',
    display: 'inline-flex',
    gridColumn: '1',
    gridRow: '1',
    height: '$space$full',
    width: '$space$full',
    outline: 'none',
    zIndex: '1',
    lineHeight: '1rem',
    '&[data-inactive]': {
        opacity: '0',
        zIndex: '0',
    },
});

const StyledPlaceholder = styled('span', {
    color: `$grey-3`,
});

const StyledInput = styled('input', {
    backgroundColor: 'transparent',
    gridColumn: '1',
    gridRow: '1',
    outline: 'none',
    zIndex: '1',
    lineHeight: '1rem',
    '&[data-inactive]': {
        cursor: 'default',
        caretColor: 'transparent',
        opacity: '0',
        zIndex: '0',
    },
});

const StyledPopover = styled('div', {
    zIndex: '30',
    '&[data-closed]': {
        display: 'none',
    },
});

const StyledCalendar = styled(BaseCalendar, {
    backgroundColor: `$base`,
    borderColor: '$grey-4',
    boxShadow: '$default',
    height: '$space$auto',
    '&[data-closed]': {
        display: 'none',
    },
});

// TODO: Need to figure out why @mdi/js isn't tree shaking
const mdiCalendar =
    'M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z';

export interface DatepickerProps
    extends BaseCalendarProps,
        BaseFormInputProps<string> {
    /** Placeholder to show when there is no value */
    placeholder?: string;

    /** Props to pass to the input */
    inputProps?: React.InputHTMLAttributes<HTMLInputElement>;

    /** Container to append the calendar to */
    container?: HTMLElement | null;
}

const _Datepicker = (
    props: DatepickerProps,
    ref: ForwardedRef<HTMLDivElement>,
): JSX.Element => {
    const {
        id,
        value,
        defaultValue,
        onChange = () => null,
        disabled = false,
        placeholder,
        inputProps,
        format = 'MM/DD/YYYY',
        valid,
        container,
        ...otherProps
    } = props;

    // refs
    const referenceRef = useRef<HTMLDivElement | null>(null);
    const inputRef = useRef<HTMLInputElement | null>(null);
    const popperRef = useRef<HTMLDivElement | null>(null);
    const calendarMethodsRef = useRef<BaseCalendarMethods | null>(null);

    // manage the internal value
    const [internalValue, setInternalValue] = useValue<string>({
        initialValue: '',
        value: value,
        defaultValue: defaultValue,
        onChange: onChange,
    });

    // state
    const [active, setActive] = useState<boolean>(false);
    const [open, setOpen] = useState<boolean>(false);
    const [inputValue, setInputValue] = useState<string>(internalValue);
    const [view, setView] = useState<BaseCalendarView>('day');
    const [highlightedDay, setHighlightedDay] = useState<Dayjs>(dayjs());

    // get the selected day
    const selectedDay: Dayjs = useMemo(() => {
        return dayjs(internalValue, format);
    }, [internalValue, format]);

    // get display value based on the selected format
    const displayValue = useMemo(() => {
        // return an empty string if its invalid
        if (!selectedDay.isValid()) {
            return '';
        }

        return dayjs(selectedDay).format(format);
    }, [selectedDay, format]);

    // create the elements (usePopper takes in the DOM Node);
    const [referenceElement, setReferenceElement] =
        useState<HTMLDivElement | null>(null);
    const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
        null,
    );

    // set the focus trap
    const { setTrap } = useFocusTrap();

    // create the popper + modifiers
    const modifiers = useMemo(
        () => [
            {
                name: 'flip',
                options: {
                    fallbackPlacements: ['top-start'],
                },
            },
            {
                name: 'offset',
                options: { offset: [0, 4] },
            },
            {
                name: 'eventListeners',
                options: {
                    scroll: open,
                    resize: open,
                },
            },
        ],
        [open],
    );

    const { styles, attributes, forceUpdate } = usePopper(
        referenceElement,
        popperElement,
        {
            strategy: 'absolute',
            placement: 'bottom-start',
            modifiers: modifiers as unknown as PopperJS.Modifier<
                unknown,
                unknown
            >[],
        },
    );

    // update the position on open
    useEffect(() => {
        if (open && forceUpdate) {
            forceUpdate();
        }
    }, [open, forceUpdate]);

    /**
     * Parse the inputValue and try to update the internal value
     */
    const parseInputValue = () => {
        // parse the input
        const p = dayjs(inputValue, format);

        // update the internal value based on validity
        if (p.isValid()) {
            // nothing to update if its the same
            if (selectedDay.isSame(p)) {
                return;
            }

            setInternalValue(p.format(format));
        } else if (selectedDay.isValid()) {
            // reset to the selected value if its valid
            setInputValue(selectedDay.format(format));
        } else {
            // reset to an empty value
            setInputValue('');
        }
    };

    // update the selected date when the value changes
    useEffect(() => {
        // update the input value based on validity
        if (selectedDay.isValid()) {
            setInputValue(selectedDay.format(format));
        } else {
            setInputValue('');
        }
    }, [selectedDay, format]);

    // update the highlightedDay base on the input and state
    useEffect(() => {
        // parse the value
        const p = dayjs(inputValue, format);

        // reset if it is not open
        if (!open) {
            if (selectedDay.isValid()) {
                setHighlightedDay(selectedDay);
            } else {
                setHighlightedDay(dayjs());
            }
            return;
        }

        if (p.isValid()) {
            setHighlightedDay(p);
        }
    }, [inputValue, open, selectedDay, format]);

    useEffect(() => {
        if (!open) {
            return;
        }

        // reset the view
        setView('day');

        /**
         * Close the datepicker if clicked outside
         * @param event - Mouse or Touch Event
         */
        const onDocumentClick = (event: MouseEvent | TouchEvent) => {
            if (!event.target) {
                return;
            }

            if (
                referenceRef.current?.contains(event.target as Node) ||
                popperRef.current?.contains(event.target as Node)
            ) {
                return;
            }

            // set as inactive
            setActive(false);

            // close the datepicker
            setOpen(false);
        };

        document.addEventListener('mousedown', onDocumentClick, true);
        document.addEventListener('touchstart', onDocumentClick, true);

        return () => {
            document.removeEventListener('mousedown', onDocumentClick, true);
            document.removeEventListener('touchstart', onDocumentClick, true);
        };
    }, [open]);

    return (
        <>
            <StyledFormInput
                ref={(node) => {
                    setReferenceElement(node);
                    if (typeof ref === 'function') {
                        ref(node);
                    } else if (ref) {
                        ref.current = node;
                    }

                    referenceRef.current = node;
                }}
                focusRef={null}
                valid={valid}
                disabled={disabled}
                onClick={() => {
                    // toggle the datepicker open / close
                    if (open) {
                        // close the datepicker
                        setOpen(false);

                        // focus on the input
                        inputRef.current?.focus();
                    } else {
                        // open the datepicker
                        setOpen(true);

                        // focus on the calendar
                        setTimeout(() => {
                            calendarMethodsRef.current?.focusOnView();
                        });
                    }
                }}
                {...otherProps}
            >
                <StyledContainer>
                    <StyledContainerInner>
                        <StyledValueContainer
                            data-inactive={active || undefined}
                        >
                            {displayValue ? (
                                <>{displayValue}</>
                            ) : (
                                <StyledPlaceholder>
                                    {placeholder || <>&nbsp;</>}
                                </StyledPlaceholder>
                            )}
                        </StyledValueContainer>
                        <StyledInput
                            ref={inputRef}
                            id={id}
                            data-inactive={!active || undefined}
                            type={'text'}
                            placeholder={placeholder}
                            disabled={disabled}
                            value={inputValue}
                            onChange={(
                                event: React.ChangeEvent<HTMLInputElement>,
                            ) => {
                                if (disabled) {
                                    return;
                                }

                                setInputValue(event.target.value);
                            }}
                            onKeyDown={(event) => {
                                if (event.key === 'Enter') {
                                    // parse the input
                                    parseInputValue();
                                } else if (event.key === 'ArrowUp') {
                                    // close the datepicker
                                    setOpen(false);
                                } else if (event.key === 'ArrowDown') {
                                    // open the datepicker
                                    setOpen(true);

                                    // focus on the calendar
                                    setTimeout(() => {
                                        calendarMethodsRef.current?.focusOnView();
                                    });
                                }
                            }}
                            onFocus={() => {
                                // set as active
                                setActive(true);
                            }}
                            onBlur={(event) => {
                                // deactivate when it leaves the input or the popover
                                if (
                                    !referenceRef.current?.contains(
                                        event.relatedTarget,
                                    ) &&
                                    !popperRef.current?.contains(
                                        event.relatedTarget,
                                    )
                                ) {
                                    // set as inactive
                                    setActive(false);

                                    // close the datepicker
                                    setOpen(false);

                                    // parse the input
                                    parseInputValue();
                                }
                            }}
                            autoComplete={'off'}
                            role={'combobox'}
                            aria-haspopup={'dialog'}
                            aria-expanded={open}
                            aria-autocomplete={'none'}
                            aria-controls={`${id}--dialog`}
                            {...inputProps}
                        />
                    </StyledContainerInner>
                    <BaseIcon
                        valid={valid}
                        data-expanded={open || undefined}
                        aria-hidden={true}
                    >
                        <Icon path={mdiCalendar} />
                    </BaseIcon>
                </StyledContainer>
            </StyledFormInput>
            <Portal container={container}>
                <StyledPopover
                    id={`${id}--dialog`}
                    data-closed={!open || undefined}
                    ref={(node) => {
                        // store the popper element
                        setPopperElement(node as HTMLDivElement | null);

                        // set the focus trap
                        setTrap(node);

                        // save the poppers ref
                        popperRef.current = node;
                    }}
                    style={styles.popper}
                    onKeyDown={(event) => {
                        if (event.key === 'Escape') {
                            // don't propagage
                            event.stopPropagation();

                            // close the datepicker
                            setOpen(false);

                            // focus on the input
                            inputRef.current?.focus();
                        }
                    }}
                    role={'dialog'}
                    aria-modal={true}
                    aria-label={'Select a Date'}
                    {...attributes.popper}
                >
                    <StyledCalendar
                        id={`${id}--calendar`}
                        methods={calendarMethodsRef}
                        view={view}
                        selectedDay={selectedDay}
                        highlightedDay={highlightedDay}
                        onView={(v) => setView(v)}
                        onHighlightedDay={(d) => setHighlightedDay(d)}
                        onSelectedDay={(d) => {
                            // parse the value
                            const v = dayjs(d).format(format);

                            // set it
                            setInternalValue(v);

                            // focus on the input
                            inputRef.current?.focus();

                            // close the popover
                            setOpen(false);
                        }}
                        disabled={disabled}
                        format={format}
                    ></StyledCalendar>
                </StyledPopover>
            </Portal>
        </>
    );
};

export const Datepicker = forwardRef(_Datepicker) as (
    props: DatepickerProps & {
        ref?: ForwardedRef<HTMLDivElement>;
    },
) => ReturnType<typeof _Datepicker>;
