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

#include "components/remote_cocoa/app_shim/window_move_loop.h"

#include <map>
#include <memory>
#include <utility>
#include <vector>

#include "base/auto_reset.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "ui/display/screen.h"
#import "ui/gfx/mac/coordinate_conversion.h"

// When event monitors process the events the full list of monitors is cached,
// and if we unregister the event monitor that's at the end of the list while
// processing the first monitor's handler -- the callback for the unregistered
// monitor will still be called even though it's unregistered. This will result
// in dereferencing an invalid pointer.
//
// WeakCocoaWindowMoveLoop is retained by the event monitor and stores weak
// pointer for the CocoaWindowMoveLoop, so there will be no invalid memory
// access.
@interface WeakCocoaWindowMoveLoop : NSObject {
 @private
  base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop> _weak;
}
@end

@implementation WeakCocoaWindowMoveLoop
- (instancetype)initWithWeakPtr:
    (const base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop>&)weak {
  if ((self = [super init])) {
    _weak = weak;
  }
  return self;
}

- (base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop>&)weak {
  return _weak;
}
@end

namespace {

// This class addresses a macOS 14 issue where child windows don't follow
// the parent during tab dragging.
class ChildWindowMover {
 public:
  ChildWindowMover(NSWindow* window) : window_(window) {
    initial_parent_origin_ = gfx::Point(window.frame.origin);
    for (NSWindow* child in window.childWindows) {
      initial_origins_.emplace_back(child, child.frame.origin);
    }
  }

  // Moves child windows based on a parent origin offset relative to their
  // initial origins captured at the construction of this class.
  void MoveByOriginOffset() {
    if (!window_) {
      return;
    }

    gfx::Point parent_origin = gfx::Point(window_.frame.origin);
    gfx::Vector2d origin_offset(parent_origin.x() - initial_parent_origin_.x(),
                                parent_origin.y() - initial_parent_origin_.y());

    for (const auto& [child, initial_origin] : initial_origins_) {
      if (!child || child.parentWindow != window_) {
        continue;
      }

      gfx::Point expected_origin = initial_origin + origin_offset;
      // On macOS 14, child windows occasionally fail to follow their parent
      // during tab dragging. A workaround for this issue is to temporarily
      // remove the child window, set its frame origin, and then re-add it.
      [window_ removeChildWindow:child];
      [child
          setFrameOrigin:NSMakePoint(expected_origin.x(), expected_origin.y())];
      [window_ addChildWindow:child ordered:NSWindowAbove];
    }
  }

 private:
  NSWindow* __weak window_;
  std::vector<std::pair<NSWindow * __weak, gfx::Point>> initial_origins_;
  gfx::Point initial_parent_origin_;
};

}  // namespace

