/*
 * SonarQube
 * Copyright (C) 2009-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

import styled from '@emotion/styled';
import classNames from 'classnames';
import {
  format,
  getYear,
  isSameMonth,
  isSameYear,
  setMonth,
  setYear,
  startOfMonth,
} from 'date-fns';
import { range } from 'lodash';
import * as React from 'react';
import {
  ActiveModifiers,
  CaptionProps,
  Matcher,
  DayPicker as OriginalDayPicker,
  useNavigation as useCalendarNavigation,
  useDayPicker,
} from 'react-day-picker';
import tw from 'twin.macro';
import { PopupPlacement, PopupZLevel, themeBorder, themeColor, themeContrast } from '../helpers';
import { InputSizeKeys } from '../types/theme';
import EscKeydownHandler from './EscKeydownHandler';
import { FocusOutHandler } from './FocusOutHandler';
import { InputField } from './InputField';
import { InputSelect } from './InputSelect';
import { InteractiveIcon } from './InteractiveIcon';
import { OutsideClickHandler } from './OutsideClickHandler';
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from './icons';
import { CloseIcon } from './icons/CloseIcon';
import { Popup } from './popups';

// When no minDate is given, year dropdown will show year options up to PAST_MAX_YEARS in the past
const YEARS_TO_DISPLAY = 10;
const MONTHS_IN_A_YEAR = 12;

interface Props {
  alignRight?: boolean;
  ariaNextMonthLabel: string;
  ariaPreviousMonthLabel: string;
  className?: string;
  clearButtonLabel: string;
  currentMonth?: Date;
  highlightFrom?: Date;
  highlightTo?: Date;
  id?: string;
  inputClassName?: string;
  inputRef?: React.Ref<HTMLInputElement>;
  maxDate?: Date;
  minDate?: Date;
  name?: string;
  onChange: (date: Date | undefined) => void;
  placeholder: string;
  showClearButton?: boolean;
  size?: InputSizeKeys;
  value?: Date;
  valueFormatter?: (date?: Date) => string;
}

interface State {
  currentMonth: Date;
  lastHovered?: Date;
  open: boolean;
}

function formatWeekdayName(date: Date) {
  return format(date, 'EEE'); // Short weekday name, e.g. Wed, Thu
}

export class DatePicker extends React.PureComponent<Props, State> {
  constructor(props: Props) {
    super(props);

    this.state = { currentMonth: props.value ?? props.currentMonth ?? new Date(), open: false };
  }

  handleResetClick = () => {
    this.closeCalendar();
    this.props.onChange(undefined);
  };

  openCalendar = () => {
    this.setState({
      currentMonth: this.props.value ?? this.props.currentMonth ?? new Date(),
      lastHovered: undefined,
      open: true,
    });
  };

  closeCalendar = () => {
    this.setState({ open: false });
  };

  handleDayClick = (day: Date, modifiers: ActiveModifiers) => {
    if (!modifiers.disabled) {
      this.closeCalendar();
      this.props.onChange(day);
    }
  };

  handleDayMouseEnter = (day: Date, modifiers: ActiveModifiers) => {
    this.setState({ lastHovered: modifiers.disabled ? undefined : day });
  };

  render() {
    const {
      alignRight,
      ariaNextMonthLabel,
      ariaPreviousMonthLabel,
      clearButtonLabel,
      highlightFrom,
      highlightTo,
      inputRef,
      minDate,
      maxDate = new Date(),
      value: selectedDay,
      name,
      className,
      inputClassName,
      id,
      placeholder,
      showClearButton = true,
      valueFormatter = (date?: Date) => (date ? format(date, 'MMM d, yyyy') : ''),
      size,
    } = this.props;
    const { lastHovered, currentMonth, open } = this.state;

    // Infer start and end dropdown year from min/max dates, if set
    const fromYear = minDate ? minDate.getFullYear() : new Date().getFullYear() - YEARS_TO_DISPLAY;
    const toYear = maxDate.getFullYear();

    const selectedDays = selectedDay ? [selectedDay] : [];
    let highlighted: Matcher = false;
    const lastHoveredOrValue = lastHovered ?? selectedDay;

    if (highlightFrom && lastHoveredOrValue) {
      highlighted = { from: highlightFrom, to: lastHoveredOrValue };
      selectedDays.push(highlightFrom);
    }

    if (highlightTo && lastHoveredOrValue) {
      highlighted = { from: lastHoveredOrValue, to: highlightTo };
      selectedDays.push(highlightTo);
    }

    return (
      <FocusOutHandler onFocusOut={this.closeCalendar}>
        <OutsideClickHandler onClickOutside={this.closeCalendar}>
          <EscKeydownHandler onKeydown={this.closeCalendar}>
            <Popup
              allowResizing
              className="sw-overflow-visible" //Necessary for the month & year selectors
              overlay={
                open ? (
                  <div className={classNames('sw-p-2')}>
                    <DayPicker
                      captionLayout="dropdown-buttons"
                      className="sw-body-sm"
                      components={{
                        Caption: getCustomCalendarNavigation({
                          ariaNextMonthLabel,
                          ariaPreviousMonthLabel,
                        }),
                      }}
                      disabled={{ after: maxDate, before: minDate }}
                      formatters={{
                        formatWeekdayName,
                      }}
                      fromYear={fromYear}
                      mode="default"
                      modifiers={{ highlighted }}
                      modifiersClassNames={{ highlighted: 'rdp-highlighted' }}
                      month={currentMonth}
                      onDayClick={this.handleDayClick}
                      onDayMouseEnter={this.handleDayMouseEnter}
                      onMonthChange={(currentMonth) => {
                        this.setState({ currentMonth });
                      }}
                      selected={selectedDays}
                      toYear={toYear}
                      weekStartsOn={1}
                    />
                  </div>
                ) : null
              }
              placement={alignRight ? PopupPlacement.BottomRight : PopupPlacement.BottomLeft}
              zLevel={PopupZLevel.Global}
            >
              <span
                className={classNames('sw-relative sw-inline-block sw-cursor-pointer', className)}
              >
                <StyledInputField
                  aria-label={placeholder}
                  className={classNames(inputClassName, {
                    'is-filled': selectedDay !== undefined && showClearButton,
                  })}
                  id={id}
                  name={name}
                  onClick={this.openCalendar}
                  onFocus={this.openCalendar}
                  placeholder={placeholder}
                  readOnly
                  ref={inputRef}
                  size={size}
                  title={valueFormatter(selectedDay)}
                  type="text"
                  value={valueFormatter(selectedDay)}
                />

                <StyledCalendarIcon fill="datePickerIcon" />

                {selectedDay !== undefined && showClearButton && (
                  <StyledInteractiveIcon
                    Icon={CloseIcon}
                    aria-label={clearButtonLabel}
                    onClick={this.handleResetClick}
                    size="small"
                  />
                )}
              </span>
            </Popup>
          </EscKeydownHandler>
        </OutsideClickHandler>
      </FocusOutHandler>
    );
  }
}

const StyledCalendarIcon = styled(CalendarIcon)`
  ${tw`sw-absolute`};
  ${tw`sw-top-[0.625rem] sw-left-2`};
`;

const StyledInteractiveIcon = styled(InteractiveIcon)`
  ${tw`sw-absolute`};
  ${tw`sw-top-[0.375rem] sw-right-[0.375rem]`};
`;

const StyledInputField = styled(InputField)`
  input[type='text']& {
    ${tw`sw-pl-8`};
    ${tw`sw-cursor-pointer`};

    &.is-filled {
      ${tw`sw-pr-8`};
    }
  }
`;

const DayPicker = styled(OriginalDayPicker)`
  --rdp-cell-size: auto;
  /* Ensures the month/year dropdowns do not move on click, but rdp outline is not shown */
  --rdp-outline: 2px solid transparent;
  --rdp-outline-selected: 2px solid transparent;

  margin: 0;

  .rdp-head {
    color: ${themeContrast('datePicker')};
  }

  .rdp-day {
    height: 28px;
    width: 33px;
    border-radius: 0;
    color: ${themeContrast('datePickerDefault')};
  }

  /* Default modifiers */

  .rdp-day_disabled {
    cursor: not-allowed;
    background: ${themeColor('datePickerDisabled')};
    color: ${themeContrast('datePickerDisabled')};
  }

  .rdp-day:hover:not(.rdp-day_outside):not(.rdp-day_disabled):not(.rdp-day_selected) {
    background: ${themeColor('datePickerHover')};
    color: ${themeContrast('datePickerHover')};
  }

  .rdp-day:focus-visible {
    outline: ${themeBorder('focus', 'inputFocus')};
    background: inherit;
    z-index: 1;
  }

  .rdp-day.rdp-highlighted:not(.rdp-day_selected) {
    background: ${themeColor('datePickerRange')};
    color: ${themeContrast('datePickerRange')};
  }

  .rdp-day_selected,
  .rdp-day_selected:focus-visible {
    background: ${themeColor('datePickerSelected')};
    color: ${themeContrast('datePickerSelected')};
  }
