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

#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"

#include <memory>

#include "base/apple/foundation_util.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/crash/core/common/crash_key.h"
#import "components/remote_cocoa/app_shim/features.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_host_helper.h"
#import "components/remote_cocoa/app_shim/views_nswindow_delegate.h"
#import "components/remote_cocoa/app_shim/window_touch_bar_delegate.h"
#include "components/remote_cocoa/common/native_widget_ns_window_host.mojom.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#import "ui/base/cocoa/user_interface_item_command_handler.h"
#import "ui/base/cocoa/window_size_constants.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/native_ui_types.h"

namespace {

bool AreWindowShadowsDisabled() {
  // When:
  // 1) Shadows are being generated by the window server
  // 2) The window with the shadow has a layer (all of Chrome's do)
  // 3) Software compositing is in use (it is in most test configs, which
  //    run in VMs)
  // 4) There are many windows in use at once (they are when running
  //    test in parallel)
  // The window server seems to crash with distressing frequency. To hopefully
  // mitigate that, disable window shadows when running on a bot.
  // For context on this see:
  //   https://crbug.com/899286
  //   https://crbug.com/828031
  //   https://crbug.com/515627, especially #63 and #67
  static bool is_headless = getenv("CHROME_HEADLESS") != nullptr;
  return is_headless;
}

// AppKit quirk: -[NSWindow orderWindow] does not handle reordering for children
// windows. Their order is fixed to the attachment order (the last attached
// window is on the top). Therefore, work around it by re-parenting in our
// desired order.
void OrderChildWindow(NSWindow* child_window,
                      NSWindow* other_window,
                      NSWindowOrderingMode ordering_mode) {
  NSWindow* parent = [child_window parentWindow];
  DCHECK(parent);

  // `ordered_children` sorts children windows back to front.
  NSArray<NSWindow*>* children = [[child_window parentWindow] childWindows];
  std::vector<std::pair<NSInteger, NSWindow*>> ordered_children;
  for (NSWindow* child in children) {
    ordered_children.emplace_back([child orderedIndex], child);
  }
  std::sort(ordered_children.begin(), ordered_children.end(), std::greater<>());

  // If `other_window` is nullptr, place `child_window` in front of (or behind)
  // all other children windows.
  if (other_window == nullptr) {
    other_window = ordering_mode == NSWindowAbove
                       ? ordered_children.back().second
                       : parent;
  }

  if (child_window == other_window) {
    return;
  }

  const bool relative_to_parent = parent == other_window;
  DCHECK(ordering_mode != NSWindowBelow || !relative_to_parent)
      << "Placing a child window behind its parent is not supported.";

  for (NSWindow* child in children) {
    [parent removeChildWindow:child];
  }

  // If `relative_to_parent` is true, `child_window` is the first child of its
  // parent.
  if (relative_to_parent) {
    [parent addChildWindow:child_window ordered:NSWindowAbove];
  }

  // Re-parent children windows in the desired order.
  for (auto [ordered_index, child] : ordered_children) {
    if (child != child_window && child != other_window) {
      [parent addChildWindow:child ordered:NSWindowAbove];
    } else if (child == other_window && !relative_to_parent) {
      if (ordering_mode == NSWindowAbove) {
        [parent addChildWindow:other_window ordered:NSWindowAbove];
        [parent addChildWindow:child_window ordered:NSWindowAbove];
      } else {
        [parent addChildWindow:child_window ordered:NSWindowAbove];
        [parent addChildWindow:other_window ordered:NSWindowAbove];
      }
    }
  }
}

}  // namespace

@interface NSNextStepFrame (Private)
- (instancetype)initWithFrame:(NSRect)frame
                    styleMask:(NSUInteger)styleMask
                        owner:(id)owner;
@end

@interface NSWindow (Private)
+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle;
- (BOOL)hasKeyAppearance;
- (long long)_resizeDirectionForMouseLocation:(CGPoint)location;
- (BOOL)_isConsideredOpenForPersistentState;
- (void)_zoomToScreenEdge:(NSUInteger)edge;
- (void)_removeFromGroups:(NSWindow*)window;
- (void)_setFrame:(NSRect)frameRect
    fromAdjustmentToScreen:(NSScreen*)screen
            anchorIfNeeded:(const CGPoint*)anchor
                   animate:(BOOL)animate;
- (void)_setFrameAfterMove:(NSRect)frameRect;
- (BOOL)_isNonactivatingPanel;
@end

struct NSEdgeAndCornerThicknesses {
  double top, topLeft, left, bottomLeft, bottom, bottomRight, right, topRight;
};

@interface NSWindow (NSWindowResizing)
+ (void)_getExteriorResizeEdgeThicknesses:
            (NSEdgeAndCornerThicknesses*)outThicknesses
                             forStyleMask:(NSWindowStyleMask)styleMask;
@end

// Private API as of at least macOS 13.
@interface NSWindow (NSWindow_Theme)
- (void)_regularMinimizeToDock;
@end

// Private API for window fill.
@interface NSWindow (NSWindow_Fill)
- (void)_zoomFill:(id)sender;
@end

@interface NativeWidgetMacNSWindow () <NSKeyedArchiverDelegate>
- (ViewsNSWindowDelegate*)viewsNSWindowDelegate;
- (BOOL)hasViewsMenuActive;
- (id<NSAccessibility>)rootAccessibilityObject;

// The child window with the highest z-order that is visible and modal, if any.
- (NSWindow*)topmostVisibleChildModalWindow;

// Private API on NSWindow, determines whether the title is drawn on the title
// bar. The title is still visible in menus, Expose, etc.
- (BOOL)_isTitleHidden;
@end

// Use this category to implement mouseDown: on multiple frame view classes
// with different superclasses.
@interface NSView (CRFrameViewAdditions)
- (void)cr_mouseDownOnFrameView:(NSEvent*)event;
@end

