// 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/toasts/toast_view.h"

#include <algorithm>
#include <climits>
#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/layer.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_client_view.h"
#include "ui/views/window/dialog_delegate.h"

namespace {
constexpr int kAnimationEntryDuration = 300;
constexpr int kAnimationExitDuration = 150;
constexpr int kAnimationHeightOffset = 50;
constexpr float kAnimationHeightScale = 0.5;

gfx::Transform GetScaleTransformation(gfx::Rect bounds) {
  gfx::Transform transform;
  transform.Translate(0,
                      bounds.CenterPoint().y() * (1 - kAnimationHeightScale));
  transform.Scale(1, kAnimationHeightScale);
  return transform;
}

bool IsCompatibleImageSize(const ui::ImageModel& image) {
  const auto intended_size = toasts::ToastView::GetIconSize();
  const auto image_size = image.Size();
  return image_size.width() == intended_size &&
         image_size.height() == intended_size;
}

gfx::Insets GetLeftMargin(const int left_margin) {
  return gfx::Insets::TLBR(0, left_margin, 0, 0);
}

// A simple `MenuModelAdapter` that runs `on_executed_command` whenever the
// command corresponding to a menu item is executed.
class ToastMenuModelAdapter : public views::MenuModelAdapter {
 public:
  ToastMenuModelAdapter(ui::MenuModel* menu_model,
                        base::RepeatingClosure on_menu_closed,
                        base::RepeatingClosure on_executed_command)
      : views::MenuModelAdapter(menu_model, std::move(on_menu_closed)),
        on_executed_command_(std::move(on_executed_command)) {}

  ~ToastMenuModelAdapter() override = default;

 private:
  // views::MenuModelAdapter:
  void ExecuteCommand(int id) override {
    views::MenuModelAdapter::ExecuteCommand(id);
    on_executed_command_.Run();
  }

  void ExecuteCommand(int id, int mouse_event_flags) override {
    views::MenuModelAdapter::ExecuteCommand(id, mouse_event_flags);
    on_executed_command_.Run();
  }

  base::RepeatingClosure on_executed_command_;
};

}  // namespace

namespace toasts {
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastView, kToastViewId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastView, kToastActionButton);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastView, kToastCloseButton);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastView, kToastMenuButton);

ToastView::ToastView(
    views::View* anchor_view,
    const std::u16string& toast_text,
    const gfx::VectorIcon& icon,
    std::optional<ui::ImageModel> image_override,
    bool render_toast_over_web_contents,
    base::RepeatingCallback<void(ToastCloseReason)> toast_close_callback)
    : BubbleDialogDelegateView(anchor_view, views::BubbleBorder::NONE),
      AnimationDelegateViews(this),
      toast_text_(toast_text),
      icon_(icon),
      image_override_(std::move(image_override)),
      render_toast_over_web_contents_(render_toast_over_web_contents),
      toast_close_callback_(std::move(toast_close_callback)) {
  SetBackgroundColor(ui::kColorToastBackgroundProminent);
  SetShowCloseButton(false);
  DialogDelegate::SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
  set_corner_radius(ChromeLayoutProvider::Get()->GetDistanceMetric(
      DISTANCE_TOAST_BUBBLE_HEIGHT));
  SetProperty(views::kElementIdentifierKey, kToastElementId);
  set_close_on_deactivate(false);
  SetProperty(views::kElementIdentifierKey, kToastViewId);
  SetAccessibleWindowRole(ax::mojom::Role::kAlert);
  SetAccessibleTitle(toast_text_);
}

ToastView::~ToastView() = default;

void ToastView::AddActionButton(const std::u16string& action_button_text,
                                base::RepeatingClosure action_button_callback) {
  CHECK(!has_action_button_);
  has_action_button_ = true;
  action_button_text_ = action_button_text;
  action_button_callback_ = std::move(action_button_callback);
}

void ToastView::AddCloseButton(base::RepeatingClosure close_callback) {
  CHECK(!has_close_button_);
  has_close_button_ = true;
  close_button_callback_ = std::move(close_callback);
}

void ToastView::AddMenu(std::unique_ptr<ui::MenuModel> model) {
  CHECK(!menu_model_);
  menu_model_ = std::move(model);
  menu_model_adapter_ = std::make_unique<ToastMenuModelAdapter>(
      menu_model_.get(),
      /*on_menu_closed=*/
      base::BindRepeating(&ToastView::OnMenuClosed, base::Unretained(this)),
      /*on_executed_command=*/
      base::BindRepeating(&ToastView::Close, base::Unretained(this),
                          ToastCloseReason::kMenuItemClick));
}

int ToastView::GetIconSize() {
  const ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
  return lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_ICON_SIZE);
}

