// Copyright 2022 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/views/bookmarks/saved_tab_groups/saved_tab_group_button.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>

#include "base/check.h"
#include "base/functional/bind.h"
#include "cc/paint/paint_flags.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/tab_group_menu_utils.h"
#include "chrome/browser/ui/tabs/tab_group_theme.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_button_util.h"
#include "chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_drag_data.h"
#include "chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_tabs_menu_model.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace tab_groups {

namespace {
// The max height of the button and the max width of a button with no title.
constexpr int kButtonSize = 20;
// The corner radius for the button.
constexpr float kButtonRadius = 6.0f;
// The amount of insets above and below the text.
constexpr float kVerticalInsets = 2.0f;
// The amount of insets before and after the text.
constexpr float kHorizontalInsets = 6.0f;
// The width of the outline of the button when open in the Tab Strip.
constexpr float kBorderThickness = 1.0f;
// The size of the squircle (rounded rect) in a button with no text.
constexpr float kEmptyChipSize = 12.0f;
// The amount of padding around the squircle (rounded rect).
constexpr float kEmptyChipInsets = 4.0f;
// The radius of the squircle (rounded rect).
constexpr float kEmptyChipCornerRadius = 2.0f;
// The amount of insets before and after the share icon when the title is empty.
constexpr float kSharedEmptyChipInsets = 2.0f;
}  // namespace

SavedTabGroupButton::SavedTabGroupButton(const SavedTabGroup& group,
                                         PressedCallback callback,
                                         BrowserWindowInterface* browser,
                                         bool animations_enabled)
    : MenuButton(std::move(callback), group.title()),
      browser_(browser),
      is_shared_(group.is_shared_tab_group()),
      tab_group_color_id_(group.color()),
      guid_(group.saved_guid()),
      local_group_id_(group.local_group_id()) {
  GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
  GetViewAccessibility().SetName(GetAccessibleNameForButton());
  GetViewAccessibility().SetRoleDescription(l10n_util::GetStringUTF16(
      IDS_ACCNAME_SAVED_TAB_GROUP_BUTTON_ROLE_DESCRIPTION));
  UpdateButtonData(group);
  SetID(VIEW_ID_BOOKMARK_BAR_ELEMENT);
  SetProperty(views::kElementIdentifierKey, kSavedTabGroupButtonElementId);
  SetMaxSize(gfx::Size(bookmark_button_util::kMaxButtonWidth, kButtonSize));
  label()->SetTextStyle(views::style::STYLE_BODY_4_EMPHASIS);

  show_animation_ = std::make_unique<gfx::SlideAnimation>(this);
  if (!animations_enabled) {
    // For some reason during testing the events generated by animating throw
    // off the test. So, don't animate while testing.
    show_animation_->Reset(1);
  } else {
    show_animation_->Show();
  }

  ConfigureInkDrop(this);
  SetImageLabelSpacing(
      ChromeLayoutProvider::Get()->GetDistanceMetric(
          ChromeDistanceMetric::DISTANCE_RELATED_LABEL_HORIZONTAL_LIST) /
      2);
  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(0),
                                                kButtonRadius);
  SetFocusBehavior(FocusBehavior::ALWAYS);

  set_drag_controller(this);
  set_context_menu_controller(this);

  UpdateCachedTooltipText();
}

SavedTabGroupButton::~SavedTabGroupButton() = default;

void SavedTabGroupButton::UpdateButtonData(const SavedTabGroup& group) {
  SetTextProperties(group);

  tab_group_color_id_ = group.color();
  local_group_id_ = group.local_group_id();
  guid_ = group.saved_guid();
  is_shared_ = group.is_shared_tab_group();

  UpdateButtonLayout();
  UpdateAccessibleName();
  UpdateCachedTooltipText();
}

bool SavedTabGroupButton::OnKeyPressed(const ui::KeyEvent& event) {
  if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) {
    ShowContextMenu(GetKeyboardContextMenuLocation(),
                    ui::mojom::MenuSourceType::kKeyboard);
    return true;
  } else if (event.key_code() == ui::KeyboardCode::VKEY_SPACE) {
    NotifyClick(event);
    return true;
  }

  return false;
}

