// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/image_editor/event_capture_mac.h"

#import <Cocoa/Cocoa.h>

#include <memory>

#include "base/apple/owned_objc.h"
#include "base/check.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#import "components/remote_cocoa/app_shim/mouse_capture.h"
#import "components/remote_cocoa/app_shim/mouse_capture_delegate.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"

namespace image_editor {

class EventCaptureMac::MouseCaptureDelegateImpl
    : public remote_cocoa::CocoaMouseCaptureDelegate {
 public:
  MouseCaptureDelegateImpl(ui::EventHandler* event_handler,
                           base::OnceClosure capture_lost_callback,
                           gfx::NativeView web_contents_view,
                           gfx::NativeWindow target_native_window)
      : event_handler_(event_handler),
        capture_lost_callback_(std::move(capture_lost_callback)),
        web_contents_view_(web_contents_view.GetNativeNSView()),
        window_(target_native_window.GetNativeNSWindow()),
        mouse_capture_(
            std::make_unique<remote_cocoa::CocoaMouseCapture>(this)) {}

  void SetKeyboardMonitor(id local_keyboard_monitor) {
    local_keyboard_monitor_ = local_keyboard_monitor;
  }

  void Reset() {
    // We do not want our callback to run if mouse capture loss was caused by
    // reset of event capture.
    std::move(capture_lost_callback_).Reset();

    // Remove the key down monitor.
    [NSEvent removeMonitor:local_keyboard_monitor_];
  }

 private:
  // remote_cocoa::CocoaMouseCaptureDelegate:
  bool PostCapturedEvent(NSEvent* event) override {
    std::unique_ptr<ui::Event> ui_event =
        ui::EventFromNative(base::apple::OwnedNSEvent(event));
    if (!ui_event) {
      return false;
    }

    // The window from where the event is sourced. If it is outside of the
    // browser, this window will not be equal to GetWindow().
    NSView* view = [event.window.contentView hitTest:event.locationInWindow];

    ui::EventType type = ui_event->type();
    if (type == ui::ET_MOUSE_DRAGGED || type == ui::ET_MOUSE_RELEASED) {
      event_handler_->OnMouseEvent(ui_event->AsMouseEvent());
    } else if ((type == ui::ET_MOUSE_PRESSED || type == ui::ET_MOUSE_MOVED) &&
               web_contents_view_ == view) {
      // We do not need to record mouse clicks outside of the web contents.
      event_handler_->OnMouseEvent(ui_event->AsMouseEvent());
    } else if (type == ui::ET_MOUSE_MOVED && web_contents_view_ != view) {
      // Manually set arrow cursor when region search UI is open and cursor is
      // moved from web contents.
      [NSCursor.arrowCursor set];
    } else if (type == ui::ET_SCROLL) {
      event_handler_->OnScrollEvent(ui_event->AsScrollEvent());
    }

    // If we set the ui event as handled, then we want to swallow the event.
    return ui_event->handled();
  }

  void OnMouseCaptureLost() override {
    if (!capture_lost_callback_.is_null()) {
      std::move(capture_lost_callback_).Run();
    }
  }

  NSWindow* GetWindow() const override { return window_; }

  raw_ptr<ui::EventHandler> event_handler_;
  base::OnceClosure capture_lost_callback_;
  NSView* __weak web_contents_view_ = nil;
  NSWindow* __weak window_ = nil;
  std::unique_ptr<remote_cocoa::CocoaMouseCapture> mouse_capture_;
  id __strong local_keyboard_monitor_ = nil;
};

EventCaptureMac::EventCaptureMac(ui::EventHandler* event_handler,
                                 base::OnceClosure capture_lost_callback,
                                 gfx::NativeView web_contents_view,
                                 gfx::NativeWindow target_native_window)
    : mouse_capture_delegate_impl_(std::make_unique<MouseCaptureDelegateImpl>(
          event_handler,
          std::move(capture_lost_callback),
          web_contents_view,
          target_native_window)) {
  CreateKeyDownLocalMonitor(event_handler, target_native_window);
}

void EventCaptureMac::CreateKeyDownLocalMonitor(
    ui::EventHandler* event_handler,
    gfx::NativeWindow target_native_window) {
  DCHECK(event_handler);
  NSWindow* target_window = target_native_window.GetNativeNSWindow();

  // Capture a WeakPtr. This allows the block to detect another event monitor
  // for the same event deleting |this|.
  base::WeakPtr<EventCaptureMac> weak_ptr = factory_.GetWeakPtr();

  auto block = ^NSEvent*(NSEvent* event) {
    if (!weak_ptr) {
      return event;
    }

    if (!target_window || event.window == target_window) {
      std::unique_ptr<ui::Event> ui_event =
          ui::EventFromNative(base::apple::OwnedNSEvent(event));
      if (!ui_event) {
        return event;
      }
      ui::EventType type = ui_event->type();
      if (type == ui::ET_KEY_PRESSED) {
        event_handler->OnKeyEvent(ui_event->AsKeyEvent());
      }
      // Consume the event if allowed and the corresponding EventHandler method
      // requested.
      if (ui_event->cancelable() && ui_event->handled()) {
        return nil;
      }
    }
    return event;
  };

  mouse_capture_delegate_impl_->SetKeyboardMonitor([NSEvent
      addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
                                   handler:block]);
}

EventCaptureMac::~EventCaptureMac() {
  mouse_capture_delegate_impl_->Reset();
}

void EventCaptureMac::SetCrossCursor() {
  [NSCursor.crosshairCursor set];
}

}  // namespace image_editor
