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

#import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_delegate.h"

#include <cmath>

#include "base/auto_reset.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/actor/ui/actor_overlay_ui.h"
#include "chrome/browser/glic/host/guest_util.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/profiles/profile.h"
#import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_history_swiper.h"
#include "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_history_swiping_control.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/tabs/inactive_window_mouse_event_controller.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_utils.h"
#include "components/prefs/pref_service.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck_panel.mojom.h"
#include "components/tabs/public/tab_interface.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/service_manager/public/cpp/interface_provider.h"

@interface ChromeRenderWidgetHostViewMacDelegate () <HistorySwiperDelegate>

@property(readonly) content::WebContents* webContents;
@property(readonly) NSView* nsView;
@property(readonly) PrefService* prefService;

@end

@implementation ChromeRenderWidgetHostViewMacDelegate {
  // The widget host (process + routing IDs) that this delegate is managing.
  int32_t _widgetProcessId;
  int32_t _widgetRoutingId;

  // Responsible for 2-finger swipes history navigation.
  HistorySwiper* __strong _historySwiper;

  // A boolean set to true while resigning first responder status, to avoid
  // infinite recursion in the case of reentrance.
  BOOL _resigningFirstResponder;
}

- (instancetype)initWithRenderWidgetHost:
    (content::RenderWidgetHost*)renderWidgetHost {
  self = [super init];
  if (self) {
    _widgetProcessId = renderWidgetHost->GetProcess()->GetDeprecatedID();
    _widgetRoutingId = renderWidgetHost->GetRoutingID();
    _historySwiper = [[HistorySwiper alloc] initWithDelegate:self];
    // Clip bounds so history swiper navigation layer doesn't overflow the
    // bounds when in Split View or with a Side Panel open.
    [self viewThatWantsHistoryOverlay].clipsToBounds = YES;
  }
  return self;
}

- (void)dealloc {
  [_historySwiper setDelegate:nil];
}

- (content::WebContents*)webContents {
  content::RenderWidgetHost* renderWidgetHost =
      content::RenderWidgetHost::FromID(_widgetProcessId, _widgetRoutingId);
  if (!renderWidgetHost) {
    return nullptr;
  }

  content::RenderViewHost* renderViewHost =
      content::RenderViewHost::From(renderWidgetHost);
  if (!renderViewHost) {
    return nullptr;
  }

  return content::WebContents::FromRenderViewHost(renderViewHost);
}

- (BOOL)shouldRefuseBecomingKeyView {
  content::WebContents* webContents = self.webContents;
  if (webContents && ShouldRefuseBecomingKeyViewForTopChromeWebUI(
                         webContents->GetLastCommittedURL())) {
    return YES;
  }

  return NO;
}

- (NSView*)nsView {
  content::RenderWidgetHost* renderWidgetHost =
      content::RenderWidgetHost::FromID(_widgetProcessId, _widgetRoutingId);
  if (!renderWidgetHost) {
    return nil;
  }

  content::RenderWidgetHostView* renderWidgetHostView =
      renderWidgetHost->GetView();
  if (!renderWidgetHostView) {
    return nil;
  }

  return renderWidgetHostView->GetNativeView().GetNativeNSView();
}

- (PrefService*)prefService {
  content::RenderWidgetHost* renderWidgetHost =
      content::RenderWidgetHost::FromID(_widgetProcessId, _widgetRoutingId);
  if (!renderWidgetHost) {
    return nullptr;
  }

  return Profile::FromBrowserContext(
             renderWidgetHost->GetProcess()->GetBrowserContext())
      ->GetPrefs();
}

// Handle an event. All incoming key and mouse events flow through this
// delegate method if implemented. Return YES if the event is fully handled, or
// NO if normal processing should take place.
- (BOOL)handleEvent:(NSEvent*)event {
  return [_historySwiper handleEvent:event];
}

// NSWindow events.

