/*
 * 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.
 */

#import "RCTTextInputComponentView.h"

#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/iostextinput/TextInputComponentDescriptor.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>

#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTScrollViewComponentView.h>

#if !TARGET_OS_OSX // [macOS]
#import <React/RCTUITextField.h>
#else // [macOS
#include <React/RCTUITextField.h>
#include <React/RCTUISecureTextField.h>
#endif // macOS]

#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#if TARGET_OS_OSX // [macOS
#import <React/RCTWrappedTextView.h>
#import <React/RCTViewKeyboardEvent.h>
#endif // macOS]

#import "RCTConversions.h"
#import "RCTTextInputNativeCommands.h"
#import "RCTTextInputUtils.h"

#import <limits>
#import "RCTFabricComponentsPlugins.h"

#if !TARGET_OS_OSX // [macOS]
/** Native iOS text field bottom keyboard offset amount */
static const CGFloat kSingleLineKeyboardBottomOffset = 15.0;
#endif // [macOS]

#if TARGET_OS_OSX // [macOS
static NSString *kEscapeKeyCode = @"\x1B";
#endif // macOS]


using namespace facebook::react;

#if !TARGET_OS_OSX // [macOS]
@interface RCTTextInputComponentView () <
    RCTBackedTextInputDelegate,
    RCTTextInputViewProtocol,
    UIDropInteractionDelegate>
@end
#else // [macOS
@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate, RCTTextInputViewProtocol>
@end
#endif // macOS]

static NSSet<NSNumber *> *returnKeyTypesSet;

@implementation RCTTextInputComponentView {
  TextInputShadowNode::ConcreteState::Shared _state;
#if !TARGET_OS_OSX // [macOS]
  RCTUIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
#else // [macOS
  RCTPlatformView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
#endif // macOS]
  NSUInteger _mostRecentEventCount;
  NSAttributedString *_lastStringStateWasUpdatedWith;

  /*
   * UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
   * entry, UITextView is for multiline entry. There is a problem with order of events when user types a character. In
   * UITextField (single line text entry), typing a character first triggers `onChange` event and then
   * onSelectionChange. In UITextView (multi line text entry), typing a character first triggers `onSelectionChange` and
   * then onChange. JavaScript depends on `onChange` to be called before `onSelectionChange`. This flag keeps state so
   * if UITextView is backing text input view, inside `-[RCTTextInputComponentView textInputDidChangeSelection]` we make
   * sure to call `onChange` before `onSelectionChange` and ignore next `-[RCTTextInputComponentView
   * textInputDidChange]` call.
   */
  BOOL _ignoreNextTextInputCall;

  /*
   * A flag that when set to true, `_mostRecentEventCount` won't be incremented when `[self _updateState]`
   * and delegate methods `textInputDidChange` and `textInputDidChangeSelection` will exit early.
   *
   * Setting `_backedTextInputView.attributedText` triggers delegate methods `textInputDidChange` and
   * `textInputDidChangeSelection` for multiline text input only.
   * In multiline text input this is undesirable as we don't want to be sending events for changes that JS triggered.
   */
  BOOL _comingFromJS;
  BOOL _didMoveToWindow;

  /*
   * Newly initialized default typing attributes contain a no-op NSParagraphStyle and NSShadow. These cause inequality
   * between the AttributedString backing the input and those generated from state. We store these attributes to make
   * later comparison insensitive to them.
   */
  NSDictionary<NSAttributedStringKey, id> *_originalTypingAttributes;

  BOOL _hasInputAccessoryView;
  CGSize _previousContentSize;
#if TARGET_OS_OSX // [macOS
  NSString *_ghostText;
  NSInteger _ghostTextPosition;
#endif // macOS]
}

#pragma mark - UIView overrides

- (instancetype)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame]) {
    const auto &defaultProps = TextInputShadowNode::defaultSharedProps();
    _props = defaultProps;

#if !TARGET_OS_OSX // [macOS]
    _backedTextInputView = defaultProps->multiline ? [RCTUITextView new] : [RCTUITextField new];
#else // [macOS
    _backedTextInputView = defaultProps->multiline ? [[RCTWrappedTextView alloc] initWithFrame:self.bounds] : [RCTUITextField new];
#endif // macOS]
    _backedTextInputView.textInputDelegate = self;
    _ignoreNextTextInputCall = NO;
    _comingFromJS = NO;
    _didMoveToWindow = NO;
    _originalTypingAttributes = [_backedTextInputView.typingAttributes copy];
    _previousContentSize = CGSizeZero;

    [self addSubview:_backedTextInputView];
#if TARGET_OS_IOS // [macOS] [visionOS]
    [self initializeReturnKeyType];
#endif // [macOS] [visionOS]
  }

  return self;
}

- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
  [super updateEventEmitter:eventEmitter];

  NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
      [_backedTextInputView.defaultTextAttributes mutableCopy];

  defaultAttributes[RCTAttributedStringEventEmitterKey] = RCTWrapEventEmitter(_eventEmitter);

  _backedTextInputView.defaultTextAttributes = defaultAttributes;
}

- (void)didMoveToWindow
{
  [super didMoveToWindow];

  if (self.window && !_didMoveToWindow) {
    const auto &props = static_cast<const TextInputProps &>(*_props);
    if (props.autoFocus) {
#if !TARGET_OS_OSX // [macOS]
      [_backedTextInputView becomeFirstResponder];
#else // [macOS
      NSWindow *window = [_backedTextInputView window];
      [window makeFirstResponder:_backedTextInputView.responder];
#endif // macOS]
      [self scrollCursorIntoView];
    }
    _didMoveToWindow = YES;
#if TARGET_OS_IOS // [macOS] [visionOS]
    [self initializeReturnKeyType];
#endif // [macOS] [visionOS]
  }

  [self _restoreTextSelection];
}