@implementation NSView (CRFrameViewAdditions)
// If a mouseDown: falls through to the frame view, turn it into a window drag.
- (void)cr_mouseDownOnFrameView:(NSEvent*)event {
  if ([self.window _resizeDirectionForMouseLocation:event.locationInWindow] !=
      -1)
    return;
  [self.window performWindowDragWithEvent:event];
}
@end

@implementation NativeWidgetMacNSWindowTitledFrame
- (void)mouseDown:(NSEvent*)event {
  if (self.window.movable) {
    [self cr_mouseDownOnFrameView:event];
  }
  [super mouseDown:event];
}
- (BOOL)usesCustomDrawing {
  return NO;
}
// The base implementation just tests [self class] == [NSThemeFrame class].
- (BOOL)_shouldFlipTrafficLightsForRTL {
  return [[self window] windowTitlebarLayoutDirection] ==
         NSUserInterfaceLayoutDirectionRightToLeft;
}
@end

@implementation NativeWidgetMacNSWindowBorderlessFrame

- (void)mouseDown:(NSEvent*)event {
  [self cr_mouseDownOnFrameView:event];
  [super mouseDown:event];
}
- (BOOL)usesCustomDrawing {
  return NO;
}
@end

@implementation NativeWidgetMacNSWindow {
 @private
  CommandDispatcher* __strong _commandDispatcher;
  id<UserInterfaceItemCommandHandler> __strong _commandHandler;
  id<WindowTouchBarDelegate> __weak _touchBarDelegate;
  NSData* __strong _lastSavedRestorableState;
  uint64_t _bridgedNativeWidgetId;
  // This field is not a raw_ptr<> because it requires @property rewrite.
  RAW_PTR_EXCLUSION remote_cocoa::NativeWidgetNSWindowBridge* _bridge;
  BOOL _willUpdateRestorableState;
  BOOL _willSaveRestorableStateAfterDelay;
  BOOL _isEnforcingNeverMadeVisible;
  BOOL _activationIndependence;
  BOOL _isTooltip;
  BOOL _isShufflingForOrdering;
  BOOL _miniaturizationInProgress;
  BOOL _inSetFrameFromAdjustment;
  std::unique_ptr<NativeWidgetMacNSWindowHeadlessInfo> _headless_info;
}
@synthesize bridgedNativeWidgetId = _bridgedNativeWidgetId;
@synthesize bridge = _bridge;
@synthesize isTooltip = _isTooltip;
@synthesize isShufflingForOrdering = _isShufflingForOrdering;
@synthesize preventKeyWindow = _preventKeyWindow;
@synthesize childWindowAddedHandler = _childWindowAddedHandler;
@synthesize childWindowRemovedHandler = _childWindowRemovedHandler;
@synthesize commandDispatchParentOverride = _commandDispatchParentOverride;

- (instancetype)initWithContentRect:(NSRect)contentRect
                          styleMask:(NSUInteger)windowStyle
                            backing:(NSBackingStoreType)bufferingType
                              defer:(BOOL)deferCreation {
  DCHECK(NSEqualRects(contentRect, ui::kWindowSizeDeterminedLater));
  if ((self = [super initWithContentRect:ui::kWindowSizeDeterminedLater
                               styleMask:windowStyle
                                 backing:bufferingType
                                   defer:deferCreation])) {
    _commandDispatcher = [[CommandDispatcher alloc] initWithOwner:self];
    self.releasedWhenClosed = NO;
  }
  return self;
}

- (BOOL)isHeadless {
  return _headless_info != nullptr;
}

- (void)setIsHeadless:(BOOL)isHeadless {
  // NativeWidgetMacNSWindowHeadlessInfo constructor overrides certain NSWindow
  // methods in order to implement headless mode behavior. This affects all
  // NativeWidgetMacNSWindow instances, however, the overrides will fallback to
  // the original implementations if there is no headless info associated with
  // the window.
  if (isHeadless) {
    _headless_info = std::make_unique<NativeWidgetMacNSWindowHeadlessInfo>();
  } else {
    _headless_info.reset();
  }
}

- (NativeWidgetMacNSWindowHeadlessInfo*)headlessInfo {
  return _headless_info.get();
}

- (BOOL)invokeOriginalIsVisibleForTesting {
  // In headless mode this is overridden and returns actual platform window
  // visibility state which is expected to aways be hidden.
  return [self isVisible];
}

// This is called by the "Move Window to {Left/Right} Side of Screen"
// Window menu alternate items (must press Option to see).
// Without this, selecting these items will move child windows like
// bubbles and the find bar, but these should not be movable.
// Instead, let's push this up to the parent window which should be
// the browser.
- (void)_zoomToScreenEdge:(NSUInteger)edge {
  if (self.parentWindow) {
    [self.parentWindow _zoomToScreenEdge:edge];
  } else {
    [super _zoomToScreenEdge:edge];
  }
}

// This override helps diagnose lifetime issues in crash stacktraces by
// inserting a symbol on NativeWidgetMacNSWindow and should be kept even if it
// does nothing.
- (void)dealloc {
  if (_isEnforcingNeverMadeVisible) {
    [self removeObserver:self forKeyPath:@"visible"];
  }
  _willUpdateRestorableState = YES;
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
}

- (void)addChildWindow:(NSWindow*)childWin ordered:(NSWindowOrderingMode)place {
  // Attaching a window to be a child window resets the window level, so
  // restore the window level afterwards.
  NSInteger level = childWin.level;
  [super addChildWindow:childWin ordered:place];
  childWin.level = level;
  if (self.childWindowAddedHandler) {
    self.childWindowAddedHandler(childWin);
  }
}