gfx::Point SavedTabGroupButton::GetKeyboardContextMenuLocation() {
  // Use center bottom of the button as menu location so the context menu does
  // not overlap with text.
  gfx::Rect vis_bounds = GetVisibleBounds();
  gfx::Point screen_point(vis_bounds.x() + vis_bounds.width() / 2,
                          vis_bounds.y() + vis_bounds.height());
  ConvertPointToScreen(this, &screen_point);
  return screen_point;
}

bool SavedTabGroupButton::IsTriggerableEvent(const ui::Event& e) {
  return e.type() == ui::EventType::kGestureTap ||
         e.type() == ui::EventType::kGestureTapDown ||
         event_utils::IsPossibleDispositionEvent(e);
}

void SavedTabGroupButton::PaintButtonContents(gfx::Canvas* canvas) {
  if (!GetText().empty()) {
    return;
  } else if (is_shared_) {
    // Not title shared groups display a share icon in place of the squircle we
    // draw for un-shared groups. See SavedTabGroupButton::UpdateButtonLayout.
    return;
  }

  // When the title is empty, we draw a circle similar to the tab group
  // header when there is no title.
  const ui::ColorProvider* const cp = GetColorProvider();
  SkColor text_and_outline_color =
      cp->GetColor(GetSavedTabGroupOutlineColorId(tab_group_color_id_));

  // Draw squircle (rounded rect).
  cc::PaintFlags flags;
  flags.setAntiAlias(true);
  flags.setStyle(cc::PaintFlags::kFill_Style);
  flags.setColor(text_and_outline_color);

  canvas->DrawRoundRect(gfx::RectF(kEmptyChipInsets, kEmptyChipInsets,
                                   kEmptyChipSize, kEmptyChipSize),
                        kEmptyChipCornerRadius, flags);
}

std::u16string SavedTabGroupButton::GetAccessibleNameForButton() const {
  const std::u16string& opened_state =
      local_group_id_.has_value()
          ? l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_OPENED)
          : l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_CLOSED);

  const std::u16string& shared_state =
      is_shared_ ? l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_SHARED)
                 : u"";

  const std::u16string saved_group_acessible_name =
      GetText().empty()
          ? l10n_util::GetStringFUTF16(
                IDS_GROUP_AX_LABEL_UNNAMED_SAVED_GROUP_FORMAT, shared_state,
                opened_state)
          : l10n_util::GetStringFUTF16(
                IDS_GROUP_AX_LABEL_NAMED_SAVED_GROUP_FORMAT, shared_state,
                std::u16string(GetText()), opened_state);
  return saved_group_acessible_name;
}

void SavedTabGroupButton::UpdateCachedTooltipText() {
  SetTooltipText(GetAccessibleNameForButton());
}

void SavedTabGroupButton::UpdateAccessibleName() {
  GetViewAccessibility().SetName(GetAccessibleNameForButton());
}

void SavedTabGroupButton::SetText(std::u16string_view text) {
  LabelButton::SetText(text);
  UpdateAccessibleName();
  UpdateCachedTooltipText();
}

void SavedTabGroupButton::SetTextProperties(const SavedTabGroup& group) {
  GetViewAccessibility().SetName(GetAccessibleNameForButton());
  SetTooltipText(group.title());
  SetText(group.title());
}