// [macOS
- (void)_updateDefaultTextAttributes
{
  const auto &props = static_cast<const TextInputProps &>(*_props);
  NSMutableDictionary<NSAttributedStringKey, id> *attrs =
      RCTNSTextAttributesFromTextAttributes(props.getEffectiveTextAttributes(RCTFontSizeMultiplier()));

  _backedTextInputView.defaultTextAttributes = attrs;

  // Also update the existing attributed text so the visible text re-renders
  // with the new color (defaultTextAttributes only affects newly typed text).
  // Wrap in _comingFromJS to prevent textInputDidChange from pushing a state
  // update back to the shadow tree, which would overwrite our fresh colors
  // with the stale cached attributed string.
  NSString *currentText = _backedTextInputView.attributedText.string;
  if (currentText.length > 0) {
    NSAttributedString *updated = [[NSAttributedString alloc] initWithString:currentText attributes:attrs];
    _comingFromJS = YES;
    _backedTextInputView.attributedText = updated;
    _comingFromJS = NO;
  }
}
// macOS]

#if !TARGET_OS_OSX // [macOS]
// TODO: replace with registerForTraitChanges once iOS 17.0 is the lowest supported version
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
  [super traitCollectionDidChange:previousTraitCollection];

  if (facebook::react::ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() &&
      UITraitCollection.currentTraitCollection.preferredContentSizeCategory !=
          previousTraitCollection.preferredContentSizeCategory) {
    const auto &newTextInputProps = static_cast<const TextInputProps &>(*_props);
    _backedTextInputView.defaultTextAttributes =
        RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
  }

  // [macOS
  if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
    [self _updateDefaultTextAttributes];
  }
  // macOS]
}
#else // [macOS
- (void)viewDidChangeEffectiveAppearance
{
  [super viewDidChangeEffectiveAppearance];
  [self _updateDefaultTextAttributes];
}
#endif // macOS]

- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView
{
#if !TARGET_OS_OSX // [macOS]
  if (![self isDescendantOfView:scrollView.scrollView] || !_backedTextInputView.isFirstResponder) {
    // View is outside scroll view or it's not a first responder.
    scrollView.firstResponderViewOutsideScrollView = _backedTextInputView;
    return;
  }

  UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
  UITextSelectionRect *selection = [_backedTextInputView selectionRectsForRange:selectedTextRange].firstObject;
  CGRect focusRect;
  if (selection == nil) {
    // No active selection or caret - fallback to entire input frame
    focusRect = self.bounds;
  } else {
    // Focus on text selection frame
    focusRect = selection.rect;
    BOOL isMultiline = [_backedTextInputView isKindOfClass:[UITextView class]];
    if (!isMultiline) {
      focusRect.size.height += kSingleLineKeyboardBottomOffset;
    }
  }
  scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil];
#endif // [macOS]
}

#pragma mark - RCTViewComponentView overrides

- (NSObject *)accessibilityElement
{
  return _backedTextInputView;
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
  return concreteComponentDescriptorProvider<TextInputComponentDescriptor>();
}

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
  const auto &oldTextInputProps = static_cast<const TextInputProps &>(*_props);
  const auto &newTextInputProps = static_cast<const TextInputProps &>(*props);

  // Traits:
  if (newTextInputProps.multiline != oldTextInputProps.multiline) {
    [self _setMultiline:newTextInputProps.multiline];
  }


#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
    _backedTextInputView.autocapitalizationType =
        RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType);
  }
#endif // [macOS]

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) {
    _backedTextInputView.autocorrectionType =
        RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect);
  }
#else // [macOS
  if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect && newTextInputProps.traits.autoCorrect.has_value()) {
    _backedTextInputView.automaticSpellingCorrectionEnabled =
        newTextInputProps.traits.autoCorrect.value();
  }
#endif // macOS]

  if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) {
    _backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden;
  }

  if (newTextInputProps.traits.editable != oldTextInputProps.traits.editable) {
    _backedTextInputView.editable = newTextInputProps.traits.editable;
  }

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.multiline &&
      newTextInputProps.traits.dataDetectorTypes != oldTextInputProps.traits.dataDetectorTypes) {
    _backedTextInputView.dataDetectorTypes =
        RCTUITextViewDataDetectorTypesFromStringVector(newTextInputProps.traits.dataDetectorTypes);
  }
#endif // [macOS]

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
      oldTextInputProps.traits.enablesReturnKeyAutomatically) {
    _backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
  }

  if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) {
    _backedTextInputView.keyboardAppearance =
        RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance);
  }
#endif // [macOS]

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) {
    _backedTextInputView.spellCheckingType =
        RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck);
  }
#else // [macOS
  if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck && newTextInputProps.traits.spellCheck.has_value()) {
    _backedTextInputView.continuousSpellCheckingEnabled =
        newTextInputProps.traits.spellCheck.value();
  }
#endif // macOS]

#if TARGET_OS_OSX // [macOS
  if (newTextInputProps.traits.grammarCheck != oldTextInputProps.traits.grammarCheck && newTextInputProps.traits.grammarCheck.has_value()) {
    _backedTextInputView.grammarCheckingEnabled =
        newTextInputProps.traits.grammarCheck.value();
  }
#endif // macOS]

  if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) {
    _backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden;
  }

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) {
    _backedTextInputView.clearButtonMode =
        RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode);
  }
#endif // [macOS]

  if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) {
    _backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled;
  }

  if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) {
#if !TARGET_OS_OSX // [macOS]
    _backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry;
#else // [macOS
    [self _setSecureTextEntry:newTextInputProps.traits.secureTextEntry];
#endif // macOS]
  }

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) {
    _backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType);
  }

  if (newTextInputProps.traits.returnKeyType != oldTextInputProps.traits.returnKeyType) {
    _backedTextInputView.returnKeyType = RCTUIReturnKeyTypeFromReturnKeyType(newTextInputProps.traits.returnKeyType);
  }

  if (newTextInputProps.traits.textContentType != oldTextInputProps.traits.textContentType) {
    _backedTextInputView.textContentType = RCTUITextContentTypeFromString(newTextInputProps.traits.textContentType);
  }

  if (newTextInputProps.traits.passwordRules != oldTextInputProps.traits.passwordRules) {
    _backedTextInputView.passwordRules = RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules);
  }

  if (newTextInputProps.traits.smartInsertDelete != oldTextInputProps.traits.smartInsertDelete) {
    _backedTextInputView.smartInsertDeleteType =
        RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete);
  }

  if (newTextInputProps.traits.showSoftInputOnFocus != oldTextInputProps.traits.showSoftInputOnFocus) {
    [self _setShowSoftInputOnFocus:newTextInputProps.traits.showSoftInputOnFocus];
  }