- (void)removeChildWindow:(NSWindow*)childWin {
  if (self != childWin.parentWindow) {
    return;
  }
  // Handle ordering groups for AppKit native windows. For instance, the
  // `TUINSWindow` is added and removed by AppKit when caps lock is active.
  // See https://crbug.com/369970893 for more details.
  if (![childWin isKindOfClass:[NativeWidgetMacNSWindow class]]) {
    [self maybeRemoveTreeFromOrderingGroups];
  }
  [super removeChildWindow:childWin];
  if (self.childWindowRemovedHandler) {
    self.childWindowRemovedHandler(childWin);
  }
}

- (void)enforceNeverMadeVisible {
  if (_isEnforcingNeverMadeVisible)
    return;
  _isEnforcingNeverMadeVisible = YES;
  [self addObserver:self
         forKeyPath:@"visible"
            options:NSKeyValueObservingOptionNew
            context:nil];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  if ([keyPath isEqual:@"visible"]) {
    DCHECK(_isEnforcingNeverMadeVisible);
    DCHECK_EQ(object, self);
    DCHECK_EQ(context, nil);
    if ([change[NSKeyValueChangeNewKey] boolValue])
      base::debug::DumpWithoutCrashing();
  }
  [super observeValueForKeyPath:keyPath
                       ofObject:object
                         change:change
                        context:context];
}

// Public methods.

- (void)setHasShadow:(BOOL)flag {
  [super setHasShadow:flag && !AreWindowShadowsDisabled()];
}

- (void)setCommandDispatcherDelegate:(id<CommandDispatcherDelegate>)delegate {
  [_commandDispatcher setDelegate:delegate];
}

- (void)setWindowTouchBarDelegate:(id<WindowTouchBarDelegate>)delegate {
  _touchBarDelegate = delegate;
}

- (void)orderFrontKeepWindowKeyState {
  _miniaturizationInProgress = NO;

  if ([self isOnActiveSpace]) {
    [self orderWindow:NSWindowAbove relativeTo:0];
    return;
  }
  // The OS will activate the window if it causes a space switch.
  // Temporarily prevent the window from becoming the key window until after
  // the space change completes.
  _preventKeyWindow = ![self isKeyWindow];
  __block id observer = [NSWorkspace.sharedWorkspace.notificationCenter
      addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
                  object:[NSWorkspace sharedWorkspace]
                   queue:[NSOperationQueue mainQueue]
              usingBlock:^(NSNotification* notification) {
                self->_preventKeyWindow = NO;
                [NSWorkspace.sharedWorkspace.notificationCenter
                    removeObserver:observer];
              }];
  [self orderWindow:NSWindowAbove relativeTo:0];
}

// Private methods.

- (ViewsNSWindowDelegate*)viewsNSWindowDelegate {
  return base::apple::ObjCCastStrict<ViewsNSWindowDelegate>([self delegate]);
}

- (BOOL)hasViewsMenuActive {
  bool hasMenuController = false;
  if (_bridge)
    _bridge->host()->GetHasMenuController(&hasMenuController);
  return hasMenuController;
}

- (id<NSAccessibility>)rootAccessibilityObject {
  id<NSAccessibility> obj =
      _bridge ? _bridge->host_helper()->GetNativeViewAccessible() : nil;
  // We should like to DCHECK that the object returned implements the
  // NSAccessibility protocol, but the NSAccessibilityRemoteUIElement interface
  // does not conform.
  // TODO(crbug.com/41448396): Create a sub-class that does.
  return obj;
}

- (NSAccessibilityRole)accessibilityRole {
  return _isTooltip ? NSAccessibilityHelpTagRole : [super accessibilityRole];
}

// NSWindow overrides.

+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
  if (windowStyle & NSWindowStyleMaskTitled) {
    if (Class customFrame = [NativeWidgetMacNSWindowTitledFrame class])
      return customFrame;
  } else if (Class customFrame =
                 [NativeWidgetMacNSWindowBorderlessFrame class]) {
    return customFrame;
  }
  return [super frameViewClassForStyleMask:windowStyle];
}

// Constrains the frame rect during an active window move loop so that the top
// of the window does not exceed the menu bar / visible top of the display,
// while allowing smooth unrestricted dragging across display boundaries without
// AppKit snapping the window to screen edges.
- (NSRect)constrainFrameRectForMoveLoop:(NSRect)frameRect {
  NSArray<NSScreen*>* screens = [NSScreen screens];
  if (screens.count == 0) {
    return frameRect;
  }

  NSPoint mouse_location = [NSEvent mouseLocation];
  NSScreen* target_screen = nil;

  // First, prefer the screen containing the mouse cursor.
  for (NSScreen* screen in screens) {
    if (NSPointInRect(mouse_location, screen.frame)) {
      target_screen = screen;
      break;
    }
  }

  // If the mouse is not inside any screen, find the screen with the largest
  // intersection with the window frame.
  if (!target_screen) {
    CGFloat max_intersection_area = 0;
    for (NSScreen* screen in screens) {
      NSRect intersection = NSIntersectionRect(frameRect, screen.frame);
      CGFloat area = intersection.size.width * intersection.size.height;
      if (area > max_intersection_area) {
        max_intersection_area = area;
        target_screen = screen;
      }
    }
  }

  // Fallback to mainScreen or the first screen.
  if (!target_screen) {
    target_screen = [NSScreen mainScreen] ?: screens.firstObject;
  }

  if (!target_screen) {
    return frameRect;
  }

  // Check if there is another screen positioned directly above target_screen
  // covering the window's horizontal span, allowing the window to cross upward.
  BOOL has_screen_above = NO;
  for (NSScreen* other_screen in screens) {
    if (other_screen == target_screen) {
      continue;
    }
    if (NSMinY(other_screen.frame) >= NSMaxY(target_screen.frame) - 1.0 &&
        NSMinX(other_screen.frame) < NSMaxX(frameRect) &&
        NSMaxX(other_screen.frame) > NSMinX(frameRect)) {
      has_screen_above = YES;
      break;
    }
  }

  NSRect constrained = frameRect;

  // If there is no screen above, constrain the top of the window to the visible
  // frame (below the menu bar).
  if (!has_screen_above) {
    CGFloat max_top = NSMaxY(target_screen.visibleFrame);
    if (NSMaxY(constrained) > max_top) {
      constrained.origin.y = max_top - constrained.size.height;
    }
  }

  return constrained;
}