// This is a low level API which provides touches associated with an event.
// It is used in conjunction with gestures to determine finger placement
// on the trackpad.
- (void)touchesMovedWithEvent:(NSEvent*)event {
  [_historySwiper touchesMovedWithEvent:event];
}

- (void)touchesBeganWithEvent:(NSEvent*)event {
  [_historySwiper touchesBeganWithEvent:event];
}

- (void)touchesCancelledWithEvent:(NSEvent*)event {
  [_historySwiper touchesCancelledWithEvent:event];
}

- (void)touchesEndedWithEvent:(NSEvent*)event {
  [_historySwiper touchesEndedWithEvent:event];
}

// HistorySwiperDelegate methods

- (BOOL)shouldAllowHistorySwiping {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return NO;
  }
  auto* swiping_control =
      history_swiper::HistorySwipingControl::FromWebContents(webContents);
  return !swiping_control || swiping_control->ShouldAllowHistorySwiping();
}

- (NSView*)viewThatWantsHistoryOverlay {
  return self.nsView;
}

- (BOOL)canNavigateInDirection:(history_swiper::NavigationDirection)direction
                      onWindow:(NSWindow*)window {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return NO;
  }

  if (direction == history_swiper::kForwards) {
    return chrome::CanGoForward(webContents);
  } else {
    return chrome::CanGoBack(webContents);
  }
}

- (void)navigateInDirection:(history_swiper::NavigationDirection)direction
                   onWindow:(NSWindow*)window {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  if (direction == history_swiper::kForwards) {
    chrome::GoForward(webContents);
  } else {
    chrome::GoBack(webContents);
  }
}

