#import "RNGestureHandlerPointerTracker.h"
#import "RNGestureHandler.h"
#import "RNGestureHandlerDetector.h"

#import <React/UIView+React.h>

@implementation RNGestureHandlerPointerTracker {
  __weak RNGestureHandler *_gestureHandler;
  RNGHUITouch *_trackedPointers[MAX_POINTERS_COUNT];
  int _trackedPointersCount;
}

- (id)initWithGestureHandler:(id)gestureHandler
{
  _gestureHandler = gestureHandler;
  _trackedPointersCount = 0;
  _changedPointersData = nil;
  _allPointersData = nil;

  for (int i = 0; i < MAX_POINTERS_COUNT; i++) {
    _trackedPointers[i] = nil;
  }

  return self;
}

- (int)registerTouch:(RNGHUITouch *)touch
{
  for (int index = 0; index < MAX_POINTERS_COUNT; index++) {
    if (_trackedPointers[index] == nil) {
      _trackedPointers[index] = touch;
      return index;
    }
  }

  return -1;
}

- (int)unregisterTouch:(RNGHUITouch *)touch
{
  for (int index = 0; index < MAX_POINTERS_COUNT; index++) {
#if TARGET_OS_OSX
    // A macOS mouse sequence delivers a fresh NSEvent per event, so identity matching
    // (valid on iOS, where a UITouch is one stable object for the whole touch) never
    // matches. There is only one mouse pointer — match the tracked slot instead.
    if (_trackedPointers[index] != nil) {
#else
    if (_trackedPointers[index] == touch) {
#endif
      _trackedPointers[index] = nil;
      return index;
    }
  }

  return -1;
}

- (int)findTouchIndex:(RNGHUITouch *)touch
{
  for (int index = 0; index < MAX_POINTERS_COUNT; index++) {
#if TARGET_OS_OSX
    // See unregisterTouch: — identity matching cannot work for NSEvents.
    if (_trackedPointers[index] != nil) {
#else
    if (_trackedPointers[index] == touch) {
#endif
      return index;
    }
  }
  return -1;
}

- (int)registeredTouchesCount
{
  int count = 0;
  for (int i = 0; i < MAX_POINTERS_COUNT; i++) {
    if (_trackedPointers[i] != nil) {
      count++;
    }
  }
  return count;
}

- (NSDictionary *)extractPointerData:(int)index forTouch:(RNGHUITouch *)touch
{
#if TARGET_OS_OSX
  CGFloat windowHeight = touch.window.contentView.frame.size.height;
  CGPoint yFlippedAbsolutePos = [touch locationInWindow];
  CGPoint absolutePos = CGPointMake(yFlippedAbsolutePos.x, windowHeight - yFlippedAbsolutePos.y);
  CGPoint relativePos = [_gestureHandler.recognizer.view convertPoint:absolutePos fromView:touch.window.contentView];
#else
  CGPoint relativePos = [touch locationInView:_gestureHandler.recognizer.view];
  CGPoint absolutePos = [touch locationInView:_gestureHandler.recognizer.view.window];
#endif

  return @{
    @"id" : @(index),
    @"x" : @(relativePos.x),
    @"y" : @(relativePos.y),
    @"absoluteX" : @(absolutePos.x),
    @"absoluteY" : @(absolutePos.y)
  };
}

- (void)extractAllTouches
{
  int registeredTouches = [self registeredTouchesCount];

  NSDictionary *data[registeredTouches];
  int nextIndex = 0;

  for (int i = 0; i < MAX_POINTERS_COUNT; i++) {
    RNGHUITouch *touch = _trackedPointers[i];
    if (touch != nil) {
      data[nextIndex++] = [self extractPointerData:i forTouch:touch];
    }
  }

  _allPointersData = [[NSArray alloc] initWithObjects:data count:registeredTouches];
}

- (void)touchesBegan:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

  _eventType = RNGHTouchEventTypePointerDown;

  NSDictionary *data[touches.count];
  int changedCount = 0;

  for (int i = 0; i < [touches count]; i++) {
    RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
    int index = [self registerTouch:touch];

    if (index < 0) {
      continue;
    }

    _trackedPointersCount++;
    data[changedCount++] = [self extractPointerData:index forTouch:touch];
  }

  if (changedCount == 0) {
    return;
  }

  _changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
  // extract all touches last to include the ones that were just added
  [self extractAllTouches];
  [self sendEvent];
}

- (void)touchesMoved:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

  _eventType = RNGHTouchEventTypePointerMove;

  NSDictionary *data[touches.count];
  int changedCount = 0;

  for (int i = 0; i < [touches count]; i++) {
    RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
    int index = [self findTouchIndex:touch];

    if (index < 0) {
      continue;
    }

#if TARGET_OS_OSX
    // Replace the stored mouse-down event with the latest one so extractAllTouches
    // reads the pointer's current position, not where the sequence started.
    _trackedPointers[index] = touch;
#endif

    data[changedCount++] = [self extractPointerData:index forTouch:touch];
  }

  if (changedCount == 0) {
    return;
  }

  _changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
  [self extractAllTouches];
  [self sendEvent];
}

