import { clsx } from 'clsx';
import { createRef, PureComponent, KeyboardEvent } from 'react';

import {
  Size,
  MonthFormat,
  Position,
  Breakpoint,
  SizeSmall,
  SizeMedium,
  SizeLarge,
} from '../common';
import { isWithinRange, moveToWithinRange } from '../common/dateUtils';
import ResponsivePanel from '../common/responsivePanel';
import { WithInputAttributesProps, withInputAttributes } from '../inputs/contexts';
import { OverlayIdContext, OverlayIdProvider } from '../provider/overlay/OverlayIdProvider';
import DateTrigger from './dateTrigger';
import DayCalendar from './dayCalendar';
import { getStartOfDay } from './getStartOfDay';
import MonthCalendar from './monthCalendar';
import YearCalendar from './yearCalendar';

export interface DateLookupProps {
  id?: string;
  value: Date | null;
  min?: Date | null;
  max?: Date | null;
  size?: SizeSmall | SizeMedium | SizeLarge;
  placeholder?: string;
  label?: string;
  'aria-labelledby'?: string;
  monthFormat?: `${MonthFormat}`;
  disabled?: boolean;
  clearable?: boolean;
  onChange: (date: Date | null) => void;
  onFocus?: () => void;
  onBlur?: () => void;
}

type DateLookupPropsWithInputAttributes = DateLookupProps & Partial<WithInputAttributesProps>;

interface DateLookupState {
  selectedDate: Date | null;
  originalDate: Date | null;
  min: Date | null;
  max: Date | null;
  viewMonth: number;
  viewYear: number;
  open: boolean;
  mode: 'day' | 'month' | 'year';
  isMobile: boolean;
}

class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateLookupState> {
  declare props: DateLookupPropsWithInputAttributes &
    Required<Pick<DateLookupPropsWithInputAttributes, keyof typeof DateLookup.defaultProps>>;

  static defaultProps = {
    value: null,
    min: null,
    max: null,
    size: Size.MEDIUM,
    placeholder: '',
    label: '',
    monthFormat: MonthFormat.LONG,
    disabled: false,
    clearable: false,
  } satisfies Partial<DateLookupPropsWithInputAttributes>;

  element = createRef<HTMLDivElement>();
  dropdown = createRef<HTMLDivElement>();

  constructor(props: DateLookup['props']) {
    super(props);
    this.state = {
      selectedDate: getStartOfDay(props.value),
      originalDate: null,
      min: getStartOfDay(props.min),
      max: getStartOfDay(props.max),
      viewMonth: (props.value || new Date()).getMonth(),
      viewYear: (props.value || new Date()).getFullYear(),
      open: false,
      mode: 'day',
      isMobile: false,
    };
  }

  static getDerivedStateFromProps(props: DateLookup['props'], state: DateLookupState) {
    const propsSelected = getStartOfDay(props.value);
    const propsMin = getStartOfDay(props.min);
    const propsMax = getStartOfDay(props.max);
    const hasSelectedChanged = state.selectedDate?.getTime() !== propsSelected?.getTime();
    const hasMinChanged = state.min?.getTime() !== propsMin?.getTime();
    const hasMaxChanged = state.max?.getTime() !== propsMax?.getTime();
    if (hasSelectedChanged || hasMinChanged || hasMaxChanged) {
      const selectedDate = hasSelectedChanged ? propsSelected : state.selectedDate;
      const min = hasMinChanged ? propsMin : state.min;
      const max = hasMaxChanged ? propsMax : state.max;
      if (selectedDate && !isWithinRange(selectedDate, min, max)) {
        props.onChange(moveToWithinRange(selectedDate, min, max));
        return null;
      }
      const viewDateThatIsWithinRange: Date =
        selectedDate || ((min || max) && moveToWithinRange(new Date(), min, max)) || new Date();
      const viewMonth = viewDateThatIsWithinRange.getMonth();
      const viewYear = viewDateThatIsWithinRange.getFullYear();
      return { selectedDate, min, max, viewMonth, viewYear };
    }
    return null;
  }

  componentDidUpdate(previousProps: DateLookupPropsWithInputAttributes) {
    if (this.props.value?.getTime() !== previousProps.value?.getTime() && this.state.open) {
      this.focusOn('.active');
    }
    const mediaQuery = window.matchMedia(`(max-width: ${Breakpoint.SMALL}px)`);
    this.setState({ isMobile: mediaQuery.matches });
  }

  componentWillUnmount() {
    // Prevents memory leak by cleaning state.
    this.setState = () => {};
  }

  open = () => {
    const { onFocus } = this.props;

    this.setState({ open: true, mode: 'day' });
    if (onFocus) {
      onFocus();
    }
  };

  discard = () => {
    const { originalDate } = this.state;
    if (originalDate !== null) {
      this.props.onChange(originalDate);
    }
    this.close();
  };

  close = () => {
    const { onBlur } = this.props;
    this.setState({ open: false, originalDate: null });
    if (onBlur) {
      onBlur();
    }
  };

  handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
    const { open, originalDate } = this.state;
    switch (event.key) {
      case 'ArrowLeft':
        if (open) {
          this.adjustDate(-1, -1, -1);
        } else {
          this.open();
        }
        event.preventDefault();
        break;
      case 'ArrowUp':
        if (open) {
          this.adjustDate(-7, -4, -4);
        } else {
          this.open();
        }
        event.preventDefault();
        break;
      case 'ArrowRight':
        if (open) {
          this.adjustDate(1, 1, 1);
        } else {
          this.open();
        }
        event.preventDefault();
        break;
      case 'ArrowDown':
        if (open) {
          this.adjustDate(7, 4, 4);
        } else {
          this.open();
        }
        event.preventDefault();
        break;
      case 'Escape':
        if (originalDate !== null) {
          this.props.onChange(originalDate);
        }
        this.close();
        event.preventDefault();
        break;
      default:
        break;
    }
  };

  adjustDate = (daysToAdd: number, monthsToAdd: number, yearsToAdd: number) => {
    const { selectedDate, min, max, mode, originalDate } = this.state;
    if (originalDate === null) {
      this.setState({ originalDate: selectedDate });
    }
    let date: Date | null;
    if (selectedDate) {
      date = new Date(
        mode === 'year' ? selectedDate.getFullYear() + yearsToAdd : selectedDate.getFullYear(),
        mode === 'month' ? selectedDate.getMonth() + monthsToAdd : selectedDate.getMonth(),
        mode === 'day' ? selectedDate.getDate() + daysToAdd : selectedDate.getDate(),
      );
    } else {
      date = getStartOfDay(new Date());
    }
    if (date) {
      date = moveToWithinRange(date, min, max);
    }
    if (date?.getTime() !== selectedDate?.getTime()) {
      this.props.onChange(date);
    }
  };

  focusOn = (preferredElement: string, fallbackElement?: string) => {
    const element = this.element.current?.querySelector(preferredElement) as HTMLElement | null;
    if (element) {
      element.focus();
    } else if (fallbackElement) {
      this.focusOn(fallbackElement);
    }
  };

  switchMode = (mode: 'day' | 'month' | 'year') => {
    this.setState({ mode }, () => {
      this.focusOn('.active', '.today');
    });
  };

  switchToDays = () => this.switchMode('day');

  switchToMonths = () => this.switchMode('month');

  switchToYears = () => this.switchMode('year');

  handleSelectedDateUpdate = (selectedDate: Date) => {
    this.setState({ selectedDate }, () => {
      this.props.onChange(selectedDate);
      this.close();
      this.focusOn('.btn');
    });
  };

  handleViewDateUpdate = ({ month = this.state.viewMonth, year = this.state.viewYear }) => {
    this.setState({ viewMonth: month, viewYear: year });
  };

  getCalendar = () => {
    const { selectedDate, min, max, viewMonth, viewYear, mode, isMobile } = this.state;
    const { placeholder, monthFormat } = this.props;
    return (
      <div className={clsx({ 'p-a-1': !isMobile })}>
        {mode === 'day' && (
          <DayCalendar
            selectedDate={selectedDate}
            min={min}
            max={max}
            viewMonth={viewMonth}
            viewYear={viewYear}
            monthFormat={monthFormat}
            onSelect={this.handleSelectedDateUpdate}
            onLabelClick={this.switchToYears}
            onViewDateUpdate={this.handleViewDateUpdate}
          />
        )}
        {mode === 'month' && (
          <MonthCalendar
            selectedDate={selectedDate}
            min={min}
            max={max}
            viewYear={viewYear}
            placeholder={placeholder}
            onSelect={this.switchToDays}
            onLabelClick={this.switchToYears}
            onViewDateUpdate={this.handleViewDateUpdate}
          />
        )}
        {mode === 'year' && (
          <YearCalendar
            selectedDate={selectedDate}
            min={min}
            max={max}
            viewYear={viewYear}
            placeholder={placeholder}
            onSelect={this.switchToMonths}
            onViewDateUpdate={this.handleViewDateUpdate}
          />
        )}
      </div>
    );
  };

  handleClear = () => {
    this.props.onChange(null);
    this.focusOn('.np-date-trigger');
  };

  render() {
    const { selectedDate, open } = this.state;

    const {
      inputAttributes,
      id: idProp,
      'aria-labelledby': ariaLabelledByProp,
      size,
      placeholder,
      label,
      monthFormat,
      disabled,
      clearable,
      value,
    } = this.props;
    const id = idProp ?? inputAttributes?.id;
    const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes?.['aria-labelledby'];

    return (
      <div
        ref={this.element}
        {...inputAttributes}
        id={id}
        className="input-group"
        onKeyDown={this.handleKeyDown}
      >
        <OverlayIdProvider open={open}>
          <DateTrigger
            ariaLabelledBy={ariaLabelledBy}
            selectedDate={selectedDate}
            size={size}
            placeholder={placeholder}
            label={label}
            monthFormat={monthFormat}
            disabled={disabled || false}
            onClick={this.open}
            onClear={!disabled && clearable && value ? this.handleClear : undefined}
          />
          <ResponsivePanel
            anchorRef={this.element}
            open={open}
            className="tw-date-lookup-menu"
            position={Position.BOTTOM}
            onClose={this.discard}
          >
            {this.getCalendar()}
          </ResponsivePanel>
        </OverlayIdProvider>
      </div>
    );
  }
}

export const DateLookupWithoutInputAttributes = DateLookup;

export default withInputAttributes(
  DateLookup as React.ComponentType<DateLookupPropsWithInputAttributes>,
  { nonLabelable: true },
);
