#import "RNGestureHandlerButtonComponentView.h"

#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>

#import <react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h>
#import <react/renderer/components/rngesturehandler_codegen/EventEmitters.h>
#import <react/renderer/components/rngesturehandler_codegen/Props.h>
#import <react/renderer/components/rngesturehandler_codegen/RCTComponentViewHelpers.h>
#import <react/renderer/components/view/BaseViewProps.h>
#import <react/renderer/components/view/ViewProps.h>

#import "RNGestureHandlerButton.h"
#import "RNGestureHandlerModule.h"

using namespace facebook::react;

static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::PointerEventsMode pointerEvents)
{
  switch (pointerEvents) {
    case facebook::react::PointerEventsMode::None:
      return RNGestureHandlerPointerEventsNone;
    case facebook::react::PointerEventsMode::BoxNone:
      return RNGestureHandlerPointerEventsBoxNone;
    case facebook::react::PointerEventsMode::BoxOnly:
      return RNGestureHandlerPointerEventsBoxOnly;
    case facebook::react::PointerEventsMode::Auto:
    default:
      return RNGestureHandlerPointerEventsAuto;
  }
}

@interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonViewProtocol, RNGHButtonEventDelegate>
@end

@implementation RNGestureHandlerButtonComponentView {
  RNGestureHandlerButton *_buttonView;
  BOOL _needsAnimationStateReset;
  int _moduleId;
  NSNumber *_managedHandlerTag;
}

#if TARGET_OS_OSX
// Here we want to disable view recycling on buttons. Listeners are not removed from views when they're being unmounted,
// therefore after navigating through other screens buttons may have different actions then they are supposed to have.
+ (BOOL)shouldBeRecycled
{
  return NO;
}
#endif

// Needed because of this: https://github.com/facebook/react-native/pull/37274
#ifdef RCT_DYNAMIC_FRAMEWORKS
+ (void)load
{
  [super load];
}
#endif

- (instancetype)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame]) {
    static const auto defaultProps = std::make_shared<const RNGestureHandlerButtonProps>();
    _props = defaultProps;
    _moduleId = -1;
    _buttonView = [[RNGestureHandlerButton alloc] initWithFrame:self.bounds];
    _buttonView.animationTarget = self;
    _buttonView.eventDelegate = self;

    self.contentView = _buttonView;
  }

  return self;
}

#if TARGET_OS_TV
- (void)emitPressInEvent
{
  if (!_buttonView.userEnabled) {
    return;
  }

  [_buttonView sendActionsForControlEvents:UIControlEventTouchDown];
}

- (void)emitPressOutEvent
{
  if (!_buttonView.userEnabled) {
    return;
  }

  [_buttonView sendActionsForControlEvents:UIControlEventTouchUpInside];
}

- (void)animatePressIn
{
  if (!_buttonView.userEnabled) {
    return;
  }

  [_buttonView handleAnimatePressIn];
}

- (void)animatePressOut
{
  if (!_buttonView.userEnabled) {
    return;
  }

  [_buttonView handleAnimatePressOut];
}

- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
       withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
  if (context.nextFocusedView == self) {
    [_buttonView onHoverIn];
  } else if (context.previouslyFocusedView == self) {
    [_buttonView onHoverOut];
  }
  [super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
}
#endif // TARGET_OS_TV

- (void)prepareForRecycle
{
  [self dropManagedHandler];

  [self.layer removeAnimationForKey:@"transform"];
  self.layer.transform = CATransform3DIdentity;

  [_buttonView prepareForRecycle];

  // The reset above forces this wrapper's alpha and transform back to neutral
  // values, but Fabric retains `_props` across recycling and `updateProps:`
  // re-applies opacity/transform only when they differ from the retained
  // props. Re-sync the layer from the retained props so the next mount's diff
  // stays valid (the same approach RN takes for Animated-managed props in its
  // `prepareForRecycle`); otherwise a remount with an unchanged style opacity
  // keeps the neutral 1.0 instead (https://github.com/software-mansion/react-native-gesture-handler/issues/4353).
  const auto &viewProps = static_cast<const ViewProps &>(*_props);
  self.layer.opacity = (float)viewProps.opacity;
  self.layer.transform = RCTCATransform3DFromTransformMatrix(viewProps.resolveTransform(_layoutMetrics));

  // Force the next updateProps: to re-run applyStartAnimationState even if
  // the new mount's defaults match the previous mount's.
  _needsAnimationStateReset = YES;

  [super prepareForRecycle];
}

