import React, {
  useMemo,
  forwardRef,
  useRef,
  useEffect,
  useCallback,
} from 'react';
import { StyleSheet, Animated, Easing, Pressable, View } from 'react-native';
import type {
  TextInputProps as RNTextInputProps,
  StyleProp,
  ViewStyle,
  TextStyle,
  TextInput as RNTextInput,
  BlurEvent,
  FocusEvent,
} from 'react-native';
import { hexToRgba, omit, pick } from '../../utils/helpers';
import {
  StyledTextInputContainer,
  StyledLabelInsideTextInput,
  StyledError,
  StyledTextInput,
  StyledContainer,
  StyledMaxLengthMessage,
  StyledErrorContainer,
  StyledAsteriskLabelInsideTextInput,
  StyledInputContentContainer,
  StyledTextInputAndLabelContainer,
  StyledLabelContainerInsideTextInput,
  StyledErrorAndHelpTextContainer,
  StyledBorderBackDrop,
  StyledErrorAndMaxLengthContainer,
  StyledHelperText,
} from './StyledTextInput';
import Icon from '../Icon';
import type { Theme } from '../../theme';
import { useTheme } from '../../theme';
import type { State } from './StyledTextInput';
import type { IconName } from '../Icon';
import Typography from '../Typography';

export type TextInputHandles = Pick<
  RNTextInput,
  'focus' | 'clear' | 'blur' | 'isFocused' | 'setNativeProps'
>;

type TextInputVariant = 'text' | 'textarea';

type NativeTextInputProps = Omit<RNTextInputProps, 'onFocus' | 'onBlur'> & {
  onFocus?: (event?: FocusEvent) => void | undefined;
  onBlur?: (event?: BlurEvent) => void | undefined;
};

interface TextInputRef {
  focus: () => void;
  blur: () => void;
  clear: () => void;
  isFocused: () => boolean;
  setNativeProps?: (props: RNTextInputProps) => void;
}

export interface TextInputProps extends NativeTextInputProps {
  /**
   * Field label.
   */
  label?: string;
  /**
   * Name of Icon or ReactElement to render on the left side of the input, before the user's cursor.
   */
  prefix?: IconName | React.ReactElement;
  /**
   * Name of Icon or ReactElement to render on the right side of the input.
   */
  suffix?: IconName | React.ReactElement;
  /**
   * Additional wrapper style.
   */
  style?: StyleProp<ViewStyle>;
  /**
   * Input text style.
   */
  textStyle?: StyleProp<TextStyle>;
  /**
   * Testing id of the component.
   */
  testID?: string;
  /**
   * Accessibility label for the input (Android).
   */
  accessibilityLabelledBy?: string;
  /**
   * Error message to display.
   */
  error?: string;
  /**
   *  Whether the input is required, if true, an asterisk will be appended to the label.
   */
  required?: boolean;
  /**
   *  Placeholder text to display.
   * */
  placeholder?: string;
  /**
   * Whether the input is editable.
   * */
  editable?: boolean;
  /**
   * Whether the input is disabled.
   */
  disabled?: boolean;
  /**
   * Whether the input is loading.
   */
  loading?: boolean;
  /**
   * The max length of the input.
   * If the max length is set, the input will display the current length and the max length.
   * */
  maxLength?: number;
  /**
   * Whether to hide the character count.
   * */
  hideCharacterCount?: boolean;
  /**
   * The helper text to display.
   */
  helpText?: string;
  /**
   * Customise input value renderer
   */
  renderInputValue?: (
    inputProps: NativeTextInputProps,
    ref?: React.ForwardedRef<TextInputRef>
  ) => React.ReactNode;
  /**
   * Component ref.
   */
  ref?: React.Ref<TextInputHandles>;
  /**
   * Component variant.
   */
  variant?: TextInputVariant;
}