- (void)backwardsSwipeNavigationLikely {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  webContents->BackNavigationLikely(
      content::preloading_predictor::kBackGestureNavigation,
      WindowOpenDisposition::CURRENT_TAB);
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
                      isValidItem:(BOOL*)valid {
  PrefService* pref = self.prefService;
  if (!pref) {
    return NO;
  }

  const PrefService::Preference* spellCheckEnablePreference =
      pref->FindPreference(spellcheck::prefs::kSpellCheckEnable);
  DCHECK(spellCheckEnablePreference);
  const bool spellCheckUserModifiable =
      spellCheckEnablePreference->IsUserModifiable();

  SEL action = item.action;
  // For now, this action is always enabled for render view;
  // this is sub-optimal.
  // TODO(suzhe): Plumb the "can*" methods up from WebCore.
  if (action == @selector(checkSpelling:)) {
    *valid = spellCheckUserModifiable;
    return YES;
  }

  // TODO(groby): Clarify who sends this and if toggleContinuousSpellChecking:
  // is still necessary.
  if (action == @selector(toggleContinuousSpellChecking:)) {
    if ([(id)item respondsToSelector:@selector(setState:)]) {
      NSControlStateValue checkedState =
          pref->GetBoolean(spellcheck::prefs::kSpellCheckEnable)
              ? NSControlStateValueOn
              : NSControlStateValueOff;
      [(id)item setState:checkedState];
    }
    *valid = spellCheckUserModifiable;
    return YES;
  }

  if (action == @selector(showGuessPanel:) ||
      action == @selector(toggleGrammarChecking:)) {
    *valid = spellCheckUserModifiable;
    return YES;
  }

  return NO;
}

- (void)rendererHandledGestureScrollEvent:(const blink::WebGestureEvent&)event
                                 consumed:(BOOL)consumed {
  [_historySwiper rendererHandledGestureScrollEvent:event consumed:consumed];
}

- (void)rendererHandledOverscrollEvent:(const ui::DidOverscrollParams&)params {
  [_historySwiper onOverscrolled:params];
}

// Spellchecking methods
// The next five methods are implemented here since this class is the first
// responder for anything in the browser.

// This message is sent whenever the user specifies that a word should be
// changed from the spellChecker.
- (void)changeSpelling:(id)sender {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  // Grab the currently selected word from the spell panel, as this is the word
  // that we want to replace the selected word in the text with.
  NSString* newWord = [[sender selectedCell] stringValue];
  if (newWord != nil) {
    webContents->ReplaceMisspelling(base::SysNSStringToUTF16(newWord));
  }
}

// This message is sent by NSSpellChecker whenever the next word should be
// advanced to, either after a correction or clicking the "Find Next" button.
// This isn't documented anywhere useful, like in NSSpellProtocol.h with the
// other spelling panel methods. This is probably because Apple assumes that the
// the spelling panel will be used with an NSText, which will automatically
// catch this and advance to the next word for you. Thanks Apple.
// This is also called from the Edit -> Spelling -> Check Spelling menu item.
- (void)checkSpelling:(id)sender {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  if (content::RenderFrameHost* frame = webContents->GetFocusedFrame()) {
    mojo::Remote<spellcheck::mojom::SpellCheckPanel>
        focused_spell_check_panel_client;
    frame->GetRemoteInterfaces()->GetInterface(
        focused_spell_check_panel_client.BindNewPipeAndPassReceiver());
    focused_spell_check_panel_client->AdvanceToNextMisspelling();
  }
}

// This message is sent by the spelling panel whenever a word is ignored.
- (void)ignoreSpelling:(id)sender {
  // Ideally, we would ask the current RenderView for its tag, but that would
  // mean making a blocking IPC call from the browser. Instead,
  // spellcheck_platform::CheckSpelling remembers the last tag and
  // spellcheck_platform::IgnoreWord assumes that is the correct tag.
  NSString* wordToIgnore = [sender stringValue];
  if (wordToIgnore != nil)
    spellcheck_platform::IgnoreWord(nullptr,
                                    base::SysNSStringToUTF16(wordToIgnore));
}

- (void)showGuessPanel:(id)sender {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  const bool visible = spellcheck_platform::SpellingPanelVisible();

  if (content::RenderFrameHost* frame = webContents->GetFocusedFrame()) {
    mojo::Remote<spellcheck::mojom::SpellCheckPanel>
        focused_spell_check_panel_client;
    frame->GetRemoteInterfaces()->GetInterface(
        focused_spell_check_panel_client.BindNewPipeAndPassReceiver());
    focused_spell_check_panel_client->ToggleSpellPanel(visible);
  }
}

- (void)toggleContinuousSpellChecking:(id)sender {
  PrefService* pref = self.prefService;
  if (!pref) {
    return;
  }
  pref->SetBoolean(spellcheck::prefs::kSpellCheckEnable,
                   !pref->GetBoolean(spellcheck::prefs::kSpellCheckEnable));
}

// END Spellchecking methods

// If a dialog is visible, make its window key. See becomeFirstResponder.
- (void)makeAnyDialogKey {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return;
  }

  web_modal::WebContentsModalDialogManager* manager =
      web_modal::WebContentsModalDialogManager::FromWebContents(webContents);
  if (!manager) {
    return;
  }

  if (manager->IsDialogActive()) {
    manager->FocusTopmostDialog();
  }
}

// If the RenderWidgetHostView becomes first responder while it has a dialog
// (say, if the user was interacting with the omnibox and then tabs back into
// the web contents), then make the dialog window key.
- (void)becomeFirstResponder {
  [self makeAnyDialogKey];
}

// If the RenderWidgetHostView is asked to resign first responder while a child
// window is key, then the user performed some action which targets the browser
// window, like clicking the omnibox or typing cmd+L. In that case, the browser
// window should become key.
- (void)resignFirstResponder {
  NSWindow* browserWindow = self.nsView.window;
  DCHECK(browserWindow);

  // If the browser window is already key, there's nothing to do.
  if (browserWindow.keyWindow) {
    return;
  }

  // Otherwise, look for it in the key window's chain of parents.
  NSWindow* keyWindowOrParent = NSApp.keyWindow;
  while (keyWindowOrParent && keyWindowOrParent != browserWindow) {
    keyWindowOrParent = keyWindowOrParent.parentWindow;
  }

  // If the browser window isn't among the parents, there's nothing to do.
  if (keyWindowOrParent != browserWindow) {
    return;
  }

  // Otherwise, temporarily set an ivar so that -windowDidBecomeKey, below,
  // doesn't immediately make the dialog key.
  base::AutoReset<BOOL> scoped(&_resigningFirstResponder, YES);

  // …then make the browser window key.
  [browserWindow makeKeyWindow];
}

