import React from 'react'
import PropTypes from 'prop-types'
import momentPropTypes from 'react-moment-proptypes'
import 'react-dates/initialize'
import { SingleDatePicker, DayPickerSingleDateController } from 'react-dates'
import {
  IconButton,
  TextInput,
  Portal,
  selectSystemProps,
  useCopy,
  useViewport,
  useThemeTokens,
  applyTextStyles,
  getTokensPropType,
  useSafeLayoutEffect
} from '@telus-uds/components-base'
import styled from 'styled-components'
import moment from 'moment'
import { isUndefined, throttle } from 'lodash'
import CalendarContainer from './CalendarContainer'
import dictionary from './dictionary'
import { htmlAttrs } from '../utils'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])

const getResponsiveDaySize = (inline = false, viewport = 'md') => {
  if (viewport === 'xs') {
    return inline ? undefined : 36
  }
  return inline ? 60 : 44
}

const getResponsiveCircleSize = (inline = false, viewport = 'md') => {
  if (viewport === 'xs') {
    return 26
  }
  return inline ? 44 : 26
}

const MonthCenterContainer = styled.div({
  display: 'flex',
  justifyContent: 'center'
})

const DateInputWrapper = styled.div({
  display: 'flex',
  flexDirection: 'column'
})

const PortalPositionedContainer = styled.div({
  position: 'absolute',
  left: ({ left }) => `${left}px`,
  top: ({ top }) => `${top}px`
})

const getInitialVisibleMonth = (initialVisibleMonth, inputDate) => {
  if (initialVisibleMonth === '' || inputDate instanceof moment) {
    return null
  }
  return () => moment(initialVisibleMonth)
}

const HiddenInputFieldContainer = styled.div`
  height: ${(props) => props.height};
  width: ${(props) => props.width};
  overflow: hidden;
`

/**
 * Use DatePicker to select a date on a calendar.
 *
 * ## Usage Criteria
 *
 * - Use DatePicker to select a date on a calendar
 * - Available in 2 formats: Overlay and Inline
 *
 * ### Overlay DatePicker
 * - Use Overlay to display in a modal container
 * - Use Overlay whenever possible as it is the most accessible solution; the input form field is type-accessible and optimized for mobile customers
 * - Includes an input form field to allow manual key-in of dates
 * - Opens the modal when the input form field receives focus
 * - Width expands 100% until max-width of 340px
 *
 * ### Inline DatePicker
 * - Use Inline to display the DatePicker in a larger vieweable area; allows the customer to quickly and easily date availability
 * - Does not include an input form field
 * - Optimized for keyboard interaction and tablet touch
 * - Recommended for viewports greater than or equal to 576px
 */