- (void)mountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
  [_buttonView mountChildComponentView:childComponentView index:index];
}

- (void)unmountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView
                            index:(NSInteger)__unused index
{
  if (childComponentView.superview == _buttonView) {
    [childComponentView removeFromSuperview];
  }
}

- (LayoutMetrics)buildWrapperMetrics:(const LayoutMetrics &)metrics
{
  LayoutMetrics result = metrics;
  result.borderWidth = EdgeInsets::ZERO;
  result.contentInsets = EdgeInsets::ZERO;
  return result;
}

- (LayoutMetrics)buildButtonMetrics:(const LayoutMetrics &)metrics
{
  LayoutMetrics result = metrics;
  result.frame.origin = {0, 0};
  return result;
}

- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
           oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
  // due to nested structure of Button and ComponentView, layout metrics for both
  // need to be modified:
  // - wrapper shouldn't have any insets as they should be applied to the button
  //   so that it can intercept touches on padding and borders, applying them
  //   twice breaks expected layout
  // - frame origin needs to be zeroes on metrics of the button as it should fill
  //   the entirety of the wrapper component
  const LayoutMetrics wrapperMetrics = [self buildWrapperMetrics:layoutMetrics];
  const LayoutMetrics oldWrapperMetrics = [self buildWrapperMetrics:oldLayoutMetrics];

  const LayoutMetrics buttonMetrics = [self buildButtonMetrics:layoutMetrics];
  const LayoutMetrics oldbuttonMetrics = [self buildButtonMetrics:oldLayoutMetrics];

  // The press-in animation sets a scale transform on `self.layer` (animationTarget
  // is this wrapper). RN's layout path sets `self.frame = frame`, which is undefined
  // behavior when the layer's transform is non-identity, so mid-press child re-layouts
  // get squished against the old bounds before snapping to the new ones. Neutralize
  // the transform and any in-flight animation around super's frame update, then
  // restore both atomically within the same transaction.
  CATransform3D savedTransform = self.layer.transform;
  CAAnimation *savedTransformAnimation = [[self.layer animationForKey:@"transform"] copy];
  BOOL hasPendingTransform = !CATransform3DIsIdentity(savedTransform) || savedTransformAnimation != nil;

  if (hasPendingTransform) {
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    [self.layer removeAnimationForKey:@"transform"];
    self.layer.transform = CATransform3DIdentity;
  }

  [super updateLayoutMetrics:wrapperMetrics oldLayoutMetrics:oldWrapperMetrics];

  if (hasPendingTransform) {
    self.layer.transform = savedTransform;
    if (savedTransformAnimation) {
      [self.layer addAnimation:savedTransformAnimation forKey:@"transform"];
    }
    [CATransaction commit];
  }

  [_buttonView updateLayoutMetrics:buttonMetrics oldLayoutMetrics:oldbuttonMetrics];
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
  // super's invalidateLayer (called via finalizeUpdates) unconditionally sets
  // self.layer.opacity to the React style.opacity, overwriting our
  // applyStartAnimationState alpha and interrupting in-flight press
  // animations. Save/restore around super, but only touch what super actually
  // disturbed — re-adding an unchanged animation resets its progress.
  float savedOpacity = self.layer.opacity;
  CAAnimation *savedOpacityAnimation = [self.layer animationForKey:@"opacity"];

  [super finalizeUpdates:updateMask];

  BOOL opacityChanged = savedOpacity != self.layer.opacity;
  BOOL animationChanged = savedOpacityAnimation != [self.layer animationForKey:@"opacity"];
  if (opacityChanged || animationChanged) {
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    if (animationChanged) {
      [self.layer removeAnimationForKey:@"opacity"];
      if (savedOpacityAnimation) {
        [self.layer addAnimation:savedOpacityAnimation forKey:@"opacity"];
      }
    }
    if (opacityChanged) {
      self.layer.opacity = savedOpacity;
    }
    [CATransaction commit];
  }

  // Resolve per-corner border radii from props and forward to the button
  // so its underlay CALayer gets the matching shape.
  const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
  [_buttonView setUnderlayCornerRadiiWithTopLeftHorizontal:borderMetrics.borderRadii.topLeft.horizontal
                                           topLeftVertical:borderMetrics.borderRadii.topLeft.vertical
                                        topRightHorizontal:borderMetrics.borderRadii.topRight.horizontal
                                          topRightVertical:borderMetrics.borderRadii.topRight.vertical
                                      bottomLeftHorizontal:borderMetrics.borderRadii.bottomLeft.horizontal
                                        bottomLeftVertical:borderMetrics.borderRadii.bottomLeft.vertical
                                     bottomRightHorizontal:borderMetrics.borderRadii.bottomRight.horizontal
                                       bottomRightVertical:borderMetrics.borderRadii.bottomRight.vertical];
  [_buttonView setUnderlayBorderInsetsWithTop:borderMetrics.borderWidths.top
                                        right:borderMetrics.borderWidths.right
                                       bottom:borderMetrics.borderWidths.bottom
                                         left:borderMetrics.borderWidths.left];
}