// AppKit calls constrainFrameRect:toScreen: to clamp window frames to screen
// boundaries. When a window move loop is active, constrain the top edge to
// respect the menu bar while avoiding AppKit's horizontal snapping across
// display boundaries.
- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen*)screen {
  if (self.isHeadless || self.parentWindow) {
    // AppKit's default implementation moves child windows down to avoid
    // the menu bar. We don't want that behavior, because widgets like the
    // Omnibox may have a big shadow that could cause invisible menu bar
    // collision in fullscreen/maximized state. We override it here to
    // return the original frameRect before the adjustment.
    return frameRect;
  }

  if (base::FeatureList::IsEnabled(
          remote_cocoa::features::
              kSuppressAppKitFrameAdjustmentsDuringMoveLoop) &&
      _bridge && _bridge->window_move_loop()) {
    return [self constrainFrameRectForMoveLoop:frameRect];
  }

  return [super constrainFrameRect:frameRect toScreen:screen];
}

// AppKit's internal screen layout manager invokes
// _setFrame:fromAdjustmentToScreen: and _setFrameAfterMove: asynchronously
// during screen layout changes or display boundary transitions to adjust the
// window origin. In headless mode or while CocoaWindowMoveLoop manually drives
// window positioning during a tab drag, suppress these callbacks so that
// headless window positioning is not constrained to physical display bounds
// and stale AppKit callbacks do not overwrite the move loop's calculated
// position.
- (void)_setFrame:(NSRect)frameRect
    fromAdjustmentToScreen:(NSScreen*)screen
            anchorIfNeeded:(const CGPoint*)anchor
                   animate:(BOOL)animate {
  if (self.isHeadless ||
      (base::FeatureList::IsEnabled(
           remote_cocoa::features::
               kSuppressAppKitFrameAdjustmentsDuringMoveLoop) &&
       _bridge && _bridge->window_move_loop())) {
    return;
  }
  if (_inSetFrameFromAdjustment) {
    return;
  }
  base::AutoReset<BOOL> resetter(&_inSetFrameFromAdjustment, YES);
  if ([NSWindow instancesRespondToSelector:_cmd]) {
    [super _setFrame:frameRect
        fromAdjustmentToScreen:screen
                anchorIfNeeded:anchor
                       animate:animate];
  }
}

- (void)_setFrameAfterMove:(NSRect)frameRect {
  if (self.isHeadless ||
      (base::FeatureList::IsEnabled(
           remote_cocoa::features::
               kSuppressAppKitFrameAdjustmentsDuringMoveLoop) &&
       _bridge && _bridge->window_move_loop())) {
    return;
  }
  if (_inSetFrameFromAdjustment) {
    return;
  }
  base::AutoReset<BOOL> resetter(&_inSetFrameFromAdjustment, YES);
  if ([NSWindow instancesRespondToSelector:_cmd]) {
    [super _setFrameAfterMove:frameRect];
  }
}

- (NSWindow*)topmostVisibleChildModalWindow {
  if (!_bridge) {
    return nil;
  }

  for (remote_cocoa::NativeWidgetNSWindowBridge* child_bridge :
       base::Reversed(_bridge->child_windows())) {
    if (child_bridge->modal_type() == ui::mojom::ModalType::kNone) {
      continue;
    }
    NSWindow* child_ns_window = child_bridge->ns_window();
    if ([child_ns_window isVisible]) {
      return child_ns_window;
    }
  }

  return nil;
}

- (BOOL)_isTitleHidden {
  bool shouldShowWindowTitle = YES;
  if (_bridge)
    _bridge->host()->GetShouldShowWindowTitle(&shouldShowWindowTitle);
  return !shouldShowWindowTitle;
}

// The base implementation returns YES if the window's frame view is a custom
// class, which causes undesirable changes in behavior. AppKit NSWindow
// subclasses are known to override it and return NO.
- (BOOL)_usesCustomDrawing {
  return NO;
}

// This override, if it returns YES, allows the window to take input events
// without activating the owning app. This is functionally equivalent to having
// the window style NSWindowStyleMaskNonactivatingPanel set; see the
// documentation for that constant for more details.
//
// The NSWindowStyleMaskNonactivatingPanel constant is only valid for NSPanels,
// not NSWindows, so the window style cannot be directly set. In addition, even
// if it were valid to set that style for windows, setting the window style
// recalculates and re-caches a bunch of stuff, so a surgical override is the
// cleanest approach.
- (BOOL)_isNonactivatingPanel {
  if (_activationIndependence) {
    return YES;
  }
  return [super _isNonactivatingPanel];
}

+ (void)_getExteriorResizeEdgeThicknesses:
            (NSEdgeAndCornerThicknesses*)outThicknesses
                             forStyleMask:(NSWindowStyleMask)styleMask {
  // Ensure non-titled resizable windows have a reasonable exterior resize area.
  // By default, they might have none, making resizing difficult.
  // Override to titled window's resize edge thickness (4px on macOS 15).
  if (styleMask & NSWindowStyleMaskResizable) {
    return [super
        _getExteriorResizeEdgeThicknesses:outThicknesses
                             forStyleMask:styleMask | NSWindowStyleMaskTitled];
  }

  return [super _getExteriorResizeEdgeThicknesses:outThicknesses
                                     forStyleMask:styleMask];
}

