// 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.

#include "chrome/browser/ui/cocoa/renderer_context_menu/render_view_context_menu_mac.h"

#include <utility>

#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/render_frame_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 "third_party/blink/public/common/context_menu_data/context_menu_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"

namespace {

std::u16string GetUserAddedWord(content::BrowserContext* browser_context,
                                const std::u16string& selection_text) {
  std::u16string trimmed_text = std::u16string(
      base::TrimWhitespace(selection_text, base::TrimPositions::TRIM_ALL));
  if (trimmed_text.empty()) {
    return std::u16string();
  }

  SpellcheckService* spellcheck_service =
      SpellcheckServiceFactory::GetForContext(browser_context);
  if (!spellcheck_service) {
    return std::u16string();
  }

  bool is_user_added_word =
      spellcheck_platform::IsUserAddedWord(
          spellcheck_service->platform_spell_checker(), trimmed_text) ||
      spellcheck_service->GetCustomDictionary()->HasWord(
          base::UTF16ToUTF8(trimmed_text));
  if (is_user_added_word) {
    return trimmed_text;
  }

  return std::u16string();
}

}  // namespace

// macOS implementation of the ToolkitDelegate.
// This simply (re)delegates calls to RVContextMenuMac.
class ToolkitDelegateMacCocoa : public RenderViewContextMenu::ToolkitDelegate {
 public:
  explicit ToolkitDelegateMacCocoa(RenderViewContextMenuMac* context_menu)
      : context_menu_(context_menu) {}

  ToolkitDelegateMacCocoa(const ToolkitDelegateMacCocoa&) = delete;
  ToolkitDelegateMacCocoa& operator=(const ToolkitDelegateMacCocoa&) = delete;

  ~ToolkitDelegateMacCocoa() override {}

 private:
  // ToolkitDelegate:
  void Init(ui::SimpleMenuModel* menu_model) override {
    context_menu_->InitToolkitMenu();
  }

  void Cancel() override { context_menu_->CancelToolkitMenu(); }

  void UpdateMenuItem(int command_id,
                      bool enabled,
                      bool hidden,
                      const std::u16string& title) override {
    context_menu_->UpdateToolkitMenuItem(command_id, enabled, hidden, title);
  }

  raw_ptr<RenderViewContextMenuMac> context_menu_;
};

RenderViewContextMenuMac::RenderViewContextMenuMac(
    content::RenderFrameHost& render_frame_host,
    const content::ContextMenuParams& params,
    bool is_paste_enabled,
    bool is_paste_and_match_style_enabled)
    : RenderViewContextMenu(render_frame_host,
                            params,
                            is_paste_enabled,
                            is_paste_and_match_style_enabled),
      text_services_context_menu_(this) {
  user_added_word_ = GetUserAddedWord(browser_context_, params_.selection_text);
  auto delegate = std::make_unique<ToolkitDelegateMacCocoa>(this);
  set_toolkit_delegate(std::move(delegate));
}

RenderViewContextMenuMac::~RenderViewContextMenuMac() = default;

void RenderViewContextMenuMac::ExecuteCommand(int command_id, int event_flags) {
  switch (command_id) {
    case IDC_CONTENT_CONTEXT_LOOK_UP:
      LookUpInDictionary();
      break;
    case IDC_SPELLCHECK_REMOVE_FROM_DICTIONARY:
      RemoveFromDictionary();
      break;
    default:
      RenderViewContextMenu::ExecuteCommand(command_id, event_flags);
  }
}

bool RenderViewContextMenuMac::IsCommandIdChecked(int command_id) const {
  if (text_services_context_menu_.SupportsCommand(command_id)) {
    return text_services_context_menu_.IsCommandIdChecked(command_id);
  }

  if (command_id == IDC_CONTENT_CONTEXT_LOOK_UP ||
      command_id == IDC_SPELLCHECK_REMOVE_FROM_DICTIONARY) {
    return false;
  }

  return RenderViewContextMenu::IsCommandIdChecked(command_id);
}

bool RenderViewContextMenuMac::IsCommandIdEnabled(int command_id) const {
  if (text_services_context_menu_.SupportsCommand(command_id)) {
    return text_services_context_menu_.IsCommandIdEnabled(command_id);
  }

  switch (command_id) {
    case IDC_CONTENT_CONTEXT_LOOK_UP:
      return true;
    case IDC_SPELLCHECK_REMOVE_FROM_DICTIONARY:
      return !user_added_word_.empty();
    default:
      return RenderViewContextMenu::IsCommandIdEnabled(command_id);
  }
}

