import { format, isValid, parse } from 'date-fns';
import padStart from 'lodash/padStart';
import { ChangeEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { Dropdown, DropdownProps } from '../../components/Dropdown/Dropdown';
import { Icon } from '../../components/Icon/Icon';
import { IconGlyph } from '../../components/Icon/constants';
import { useDateFnsLocale } from '../../core/hooks/useDateFnsLocale';
import { useTranslation } from '../../core/hooks/useTranslation';
import { useId } from '../../hooks/useId';
import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { changeInputValue } from '../../utils/changeInputValue';
import { makeTestId } from '../../utils/makeTestId';
import { SupportedInputProps } from '../types';

import { DatePicker } from './components/DatePicker/DatePicker';
import { StyledCalendarButton, StyledWrapper, TunedDatePickerInput } from './styled';

// The default function {@link Date.toIsoDate} has problems with timezones. This function has no
// problems.
function dateToIsoDate(date: Date): string {
  return `${date.getFullYear()}-${padStart((date.getMonth() + 1).toString(), 2, '0')}-${padStart(
    date.getDate().toString(),
    2,
    '0',
  )}`;
}

export interface DateFieldProps extends SupportedInputProps {
  /** Date in ISO format `YYYY-MM-DD` */
  value?: string;
}

const ISO_FORMAT = 'yyyy-MM-dd';

/**
 * Form field that allow to input date, also this field allows selecting date
 * by date select dropdown.
 *
 * Value should be in standard ISO format `YYYY-MM-DD`.
 *
 * ```tsx
 * import { DateField } from 'ui-kit';
 *
 * <DateField value="2022-12-20" onChange={console.log} />
 * ```
 */
export function DateField(props: DateFieldProps) {
  const {
    id,
    ariaLabel,
    ariaInvalid,
    ariaErrorMessage,
    ariaDescribedBy,
    disabled,
    onBlur,
    onChange,
    value,
    placeholder,
    className,
    testId,
    required,
    ariaRequired,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const { t } = useTranslation();
  const testIdAttribute = useTestIdAttribute();

  const dropdownId = useId();
  const [inputValue, setInputValue] = useState('');
  const [isCalendarOpen, setCalendarOpen] = useState(false);

  const inputRef = useRef<HTMLInputElement>(null);
  const calendarButtonRef = useRef(null);

  const dateFnsLocale = useDateFnsLocale();

  // Some short locales contain short year format, we want to use always full year format.
  const dateFormat = useMemo<string>(
    () => dateFnsLocale.formatLong!.date({ width: 'short' }).replace(/y+/, 'yyyy'),
    [dateFnsLocale],
  );

  const dateMask = useMemo(
    () => dateFormat.split('').map((char) => (/[dMy]/.test(char) ? /\d/ : char)),
    [dateFormat],
  );

  const selectedDate = useMemo(() => {
    const result = parse(inputValue, dateFormat, new Date());
    return isValid(result) ? result : undefined;
  }, [dateFormat, inputValue]);

  useEffect(() => {
    if (!value) {
      setInputValue('');
      return;
    }

    if (value.length === ISO_FORMAT.length) {
      const date = parse(value, ISO_FORMAT, new Date());
      /* istanbul ignore else */
      if (isValid(date)) {
        setInputValue(format(date, dateFormat));
        return;
      }
    }

    setInputValue(value);
  }, [dateFormat, value]);

  const handleDateInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    setInputValue(event.target.value);

    if (onChange) {
      const inputString = event.target.value;
      // We should check date length beacuse parse method is not strict
      // See https://github.com/date-fns/date-fns/issues/1849
      if (inputString.length === dateFormat.length) {
        const date = parse(inputString, dateFormat, new Date());
        /* istanbul ignore else */
        if (isValid(date)) {
          const isoDate = dateToIsoDate(date);
          onChange(isoDate);
          return;
        }
      }

      onChange(event.target.value);
    }
  };

  const handleCalendarButtonClick = useCallback(() => {
    setCalendarOpen(!isCalendarOpen);
  }, [isCalendarOpen]);

  const handleDatePickerChange = (date: Date) => {
    setCalendarOpen(false);

    /* istanbul ignore else */
    if (inputRef.current) {
      const dateString = format(date, dateFormat);
      changeInputValue(inputRef.current, dateString);
    }
  };

  const handleDropdownOutsideClick = (event: MouseEvent) => {
    // User can click by input and type some value here
    if (event.target !== inputRef.current) {
      setCalendarOpen(false);
    }
  };

  const handleDropdownEscKeyPress = () => {
    setCalendarOpen(false);
  };

  const datePickerRef = useRef<HTMLDivElement>(null);

  const focusTrapOptions = useMemo<DropdownProps['focusTrapOptions']>(() => {
    return {
      initialFocus() {
        return (
          datePickerRef.current?.querySelector<HTMLDivElement>('.react-datepicker__day--selected') ??
          datePickerRef.current?.querySelector<HTMLDivElement>('.react-datepicker__day--001') ??
          /* istanbul ignore next */ false
        );
      },
      setReturnFocus() {
        /* istanbul ignore next */
        return inputRef.current ? inputRef.current : false;
      },
      allowOutsideClick: true,
    };
  }, []);

  const popperOptions = useMemo<DropdownProps['popperOptions']>(() => {
    return {
      placement: 'bottom-end',
      modifiers: [
        {
          name: 'offset',
          options: {
            offset: [0, 3],
          },
        },
      ],
    };
  }, []);

  return (
    <StyledWrapper className={className} {...{ [testIdAttribute]: testId }}>
      <TunedDatePickerInput
        ref={inputRef}
        ariaDescribedBy={ariaDescribedBy}
        ariaErrorMessage={ariaErrorMessage}
        ariaInvalid={!!ariaInvalid}
        ariaLabel={ariaLabel}
        ariaRequired={ariaRequired}
        disabled={disabled}
        guide={false}
        id={id}
        mask={dateMask}
        onBlur={onBlur}
        onChange={handleDateInputChange}
        placeholder={placeholder}
        required={required}
        testId={makeTestId(testId, 'input')}
        value={inputValue}
      />

      <StyledCalendarButton
        ref={calendarButtonRef}
        aria-controls={dropdownId}
        aria-hidden
        aria-label={t('ui.dateField.selectDate')}
        disabled={disabled}
        onClick={handleCalendarButtonClick}
        tabIndex={-1}
        type="button"
        {...{ [testIdAttribute]: makeTestId(testId, 'calendar-button') }}
      >
        <Icon glyph={IconGlyph.Calendar} />
      </StyledCalendarButton>

      <Dropdown
        focusTrapOptions={focusTrapOptions}
        id={dropdownId}
        onEscButtonPress={handleDropdownEscKeyPress}
        onOutsideClick={handleDropdownOutsideClick}
        popperOptions={popperOptions}
        trigger={calendarButtonRef.current}
        visible={isCalendarOpen}
      >
        <DatePicker
          ref={datePickerRef}
          onChange={handleDatePickerChange}
          testId={makeTestId(testId, 'datepicker-popup')}
          value={selectedDate}
        />
      </Dropdown>
    </StyledWrapper>
  );
}