// Ignore [super canBecome{Key,Main}Window]. The default is NO for windows with
// NSWindowStyleMaskBorderless, which is not the desired behavior.
// Note these can be called via -[NSWindow close] while the widget is being torn
// down, so check for a delegate.
- (BOOL)canBecomeKeyWindow {
  if (_preventKeyWindow)
    return NO;
  bool canBecomeKey = NO;
  if (_bridge)
    _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey);
  return canBecomeKey;
}

- (BOOL)canBecomeMainWindow {
  if (!_bridge)
    return NO;

  // Dialogs and bubbles shouldn't take large shadows away from their parent.
  if (_bridge->parent())
    return NO;

  bool canBecomeKey = NO;
  if (_bridge)
    _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey);
  return canBecomeKey;
}

// Lets the traffic light buttons on the parent window keep their active state.
- (BOOL)hasKeyAppearance {
  // Note that this function is called off of the main thread. In such cases,
  // it is not safe to access the mojo interface or the ui::Widget, as they are
  // not reentrant.
  // https://crbug.com/941506.
  if (![NSThread isMainThread])
    return [super hasKeyAppearance];
  if (_bridge) {
    bool isAlwaysRenderWindowAsKey = NO;
    _bridge->host()->GetAlwaysRenderWindowAsKey(&isAlwaysRenderWindowAsKey);
    if (isAlwaysRenderWindowAsKey)
      return YES;
  }
  return [super hasKeyAppearance];
}

// Override sendEvent to intercept window drag events and allow key events to be
// forwarded to a toolkit-views menu while it is active, and while still
// allowing any native subview to retain firstResponder status.
- (void)sendEvent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::sendEvent", "WindowNum",
               [self windowNumber]);

  // Let CommandDispatcher check if this is a redispatched event.
  if ([_commandDispatcher preSendEvent:event]) {
    TRACE_EVENT_INSTANT("browser", "StopSendEvent");
    return;
  }

  NSEventType type = [event type];

  // Handle double-click on custom draggable regions outside the native
  // titlebar. macOS AppKit natively handles double-click-to-zoom for the
  // native titlebar region (via _NSTitlebarContainerView), but not for custom
  // draggable areas like the empty space in a vertical tab strip.
  //
  // Only intercept mouse-up events within contentLayoutRect (which excludes
  // the native titlebar and the window resize handle) where hitTest: returns
  // nil (indicating a custom draggable background). Performing the action on
  // mouse-up matches the native titlebar behavior.
  if (type == NSEventTypeLeftMouseUp && [event clickCount] == 2) {
    const BOOL hitCustomDraggableArea =
        NSPointInRect(event.locationInWindow, self.contentLayoutRect) &&
        [[self contentView] hitTest:event.locationInWindow] == nil;
    if (hitCustomDraggableArea) {
      NSString* action = [[NSUserDefaults standardUserDefaults]
          stringForKey:@"AppleActionOnDoubleClick"];
      if ([action isEqualToString:@"Fill"] &&
          [self respondsToSelector:@selector(_zoomFill:)]) {
        [self _zoomFill:nil];
      } else if (!action || [action isEqualToString:@"Maximize"]) {
        [self performZoom:nil];
      } else if ([action isEqualToString:@"Minimize"]) {
        [self performMiniaturize:nil];
      }
      // "None" or unrecognized value => do nothing.
      return;
    }
  }

  // Draggable regions only respond to left-click dragging, but the system will
  // still suppress right-clicks in a draggable region. Forwarding right-clicks
  // and ctrl+left-clicks allows the underlying views to respond to right-click
  // to potentially bring up a frame context menu.
  if (type == NSEventTypeRightMouseDown ||
      (type == NSEventTypeLeftMouseDown &&
       ([event modifierFlags] & NSEventModifierFlagControl))) {
    if ([[self contentView] hitTest:event.locationInWindow] == nil) {
      [[self contentView] rightMouseDown:event];
      return;
    }
  } else if (type == NSEventTypeRightMouseUp) {
    if ([[self contentView] hitTest:event.locationInWindow] == nil) {
      [[self contentView] rightMouseUp:event];
      return;
    }
  } else if (type == NSEventTypeLeftMouseDown) {
    // Check whether the click was in a blocked area via a hit test.
    bool is_blocked_by_modal = false;
    if (_bridge) {
      NSView* content_view = [self contentView];
      NSPoint point_in_view = [content_view convertPoint:event.locationInWindow
                                                fromView:nil];
      gfx::Point flipped_point(
          point_in_view.x, NSHeight([content_view frame]) - point_in_view.y);
      remote_cocoa::mojom::HitTestResult hit_test_result =
          remote_cocoa::mojom::HitTestResult::kOther;
      _bridge->host()->GetHitTestResult(flipped_point, &hit_test_result);
      is_blocked_by_modal = hit_test_result ==
                            remote_cocoa::mojom::HitTestResult::kBlockedSubView;
    }

    NSWindow* child_modal_window = [self topmostVisibleChildModalWindow];
    // If the click was in a blocked area and we're displaying a child modal
    // window, swallow the event to prevent the web contents from processing it
    // (and potentially triggering new dialogs).
    if (is_blocked_by_modal && child_modal_window) {
      if (![child_modal_window isKeyWindow]) {
        [child_modal_window makeKeyWindow];
      }
      return;
    }
  } else if ([self hasViewsMenuActive]) {
    // Send to the menu, after converting the event into an action message using
    // the content view.
    if (type == NSEventTypeKeyDown) {
      [[self contentView] keyDown:event];
      return;
    } else if (type == NSEventTypeKeyUp) {
      [[self contentView] keyUp:event];
      return;
    }
  }

  [super sendEvent:event];
}

