//
//  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;
  RNGestureHandlerEventExtraData *_lastActiveExtraData;
}

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

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

#if !TARGET_OS_OSX

- (void)bindToView:(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
  UIScrollView *scrollView = [self retrieveScrollView:view];
  scrollView.delaysContentTouches = YES;
}

- (void)unbindFromView
{
  UIView *view = self.recognizer.view;

  if ([view isKindOfClass:[UIControl class]]) {
    [(UIControl *)view removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents];
  }

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

  [super unbindFromView];
}

- (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData
{
  if (_lastActiveExtraData != nil && [_lastActiveExtraData.data isEqualToDictionary:extraData.data]) {
    return YES;
  }

  _lastActiveExtraData = extraData;
  return NO;
}

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

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

- (void)handleSwitch:(UIView *)sender
{
  [self sendEventsInState:RNGestureHandlerStateBegan
           forViewWithTag:sender.reactTag
            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                       withNumberOfTouches:1
                                                           withPointerType:_pointerType]];
  [self sendEventsInState:RNGestureHandlerStateActive
           forViewWithTag:sender.reactTag
            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                       withNumberOfTouches:1
                                                           withPointerType:_pointerType]];
  [self sendEventsInState:RNGestureHandlerStateEnd
           forViewWithTag:sender.reactTag
            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                       withNumberOfTouches:1
                                                           withPointerType:_pointerType]];

  [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:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                       withNumberOfTouches:event.allTouches.count
                                                           withPointerType:_pointerType]];
}

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

  [self sendEventsInState:RNGestureHandlerStateEnd
           forViewWithTag:sender.reactTag
            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
                                                       withNumberOfTouches:event.allTouches.count
                                                           withPointerType:_pointerType]];
}

- (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event
{
  RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES
                                                                           withNumberOfTouches:event.allTouches.count
                                                                               withPointerType:_pointerType];

  [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:[RNGestureHandlerEventExtraData forPointerInside:NO
                                                                            withNumberOfTouches:event.allTouches.count
                                                                                withPointerType:_pointerType]];
  }
}

- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                                          withNumberOfTouches:event.allTouches.count
                                                                              withPointerType:_pointerType]];
}

- (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
                                                                          withNumberOfTouches:event.allTouches.count
                                                                              withPointerType:_pointerType]];
}

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

  [self sendActiveStateEventIfChangedForView:sender
                                   extraData:[RNGestureHandlerEventExtraData forPointerInside:NO
                                                                          withNumberOfTouches:event.allTouches.count
                                                                              withPointerType:_pointerType]];
}

- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event
{
  [self sendEventsInState:RNGestureHandlerStateCancelled
           forViewWithTag:sender.reactTag
            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
                                                       withNumberOfTouches:event.allTouches.count
                                                           withPointerType:_pointerType]];
}

- (BOOL)wantsToAttachDirectlyToView
{
  return YES;
}

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

#endif

- (RNGestureHandlerEventExtraData *)eventExtraData:(RNDummyGestureRecognizer *)recognizer
{
  return [RNGestureHandlerEventExtraData forPointerInside:[self containsPointInView]
                                      withNumberOfTouches:1
                                          withPointerType:RNGestureHandlerMouse];
}

- (BOOL)isContinuous
{
  return YES;
}

@end
