#import <ApplicationServices/ApplicationServices.h>
#import <AppKit/AppKit.h>
#include <stdbool.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

typedef struct {
  bool hasLastClick;
  bool hasActiveClick;
  CGMouseButton lastButton;
  CGMouseButton activeButton;
  double lastX;
  double lastY;
  double lastDownMs;
  int clickCount;
  int activeClickCount;
} ClickTracker;

static ClickTracker clickTracker = {0};
static const double clickDistanceTolerance = 6.0;

static CGMouseButton mouse_button(const char *name) {
  if (strcmp(name, "right") == 0) return kCGMouseButtonRight;
  if (strcmp(name, "middle") == 0 || strcmp(name, "center") == 0) return kCGMouseButtonCenter;
  if (strcmp(name, "back") == 0 || strcmp(name, "x1") == 0 || strcmp(name, "button4") == 0) return 3;
  if (strcmp(name, "forward") == 0 || strcmp(name, "x2") == 0 || strcmp(name, "button5") == 0) return 4;
  return kCGMouseButtonLeft;
}

static CGEventType mouse_event_type(const char *action, CGMouseButton button) {
  if (strcmp(action, "move") == 0) return kCGEventMouseMoved;
  if (button == kCGMouseButtonRight) {
    if (strcmp(action, "down") == 0) return kCGEventRightMouseDown;
    if (strcmp(action, "up") == 0) return kCGEventRightMouseUp;
    return kCGEventRightMouseDragged;
  }
  if (button == kCGMouseButtonCenter) {
    if (strcmp(action, "down") == 0) return kCGEventOtherMouseDown;
    if (strcmp(action, "up") == 0) return kCGEventOtherMouseUp;
    return kCGEventOtherMouseDragged;
  }
  if (button != kCGMouseButtonLeft) {
    if (strcmp(action, "down") == 0) return kCGEventOtherMouseDown;
    if (strcmp(action, "up") == 0) return kCGEventOtherMouseUp;
    return kCGEventOtherMouseDragged;
  }
  if (strcmp(action, "down") == 0) return kCGEventLeftMouseDown;
  if (strcmp(action, "up") == 0) return kCGEventLeftMouseUp;
  return kCGEventLeftMouseDragged;
}

static double now_ms(void) {
  struct timespec ts;
  clock_gettime(CLOCK_MONOTONIC, &ts);
  return ((double)ts.tv_sec * 1000.0) + ((double)ts.tv_nsec / 1000000.0);
}

static double double_click_interval_ms(void) {
  NSTimeInterval interval = [NSEvent doubleClickInterval];
  if (!isfinite(interval) || interval <= 0) {
    return 500.0;
  }
  return interval * 1000.0;
}

static void reset_click_tracker(void) {
  memset(&clickTracker, 0, sizeof(clickTracker));
}

static bool click_position_matches(double x1, double y1, double x2, double y2) {
  return fabs(x1 - x2) <= clickDistanceTolerance &&
    fabs(y1 - y2) <= clickDistanceTolerance;
}

static int click_state_for_event(const char *action,
                                 CGMouseButton button,
                                 double x,
                                 double y,
                                 double eventMs,
                                 double intervalMs) {
  if (strcmp(action, "down") == 0) {
    bool repeatedClick = clickTracker.hasLastClick &&
      clickTracker.lastButton == button &&
      eventMs >= clickTracker.lastDownMs &&
      eventMs - clickTracker.lastDownMs <= intervalMs &&
      click_position_matches(x, y, clickTracker.lastX, clickTracker.lastY);
    clickTracker.clickCount = repeatedClick ? clickTracker.clickCount + 1 : 1;
    clickTracker.lastButton = button;
    clickTracker.lastX = x;
    clickTracker.lastY = y;
    clickTracker.lastDownMs = eventMs;
    clickTracker.hasLastClick = true;
    clickTracker.activeButton = button;
    clickTracker.activeClickCount = clickTracker.clickCount;
    clickTracker.hasActiveClick = true;
    return clickTracker.clickCount;
  }

  if (strcmp(action, "up") == 0) {
    if (clickTracker.hasActiveClick && clickTracker.activeButton == button) {
      int clickCount = clickTracker.activeClickCount;
      clickTracker.hasActiveClick = false;
      return clickCount;
    }
    return 1;
  }

  return 0;
}

static bool post_mouse(const char *action, double x, double y, const char *button_name) {
  CGMouseButton button = mouse_button(button_name ? button_name : "left");
  CGPoint point = CGPointMake(x, y);
  CGEventType type = mouse_event_type(action, button);
  int clickState = click_state_for_event(action, button, x, y, now_ms(), double_click_interval_ms());
  CGEventRef event = CGEventCreateMouseEvent(NULL, type, point, button);
  if (!event) {
    return false;
  }

  if (type == kCGEventLeftMouseDown || type == kCGEventLeftMouseUp ||
      type == kCGEventRightMouseDown || type == kCGEventRightMouseUp ||
      type == kCGEventOtherMouseDown || type == kCGEventOtherMouseUp) {
    CGEventSetIntegerValueField(event, kCGMouseEventClickState, clickState);
  }
  CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button);
  CGEventPost(kCGHIDEventTap, event);
  CFRelease(event);
  return true;
}

static void trim_newline(char *line) {
  size_t len = strlen(line);
  while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
    line[len - 1] = '\0';
    len -= 1;
  }
}

#ifndef OPENMOUSE_MOUSESEND_TEST
int main(void) {
  char line[256];

  while (fgets(line, sizeof(line), stdin)) {
    trim_newline(line);
    if (line[0] == '\0') {
      continue;
    }

    char action[16] = {0};
    char button[16] = "left";
    double x = 0;
    double y = 0;
    int parsed = sscanf(line, "%15s %lf %lf %15s", action, &x, &y, button);
    if (strcmp(action, "quit") == 0) {
      return 0;
    }
    if (parsed < 3) {
      fprintf(stderr, "bad command: %s\n", line);
      fflush(stderr);
      continue;
    }
    if (strcmp(action, "move") != 0 && strcmp(action, "drag") != 0 &&
        strcmp(action, "down") != 0 && strcmp(action, "up") != 0) {
      fprintf(stderr, "unknown action: %s\n", action);
      fflush(stderr);
      continue;
    }

    bool ok = post_mouse(action, x, y, button);
    if (!ok) {
      fprintf(stderr, "post failed: %s %.1f %.1f %s\n", action, x, y, button);
      fflush(stderr);
    }
  }

  return 0;
}
#endif
