// Copyright 2024 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/toolbar/pinned_action_toolbar_button.h"

#include <string>
#include <type_traits>

#include "base/auto_reset.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_actions.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/customize_chrome/side_panel_controller.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/side_panel/side_panel_action_callback.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_ids.h"
#include "chrome/browser/ui/views/event_utils.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/pinned_action_toolbar_button_menu_model.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container_layout.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_button_status_indicator.h"
#include "chrome/browser/ui/views/toolbar/toolbar_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/grit/generated_resources.h"
#include "ui/actions/action_id.h"
#include "ui/actions/actions.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace {
// Width of the status indicator shown across the button.
constexpr int kStatusIndicatorWidth = 14;
// Height of the status indicator shown across the button.
constexpr int kStatusIndicatorHeight = 2;
// Spacing between the button's icon and the status indicator.
constexpr int kStatusIndicatorSpacing = 1;
}  // namespace

DEFINE_UI_CLASS_PROPERTY_TYPE(PinnedToolbarActionFlexPriority)
DEFINE_UI_CLASS_PROPERTY_KEY(
    std::underlying_type_t<PinnedToolbarActionFlexPriority>,
    kToolbarButtonFlexPriorityKey,
    std::underlying_type_t<PinnedToolbarActionFlexPriority>(
        PinnedToolbarActionFlexPriority::kLow))

DEFINE_UI_CLASS_PROPERTY_TYPE(CreateCustomPinnedActionToolbarButtonCallback*)
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(
    CreateCustomPinnedActionToolbarButtonCallback,
    kCustomPinnedActionToolbarButtonFactoryKey)

PinnedActionToolbarButton::PinnedActionToolbarButton(
    BrowserWindowInterface* browser,
    actions::ActionId action_id,
    base::WeakPtr<PinnedToolbarActionsContainer> container)
    : ToolbarButton(
          views::Button::PressedCallback(),
          std::make_unique<PinnedActionToolbarButtonMenuModel>(browser,
                                                               action_id),
          nullptr,
          false),
      browser_(browser),
      action_id_(action_id),
      container_(container) {
  if (auto element_id =
          pinned_toolbar_actions::GetElementIdentifierForAction(action_id)) {
    SetProperty(views::kElementIdentifierKey, element_id);
  }
  ConfigureInkDrop(this);
  SetHorizontalAlignment(gfx::ALIGN_CENTER);
  // Pinned action toolbar buttons have right margin and no left margin.
  SetProperty(
      views::kMarginsKey,
      gfx::Insets::TLBR(
          0, 0, 0,
          GetLayoutConstant(LayoutConstant::kToolbarIconDefaultMargin)));
  set_drag_controller(container_.get());
  GetViewAccessibility().SetDescription(
      std::u16string(), ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty);

  // Normally, the notify action is determined by whether a view is draggable
  // (and is set to press for non-draggable and release for draggable views).
  // However, PinnedActionToolbarButton may be draggable or non-draggable
  // depending on whether they are shown in an incognito window or unpinned and
  // popped-out. We want to preserve the same trigger event to keep the UX
  // (more) consistent. Set all PinnedActionToolbarButton to trigger on mouse
  // release.
  button_controller()->set_notify_action(
      views::ButtonController::NotifyAction::kOnRelease);

  // Do not flip the icon for RTL languages.
  SetFlipCanvasOnPaintForRTLUI(false);
  action_count_changed_subscription_ = AddAnchorCountChangedCallback(
      base::BindRepeating(&PinnedActionToolbarButton::OnAnchorCountChanged,
                          base::Unretained(this)));

  status_indicator_ =
      PinnedToolbarButtonStatusIndicator::Install(image_container_view());
  status_indicator_->SetColorId(kColorToolbarActionItemEngaged,
                                kColorToolbarButtonIconInactive);

  // TODO(shibalik): Revisit since all pinned actions should not be toggle
  // buttons.
  GetViewAccessibility().SetRole(ax::mojom::Role::kToggleButton);
  GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kFalse);

  if (web_app::AppBrowserController::IsWebApp(browser_)) {
    SetLayoutInsets(gfx::Insets());
    SetHorizontalAlignment(gfx::ALIGN_CENTER);
    SetAppearDisabledInInactiveWidget(true);
  }
}