void ToastView::Init() {
  ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();

  // FlexLayout lets the toast compress itself in narrow browser windows.
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kHorizontal);

  icon_view_ = AddChildView(std::make_unique<views::ImageView>());
  icon_view_->SetProperty(
      views::kMarginsKey,
      gfx::Insets::VH(0, lp->GetDistanceMetric(
                             DISTANCE_TOAST_BUBBLE_LEADING_ICON_SIDE_MARGINS)));

  label_ = AddChildView(
      std::make_unique<views::Label>(toast_text_, CONTEXT_TOAST_BODY_TEXT));
  label_->SetEnabledColor(ui::kColorToastForeground);
  label_->SetMultiLine(false);
  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  label_->SetAllowCharacterBreak(false);
  label_->SetAutoColorReadabilityEnabled(false);
  label_->SetLineHeight(
      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT));
  label_->SetProperty(views::kMarginsKey,
                      GetLeftMargin(lp->GetDistanceMetric(
                          DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING)));
  label_->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::LayoutOrientation::kHorizontal,
                               views::MinimumFlexSizeRule::kScaleToZero));

  int max_child_height =
      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT);

  if (has_action_button_) {
    action_button_ = AddChildView(std::make_unique<views::MdTextButton>(
        action_button_callback_.Then(
            base::BindRepeating(&ToastView::Close, base::Unretained(this),
                                ToastCloseReason::kActionButton)),
        action_button_text_));
    action_button_->SetEnabledTextColors(ui::kColorToastButton);
    action_button_->SetBgColorIdOverride(ui::kColorToastBackgroundProminent);
    action_button_->SetStrokeColorIdOverride(ui::kColorToastButton);
    action_button_->SetPreferredSize(gfx::Size(
        action_button_->GetPreferredSize().width(),
        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON)));
    action_button_->SetStyle(ui::ButtonStyle::kProminent);
    action_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
    action_button_->SetProperty(views::kElementIdentifierKey,
                                kToastActionButton);
    action_button_->SetAppearDisabledInInactiveWidget(false);
    action_button_->SetProperty(
        views::kMarginsKey,
        GetLeftMargin(lp->GetDistanceMetric(
            DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING)));
    SetInitiallyFocusedView(action_button_);
    max_child_height = std::max(
        max_child_height,
        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON));
  }

  if (has_close_button_) {
    close_button_ = AddChildView(views::CreateVectorImageButtonWithNativeTheme(
        close_button_callback_.Then(
            base::BindRepeating(&ToastView::Close, base::Unretained(this),
                                ToastCloseReason::kCloseButton)),
        features::IsRoundedIconsEnabled()
            ? vector_icons::kCloseIcon
            : vector_icons::kCloseChromeRefreshOldIcon,
        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_ICON_SIZE),
        ui::kColorToastForeground, ui::kColorIconDisabled,
        ui::kColorToastForeground));
    // Override the image button's border with the appropriate icon border size.
    const gfx::Insets insets =
        lp->GetInsetsMetric(views::InsetsMetric::INSETS_VECTOR_IMAGE_BUTTON);
    close_button_->SetBorder(views::CreateEmptyBorder(insets));
    views::InstallCircleHighlightPathGenerator(close_button_);
    close_button_->SetAccessibleName(l10n_util::GetStringUTF16(IDS_CLOSE));
    close_button_->SetProperty(views::kElementIdentifierKey, kToastCloseButton);
    close_button_->SetProperty(
        views::kMarginsKey, GetLeftMargin(lp->GetDistanceMetric(
                                DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING)));
    close_button_->SetTooltipText(
        l10n_util::GetStringUTF16(IDS_TOAST_CLOSE_TOOLTIP));
    if (!HasConfiguredInitiallyFocusedView()) {
      SetInitiallyFocusedView(close_button_);
    }
    max_child_height =
        std::max(max_child_height,
                 lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_ICON_SIZE) +
                     insets.height());
  }

  if (menu_model_) {
    menu_button_ = AddChildView(views::CreateVectorImageButtonWithNativeTheme(
        base::RepeatingClosure(),
        features::IsRoundedIconsEnabled() ? kMoreVertIcon
                                          : kBrowserToolsChromeRefreshOldIcon,
        /*dip_size=*/
        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MENU_ICON_SIZE),
        ui::kColorToastForeground, ui::kColorIconDisabled,
        ui::kColorToastForeground));
    views::InstallCircleHighlightPathGenerator(menu_button_);
    menu_button_->SetProperty(views::kElementIdentifierKey, kToastMenuButton);
    menu_button_->SetButtonController(
        std::make_unique<views::MenuButtonController>(
            menu_button_,
            base::BindRepeating(&ToastView::OnMenuButtonClicked,
                                base::Unretained(this)),
            std::make_unique<views::Button::DefaultButtonControllerDelegate>(
                menu_button_)));
    menu_button_->SetAccessibleName(
        l10n_util::GetStringUTF16(IDS_TOAST_MENU_BUTTON_NAME));
    const gfx::Insets insets = menu_button_->GetInsets();
    const int left_margin =
        lp->GetDistanceMetric(
            DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_MENU_BUTTON_SPACING) -
        insets.left();
    menu_button_->SetProperty(views::kMarginsKey, GetLeftMargin(left_margin));
    if (!HasConfiguredInitiallyFocusedView()) {
      SetInitiallyFocusedView(menu_button_);
    }
    max_child_height =
        std::max(max_child_height,
                 lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MENU_ICON_SIZE) +
                     insets.height());
  }

  // Height of the toast is set implicitly by adding margins depending on the
  // height of the tallest child. We cannot simply iterate over all children
  // here because the `ImageModel` for `ImageButton`s is only set once the theme
  // is available.
  const int total_vertical_margins =
      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT) - max_child_height;
  const int top_margin = total_vertical_margins / 2;
  const int right_margin = [&]() {
    if (menu_button_) {
      return lp->GetDistanceMetric(
          DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON);
    } else if (close_button_) {
      return lp->GetDistanceMetric(
          DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON);
    } else if (action_button_) {
      return lp->GetDistanceMetric(
          DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON);
    }
    return lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL);
  }();
  set_margins(gfx::Insets::TLBR(
      top_margin, lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_LEFT),
      total_vertical_margins - top_margin, right_margin));

  if (has_action_button_ || has_close_button_ || menu_model_) {
    SetFocusTraversesOut(true);
  } else {
    set_focus_traversable_from_anchor_view(false);
    SetCanActivate(false);
  }
}

