import React, { forwardRef, ReactNode, useEffect, useRef, useState } from 'react'

import classNames from 'classnames'
import PropTypes from 'prop-types'
import { isMobile } from 'react-device-detect'

import { CButton } from '../button'
import { CCalendar, CCalendarProps } from '../calendar/CCalendar'
import {
  convertToDateObject,
  getDateBySelectionType,
  getLocalDateFromString,
  isDateDisabled,
} from '../calendar/utils'
import { CFormControlWrapper, CFormControlWrapperProps } from '../form/CFormControlWrapper'
import { CPicker, CPickerProps } from '../picker/CPicker'
import { CTimePicker } from '../time-picker/CTimePicker'

import { useDebouncedCallback } from '../../hooks'
import { Colors } from '../../types'
import { getInputIdOrName } from './utils'

export interface CDateRangePickerProps
  extends Omit<CFormControlWrapperProps, 'floatingLabel' | 'id'>,
    Omit<CPickerProps, 'placeholder' | 'id'>,
    Omit<CCalendarProps, 'onDayHover' | 'onCalendarDateChange'> {
  /**
   * The number of calendars that render on desktop devices.
   */
  calendars?: number
  /**
   * Toggle visibility or set the content of cancel button.
   *
   * @default 'Cancel'
   */
  cancelButton?: boolean | ReactNode
  /**
   * Sets the color context of the cancel button to one of CoreUI’s themed colors.
   *
   * @type 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' | string
   * @default 'primary'
   */
  cancelButtonColor?: Colors
  /**
   * Size the cancel button small or large.
   *
   * @default 'sm'
   */
  cancelButtonSize?: 'sm' | 'lg'
  /**
   * Set the cancel button variant to an outlined button or a ghost button.
   *
   * @default 'ghost'
   */
  cancelButtonVariant?: 'outline' | 'ghost'
  /**
   * A string of all className you want applied to the component.
   */
  className?: string
  /**
   * If true the dropdown will be immediately closed after submitting the full date.
   *
   * @since 4.8.0
   */
  closeOnSelect?: boolean
  /**
   * Toggle visibility or set the content of the cleaner button.
   */
  cleaner?: boolean
  /**
   * Toggle visibility or set the content of confirm button.
   *
   * @default 'OK'
   */
  confirmButton?: boolean | ReactNode
  /**
   * Sets the color context of the confirm button to one of CoreUI’s themed colors.
   *
   * @type 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' | string
   * @default 'primary'
   */
  confirmButtonColor?: Colors
  /**
   * Size the confirm button small or large.
   *
   * @default 'sm'
   */
  confirmButtonSize?: 'sm' | 'lg'
  /**
   * Set the confirm button variant to an outlined button or a ghost button.
   */
  confirmButtonVariant?: 'outline' | 'ghost'
  /**
   * The id attribute for the input elements. It can be a single string for both the start and end dates. If a single string is used, the postfix "-start-date" and "-end-date" will be automatically added to make the IDs unique. Alternatively, you can use an array of two strings for start and end dates separately.
   *
   * **[Deprecated since v5.3.0]** If the property is a type of string, the name attributes for input elements are generated based on this property until you define name prop ex.:
   * - `{id}-start-date`
   * - `{id}-end-date`
   */
  id?: string | [string, string]
  /**
   * Toggle visibility or set the content of the input indicator.
   */
  indicator?: ReactNode | boolean
  /**
   * Custom function to format the selected date into a string according to a custom format.
   *
   * @since 5.0.0
   */
  inputDateFormat?: (date: Date) => string
  /**
   * Custom function to parse the input value into a valid Date object.
   *
   * @since 5.0.0
   */
  inputDateParse?: (date: string | Date) => Date
  /**
   * Defines the delay (in milliseconds) for the input field's onChange event.
   *
   * @since 5.0.0
   */
  inputOnChangeDelay?: number
  /**
   * Toggle the readonly state for the component.
   */
  inputReadOnly?: boolean
  /**
   * The name attribute for the input elements. It can be a single string for both the start and end dates. If a single string is used, the postfix "-start-date" and "-end-date" will be automatically added to make the names unique. Alternatively, you can use an array of two strings for start and end dates separately.
   *
   * Example for single string: 'date-input'
   * Result: 'date-input-start-date', 'date-input-end-date'
   *
   * Example for array: ['start-date-input', 'end-date-input']
   * Result: 'start-date-input', 'end-date-input'
   *
   * @since 5.3.0
   */
  name?: string | [string, string]
  /**
   * Specifies short hints that are visible in start date and end date inputs.
   */
  placeholder?: string | string[]
  /**
   * Enable live preview of dates in input fields when hovering over calendar cells.
   *
   * @since 5.20.0
   */
  previewDateOnHover?: boolean
  /**
   * @ignore
   */
  range?: boolean
  /**
   * Predefined date ranges the user can select from.
   */
  ranges?: object
  /**
   * Sets the color context of the cancel button to one of CoreUI’s themed colors.
   *
   * @type 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' | string
   */
  rangesButtonsColor?: Colors
  /**
   * Size the ranges button small or large.
   */
  rangesButtonsSize?: 'sm' | 'lg'
  /**
   * Set the ranges button variant to an outlined button or a ghost button.
   */
  rangesButtonsVariant?: 'outline' | 'ghost'
  /**
   * When present, it specifies that date must be filled out before submitting the form.
   *
   * @since 4.10.0
   */
  required?: boolean
  /**
   * Default icon or character character that separates two dates.
   */
  separator?: ReactNode | boolean
  /**
   * Size the component small or large.
   */
  size?: 'sm' | 'lg'
  /**
   * Provide an additional time selection by adding select boxes to choose times.
   */
  timepicker?: boolean
  /**
   * Toggle visibility or set the content of today button.
   *
   * @default 'Today'
   */
  todayButton?: boolean | ReactNode
  /**
   * Sets the color context of the today button to one of CoreUI’s themed colors.
   *
   * @type 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' | string
   * @default 'primary'
   */
  todayButtonColor?: Colors
  /**
   * Size the today button small or large.
   *
   * @default 'sm'
   */
  todayButtonSize?: 'sm' | 'lg'
  /**
   * Set the today button variant to an outlined button or a ghost button.
   */
  todayButtonVariant?: 'outline' | 'ghost'
}

