//
//  RNNativeViewHandler.m
//  RNGestureHandler
//
//  Created by Krzysztof Magiera on 12/10/2017.
//  Copyright © 2017 Software Mansion. All rights reserved.
//

#import "RNNativeViewHandler.h"

#if !TARGET_OS_OSX
#import <UIKit/UIGestureRecognizerSubclass.h>
#endif

#import <React/RCTConvert.h>
#import <React/RCTScrollViewComponentView.h>
#import <React/UIView+React.h>

#pragma mark RNDummyGestureRecognizer

@implementation RNDummyGestureRecognizer {
  __weak RNGestureHandler *_gestureHandler;
}

- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler
{
  if ((self = [super initWithTarget:gestureHandler action:@selector(handleGesture:)])) {
    _gestureHandler = gestureHandler;
  }
  return self;
}

#if !TARGET_OS_OSX
- (void)touchesBegan:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  [_gestureHandler setCurrentPointerTypeForEvent:event];
  [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  [self updateStateIfScrollView];
  [_gestureHandler.pointerTracker touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
  self.state = UIGestureRecognizerStateFailed;

  // For now, we are handling only the scroll view case.
  // If more views need special treatment, then we can switch to a delegate pattern
  if ([_gestureHandler retrieveScrollView:self.view] == nil) {
    [self reset];
  }
}

- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
  self.state = UIGestureRecognizerStateCancelled;
  [self reset];
}

#else
- (void)mouseDown:(NSEvent *)event
{
  [_gestureHandler setCurrentPointerTypeToMouse];

  self.state = NSGestureRecognizerStateBegan;
  [_gestureHandler.pointerTracker touchesBegan:[NSSet setWithObject:event] withEvent:event];
}

- (void)mouseDragged:(NSEvent *)event
{
  self.state = NSGestureRecognizerStateChanged;
  [_gestureHandler.pointerTracker touchesMoved:[NSSet setWithObject:event] withEvent:event];
}

- (void)mouseUp:(NSEvent *)event
{
  self.state = NSGestureRecognizerStateEnded;
  [_gestureHandler.pointerTracker touchesEnded:[NSSet setWithObject:event] withEvent:event];
  [self reset];
}

#endif

- (void)reset
{
  [_gestureHandler.pointerTracker reset];
  [super reset];
  [_gestureHandler reset];
}

- (void)updateStateIfScrollView
{
  RNGHUIScrollView *scrollView = [_gestureHandler retrieveScrollView:self.view];
  if (!scrollView) {
    return;
  }
  for (UIGestureRecognizer *scrollViewGestureRecognizer in scrollView.gestureRecognizers) {
    if ([_gestureHandler isUIScrollViewPanGestureRecognizer:scrollViewGestureRecognizer]) {
      self.state = scrollViewGestureRecognizer.state;
    }
  }
}

@end

#pragma mark RNNativeViewGestureHandler

@implementation RNNativeViewGestureHandler {
  BOOL _shouldActivateOnStart;
  BOOL _disallowInterruption;
  BOOL _yieldsToContinuousGestures;
  BOOL _delaysChildPressedState;
  RNGestureHandlerEventExtraData *_lastActiveExtraData;

  // For UIControl-based views the recognizer is never attached, so
  // `recognizer.view` cannot be used to retrieve the bound view.
  __weak RNGHUIView *_boundView;
  __weak id<RNGHNativeViewHandlerStateObserver> _stateObserver;
}

- (instancetype)initWithTag:(NSNumber *)tag
{
  if ((self = [super initWithTag:tag])) {
    _recognizer = [[RNDummyGestureRecognizer alloc] initWithGestureHandler:self];
    _delaysChildPressedState = YES;
  }
  return self;
}

- (void)updateConfig:(NSDictionary *)config
{
  [super updateConfig:config];
  _shouldActivateOnStart = [RCTConvert BOOL:config[@"shouldActivateOnStart"]];
  _disallowInterruption = [RCTConvert BOOL:config[@"disallowInterruption"]];
  _yieldsToContinuousGestures = [RCTConvert BOOL:config[@"yieldsToContinuousGestures"]];

  id delaysChildPressedState = config[@"delaysChildPressedState"];
  _delaysChildPressedState = delaysChildPressedState == nil ? YES : [RCTConvert BOOL:delaysChildPressedState];

#if !TARGET_OS_OSX
  // Config may be updated after the handler is bound to a view — re-apply to the connected
  // scroll view if there is one.
  if (self.recognizer.view != nil) {
    [self retrieveScrollView:self.recognizer.view].delaysContentTouches = _delaysChildPressedState;
  }
#endif
}