#pragma mark - Managed gesture handler

- (NSDictionary *)buildManagedHandlerConfig:(const RNGestureHandlerButtonProps &)props
{
  NSMutableDictionary *config = [NSMutableDictionary new];
  config[@"shouldActivateOnStart"] = @NO;
  config[@"disallowInterruption"] = @YES;
  config[@"yieldsToContinuousGestures"] = @YES;
  config[@"enabled"] = @(props.enabled);
  config[@"shouldCancelWhenOutside"] = @(props.cancelOnLeave);

  if (!props.gestureTestID.empty()) {
    config[@"testID"] = RCTNSStringFromString(props.gestureTestID);
  }

  // `gestureHitSlop` is optional on the JS side, but the generated props struct cannot carry
  // null — an unset (or explicitly nulled out) prop arrives zero-initialized, i.e. 0 on every
  // edge. That is equivalent to no hit slop, so the key is left out of the config entirely
  // and the handler keeps its unset default instead of hit-testing against an identical frame.
  if (props.gestureHitSlop.top != 0 || props.gestureHitSlop.left != 0 || props.gestureHitSlop.bottom != 0 ||
      props.gestureHitSlop.right != 0) {
    // Matches the normalized `[left, top, right, bottom, width, height]` layout the JS side sends;
    // the button only exposes the four edges, so width and height are always unset.
    config[@"hitSlop"] = @[
      @(props.gestureHitSlop.left),
      @(props.gestureHitSlop.top),
      @(props.gestureHitSlop.right),
      @(props.gestureHitSlop.bottom),
      [NSNull null],
      [NSNull null],
    ];
  }

  return config;
}

- (void)updateManagedHandler:(const RNGestureHandlerButtonProps &)newProps
                    oldProps:(const RNGestureHandlerButtonProps *)oldProps
{
  if (newProps.handlerTag <= 0) {
    [self dropManagedHandler];
    return;
  }

  RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
  react_native_assert(manager != nil && "Tried to access a non-existent handler manager");

  BOOL tagChanged = _managedHandlerTag == nil || [_managedHandlerTag doubleValue] != newProps.handlerTag;

  if (tagChanged) {
    [self dropManagedHandler];

    NSNumber *handlerTag = @(newProps.handlerTag);
    [manager createGestureHandler:@"NativeViewGestureHandler"
                              tag:handlerTag
                           config:[self buildManagedHandlerConfig:newProps]];

    // Events dispatched by the handler carry the view's reactTag; without it the
    // dispatch is skipped (see `handleGesture:fromReset:fromManualStateChange:`).
    _buttonView.reactTag = @(self.tag);
    // The cast is needed on macOS, where the button is an NSControl and outside
    // of the RCTUIView hierarchy.
    [manager attachHandlerForDetectorWithTag:handlerTag
                                      toView:(RNGHUIView *)_buttonView
                              withActionType:RNGestureHandlerActionTypeNone
                            withHostDetector:nil];

    _managedHandlerTag = handlerTag;
    _buttonView.managedHandlerTag = handlerTag;
    return;
  }

  BOOL configChanged = oldProps == nullptr || oldProps->enabled != newProps.enabled ||
      oldProps->cancelOnLeave != newProps.cancelOnLeave || oldProps->gestureTestID != newProps.gestureTestID ||
      oldProps->gestureHitSlop.top != newProps.gestureHitSlop.top ||
      oldProps->gestureHitSlop.left != newProps.gestureHitSlop.left ||
      oldProps->gestureHitSlop.bottom != newProps.gestureHitSlop.bottom ||
      oldProps->gestureHitSlop.right != newProps.gestureHitSlop.right;

  if (configChanged) {
    // `setConfig:` resets to defaults before applying, so keys omitted from the
    // dictionary (e.g. `hitSlop`, `testID`) get cleared rather than kept.
    [manager setGestureHandlerConfig:_managedHandlerTag config:[self buildManagedHandlerConfig:newProps]];
  }
}