namespace remote_cocoa {

namespace {
bool g_move_loop_active = false;
}  // namespace

// static
bool CocoaWindowMoveLoop::IsActive() {
  return g_move_loop_active;
}

CocoaWindowMoveLoop::CocoaWindowMoveLoop(NativeWidgetNSWindowBridge* owner,
                                         const NSPoint& initial_mouse_in_screen)
    : owner_(owner),
      base_mouse_in_screen_(initial_mouse_in_screen),
      weak_factory_(this) {}

CocoaWindowMoveLoop::~CocoaWindowMoveLoop() {
  // Handle the pathological case, where |this| is destroyed while running.
  if (exit_reason_ref_) {
    *exit_reason_ref_ = WINDOW_DESTROYED;
    std::move(quit_closure_).Run();
  }

  owner_ = nullptr;
}

bool CocoaWindowMoveLoop::Run() {
  CHECK(!g_move_loop_active);
  // |this| may be deleted while the nested run loop below is spinning, so this
  // must not refer to any member state on destruction.
  base::AutoReset<bool> active_resetter(&g_move_loop_active, true);

  LoopExitReason exit_reason = ENDED_EXTERNALLY;
  exit_reason_ref_ = &exit_reason;
  NSWindow* window = owner_->ns_window();
  base_frame_ = [window frame];
  last_set_frame_ = base_frame_;
  __block ChildWindowMover child_window_mover(window);

  base::RunLoop run_loop;
  quit_closure_ = run_loop.QuitClosure();

  // Will be retained by the monitor handler block.
  WeakCocoaWindowMoveLoop* weak_cocoa_window_move_loop =
      [[WeakCocoaWindowMoveLoop alloc]
          initWithWeakPtr:weak_factory_.GetWeakPtr()];

  __block BOOL has_moved = NO;
  screen_disabler_ = std::make_unique<gfx::ScopedCocoaDisableScreenUpdates>();

  // Esc keypress is handled by EscapeTracker, which is installed by
  // TabDragController.
  NSEventMask mask = NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged |
                     NSEventMaskMouseMoved;
  auto handler = ^NSEvent*(NSEvent* event) {
    // The docs say this always runs on the main thread, but if it didn't,
    // it would explain https://crbug.com/876493, so let's make sure.
    CHECK(NSThread.isMainThread);

    CocoaWindowMoveLoop* strong = [weak_cocoa_window_move_loop weak].get();
    if (!strong || !strong->exit_reason_ref_) {
      // By this point CocoaWindowMoveLoop was deleted while processing this
      // same event, and this event monitor was not unregistered in time. See
      // the WeakCocoaWindowMoveLoop comment above.
      // Continue processing the event.
      return event;
    }

    if ([event type] == NSEventTypeLeftMouseDragged) {
      const NSPoint mouse_in_screen = [NSEvent mouseLocation];
      NSRect current_frame = [window frame];
      if (!NSEqualSizes(current_frame.size, strong->last_set_frame_.size)) {
        // If the window frame has been modified programmatically (e.g. resized
        // by TabDragController to fit display boundaries), re-baseline our move
        // loop coordinate calculations so future drag events don't revert it.
        // On Mac, coordinates are bottom-left relative, so resizing the window
        // height shifts its bottom-left origin. Re-baselining here prevents
        // the window from jumping vertically on the next drag event.
        strong->base_frame_ = current_frame;
        strong->base_mouse_in_screen_ = mouse_in_screen;
      }
      gfx::Vector2d mouse_offset(
          mouse_in_screen.x - strong->base_mouse_in_screen_.x,
          mouse_in_screen.y - strong->base_mouse_in_screen_.y);
      NSRect ns_frame =
          NSOffsetRect(strong->base_frame_, mouse_offset.x(), mouse_offset.y());
      [window setFrame:ns_frame display:NO animate:NO];
      child_window_mover.MoveByOriginOffset();
      // `setFrame:...` may have destroyed `this`, so do the weak check again.
      bool is_valid = [weak_cocoa_window_move_loop weak].get() == strong;
      if (is_valid) {
        // Read `[window frame]` rather than using `ns_frame` because
        // `setFrame:` can trigger synchronous geometry callbacks (e.g.
        // TabDragController resizing across display boundaries via SetBounds).
        // Reading `[window frame]` preserves any programmatic adjustments
        // applied during that call stack rather than overwriting with stale
        // `ns_frame`.
        strong->last_set_frame_ = [window frame];
        if (!has_moved) {
          has_moved = YES;
          strong->screen_disabler_.reset();
        }
      }

      return event;
    }

    // In theory, we shouldn't see any kind of NSEventTypeMouseMoved, but if we
    // see one and the left button isn't pressed, we know for a fact that we
    // missed a NSEventTypeLeftMouseUp.
    BOOL unexpectedMove = [event type] == NSEventTypeMouseMoved &&
                          ([NSEvent pressedMouseButtons] & 1) != 1;
    if (unexpectedMove || [event type] == NSEventTypeLeftMouseUp) {
      *strong->exit_reason_ref_ = MOUSE_UP;
      std::move(strong->quit_closure_).Run();
    }
    return event;  // Process the MouseUp.
  };
  id monitor = [NSEvent addLocalMonitorForEventsMatchingMask:mask
                                                     handler:handler];

  run_loop.Run();
  [NSEvent removeMonitor:monitor];

  if (exit_reason != WINDOW_DESTROYED && exit_reason != ENDED_EXTERNALLY) {
    exit_reason_ref_ = nullptr;  // Ensure End() doesn't replace the reason.
    owner_->EndMoveLoop();       // Deletes |this|.
  }

  return exit_reason == MOUSE_UP;
}

void CocoaWindowMoveLoop::SetBaseFrame(const NSRect& new_base_frame,
                                       const NSPoint& new_base_mouse) {
  base_frame_ = new_base_frame;
  base_mouse_in_screen_ = new_base_mouse;
  last_set_frame_ = new_base_frame;
}

void CocoaWindowMoveLoop::End() {
  screen_disabler_.reset();
  if (exit_reason_ref_) {
    DCHECK_EQ(*exit_reason_ref_, ENDED_EXTERNALLY);
    // Ensure the destructor doesn't replace the reason.
    exit_reason_ref_ = nullptr;
    std::move(quit_closure_).Run();
  }
}

}  // namespace remote_cocoa