- (void)bindToView:(RNGHUIView *)view
{
  _boundView = view;

  if ([view conformsToProtocol:@protocol(RNGHNativeViewHandlerStateObserver)]) {
    _stateObserver = (id<RNGHNativeViewHandlerStateObserver>)view;
  }

#if !TARGET_OS_OSX
  [self bindToUIKitView:view];
#else
  [super bindToView:view];
#endif
}

- (void)unbindFromView
{
#if !TARGET_OS_OSX
  if ([_boundView isKindOfClass:[UIControl class]]) {
    [(UIControl *)_boundView removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents];
  }

  // Restore the React Native's overridden behavor for not delaying content touches
  UIScrollView *scrollView = [self retrieveScrollView:_boundView];
  scrollView.delaysContentTouches = NO;
#endif

  _boundView = nil;
  _stateObserver = nil;

  [super unbindFromView];
}

- (void)dispatchStateChange:(RNGestureHandlerState)newState
                  prevState:(RNGestureHandlerState)prevState
                  extraData:(RNGestureHandlerEventExtraData *)extraData
{
  [_stateObserver onHandlerStateChange:newState prevState:prevState extraData:extraData];
}

- (void)dispatchHandlerUpdate:(RNGestureHandlerEventExtraData *)extraData
{
  [_stateObserver onHandlerUpdate:extraData];
}

#if !TARGET_OS_OSX