void ToastView::AnimationProgressed(const gfx::Animation* animation) {
  const double value = gfx::Tween::CalculateValue(
      height_animation_tween_, height_animation_.GetCurrentValue());
  const gfx::Rect current_bounds = gfx::Tween::RectValueBetween(
      value, starting_widget_bounds_, target_widget_bounds_);
  GetWidget()->SetBounds(current_bounds);
}

void ToastView::AnimateIn() {
  if (!gfx::Animation::ShouldRenderRichAnimation()) {
    return;
  }

  target_widget_bounds_ = GetWidget()->GetWindowBoundsInScreen();
  starting_widget_bounds_ =
      target_widget_bounds_ - gfx::Vector2d{0, kAnimationHeightOffset};
  height_animation_tween_ = gfx::Tween::ACCEL_5_70_DECEL_90;
  height_animation_.SetDuration(base::Milliseconds(kAnimationEntryDuration));
  height_animation_.Start();

  views::View* const bubble_frame_view = GetBubbleFrameView();
  bubble_frame_view->SetPaintToLayer();
  bubble_frame_view->layer()->SetFillsBoundsOpaquely(false);
  bubble_frame_view->SetTransform(
      GetScaleTransformation(bubble_frame_view->bounds()));
  bubble_frame_view->layer()->SetOpacity(0);
  GetDialogClientView()->SetBackground(
      views::CreateSolidBackground(ui::kColorToastBackgroundProminent));
  GetDialogClientView()->layer()->SetOpacity(0);
  views::AnimationBuilder()
      .Once()
      .SetDuration(base::Milliseconds(kAnimationEntryDuration))
      .SetTransform(bubble_frame_view, gfx::Transform(),
                    height_animation_tween_)
      .At(base::TimeDelta())
      .SetDuration(base::Milliseconds(50))
      .SetOpacity(bubble_frame_view, 1)
      .Then()
      .SetDuration(base::Milliseconds(150))
      .SetOpacity(GetDialogClientView(), 1);
}

void ToastView::Close(ToastCloseReason reason) {
  // Do not close if the menu is open - instead, remember the call to close it.
  if (menu_runner_) {
    pending_close_reason_ = reason;
    return;
  }

  // TODO(crbug.com/358610872): Log toast close reason metric.
  views::Widget::ClosedReason widget_closed_reason =
      views::Widget::ClosedReason::kUnspecified;
  switch (reason) {
    case ToastCloseReason::kCloseButton:
      widget_closed_reason = views::Widget::ClosedReason::kCloseButtonClicked;
      break;
    case ToastCloseReason::kActionButton:
      widget_closed_reason = views::Widget::ClosedReason::kAcceptButtonClicked;
      break;
    default:
      break;
  }

  toast_close_callback_.Run(reason);
  if (GetWidget()->IsVisible()) {
    AnimateOut(
        base::BindOnce(&views::Widget::CloseWithReason,
                       base::Unretained(GetWidget()), widget_closed_reason),
        reason != ToastCloseReason::kPreempted);
  } else {
    GetWidget()->CloseWithReason(widget_closed_reason);
  }
}