export const CDateRangePicker = forwardRef<HTMLDivElement | HTMLLIElement, CDateRangePickerProps>(
  (
    {
      ariaNavNextMonthLabel,
      ariaNavNextYearLabel,
      ariaNavPrevMonthLabel,
      ariaNavPrevYearLabel,
      calendars = 2,
      cancelButton = 'Cancel',
      cancelButtonColor = 'primary',
      cancelButtonSize = 'sm',
      cancelButtonVariant = 'ghost',
      className,
      cleaner = true,
      closeOnSelect = true,
      confirmButton = 'OK',
      confirmButtonColor = 'primary',
      confirmButtonSize = 'sm',
      confirmButtonVariant,
      dayFormat,
      disabled,
      disabledDates,
      endDate,
      feedback,
      feedbackInvalid,
      feedbackValid,
      firstDayOfWeek,
      footer,
      id,
      indicator = true,
      inputDateFormat,
      inputDateParse,
      inputOnChangeDelay = 750,
      inputReadOnly,
      invalid,
      label,
      locale = 'default',
      maxDate,
      minDate,
      name,
      navigation,
      navYearFirst,
      onEndDateChange,
      onHide,
      onStartDateChange,
      onShow,
      placeholder = ['Start date', 'End date'],
      portal = true,
      previewDateOnHover = true,
      range = true,
      ranges,
      rangesButtonsColor = 'secondary',
      rangesButtonsSize,
      rangesButtonsVariant = 'ghost',
      required,
      separator = true,
      selectAdjacementDays,
      selectionType = 'day',
      showAdjacementDays,
      showWeekNumber,
      size,
      startDate,
      text,
      timepicker,
      toggler,
      todayButton = 'Today',
      todayButtonColor = 'primary',
      todayButtonSize = 'sm',
      todayButtonVariant,
      tooltipFeedback,
      valid,
      visible,
      weekdayFormat,
      weekNumbersLabel,
      calendarDate = startDate || endDate || null,
      ...rest
    },
    ref
  ) => {
    const inputEndRef = useRef<HTMLInputElement>(null)
    const inputStartRef = useRef<HTMLInputElement>(null)
    const formRef = useRef<HTMLFormElement>(null)

    const [_calendarDate, setCalendarDate] = useState<Date | string | null>(calendarDate)
    const [_endDate, setEndDate] = useState<Date | string | null>(endDate ?? null)
    const [_maxDate, setMaxDate] = useState<Date | string | null>(maxDate ?? null)
    const [_minDate, setMinDate] = useState<Date | string | null>(minDate ?? null)
    const [_startDate, setStartDate] = useState<Date | string | null>(startDate ?? null)
    const [_visible, setVisible] = useState(visible)

    const [initialStartDate, setInitialStartDate] = useState<Date | string | null>(
      startDate ?? null
    )
    const [initialEndDate, setInitialEndDate] = useState<Date | string | null>(endDate ?? null)
    const [inputStartHoverValue, setInputStartHoverValue] = useState<Date | string | null>(null)
    const [inputEndHoverValue, setInputEndHoverValue] = useState<Date | string | null>(null)
    const [isValid, setIsValid] = useState(valid ?? (invalid === true ? false : undefined))
    const [selectEndDate, setSelectEndDate] = useState(false)

    useEffect(() => {
      setIsValid(valid ?? (invalid === true ? false : undefined))
    }, [valid, invalid])

    useEffect(() => {
      if (startDate) {
        setStartDate(startDate)
        setInputValue(inputStartRef.current, startDate)
      }
    }, [startDate])

    useEffect(() => {
      if (endDate) {
        setEndDate(endDate)
        setInputValue(inputEndRef.current, endDate)
      }
    }, [endDate])

    useEffect(() => {
      if (maxDate) {
        setMaxDate(maxDate)
      }
    }, [maxDate])

    useEffect(() => {
      if (minDate) {
        setMinDate(minDate)
      }
    }, [minDate])

    useEffect(() => {
      if (inputStartRef.current && inputStartRef.current.form) {
        formRef.current = inputStartRef.current.form
      }
    }, [inputStartRef])

    useEffect(() => {
      if (formRef.current) {
        formRef.current.addEventListener('submit', (event) => {
          setTimeout(() => handleFormValidation(event.target as HTMLFormElement))
        })

        handleFormValidation(formRef.current)
      }
    }, [formRef, _startDate, _endDate])

    const formatDate = (date: Date | string) => {
      if (inputDateFormat) {
        const convertedDate =
          date instanceof Date ? new Date(date) : convertToDateObject(date, selectionType)

        if (
          !convertedDate ||
          (convertedDate instanceof Date && Number.isNaN(convertedDate.getTime()))
        ) {
          return ''
        }

        return inputDateFormat(convertedDate)
      }

      if (selectionType !== 'day') {
        return date
      }

      const _date = new Date(date)
      if (Number.isNaN(_date.getTime())) {
        return ''
      }

      return timepicker ? _date.toLocaleString(locale) : _date.toLocaleDateString(locale)
    }

    const setInputValue = (el: HTMLInputElement | null, date: Date | string | null) => {
      if (!el) {
        return
      }

      if (date) {
        el.value = formatDate(date) as string
        return
      }

      el.value = ''
    }

    const handleDateHover = (date: Date | string | null) => {
      if (!previewDateOnHover) {
        return
      }

      if (selectEndDate) {
        setInputEndHoverValue(date)
      } else {
        setInputStartHoverValue(date)
      }
    }

    const handleClear = (event: React.MouseEvent<HTMLElement>) => {
      event.stopPropagation()
      setStartDate(null)
      setEndDate(null)
      setInputValue(inputStartRef.current, null)
      setInputValue(inputEndRef.current, null)
      onStartDateChange?.(null)
      onEndDateChange?.(null)
    }

    const handleEndDateChange = (date: Date | string | null) => {
      setEndDate(date)
      setInputValue(inputEndRef.current, date)
      setInputEndHoverValue(null)
      onEndDateChange?.(date)

      if (timepicker || footer) {
        return
      }

      if (closeOnSelect && _startDate !== null) {
        setVisible(false)
      }
    }

    const handleFormValidation = (form: HTMLFormElement) => {
      if (!form.classList.contains('was-validated')) {
        return
      }

      if ((range && _startDate && _endDate) || (!range && _startDate)) {
        return setIsValid(true)
      }

      setIsValid(false)
    }

    const handleStartDateChange = (date: Date | string | null) => {
      setStartDate(date)
      setInputValue(inputStartRef.current, date)
      setInputStartHoverValue(null)
      onStartDateChange?.(date)

      if (timepicker || footer) {
        return
      }

      if (closeOnSelect && !range) {
        setVisible(false)
      }
    }

    const handleOnChange = useDebouncedCallback((value: string, input: string) => {
      let date: Date | string | null = null

      if (inputDateParse) {
        date = inputDateParse(value)
      } else if (selectionType === 'day') {
        date = getLocalDateFromString(value, locale, timepicker)
      } else {
        date = convertToDateObject(value, selectionType)
      }

      if (
        date instanceof Date &&
        isDateDisabled(
          date,
          _minDate ? convertToDateObject(_minDate, selectionType) : null,
          _maxDate ? convertToDateObject(_maxDate, selectionType) : null,
          disabledDates
        )
      ) {
        return // Don't update if date is disabled
      }

      const isStartInput = input === 'start'
      const inputRef = isStartInput ? inputStartRef : inputEndRef
      const setDate = isStartInput ? setStartDate : setEndDate
      const onChange = isStartInput ? onStartDateChange : onEndDateChange
      const validDate = date ?? null
      let formatedDate: Date | string | null = validDate

      // Update calendar date for valid dates
      if (validDate instanceof Date && validDate.getTime()) {
        if (selectionType !== 'day') {
          formatedDate = getDateBySelectionType(validDate, selectionType)
        }

        setCalendarDate(formatedDate)
        setInputValue(inputRef.current, formatedDate)
      }

      // Update state and input
      setDate(formatedDate)
      onChange?.(formatedDate)
    }, inputOnChangeDelay)

    const renderInput = (type: 'start' | 'end') => {
      const isStart = type === 'start'
      const hoverValue = isStart ? inputStartHoverValue : inputEndHoverValue
      const inputRef = isStart ? inputStartRef : inputEndRef
      const placeholderText = isStart
        ? Array.isArray(placeholder)
          ? placeholder[0]
          : placeholder
        : placeholder[1]

      const inputElement = (
        <input
          autoComplete="off"
          className={classNames('date-picker-input', {
            hover: hoverValue,
          })}
          disabled={disabled}
          {...(id && { id: getInputIdOrName(id, range, type) })}
          {...(name && { name: getInputIdOrName(name, range, type) })}
          {...(id &&
            !Array.isArray(id) &&
            !name && {
              name: isStart ? (range ? `${id}-start-date` : `${id}-date`) : `${id}-end-date`,
            })} // TODO: remove in v6
          placeholder={placeholderText}
          readOnly={inputReadOnly}
          required={required}
          onChange={(event) => {
            handleOnChange(event.target.value, type)
          }}
          onClick={() => setSelectEndDate(!isStart)}
          ref={inputRef}
        />
      )

      if (previewDateOnHover && !disabled) {
        return (
          <div className="date-picker-input-wrapper">
            {inputElement}
            {hoverValue && (
              <input
                className="date-picker-input date-picker-input-preview"
                readOnly
                value={formatDate(hoverValue) as string}
              />
            )}
          </div>
        )
      }

      return inputElement
    }

    const InputGroup = () => (
      <div className="date-picker-input-group">
        {renderInput('start')}
        {range && separator !== false && <div className="date-picker-separator" />}
        {range && renderInput('end')}
        {indicator &&
          (typeof indicator === 'boolean' ? (
            <div
              className="date-picker-indicator"
              {...(!disabled && {
                onClick: () => setVisible(!_visible),
                onKeyDown: (event) => {
                  if (event.key === 'Enter') {
                    setVisible(!_visible)
                  }
                },
                tabIndex: 0,
              })}
            />
          ) : (
            indicator
          ))}
        {cleaner &&
          (_startDate || _endDate) &&
          (typeof cleaner === 'boolean' ? (
            <div className="date-picker-cleaner" onClick={(event) => handleClear(event)} />
          ) : (
            React.isValidElement(cleaner) &&
            React.cloneElement(cleaner as React.ReactElement<any>, {
              onClick: (event: React.MouseEvent<HTMLElement>) => handleClear(event),
            })
          ))}
      </div>
    )

    return (
      <CFormControlWrapper
        describedby={rest['aria-describedby']}
        feedback={feedback}
        feedbackInvalid={feedbackInvalid}
        feedbackValid={feedbackValid}
        {...(id && !Array.isArray(id) && { id: id })}
        invalid={isValid === false ? true : false}
        label={label}
        text={text}
        tooltipFeedback={tooltipFeedback}
        valid={isValid}
      >
        <CPicker
          className={classNames(
            'date-picker',
            {
              [`date-picker-${size}`]: size,
              disabled: disabled,
              'is-invalid': isValid === false ? true : false,
              'is-valid': isValid,
            },
            className
          )}
          disabled={disabled}
          dropdownClassNames="date-picker-dropdown"
          footer={footer || timepicker}
          footerContent={
            <div className="date-picker-footer">
              {todayButton && (
                <CButton
                  className="me-auto"
                  color={todayButtonColor}
                  size={todayButtonSize}
                  variant={todayButtonVariant}
                  onClick={() => {
                    const date = new Date()
                    handleStartDateChange(date)
                    if (range) handleEndDateChange(date)
                    setCalendarDate(date)
                  }}
                >
                  {todayButton}
                </CButton>
              )}
              {cancelButton && (
                <CButton
                  color={cancelButtonColor}
                  size={cancelButtonSize}
                  variant={cancelButtonVariant}
                  onClick={() => {
                    handleStartDateChange(initialStartDate)
                    if (range) handleEndDateChange(initialEndDate)
                    setVisible(false)
                  }}
                >
                  {cancelButton}
                </CButton>
              )}
              {confirmButton && (
                <CButton
                  color={confirmButtonColor}
                  size={confirmButtonSize}
                  variant={confirmButtonVariant}
                  onClick={() => {
                    setVisible(false)
                  }}
                >
                  {confirmButton}
                </CButton>
              )}
            </div>
          }
          toggler={toggler ?? InputGroup()}
          portal={portal}
          onHide={() => {
            setInputStartHoverValue(null)
            setInputEndHoverValue(null)
            setVisible(false)
            onHide?.()
          }}
          onShow={() => {
            setInitialStartDate(_startDate)
            setInitialEndDate(_endDate)
            setVisible(true)
            onShow?.()
          }}
          visible={_visible}
          {...rest}
          ref={ref}
        >
          <div className="date-picker-body">
            {ranges && (
              <div className="date-picker-ranges">
                {Object.keys(ranges).map((key: string, index: number) => (
                  <CButton
                    color={rangesButtonsColor}
                    key={index}
                    onClick={() => {
                      handleStartDateChange((ranges as { [key: string]: Date[] })[key][0])
                      handleEndDateChange((ranges as { [key: string]: Date[] })[key][1])
                    }}
                    size={rangesButtonsSize}
                    variant={rangesButtonsVariant}
                  >
                    {key}
                  </CButton>
                ))}
              </div>
            )}
            <CCalendar
              ariaNavNextMonthLabel={ariaNavNextMonthLabel}
              ariaNavNextYearLabel={ariaNavNextYearLabel}
              ariaNavPrevMonthLabel={ariaNavPrevMonthLabel}
              ariaNavPrevYearLabel={ariaNavPrevYearLabel}
              calendarDate={_calendarDate}
              calendars={isMobile ? 1 : calendars}
              className="date-picker-calendars"
              dayFormat={dayFormat}
              disabledDates={disabledDates}
              endDate={_endDate}
              firstDayOfWeek={firstDayOfWeek}
              locale={locale}
              maxDate={_maxDate}
              minDate={_minDate}
              navigation={navigation}
              navYearFirst={navYearFirst}
              range={range}
              selectAdjacementDays={selectAdjacementDays}
              selectEndDate={selectEndDate}
              selectionType={selectionType}
              showAdjacementDays={showAdjacementDays}
              showWeekNumber={showWeekNumber}
              startDate={_startDate}
              weekdayFormat={weekdayFormat}
              weekNumbersLabel={weekNumbersLabel}
              onDateHover={(date) => handleDateHover(date)}
              onCalendarDateChange={(date) => setCalendarDate(date)}
              onStartDateChange={(date) => handleStartDateChange(date)}
              onEndDateChange={(date) => handleEndDateChange(date)}
              onSelectEndChange={(value) => setSelectEndDate(value)}
            />
            {timepicker && (
              <div className="date-picker-timepickers">
                {(isMobile && range) || (range && calendars === 1) ? (
                  <>
                    <CTimePicker
                      container="inline"
                      disabled={_startDate === null ? true : false}
                      locale={locale}
                      onTimeChange={(_, __, date) => date && handleStartDateChange(date)}
                      time={_startDate && new Date(_startDate)}
                      variant="select"
                    />
                    <CTimePicker
                      container="inline"
                      disabled={_endDate === null ? true : false}
                      locale={locale}
                      onTimeChange={(_, __, date) => date && handleEndDateChange(date)}
                      time={_endDate && new Date(_endDate)}
                      variant="select"
                    />
                  </>
                ) : (
                  Array.from({ length: calendars }).map((_, index) => (
                    <CTimePicker
                      container="inline"
                      disabled={
                        index === 0
                          ? _startDate === null
                            ? true
                            : false
                          : _endDate === null
                            ? true
                            : false
                      }
                      key={index}
                      locale={locale}
                      onTimeChange={(_, __, date) =>
                        index === 0
                          ? date && handleStartDateChange(date)
                          : date && handleEndDateChange(date)
                      }
                      time={
                        index === 0
                          ? _startDate && new Date(_startDate)
                          : _endDate && new Date(_endDate)
                      }
                      variant="select"
                    />
                  ))
                )}
              </div>
            )}
          </div>
        </CPicker>
      </CFormControlWrapper>
    )
  }
)