#endif // [macOS]

  // Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentionally here
  // because they are being checked on-demand.

  // Other props:
  if (newTextInputProps.placeholder != oldTextInputProps.placeholder) {
    _backedTextInputView.placeholder = RCTNSStringFromString(newTextInputProps.placeholder);
  }

  if (newTextInputProps.placeholderTextColor != oldTextInputProps.placeholderTextColor) {
    _backedTextInputView.placeholderColor = RCTUIColorFromSharedColor(newTextInputProps.placeholderTextColor);
  }

  if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
    NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
        RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
    defaultAttributes[RCTAttributedStringEventEmitterKey] =
        _backedTextInputView.defaultTextAttributes[RCTAttributedStringEventEmitterKey];
    _backedTextInputView.defaultTextAttributes = defaultAttributes;
  }

#if !TARGET_OS_OSX // [macOS]
  if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
    _backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
  }
#endif // [macOS]

  if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
    _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
  }

  if (newTextInputProps.inputAccessoryViewButtonLabel != oldTextInputProps.inputAccessoryViewButtonLabel) {
    _backedTextInputView.inputAccessoryViewButtonLabel =
        RCTNSStringFromString(newTextInputProps.inputAccessoryViewButtonLabel);
  }

  if (newTextInputProps.disableKeyboardShortcuts != oldTextInputProps.disableKeyboardShortcuts) {
    _backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts;
  }

  if (newTextInputProps.acceptDragAndDropTypes != oldTextInputProps.acceptDragAndDropTypes) {
    if (!newTextInputProps.acceptDragAndDropTypes.has_value()) {
      _backedTextInputView.acceptDragAndDropTypes = nil;
    } else {
      auto &vector = newTextInputProps.acceptDragAndDropTypes.value();
      NSMutableArray<NSString *> *array = [NSMutableArray arrayWithCapacity:vector.size()];
      for (const std::string &str : vector) {
        [array addObject:[NSString stringWithUTF8String:str.c_str()]];
      }
      _backedTextInputView.acceptDragAndDropTypes = array;
    }
  }

#if TARGET_OS_OSX // [macOS
  if (newTextInputProps.traits.pastedTypes!= oldTextInputProps.traits.pastedTypes) {
    NSArray<NSPasteboardType> *types = RCTPasteboardTypeArrayFromProps(newTextInputProps.traits.pastedTypes);
    [_backedTextInputView setReadablePasteBoardTypes:types];
  }

  if (newTextInputProps.enableFocusRing != oldTextInputProps.enableFocusRing) {
    if ([_backedTextInputView respondsToSelector:@selector(setEnableFocusRing:)]) {
      [_backedTextInputView setEnableFocusRing:newTextInputProps.enableFocusRing];
    }
  }
#endif // macOS]

  [super updateProps:props oldProps:oldProps];

#if TARGET_OS_IOS // [macOS] [visionOS]
  [self setDefaultInputAccessoryView];
#endif // [macOS] [visionOS]
}

- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
  _state = std::static_pointer_cast<const TextInputShadowNode::ConcreteState>(state);

  if (!_state) {
    assert(false && "State is `null` for <TextInput> component.");
    _backedTextInputView.attributedText = nil;
    return;
  }

  auto data = _state->getData();

  if (!oldState) {
    _mostRecentEventCount = _state->getData().mostRecentEventCount;
  }

  if (_mostRecentEventCount == _state->getData().mostRecentEventCount) {
    _comingFromJS = YES;
    [self _setAttributedString:RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox)];
    _comingFromJS = NO;
  }
}

- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
           oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];

#if TARGET_OS_OSX // [macOS
  _backedTextInputView.pointScaleFactor = layoutMetrics.pointScaleFactor;
#endif // macOS]
  _backedTextInputView.frame =
      UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
  _backedTextInputView.textContainerInset =
      RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);

  if (!CGSizeEqualToSize(_previousContentSize, _backedTextInputView.contentSize) && _eventEmitter) {
    _previousContentSize = _backedTextInputView.contentSize;
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onContentSizeChange([self _textInputMetrics]);
  }
}

- (void)prepareForRecycle
{
  [super prepareForRecycle];
  _state.reset();
  _backedTextInputView.attributedText = nil;
  _mostRecentEventCount = 0;
  _comingFromJS = NO;
  _lastStringStateWasUpdatedWith = nil;
  _ignoreNextTextInputCall = NO;
  _didMoveToWindow = NO;
#if TARGET_OS_OSX // [macOS
  _ghostText = nil;
  _ghostTextPosition = 0;
#endif // macOS]
  [_backedTextInputView resignFirstResponder];
}

#pragma mark - RCTBackedTextInputDelegate

- (BOOL)textInputShouldBeginEditing
{
  return YES;
}

- (void)textInputDidBeginEditing
{
  if (_eventEmitter) {
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onFocus([self _textInputMetrics]);
  }
}

- (BOOL)textInputShouldEndEditing
{
  return YES;
}

- (void)textInputDidEndEditing
{
#if TARGET_OS_OSX // [macOS
  [self setGhostText:nil];
#endif // macOS]

  if (_eventEmitter) {
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onEndEditing([self _textInputMetrics]);
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onBlur([self _textInputMetrics]);
  }
}

- (BOOL)textInputShouldSubmitOnReturn
{
  const SubmitBehavior submitBehavior = [self getSubmitBehavior];
  const BOOL shouldSubmit = submitBehavior == SubmitBehavior::Submit || submitBehavior == SubmitBehavior::BlurAndSubmit;
  // We send `submit` event here, in `textInputShouldSubmitOnReturn`
  // (not in `textInputDidReturn)`, because of semantic of the event:
  // `onSubmitEditing` is called when "Submit" button
  // (the blue key on onscreen keyboard) did pressed
  // (no connection to any specific "submitting" process).

  if (_eventEmitter && shouldSubmit) {
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onSubmitEditing([self _textInputMetrics]);
  }
  return shouldSubmit;
}