- (void)bindToUIKitView:(UIView *)view
{
  // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow
  // for properties like `disallowInterruption` to work.
  if ([view isKindOfClass:[UIControl class]]) {
    UIControl *control = (UIControl *)view;

    // Pressing UISwitch triggers only touchUp and valueChanged callbacks. In order to align its behavior
    // with other UIControls, we have to dispatch full Gesture Handler events flow in one callback, as
    // touchesDown is not executed.
#if !TARGET_OS_TV
    if ([view isKindOfClass:[UISwitch class]]) {
      _pointerType = RNGestureHandlerTouch;
      [control addTarget:self action:@selector(handleSwitch:) forControlEvents:UIControlEventValueChanged];
    } else
#endif // !TARGET_OS_TV
    {
      [control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown];
      [control addTarget:self
                    action:@selector(handleTouchUpOutside:forEvent:)
          forControlEvents:UIControlEventTouchUpOutside];
      [control addTarget:self
                    action:@selector(handleTouchUpInside:forEvent:)
          forControlEvents:UIControlEventTouchUpInside];
      [control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
      [control addTarget:self
                    action:@selector(handleDragInside:forEvent:)
          forControlEvents:UIControlEventTouchDragInside];
      [control addTarget:self
                    action:@selector(handleDragOutside:forEvent:)
          forControlEvents:UIControlEventTouchDragOutside];
      [control addTarget:self
                    action:@selector(handleDragEnter:forEvent:)
          forControlEvents:UIControlEventTouchDragEnter];
      [control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
    }
  } else {
    [super bindToView:view];
  }

  // We can restore default scrollview behaviour to delay touches to scrollview's children
  // because gesture handler system can handle cancellation of scroll recognizer when JS responder
  // is set. Setting `delaysChildPressedState` to `false` opts out of this, keeping touches delivered
  // to children immediately.
  UIScrollView *scrollView = [self retrieveScrollView:view];
  scrollView.delaysContentTouches = _delaysChildPressedState;
}

- (RNGestureHandlerEventExtraData *)extraDataForView:(UIView *)sender
                                               event:(UIEvent *)event
                                       pointerInside:(BOOL)pointerInside
{
  RNGHUITouch *touch = [[event allTouches] anyObject];
  CGPoint position = touch ? [touch locationInView:sender] : CGPointZero;
  CGPoint absolutePosition = touch ? [touch locationInView:nil] : [sender convertPoint:CGPointZero toView:nil];

  return [RNGestureHandlerEventExtraData forPointerInside:pointerInside
                                             withPosition:position
                                     withAbsolutePosition:absolutePosition
                                      withNumberOfTouches:event.allTouches.count
                                          withPointerType:_pointerType];
}

- (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestureHandlerEventExtraData *)extraData
{
  if ([self shouldSuppressActiveEvent:extraData]) {
    return;
  }

  [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData];
}

- (void)handleSwitch:(UIView *)sender
{
  CGPoint center = CGPointMake(CGRectGetMidX(sender.bounds), CGRectGetMidY(sender.bounds));
  CGPoint absoluteCenter = [sender convertPoint:center toView:nil];
  RNGestureHandlerEventExtraData * (^extraData)(void) = ^{
    return [RNGestureHandlerEventExtraData forPointerInside:YES
                                               withPosition:center
                                       withAbsolutePosition:absoluteCenter
                                        withNumberOfTouches:1
                                            withPointerType:self->_pointerType];
  };

  [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:sender.reactTag withExtraData:extraData()];
  [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData()];
  [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData()];

  [self reset];
}

- (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
{
  [self setCurrentPointerTypeForEvent:event];
  [self reset];

  if (_disallowInterruption) {
    // When `disallowInterruption` is set we cancel all gesture handlers when this UIControl
    // gets DOWN event. When `yieldsToContinuousGestures` is also set we leave alone:
    //   - non-RNGH recognizers (e.g. UIScrollView's pan), so native containers can take over
    //   - continuous RNGH recognizers, so other continuous gestures wrapping the touchable can still take over
    for (RNGHUITouch *touch in [event allTouches]) {
      for (UIGestureRecognizer *recognizer in [touch gestureRecognizers]) {
        if (_yieldsToContinuousGestures &&
            (recognizer.gestureHandler == nil || [recognizer.gestureHandler isContinuous])) {
          continue;
        }

        recognizer.enabled = NO;
        recognizer.enabled = YES;
      }
    }
  }

  [self sendEventsInState:RNGestureHandlerStateBegan
           forViewWithTag:sender.reactTag
            withExtraData:[self extraDataForView:sender event:event pointerInside:YES]];
}

- (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event
{
  if (self.shouldCancelWhenOutside) {
    return;
  }

  [self sendEventsInState:RNGestureHandlerStateEnd
           forViewWithTag:sender.reactTag
            withExtraData:[self extraDataForView:sender event:event pointerInside:NO]];
}

- (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event
{
  RNGestureHandlerEventExtraData *extraData = [self extraDataForView:sender event:event pointerInside:YES];

  [self sendActiveStateEventIfChangedForView:sender extraData:extraData];
  [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData];
}

- (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event
{
  // Pointer is moved outside of the view bounds, we cancel button when `shouldCancelWhenOutside` is set
  if (self.shouldCancelWhenOutside) {
    UIControl *control = (UIControl *)sender;
    [control cancelTrackingWithEvent:event];
  } else {
    [self sendActiveStateEventIfChangedForView:sender
                                     extraData:[self extraDataForView:sender event:event pointerInside:NO]];
  }
}

- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[self extraDataForView:sender event:event pointerInside:YES]];
}

- (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[self extraDataForView:sender event:event pointerInside:YES]];
}

- (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event
{
  if (self.shouldCancelWhenOutside) {
    return;
  }

  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[self extraDataForView:sender event:event pointerInside:NO]];
}

- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendEventsInState:RNGestureHandlerStateCancelled
           forViewWithTag:sender.reactTag
            withExtraData:[self extraDataForView:sender event:event pointerInside:NO]];
}

- (BOOL)wantsToAttachDirectlyToView
{
  return YES;
}

#endif

- (void)reset
{
  [super reset];
  _lastActiveExtraData = nil;
}

- (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData
{
  NSDictionary *last = _lastActiveExtraData.data;
  NSDictionary *current = extraData.data;

  if (last != nil && [last[@"pointerInside"] isEqual:current[@"pointerInside"]] &&
      [last[@"numberOfPointers"] isEqual:current[@"numberOfPointers"]] &&
      [last[@"pointerType"] isEqual:current[@"pointerType"]]) {
    return YES;
  }

  _lastActiveExtraData = extraData;
  return NO;
}

- (RNGestureHandlerEventExtraData *)eventExtraData:(RNDummyGestureRecognizer *)recognizer
{
#if TARGET_OS_OSX
  // macOS window coordinates are bottom-left origin; flip Y to match RN's
  // top-left origin, mirroring RNGestureHandlerPointerTracker.
  CGFloat windowHeight = recognizer.view.window.contentView.frame.size.height;
  CGPoint yFlippedAbsolute = [recognizer locationInView:nil];
  CGPoint absolutePosition = CGPointMake(yFlippedAbsolute.x, windowHeight - yFlippedAbsolute.y);
  CGPoint position = [recognizer.view convertPoint:absolutePosition fromView:recognizer.view.window.contentView];
#else
  CGPoint position = [recognizer locationInView:recognizer.view];
  CGPoint absolutePosition = [recognizer locationInView:nil];
#endif

  return [RNGestureHandlerEventExtraData forPointerInside:[self containsPointInView]
                                             withPosition:position
                                     withAbsolutePosition:absolutePosition
                                      withNumberOfTouches:1
                                          withPointerType:RNGestureHandlerMouse];
}

- (BOOL)isContinuous
{
  return YES;
}

@end
