import dayjs from 'dayjs';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';

import * as styles from './calendar.css';
import { DATE_MAX, DATE_MIN } from './constants';
import { CalendarLayout, DefaultDateCell, NavButtons } from './items';
import type { DateCell, DatePickerModePanelProps } from './types';

export const DayPicker = memo(function DayPicker(
  props: DatePickerModePanelProps
) {
  const dayPickerRootRef = useRef<HTMLDivElement>(null);
  const headerMonthRef = useRef<HTMLButtonElement>(null);

  const {
    value,
    cursor,
    weekDays,
    monthNames,
    format,
    todayLabel,
    customDayRenderer,
    onChange,
    onCursorChange,
    onModeChange,
  } = props;

  const matrix = useMemo(() => {
    const firstDayOfMonth = cursor.startOf('month');
    const firstDayOfFirstWeek = firstDayOfMonth.startOf('week');

    const lastDayOfMonth = cursor.endOf('month');
    const lastDayOfLastWeek = lastDayOfMonth.endOf('week');

    const matrix = [];
    let currentDay = firstDayOfFirstWeek;
    while (currentDay.isBefore(lastDayOfLastWeek)) {
      const week: DateCell[] = [];
      for (let i = 0; i < 7; i++) {
        week.push({
          date: currentDay,
          label: currentDay.date().toString(),
          isToday: currentDay.isSame(dayjs(), 'day'),
          notCurrentMonth: !currentDay.isSame(cursor, 'month'),
          selected: value ? currentDay.isSame(value, 'day') : false,
          focused: currentDay.isSame(cursor, 'day'),
        });
        currentDay = currentDay.add(1, 'day');
      }
      matrix.push(week);
    }
    return matrix;
  }, [cursor, value]);

  const prevDisabled = useMemo(() => {
    const firstDayOfMonth = cursor.startOf('month');
    return firstDayOfMonth.isSame(DATE_MIN, 'day');
  }, [cursor]);
  const nextDisabled = useMemo(() => {
    const lastDayOfMonth = cursor.endOf('month');
    return lastDayOfMonth.isSame(DATE_MAX, 'day');
  }, [cursor]);

  const onNextMonth = useCallback(() => {
    onCursorChange?.(cursor.add(1, 'month').set('date', 1));
  }, [cursor, onCursorChange]);
  const onPrevMonth = useCallback(() => {
    onCursorChange?.(cursor.add(-1, 'month').set('date', 1));
  }, [cursor, onCursorChange]);

  const focusCursor = useCallback(() => {
    const div = dayPickerRootRef.current;
    if (!div) return;
    const focused = div.querySelector('[data-is-date-cell][tabindex="0"]');
    focused && (focused as HTMLElement).focus();
  }, []);
  const openMonthPicker = useCallback(
    () => onModeChange?.('month'),
    [onModeChange]
  );
  const openYearPicker = useCallback(
    () => onModeChange?.('year'),
    [onModeChange]
  );

  // keyboard navigation
  useEffect(() => {
    const div = dayPickerRootRef.current;
    if (!div) return;
    const onKeyDown = (e: KeyboardEvent) => {
      if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key))
        return;

      const focused = document.activeElement;

      // check if focused is a date cell
      if (!focused?.hasAttribute('data-is-date-cell')) return;
      if (e.shiftKey) return;

      e.preventDefault();
      e.stopPropagation();

      if (e.key === 'ArrowUp') onCursorChange?.(cursor.add(-7, 'day'));
      if (e.key === 'ArrowDown') onCursorChange?.(cursor.add(7, 'day'));
      if (e.key === 'ArrowLeft') onCursorChange?.(cursor.add(-1, 'day'));
      if (e.key === 'ArrowRight') onCursorChange?.(cursor.add(1, 'day'));
      setTimeout(focusCursor);
    };
    div.addEventListener('keydown', onKeyDown);
    return () => {
      div?.removeEventListener('keydown', onKeyDown);
    };
  }, [cursor, focusCursor, onCursorChange]);

  const HeaderLeft = useMemo(
    () => (
      <div style={{ whiteSpace: 'nowrap' }}>
        <button
          onClick={openMonthPicker}
          ref={headerMonthRef}
          className={styles.calendarHeaderTriggerButton}
          data-testid="month-picker-button"
          data-month={cursor.month()}
          data-year={cursor.year()}
        >
          {monthNames.split(',')[cursor.month()]}
        </button>
        <button
          className={styles.calendarHeaderTriggerButton}
          onClick={openYearPicker}
          data-testid="year-picker-button"
          data-year={cursor.year()}
        >
          {cursor.year()}
        </button>
      </div>
    ),
    [cursor, monthNames, openMonthPicker, openYearPicker]
  );
  const HeaderRight = useMemo(
    () => (
      <NavButtons
        key="nav-buttons"
        onNext={onNextMonth}
        onPrev={onPrevMonth}
        prevDisabled={prevDisabled}
        nextDisabled={nextDisabled}
      >
        <button
          className={styles.headerNavToday}
          onClick={() => onChange?.(dayjs().format(format))}
        >
          {todayLabel}
        </button>
      </NavButtons>
    ),
    [
      format,
      nextDisabled,
      onChange,
      onNextMonth,
      onPrevMonth,
      prevDisabled,
      todayLabel,
    ]
  );
  const Body = useMemo(
    () => (
      <main className={styles.monthViewBody}>
        {/* weekDays */}
        <div className={styles.monthViewRow}>
          {weekDays.split(',').map(day => (
            <div key={day} className={styles.monthViewHeaderCell}>
              {day}
            </div>
          ))}
        </div>
        {/* Weeks in month */}
        {matrix.map((week, i) => {
          return (
            <div key={i} className={styles.monthViewRow}>
              {week.map((cell, j) => (
                <div
                  className={styles.monthViewBodyCell}
                  key={j}
                  onClick={() => onChange?.(cell.date.format(format))}
                >
                  {customDayRenderer ? (
                    customDayRenderer(cell)
                  ) : (
                    <DefaultDateCell key={j} {...cell} />
                  )}
                </div>
              ))}
            </div>
          );
        })}
      </main>
    ),
    [customDayRenderer, format, matrix, onChange, weekDays]
  );

  return (
    <CalendarLayout
      mode="day"
      ref={dayPickerRootRef}
      length={7}
      headerLeft={HeaderLeft}
      headerRight={HeaderRight}
      body={Body}
    />
  );
});
