import React, { forwardRef, ReactNode, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import { CButton } from '../button/CButton'

import {
  createGroupsInArray,
  getMonthDetails,
  getMonthsNames,
  getYears,
  isDateDisabled,
  isDateInRange,
  isDateSelected,
  isDisableDateInRange,
  isLastDayOfMonth,
  isSameDateAs,
  isToday,
  isStartDate,
  isEndDate,
} from '../../utils/calendar'
import { useStateWithCallback } from '../../utils/hooks'

export interface CCalendarProps {
  /**
   * Default date of the component
   */
  calendarDate?: Date | string
  /**
   * The number of calendars that render on desktop devices.
   */
  calendars?: number
  /**
   * A string of all className you want applied to the component.
   */
  className?: string
  /**
   * Specify the list of dates that cannot be selected.
   */
  disabledDates?: Date[] | Date[][]
  /**
   * Initial selected to date (range).
   */
  endDate?: Date | string | null
  /**
   * Sets the day of start week.
   * - 0 - Sunday,
   * - 1 - Monday,
   * - 2 - Tuesday,
   * - 3 - Wednesday,
   * - 4 - Thursday,
   * - 5 - Friday,
   * - 6 - Saturday,
   *
   * @default 1
   */
  firstDayOfWeek?: number
  /**
   * Sets the default locale for components. If not set, it is inherited from the browser.
   *
   * @default 'default'
   */
  locale?: string
  /**
   * Max selectable date.
   */
  maxDate?: Date | string | null
  /**
   * Min selectable date.
   */
  minDate?: Date | string | null
  /**
   * Show arrows navigation.
   */
  navigation?: boolean
  /**
   * The custom next icon.
   */
  navNextIcon?: ReactNode
  /**
   * The custom next double icon.
   */
  navNextDoubleIcon?: ReactNode
  /**
   * The custom prev icon.
   */
  navPrevIcon?: ReactNode
  /**
   * The custom prev double icon.
   */
  navPrevDoubleIcon?: ReactNode
  /**
   * Allow range selection.
   */
  range?: boolean
  /**
   * Toggle select mode between start and end date.
   */
  selectEndDate?: boolean
  /**
   * Initial selected date.
   */
  startDate?: Date | string | null
  /**
   * Set length or format of day name.
   *
   * @default 2
   */
  weekdayFormat?: number | 'long' | 'narrow' | 'short'
  /**
   * Callback fired when the user hovers over the calendar cell.
   */
  onCalendarCellHover?: (date: Date | null) => void
  /**
   * Callback fired when the calendar date changed.
   */
  onCalendarDateChange?: (date: Date) => void
  /**
   * Callback fired when the start date changed.
   */
  onStartDateChange?: (date: Date | null, formatedDate?: string | undefined) => void
  /**
   * Callback fired when the end date changed.
   */
  onEndDateChange?: (date: Date | null, formatedDate?: string | undefined) => void
  /**
   * Callback fired when the selection type changed.
   */
  onSelectEndChange?: (value: boolean) => void
  /**
   * Callback fired when the view type of calendar changed.
   */
  onViewChanged?: (view: string) => void
}

export const CCalendar = forwardRef<HTMLDivElement, CCalendarProps>(
  (
    {
      calendarDate,
      calendars = 1,
      className,
      disabledDates,
      endDate,
      firstDayOfWeek = 1,
      locale = 'default',
      maxDate,
      minDate,
      navigation = true,
      navNextIcon,
      navNextDoubleIcon,
      navPrevIcon,
      navPrevDoubleIcon,
      range,
      selectEndDate,
      startDate,
      weekdayFormat = 2,
      onCalendarCellHover,
      onCalendarDateChange,
      onEndDateChange,
      onStartDateChange,
      onSelectEndChange,
      onViewChanged,
    },
    ref,
  ) => {
    const [_calendarDate, setCalendarDate] = useState<Date>(
      calendarDate ? new Date(calendarDate) : startDate ? new Date(startDate) : new Date(),
    )
    useEffect(() => {
      if (calendarDate) {
        const d = new Date(calendarDate)
        !isSameDateAs(_calendarDate, d) && setCalendarDate(d)
      }
    }, [calendarDate])

    const [_startDate, setStartDate] = useStateWithCallback<Date | null>(
      startDate ? new Date(startDate) : null,
      onStartDateChange,
    )
    useEffect(() => {
      const date = startDate ? new Date(startDate) : null
      if (!isSameDateAs(date, _startDate)) {
        setStartDate(date)
      }
    }, [startDate])

    const [_endDate, setEndDate] = useStateWithCallback<Date | null>(
      endDate ? new Date(endDate) : null,
      onEndDateChange,
    )
    useEffect(() => {
      const date = endDate ? new Date(endDate) : null
      if (!isSameDateAs(date, _endDate)) {
        setEndDate(date)
      }
    }, [endDate])

    const [_hoverDate, setHoverDate] = useState<Date | null>(null)

    const [_selectEndDate, setSelectEndDate] = useStateWithCallback(
      selectEndDate,
      onSelectEndChange,
    )
    useEffect(() => {
      setSelectEndDate(selectEndDate)
    }, [selectEndDate])

    const [_maxDate, setMaxDate] = useState<Date | null>(maxDate ? new Date(maxDate) : null)
    useEffect(() => {
      maxDate && setMaxDate(new Date(maxDate))
    }, [maxDate])

    const [_minDate, setMinDate] = useState<Date | null>(minDate ? new Date(minDate) : null)
    useEffect(() => {
      minDate && setMinDate(new Date(minDate))
    }, [minDate])

    const [view, setView] = useStateWithCallback('days', onViewChanged)

    const setCalendarPage = (years: number, months = 0, setMonth?: number) => {
      const year = _calendarDate.getFullYear()
      const month = _calendarDate.getMonth()
      const d = new Date(year, month, 1)

      years && d.setFullYear(d.getFullYear() + years)
      months && d.setMonth(d.getMonth() + months)
      typeof setMonth === 'number' && d.setMonth(setMonth)

      setCalendarDate(d)
      onCalendarDateChange && onCalendarDateChange(d)
    }

    const handleCellOnClick = (date: Date) => {
      if (isDateDisabled(date, _minDate, _maxDate, disabledDates)) {
        return
      }

      if (range) {
        if (_selectEndDate) {
          setSelectEndDate(false)

          if (_startDate && _startDate > date) {
            setStartDate(null)
            setEndDate(null)
            return
          }

          if (isDisableDateInRange(_startDate, date, disabledDates)) {
            setStartDate(null)
            setEndDate(null)
            return
          }

          setEndDate(date)
          return
        }

        if (_endDate && _endDate < date) {
          setStartDate(null)
          setEndDate(null)
          return
        }

        if (isDisableDateInRange(date, _endDate, disabledDates)) {
          setStartDate(null)
          setEndDate(null)
          return
        }

        setSelectEndDate(true)
        setStartDate(date)
        return
      }

      setStartDate(date)
    }

    const handleCellMouseEnter = (date: Date) => {
      if (isDateDisabled(date, _minDate, _maxDate, disabledDates)) {
        return
      }
      setHoverDate(date)
      onCalendarCellHover && onCalendarCellHover(date)
    }

    const handleCellMouseLeave = () => {
      setHoverDate(null)
      onCalendarCellHover && onCalendarCellHover(null)
    }

    const handleNavigationOnClick = (direction: string, double = false) => {
      if (direction === 'prev') {
        if (double) {
          setCalendarPage(view === 'years' ? -10 : -1)
          return
        }

        if (view !== 'days') {
          setCalendarPage(-1)
          return
        }

        setCalendarPage(0, -1)
        return
      }
      if (direction === 'next') {
        if (double) {
          setCalendarPage(view === 'years' ? 10 : 1)
          return
        }

        if (view !== 'days') {
          setCalendarPage(1)
          return
        }

        setCalendarPage(0, 1)
        return
      }
    }

    const Calendar = (props: { addMonths: number }) => {
      const { addMonths } = props
      let date = _calendarDate

      if (addMonths !== 0) {
        date = new Date(_calendarDate.getFullYear(), _calendarDate.getMonth() + addMonths, 1)
      }

      const monthDetails = getMonthDetails(date.getFullYear(), date.getMonth(), firstDayOfWeek)
      const listOfMonths = createGroupsInArray(getMonthsNames(locale), 4)
      const listOfYears = createGroupsInArray(getYears(date.getFullYear()), 4)
      const weekDays = monthDetails[0]
      return (
        <table>
          {view === 'days' && (
            <thead>
              <tr>
                {weekDays.map(({ date }: { date: Date }, idx: number) => (
                  <th key={idx} className="calendar-cell">
                    <div className="calendar-header-cell-inner">
                      {typeof weekdayFormat === 'string'
                        ? date.toLocaleDateString(locale, { weekday: weekdayFormat })
                        : date
                            .toLocaleDateString(locale, { weekday: 'long' })
                            .slice(0, weekdayFormat)}
                    </div>
                  </th>
                ))}
              </tr>
            </thead>
          )}
          <tbody>
            {view === 'days' &&
              monthDetails.map((week, index) => {
                return (
                  <tr key={index}>
                    {week.map(({ date, month }: { date: Date; month: string }, idx: number) => (
                      <td
                        className={classNames('calendar-cell', {
                          today: month === 'current' && isToday(date),
                          disabled: isDateDisabled(date, _minDate, _maxDate, disabledDates),
                          [month]: true,
                          last: isLastDayOfMonth(date),
                          'range-hover':
                            month === 'current' &&
                            (_hoverDate && _selectEndDate
                              ? isDateInRange(date, _startDate, _hoverDate)
                              : isDateInRange(date, _hoverDate, _endDate)),
                          range: month === 'current' && isDateInRange(date, _startDate, _endDate),
                          selected: isDateSelected(date, _startDate, _endDate),
                          start: isStartDate(date, _startDate, _endDate),
                          end: isEndDate(date, _startDate, _endDate),
                        })}
                        key={idx}
                        title={date.toLocaleDateString(locale)}
                        {...(month === 'current' && {
                          onClick: () => handleCellOnClick(date),
                          onMouseEnter: () => handleCellMouseEnter(date),
                          onMouseLeave: () => handleCellMouseLeave(),
                        })}
                        {...(month !== 'current' && {
                          onMouseEnter: () => handleCellMouseLeave(),
                        })}
                      >
                        <div className="calendar-cell-inner">
                          {date.toLocaleDateString(locale, { day: 'numeric' })}
                        </div>
                      </td>
                    ))}
                  </tr>
                )
              })}
            {view === 'months' &&
              listOfMonths.map((row, index) => {
                return (
                  <tr key={index}>
                    {row.map((month, idx) => {
                      return (
                        <td
                          key={idx}
                          className="calendar-cell month"
                          onClick={() => {
                            setCalendarPage(0, 0, index * 3 + idx - addMonths)
                            setView('days')
                          }}
                        >
                          <div className="calendar-cell-inner">{month}</div>
                        </td>
                      )
                    })}
                  </tr>
                )
              })}
            {view === 'years' &&
              listOfYears.map((row, index) => {
                return (
                  <tr key={index}>
                    {row.map((year, idx) => {
                      return (
                        <td
                          key={idx}
                          className="calendar-cell year"
                          onClick={() => {
                            setCalendarDate(
                              new Date(year, date.getMonth() - addMonths, date.getDate()),
                            )
                            setView('months')
                          }}
                        >
                          <div className="calendar-cell-inner">
                            {new Date(year, 0, 1).toLocaleDateString(locale, { year: 'numeric' })}
                          </div>
                        </td>
                      )
                    })}
                  </tr>
                )
              })}
          </tbody>
        </table>
      )
    }

    const Navigation = (props: { addMonths: number }) => {
      const { addMonths } = props
      let date = _calendarDate

      if (addMonths !== 0) {
        date = new Date(_calendarDate.getFullYear(), _calendarDate.getMonth() + addMonths, 1)
      }

      return (
        <div className="calendar-nav">
          {navigation && (
            <div className="calendar-nav-prev">
              <CButton
                color="transparent"
                size="sm"
                onClick={() => handleNavigationOnClick('prev', true)}
              >
                {navPrevDoubleIcon ? (
                  navPrevDoubleIcon
                ) : (
                  <span className="calendar-nav-icon calendar-nav-icon-double-prev" />
                )}
              </CButton>
              {view === 'days' && (
                <CButton
                  color="transparent"
                  size="sm"
                  onClick={() => handleNavigationOnClick('prev')}
                >
                  {navPrevIcon ? (
                    navPrevIcon
                  ) : (
                    <span className="calendar-nav-icon calendar-nav-icon-prev" />
                  )}
                </CButton>
              )}
            </div>
          )}
          <div className="calendar-nav-date">
            {view === 'days' && (
              <CButton
                color="transparent"
                size="sm"
                onClick={() => navigation && setView('months')}
              >
                {date.toLocaleDateString(locale, { month: 'long' })}
              </CButton>
            )}
            <CButton color="transparent" size="sm" onClick={() => navigation && setView('years')}>
              {date.toLocaleDateString(locale, { year: 'numeric' })}
            </CButton>
          </div>
          {navigation && (
            <div className="calendar-nav-next">
              {view === 'days' && (
                <CButton
                  color="transparent"
                  size="sm"
                  onClick={() => handleNavigationOnClick('next')}
                >
                  {navNextIcon ? (
                    navNextIcon
                  ) : (
                    <span className="calendar-nav-icon calendar-nav-icon-next" />
                  )}
                </CButton>
              )}
              <CButton
                color="transparent"
                size="sm"
                onClick={() => handleNavigationOnClick('next', true)}
              >
                {navNextDoubleIcon ? (
                  navNextDoubleIcon
                ) : (
                  <span className="calendar-nav-icon calendar-nav-icon-double-next" />
                )}
              </CButton>
            </div>
          )}
        </div>
      )
    }

    return (
      <div className={classNames('calendars', className)}>
        {Array.from({ length: calendars }, (_, index) => (
          <div className={classNames('calendar', view)} key={index} ref={ref}>
            {Navigation({ addMonths: index })}
            {Calendar({ addMonths: index })}
          </div>
        ))}
      </div>
    )
  },
)

CCalendar.propTypes = {
  className: PropTypes.string,
  calendarDate: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
  calendars: PropTypes.number,
  disabledDates: PropTypes.array,
  endDate: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
  firstDayOfWeek: PropTypes.number,
  locale: PropTypes.string,
  maxDate: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
  minDate: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
  navigation: PropTypes.bool,
  navNextIcon: PropTypes.node,
  navNextDoubleIcon: PropTypes.node,
  navPrevIcon: PropTypes.node,
  navPrevDoubleIcon: PropTypes.node,
  range: PropTypes.bool,
  selectEndDate: PropTypes.bool,
  startDate: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
  weekdayFormat: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.oneOf<'long' | 'narrow' | 'short'>(['long', 'narrow', 'short']),
  ]),
  onCalendarCellHover: PropTypes.func,
  onCalendarDateChange: PropTypes.func,
  onEndDateChange: PropTypes.func,
  onSelectEndChange: PropTypes.func,
  onStartDateChange: PropTypes.func,
  onViewChanged: PropTypes.func,
}

CCalendar.displayName = 'CCalendar'