- (BOOL)textInputShouldReturn
{
  return [self getSubmitBehavior] == SubmitBehavior::BlurAndSubmit;
}

- (void)textInputDidReturn
{
  // Does nothing.
}

- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
{
#if TARGET_OS_OSX // [macOS
  // Clear ghost text before the text change so the undo manager's snapshot
  // of the pre-edit state never contains ghost text.
  [self setGhostText:nil];
#endif // macOS]

  const auto &props = static_cast<const TextInputProps &>(*_props);

  if (!_backedTextInputView.textWasPasted) {
    if (_eventEmitter) {
      const auto &textInputEventEmitter = static_cast<const TextInputEventEmitter &>(*_eventEmitter);
      textInputEventEmitter.onKeyPress({
          .text = RCTStringFromNSString(text),
          .eventCount = static_cast<int>(_mostRecentEventCount),
      });
    }
  }

  if (props.maxLength < std::numeric_limits<int>::max()) {
    NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length;

    if (allowedLength > 0 && text.length > allowedLength) {
      // make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off
      NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex:allowedLength - 1];
      if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) {
        // the character at the length limit takes more than 16bits, truncation should end at the character before
        allowedLength = cutOffCharacterRange.location;
      }
    }

    if (allowedLength <= 0) {
      return nil;
    }

    return allowedLength > text.length ? text : [text substringToIndex:allowedLength];
  }

  return text;
}

- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
  return YES;
}

- (void)textInputDidChange
{
  if (_comingFromJS) {
    return;
  }

#if TARGET_OS_OSX // [macOS
  if (_ghostText != nil) {
    NSAttributedString *attributedStringWithoutGhostText = [self removingGhostTextFromString:_backedTextInputView.attributedText strict:NO];
    if (attributedStringWithoutGhostText != nil && ![attributedStringWithoutGhostText isEqual:_backedTextInputView.attributedText]) {
      _backedTextInputView.attributedText = attributedStringWithoutGhostText;
    }
    _ghostText = nil;
    _ghostTextPosition = 0;
  }
#endif // macOS]

  if (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
    _ignoreNextTextInputCall = NO;
    return;
  }

  [self _updateState];

  if (_eventEmitter) {
    const auto &textInputEventEmitter = static_cast<const TextInputEventEmitter &>(*_eventEmitter);
    textInputEventEmitter.onChange([self _textInputMetrics]);
  }
}

- (void)textInputDidChangeSelection
{
#if TARGET_OS_OSX // [macOS
  // Clear ghost text on any user selection change, matching Paper behavior.
  // This prevents the user from selecting ghost text.
  if (_ghostText != nil && !_comingFromJS && !_backedTextInputView.ghostTextChanging) {
    [self setGhostText:nil];
  }
#endif // macOS]

  if (_comingFromJS) {
    return;
  }

  // T207198334: Setting a new AttributedString (_comingFromJS) will trigger a selection change before the backing
  // string is updated, so indicies won't point to what we want yet. Only respond to user selection change, and let
  // `_setAttributedString` handle updating typing attributes if content changes.
  [self _updateTypingAttributes];

  const auto &props = static_cast<const TextInputProps &>(*_props);
  if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
    [self textInputDidChange];
    _ignoreNextTextInputCall = YES;
  }

  if (_eventEmitter) {
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onSelectionChange([self _textInputMetrics]);
  }
}

#if TARGET_OS_OSX // [macOS
- (void)setEnableFocusRing:(BOOL)enableFocusRing {
  [super setEnableFocusRing:enableFocusRing];
  if ([_backedTextInputView respondsToSelector:@selector(setEnableFocusRing:)]) {
    [_backedTextInputView setEnableFocusRing:enableFocusRing];
  }
}

- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled {
  if (_eventEmitter) {
    std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onAutoCorrectChange({.autoCorrectEnabled = static_cast<bool>(enabled)});
  }
}

- (void)continuousSpellCheckingDidChange:(BOOL)enabled
{
  if (_eventEmitter) {
    std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSpellCheckChange({.spellCheckEnabled = static_cast<bool>(enabled)});
  }
}

- (void)grammarCheckingDidChange:(BOOL)enabled
{
  if (_eventEmitter) {
    std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onGrammarCheckChange({.grammarCheckEnabled = static_cast<bool>(enabled)});
  }
}

- (void)submitOnKeyDownIfNeeded:(nonnull NSEvent *)event
{
  BOOL shouldSubmit = NO;
  NSDictionary *keyEvent = [RCTViewKeyboardEvent bodyFromEvent:event];
  auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
  if (props.traits.submitKeyEvents.empty()) {
    shouldSubmit = [keyEvent[@"key"] isEqualToString:@"Enter"]
      && ![keyEvent[@"altKey"] boolValue]
      && ![keyEvent[@"shiftKey"] boolValue]
      && ![keyEvent[@"ctrlKey"] boolValue]
      && ![keyEvent[@"metaKey"] boolValue]
      && ![keyEvent[@"functionKey"] boolValue]; // Default clearTextOnSubmit key
  } else {
    NSString *keyValue = keyEvent[@"key"];
    const char *keyCString = [keyValue UTF8String];
    if (keyCString != nullptr) {
      std::string_view key(keyCString);
      const bool altKey = [keyEvent[@"altKey"] boolValue];
      const bool shiftKey = [keyEvent[@"shiftKey"] boolValue];
      const bool ctrlKey = [keyEvent[@"ctrlKey"] boolValue];
      const bool metaKey = [keyEvent[@"metaKey"] boolValue];
      const bool functionKey = [keyEvent[@"functionKey"] boolValue];

      shouldSubmit = std::any_of(
        props.traits.submitKeyEvents.begin(),
        props.traits.submitKeyEvents.end(),
        [&](auto const &submitKeyEvent) {
          return submitKeyEvent.key == key && submitKeyEvent.altKey == altKey &&
            submitKeyEvent.shiftKey == shiftKey && submitKeyEvent.ctrlKey == ctrlKey &&
            submitKeyEvent.metaKey == metaKey && submitKeyEvent.functionKey == functionKey;
      });
    }
  }

  if (shouldSubmit) {
    if (_eventEmitter) {
      auto const &textInputEventEmitter = *std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter);
      textInputEventEmitter.onSubmitEditing([self _textInputMetrics]);
    }

    if (props.traits.clearTextOnSubmit) {
      _backedTextInputView.attributedText = nil;
      [self textInputDidChange];
    }
  }
}