CDateRangePicker.displayName = 'CDateRangePicker'

CDateRangePicker.propTypes = {
  ...CCalendar.propTypes,
  ...CFormControlWrapper.propTypes,
  ...CPicker.propTypes,
  cancelButton: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  cancelButtonColor: CButton.propTypes?.color,
  cancelButtonSize: CButton.propTypes?.size,
  cancelButtonVariant: CButton.propTypes?.variant,
  calendars: PropTypes.number,
  className: PropTypes.string,
  cleaner: PropTypes.bool,
  closeOnSelect: PropTypes.bool,
  confirmButton: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  confirmButtonColor: CButton.propTypes?.color,
  confirmButtonSize: CButton.propTypes?.size,
  confirmButtonVariant: CButton.propTypes?.variant,
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.any]),
  indicator: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  inputDateFormat: PropTypes.func,
  inputDateParse: PropTypes.func,
  inputOnChangeDelay: PropTypes.number,
  inputReadOnly: PropTypes.bool,
  name: PropTypes.oneOfType([PropTypes.string, PropTypes.any]),
  placeholder: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.arrayOf(PropTypes.string.isRequired),
  ]),
  previewDateOnHover: PropTypes.bool,
  range: PropTypes.bool,
  ranges: PropTypes.object,
  rangesButtonsColor: CButton.propTypes?.color,
  rangesButtonsSize: CButton.propTypes?.size,
  rangesButtonsVariant: CButton.propTypes?.variant,
  required: PropTypes.bool,
  separator: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  size: PropTypes.oneOf(['sm', 'lg']),
  timepicker: PropTypes.bool,
  todayButton: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  todayButtonColor: CButton.propTypes?.color,
  todayButtonSize: CButton.propTypes?.size,
  todayButtonVariant: CButton.propTypes?.variant,
}