PinnedActionToolbarButton::~PinnedActionToolbarButton() {
  action_count_changed_subscription_ = {};
}

bool PinnedActionToolbarButton::IsActive() {
  return anchor_higlight_.has_value();
}

base::AutoReset<bool> PinnedActionToolbarButton::SetNeedsDelayedDestruction(
    bool needs_delayed_destruction) {
  return base::AutoReset<bool>(&needs_delayed_destruction_,
                               needs_delayed_destruction);
}

void PinnedActionToolbarButton::SetIconVisibility(bool is_visible) {
  is_icon_visible_ = is_visible;
  NotifyViewControllerCallback();
}

void PinnedActionToolbarButton::AddHighlight() {
  anchor_higlight_ = AddAnchorHighlight();
  GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kTrue);
}

void PinnedActionToolbarButton::ResetHighlight() {
  anchor_higlight_.reset();
  GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kFalse);
}

void PinnedActionToolbarButton::SetPinned(bool pinned) {
  if (pinned_ == pinned) {
    return;
  }
  pinned_ = pinned;
  NotifyViewControllerCallback();
}

bool PinnedActionToolbarButton::OnKeyPressed(const ui::KeyEvent& event) {
  std::optional<event_utils::ReorderDirection> reorder_direction =
      event_utils::GetReorderCommandForKeyboardEvent(event);
  if (reorder_direction && pinned_ &&
      browser_->GetProfile()->IsRegularProfile()) {
    int move_by = 0;
    switch (*reorder_direction) {
      case event_utils::ReorderDirection::kPrevious:
        move_by = -1;
        break;
      case event_utils::ReorderDirection::kNext:
        move_by = 1;
        break;
    }

    container_->MovePinnedActionBy(action_id_, move_by);
    return true;
  }

  return ToolbarButton::OnKeyPressed(event);
}

gfx::Size PinnedActionToolbarButton::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // This makes sure the buttons are at least the toolbar button sized width.
  // The preferred size might be smaller when the button's icon is removed
  // during drag/drop.
  if (!container_) {
    // Want to avoid this ever getting called during teardown.
    return gfx::Size();
  }
  const gfx::Size toolbar_button_size = container_->GetDefaultButtonSize();
  const gfx::Size preferred_size =
      ToolbarButton::CalculatePreferredSize(available_size);
  return std::max(preferred_size, toolbar_button_size,
                  [](const gfx::Size s1, const gfx::Size s2) {
                    return s1.width() < s2.width();
                  });
}

void PinnedActionToolbarButton::Layout(PassKey) {
  LayoutSuperclass<ToolbarButton>(this);
  gfx::Rect status_rect(kStatusIndicatorWidth, kStatusIndicatorHeight);
  const gfx::Rect image_container_bounds =
      image_container_view()->GetLocalBounds();
  const int new_x = image_container_bounds.x() +
                    (image_container_bounds.width() - status_rect.width()) / 2;
  const int new_y =
      image_container_bounds.bottom() +
      (features::IsRoundedIconsEnabled() ? 0 : kStatusIndicatorSpacing);
  // Set the new origin for status_rect
  status_rect.set_origin(gfx::Point(new_x, new_y));
  status_indicator_->SetBoundsRect(status_rect);
}

bool PinnedActionToolbarButton::OnMousePressed(const ui::MouseEvent& event) {
  skip_execution_ = is_action_showing_bubble_;
  return ToolbarButton::OnMousePressed(event);
}