- (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

#if TARGET_OS_OSX
  // Refresh the tracked slot with the ending event so extractAllTouches below reports
  // the pointer's final position. On iOS the stored UITouch is a live object that
  // always reads its current location; a stored NSEvent is a stale snapshot.
  for (RNGHUITouch *touch in touches) {
    int index = [self findTouchIndex:touch];
    if (index >= 0) {
      _trackedPointers[index] = touch;
    }
  }
#endif

  // extract all touches first to include the ones that were just lifted
  [self extractAllTouches];

  _eventType = RNGHTouchEventTypePointerUp;

  NSDictionary *data[touches.count];
  int changedCount = 0;

  for (int i = 0; i < [touches count]; i++) {
    RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
    int index = [self unregisterTouch:touch];

    if (index < 0) {
      continue;
    }

    _trackedPointersCount--;
    data[changedCount++] = [self extractPointerData:index forTouch:touch];
  }

  if (changedCount == 0) {
    return;
  }

  _changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
  [self sendEvent];
}

- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

  [self reset];
}

- (void)reset
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

  if (_trackedPointersCount == 0) {
    // gesture has finished because all pointers were lifted, reset event type to send state change event
    _eventType = RNGHTouchEventTypeUndetermined;
  } else {
    // turns out that the gesture may be made to fail without calling touchesCancelled in that case there
    // are still tracked pointers but the recognizer state is already set to UIGestureRecognizerStateFailed
    // we need to clear the pointers and send info about their cancellation
    [self cancelPointers];
  }

  [_gestureHandler reset];
}

- (void)cancelPointers
{
  // extract all touches first to include the ones that were just cancelled
  [self extractAllTouches];

  int registeredTouches = [self registeredTouchesCount];

  if (registeredTouches > 0) {
    int nextIndex = 0;
    NSDictionary *data[registeredTouches];

    for (int i = 0; i < MAX_POINTERS_COUNT; i++) {
      RNGHUITouch *touch = _trackedPointers[i];
      if (touch != nil) {
        data[nextIndex++] = [self extractPointerData:i forTouch:touch];
        [self unregisterTouch:touch];
      }
    }

    _eventType = RNGHTouchEventTypePointerCancel;
    _changedPointersData = [[NSArray alloc] initWithObjects:data count:registeredTouches];
    [self sendEvent];
    _trackedPointersCount = 0;
  }
}

- (void)sendEvent
{
  if (!_gestureHandler.needsPointerData) {
    return;
  }

  // it may happen that the gesture recognizer is reset after it's been unbound from the view,
  // if that recognizer tried to send an event, the app would crash because the target of the event
  // would be nil. Detector-attached handlers (native and virtual alike) don't route events by
  // the view's reactTag — they dispatch through the detector view — so a nil tag is not an
  // obstacle for them. The virtual recognizer's view has no reactTag at all, so without this
  // exemption virtual handlers would never deliver touch events.
  if (_gestureHandler.recognizer.view.reactTag == nil && ![_gestureHandler usesNativeOrVirtualDetector]) {
    return;
  }

  [_gestureHandler sendTouchEventInState:[_gestureHandler state]
                          forViewWithTag:_gestureHandler.recognizer.view.reactTag];
}

@end
