import React, { AriaAttributes, ChangeEventHandler, forwardRef, useCallback, useEffect, useRef } from 'react';
import { createTextMaskInputElement } from 'text-mask-core';

import { useTestIdAttribute } from '../hooks/useTestIdAttribute';
import { TestIdProps } from '../types';
import { assertEmptyObject } from '../utils/assertEmptyObject';

/**
 * Mask can be array of strings or RegExp
 *
 * If the `mask` is `false` then mask will be not applied to value.
 *
 * @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#mask
 */
export type Mask = Array<string | RegExp> | false;

/**
 * @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#pipe
 */
export interface PipeConfig {
  placeholder: string;
  placeholderChar: string;
  currentCaretPosition: number;
  keepCharPositions: boolean;
  rawValue: string;
  guide: boolean | undefined;
  previousConformedValue: string | undefined;
}

/**
 * Based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-text-mask/index.d.ts
 */
export interface MaskedInputProps
  extends TestIdProps,
    Pick<
      React.InputHTMLAttributes<HTMLInputElement>,
      'value' | 'onChange' | 'onBlur' | 'disabled' | 'id' | 'placeholder' | 'required' | 'className'
    > {
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#mask */
  mask: Mask | ((value: string) => Mask);
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#guide */
  guide?: boolean;
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#placeholderchar */
  placeholderChar?: string;
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#keepcharpositions */
  keepCharPositions?: boolean;
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#pipe */
  pipe?: (
    conformedValue: string,
    config: PipeConfig,
  ) => false | string | { value: string; indexesOfPipedChars: number[] };
  /** @see https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#showmask */
  showMask?: boolean;
  ariaLabel?: AriaAttributes['aria-label'];
  ariaDescribedBy?: AriaAttributes['aria-describedby'];
  ariaInvalid?: AriaAttributes['aria-invalid'];
  ariaErrorMessage?: AriaAttributes['aria-errormessage'];
  ariaRequired?: AriaAttributes['aria-required'];
}

/**
 * Wrapper on text-mask
 *
 * @see https://github.com/text-mask/text-mask/tree/master/react#readme
 *
 * Base on https://github.com/text-mask/text-mask/blob/master/react/src/reactTextMask.js
 */
export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>((props, forwardedRef) => {
  const {
    ariaLabel,
    ariaInvalid,
    ariaDescribedBy,
    ariaErrorMessage,
    ariaRequired,
    onChange,
    mask,
    showMask,
    id,
    pipe,
    guide,
    placeholderChar,
    placeholder,
    keepCharPositions,
    onBlur,
    required,
    disabled,
    value,
    testId,
    className,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const inputElement = useRef<HTMLInputElement | null>(null);

  const textMaskInputElement = useRef<{ update(value?: MaskedInputProps['value']): void } | null>(null);
  const initTextMask = useCallback(() => {
    textMaskInputElement.current = createTextMaskInputElement({
      inputElement: inputElement.current,
      guide,
      mask,
      placeholderChar,
      keepCharPositions,
      pipe,
      showMask,
    });
    textMaskInputElement.current?.update(value);
  }, [guide, mask, placeholderChar, keepCharPositions, pipe, showMask, value]);

  const setRef = (ref: HTMLInputElement | null) => {
    if (ref && inputElement.current !== ref) {
      inputElement.current = ref;
      if (typeof forwardedRef === 'function') {
        forwardedRef(ref);
      } else if (forwardedRef !== null) {
        forwardedRef.current = ref;
      }
      initTextMask();
    }
  };

  const prevProps = useRef<
    Pick<MaskedInputProps, 'pipe' | 'guide' | 'placeholderChar' | 'showMask' | 'mask'>
  >({
    pipe,
    mask,
    guide,
    placeholderChar,
    showMask,
  });
  useEffect(() => {
    // Сalculate that settings was changed:
    // - `keepCharPositions` exludes, because it affect only cursor position
    const isSettingChanged =
      guide !== prevProps.current.guide ||
      placeholderChar !== prevProps.current.placeholderChar ||
      showMask !== prevProps.current.showMask ||
      mask !== prevProps.current.mask ||
      pipe !== prevProps.current.pipe;

    // Сalculate that value was changed
    const isValueChanged = value !== inputElement.current?.value;

    // Check value and settings to prevent duplicating update() call
    if (isValueChanged || isSettingChanged) {
      initTextMask();
    }

    prevProps.current = { pipe, mask, guide, placeholderChar, showMask };
  }, [initTextMask, value, pipe, mask, guide, placeholderChar, showMask]);

  const onInputHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
    (event) => {
      textMaskInputElement.current?.update();
      onChange?.(event);
    },
    [onChange],
  );

  const testIdAttribute = useTestIdAttribute();

  return (
    <input
      ref={setRef}
      aria-describedby={ariaDescribedBy}
      aria-errormessage={ariaErrorMessage}
      aria-invalid={ariaInvalid}
      aria-label={ariaLabel}
      aria-required={ariaRequired}
      className={className}
      defaultValue={value}
      disabled={disabled}
      id={id}
      onBlur={onBlur}
      onInput={onInputHandler}
      placeholder={placeholder}
      required={required}
      {...{ [testIdAttribute]: testId }}
    />
  );
});