void ToastView::UpdateRenderToastOverWebContentsAndPaint(
    const bool render_toast_over_web_contents) {
  render_toast_over_web_contents_ = render_toast_over_web_contents;
  SizeToContents();
}

gfx::Rect ToastView::GetBubbleBounds() {
  views::View* anchor_view = GetAnchorView();
  if (!anchor_view) {
    return gfx::Rect();
  }

  const gfx::Size preferred_size =
      GetWidget()->GetContentsView()->GetPreferredSize();
  const gfx::Rect anchor_bounds = anchor_view->GetBoundsInScreen();
  const gfx::Rect anchor_widget_bounds =
      anchor_view->GetWidget()->GetWindowBoundsInScreen();

  // A wide toast in a narrow browser window needs to be compressed to fit.
  const int minimum_margin = ChromeLayoutProvider::Get()->GetDistanceMetric(
                                 DISTANCE_TOAST_BUBBLE_BROWSER_WINDOW_MARGIN) -
                             views::BubbleBorder::kShadowBlur;
  const int width =
      std::min(preferred_size.width(),
               std::max(anchor_widget_bounds.width() - 2 * minimum_margin, 0));
  const int x =
      std::clamp(anchor_bounds.x() + ((anchor_bounds.width() - width) / 2),
                 anchor_widget_bounds.x() + minimum_margin,
                 anchor_widget_bounds.right() - minimum_margin - width);

  // Take bubble out of its original bounds to cross "line of death", unless in
  // fullscreen mode where the top container isn't rendered.
  const int y = anchor_bounds.bottom() - (render_toast_over_web_contents_
                                              ? views::BubbleBorder::kShadowBlur
                                              : (preferred_size.height() / 2));
  return gfx::Rect(x, y, width, preferred_size.height());
}

void ToastView::OnThemeChanged() {
  BubbleDialogDelegateView::OnThemeChanged();
  const auto* color_provider = GetColorProvider();
  if (image_override_.has_value() &&
      IsCompatibleImageSize(image_override_.value())) {
    icon_view_->SetImage(image_override_.value());
  } else {
    icon_view_->SetImage(ui::ImageModel::FromVectorIcon(
        *icon_, color_provider->GetColor(ui::kColorToastForeground),
        GetIconSize()));
  }
}

void ToastView::AnimateOut(base::OnceClosure callback,
                           bool show_height_animation) {
  if (!gfx::Animation::ShouldRenderRichAnimation()) {
    std::move(callback).Run();
    return;
  }

  views::View* const bubble_frame_view = GetBubbleFrameView();
  if (!bubble_frame_view->layer()) {
    bubble_frame_view->SetPaintToLayer();
    bubble_frame_view->layer()->SetFillsBoundsOpaquely(false);
  }

  if (show_height_animation) {
    starting_widget_bounds_ = GetWidget()->GetWindowBoundsInScreen();
    target_widget_bounds_ =
        starting_widget_bounds_ - gfx::Vector2d{0, kAnimationHeightOffset};
    height_animation_tween_ = gfx::Tween::ACCEL_30_DECEL_20_85;
    height_animation_.SetDuration(base::Milliseconds(kAnimationExitDuration));
    height_animation_.Start();

    views::AnimationBuilder()
        .Once()
        .SetDuration(base::Milliseconds(kAnimationExitDuration))
        .SetTransform(bubble_frame_view,
                      GetScaleTransformation(bubble_frame_view->bounds()),
                      height_animation_tween_);
  }

  views::AnimationBuilder()
      .OnEnded(std::move(callback))
      .Once()
      .SetDuration(base::Milliseconds(100))
      .SetOpacity(GetDialogClientView(), 0)
      .Then()
      .SetDuration(base::Milliseconds(50))
      .SetOpacity(bubble_frame_view, 0);
}

void ToastView::OnMenuButtonClicked() {
  menu_runner_ = std::make_unique<views::MenuRunner>(
      menu_model_adapter_->CreateMenu(), views::MenuRunner::FIXED_ANCHOR);
  menu_runner_->RunMenuAt(GetWidget(),
                          static_cast<views::MenuButtonController*>(
                              menu_button_->button_controller()),
                          menu_button_->GetBoundsInScreen(),
                          views::MenuAnchorPosition::kTopRight,
                          ui::mojom::MenuSourceType::kNone);
}

void ToastView::OnMenuClosed() {
  menu_runner_.reset();
  if (pending_close_reason_) {
    Close(pending_close_reason_.value());
  }
}

BEGIN_METADATA(ToastView)
END_METADATA

}  // namespace toasts
