#import <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h>
#import <objc/runtime.h>

#include <napi.h>

static IMP g_originalHitTest;
static IMP g_originalMouseEvent;

static char kIsDraggableKey;
static char kIsDraggingKey;
static char kMouseEventTypeKey;

NSView* viewUnderneathPoint(NSView* self, NSPoint point) {
  NSView *contentView = self.window.contentView;

  for (NSView *v in contentView.subviews.reverseObjectEnumerator) {
    if (v != self && ![v isKindOfClass:[NSVisualEffectView class]]) {
      NSPoint pointInView = [v convertPoint:point fromView:nil];
      if ([v hitTest:pointInView] && [v mouse:pointInView inRect:v.bounds]) {
        return v;
      }
    }
  }

  return nil;
}

NSView* swizzledHitTest(id obj, SEL sel, NSPoint point) {
  NSView* view = obj;
  NSView* originalReturn =
    ((NSView*(*) (id, SEL, NSPoint))g_originalHitTest)(obj, sel, point);

  // Do the original implementation when window is not focused, so window can be
  // dragged immediately even, when not focused.
  if (!view.window.isKeyWindow) {
    return originalReturn;
  }

  objc_setAssociatedObject(obj,
                           &kIsDraggableKey,
                           @(originalReturn == nil),
                           OBJC_ASSOCIATION_COPY_NONATOMIC);

  NSView* viewUnderPoint = viewUnderneathPoint(view, point);

  return [viewUnderPoint hitTest:point];
}

void swizzledMouseEvent(id obj, SEL sel, NSEvent* theEvent) {
  NSView* view = obj;
  NSNumber* isDraggable = objc_getAssociatedObject(view.window.contentView,
                                                  &kIsDraggableKey);
  NSNumber* isDragging = objc_getAssociatedObject(obj,
                                                  &kIsDraggingKey);
  NSNumber* previousEventType = objc_getAssociatedObject(obj,
                                                         &kMouseEventTypeKey);

  if ([theEvent type] == NSEventTypeLeftMouseDown && isDraggable.boolValue) {
    NSView* self = obj;
    [self.window performWindowDragWithEvent:theEvent];
  }

  BOOL isPreviousMouseDown =
    previousEventType.integerValue == NSEventTypeLeftMouseDown;
  BOOL isCurrentMouseDragged = [theEvent type] == NSEventTypeLeftMouseDragged;

  if (isPreviousMouseDown && isCurrentMouseDragged && isDraggable.boolValue) {
    objc_setAssociatedObject(obj,
                             &kIsDraggingKey,
                             @(YES),
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
  }

  if ([theEvent type] == NSEventTypeLeftMouseUp && isDragging.boolValue) {
    objc_setAssociatedObject(obj,
                             &kIsDraggingKey,
                             @(NO),
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
  } else {
    ((void(*) (id, SEL, NSEvent*))g_originalMouseEvent)(obj, sel, theEvent);
  }

  objc_setAssociatedObject(obj,
                           &kMouseEventTypeKey,
                           @([theEvent type]),
                           OBJC_ASSOCIATION_COPY_NONATOMIC);
}

void Setup(const Napi::CallbackInfo &info) {
  auto hitTestMethod = class_getInstanceMethod(
    NSClassFromString(@"BridgedContentView"),
    NSSelectorFromString(@"hitTest:"));

  g_originalHitTest = method_setImplementation(hitTestMethod,
    (IMP)&swizzledHitTest);

  auto mouseEventMethod = class_getInstanceMethod(
    NSClassFromString(@"RenderWidgetHostViewCocoa"),
    NSSelectorFromString(@"mouseEvent:"));

  g_originalMouseEvent = method_setImplementation(mouseEventMethod,
    (IMP)&swizzledMouseEvent);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "setup"),
              Napi::Function::New(env, Setup));

  return exports;
}

NODE_API_MODULE(electron_drag_click, Init)