const DatePicker = React.forwardRef(
  (
    {
      copy = 'en',
      id,
      date,
      dateFormat = 'DD / MM / YYYY',
      feedback,
      inline = false,
      isDayDisabled,
      label,
      onDateChange = () => {},
      hint,
      hintPosition = 'inline',
      tooltip,
      tokens,
      variant = {},
      validation,
      disabled = false,
      prevTestID = '',
      nextTestID = '',
      placeholder = dateFormat,

      initialVisibleMonth = '',
      ...rest
    },
    ref
  ) => {
    const [inputDate, setInputDate] = React.useState(date instanceof moment ? date : undefined)

    const [inputText, setInputText] = React.useState(
      date instanceof moment ? date.format(dateFormat) : ''
    )
    const dateFormatWithoutSpaces = dateFormat.replace(/\s/g, '')

    const textInputRef = React.useRef()
    const prevButtonRef = React.useRef()
    const [datePickerPosition, setDatePickerPosition] = React.useState({ left: 0, top: 0 })
    const datePickerRef = React.useRef(null)

    useSafeLayoutEffect(() => {
      const updateDatePickerPosition = () => {
        if (inline || !textInputRef?.current) return
        const { left, top } = textInputRef.current.getBoundingClientRect()
        const inputTop = top + window.scrollY
        const inputLeft = left + window.scrollX
        setDatePickerPosition({
          top: inputTop + textInputRef.current.offsetHeight,
          left: inputLeft
        })
      }

      const throttledUpdate = throttle(updateDatePickerPosition, 100, { leading: false })

      // Initial call to set the position
      throttledUpdate()

      // Register event listeners
      window.addEventListener('resize', throttledUpdate)
      window.addEventListener('scroll', updateDatePickerPosition, { capture: true })

      return () => {
        window.removeEventListener('resize', throttledUpdate)
        window.removeEventListener('scroll', updateDatePickerPosition, { capture: true })
      }
    }, [])

    const [isFocused, setIsFocused] = React.useState(false)
    const [isClickedInside, setIsClickedInside] = React.useState(false)
    const getCopy = useCopy({ dictionary, copy })

    React.useEffect(() => {
      /**
       * `date` could be passed as `null` to reset the value so explicitly
       * checking for not being `undefined`
       */
      if (!isUndefined(date) && !moment(date).isSame(inputDate)) {
        setInputDate(date)
        setInputText(date instanceof moment ? date.format(dateFormat) : '')
      }
    }, [date, inputDate, dateFormat])

    React.useEffect(() => {
      let timeoutId

      if (prevButtonRef.current && isFocused) {
        timeoutId = setTimeout(() => prevButtonRef.current.focus(), 100)
      }

      return () => clearTimeout(timeoutId)
    }, [isFocused])

    React.useEffect(() => {
      if (inputText !== '' && inputDate !== undefined) {
        textInputRef?.current?.focus()
      }
    }, [inputDate, inputText])

    const onFocusChange = ({ focused }) => {
      if (!isClickedInside) {
        setIsFocused(focused)
      }
      setIsClickedInside(false)
    }
    const handleFocus = (event) => {
      if (
        event.target.tagName === 'INPUT' &&
        !disabled &&
        inputDate === undefined &&
        inputText === ''
      ) {
        setIsFocused(true)
      }
    }
    const handleMouseDown = (event) => {
      if (!disabled) {
        if (event.target.tagName === 'INPUT') {
          setIsClickedInside(true)
          setIsFocused(true)
        } else if (event.target.tagName === 'path') {
          // needed to handle the case where the user clicks on the tooltip icon
          setIsClickedInside(true)
          event.stopPropagation()
        } else {
          event.stopPropagation()
        }
      }
    }
    const onChange = (value) => {
      setInputDate(value)
      setInputText(value.format(dateFormat))
      setIsFocused(false)
      if (onDateChange) onDateChange(value)
    }
    const onChangeInput = (value) => {
      if (
        value === '' ||
        moment(value.replace(/\s/g, ''), dateFormatWithoutSpaces, true).isValid()
      ) {
        if (value === '') {
          setInputDate(undefined) // Set inputDate to undefined if input is empty
          onDateChange?.(undefined)
        } else {
          setInputDate(moment(value, dateFormat))
          onDateChange?.(moment(value, dateFormat))
        }
        setInputText(value === '' ? '' : moment(value, dateFormatWithoutSpaces).format(dateFormat))
      } else {
        setInputText(value)
      }
    }

    const handleOnKeyPress = (event) => {
      if (event.key === 'Backspace' && inputText === '') {
        setInputDate(undefined)
        onDateChange?.(undefined)
      }
    }

    const handleValidation = (inputValidation) => {
      const momentDate = moment(inputText, dateFormat, true)
      const isValidDate = momentDate.isValid()

      if (!isValidDate && inputText !== '') {
        return 'error'
      }

      if (inputValidation === 'error' || inputValidation === 'success') {
        return inputValidation
      }

      return inputValidation
    }

    const viewport = useViewport()
    const daySize = getResponsiveDaySize(inline, viewport)
    const circleSize = getResponsiveCircleSize(inline, viewport)

    const {
      hiddenInputFieldContainerHeight,
      hiddenInputFieldContainerWidth,
      previousIcon,
      nextIcon,
      ...remainingTokens
    } = useThemeTokens('DatePicker', tokens, { ...variant, inline }, { viewport })

    const defaultFontTokens = applyTextStyles({
      fontName: remainingTokens.calendarDayDefaultFontName,
      fontWeight: remainingTokens.calendarDayDefaultFontWeight
    })

    const calendarMonthFontTokens = applyTextStyles({
      fontName: remainingTokens.calendarMonthCaptionFontName,
      fontWeight: remainingTokens.calendarMonthCaptionFontWeight
    })

    const calendarWeekFontTokens = applyTextStyles({
      fontName: remainingTokens.dayPickerWeekHeaderFontName,
      fontWeight: remainingTokens.dayPickerWeekHeaderFontWeight
    })
    const renderPrevButton = ({ onClick }) => (
      <IconButton
        onPress={() => {
          onClick()
        }}
        icon={previousIcon}
        variant={{ size: 'small' }}
        testID={prevTestID}
        ref={prevButtonRef}
      />
    )
    const renderNextButton = ({ onClick }) => (
      <IconButton
        onPress={() => {
          onClick()
        }}
        icon={nextIcon}
        variant={{ size: 'small' }}
        testID={nextTestID}
      />
    )
    return (
      <>
        {inline ? (
          <>
            <HiddenInputFieldContainer
              height={hiddenInputFieldContainerHeight}
              width={hiddenInputFieldContainerWidth}
            >
              <input ref={ref} id={id} type="text" value={inputText} readOnly />
            </HiddenInputFieldContainer>
            <CalendarContainer
              {...selectProps(rest)}
              daySize={daySize}
              validation={validation}
              remainingTokens={remainingTokens}
              calendarDayDefaultHeight={circleSize}
              calendarDayDefaultWidth={circleSize}
              calendarMonthFontTokens={calendarMonthFontTokens}
              calendarWeekFontTokens={calendarWeekFontTokens}
              defaultFontTokens={defaultFontTokens}
            >
              <DayPickerSingleDateController
                date={inputDate}
                onDateChange={onChange}
                focused={(inputDate ?? isFocused) || initialVisibleMonth !== ''}
                onFocusChange={onFocusChange}
                numberOfMonths={1}
                hideKeyboardShortcutsPanel={true}
                keepOpenOnDateSelect={false}
                daySize={daySize}
                renderNavPrevButton={renderPrevButton}
                renderNavNextButton={renderNextButton}
                isOutsideRange={isDayDisabled}
                phrases={getCopy()}
                initialVisibleMonth={getInitialVisibleMonth(initialVisibleMonth, inputDate)}
                renderMonthElement={({ month }) => (
                  <MonthCenterContainer>
                    <div>
                      {dictionary[copy]
                        ? dictionary[copy].months[month.month()]
                        : month.format('MMMM')}{' '}
                      {month.year()}
                    </div>
                  </MonthCenterContainer>
                )}
                renderWeekHeaderElement={(day) => (
                  <div>{dictionary[copy] ? dictionary[copy].weekDays[day] : day}</div>
                )}
              />
            </CalendarContainer>
          </>
        ) : (
          <DateInputWrapper onMouseDown={handleMouseDown} onFocus={handleFocus}>
            <TextInput
              copy={copy}
              feedback={feedback}
              hint={hint}
              placeholder={placeholder}
              onChange={onChangeInput}
              onKeyPress={handleOnKeyPress}
              tooltip={tooltip}
              hintPosition={hintPosition}
              label={label ?? dictionary[copy]?.roleDescription}
              value={inputText}
              validation={handleValidation(validation)}
              inactive={disabled}
              ref={textInputRef}
            >
              <Portal>
                {/* Because `SingleDatePicker` is an absolutely positioned element,
                 * putting it in a `Portal` breaks view heirarchy because it doesn't
                 * align with `TextInput`, but rather position itself with the nearest
                 * positioned ancestor.
                 *
                 * This means that the `Portal` needs to be positioned absolutely,
                 * but the `SingleDatePicker` needs to be positioned relative to the
                 * `Portal`.
                 *
                 * This is accomplished by wrapping the `SingleDatePicker` in a
                 * `PortalPositionedContainer` which positions the `SingleDatePicker`
                 * relative to the `Portal`. This container is then positioned absolutely
                 * relative to the `TextInput`.
                 *
                 * TODO: Using `floating-ui` or something like that is a more preferred and streamlined way
                 * to position popovers and overlays.
                 */}
                <PortalPositionedContainer
                  top={datePickerPosition.top}
                  left={datePickerPosition.left}
                  ref={datePickerRef}
                >
                  <CalendarContainer
                    {...selectProps(rest)}
                    daySize={daySize}
                    validation={validation}
                    remainingTokens={remainingTokens}
                    calendarDayDefaultHeight={circleSize}
                    calendarDayDefaultWidth={circleSize}
                    calendarMonthFontTokens={calendarMonthFontTokens}
                    calendarWeekFontTokens={calendarWeekFontTokens}
                    defaultFontTokens={defaultFontTokens}
                  >
                    <SingleDatePicker
                      date={inputDate}
                      disabled={disabled}
                      onDateChange={onChange}
                      focused={isFocused}
                      onFocusChange={onFocusChange}
                      numberOfMonths={1}
                      hideKeyboardShortcutsPanel={true}
                      keepOpenOnDateSelect={true}
                      daySize={daySize}
                      ref={ref}
                      renderNavPrevButton={renderPrevButton}
                      isOutsideRange={isDayDisabled}
                      phrases={getCopy()}
                      id={id}
                      renderNavNextButton={renderNextButton}
                      initialVisibleMonth={getInitialVisibleMonth(initialVisibleMonth, inputDate)}
                      renderMonthElement={({ month }) => (
                        <MonthCenterContainer>
                          <div>
                            {dictionary[copy]
                              ? dictionary[copy].months[month.month()]
                              : month.format('MMMM')}{' '}
                            {month.year()}
                          </div>
                        </MonthCenterContainer>
                      )}
                      renderWeekHeaderElement={(day) => (
                        <div>{dictionary[copy] ? dictionary[copy].weekDays[day] : day}</div>
                      )}
                    />
                  </CalendarContainer>
                </PortalPositionedContainer>
              </Portal>
            </TextInput>
          </DateInputWrapper>
        )}
      </>
    )
  }
)