// If the browser window becomes key while the RenderWidgetHostView is first
// responder, make the dialog key (if there is one).
- (void)windowDidBecomeKey {
  if (_resigningFirstResponder) {
    return;
  }
  NSView* view = self.nsView;
  if (view.window.firstResponder == view) {
    [self makeAnyDialogKey];
  }
}

- (AcceptMouseEvents)acceptsMouseEventsOption {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return AcceptMouseEvents::kWhenInActiveWindow;
  }

  // Allow mouse move events in inactive windows when inspecting.
  if (content::DevToolsAgentHost::IsDebuggerAttached(webContents)) {
    return AcceptMouseEvents::kWhenInActiveApp;
  }

  // If this web contents is in a tab, and the tab wants to accept mouse events
  // while the window is inactive.
  if (tabs::TabInterface* tab =
          tabs::TabInterface::MaybeGetFromContents(webContents)) {
    if (tabs::TabFeatures* features = tab->GetTabFeatures()) {
      if (tabs::InactiveWindowMouseEventController* inactive_event_controller =
              features->inactive_window_mouse_event_controller()) {
        if (inactive_event_controller
                ->ShouldAcceptMouseEventsWhileWindowInactive()) {
          return AcceptMouseEvents::kWhenInActiveApp;
        }
      }
    }
  }

  // For Top Chrome WebUIs, allows inactive windows to accept
  // mouse events as long as the application is active. This
  // mimics the behavior of views UI.
  if (IsTopChromeWebUIURL(webContents->GetVisibleURL()) ||
      IsTopChromeUntrustedWebUIURL(webContents->GetVisibleURL())) {
    return AcceptMouseEvents::kWhenInActiveApp;
  }

  // WebContents managed by glic should be allowed to accept mouse events while
  // inactive, aligning with the expected behavior of native chrome dialogs.
  // TODO(crbug.com/399119513): Consider making this a single WebContents
  // scoped setting, allowing this behavior to be configured by feature code.
  if (glic::GetGlicGuestWebContents(webContents) != nullptr) {
    return AcceptMouseEvents::kWhenInActiveApp;
  }

  // If the WebContents are from the ActorOverlayUI WebUIController, we should
  // accept mouse events when any part of the application is active.
  if (actor::ui::ActorOverlayUI::IsActorOverlayWebContents(webContents)) {
    return AcceptMouseEvents::kWhenInActiveApp;
  }

  return AcceptMouseEvents::kWhenInActiveWindow;
}

- (AcceptTooltipEvents)acceptsTooltipEvents {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return AcceptTooltipEvents::kWhenInKeyWindow;
  }

  // For Top Chrome WebUIs, allows non-key windows to accept
  // tooltip events.
  if (IsTopChromeWebUIURL(webContents->GetVisibleURL()) ||
      IsTopChromeUntrustedWebUIURL(webContents->GetVisibleURL())) {
    return AcceptTooltipEvents::kWhenInActiveApp;
  }

  return AcceptTooltipEvents::kWhenInKeyWindow;
}

- (BOOL)shouldBecomeFirstResponderOnRightClick {
  content::WebContents* webContents = self.webContents;
  if (!webContents) {
    return NO;
  }

  // For Top Chrome WebUIs, allow right click to make the view first responder.
  if (IsTopChromeWebUIURL(webContents->GetVisibleURL()) ||
      IsTopChromeUntrustedWebUIURL(webContents->GetVisibleURL())) {
    return YES;
  }

  return NO;
}

@end