- (void)textInputDidCancel
{
  if (_eventEmitter) {
    auto const &textInputEventEmitter = *std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter);
    textInputEventEmitter.onKeyPress({
      .text = RCTStringFromNSString(kEscapeKeyCode),
      .eventCount = static_cast<int>(_mostRecentEventCount),
    });
  }

  [self textInputDidEndEditing];
}

- (NSDragOperation)textInputDraggingEntered:(nonnull id<NSDraggingInfo>)draggingInfo {
  if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) {
    return [self draggingEntered:draggingInfo];
  }
  return NSDragOperationNone;
}

- (void)textInputDraggingExited:(nonnull id<NSDraggingInfo>)draggingInfo {
  if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) {
    [self draggingExited:draggingInfo];
  }
}

- (BOOL)textInputShouldHandleDragOperation:(nonnull id<NSDraggingInfo>)draggingInfo {
  if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) {
    [self performDragOperation:draggingInfo];
    return NO;
  }

  return YES;
}

- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id<RCTBackedTextInputViewProtocol>)sender {
  return YES;
}

- (BOOL)textInputShouldHandleDeleteForward:(nonnull id<RCTBackedTextInputViewProtocol>)sender {
  return YES;
}

- (BOOL)textInputShouldHandleKeyEvent:(nonnull NSEvent *)event {
  return ![self handleKeyboardEvent:event];
}

- (BOOL)textInputShouldHandlePaste:(nonnull id<RCTBackedTextInputViewProtocol>)sender {
  NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
  NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]];
  NSArray<NSPasteboardType>* pastedTypes = ((RCTUITextView*) _backedTextInputView).readablePasteboardTypes;

  // If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste
  if (_eventEmitter && fileType != nil && [pastedTypes containsObject:fileType]) {
    auto const &textInputEventEmitter = *std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter);
  DataTransfer dataTransfer = [self dataTransferForPasteboard:pasteboard];
  textInputEventEmitter.onPaste({.dataTransfer = std::move(dataTransfer)});
  }

  // Only allow pasting text.
  return fileType == nil;
}

#endif // macOS]

#pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate)

- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS]
{
  if (_eventEmitter) {
#if !TARGET_OS_OSX // [macOS]
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onScroll([self _textInputMetrics]);
#else // [macOS
    static_cast<const TextInputEventEmitter &>(*_eventEmitter).onScroll([self _textInputMetricsWithScrollView:scrollView]);
#endif // macOS]
  }
}

#if TARGET_OS_OSX // [macOS
#pragma mark - Ghost Text

- (NSDictionary<NSAttributedStringKey, id> *)ghostTextAttributes
{
  NSMutableDictionary<NSAttributedStringKey, id> *textAttributes =
      [_backedTextInputView.defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];

  [textAttributes setValue:_backedTextInputView.placeholderColor ?: [RCTPlatformColor placeholderTextColor]
                    forKey:NSForegroundColorAttributeName];

  return textAttributes;
}

- (void)setGhostText:(NSString *)ghostText
{
  NSRange selectedRange = [_backedTextInputView selectedTextRange];
  NSInteger selectionStart = selectedRange.location;
  NSInteger selectionEnd = selectedRange.location + selectedRange.length;
  NSString *newGhostText = ghostText.length > 0 ? ghostText : nil;

  if (selectionStart != selectionEnd) {
    newGhostText = nil;
  }

  if ((_ghostText == nil && newGhostText == nil) || [_ghostText isEqual:newGhostText]) {
    return;
  }

  if (_backedTextInputView.ghostTextChanging) {
    // look out for nested callbacks -- this can happen for example when selection changes in response to
    // attributed text changing. Such callbacks are initiated by Apple, or we could suppress this other ways.
    return;
  }

  _backedTextInputView.ghostTextChanging = YES;

  if (_ghostText != nil) {
    // When setGhostText: is called after making a standard edit, the ghost text may already be gone
    BOOL ghostTextMayAlreadyBeGone = newGhostText == nil;
    NSAttributedString *attributedStringWithoutGhostText = [self removingGhostTextFromString:_backedTextInputView.attributedText strict:!ghostTextMayAlreadyBeGone];

    if (attributedStringWithoutGhostText != nil) {
      _backedTextInputView.attributedText = attributedStringWithoutGhostText;
      [_backedTextInputView setSelectedTextRange:NSMakeRange(selectionStart, selectionEnd - selectionStart) notifyDelegate:NO];
    }
  }

  _ghostText = [newGhostText copy];
  _ghostTextPosition = selectionStart;

  if (_ghostText != nil) {
    NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy];
    NSAttributedString *ghostAttributedString = [[NSAttributedString alloc] initWithString:_ghostText
                                                                                attributes:self.ghostTextAttributes];

    [attributedString insertAttributedString:ghostAttributedString atIndex:_ghostTextPosition];
    _backedTextInputView.attributedText = attributedString;
    [_backedTextInputView setSelectedTextRange:NSMakeRange(_ghostTextPosition, 0) notifyDelegate:NO];
  }

  _backedTextInputView.ghostTextChanging = NO;
}

/**
 * Attempts to remove the ghost text from a provided string given our current state.
 *
 * If `strict` mode is enabled, this method assumes the ghost text exists exactly
 * where we expect it to be. We assert and return `nil` if we don't find the expected ghost text.
 * It's the responsibility of the caller to make sure the result isn't `nil`.
 *
 * If disabled, we allow for the possibility that the ghost text has already been removed,
 * which can happen if a delegate callback is trying to remove ghost text after invoking `setAttributedText:`.
 */