void PinnedActionToolbarButton::OnMouseReleased(const ui::MouseEvent& event) {
  if (!skip_execution_) {
    ToolbarButton::OnMouseReleased(event);
  } else {
    OnClickCanceled(event);
  }
  skip_execution_ = false;
}

void PinnedActionToolbarButton::UpdateIcon() {
  const std::optional<VectorIcons>& icons = GetVectorIcons();
  // If the button is a cached permanent button the color provider will not be
  // available.
  if (!icons.has_value() || !GetColorProvider()) {
    return;
  }

  const gfx::VectorIcon& icon = ui::TouchUiController::Get()->touch_ui()
                                    ? icons->touch_icon
                                    : icons->icon;

  if (is_icon_visible_ && action_engaged_) {
    UpdateIconsWithColors(
        icon, GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
        GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
        GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
        GetForegroundColor(ButtonState::STATE_DISABLED));
  } else {
    UpdateIconsWithColors(icon, GetForegroundColor(ButtonState::STATE_NORMAL),
                          GetForegroundColor(ButtonState::STATE_HOVERED),
                          GetForegroundColor(ButtonState::STATE_PRESSED),
                          GetForegroundColor(ButtonState::STATE_DISABLED));
  }
}

bool PinnedActionToolbarButton::ShouldShowEphemerallyInToolbar() {
  return should_show_in_toolbar_ || has_anchor_;
}

void PinnedActionToolbarButton::SetActionEngaged(bool action_engaged) {
  action_engaged_ = action_engaged;
  UpdateFlexPriority();
}

void PinnedActionToolbarButton::HideStatusIndicator() {
  status_indicator_->Hide();
}

void PinnedActionToolbarButton::UpdateStatusIndicator() {
  if (action_engaged_ && is_icon_visible_) {
    status_indicator_->Show();
  } else {
    status_indicator_->Hide();
  }
}

void PinnedActionToolbarButton::OnAnchorCountChanged(size_t anchor_count) {
  // If there is something anchored to the button we want to make sure the
  // button will be visible in the toolbar in cases where the window might be
  // small enough that icons must overflow. Update the
  // kToolbarButtonFlexPriorityKey to make sure icons are forced visible or able
  // to overflow.
  has_anchor_ = anchor_count > 0;
  UpdateFlexPriority();
  if (!has_anchor_) {
    container_->MaybeRemovePoppedOutButtonFor(GetActionId());
  }
}

void PinnedActionToolbarButton::UpdateFlexPriority() {
  PinnedToolbarActionFlexPriority priority =
      PinnedToolbarActionFlexPriority::kLow;
  if (has_anchor_) {
    priority = PinnedToolbarActionFlexPriority::kHigh;
  } else if (action_engaged_) {
    priority = PinnedToolbarActionFlexPriority::kMedium;
  }

  if (static_cast<PinnedToolbarActionFlexPriority>(
          GetProperty(kToolbarButtonFlexPriorityKey)) != priority) {
    SetProperty(
        kToolbarButtonFlexPriorityKey,
        static_cast<std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
            priority));
    InvalidateLayout();
  }
}

std::unique_ptr<views::ActionViewInterface>
PinnedActionToolbarButton::GetActionViewInterface() {
  return std::make_unique<PinnedActionToolbarButtonActionViewInterface>(this);
}

PinnedActionToolbarButtonActionViewInterface::
    PinnedActionToolbarButtonActionViewInterface(
        PinnedActionToolbarButton* action_view)
    : ToolbarButtonActionViewInterface(action_view),
      action_view_(action_view) {}

