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

#include "ui/views/controls/views_text_services_context_menu.h"

#import <Cocoa/Cocoa.h>

#include "ui/base/ime/text_input_flags.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/decorated_text.h"
#import "ui/gfx/decorated_text_mac.h"
#include "ui/gfx/mac/menu_text_elider_mac.h"
#include "ui/menus/cocoa/text_services_context_menu.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/views_text_services_context_menu_base.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

// This class serves as a bridge to TextServicesContextMenu to add and handle
// text service items in the context menu. The items include Speech, Look Up
// and BiDi.
class ViewsTextServicesContextMenuMac
    : public ViewsTextServicesContextMenuBase,
      public ui::TextServicesContextMenu::Delegate {
 public:
  ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client);
  ViewsTextServicesContextMenuMac(const ViewsTextServicesContextMenuMac&) =
      delete;
  ViewsTextServicesContextMenuMac& operator=(
      const ViewsTextServicesContextMenuMac&) = delete;
  ~ViewsTextServicesContextMenuMac() override = default;

  // ViewsTextServicesContextMenu:
  bool IsCommandIdChecked(int command_id) const override;
  bool IsCommandIdEnabled(int command_id) const override;
  void ExecuteCommand(int command_id, int event_flags) override;
  bool SupportsCommand(int command_id) const override;

  // TextServicesContextMenu::Delegate:
  std::u16string_view GetSelectedText() const override;
  bool IsTextDirectionEnabled(
      base::i18n::TextDirection direction) const override;
  bool IsTextDirectionChecked(
      base::i18n::TextDirection direction) const override;
  void UpdateTextDirection(base::i18n::TextDirection direction) override;

 private:
  // Handler for the "Look Up" menu item.
  void LookUpInDictionary();

  ui::TextServicesContextMenu text_services_menu_{this};
};

ViewsTextServicesContextMenuMac::ViewsTextServicesContextMenuMac(
    ui::SimpleMenuModel* menu,
    Textfield* client)
    : ViewsTextServicesContextMenuBase(menu, client) {
  // Insert the "Look up" item in the first position.
  const std::u16string_view text = GetSelectedText();
  if (!text.empty() && client->SupportsLookUp()) {
    menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR);
    // Truncate the selected text to prevent overly long menu item titles.
    const std::u16string truncated_text =
        gfx::ElideMenuItemTitle(std::u16string(text));
    menu->InsertItemAt(0, IDS_CONTENT_CONTEXT_LOOK_UP,
                       l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP,
                                                  truncated_text));

    text_services_menu_.AppendToContextMenu(menu);
  }

  if (client->SupportsEditableContextMenuItems()) {
    text_services_menu_.AppendEditableItems(menu);
  }
}

bool ViewsTextServicesContextMenuMac::IsCommandIdChecked(int command_id) const {
  return text_services_menu_.SupportsCommand(command_id)
             ? text_services_menu_.IsCommandIdChecked(command_id)
             : ViewsTextServicesContextMenuBase::IsCommandIdChecked(command_id);
}

bool ViewsTextServicesContextMenuMac::IsCommandIdEnabled(int command_id) const {
  if (text_services_menu_.SupportsCommand(command_id)) {
    return text_services_menu_.IsCommandIdEnabled(command_id);
  }
  return (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) ||
         ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id);
}

void ViewsTextServicesContextMenuMac::ExecuteCommand(int command_id,
                                                     int event_flags) {
  if (text_services_menu_.SupportsCommand(command_id)) {
    text_services_menu_.ExecuteCommand(command_id, event_flags);
  } else if (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) {
    LookUpInDictionary();
  } else {
    ViewsTextServicesContextMenuBase::ExecuteCommand(command_id, event_flags);
  }
}

bool ViewsTextServicesContextMenuMac::SupportsCommand(int command_id) const {
  return text_services_menu_.SupportsCommand(command_id) ||
         command_id == IDS_CONTENT_CONTEXT_LOOK_UP ||
         ViewsTextServicesContextMenuBase::SupportsCommand(command_id);
}

std::u16string_view ViewsTextServicesContextMenuMac::GetSelectedText() const {
  // Do not allow sensitive data (e.g. password fields) to escape via external
  // services.
  if (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD ||
      client()->GetTextInputFlags() & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD ||
      client()->GetTextInputFlags() &
          ui::TEXT_INPUT_FLAG_HAS_BEEN_CUSTOM_PASSWORD) {
    return {};
  }

  return client()->GetSelectedText();
}

bool ViewsTextServicesContextMenuMac::IsTextDirectionEnabled(
    base::i18n::TextDirection direction) const {
  if (client()->force_text_directionality()) {
    return false;
  }
  return direction != base::i18n::UNKNOWN_DIRECTION;
}

bool ViewsTextServicesContextMenuMac::IsTextDirectionChecked(
    base::i18n::TextDirection direction) const {
  if (client()->force_text_directionality()) {
    return direction == base::i18n::UNKNOWN_DIRECTION;
  }
  return IsTextDirectionEnabled(direction) &&
         client()->GetTextDirection() == direction;
}

void ViewsTextServicesContextMenuMac::UpdateTextDirection(
    base::i18n::TextDirection direction) {
  DCHECK(IsTextDirectionEnabled(direction));
  client()->ChangeTextDirectionAndLayoutAlignment(direction);
}

void ViewsTextServicesContextMenuMac::LookUpInDictionary() {
  gfx::DecoratedText text;
  gfx::Rect rect;
  if (client()->GetWordLookupDataFromSelection(&text, &rect)) {
    Widget* widget = client()->GetWidget();

    // We only care about the baseline of the glyph, not the space it occupies.
    gfx::Point baseline_point = rect.origin();
    views::View::ConvertPointToTarget(client(), widget->GetRootView(),
                                      &baseline_point);
    NSView* view = widget->GetNativeView().GetNativeNSView();
    NSPoint lookup_point = NSMakePoint(
        baseline_point.x(), NSHeight([view frame]) - baseline_point.y());
    [view showDefinitionForAttributedString:
              gfx::GetAttributedStringFromDecoratedText(text)
                                    atPoint:lookup_point];
  }
}

}  // namespace

// static
std::unique_ptr<ViewsTextServicesContextMenu>
ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu,
                                     Textfield* client) {
  return std::make_unique<ViewsTextServicesContextMenuMac>(menu, client);
}

}  // namespace views
