/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

#import "RCTTouchHandler.h"

#import <UIKit/UIGestureRecognizerSubclass.h>

#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTLog.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "UIView+React.h"

// TODO: this class behaves a lot like a module, and could be implemented as a
// module if we were to assume that modules and RootViews had a 1:1 relationship

@interface RCTTouchEvent : NSObject

@property (nonatomic, assign, readonly) NSUInteger id;
@property (nonatomic, copy, readonly) NSString *eventName;
@property (nonatomic, copy, readonly) NSArray *touches;
@property (nonatomic, copy, readonly) NSArray *changedIndexes;
@property (nonatomic, assign, readonly) CFTimeInterval originatingTime;

@end


@implementation RCTTouchEvent

+ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime
{
  RCTTouchEvent *touchEvent = [[self alloc] init];
  touchEvent->_id = [self newID];
  touchEvent->_eventName = [eventName copy];
  touchEvent->_touches = [touches copy];
  touchEvent->_changedIndexes = [changedIndexes copy];
  touchEvent->_originatingTime = originatingTime;
  return touchEvent;
}

+ (NSUInteger)newID
{
  static NSUInteger id = 0;
  return ++id;
}

@end

@implementation RCTTouchHandler
{
  __weak RCTBridge *_bridge;

  /**
   * Arrays managed in parallel tracking native touch object along with the
   * native view that was touched, and the react touch data dictionary.
   * This must be kept track of because `UIKit` destroys the touch targets
   * if touches are canceled and we have no other way to recover this information.
   */
  NSMutableOrderedSet *_nativeTouches;
  NSMutableArray *_reactTouches;
  NSMutableArray *_touchViews;

  BOOL _recordingInteractionTiming;
  CFTimeInterval _mostRecentEnqueueJS;
  CADisplayLink *_displayLink;
  NSMutableArray *_pendingTouches;
  NSMutableArray *_bridgeInteractionTiming;
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
  if ((self = [super initWithTarget:nil action:NULL])) {

    RCTAssert(bridge != nil, @"Expect an event dispatcher");

    _bridge = bridge;

    _nativeTouches = [[NSMutableOrderedSet alloc] init];
    _reactTouches = [[NSMutableArray alloc] init];
    _touchViews = [[NSMutableArray alloc] init];

    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)];
    _pendingTouches = [[NSMutableArray alloc] init];
    _bridgeInteractionTiming = [[NSMutableArray alloc] init];

    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

    // `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower
    // level components not build using RCT, will fail to recognize gestures.
    self.cancelsTouchesInView = NO;
  }
  return self;
}

- (BOOL)isValid
{
  return _displayLink != nil;
}

- (void)invalidate
{
  [_displayLink invalidate];
  _displayLink = nil;
}

typedef NS_ENUM(NSInteger, RCTTouchEventType) {
  RCTTouchEventTypeStart,
  RCTTouchEventTypeMove,
  RCTTouchEventTypeEnd,
  RCTTouchEventTypeCancel
};

#pragma mark - Bookkeeping for touch indices

- (void)_recordNewTouches:(NSSet *)touches
{
  for (UITouch *touch in touches) {

    RCTAssert(![_nativeTouches containsObject:touch],
              @"Touch is already recorded. This is a critical bug.");

    // Find closest React-managed touchable view
    UIView *targetView = touch.view;
    while (targetView) {
      if (targetView.reactTag && targetView.userInteractionEnabled &&
          [targetView reactRespondsToTouch:touch]) {
        break;
      }
      targetView = targetView.superview;
    }

    NSNumber *reactTag = [targetView reactTagAtPoint:[touch locationInView:targetView]];
    if (!reactTag || !targetView.userInteractionEnabled) {
      return;
    }

    // Get new, unique touch id
    const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
    NSInteger touchID = ([_reactTouches.lastObject[@"target"] integerValue] + 1) % RCTMaxTouches;
    for (NSDictionary *reactTouch in _reactTouches) {
      NSInteger usedID = [reactTouch[@"target"] integerValue];
      if (usedID == touchID) {
        // ID has already been used, try next value
        touchID ++;
      } else if (usedID > touchID) {
        // If usedID > touchID, touchID must be unique, so we can stop looking
        break;
      }
    }

    // Create touch
    NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
    reactTouch[@"target"] = reactTag;
    reactTouch[@"identifier"] = @(touchID);
    reactTouch[@"touches"] = [NSNull null];        // We hijack this touchObj to serve both as an event
    reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.

    // Add to arrays
    [_touchViews addObject:targetView];
    [_nativeTouches addObject:touch];
    [_reactTouches addObject:reactTouch];
  }
}

- (void)_recordRemovedTouches:(NSSet *)touches
{
  for (UITouch *touch in touches) {
    NSUInteger index = [_nativeTouches indexOfObject:touch];
    if(index == NSNotFound) {
      continue;
    }

    [_touchViews removeObjectAtIndex:index];
    [_nativeTouches removeObjectAtIndex:index];
    [_reactTouches removeObjectAtIndex:index];
  }
}