void SavedTabGroupButton::UpdateButtonLayout() {
  SetEnabledTextColors(GetSavedTabGroupForegroundColorId(tab_group_color_id_));
  SetBackground(views::CreateRoundedRectBackground(
      GetTabGroupBookmarkColorId(tab_group_color_id_), kButtonRadius));

  // Adjust the insets so the share icon can fit within the bounds of this
  // button if the group has no title.
  bool use_shared_empty_chip_insets = is_shared_ && GetText().empty();
  int horizontal_insets =
      use_shared_empty_chip_insets ? kSharedEmptyChipInsets : kHorizontalInsets;
  const gfx::Insets& insets =
      gfx::Insets::VH(kVerticalInsets, horizontal_insets);

  // Only draw a border if the group is open in the tab strip.
  if (!local_group_id_.has_value()) {
    SetBorder(views::CreateEmptyBorder(insets));
  } else {
    std::unique_ptr<views::Border> border = views::CreateRoundedRectBorder(
        kBorderThickness, kButtonRadius,
        GetSavedTabGroupOutlineColorId(tab_group_color_id_));
    SetBorder(views::CreatePaddedBorder(std::move(border), insets));
  }

  if (is_shared_) {
    SetImageModel(ButtonState::STATE_NORMAL,
                  ui::ImageModel::FromVectorIcon(
                      features::IsRoundedIconsEnabled() ? kGroupCustomIcon
                                                        : kPeopleGroupOldIcon,
                      GetSavedTabGroupForegroundColorId(tab_group_color_id_),
                      gfx::kFaviconSize));
  }

  if (GetText().empty()) {
    // When the text is empty force the button to have square dimensions.
    SetPreferredSize(gfx::Size(kButtonSize, kButtonSize));
  } else {
    SetPreferredSize(CalculatePreferredSize({}));
  }
}

std::unique_ptr<views::LabelButtonBorder>
SavedTabGroupButton::CreateDefaultBorder() const {
  auto border = std::make_unique<views::LabelButtonBorder>();
  return border;
}

void SavedTabGroupButton::OnThemeChanged() {
  views::MenuButton::OnThemeChanged();
  UpdateButtonLayout();
}

void SavedTabGroupButton::WriteDragDataForView(View* sender,
                                               const gfx::Point& press_pt,
                                               ui::OSExchangeData* data) {
  SavedTabGroupButton* const button =
      views::AsViewClass<SavedTabGroupButton>(sender);
  CHECK(button);
  CHECK(button == this);

  // Write the image and MIME type to the OSExchangeData.
  SavedTabGroupDragData::WriteToOSExchangeData(this, press_pt,
                                               GetThemeProvider(), data);
}

int SavedTabGroupButton::GetDragOperationsForView(View* sender,
                                                  const gfx::Point& p) {
  // This may need to become more complicated
  return ui::DragDropTypes::DRAG_MOVE;
}

bool SavedTabGroupButton::CanStartDragForView(View* sender,
                                              const gfx::Point& press_pt,
                                              const gfx::Point& p) {
  // Check if we have not moved enough horizontally but we have moved downward
  // vertically - downward drag.
  gfx::Vector2d move_offset = p - press_pt;
  return View::ExceededDragThreshold(move_offset);
}

int SavedTabGroupButton::GetAndIncrementLatestCommandId() {
  return latest_command_id_ += 1;
}

void SavedTabGroupButton::ShowContextMenuForViewImpl(
    View* source,
    const gfx::Point& point,
    ui::mojom::MenuSourceType source_type) {
  TabGroupSyncService* tab_group_service =
      tab_groups::TabGroupSyncServiceFactory::GetForProfile(
          browser_->GetProfile());

  const std::optional<SavedTabGroup> saved_group =
      tab_group_service->GetGroup(guid_);
  // If the group has been deleted remotely.
  if (!saved_group.has_value()) {
    return;
  }

  menu_model_ = std::make_unique<STGTabsMenuModel>(
      browser_, TabGroupMenuContext::SAVED_TAB_GROUP_BUTTON_CONTEXT_MENU);
  menu_model_->Build(
      saved_group.value(),
      base::BindRepeating(&SavedTabGroupButton::GetAndIncrementLatestCommandId,
                          base::Unretained(this)));

  context_menu_highlight_ = AddAnchorHighlight();
  context_menu_runner_ = std::make_unique<views::MenuRunner>(
      menu_model_.get(),
      views::MenuRunner::CONTEXT_MENU | views::MenuRunner::IS_NESTED,
      base::BindRepeating(&SavedTabGroupButton::OnContextMenuClosed,
                          base::Unretained(this)));
  context_menu_runner_->RunMenuAt(
      source->GetWidget(), button_controller(), gfx::Rect(point, gfx::Size()),
      views::MenuAnchorPosition::kTopLeft, source_type);
}

void SavedTabGroupButton::OnContextMenuClosed() {
  context_menu_highlight_.reset();
}

BEGIN_METADATA(SavedTabGroupButton)
END_METADATA

}  // namespace tab_groups