- (NSAttributedString *)removingGhostTextFromString:(NSAttributedString *)string strict:(BOOL)strict
{
  if (_ghostText == nil) {
    return string;
  }

  NSRange ghostTextRange = NSMakeRange(_ghostTextPosition, _ghostText.length);
  NSMutableAttributedString *attributedString = [string mutableCopy];

  if ([attributedString length] < NSMaxRange(ghostTextRange)) {
    if (strict) {
      RCTAssert(false, @"Ghost text not fully present in text view text");
      return nil;
    } else {
      return string;
    }
  }

  NSString *actualGhostText = [[attributedString attributedSubstringFromRange:ghostTextRange] string];

  if (![actualGhostText isEqual:_ghostText]) {
    if (strict) {
      RCTAssert(false, @"Ghost text does not match text view text");
      return nil;
    } else {
      return string;
    }
  }

  [attributedString deleteCharactersInRange:ghostTextRange];
  return attributedString;
}

#endif // macOS]

#pragma mark - Native Commands

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
  RCTTextInputHandleCommand(self, commandName, args);
}

- (void)focus
{
#if !TARGET_OS_OSX // [macOS]
  [_backedTextInputView becomeFirstResponder];
#else // [macOS
  NSWindow *window = [_backedTextInputView window];
  NSResponder *responder = [_backedTextInputView respondsToSelector:@selector(responder)]
      ? _backedTextInputView.responder
      : (NSResponder *)_backedTextInputView;
  [window makeFirstResponder:responder];
#endif // macOS]

  const auto &props = static_cast<const TextInputProps &>(*_props);

  if (props.traits.clearTextOnFocus) {
    _backedTextInputView.attributedText = nil;
    [self textInputDidChange];
  }

  if (props.traits.selectTextOnFocus) {
    [_backedTextInputView selectAll:nil];
    [self textInputDidChangeSelection];
  }

  [self scrollCursorIntoView];
}

- (void)blur
{
#if !TARGET_OS_OSX // [macOS]
  [_backedTextInputView resignFirstResponder];
#else // [macOS
  NSWindow *window = [_backedTextInputView window];
  // On macOS, when an NSTextField is focused, the window's firstResponder is the
  // field editor (an NSTextView), not the text field itself. Check currentEditor
  // to determine if the text field is actively being edited.
  if ([_backedTextInputView isKindOfClass:[NSTextField class]] &&
      [(NSTextField *)_backedTextInputView currentEditor] != nil) {
    [window makeFirstResponder:nil];
  } else {
    NSResponder *responder = [_backedTextInputView respondsToSelector:@selector(responder)]
        ? _backedTextInputView.responder
        : (NSResponder *)_backedTextInputView;
    if ([window firstResponder] == responder) {
      [window makeFirstResponder:nil];
    }
  }
#endif // macOS]
}

- (void)setTextAndSelection:(NSInteger)eventCount
                      value:(NSString *__nullable)value
                      start:(NSInteger)start
                        end:(NSInteger)end
{
  if (_mostRecentEventCount != eventCount) {
    return;
  }
  _comingFromJS = YES;
  if (value && ![value isEqualToString:_backedTextInputView.attributedText.string]) {
    NSAttributedString *attributedString =
        [[NSAttributedString alloc] initWithString:value attributes:_backedTextInputView.defaultTextAttributes];
    [self _setAttributedString:attributedString];
    [self _updateState];
  }

#if !TARGET_OS_OSX // [macOS]
  UITextPosition *startPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
                                                                      offset:start];
  UITextPosition *endPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
                                                                    offset:end];

  if (startPosition && endPosition) {
    UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
    [_backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
    // ensure we scroll to the selected position
    NSInteger offsetEnd = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                        toPosition:range.end];
    [_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetEnd, 0)];
  }
#else // [macOS
  NSInteger startPosition = MIN(start, end);
  NSInteger endPosition = MAX(start, end);
  [_backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:NO];
#endif // macOS]
  _comingFromJS = NO;
}

#pragma mark - Default input accessory view

#if TARGET_OS_IOS // [macOS] [visionOS] Input Accessory Views are only a concept on iOS
- (NSString *)returnKeyTypeToString:(UIReturnKeyType)returnKeyType
{
  switch (returnKeyType) {
    case UIReturnKeyGo:
      return @"Go";
    case UIReturnKeyNext:
      return @"Next";
    case UIReturnKeySearch:
      return @"Search";
    case UIReturnKeySend:
      return @"Send";
    case UIReturnKeyYahoo:
      return @"Yahoo";
    case UIReturnKeyGoogle:
      return @"Google";
    case UIReturnKeyRoute:
      return @"Route";
    case UIReturnKeyJoin:
      return @"Join";
    case UIReturnKeyEmergencyCall:
      return @"Emergency Call";
    default:
      return @"Done";
  }
}

- (void)initializeReturnKeyType
{
  returnKeyTypesSet = [NSSet setWithObjects:@(UIReturnKeyDone),
                                            @(UIReturnKeyGo),
                                            @(UIReturnKeyNext),
                                            @(UIReturnKeySearch),
                                            @(UIReturnKeySend),
                                            @(UIReturnKeyYahoo),
                                            @(UIReturnKeyGoogle),
                                            @(UIReturnKeyRoute),
                                            @(UIReturnKeyJoin),
                                            @(UIReturnKeyRoute),
                                            @(UIReturnKeyEmergencyCall),
                                            nil];
}