export const getState = ({
  disabled,
  error,
  editable,
  loading,
  isEmptyValue,
}: {
  disabled?: boolean;
  error?: string;
  editable?: boolean;
  loading: boolean;
  isFocused?: boolean;
  isEmptyValue?: boolean;
}): State => {
  if (disabled) {
    return 'disabled';
  }
  if (error) {
    return 'error';
  }
  if (!editable || loading) {
    return 'readonly';
  }
  if (!isEmptyValue) {
    return 'filled';
  }
  return 'default';
};

// Fix issue: Placeholder is not shown on iOS when multiline is true
// https://github.com/callstack/react-native-paper/pull/3331
const EMPTY_PLACEHOLDER_VALUE = ' ';

export const LABEL_ANIMATION_DURATION = 150;

export const renderErrorOrHelpText = ({
  error,
  helpText,
}: {
  error?: string;
  helpText?: string;
}) => {
  return error ? (
    <StyledErrorContainer>
      <StyledError testID="input-error-message">{error}</StyledError>
    </StyledErrorContainer>
  ) : (
    !!helpText && <StyledHelperText>{helpText}</StyledHelperText>
  );
};

export const renderInput = ({
  variant,
  nativeInputProps,
  renderInputValue,
  ref,
  theme,
  state,
}: {
  variant: TextInputVariant;
  nativeInputProps: NativeTextInputProps;
  multiline?: boolean;
  renderInputValue?: (
    inputProps: NativeTextInputProps,
    ref?: React.Ref<TextInputRef>
  ) => React.ReactNode;
  ref?: React.Ref<RNTextInput>;
  theme: Theme;
  state: State;
}) => {
  const multiline = variant === 'textarea' || nativeInputProps.multiline;
  // `numberOfLines` must be `1` for single-line inputs to render properly on Android.
  const numberOfLines = multiline ? nativeInputProps.numberOfLines : 1;

  return renderInputValue ? (
    renderInputValue(nativeInputProps, ref)
  ) : (
    <StyledTextInput
      {...nativeInputProps}
      themeVariant={variant}
      multiline={multiline}
      numberOfLines={numberOfLines}
      ref={ref}
      placeholderTextColor={theme.__hd__.textInput.colors.placeholder[state]}
      onBlur={nativeInputProps?.onBlur}
      onFocus={nativeInputProps?.onFocus}
    />
  );
};

export const renderSuffix = ({
  state,
  loading,
  suffix,
}: {
  state: State;
  loading: boolean;
  suffix?: IconName | React.ReactElement;
}) => {
  const actualSuffix = loading ? 'loading' : suffix;
  return typeof actualSuffix === 'string' ? (
    <Icon
      intent={state === 'disabled' ? 'disabled-text' : 'muted'}
      testID="input-suffix"
      icon={actualSuffix}
      spin={actualSuffix === 'loading'}
      size="medium"
    />
  ) : (
    suffix
  );
};

export const renderPrefix = ({
  state,
  prefix,
}: {
  state: State;
  prefix?: IconName | React.ReactElement;
}) => {
  return typeof prefix === 'string' ? (
    <Icon
      intent={state === 'disabled' ? 'disabled-text' : 'muted'}
      testID="input-prefix"
      icon={prefix}
      size="medium"
    />
  ) : (
    prefix
  );
};

export const renderMaxLengthMessage = ({
  maxLength,
  state,
  currentLength,
  hideCharacterCount,
}: {
  state: State;
  currentLength: number;
  maxLength?: number;
  hideCharacterCount: boolean;
}) => {
  const shouldShowMaxLength = maxLength !== undefined && !hideCharacterCount;
  return (
    shouldShowMaxLength && (
      <StyledMaxLengthMessage themeState={state}>
        {currentLength}/{maxLength}
      </StyledMaxLengthMessage>
    )
  );
};

const getDisplayText = (value?: string, defaultValue?: string) => {
  return (value !== undefined ? value : defaultValue) ?? '';
};