- (void)orderWindowByShuffling:(NSWindowOrderingMode)orderingMode
                    relativeTo:(NSInteger)otherWindowNumber {
  NativeWidgetMacNSWindow* parent =
      static_cast<NativeWidgetMacNSWindow*>([self parentWindow]);

  // This is not a child window. No need to patch.
  if (!parent) {
    [self orderWindow:orderingMode relativeTo:otherWindowNumber];
    return;
  }

  base::AutoReset<BOOL> shuffling(&_isShufflingForOrdering, YES);

  // `otherWindow` is nil if `otherWindowNumber` is 0. In this case, place
  // `self` at the top / bottom, depending on `orderingMode`.
  NSWindow* otherWindow = [NSApp windowWithWindowNumber:otherWindowNumber];
  if (otherWindow == nullptr || parent == [otherWindow parentWindow] ||
      parent == otherWindow) {
    OrderChildWindow(self, otherWindow, orderingMode);
  }

  [[self viewsNSWindowDelegate] onWindowOrderChanged:nil];
}

- (void)setActivationIndependence:(BOOL)independence {
  self.canHide = !independence;
  _activationIndependence = independence;
}

- (bool)activationIndependence {
  return _activationIndependence;
}

// Override window order functions to intercept other visibility changes. This
// is needed in addition to the -[NSWindow display] override because Cocoa
// hardly ever calls display, and reports -[NSWindow isVisible] incorrectly
// when ordering in a window for the first time.
// Note that this methods has no effect for children windows. Use
// -orderWindowByShuffling:relativeTo: instead.
- (void)orderWindow:(NSWindowOrderingMode)orderingMode
         relativeTo:(NSInteger)otherWindowNumber {
  // Prevent a window that should never be visible from being ordered in.
  // External frameworks (e.g., AuthenticationServicesCore presenting
  // passkey/WebAuthn dialogs) can trigger window ordering on the invisible
  // browser-side proxy window for app shims, causing a DumpWithoutCrashing.
  // See https://crbug.com/325931972 and https://crbug.com/40626510.
  if (_isEnforcingNeverMadeVisible && orderingMode != NSWindowOut) {
    return;
  }
  [super orderWindow:orderingMode relativeTo:otherWindowNumber];
  [[self viewsNSWindowDelegate] onWindowOrderChanged:nil];
}

- (void)miniaturize:(id)sender {
  // The miniaturize operation appears to not be "atomic" because of
  // non-blocking roundtrip IPC with the Dock. We want to note here that
  // miniaturization is in progress. The process completes when we reach
  // -_regularMinimizeToDock:.
  _miniaturizationInProgress = YES;

  [super miniaturize:sender];
}

- (void)_regularMinimizeToDock {
  // A call to -miniaturize: kicks of an async round-trip IPC with the Dock that
  // ends up in this method. Unfortunately, it appears that if we immediately
  // follow a call to -miniaturize: with -makeKeyAndOrderFront:, the AppKit
  // doesn't cancel the in-flight round-trip IPC. As a result,
  // _regularMinimizeToDock gets called sometime after -makeKeyAndOrderFront:
  // and miniaturizes the window anyway. This is  a potential problem in session
  // restore where we might restart with a single browser window sitting Dock.
  // In that case, Session Restore creates the window, miniaturizes to the dock,
  // and then brings it back out. With this behavior (which seems like a bug),
  // the browser window may not be restored from the Dock.
  //
  // To get around this problem, if we arrive here and
  // _miniaturizationInProgress is NO, the miniaturization process was cancelled
  // by a call to -makeKeyAndOrderFront:. In that case, we don't want to proceed
  // with miniaturization.
  if (!_miniaturizationInProgress) {
    return;
  }

  _miniaturizationInProgress = NO;
  [super _regularMinimizeToDock];
}

- (void)makeKeyAndOrderFront:(id)sender {
  _miniaturizationInProgress = NO;
  [super makeKeyAndOrderFront:sender];
}

- (void)orderOut:(id)sender {
  _miniaturizationInProgress = NO;
  [self maybeRemoveTreeFromOrderingGroups];
  [super orderOut:sender];
}

- (void)close {
  [self maybeRemoveTreeFromOrderingGroups];
  [super close];
}

// NSResponder implementation.

- (BOOL)performKeyEquivalent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::performKeyEquivalent", "WindowNum",
               [self windowNumber]);
  return [_commandDispatcher performKeyEquivalent:event];
}

- (void)cursorUpdate:(NSEvent*)theEvent {
  // The cursor provided by the delegate should only be applied within the
  // content area. This is because we rely on the contentView to track the
  // mouse cursor and forward cursorUpdate: messages up the responder chain.
  // The cursorUpdate: isn't handled in BridgedContentView because views-style
  // SetCapture() conflicts with the way tracking events are processed for
  // the view during a drag. Since the NSWindow is still in the responder chain
  // overriding cursorUpdate: here handles both cases.
  if (!NSPointInRect([theEvent locationInWindow], [[self contentView] frame])) {
    [super cursorUpdate:theEvent];
    return;
  }

  NSCursor* cursor = [[self viewsNSWindowDelegate] cursor];
  if (cursor)
    [cursor set];
  else
    [super cursorUpdate:theEvent];
}

- (NSTouchBar*)makeTouchBar {
  return _touchBarDelegate ? [_touchBarDelegate makeTouchBar] : nil;
}

// Called when the window is the delegate of the archiver passed to
// |-encodeRestorableStateWithCoder:|, below. It prevents the archiver from
// trying to encode the window or an NSView, say, to represent the first
// responder. When AppKit calls |-encodeRestorableStateWithCoder:|, it
// accomplishes the same thing by passing a custom coder.
- (id)archiver:(NSKeyedArchiver*)archiver willEncodeObject:(id)object {
  if (object == self)
    return nil;
  if ([object isKindOfClass:[NSView class]])
    return nil;
  return object;
}