- (void)setDefaultInputAccessoryView
{
  // InputAccessoryView component sets the inputAccessoryView when inputAccessoryViewID exists
  if (_backedTextInputView.inputAccessoryViewID) {
    if (_backedTextInputView.isFirstResponder) {
      [_backedTextInputView reloadInputViews];
    }
    return;
  }

  UIKeyboardType keyboardType = _backedTextInputView.keyboardType;
  UIReturnKeyType returnKeyType = _backedTextInputView.returnKeyType;
  NSString *inputAccessoryViewButtonLabel = _backedTextInputView.inputAccessoryViewButtonLabel;

  BOOL containsKeyType = [returnKeyTypesSet containsObject:@(returnKeyType)];
  BOOL containsInputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel != nil;

  // These keyboard types (all are number pads) don't have a "returnKey" button by default,
  // so we create an `inputAccessoryView` with this button for them.
  BOOL shouldHaveInputAccessoryView =
      (keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad ||
       keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) &&
      (containsKeyType || containsInputAccessoryViewButtonLabel);

  if (_hasInputAccessoryView == shouldHaveInputAccessoryView) {
    return;
  }

  _hasInputAccessoryView = shouldHaveInputAccessoryView;

  if (shouldHaveInputAccessoryView) {
    NSString *buttonLabel = inputAccessoryViewButtonLabel != nil ? inputAccessoryViewButtonLabel
                                                                 : [self returnKeyTypeToString:returnKeyType];

    UIToolbar *toolbarView = [UIToolbar new];
    [toolbarView sizeToFit];
    UIBarButtonItem *flexibleSpace =
        [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:buttonLabel
                                                                   style:UIBarButtonItemStylePlain
                                                                  target:self
                                                                  action:@selector(handleInputAccessoryDoneButton)];
    toolbarView.items = @[ flexibleSpace, doneButton ];
    _backedTextInputView.inputAccessoryView = toolbarView;
  } else {
    _backedTextInputView.inputAccessoryView = nil;
  }

  if (_backedTextInputView.isFirstResponder) {
    [_backedTextInputView reloadInputViews];
  }
}

- (void)handleInputAccessoryDoneButton
{
  // Ignore the value of whether we submitted; just make sure the submit event is called if necessary.
  [self textInputShouldSubmitOnReturn];
  if ([self textInputShouldReturn]) {
    [_backedTextInputView endEditing:YES];
  }
}
#endif // [macOS] [visionOS]

#pragma mark - Other

- (TextInputEventEmitter::Metrics)_textInputMetrics
{
  return {
      .text = RCTStringFromNSString(_backedTextInputView.attributedText.string),
      .selectionRange = [self _selectionRange],
      .eventCount = static_cast<int>(_mostRecentEventCount),
#if !TARGET_OS_OSX // [macOS]
      .contentOffset = RCTPointFromCGPoint(_backedTextInputView.contentOffset),
      .contentInset = RCTEdgeInsetsFromUIEdgeInsets(_backedTextInputView.contentInset),
#else // [macOS
      .contentOffset = {.x = 0, .y = 0},
      .contentInset = EdgeInsets{},
#endif // macOS]
      .contentSize = RCTSizeFromCGSize(_backedTextInputView.contentSize),
      .layoutMeasurement = RCTSizeFromCGSize(_backedTextInputView.bounds.size),
      .zoomScale = 1,
  };
}

#if TARGET_OS_OSX // [macOS
- (TextInputEventEmitter::Metrics)_textInputMetricsWithScrollView:(RCTUIScrollView *)scrollView
{
  TextInputEventEmitter::Metrics metrics = [self _textInputMetrics];

  if (scrollView) {
    metrics.contentOffset = RCTPointFromCGPoint(scrollView.contentOffset);
    metrics.contentInset = RCTEdgeInsetsFromUIEdgeInsets(scrollView.contentInset);
    metrics.contentSize = RCTSizeFromCGSize(scrollView.contentSize);
    metrics.layoutMeasurement = RCTSizeFromCGSize(scrollView.bounds.size);
    metrics.zoomScale = scrollView.zoomScale ?: 1;
  }

  return metrics;
}
#endif // macOS]


- (void)_updateState
{
  if (!_state) {
    return;
  }
  NSAttributedString *attributedString = _backedTextInputView.attributedText;
  auto data = _state->getData();
  _lastStringStateWasUpdatedWith = attributedString;
  data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString);
  _mostRecentEventCount += _comingFromJS ? 0 : 1;
  data.mostRecentEventCount = _mostRecentEventCount;
  _state->updateState(std::move(data));
}

- (AttributedString::Range)_selectionRange
{
#if !TARGET_OS_OSX // [macOS]
  UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
  NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                  toPosition:selectedTextRange.start];
  NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                toPosition:selectedTextRange.end];
  return AttributedString::Range{(int)start, (int)(end - start)};
#else // [macOS
  NSRange selectedTextRange = [_backedTextInputView selectedTextRange];
  return AttributedString::Range{(int)selectedTextRange.location, (int)selectedTextRange.length};
#endif // macOS]
}

- (void)_restoreTextSelection
{
  [self _restoreTextSelectionAndIgnoreCaretChange:NO];
}

- (void)_restoreTextSelectionAndIgnoreCaretChange:(BOOL)ignore
{
  const auto &selection = static_cast<const TextInputProps &>(*_props).selection;
  if (!selection.has_value()) {
    return;
  }
#if !TARGET_OS_OSX // [macOS]
  auto start = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
                                                   offset:selection->start];
  auto end = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:selection->end];
  auto range = [_backedTextInputView textRangeFromPosition:start toPosition:end];
  if (ignore && range.empty) {
    return;
  }
  [_backedTextInputView setSelectedTextRange:range notifyDelegate:YES];
#endif // [macOS]
}

- (void)_setAttributedString:(NSAttributedString *)attributedString
{
#if TARGET_OS_OSX // [macOS
  // When the text view displays temporary content (e.g. completions, accents), do not update the attributed string.
  if (_backedTextInputView.hasMarkedText) {
    return;
  }
#endif // macOS]

  if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) {
    return;
  }
#if !TARGET_OS_OSX // [macOS]
  UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
#else
  NSRange selectedRange = [_backedTextInputView selectedTextRange];
#endif // macOS]
  NSInteger oldTextLength = _backedTextInputView.attributedText.string.length;
  _backedTextInputView.attributedText = attributedString;
#if !TARGET_OS_OSX // [macOS]
  // Updating the UITextView attributedText, for example changing the lineHeight, the color or adding
  // a new paragraph with \n, causes the cursor to move to the end of the Text and scroll.
  // This is fixed by restoring the cursor position and scrolling to that position (iOS issue 652653).
  // Maintaining a cursor position relative to the end of the old text.
  NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                        toPosition:selectedRange.start];
  NSInteger offsetFromEnd = oldTextLength - offsetStart;
  NSInteger newOffset = attributedString.string.length - offsetFromEnd;
  UITextPosition *position = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
                                                                 offset:newOffset];
  [_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
                              notifyDelegate:YES];
  [_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];

  // A zero-length selection range can cause the caret position to change on iOS,
  // and we have already updated the caret position, so we can safely ignore caret changing in this place.
  [self _restoreTextSelectionAndIgnoreCaretChange:YES];
  [self _updateTypingAttributes];
  _lastStringStateWasUpdatedWith = attributedString;