DatePicker.displayName = 'DatePicker'

DatePicker.propTypes = {
  ...selectedSystemPropTypes,
  tokens: getTokensPropType('DatePicker'),
  /**
   * A unique identifier.
   */
  id: PropTypes.string.isRequired,
  /**
   *  Whether the English or French copy will be used (e.g. for accessibility labels).
   */
  copy: PropTypes.oneOf(['en', 'fr']),
  /**
   * A Moment instance representing the currently selected date, i.e. `moment()`
   */
  date: momentPropTypes.momentObj,
  /**
   * A Moment instance representing the currently selected date, i.e. `moment()`
   */
  dateFormat: PropTypes.string,
  /**
   * Optional date format for the date input. If not set, the default value will be `DD / MM / YYYY`
   */
  feedback: PropTypes.string,
  /**
   * Event triggered every time a new date is clicked on
   * @param {Moment} date The new date that was selected
   */
  onDateChange: PropTypes.func,
  /**
   * A function determining whether a given date should be disabled
   * @param {Moment} date The date to optionally disable
   * @returns {bool}
   */
  isDayDisabled: PropTypes.func,
  /**
   * The field label to be displayed above the calendar
   */
  label: PropTypes.string,
  /**
   * A flag determining if the calendar picker is standalone or an input with overlay
   */
  inline: PropTypes.bool,
  /**
   * A short description of the expected input.
   */
  hint: PropTypes.string,
  /**
   * Position of the hint relative to label. Use `below` to display a larger hint below the label.
   */
  hintPosition: PropTypes.oneOf(['inline', 'below']),

  /**
   * Content of an optional `Tooltip`. If set, a tooltip button will be shown next to the label.
   */
  tooltip: PropTypes.string,
  /**
   * Use to visually mark an input as valid or invalid.
   */
  validation: PropTypes.oneOf(['error', 'success']),
  /**
   * Disable the input which will not open the calendar picker
   */
  disabled: PropTypes.bool,
  /**
   * A unique identifier for the previous icon button located on the datepicker.
   * This is for automation testing purposes.
   * Will be added as a `data-testid-prev` attribute for example.
   */
  prevTestID: PropTypes.string,
  /**
   * A unique identifier for the next icon button located on the datepicker.
   * This is for automation testing purposes.
   * Will be added as a `data-testid-next` attribute for example.
   */
  nextTestID: PropTypes.string,
  /**
   * Optional placeholder for the date input. If not set, the default value will be equal to the `dateFormat` prop
   */
  placeholder: PropTypes.string,
  /**
   * The initial month to display when the calendar is opened. Expects a string in the format `YYYY-MM`
   */
  initialVisibleMonth: PropTypes.string
}

export default DatePicker