- (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
{
  UITouch *nativeTouch = _nativeTouches[touchIndex];
  CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
  CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view];

  UIView *touchView = _touchViews[touchIndex];
  CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];

  NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
  reactTouch[@"pageX"] = @(rootViewLocation.x);
  reactTouch[@"pageY"] = @(rootViewLocation.y);
  reactTouch[@"locationX"] = @(touchViewLocation.x);
  reactTouch[@"locationY"] = @(touchViewLocation.y);
  reactTouch[@"timestamp"] =  @(nativeTouch.timestamp * 1000); // in ms, for JS
}

RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches);

/**
 * Constructs information about touch events to send across the serialized
 * boundary. This data should be compliant with W3C `Touch` objects. This data
 * alone isn't sufficient to construct W3C `Event` objects. To construct that,
 * there must be a simple receiver on the other side of the bridge that
 * organizes the touch objects into `Event`s.
 *
 * We send the data as an array of `Touch`es, the type of action
 * (start/end/move/cancel) and the indices that represent "changed" `Touch`es
 * from that array.
 */
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime
{
  // Update touches
  CFTimeInterval enqueueTime = CACurrentMediaTime();
  NSMutableArray *changedIndexes = [[NSMutableArray alloc] init];
  for (UITouch *touch in touches) {
    NSInteger index = [_nativeTouches indexOfObject:touch];
    if (index == NSNotFound) {
      continue;
    }

    [self _updateReactTouchAtIndex:index];
    [changedIndexes addObject:@(index)];
  }

  if (changedIndexes.count == 0) {
    return;
  }

  // Deep copy the touches because they will be accessed from another thread
  // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller?
  NSMutableArray *reactTouches = [[NSMutableArray alloc] initWithCapacity:_reactTouches.count];
  for (NSDictionary *touch in _reactTouches) {
    [reactTouches addObject:[touch copy]];
  }

  RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName
                                              touches:reactTouches
                                       changedIndexes:changedIndexes
                                      originatingTime:originatingTime];
  [_pendingTouches addObject:touch];

  if (_recordingInteractionTiming) {
    [_bridgeInteractionTiming addObject:@{
      @"timeSeconds": @(touch.originatingTime),
      @"operation": @"taskOriginated",
      @"taskID": @(touch.id),
    }];
    [_bridgeInteractionTiming addObject:@{
      @"timeSeconds": @(enqueueTime),
      @"operation": @"taskEnqueuedPending",
      @"taskID": @(touch.id),
    }];
  }
}

- (void)_update:(CADisplayLink *)sender
{
  // Dispatch touch event
  NSUInteger pendingCount = _pendingTouches.count;
  for (RCTTouchEvent *touch in _pendingTouches) {
    _mostRecentEnqueueJS = CACurrentMediaTime();
    [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
                      args:@[touch.eventName, touch.touches, touch.changedIndexes]];
  }

  if (_recordingInteractionTiming) {
    for (RCTTouchEvent *touch in _pendingTouches) {
      [_bridgeInteractionTiming addObject:@{
        @"timeSeconds": @(sender.timestamp),
        @"operation": @"frameAlignedDispatch",
        @"taskID": @(touch.id),
      }];
    }

    if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) {
      [_bridgeInteractionTiming addObject:@{
        @"timeSeconds": @(sender.timestamp),
        @"operation": @"mainThreadDisplayLink",
        @"taskID": @([RCTTouchEvent newID]),
      }];
    }
  }

  [_pendingTouches removeAllObjects];
}

- (void)startOrResetInteractionTiming
{
  RCTAssertMainThread();
  [_bridgeInteractionTiming removeAllObjects];
  _recordingInteractionTiming = YES;
}

- (NSDictionary *)endAndResetInteractionTiming
{
  RCTAssertMainThread();
  _recordingInteractionTiming = NO;
  NSArray *_prevInteractionTimingData = _bridgeInteractionTiming;
  _bridgeInteractionTiming = [[NSMutableArray alloc] init];
  return @{ @"interactionTiming": _prevInteractionTimingData };
}

#pragma mark - Gesture Recognizer Delegate Callbacks

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  [super touchesBegan:touches withEvent:event];
  self.state = UIGestureRecognizerStateBegan;

  // "start" has to record new touches before extracting the event.
  // "end"/"cancel" needs to remove the touch *after* extracting the event.
  [self _recordNewTouches:touches];
  [self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
  [super touchesMoved:touches withEvent:event];
  if (self.state == UIGestureRecognizerStateFailed) {
    return;
  }
  [self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  [super touchesEnded:touches withEvent:event];
  [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp];
  [self _recordRemovedTouches:touches];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
  [super touchesCancelled:touches withEvent:event];
  [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp];
  [self _recordRemovedTouches:touches];
}

- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
  return NO;
}

- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
  return NO;
}

@end