void PinnedActionToolbarButtonActionViewInterface::ActionItemChangedImpl(
    actions::ActionItem* action_item) {
  ButtonActionViewInterface::ActionItemChangedImpl(action_item);

  if (action_view_->IsIconVisible() &&
      actions::IsActionClass<actions::StatefulImageActionItem>(action_item)) {
    auto* stateful_action_item =
        static_cast<actions::StatefulImageActionItem*>(action_item);
    if (stateful_action_item->GetStatefulImage().IsVectorIcon()) {
      action_view_->SetVectorIcon(*stateful_action_item->GetStatefulImage()
                                       .GetVectorIcon()
                                       .vector_icon());
    }
  }

  // Update whether the action is engaged before updating the view.
  action_view_->SetActionEngaged(
      action_item->GetProperty(kActionItemUnderlineIndicatorKey));

  OnViewChangedImpl(action_item);

  action_view_->SetIsActionShowingBubble(action_item->GetIsShowingBubble());
}

void PinnedActionToolbarButtonActionViewInterface::InvokeActionImpl(
    actions::ActionItem* action_item) {
  base::RecordAction(
      base::UserMetricsAction("Actions.PinnedToolbarButtonActivation"));
  std::optional<actions::ActionId> action_id = action_item->GetActionId();
  CHECK(action_id.has_value());
  const std::optional<std::string> metrics_name =
      actions::ActionIdMap::ActionIdToString(action_id.value());
  // ActionIdToStringMappings are not initialized in unit tests, therefore will
  // not have a value. In the normal case, `metrics_name` should always have a
  // value.
  if (metrics_name.has_value()) {
    base::RecordComputedAction(base::StrCat(
        {"Actions.PinnedToolbarButtonActivation.", metrics_name.value()}));
  }

  base::AutoReset<bool> needs_delayed_destruction =
      action_view_->SetNeedsDelayedDestruction(true);
  action_item->InvokeAction(
      actions::ActionInvocationContext::Builder()
          .SetProperty(kSidePanelOpenTriggerKey,
                       SidePanelOpenTrigger::kPinnedEntryToolbarButton)
          .Build());
}

void PinnedActionToolbarButtonActionViewInterface::OnViewChangedImpl(
    actions::ActionItem* action_item) {
  bool is_pinnable = true;
  switch (static_cast<actions::ActionPinnableState>(
      action_item->GetProperty(actions::kActionItemPinnableKey))) {
    case actions::ActionPinnableState::kNotPinnable:
      is_pinnable = false;
      break;
    case actions::ActionPinnableState::kPinnable:
    case actions::ActionPinnableState::kEnterpriseControlled:
      is_pinnable = true;
      break;
    default:
      NOTREACHED();
  }

  if (!is_pinnable && action_view_->IsPinned()) {
    action_view_->SetVisible(false);
  }

  // Update the button's icon. If the action item is a stateful image action
  // item, use the stateful image. Otherwise, use the action item's image.
  ui::ImageModel image_model;
  if (actions::IsActionClass<actions::StatefulImageActionItem>(action_item)) {
    image_model = static_cast<actions::StatefulImageActionItem*>(action_item)
                      ->GetStatefulImage();
  } else {
    image_model = action_item->GetImage();
  }

  if (image_model.IsVectorIcon()) {
    action_view_->SetVectorIcon(action_view_->IsIconVisible()
                                    ? *image_model.GetVectorIcon().vector_icon()
                                    : gfx::VectorIcon::EmptyIcon());
  } else {
    action_view_->SetImageModel(
        views::Button::STATE_NORMAL,
        action_view_->IsIconVisible() ? image_model : ui::ImageModel());
  }
  // Set the accessible name. Fall back to the tooltip if one is not provided.
  // If pinned, the pinned state is added to the accessible name.
  const std::u16string accessible_name(action_item->GetAccessibleName().empty()
                                           ? action_view_->GetTooltipText()
                                           : action_item->GetAccessibleName());
  action_view_->GetViewAccessibility().SetName(
      action_view_->IsPinned()
          ? l10n_util::GetStringFUTF16(
                IDS_PINNED_ACTION_BUTTON_ACCESSIBLE_TITLE, accessible_name)
          : accessible_name);
  action_view_->UpdateStatusIndicator();
}

BEGIN_METADATA(PinnedActionToolbarButton)
END_METADATA