- (void)dropManagedHandler
{
  if (_managedHandlerTag == nil) {
    return;
  }

  RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
  [manager dropGestureHandler:_managedHandlerTag];

  _managedHandlerTag = nil;
  _buttonView.managedHandlerTag = nil;
}

- (void)dealloc
{
  // On macOS the buttons are not recycled, `prepareForRecycle` may never run.
  [self dropManagedHandler];
}

#pragma mark - RNGHButtonEventDelegate

- (void)dispatchButtonEvent:(RNGHButtonEventType)type withExtraData:(RNGestureHandlerEventExtraData *)extraData
{
  if (_eventEmitter == nullptr) {
    return;
  }

  const auto &eventEmitter = static_cast<const RNGestureHandlerButtonEventEmitter &>(*_eventEmitter);
  NSDictionary *data = extraData.data;

  // The generated event structs are distinct types with identical fields, hence
  // the generic lambda.
  auto fillEvent = [&](auto event) {
    event.pointerInside = [data[@"pointerInside"] boolValue];
    event.x = [data[@"x"] doubleValue];
    event.y = [data[@"y"] doubleValue];
    event.absoluteX = [data[@"absoluteX"] doubleValue];
    event.absoluteY = [data[@"absoluteY"] doubleValue];
    event.numberOfPointers = [data[@"numberOfPointers"] intValue];
    event.pointerType = [data[@"pointerType"] intValue];
    return event;
  };

  switch (type) {
    case RNGHButtonEventTypePress:
      eventEmitter.onButtonPress(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonPress{}));
      break;
    case RNGHButtonEventTypePressIn:
      eventEmitter.onButtonPressIn(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonPressIn{}));
      break;
    case RNGHButtonEventTypePressOut:
      eventEmitter.onButtonPressOut(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonPressOut{}));
      break;
    case RNGHButtonEventTypeLongPress:
      eventEmitter.onButtonLongPress(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonLongPress{}));
      break;
    case RNGHButtonEventTypeHoverIn:
      eventEmitter.onButtonHoverIn(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonHoverIn{}));
      break;
    case RNGHButtonEventTypeHoverOut:
      eventEmitter.onButtonHoverOut(fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonHoverOut{}));
      break;
    case RNGHButtonEventTypeInteractionFinished:
      eventEmitter.onButtonInteractionFinished(
          fillEvent(RNGestureHandlerButtonEventEmitter::OnButtonInteractionFinished{}));
      break;
  }
}

#pragma mark - RCTComponentViewProtocol

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

