import 'react-day-picker/dist/style.css';

import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';

import { DayPicker, ClassNames, CustomComponents } from 'react-day-picker';

import { bem } from 'utils/bem';

import { THEME } from '../../constants';
import { usePosition, useValue } from './hooks';
import { Footer, Caption, Input, Sidebar } from './components';
import {
  DatePickerInputRef,
  DateRange,
  DATE_PICKER_TYPE,
  DATE_PICKER_VARIANT,
  DATE_PICKER_WEEKDAY,
  RangeDatePickerProps,
  SingleDatePickerProps,
} from './types';
import { DEFAULT_PREFERENCES, ONE, TWO } from './constants';
import {
  getInitialMonth,
  getInitialValue,
  getInputMask,
  getInputPlaceholder,
  parseDate,
  parseDateRange,
  stringifyDateRange,
} from './helpers';

import styles from './date-picker.scss';

export const className = bem(styles, 'date-picker');

export const DatePicker: React.FC<SingleDatePickerProps | RangeDatePickerProps> = (props) => {
  const {
    theme = THEME.light,
    type,
    variant = DATE_PICKER_VARIANT.Default,
    preferences = DEFAULT_PREFERENCES,
    weekStartDay = DATE_PICKER_WEEKDAY.Monday,
    isRequired = false,
    isDisabled = false,
    disabledDates,
    onApply,
  } = props;

  const initialValue = getInitialValue(props);

  const triggerRef = useRef<DatePickerInputRef>(null);
  const calendarRef = useRef<HTMLDivElement>(null);

  const [month, setMonth] = useState<Date>(getInitialMonth(props));
  const [isOpened, setIsOpened] = useState(false);

  const value = useValue(initialValue, preferences, isRequired, type, disabledDates);
  const position = usePosition(isOpened, calendarRef, triggerRef.current?.containerRef);

  const classNames = useMemo<ClassNames>(
    () => ({
      root: className('container'),
      months: className('months'),
      month: className('month'),
      head_cell: className('day'),
      button: className('button'),
      day: className('date'),
      day_today: className('date--isToday'),
      day_range_middle: className('date--isMiddle'),
      day_range_start: className('date--isStart'),
      day_range_end: className('date--isEnd'),
      day_selected: className('date--isSelected'),
      day_disabled: className('date--isDisabled'),
      caption: className('caption'),
      caption_start: className('caption--isStart'),
      caption_end: className('caption--isEnd'),
    }),
    [],
  );

  const components = useMemo<CustomComponents>(
    () => ({
      Caption: (captionProps) => (
        <Caption
          {...captionProps}
          type={type}
          disabledDates={disabledDates}
          switchMonth={setMonth}
        />
      ),
    }),
    [disabledDates, type],
  );

  const openCalendar = useCallback(() => {
    if (isDisabled) return;

    setIsOpened(true);
  }, [isDisabled]);

  const closeCalendar = useCallback(() => setIsOpened(false), []);

  const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
    (event) => value.update(event.target.value),
    [value],
  );

  const handleApply = useCallback(() => {
    const inputElement = triggerRef.current?.inputRef.current;
    const isValueValid = value.validate();

    if (isValueValid) {
      switch (type) {
        case DATE_PICKER_TYPE.Single:
          const parsedDate = parseDate(preferences, value.data);
          onApply(parsedDate);
          break;

        case DATE_PICKER_TYPE.Range:
          const parsedDateRange = parseDateRange(preferences, value.data);
          onApply(parsedDateRange);
          break;
      }

      inputElement?.blur();
      closeCalendar();
    }
  }, [closeCalendar, onApply, preferences, type, value]);

  const handleCancel = useCallback(() => {
    value.update(initialValue);
    closeCalendar();
  }, [closeCalendar, initialValue, value]);

  const handleReset = useCallback(() => {
    value.clear();
  }, [value]);

  const sidebar = useMemo(() => {
    switch (type) {
      case DATE_PICKER_TYPE.Range:
        const { presets } = props;
        const { data, update, clear } = value;

        return presets ? (
          <div className={className('sidebar')}>
            <Sidebar
              value={data}
              presets={presets}
              preferences={preferences}
              updateValue={update}
              clearValue={clear}
            />
          </div>
        ) : null;

      default:
        return null;
    }
  }, [preferences, props, type, value]);

  const calendar = useMemo(() => {
    switch (type) {
      case DATE_PICKER_TYPE.Single:
        return (
          <DayPicker
            classNames={classNames}
            mode="single"
            month={month}
            weekStartsOn={weekStartDay}
            selected={parseDate(preferences, value.data, disabledDates)}
            onSelect={(date: Date | undefined): void => {
              onApply(date);
              closeCalendar();
            }}
            numberOfMonths={ONE}
            components={components}
            disabled={disabledDates}
          />
        );

      case DATE_PICKER_TYPE.Range:
        return (
          <DayPicker
            classNames={classNames}
            mode="range"
            month={month}
            weekStartsOn={weekStartDay}
            selected={parseDateRange(preferences, value.data, disabledDates)}
            onSelect={(range: DateRange | undefined): void => {
              const stringifiedValue = stringifyDateRange(preferences, range);
              value.update(stringifiedValue);
            }}
            numberOfMonths={TWO}
            components={components}
            disabled={disabledDates}
          />
        );
    }
  }, [classNames, closeCalendar, components, disabledDates, month, onApply, preferences, type, value, weekStartDay]);

  const footer = useMemo(() => {
    switch (type) {
      case DATE_PICKER_TYPE.Single:
        return null;
      case DATE_PICKER_TYPE.Range:
        return (
          <div className={className('footer')}>
            <Footer
              onApply={handleApply}
              onCancel={handleCancel}
              onClear={handleReset}
            />
          </div>
        );
    }
  }, [handleApply, handleCancel, handleReset, type]);

  useLayoutEffect(() => {
    if (!isOpened) return;

    const initialMonth = getInitialMonth(props);

    setMonth(initialMonth);
  }, [isOpened, props]);

  useEffect(() => {
    const containerElement = triggerRef.current?.containerRef.current;

    const handleEnterPress = (event: KeyboardEvent): void => {
      if (event.code === 'Enter') {
        handleApply();
      }
    };

    containerElement?.addEventListener('keydown', handleEnterPress, true);

    return () => {
      containerElement?.removeEventListener('keydown', handleEnterPress, true);
    };
  }, [handleApply]);

  useEffect(() => {
    const handleCloseCalendar = (event: MouseEvent): void => {
      const isOutsideTrigger = !triggerRef.current?.containerRef.current?.contains(event.target as Node);
      const isOutsideCalendar = !calendarRef.current?.contains(event.target as Node);

      if (isOutsideTrigger && isOutsideCalendar) {
        value.update(initialValue);
        closeCalendar();
      }
    };

    document.addEventListener('mousedown', handleCloseCalendar, true);

    return () => {
      document.removeEventListener('mousedown', handleCloseCalendar, true);
    };
  }, [closeCalendar, initialValue, value]);

  return (
    <div className={className(null, { [theme]: true })}>
      <Input
        ref={triggerRef}
        mask={getInputMask(preferences.mask, type)}
        placeholder={getInputPlaceholder(preferences.placeholder, type)}
        variant={variant}
        value={value.data}
        isDisabled={isDisabled}
        hasError={!!value.error}
        openCalendar={openCalendar}
        onChange={handleInputChange}
      />

      {isOpened ? (
        <div
          ref={calendarRef}
          style={position}
          className={className('calendar')}
        >
          {sidebar}

          <div
            className={className('body', {
              isSingle: type === DATE_PICKER_TYPE.Single,
              isRange: type === DATE_PICKER_TYPE.Range,
            })}
          >
            {!!value.error && <div className={className('error')}>{value.error}</div>}

            {calendar}

            {footer}
          </div>
        </div>
      ) : null}
    </div>
  );
};