std::u16string_view RenderViewContextMenuMac::GetSelectedText() const {
  return params_.selection_text;
}

bool RenderViewContextMenuMac::IsTextDirectionEnabled(
    base::i18n::TextDirection direction) const {
  return ParamsForTextDirection(direction) &
         blink::ContextMenuData::kCheckableMenuItemEnabled;
}

bool RenderViewContextMenuMac::IsTextDirectionChecked(
    base::i18n::TextDirection direction) const {
  return ParamsForTextDirection(direction) &
         blink::ContextMenuData::kCheckableMenuItemChecked;
}

void RenderViewContextMenuMac::UpdateTextDirection(
    base::i18n::TextDirection direction) {
  DCHECK_NE(direction, base::i18n::UNKNOWN_DIRECTION);

  int command_id = IDC_WRITING_DIRECTION_LTR;
  if (direction == base::i18n::RIGHT_TO_LEFT) {
    command_id = IDC_WRITING_DIRECTION_RTL;
  }

  // Note: we get the local render frame host so that the writing mode settings
  // changes apply to the correct frame. See crbug.com/40149229 for a
  // description of what happens if we use the outermost frame.
  content::RenderFrameHost* rfh = GetRenderFrameHost();
  // It's possible that the frame drops out from under us while the context
  // menu is open. In this case, we'll not perform the action, but still record
  // metrics.
  if (rfh) {
    rfh->GetRenderWidgetHost()->UpdateTextDirection(direction);
    rfh->GetRenderWidgetHost()->NotifyTextDirection();
  }

  RenderViewContextMenu::RecordUsedItem(command_id);
}

void RenderViewContextMenuMac::AppendPlatformEditableItems() {
  text_services_context_menu_.AppendEditableItems(&menu_model_);
}

void RenderViewContextMenuMac::InitToolkitMenu() {
  if (params_.form_control_type ==
      blink::mojom::FormControlType::kInputPassword) {
    return;
  }

  if (!params_.selection_text.empty() && params_.link_url.is_empty()) {
    // In case the user has selected a word that triggers spelling suggestions,
    // show the dictionary lookup under the group that contains the command to
    // “Add to Dictionary” or “Remove from Dictionary”.
    const std::optional<size_t> index_opt =
        menu_model_.GetIndexOfCommandId(IDC_SPELLCHECK_ADD_TO_DICTIONARY);
    size_t index = index_opt.value_or(0);
    if (index_opt.has_value()) {
      while (menu_model_.GetTypeAt(index) != ui::MenuModel::TYPE_SEPARATOR) {
        index++;
      }
      ++index;  // Place it below the separator.
    }

    if (!user_added_word_.empty() && params_.misspelled_word.empty()) {
      menu_model_.InsertItemAt(index++, IDC_SPELLCHECK_REMOVE_FROM_DICTIONARY,
                               l10n_util::GetStringUTF16(
                                   IDS_CONTENT_CONTEXT_REMOVE_FROM_DICTIONARY));
    }

    std::u16string printable_selection_text = PrintableSelectionText();
    EscapeAmpersands(&printable_selection_text);
    menu_model_.InsertItemAt(
        index++, IDC_CONTENT_CONTEXT_LOOK_UP,
        l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP,
                                   printable_selection_text));
    menu_model_.InsertSeparatorAt(index++, ui::NORMAL_SEPARATOR);
  }

  if (!params_.selection_text.empty()) {
    text_services_context_menu_.AppendToContextMenu(&menu_model_);
  }
}

void RenderViewContextMenuMac::LookUpInDictionary() {
  content::RenderWidgetHostView* view =
      GetRenderFrameHost()->GetRenderWidgetHost()->GetView();
  if (view) {
    view->ShowDefinitionForSelection();
  }
}

void RenderViewContextMenuMac::RemoveFromDictionary() {
  SpellcheckService* spellcheck =
      SpellcheckServiceFactory::GetForContext(browser_context_);
  if (!spellcheck) {
    return;
  }

  spellcheck->GetCustomDictionary()->RemoveWord(
      base::UTF16ToUTF8(user_added_word_));
  spellcheck_platform::RemoveWord(spellcheck->platform_spell_checker(),
                                  user_added_word_);
}

int RenderViewContextMenuMac::ParamsForTextDirection(
    base::i18n::TextDirection direction) const {
  switch (direction) {
    case base::i18n::UNKNOWN_DIRECTION:
      return params_.writing_direction_default;
    case base::i18n::RIGHT_TO_LEFT:
      return params_.writing_direction_right_to_left;
    case base::i18n::LEFT_TO_RIGHT:
      return params_.writing_direction_left_to_right;
  }
}