#if TARGET_OS_IOS
// Taken from
// https://github.com/facebook/react-native/blob/b226049a4a28ea3f7f32266269fb76340c324d42/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm#L343
- (void)setAccessibilityProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
  const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
  const auto &newButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(props);

  if (!oldProps || oldButtonProps.accessible != newButtonProps.accessible) {
    _buttonView.isAccessibilityElement = newButtonProps.accessible;
  }

  if (!oldProps || oldButtonProps.accessibilityLabel != newButtonProps.accessibilityLabel) {
    _buttonView.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newButtonProps.accessibilityLabel);
  }

  if (!oldProps || oldButtonProps.accessibilityLanguage != newButtonProps.accessibilityLanguage) {
    _buttonView.accessibilityLanguage = RCTNSStringFromStringNilIfEmpty(newButtonProps.accessibilityLanguage);
  }

  if (!oldProps || oldButtonProps.accessibilityHint != newButtonProps.accessibilityHint) {
    _buttonView.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newButtonProps.accessibilityHint);
  }

  if (!oldProps || oldButtonProps.accessibilityViewIsModal != newButtonProps.accessibilityViewIsModal) {
    _buttonView.accessibilityViewIsModal = newButtonProps.accessibilityViewIsModal;
  }

  if (!oldProps || oldButtonProps.accessibilityElementsHidden != newButtonProps.accessibilityElementsHidden) {
    _buttonView.accessibilityElementsHidden = newButtonProps.accessibilityElementsHidden;
  }

  if (!oldProps ||
      oldButtonProps.accessibilityShowsLargeContentViewer != newButtonProps.accessibilityShowsLargeContentViewer) {
    if (newButtonProps.accessibilityShowsLargeContentViewer) {
      _buttonView.showsLargeContentViewer = YES;
      UILargeContentViewerInteraction *interaction = [[UILargeContentViewerInteraction alloc] init];
      [_buttonView addInteraction:interaction];
    } else {
      _buttonView.showsLargeContentViewer = NO;
    }
  }

  if (!oldProps || oldButtonProps.accessibilityLargeContentTitle != newButtonProps.accessibilityLargeContentTitle) {
    _buttonView.largeContentTitle = RCTNSStringFromStringNilIfEmpty(newButtonProps.accessibilityLargeContentTitle);
  }

  if (!oldProps || oldButtonProps.accessibilityTraits != newButtonProps.accessibilityTraits) {
    _buttonView.accessibilityTraits =
        RCTUIAccessibilityTraitsFromAccessibilityTraits(newButtonProps.accessibilityTraits);
  }

  if (!oldProps || oldButtonProps.accessibilityState != newButtonProps.accessibilityState) {
    _buttonView.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected);
    const auto accessibilityState = newButtonProps.accessibilityState.value_or(AccessibilityState{});
    if (accessibilityState.selected) {
      _buttonView.accessibilityTraits |= UIAccessibilityTraitSelected;
    }
    if (accessibilityState.disabled) {
      _buttonView.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
    }
  }

  if (!oldProps || oldButtonProps.accessibilityIgnoresInvertColors != newButtonProps.accessibilityIgnoresInvertColors) {
    _buttonView.accessibilityIgnoresInvertColors = newButtonProps.accessibilityIgnoresInvertColors;
  }

  if (!oldProps || oldButtonProps.accessibilityValue != newButtonProps.accessibilityValue) {
    if (newButtonProps.accessibilityValue.text.has_value()) {
      _buttonView.accessibilityValue = RCTNSStringFromStringNilIfEmpty(newButtonProps.accessibilityValue.text.value());
    } else if (
        newButtonProps.accessibilityValue.now.has_value() && newButtonProps.accessibilityValue.min.has_value() &&
        newButtonProps.accessibilityValue.max.has_value()) {
      CGFloat val = (CGFloat)(newButtonProps.accessibilityValue.now.value()) /
          (newButtonProps.accessibilityValue.max.value() - newButtonProps.accessibilityValue.min.value());
      _buttonView.accessibilityValue = [NSNumberFormatter localizedStringFromNumber:@(val)
                                                                        numberStyle:NSNumberFormatterPercentStyle];
      ;
    } else {
      _buttonView.accessibilityValue = nil;
    }
  }

  if (!oldProps || oldButtonProps.testId != newButtonProps.testId) {
    UIView *accessibilityView = (UIView *)_buttonView;
    accessibilityView.accessibilityIdentifier = RCTNSStringFromString(newButtonProps.testId);
  }
}
#endif

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
  const auto &newProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(props);

  // After recycling, treat diffing branches as a fresh mount so values that
  // survived recycling on _buttonView (e.g. pointerEvents) get re-applied.
  BOOL treatAsFirstMount = !oldProps || _needsAnimationStateReset;
  _needsAnimationStateReset = NO;

  // Avoid re-running applyStartAnimationState on every commit — it would
  // interrupt in-flight press animations during mid-press re-renders.
  BOOL shouldApplyStartAnimationState = treatAsFirstMount;
  if (!treatAsFirstMount) {
    const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
    shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
        oldButtonProps.defaultScale != newProps.defaultScale ||
        oldButtonProps.defaultUnderlayOpacity != newProps.defaultUnderlayOpacity;
  }

  _moduleId = newProps.moduleId;
  _buttonView.userEnabled = newProps.enabled;
  _buttonView.hasLongPressHandler = newProps.hasLongPressHandler;
  _buttonView.tapAnimationInDuration = newProps.tapAnimationInDuration > 0 ? newProps.tapAnimationInDuration : 0;
  _buttonView.tapAnimationOutDuration = newProps.tapAnimationOutDuration > 0 ? newProps.tapAnimationOutDuration : 0;
  _buttonView.longPressDuration = newProps.longPressDuration;
  _buttonView.longPressAnimationOutDuration = newProps.longPressAnimationOutDuration;
  _buttonView.activeOpacity = newProps.activeOpacity;
  _buttonView.defaultOpacity = newProps.defaultOpacity;
  _buttonView.activeScale = newProps.activeScale;
  _buttonView.defaultScale = newProps.defaultScale;
  _buttonView.defaultUnderlayOpacity = newProps.defaultUnderlayOpacity;
  _buttonView.activeUnderlayOpacity = newProps.activeUnderlayOpacity;
  _buttonView.hoverOpacity = newProps.hoverOpacity;
  _buttonView.hoverScale = newProps.hoverScale;
  _buttonView.hoverUnderlayOpacity = newProps.hoverUnderlayOpacity;
  _buttonView.hoverAnimationInDuration = newProps.hoverAnimationInDuration > 0 ? newProps.hoverAnimationInDuration : 0;
  _buttonView.hoverAnimationOutDuration =
      newProps.hoverAnimationOutDuration > 0 ? newProps.hoverAnimationOutDuration : 0;
  if (newProps.underlayColor) {
    _buttonView.underlayColor = RCTUIColorFromSharedColor(newProps.underlayColor);
  } else {
    _buttonView.underlayColor = nil;
  }