- (void)saveRestorableState {
  if (!_bridge || ![self _isConsideredOpenForPersistentState]) {
    return;
  }

  // Certain conditions, such as in the Speedometer 3 benchmark, can trigger a
  // rapid succession of calls to saveRestorableState. If there's no pending
  // save of restorable state, save the state now. This ensures that the first
  // new state change gets saved immediately. Then, set up to save again 500ms
  // after the last request. This will coalesce a storm of restorable state
  // saves into the first and last requests. This might ultimately result in a
  // single save operation if the first and last states are identical.
  //
  // We take pains to save the first and last requests to ensure we get the
  // expected state save on browser close. For example, if a browser window
  // miniaturizes and then the browser quits within our 500ms delay, the
  // miniaturized state may not get saved. Even if the call to
  // -reallySaveRestorableState occurs in time, we might still be in trouble
  // because the save has to cross the remote cocoa boundary (and so is
  // dependent on a couple more turns of the run loop to get the save to take).
  if (!_willSaveRestorableStateAfterDelay) {
    [self reallySaveRestorableState];
    _willSaveRestorableStateAfterDelay = YES;
  }

  [NSObject cancelPreviousPerformRequestsWithTarget:self
                                           selector:@selector
                                           (reallySaveRestorableState)
                                             object:nil];
  [self performSelector:@selector(reallySaveRestorableState)
             withObject:nil
             afterDelay:0.5];
}

- (void)reallySaveRestorableState {
  _willSaveRestorableStateAfterDelay = NO;

  if (!_bridge) {
    return;
  }

  _willUpdateRestorableState = NO;

  // Create restorable state archives with secure encoding. See the article at
  // https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/
  // for more details.
  NSKeyedArchiver* encoder =
      [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
  encoder.delegate = self;
  [self encodeRestorableStateWithCoder:encoder];
  [encoder finishEncoding];
  NSData* restorableState = encoder.encodedData;

  // Don't bother saving restorable state if it didn't actually change since
  // the last save. This avoids an extra IPC when nothing has changed.
  if ([restorableState isEqual:_lastSavedRestorableState]) {
    return;
  }
  _lastSavedRestorableState = restorableState;

  auto data_span = base::apple::NSDataToSpan(restorableState);
  std::vector<uint8_t> data(data_span.size());
  base::span<uint8_t>(data).copy_from(data_span);
  _bridge->host()->OnWindowStateRestorationDataChanged(std::move(data));
}

// AppKit calls -invalidateRestorableState when a property of the window which
// affects its restorable state changes.
- (void)invalidateRestorableState {
  [super invalidateRestorableState];
  if ([self _isConsideredOpenForPersistentState]) {
    if (_willUpdateRestorableState)
      return;
    _willUpdateRestorableState = YES;
    [self performSelectorOnMainThread:@selector(saveRestorableState)
                           withObject:nil
                        waitUntilDone:NO
                                modes:@[ NSDefaultRunLoopMode ]];
  } else if (_willUpdateRestorableState) {
    _willUpdateRestorableState = NO;
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
  }
}

// On newer SDKs, _canMiniaturize respects NSWindowStyleMaskMiniaturizable in
// the window's styleMask. Views assumes that Widgets can always be minimized,
// regardless of their window style, so override that behavior here.
- (BOOL)_canMiniaturize {
  return ![self immersiveFullscreen];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
  // If this window or its parent does not handle commands, remove it from the
  // chain.
  bool isCommandDispatch =
      aSelector == @selector(commandDispatch:) ||
      aSelector == @selector(commandDispatchUsingKeyModifiers:);
  if (isCommandDispatch && _commandHandler == nil &&
      self.commandDispatchParent == nil) {
    return NO;
  }

  // Private AppKit frame adjustment methods should only be advertised if the
  // superclass implements them on this macOS version.
  if (aSelector ==
          @selector(_setFrame:fromAdjustmentToScreen:anchorIfNeeded:animate:) ||
      aSelector == @selector(_setFrameAfterMove:)) {
    return [NSWindow instancesRespondToSelector:aSelector];
  }

  return [super respondsToSelector:aSelector];
}

// CommandDispatchingWindow implementation.

- (void)setCommandHandler:(id<UserInterfaceItemCommandHandler>)commandHandler {
  _commandHandler = commandHandler;
}

- (CommandDispatcher*)commandDispatcher {
  return _commandDispatcher;
}

- (BOOL)defaultPerformKeyEquivalent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::defaultPerformKeyEquivalent", "WindowNum",
               [self windowNumber]);
  return [super performKeyEquivalent:event];
}

- (BOOL)defaultValidateUserInterfaceItem:
    (id<NSValidatedUserInterfaceItem>)item {
  return [super validateUserInterfaceItem:item];
}

- (void)commandDispatch:(id)sender {
  [_commandDispatcher dispatch:sender forHandler:_commandHandler];
}

- (void)commandDispatchUsingKeyModifiers:(id)sender {
  [_commandDispatcher dispatchUsingKeyModifiers:sender
                                     forHandler:_commandHandler];
}

// NSWindow overrides (NSUserInterfaceItemValidations implementation)

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return [_commandDispatcher validateUserInterfaceItem:item
                                            forHandler:_commandHandler];
}

// Override performClose: to forward to the parent window when this window
// doesn't have NSWindowStyleMaskClosable. This ensures that when a bubble
// (child window) is focused in a PWA and the user presses Cmd+W, the action
// closes the parent window.
- (void)performClose:(id)sender {
  if (!(self.styleMask & NSWindowStyleMaskClosable) && self.parentWindow) {
    [self.parentWindow performClose:sender];
    return;
  }
  [super performClose:sender];
}

// NSWindow overrides (NSAccessibility informal protocol implementation).