const TextInput = forwardRef<TextInputHandles, TextInputProps>(
  (
    {
      label,
      prefix,
      suffix,
      style,
      textStyle,
      testID,
      accessibilityLabelledBy,
      error,
      required,
      editable = true,
      disabled = false,
      loading = false,
      maxLength,
      hideCharacterCount = false,
      helpText,
      value,
      defaultValue,
      renderInputValue,
      allowFontScaling = false,
      variant = 'text',
      ...nativeProps
    }: TextInputProps,
    ref?: React.Ref<TextInputHandles>
  ) => {
    const displayText = getDisplayText(value, defaultValue);
    const isEmptyValue = displayText.length === 0;

    const [containerHeight, setContainerHeight] = React.useState(0);

    const [isFocused, setIsFocused] = React.useState(false);

    const state = getState({
      disabled,
      error,
      editable,
      loading,
      isFocused,
      isEmptyValue,
    });

    const theme = useTheme();

    const focusAnimation = useRef(new Animated.Value(0)).current;

    useEffect(() => {
      Animated.timing(focusAnimation, {
        toValue: isFocused || !isEmptyValue ? 1 : 0,
        duration: LABEL_ANIMATION_DURATION,
        easing: Easing.bezier(0.4, 0, 0.2, 1),
        useNativeDriver: true,
      }).start();
    }, [focusAnimation, isEmptyValue, isFocused]);

    const innerTextInput = React.useRef<RNTextInput | undefined | null>(null);
    const focusInnerTextInput = useCallback(
      () => innerTextInput.current?.focus(),
      []
    );
    const onContentLayout = useCallback(
      (e: { nativeEvent: { layout: { height: number } } }) => {
        setContainerHeight(e.nativeEvent.layout.height);
      },
      []
    );
    React.useImperativeHandle(
      ref,
      () => ({
        // we don't expose this method, it's for testing https://medium.com/developer-rants/how-to-test-useref-without-mocking-useref-699165f4994e
        getNativeTextInputRef: () => innerTextInput.current,
        focus: () => {
          innerTextInput.current?.focus();
        },
        clear: () => innerTextInput.current?.clear(),
        setNativeProps: (args: NativeTextInputProps) =>
          innerTextInput.current?.setNativeProps(args),
        isFocused: () => innerTextInput.current?.isFocused() || false,
        blur: () => innerTextInput.current?.blur(),
      }),
      [innerTextInput]
    );

    const { borderStyle, textStyleWithoutBorderStyle } = useMemo(() => {
      if (!textStyle) {
        return {};
      }

      const flattenTextStyle = StyleSheet.flatten(textStyle);
      const borderKeys = Object.keys(flattenTextStyle).filter((key) => {
        return key.startsWith('border');
      }) as Array<keyof TextStyle>;
      return {
        borderStyle: pick(borderKeys, flattenTextStyle),
        textStyleWithoutBorderStyle: omit(borderKeys, flattenTextStyle),
      };
    }, [textStyle]);

    const readonlyBackground = hexToRgba(
      theme.__hd__.textInput.colors.readonlyBackground,
      0.7
    );

    const { backgroundColor, styleWithoutBackgroundColor } = useMemo(() => {
      const defaultBackground =
        state === 'readonly'
          ? readonlyBackground
          : theme.__hd__.textInput.colors.containerBackground;

      if (!style) {
        return { backgroundColor: defaultBackground };
      }

      const flattenTextStyle = StyleSheet.flatten(style);
      return {
        backgroundColor: flattenTextStyle.backgroundColor ?? defaultBackground,
        styleWithoutBackgroundColor: omit(
          ['backgroundColor'],
          flattenTextStyle
        ),
      };
    }, [style, theme, state, readonlyBackground]);

    const nativeInputTestIDSuffix = testID ? `-${testID}` : '';

    const nativeInputProps: NativeTextInputProps = {
      style: StyleSheet.flatten([
        {
          backgroundColor,
          color: theme.__hd__.textInput.colors.text[state],
        },
        textStyleWithoutBorderStyle,
      ]),
      testID: `text-input${nativeInputTestIDSuffix}`,
      accessibilityState: {
        disabled: state === 'disabled' || state === 'readonly',
      },
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      accessibilityLabelledBy,
      allowFontScaling,
      ...nativeProps,
      onFocus: (event) => {
        setIsFocused(true);
        nativeProps.onFocus?.(event);
      },
      onBlur: (event) => {
        setIsFocused(false);
        nativeProps.onBlur?.(event);
      },
      editable,
      maxLength,
      value,
      onChangeText: (text) => {
        nativeProps.onChangeText?.(text);
      },
      defaultValue,
      placeholder:
        isFocused || label === undefined
          ? nativeProps.placeholder
          : EMPTY_PLACEHOLDER_VALUE,
    };

    return (
      <StyledContainer
        style={styleWithoutBackgroundColor}
        pointerEvents={
          state === 'disabled' || state === 'readonly' || loading
            ? 'none'
            : 'auto'
        }
        testID={testID}
      >
        <Pressable
          onPress={focusInnerTextInput}
          accessible={false}
          importantForAccessibility="no"
        >
          <StyledTextInputContainer themeVariant={variant} themeState={state}>
            <StyledBorderBackDrop
              themeFocused={isFocused}
              themeState={state}
              themeFilled={!isEmptyValue}
              testID="text-input-border"
              style={[{ backgroundColor }, borderStyle]}
            />

            {prefix !== undefined && (
              <View>{renderPrefix({ state, prefix })}</View>
            )}
            <StyledInputContentContainer
              themeHasLabel={!!label}
              onLayout={onContentLayout}
            >
              <StyledLabelContainerInsideTextInput
                themeVariant={variant}
                pointerEvents="none"
                style={[
                  {
                    transformOrigin: 'left top',
                  },
                  {
                    transform: [
                      {
                        translateY: focusAnimation.interpolate({
                          inputRange: [0, 1],
                          outputRange: [
                            variant !== 'textarea'
                              ? Math.max(
                                  0,
                                  (containerHeight -
                                    theme.__hd__.textInput.lineHeights.label) /
                                    2
                                )
                              : theme.__hd__.textInput.space
                                  .textareaLabelIdleTranslateY,
                            0,
                          ],
                        }),
                      },
                      {
                        scale: focusAnimation.interpolate({
                          inputRange: [0, 1],
                          outputRange: [1, 0.75],
                        }),
                      },
                    ],
                  },
                ]}
              >
                {!!label && (
                  <StyledLabelInsideTextInput
                    style={{ backgroundColor }}
                    nativeID={accessibilityLabelledBy}
                    testID={`input-label${nativeInputTestIDSuffix}`}
                    themeState={state}
                  >
                    {required && (
                      <StyledAsteriskLabelInsideTextInput
                        style={{ backgroundColor }}
                        themeState={state}
                        testID="input-label-asterisk"
                      >
                        *
                      </StyledAsteriskLabelInsideTextInput>
                    )}
                    <Typography.Body
                      testID={`input-label-text${nativeInputTestIDSuffix}`}
                      numberOfLines={1}
                      intent="muted"
                    >
                      {label}
                    </Typography.Body>
                  </StyledLabelInsideTextInput>
                )}
              </StyledLabelContainerInsideTextInput>

              <StyledTextInputAndLabelContainer themeHasLabel={!!label}>
                {renderInput({
                  variant,
                  nativeInputProps,
                  renderInputValue,
                  ref: (rnTextInputRef) => {
                    innerTextInput.current = rnTextInputRef;
                  },
                  theme,
                  state,
                })}
              </StyledTextInputAndLabelContainer>
            </StyledInputContentContainer>
            {renderSuffix({ state, loading, suffix })}
          </StyledTextInputContainer>
        </Pressable>
        <StyledErrorAndHelpTextContainer>
          <StyledErrorAndMaxLengthContainer>
            {renderErrorOrHelpText({ error, helpText })}
            {renderMaxLengthMessage({
              state,
              currentLength: displayText.length,
              maxLength,
              hideCharacterCount,
            })}
          </StyledErrorAndMaxLengthContainer>
        </StyledErrorAndHelpTextContainer>
      </StyledContainer>
    );
  }
);

export default TextInput;