#if !TARGET_OS_TV && !TARGET_OS_OSX
  _buttonView.exclusiveTouch = newProps.exclusive;
  [self setAccessibilityProps:props oldProps:oldProps];
#endif
  _buttonView.hitTestEdgeInsets = UIEdgeInsetsMake(
      -newProps.hitSlop.top, -newProps.hitSlop.left, -newProps.hitSlop.bottom, -newProps.hitSlop.right);

  // We need to cast to ViewProps to access the pointerEvents property with the correct type.
  // This is necessary because pointerEvents is redefined in the spec,
  // which shadows the base property with a different, incompatible type.
  const auto &newViewProps = static_cast<const ViewProps &>(newProps);
  if (treatAsFirstMount) {
    _buttonView.pointerEvents = RCTPointerEventsToEnum(newViewProps.pointerEvents);
  } else {
    const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
    const auto &oldViewProps = static_cast<const ViewProps &>(oldButtonProps);
    if (oldViewProps.pointerEvents != newViewProps.pointerEvents) {
      _buttonView.pointerEvents = RCTPointerEventsToEnum(newViewProps.pointerEvents);
    }
  }

  [super updateProps:props oldProps:oldProps];

#if !TARGET_OS_TV && !TARGET_OS_OSX
  // super's updateProps sets self.accessibilityIdentifier from testID via the
  // standard Fabric mechanism. However, setAccessibilityProps already forwards
  // testID to _buttonView.accessibilityIdentifier (the actual button element).
  // Having the identifier on both views causes testing frameworks (e.g. Detox)
  // to report multiple matches for the same testID. Clear it from the wrapper so
  // only _buttonView carries the identifier.
  if (!newProps.testId.empty()) {
    self.accessibilityIdentifier = nil;
  }
#endif

  if (shouldApplyStartAnimationState) {
    [_buttonView applyStartAnimationState];
  }

  const auto *oldButtonPropsPtr =
      treatAsFirstMount ? nullptr : std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps).get();
  [self updateManagedHandler:newProps oldProps:oldButtonPropsPtr];
}

#if !TARGET_OS_OSX
// Override hitTest to forward touches to _buttonView
// This is necessary because RCTViewComponentView's hitTest might handle pointerEvents
// from ViewProps and prevent touches from reaching _buttonView (which is the contentView).
// Since _buttonView has its own pointerEvents handling, we always forward to it.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
  if (![self pointInside:point withEvent:event]) {
    return nil;
  }

  CGPoint buttonPoint = [self convertPoint:point toView:_buttonView];

  return [_buttonView hitTest:buttonPoint withEvent:event];
}

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
  [super traitCollectionDidChange:previousTraitCollection];
  [_buttonView applyStartAnimationState];
}
#endif

@end

Class<RCTComponentViewProtocol> RNGestureHandlerButtonCls(void)
{
  return RNGestureHandlerButtonComponentView.class;
}
