import {
    ComponentPropsWithoutRef,
    forwardRef,
    ForwardedRef,
    RefObject,
    useImperativeHandle,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
} from 'react';
import dayjs, { Dayjs } from 'dayjs';

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

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

const StyledCalendar = styled('div', {
    backgroundColor: `$base`,
    borderColor: `$grey-4`,
    borderWidth: '$default',
    borderRadius: '$default',
    boxShadow: 'none',
    color: '$grey-1',
    fontSize: '$sm',
    height: '$space$80',
    lineHeight: '$none',
    margin: '0',
    outline: 'none',
    padding: '$4',
    userSelect: 'none',
    overflow: 'hidden',
    width: '$space$72',
    '&[data-disabled]': {
        cursor: 'default',
        pointerEvents: 'none',
    },
});

const StyledHeader = styled('div', {
    alignItems: 'center',
    display: 'grid',
    gridAutoFlow: 'column',
    height: '$space$8',
    justifyContent: 'space-between',
    margin: '0 0 $2 0',
});

const StyledViewButton = styled('button', {
    alignItems: 'center',
    display: 'grid',
    gap: '$1',
    gridAutoFlow: 'column',
    gridAutoColumns: 'min-content',
    paddingLeft: '$2',
    paddingTop: '$1',
    paddingBottom: '$1',
    borderRadius: '$default',
    cursor: 'pointer',
    fontSize: '$xl',
    color: '$grey-1',
    outline: 'none',
    '&:hover': {
        backgroundColor: '$primary-5',
    },
    '&:focus': {
        outline: '2px solid $primary-1',
        outlineOffset: '2px',
    },
});

const StyledViewText = styled('div', {
    justifySelf: 'flex-start',
    whiteSpace: 'nowrap',
});

const StyledViewArrow = styled(Icon, {
    width: '$space$6',
});

const StyledPage = styled('div', {
    display: 'grid',
    gridAutoFlow: 'column',
});

const StyledGrid = styled('div', {
    alignItems: 'center',
    display: 'grid',
    justifyItems: 'center',
    variants: {
        view: {
            day: {
                gridTemplateColumns: 'repeat(7, minmax(0px, 1fr))',
                gridTemplateRows: 'repeat(7, minmax(0px, 1fr))',
                gridGap: '$1 $1',
            },
            month: {
                gridTemplateColumns: 'repeat(3, minmax(0px, 1fr))',
                gridTemplateRows: 'repeat(4, minmax(0px, 1fr))',
                gridGap: '$3 $2',
            },
            year: {
                gridTemplateColumns: 'repeat(4, minmax(0px, 1fr))',
                gridTemplateRows: 'repeat(7, minmax(0px, 1fr))',
                gridGap: '$3 $2',
            },
        },
    },
});

const StyledCell = styled('div', {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    variants: {
        view: {
            day: {
                height: '$space$8',
                width: '$space$8',
            },
            month: {
                height: '$space$8',
                width: '$space$16',
            },
            year: {
                height: '$space$6',
                width: '$space$12',
            },
        },
    },
});

const buildStyledCellButton = (selected: boolean, current: boolean) => {
    let css = {};
    if (selected) {
        css = {
            ...css,
            backgroundColor: '$primary-1',
            color: 'white',
            '&:hover': {
                backgroundColor: '$primary-2',
            },
            '&:disabled': {
                backgroundColor: `$grey-4`,
                borderColor: `$grey-4`,
                color: 'white',
                cursor: 'default',
                pointerEvents: 'none',
            },
        };
    } else if (current) {
        css = {
            ...css,
            color: '$grey-1',
            '&:hover': {
                backgroundColor: '$primary-5',
            },
        };
    } else {
        css = {
            ...css,
            color: '$grey-4',
            '&:hover': {
                backgroundColor: '$primary-5',
            },
        };
    }
    return {
        selected,
        current,
        css,
    };
};

const StyledCellButton = styled('button', {
    height: '$space$full',
    width: '$space$full',
    borderRadius: '$full',
    cursor: 'pointer',
    outline: 'none',
    '&:focus': {
        outline: '2px solid $primary-1',
        outlineOffset: '2px',
    },
    variants: {
        selected: {
            true: {},
            false: {},
        },
        current: {
            true: {},
            false: {},
        },
    },
    compoundVariants: [
        // selected, current
        buildStyledCellButton(true, true),
        buildStyledCellButton(true, false),
        buildStyledCellButton(false, true),
        buildStyledCellButton(false, false),
    ],
});