`;

function getCustomCalendarNavigation({
  ariaNextMonthLabel,
  ariaPreviousMonthLabel,
}: {
  ariaNextMonthLabel: string;
  ariaPreviousMonthLabel: string;
}) {
  return function CalendarNavigation(props: CaptionProps) {
    const { displayMonth } = props;
    const { fromYear, toYear } = useDayPicker();
    const { goToMonth, nextMonth, previousMonth } = useCalendarNavigation();

    const baseDate = startOfMonth(displayMonth); // reference date

    const months = range(MONTHS_IN_A_YEAR).map((month) => {
      const monthValue = setMonth(baseDate, month);

      return {
        label: format(monthValue, 'MMM'),
        value: monthValue,
      };
    });

    const startYear = fromYear ?? getYear(Date.now()) - YEARS_TO_DISPLAY;

    const years = range(startYear, toYear ? toYear + 1 : undefined).map((year) => {
      const yearValue = setYear(baseDate, year);

      return {
        label: String(year),
        value: yearValue,
      };
    });

    return (
      <nav className="sw-flex sw-items-center sw-justify-between sw-py-1">
        <InteractiveIcon
          Icon={ChevronLeftIcon}
          aria-label={ariaPreviousMonthLabel}
          className="sw-mr-2"
          onClick={() => {
            if (previousMonth) {
              goToMonth(previousMonth);
            }
          }}
          size="small"
        />

        <span data-testid="month-select">
          <InputSelect
            isClearable={false}
            onChange={(value) => {
              if (value) {
                goToMonth(value.value);
              }
            }}
            options={months}
            size="full"
            value={months.find((m) => isSameMonth(m.value, displayMonth))}
          />
        </span>

        <span data-testid="year-select">
          <InputSelect
            className="sw-ml-1"
            data-testid="year-select"
            isClearable={false}
            onChange={(value) => {
              if (value) {
                goToMonth(value.value);
              }
            }}
            options={years}
            size="full"
            value={years.find((y) => isSameYear(y.value, displayMonth))}
          />
        </span>

        <InteractiveIcon
          Icon={ChevronRightIcon}
          aria-label={ariaNextMonthLabel}
          className="sw-ml-2"
          onClick={() => {
            if (nextMonth) {
              goToMonth(nextMonth);
            }
          }}
          size="small"
        />
      </nav>
    );
  };
}