- (NSString*)accessibilityDocument {
  if (id<NSAccessibility> root = [self rootAccessibilityObject]) {
    if (auto* cocoaNode = ui::AXPlatformNode::FromNativeViewAccessible(
            gfx::NativeViewAccessible(root))) {
      return [NSString stringWithUTF8String:cocoaNode->GetRootURL().c_str()];
    }
  }
  return nil;
}

- (id)accessibilityFocusedUIElement {
  if (![self delegate])
    return [super accessibilityFocusedUIElement];

  // The SDK documents this as "The deepest descendant of the accessibility
  // hierarchy that has the focus" and says "if a child element does not have
  // the focus, either return self or, if available, invoke the superclass's
  // implementation."
  // The behavior of NSWindow is usually to return null, except when the window
  // is first shown, when it returns self. But in the second case, we can
  // provide richer a11y information by reporting the views::RootView instead.
  // Additionally, if we don't do this, VoiceOver reads out the partial a11y
  // properties on the NSWindow and repeats them when focusing an item in the
  // RootView's a11y group. See http://crbug.com/748221.
  id superFocus = [super accessibilityFocusedUIElement];
  if (!_bridge || superFocus != self)
    return superFocus;

  return _bridge->host_helper()->GetNativeViewAccessible();
}

- (NSString*)accessibilityTitle {
  // Check when NSWindow is asked for its title to provide the title given by
  // the views::RootView (and WidgetDelegate::GetAccessibleWindowTitle()). For
  // all other attributes, use what NSWindow provides by default since diverging
  // from NSWindow's behavior can easily break VoiceOver integration.
  NSString* viewsValue = self.rootAccessibilityObject.accessibilityTitle;
  return viewsValue ? viewsValue : [super accessibilityTitle];
}

- (NSWindow<CommandDispatchingWindow>*)commandDispatchParent {
  if (_commandDispatchParentOverride) {
    return _commandDispatchParentOverride;
  }
  NSWindow* parent = self.parentWindow;
  if (parent && [parent hasKeyAppearance] &&
      [parent conformsToProtocol:@protocol(CommandDispatchingWindow)]) {
    return static_cast<NSWindow<CommandDispatchingWindow>*>(parent);
  }
  return nil;
}

// During window ordering AppKit rebuilds its internal ordering group for the
// window tree. A window tree in Chrome's case is the browser window and all of
// its descendants, which can include Chrome and AppKit created windows. It does
// this by removing and re-adding each window in the window tree from the
// ordering group. If a window is re-added to the group while in a non-active
// space, a space switch can occur. The space switch will only happen if the
// current window tree has existing windows that are still a part of the
// ordering group. When there are two levels in the window tree, each window
// will be removed from the group before windows are re-added to the group. If
// three or more window levels exist in the tree not all windows will be removed
// from the group before windows are re-added to the group, causing a space
// switch to occur. It seems this is an unintentional side effect of AppKit's
// recursive window tree group rebuilding.
//
// To work around this behavior, preemptively remove the window tree from
// ordering groups. This workaround should be considered low risk, while we are
// calling an undocumented NSWindow method, removing the window tree from
// ordering groups is ubiquitous throughout AppKit. Additionally, this
// preemptive removal is only called before an -orderOut: or -close. AppKit will
// be doing an ordering group rebuild during those calls. We are also taking
// care to only apply this workaround when necessary.
//
// TODO(http://crbug.com/1454606): Remove this workaround once FB13529873 is
// fixed in AppKit.
- (void)maybeRemoveTreeFromOrderingGroups {
  if (!base::FeatureList::IsEnabled(
          remote_cocoa::features::kImmersiveFullscreenSpaceSwitchMitigation)) {
    return;
  }

  // Only remove from groups if this window is not on the active space.
  if (self.onActiveSpace) {
    return;
  }

  // Only remove from groups if the browser is in immersive fullscreen.
  if (![self immersiveFullscreen]) {
    return;
  }

  // Since _removeFromGroups: is not documented it could go away in newer
  // versions of macOS. If the selector does not exist, DumpWithoutCrashing() so
  // we hear about the change.
  if (![NSWindow instancesRespondToSelector:@selector(_removeFromGroups:)]) {
    base::debug::DumpWithoutCrashing();
    return;
  }

  // Iterate instead of recurse. There are other NSWindow types in the tree
  // besides NativeWidgetMacNSWindow that would not implement our recursion.
  NSMutableArray* nextWindows = [NSMutableArray array];
  [nextWindows addObject:[self rootWindow]];
  while (nextWindows.count) {
    NSWindow* currentWindow = nextWindows.lastObject;
    [nextWindows removeLastObject];
    for (NSWindow* child in currentWindow.childWindows) {
      [nextWindows addObject:child];
      [currentWindow _removeFromGroups:child];
    }
  }
}

- (NSWindow*)rootWindow {
  NSWindow* root = self;
  while (root.parentWindow) {
    root = root.parentWindow;
  }
  return root;
}

- (BOOL)immersiveFullscreen {
  NativeWidgetMacNSWindow* rootWidgetWindow =
      base::apple::ObjCCast<NativeWidgetMacNSWindow>([self rootWindow]);
  if (rootWidgetWindow &&
      (rootWidgetWindow.styleMask & NSWindowStyleMaskFullScreen) &&
      rootWidgetWindow.bridge &&
      rootWidgetWindow.bridge->ImmersiveFullscreenEnabled()) {
    return YES;
  }
  return NO;
}

- (NSWindow*)preferredSheetParent {
  return [self immersiveFullscreen] ? [self rootWindow] : self;
}

#ifndef NDEBUG
- (NSString*)debugDescription {
  if (!self.title.length) {
    return [super debugDescription];
  }
  return [NSString
      stringWithFormat:@"%@ - %@", [super debugDescription], self.title];
}
#endif  // NDEBUG

@end
