/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

import type {TextStyleProp} from '../StyleSheet/StyleSheet';
import type {____TextStyle_Internal as TextStyleInternal} from '../StyleSheet/StyleSheetTypes';
import type {PressEvent} from '../Types/CoreEventTypes';
import type {NativeTextProps} from './TextNativeComponent';
import type {PressRetentionOffset, TextProps} from './TextProps';

import * as PressabilityDebug from '../Pressability/PressabilityDebug';
import usePressability from '../Pressability/usePressability';
import flattenStyle from '../StyleSheet/flattenStyle';
import processColor from '../StyleSheet/processColor';
import Platform from '../Utilities/Platform';
import TextAncestor from './TextAncestor';
import {NativeText, NativeVirtualText} from './TextNativeComponent';
import * as React from 'react';
import {useContext, useMemo, useState} from 'react';

type TextForwardRef = React.ElementRef<
  typeof NativeText | typeof NativeVirtualText,
>;

/**
 * Text is the fundamental component for displaying text.
 *
 * @see https://reactnative.dev/docs/text
 */
const Text: component(
  ref: React.RefSetter<TextForwardRef>,
  ...props: TextProps
) = React.forwardRef(
  (
    {
      accessible,
      accessibilityLabel,
      accessibilityState,
      allowFontScaling,
      'aria-busy': ariaBusy,
      'aria-checked': ariaChecked,
      'aria-disabled': ariaDisabled,
      'aria-expanded': ariaExpanded,
      'aria-label': ariaLabel,
      'aria-selected': ariaSelected,
      children,
      ellipsizeMode,
      disabled,
      id,
      nativeID,
      numberOfLines,
      onLongPress,
      onPress,
      onPressIn,
      onPressOut,
      onResponderGrant,
      onResponderMove,
      onResponderRelease,
      onResponderTerminate,
      onResponderTerminationRequest,
      onStartShouldSetResponder,
      pressRetentionOffset,
      selectable,
      selectionColor,
      suppressHighlighting,
      style,
      ...restProps
    }: TextProps,
    forwardedRef,
  ) => {
    const _accessibilityLabel = ariaLabel ?? accessibilityLabel;

    let _accessibilityState: ?TextProps['accessibilityState'] =
      accessibilityState;
    if (
      ariaBusy != null ||
      ariaChecked != null ||
      ariaDisabled != null ||
      ariaExpanded != null ||
      ariaSelected != null
    ) {
      if (_accessibilityState != null) {
        _accessibilityState = {
          busy: ariaBusy ?? _accessibilityState.busy,
          checked: ariaChecked ?? _accessibilityState.checked,
          disabled: ariaDisabled ?? _accessibilityState.disabled,
          expanded: ariaExpanded ?? _accessibilityState.expanded,
          selected: ariaSelected ?? _accessibilityState.selected,
        };
      } else {
        _accessibilityState = {
          busy: ariaBusy,
          checked: ariaChecked,
          disabled: ariaDisabled,
          expanded: ariaExpanded,
          selected: ariaSelected,
        };
      }
    }

    const _accessibilityStateDisabled = _accessibilityState?.disabled;
    const _disabled = disabled ?? _accessibilityStateDisabled;

    const isPressable =
      (onPress != null ||
        onLongPress != null ||
        onStartShouldSetResponder != null) &&
      _disabled !== true;

    // TODO: Move this processing to the view configuration.
    const _selectionColor =
      selectionColor != null ? processColor(selectionColor) : undefined;

    let _style = style;
    if (__DEV__) {
      if (PressabilityDebug.isEnabled() && onPress != null) {
        _style = [style, {color: 'magenta'}];
      }
    }

    let _numberOfLines = numberOfLines;
    if (_numberOfLines != null && !(_numberOfLines >= 0)) {
      if (__DEV__) {
        console.error(
          `'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
        );
      }
      _numberOfLines = 0;
    }

    let _selectable = selectable;

    let processedStyle = flattenStyle<TextStyleProp>(_style);
    if (processedStyle != null) {
      let overrides: ?{...TextStyleInternal} = null;
      if (typeof processedStyle.fontWeight === 'number') {
        overrides = overrides || ({}: {...TextStyleInternal});
        overrides.fontWeight =
          // $FlowFixMe[incompatible-cast]
          (processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
      }

      if (processedStyle.userSelect != null) {
        _selectable = userSelectToSelectableMap[processedStyle.userSelect];
        overrides = overrides || ({}: {...TextStyleInternal});
        overrides.userSelect = undefined;
      }

      if (processedStyle.verticalAlign != null) {
        overrides = overrides || ({}: {...TextStyleInternal});
        overrides.textAlignVertical =
          verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
        overrides.verticalAlign = undefined;
      }

      if (overrides != null) {
        // $FlowFixMe[incompatible-type]
        _style = [_style, overrides];
      }
    }

    const _nativeID = id ?? nativeID;

    const hasTextAncestor = useContext(TextAncestor);
    if (hasTextAncestor) {
      if (isPressable) {
        return (
          <NativePressableVirtualText
            ref={forwardedRef}
            textProps={{
              ...restProps,
              accessibilityLabel: _accessibilityLabel,
              accessibilityState: _accessibilityState,
              nativeID: _nativeID,
              numberOfLines: _numberOfLines,
              selectable: _selectable,
              selectionColor: _selectionColor,
              style: _style,
              disabled: disabled,
              children,
            }}
            textPressabilityProps={{
              onLongPress,
              onPress,
              onPressIn,
              onPressOut,
              onResponderGrant,
              onResponderMove,
              onResponderRelease,
              onResponderTerminate,
              onResponderTerminationRequest,
              onStartShouldSetResponder,
              pressRetentionOffset,
              suppressHighlighting,
            }}
          />
        );
      }

      return (
        <NativeVirtualText
          {...restProps}
          accessibilityLabel={_accessibilityLabel}
          accessibilityState={_accessibilityState}
          nativeID={_nativeID}
          numberOfLines={_numberOfLines}
          ref={forwardedRef}
          selectable={_selectable}
          selectionColor={_selectionColor}
          style={_style}
          disabled={disabled}>
          {children}
        </NativeVirtualText>
      );
    }

    // If the disabled prop and accessibilityState.disabled are out of sync but not both in
    // falsy states we need to update the accessibilityState object to use the disabled prop.
    if (
      _disabled !== _accessibilityStateDisabled &&
      ((_disabled != null && _disabled !== false) ||
        (_accessibilityStateDisabled != null &&
          _accessibilityStateDisabled !== false))
    ) {
      _accessibilityState = {..._accessibilityState, disabled: _disabled};
    }

    const _accessible = Platform.select({
      ios: accessible !== false,
      android:
        accessible == null
          ? onPress != null || onLongPress != null
          : accessible,
      default: accessible,
    });

    let nativeText = null;
    if (isPressable) {
      nativeText = (
        <NativePressableText
          ref={forwardedRef}
          textProps={{
            ...restProps,
            accessibilityLabel: _accessibilityLabel,
            accessibilityState: _accessibilityState,
            accessible: _accessible,
            allowFontScaling: allowFontScaling !== false,
            disabled: _disabled,
            ellipsizeMode: ellipsizeMode ?? 'tail',
            nativeID: _nativeID,
            numberOfLines: _numberOfLines,
            selectable: _selectable,
            selectionColor: _selectionColor,
            style: _style,
            children,
          }}
          textPressabilityProps={{
            onLongPress,
            onPress,
            onPressIn,
            onPressOut,
            onResponderGrant,
            onResponderMove,
            onResponderRelease,
            onResponderTerminate,
            onResponderTerminationRequest,
            onStartShouldSetResponder,
            pressRetentionOffset,
            suppressHighlighting,
          }}
        />
      );
    } else {
      nativeText = (
        <NativeText
          {...restProps}
          accessibilityLabel={_accessibilityLabel}
          accessibilityState={_accessibilityState}
          accessible={_accessible}
          allowFontScaling={allowFontScaling !== false}
          disabled={_disabled}
          ellipsizeMode={ellipsizeMode ?? 'tail'}
          nativeID={_nativeID}
          numberOfLines={_numberOfLines}
          ref={forwardedRef}
          selectable={_selectable}
          selectionColor={_selectionColor}
          style={_style}>
          {children}
        </NativeText>
      );
    }

    if (children == null) {
      return nativeText;
    }

    // If the children do not contain a JSX element it would not be possible to have a
    // nested `Text` component so we can skip adding the `TextAncestor` context wrapper
    // which has a performance overhead. Since we do this for performance reasons we need
    // to keep the check simple to avoid regressing overall perf. For this reason the
    // `children.length` constant is set to `3`, this should be a reasonable tradeoff
    // to capture the majority of `Text` uses but also not make this check too expensive.
    if (Array.isArray(children) && children.length <= 3) {
      let hasNonTextChild = false;
      for (let child of children) {
        if (child != null && typeof child === 'object') {
          hasNonTextChild = true;
          break;
        }
      }
      if (!hasNonTextChild) {
        return nativeText;
      }
    } else if (typeof children !== 'object') {
      return nativeText;
    }

    return (
      <TextAncestor.Provider value={true}>{nativeText}</TextAncestor.Provider>
    );
  },
);

Text.displayName = 'Text';

type TextPressabilityProps = $ReadOnly<{
  onLongPress?: ?(event: PressEvent) => mixed,
  onPress?: ?(event: PressEvent) => mixed,
  onPressIn?: ?(event: PressEvent) => mixed,
  onPressOut?: ?(event: PressEvent) => mixed,
  onResponderGrant?: ?(event: PressEvent) => void,
  onResponderMove?: ?(event: PressEvent) => void,
  onResponderRelease?: ?(event: PressEvent) => void,
  onResponderTerminate?: ?(event: PressEvent) => void,
  onResponderTerminationRequest?: ?() => boolean,
  onStartShouldSetResponder?: ?() => boolean,
  pressRetentionOffset?: ?PressRetentionOffset,
  suppressHighlighting?: ?boolean,
}>;

/**
 * Hook that handles setting up Pressability of Text components.
 *
 * NOTE: This hook is relatively expensive so it should only be used absolutely necessary.
 */
function useTextPressability({
  onLongPress,
  onPress,
  onPressIn,
  onPressOut,
  onResponderGrant,
  onResponderMove,
  onResponderRelease,
  onResponderTerminate,
  onResponderTerminationRequest,
  onStartShouldSetResponder,
  pressRetentionOffset,
  suppressHighlighting,
}: TextPressabilityProps) {
  const [isHighlighted, setHighlighted] = useState(false);

  // Setup pressability config and wrap callbacks needs to track the highlight state.
  const config = useMemo(() => {
    let _onPressIn = onPressIn;
    let _onPressOut = onPressOut;

    // Updating isHighlighted causes unnecessary re-renders for platforms that don't use it
    // in the best case, and cause issues with text selection in the worst case. Forcing
    // the isHighlighted prop to false on all platforms except iOS.
    if (Platform.OS === 'ios') {
      _onPressIn = (event: PressEvent) => {
        setHighlighted(suppressHighlighting == null || !suppressHighlighting);
        onPressIn?.(event);
      };

      _onPressOut = (event: PressEvent) => {
        setHighlighted(false);
        onPressOut?.(event);
      };
    }

    return {
      disabled: false,
      pressRectOffset: pressRetentionOffset,
      onLongPress,
      onPress,
      onPressIn: _onPressIn,
      onPressOut: _onPressOut,
    };
  }, [
    pressRetentionOffset,
    onLongPress,
    onPress,
    onPressIn,
    onPressOut,
    suppressHighlighting,
  ]);

  // Init the pressability class
  const eventHandlers = usePressability(config);

  // Create NativeText event handlers which proxy events to pressability
  const eventHandlersForText = useMemo(
    () =>
      eventHandlers == null
        ? null
        : {
            onResponderGrant(event: PressEvent) {
              eventHandlers.onResponderGrant(event);
              if (onResponderGrant != null) {
                onResponderGrant(event);
              }
            },
            onResponderMove(event: PressEvent) {
              eventHandlers.onResponderMove(event);
              if (onResponderMove != null) {
                onResponderMove(event);
              }
            },
            onResponderRelease(event: PressEvent) {
              eventHandlers.onResponderRelease(event);
              if (onResponderRelease != null) {
                onResponderRelease(event);
              }
            },
            onResponderTerminate(event: PressEvent) {
              eventHandlers.onResponderTerminate(event);
              if (onResponderTerminate != null) {
                onResponderTerminate(event);
              }
            },
            onClick: eventHandlers.onClick,
            onResponderTerminationRequest:
              onResponderTerminationRequest != null
                ? onResponderTerminationRequest
                : eventHandlers.onResponderTerminationRequest,
            onStartShouldSetResponder:
              onStartShouldSetResponder != null
                ? onStartShouldSetResponder
                : eventHandlers.onStartShouldSetResponder,
          },
    [
      eventHandlers,
      onResponderGrant,
      onResponderMove,
      onResponderRelease,
      onResponderTerminate,
      onResponderTerminationRequest,
      onStartShouldSetResponder,
    ],
  );

  // Return the highlight state and NativeText event handlers
  return useMemo(
    () => [isHighlighted, eventHandlersForText],
    [isHighlighted, eventHandlersForText],
  );
}

type NativePressableTextProps = $ReadOnly<{
  textProps: NativeTextProps,
  textPressabilityProps: TextPressabilityProps,
}>;

/**
 * Wrap the NativeVirtualText component and initialize pressability.
 *
 * This logic is split out from the main Text component to enable the more
 * expensive pressability logic to be only initialized when needed.
 */
const NativePressableVirtualText: component(
  ref: React.RefSetter<TextForwardRef>,
  ...props: NativePressableTextProps
) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => {
  const [isHighlighted, eventHandlersForText] = useTextPressability(
    textPressabilityProps,
  );

  return (
    <NativeVirtualText
      {...textProps}
      {...eventHandlersForText}
      isHighlighted={isHighlighted}
      isPressable={true}
      ref={forwardedRef}
    />
  );
});

/**
 * Wrap the NativeText component and initialize pressability.
 *
 * This logic is split out from the main Text component to enable the more
 * expensive pressability logic to be only initialized when needed.
 */
const NativePressableText: component(
  ref: React.RefSetter<TextForwardRef>,
  ...props: NativePressableTextProps
) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => {
  const [isHighlighted, eventHandlersForText] = useTextPressability(
    textPressabilityProps,
  );

  return (
    <NativeText
      {...textProps}
      {...eventHandlersForText}
      isHighlighted={isHighlighted}
      isPressable={true}
      ref={forwardedRef}
    />
  );
});

const userSelectToSelectableMap = {
  auto: true,
  text: true,
  none: false,
  contain: true,
  all: true,
};

const verticalAlignToTextAlignVerticalMap = {
  auto: 'auto',
  top: 'top',
  bottom: 'bottom',
  middle: 'center',
};

module.exports = Text;
