import {useControlledState} from '@react-stately/utils';
import {HTMLAttributes, useCallback, useState} from 'react';
import {
  CalendarDate,
  DateValue,
  endOfMonth,
  getLocalTimeZone,
  isSameDay,
  isSameMonth,
  now,
  startOfMonth,
  toCalendarDate,
  toZoned,
  ZonedDateTime
} from '@internationalized/date';
import {BaseDatePickerState, DatePickerValueProps} from '../date-picker/use-date-picker-state';
import {DateRangeValue} from './date-range-value';
import {useBaseDatePickerState} from '../use-base-date-picker-state';

const Now = now(getLocalTimeZone());

export interface IsPlaceholderValue {
  start: boolean;
  end: boolean;
}

export type DateRangePickerState = BaseDatePickerState<
  DateRangeValue,
  IsPlaceholderValue
>;

export function useDateRangePickerState(
  props: DatePickerValueProps<Partial<DateRangeValue>>
): DateRangePickerState {
  const [isPlaceholder, setIsPlaceholder] = useState<IsPlaceholderValue>({
    start: (!props.value || !props.value.start) && !props.defaultValue?.start,
    end: (!props.value || !props.value.end) && !props.defaultValue?.end,
  });

  const [selectedValue, setSelectedStateValue] = useControlledState(
    props.value ? completeRange(props.value) : undefined,
    !props.value ? completeRange(props.defaultValue) : undefined,
    props.onChange
  );

  const {
    min,
    max,
    granularity,
    timeZone,
    calendarIsOpen,
    setCalendarIsOpen,
    closeDialogOnSelection,
  } = useBaseDatePickerState(selectedValue.start, props);

  const [anchorDate, setAnchorDate] = useState<CalendarDate | null>(null);
  const [isHighlighting, setIsHighlighting] = useState(false);
  const [highlightedRange, setHighlightedRange] =
    useState<DateRangeValue>(selectedValue);
  const [calendarDates, setCalendarDates] = useState<CalendarDate[]>(() => {
    return rangeToCalendarDates(selectedValue, max);
  });

  const prepareValue = useCallback(
    (newDate: DateValue, oldDate?: ZonedDateTime) => {
      if (min && newDate.compare(min) < 0) {
        newDate = min;
      } else if (max && newDate.compare(max) > 0) {
        newDate = max;
      }

      return oldDate
        ? oldDate.set(newDate)
        : toZoned(newDate, getLocalTimeZone());
    },
    [min, max]
  );

  const setSelectedValue = useCallback(
    (newValue: DateRangeValue) => {
      const value = {
        start: prepareValue(newValue.start),
        end: prepareValue(newValue.end),
        preset: newValue.preset,
      };
      setSelectedStateValue(value);
      setHighlightedRange(value);
      setCalendarDates(rangeToCalendarDates(value, max));
      setIsPlaceholder({start: false, end: false});
    },
    [setSelectedStateValue, prepareValue, max]
  );

  const dayIsActive = useCallback(
    (day: CalendarDate) => {
      return (
        (!isPlaceholder.start && isSameDay(day, highlightedRange.start)) ||
        (!isPlaceholder.end && isSameDay(day, highlightedRange.end))
      );
    },
    [highlightedRange, isPlaceholder]
  );

  const dayIsHighlighted = useCallback(
    (day: CalendarDate) => {
      return (
        (isHighlighting || (!isPlaceholder.start && !isPlaceholder.end)) &&
        day.compare(highlightedRange.start) >= 0 &&
        day.compare(highlightedRange.end) <= 0
      );
    },
    [highlightedRange, isPlaceholder, isHighlighting]
  );

  const dayIsRangeStart = useCallback(
    (day: CalendarDate) => isSameDay(day, highlightedRange.start),
    [highlightedRange]
  );

  const dayIsRangeEnd = useCallback(
    (day: CalendarDate) => isSameDay(day, highlightedRange.end),
    [highlightedRange]
  );

  const getCellProps = useCallback(
    (date: CalendarDate, isSameMonth: boolean): HTMLAttributes<HTMLElement> => {
      return {
        onPointerEnter: () => {
          if (isHighlighting && isSameMonth) {
            setHighlightedRange(makeRange({start: anchorDate!, end: date}));
          }
        },
        onClick: () => {
          if (!isHighlighting) {
            setIsHighlighting(true);
            setAnchorDate(date);
            setHighlightedRange(makeRange({start: date, end: date}));
          } else {
            const finalRange = makeRange({start: anchorDate!, end: date});
            setIsHighlighting(false);
            setAnchorDate(null);
            setSelectedValue?.(finalRange);
            if (closeDialogOnSelection) {
              setCalendarIsOpen?.(false);
            }
          }
        },
      };
    },
    [
      anchorDate,
      isHighlighting,
      setSelectedValue,
      setCalendarIsOpen,
      closeDialogOnSelection,
    ]
  );

  return {
    selectedValue,
    setSelectedValue,
    calendarIsOpen,
    setCalendarIsOpen,
    dayIsActive,
    dayIsHighlighted,
    dayIsRangeStart,
    dayIsRangeEnd,
    getCellProps,
    calendarDates,
    setIsPlaceholder,
    isPlaceholder,
    setCalendarDates,
    min,
    max,
    granularity,
    timeZone,
    closeDialogOnSelection,
  };
}

function rangeToCalendarDates(
  range: DateRangeValue,
  max?: DateValue
): CalendarDate[] {
  let start = toCalendarDate(startOfMonth(range.start));
  let end = toCalendarDate(endOfMonth(range.end));

  // make sure we don't show the same month twice
  if (isSameMonth(start, end)) {
    end = endOfMonth(end.add({months: 1}));
  }

  // if next month is disabled, show previous instead
  if (max && end.compare(max) > 0) {
    end = start;
    start = startOfMonth(start.subtract({months: 1}));
  }
  return [start, end];
}

interface MakeRangeProps {
  start: DateValue;
  end: DateValue;
}
function makeRange(props: MakeRangeProps): DateRangeValue {
  const start = toZoned(props.start, getLocalTimeZone());
  const end = toZoned(props.end, getLocalTimeZone());
  if (start.compare(end) > 0) {
    return {start: end, end: start};
  }
  return {start, end};
}

function completeRange(
  range: Partial<DateRangeValue> | null | undefined
): DateRangeValue {
  if (range?.start && range?.end) {
    return range as DateRangeValue;
  } else if (!range?.start && range?.end) {
    range.start = range.end.subtract({months: 1});
    return range as DateRangeValue;
  } else if (!range?.end && range?.start) {
    range.end = range.start.add({months: 1});
    return range as DateRangeValue;
  }
  return {start: Now, end: Now.add({months: 1})};
}