const StyledCellHeader = styled('div', {
    color: '$grey-1',
    fontWeight: '$semibold',
});

interface CalendarCell {
    date: Dayjs;
    display: string;
    label: string;
    current: boolean;
}

/**
 * Get the days
 * @param highlighted - date to focus on
 * @returns an array of arrays containing the day information
 */
function getDays(highlighted: Dayjs): CalendarCell[] {
    // store all of the days
    const days: CalendarCell[] = [];

    // get the dates from the previous month
    let firstDay = highlighted.startOf('month').day();
    if (firstDay > 0) {
        const lastMonthDate = highlighted.subtract(1, 'month').endOf('month');
        const dateOfMonth = lastMonthDate.date();

        while (firstDay > 0) {
            const d = lastMonthDate.date(dateOfMonth - (firstDay - 1));

            days.push({
                date: d,
                display: `${d.date()}`,
                label: d.format('MMMM D, YYYY'),
                current: false,
            });

            firstDay--;
        }
    }

    // get the dates from the current month
    const daysInMonth = highlighted.daysInMonth();
    let nextDate = 0;
    while (nextDate < daysInMonth) {
        const d = highlighted.startOf('month').add(nextDate, 'day');

        days.push({
            date: d,
            display: `${d.date()}`,
            label: d.format('MMMM D, YYYY'),
            current: true,
        });

        // go to the next day
        nextDate++;
    }

    // get the dates from the next month
    const nextMonth = highlighted.add(1, 'month').startOf('month');
    let nextMonthDate = 0;
    while (days.length < 42) {
        const d = nextMonth.add(nextMonthDate, 'day');

        days.push({
            date: d,
            display: d.format('D'),
            label: d.format('MMMM D, YYYY'),
            current: false,
        });

        // go to the next day
        nextMonthDate++;
    }

    return days;
}

/**
 * Get the months
 * @param highlighted - date to focus on
 * @returns an array of arrays containing the month information
 */
function getMonths(highlighted: Dayjs): CalendarCell[] {
    const months: CalendarCell[] = [];

    const firstDay = highlighted.startOf('year');
    for (let mIdx = 0; mIdx < 12; mIdx++) {
        const d = firstDay.add(mIdx, 'month');

        months.push({
            date: d,
            display: d.format('MMM'),
            label: d.format('MMMM YYYY'),
            current: true,
        });
    }

    return months;
}

/**
 * Get the years
 * @param highlighted - date to focus on
 * @returns an array of arrays containing the month information
 */
function getYears(highlighted: Dayjs): CalendarCell[] {
    const years: CalendarCell[] = [];

    const startYear = highlighted.year();
    const firstDay = highlighted
        .startOf('year')
        .year(Math.floor(startYear / 28) * 28);
    for (let mIdx = 0; mIdx < 28; mIdx++) {
        const d = firstDay.add(mIdx, 'year');

        years.push({
            date: d,
            display: d.format('YYYY'),
            label: d.format('YYYY'),
            current: true,
        });
    }

    return years;
}

export interface BaseCalendarProps
    extends Omit<
        ComponentPropsWithoutRef<'div'>,
        'value' | 'defaultValue' | 'defaultChecked' | 'onChange'
    > {
    /** Format of the Calendar's value */
    format?: string;
}

export type BaseCalendarView = 'day' | 'year' | 'month';

export type BaseCalendarMethods = { focusOnView: () => void };

interface _BaseCalendarProps extends BaseCalendarProps {
    /** Current view */
    methods?: RefObject<BaseCalendarMethods>;

    /** Current view */
    view: 'day' | 'year' | 'month';

    /** Current selected date */
    selectedDay: Dayjs;

    /** Current highlighted date */
    highlightedDay: Dayjs;

    /** Callback that is triggered when the view changes */
    onView: (view: BaseCalendarView) => void;

    /** Callback that is triggered when the selected day changes */
    onSelectedDay: (date: Dayjs) => void;

    /** Callback that is triggered when the highlighted day changes */
    onHighlightedDay: (date: Dayjs) => void;

    /** Mark the view in a disabled/enabled state */
    disabled: boolean;
}

// icons
const mdiChevronLeft =
    'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z';
const mdiChevronRight = 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z';
const mdiMenuDown = 'M7,10L12,15L17,10H7Z';
const mdiMenuUp = 'M7,15L12,10L17,15H7Z';