#else // [macOS
  if (selectedRange.length == 0) {
    // Maintaining a cursor position relative to the end of the old text.
    NSInteger start = selectedRange.location;
    NSInteger offsetFromEnd = oldTextLength - start;
    NSInteger newOffset = _backedTextInputView.attributedText.length - offsetFromEnd;
    [_backedTextInputView setSelectedTextRange:NSMakeRange(newOffset, 0)
                                      notifyDelegate:YES];
  }
#endif // macOS]
}

// Ensure that newly typed text will inherit any custom attributes. We follow the logic of RN Android, where attributes
// to the left of the cursor are copied into new text, unless we are at the start of the field, in which case we will
// copy the attributes from text to the right. This allows consistency between backed input and new AttributedText
// https://github.com/facebook/react-native/blob/3102a58df38d96f3dacef0530e4dbb399037fcd2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt#L30
- (void)_updateTypingAttributes
{
#if !TARGET_OS_OSX // [macOS
  if (_backedTextInputView.attributedText.length > 0 && _backedTextInputView.selectedTextRange != nil) {
    NSUInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                           toPosition:_backedTextInputView.selectedTextRange.start];

    NSUInteger samplePoint = offsetStart == 0 ? 0 : offsetStart - 1;
    _backedTextInputView.typingAttributes = [_backedTextInputView.attributedText attributesAtIndex:samplePoint
                                                                                    effectiveRange:NULL];
  }
#else // [macOS
  // TODO
#endif // macOS]
}

- (void)scrollCursorIntoView
{
#if !TARGET_OS_OSX // [macOS
  UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
  if (selectedRange.empty) {
    NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
                                                          toPosition:selectedRange.start];
    [_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];
  }
#else // [macOS
  // TODO
#endif // macOS]
}

- (void)_setMultiline:(BOOL)multiline
{
  [_backedTextInputView removeFromSuperview];
#if !TARGET_OS_OSX // [macOS]
  RCTUIView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new];
#else // [macOS
  RCTPlatformView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTWrappedTextView new] : [RCTUITextField new];
#endif // macOS]
  backedTextInputView.frame = _backedTextInputView.frame;
  RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
  _backedTextInputView = backedTextInputView;
  [self addSubview:_backedTextInputView];
}

#if !TARGET_OS_OSX // [macOS]
- (void)_setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus
{
  if (showSoftInputOnFocus) {
    // Resets to default keyboard.
    _backedTextInputView.inputView = nil;

    // Without the call to reloadInputViews, the keyboard will not change until the textInput field (the first
    // responder) loses and regains focus.
    if (_backedTextInputView.isFirstResponder) {
      [_backedTextInputView reloadInputViews];
    }
  } else {
    // Hides keyboard, but keeps blinking cursor.
    _backedTextInputView.inputView = [UIView new];
  }
}
#endif // macOS]

#if TARGET_OS_OSX // [macOS
- (void)_setSecureTextEntry:(BOOL)secureTextEntry
{
  [_backedTextInputView removeFromSuperview];
  RCTPlatformView<RCTBackedTextInputViewProtocol> *backedTextInputView = secureTextEntry ? [RCTUISecureTextField new] : [RCTUITextField new];
  backedTextInputView.frame = _backedTextInputView.frame;
  RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);

  // Copy the text field specific properties if we came from a single line input before the switch
  if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) {
    RCTUITextField *previousTextField = (RCTUITextField *)_backedTextInputView;
    RCTUITextField *newTextField = (RCTUITextField *)backedTextInputView;
    newTextField.textAlignment = previousTextField.textAlignment;
    newTextField.text = previousTextField.text;
  }

  _backedTextInputView = backedTextInputView;
  [self addSubview:_backedTextInputView];
}
#endif // macOS]

- (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText
{
  // When the dictation is running we can't update the attributed text on the backed up text view
  // because setting the attributed string will kill the dictation. This means that we can't impose
  // the settings on a dictation.
  // Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
  // text that we should disregard. See
  // https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc for more info.
  // Also, updating the attributed text while inputting Korean language will break input mechanism.
  // If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in
  // NSOriginalFont. Lastly, when entering a password, etc., there will be additional styling on the field as the native
  // text view handles showing the last character for a split second.
  __block BOOL fontHasBeenUpdatedBySystem = false;
  [oldText enumerateAttribute:@"NSOriginalFont"
                      inRange:NSMakeRange(0, oldText.length)
                      options:0
                   usingBlock:^(id value, NSRange range, BOOL *stop) {
                     if (value) {
                       fontHasBeenUpdatedBySystem = true;
                     }
                   }];

  BOOL shouldFallbackToBareTextComparison =
#if !TARGET_OS_OSX // [macOS]
  [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
  [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] ||
  _backedTextInputView.markedTextRange ||
  _backedTextInputView.isSecureTextEntry ||
#else // [macOS
  // There are multiple Korean input sources (2-Set, 3-Set, etc). Check substring instead instead
  [[[_backedTextInputView inputContext] selectedKeyboardInputSource] containsString:@"com.apple.inputmethod.Korean"] ||
  [_backedTextInputView hasMarkedText] ||
  [_backedTextInputView isKindOfClass:[RCTUISecureTextField class]] ||
#endif // macOS]
  fontHasBeenUpdatedBySystem;

  if (shouldFallbackToBareTextComparison) {
    return [newText.string isEqualToString:oldText.string];
  } else {
    return RCTIsAttributedStringEffectivelySame(
        newText, oldText, _originalTypingAttributes, static_cast<const TextInputProps &>(*_props).textAttributes);
  }
}

- (SubmitBehavior)getSubmitBehavior
{
  const auto &props = static_cast<const TextInputProps &>(*_props);
  return props.getNonDefaultSubmitBehavior();
}

@end

Class<RCTComponentViewProtocol> RCTTextInputCls(void)
{
  return RCTTextInputComponentView.class;
}