/**
 * BaseCalendar component
 */
const _BaseCalendar = (
    props: _BaseCalendarProps,
    ref: ForwardedRef<HTMLDivElement>,
): JSX.Element => {
    const {
        methods,
        view,
        selectedDay,
        highlightedDay,
        onView = () => null,
        onSelectedDay = () => null,
        onHighlightedDay = () => null,
        disabled = false,
        ...otherProps
    } = props;

    // state
    const [active, setActive] = useState<boolean>(false);

    // get the view based on the highlighted date
    const title = useMemo(() => {
        let token;
        switch (view) {
            case 'day':
                token = 'MMM YYYY';
                break;
            case 'month':
                token = 'YYYY';
                break;
            case 'year':
                token = 'YYYY';
                break;
        }

        return dayjs(highlightedDay).format(token);
    }, [highlightedDay, view]);

    // get the view based on the highlighted date
    const cells: CalendarCell[] = useMemo(() => {
        if (view === 'day') {
            return getDays(highlightedDay);
        } else if (view === 'month') {
            return getMonths(highlightedDay);
        } else if (view === 'year') {
            return getYears(highlightedDay);
        }

        return [];
    }, [highlightedDay, view]);

    // store the view component
    const focusRef = useRef<HTMLElement | null>(null);

    // expose methods to the parent
    useImperativeHandle(
        methods,
        () => {
            return {
                focusOnView: () => {
                    // focus on the element if it is there
                    focusRef.current?.focus();
                },
            };
        },
        [],
    );

    // focus on the highlighted one
    useLayoutEffect(() => {
        if (!active) {
            return;
        }

        // focus on the element if it is there
        focusRef.current?.focus();
    }, [active, view, highlightedDay]);

    return (
        <StyledCalendar
            ref={ref}
            data-disabled={disabled || undefined}
            {...otherProps}
        >
            <StyledHeader>
                <StyledViewButton
                    type="button"
                    disabled={disabled}
                    aria-label={`Switch View`}
                    onClick={(event) => {
                        // don't allow it to propagate
                        event.stopPropagation();

                        let next = view;
                        if (view === 'day') {
                            next = 'month';
                        } else if (view === 'month') {
                            next = 'year';
                        } else if (view === 'year') {
                            next = 'day';
                        }

                        if (next !== view) {
                            onView(next);
                        }
                    }}
                >
                    <StyledViewText>{title}</StyledViewText>
                    <StyledViewArrow
                        path={view === 'day' ? mdiMenuDown : mdiMenuUp}
                    ></StyledViewArrow>
                </StyledViewButton>
                {
                    <StyledPage>
                        <IconButton
                            size={'lg'}
                            disabled={disabled}
                            aria-label={`Navigate to the previous ${view}`}
                            onClick={(event) => {
                                // don't allow it to propagate
                                event.stopPropagation();

                                let next = highlightedDay.clone();
                                if (view === 'day') {
                                    next = next
                                        .startOf('month')
                                        .subtract(1, 'month');
                                } else if (view === 'month') {
                                    next = next
                                        .startOf('year')
                                        .subtract(1, 'year');
                                } else if (view === 'year') {
                                    next = next
                                        .startOf('year')
                                        .subtract(28, 'year');
                                }

                                if (highlightedDay.isSame(next, 'day')) {
                                    return;
                                }

                                onHighlightedDay(next);
                            }}
                        >
                            <Icon path={mdiChevronLeft}></Icon>
                        </IconButton>
                        <IconButton
                            size={'lg'}
                            disabled={disabled}
                            aria-label={`Navigate to the next ${view}`}
                            onClick={(event) => {
                                // don't allow it to propagate
                                event.stopPropagation();

                                let next = highlightedDay.clone();
                                if (view === 'day') {
                                    next = next
                                        .startOf('month')
                                        .add(1, 'month');
                                } else if (view === 'month') {
                                    next = next.startOf('year').add(1, 'year');
                                } else if (view === 'year') {
                                    next = next.startOf('year').add(28, 'year');
                                }

                                if (highlightedDay.isSame(next, 'day')) {
                                    return;
                                }

                                onHighlightedDay(next);
                            }}
                        >
                            <Icon path={mdiChevronRight}></Icon>
                        </IconButton>
                    </StyledPage>
                }
            </StyledHeader>
            <StyledGrid
                view={view}
                onKeyDown={(event) => {
                    if (
                        event.key === 'ArrowUp' ||
                        event.key === 'ArrowDown' ||
                        event.key === 'ArrowLeft' ||
                        event.key === 'ArrowRight'
                    ) {
                        // prevent the default action
                        event.preventDefault();

                        let next = highlightedDay.clone();
                        if (event.key === 'ArrowUp') {
                            if (view === 'day') {
                                next = next.subtract(1, 'week');
                            } else if (view === 'month') {
                                next = next
                                    .startOf('month')
                                    .subtract(3, 'month');
                            } else if (view === 'year') {
                                next = next.startOf('year').subtract(4, 'year');
                            }
                        } else if (event.key === 'ArrowDown') {
                            if (view === 'day') {
                                next = next.add(1, 'week');
                            } else if (view === 'month') {
                                next = next.startOf('month').add(3, 'month');
                            } else if (view === 'year') {
                                next = next.startOf('year').add(4, 'year');
                            }
                        } else if (event.key === 'ArrowLeft') {
                            if (view === 'day') {
                                next = next.subtract(1, 'day');
                            } else if (view === 'month') {
                                next = next
                                    .startOf('month')
                                    .subtract(1, 'month');
                            } else if (view === 'year') {
                                next = next.startOf('year').subtract(1, 'year');
                            }
                        } else if (event.key === 'ArrowRight') {
                            if (view === 'day') {
                                next = next.add(1, 'day');
                            } else if (view === 'month') {
                                next = next.startOf('month').add(1, 'month');
                            } else if (view === 'year') {
                                next = next.startOf('year').add(1, 'year');
                            }
                        }

                        if (highlightedDay.isSame(next, 'day')) {
                            return;
                        }

                        onHighlightedDay(next);
                    }
                }}
                onFocus={(event) => {
                    // set active when it enters this or a child
                    if (!event.currentTarget.contains(event.relatedTarget)) {
                        setActive(true);
                    }
                }}
                onBlur={(event) => {
                    // set inactive when it enters this or a child
                    if (!event.currentTarget.contains(event.relatedTarget)) {
                        setActive(false);
                    }
                }}
            >
                {view === 'day' && (
                    <>
                        <StyledCell title="Sunday" view={'day'}>
                            <StyledCellHeader>S</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Monday" view={'day'}>
                            <StyledCellHeader>M</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Tuesday" view={'day'}>
                            <StyledCellHeader>T</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Wednesday" view={'day'}>
                            <StyledCellHeader>W</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Thursday" view={'day'}>
                            <StyledCellHeader>Th</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Friday" view={'day'}>
                            <StyledCellHeader>F</StyledCellHeader>
                        </StyledCell>
                        <StyledCell title="Saturday" view={'day'}>
                            <StyledCellHeader>Sa</StyledCellHeader>
                        </StyledCell>
                    </>
                )}
                {cells.map((c) => {
                    const isHighlighted = c.date.isSame(highlightedDay, view);

                    const isSelected = c.date.isSame(selectedDay, view);

                    return (
                        <StyledCell key={`${c.label}`} view={view}>
                            <StyledCellButton
                                type="button"
                                ref={(node) => {
                                    if (isHighlighted && node) {
                                        focusRef.current = node;
                                    }
                                }}
                                current={c.current}
                                tabIndex={isHighlighted ? 0 : -1}
                                selected={isSelected}
                                disabled={disabled}
                                onClick={(event) => {
                                    // don't allow it to propagate
                                    event.stopPropagation();

                                    if (view === 'day') {
                                        // select the date
                                        if (selectedDay.isSame(c.date, 'day')) {
                                            return;
                                        }

                                        onHighlightedDay(c.date);
                                        onSelectedDay(c.date);
                                        return;
                                    } else if (view === 'month') {
                                        onHighlightedDay(c.date);
                                        onView('day');
                                        return;
                                    } else if (view === 'year') {
                                        onHighlightedDay(c.date);
                                        onView('month');
                                        return;
                                    }
                                }}
                                role="option"
                                aria-selected={isSelected}
                                aria-label={`Select ${c.label}`}
                            >
                                {c.display}
                            </StyledCellButton>
                        </StyledCell>
                    );
                })}
            </StyledGrid>
        </StyledCalendar>
    );
};

export const BaseCalendar = forwardRef(_BaseCalendar) as (
    props: _BaseCalendarProps & {
        ref?: ForwardedRef<HTMLDivElement>;
    },
) => ReturnType<typeof _BaseCalendar>;
