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

#include "components/user_education/views/help_bubble_view.h"

#include <initializer_list>
#include <memory>
#include <numeric>
#include <string>
#include <utility>

#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/scoped_observation.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/views/help_bubble_delegate.h"
#include "components/user_education/views/help_bubble_event_relay.h"
#include "components/user_education/views/help_bubble_factory_views.h"
#include "components/user_education/views/help_bubble_views.h"
#include "components/variations/variations_associated_data.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/dot_indicator.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.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/layout/layout_types.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/style/typography.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_tracker.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"

namespace user_education {

namespace {

class MdIPHBubbleButton : public views::MdTextButton {
  METADATA_HEADER(MdIPHBubbleButton, views::MdTextButton)

 public:
  MdIPHBubbleButton(const HelpBubbleDelegate* delegate,
                    PressedCallback callback,
                    const std::u16string& text,
                    bool is_default_button)
      : MdTextButton(std::move(callback), text),
        delegate_(delegate),
        is_default_button_(is_default_button) {
    GetViewAccessibility().SetIsLeaf(true);

    views::FocusRing::Get(this)->SetColorId(
        delegate_->GetHelpBubbleForegroundColorId());

    ui::ColorId foreground_color =
        is_default_button_
            ? delegate_->GetHelpBubbleDefaultButtonForegroundColorId()
            : delegate_->GetHelpBubbleForegroundColorId();
    SetEnabledTextColors(foreground_color);
    // TODO(crbug.com/40709599): Temporary fix for Mac. Bubble shouldn't be in
    // inactive style when the bubble loses focus.
    SetTextColor(ButtonState::STATE_DISABLED, foreground_color);

    // The default behavior in 2023 refresh is for MD buttons is to have the
    // alpha baked into the color, but we currently don't have that yet, so
    // switch back to using the old default alpha blending mode.
    auto* const ink_drop = views::InkDrop::Get(this);
    ink_drop->SetBaseColor(
        is_default_button_
            ? delegate_->GetHelpBubbleDefaultButtonForegroundColorId()
            : delegate_->GetHelpBubbleForegroundColorId());
    ink_drop->SetHighlightOpacity(std::nullopt);
  }
  MdIPHBubbleButton(const MdIPHBubbleButton&) = delete;
  MdIPHBubbleButton& operator=(const MdIPHBubbleButton&) = delete;
  ~MdIPHBubbleButton() override = default;

  void UpdateBackgroundColor() override {
    // Prominent MD button does not have a border.
    // Override this method to draw a border.
    // Adapted from MdTextButton::UpdateBackgroundColor()
    const auto* color_provider = GetColorProvider();
    if (!color_provider)
      return;
    SkColor background_color = color_provider->GetColor(
        is_default_button_
            ? delegate_->GetHelpBubbleDefaultButtonBackgroundColorId()
            : delegate_->GetHelpBubbleBackgroundColorId());
    if (GetState() == STATE_PRESSED) {
      background_color =
          GetNativeTheme()->GetSystemButtonPressedColor(background_color);
    }
    const SkColor stroke_color = color_provider->GetColor(
        is_default_button_
            ? delegate_->GetHelpBubbleDefaultButtonBackgroundColorId()
            : delegate_->GetHelpBubbleButtonBorderColorId());
    SetBackground(CreateBackgroundFromPainter(
        views::Painter::CreateRoundRectWith1PxBorderPainter(
            background_color, stroke_color, GetCornerRadii())));
  }

 private:
  const raw_ptr<const HelpBubbleDelegate> delegate_;
  bool is_default_button_;
};

BEGIN_METADATA(MdIPHBubbleButton)
END_METADATA

// Displays a simple "X" close button that will close a promo bubble view.
// The alt-text and button callback can be set based on the needs of the
// specific bubble.
class ClosePromoButton : public views::ImageButton {
  METADATA_HEADER(ClosePromoButton, views::ImageButton)

 public:
  ClosePromoButton(const HelpBubbleDelegate* delegate,
                   const std::u16string accessible_name,
                   PressedCallback callback)
      : delegate_(delegate) {
    SetCallback(std::move(callback));
    views::ConfigureVectorImageButton(this);
    views::HighlightPathGenerator::Install(
        this,
        std::make_unique<views::CircleHighlightPathGenerator>(gfx::Insets()));
    GetViewAccessibility().SetName(accessible_name);
    SetTooltipText(accessible_name);

    constexpr int kIconSize = 16;
    SetImageModel(
        views::ImageButton::STATE_NORMAL,
        ui::ImageModel::FromVectorIcon(
            features::IsRoundedIconsEnabled() ? views::kCloseIcon
                                              : views::kIcCloseOldIcon,
            delegate_->GetHelpBubbleForegroundColorId(), kIconSize));

    constexpr float kCloseButtonFocusRingHaloThickness = 1.25f;
    views::FocusRing::Get(this)->SetHaloThickness(
        kCloseButtonFocusRingHaloThickness);
  }

  void OnThemeChanged() override {
    views::ImageButton::OnThemeChanged();
    const auto* color_provider = GetColorProvider();
    views::InkDrop::Get(this)->SetBaseColor(color_provider->GetColor(
        delegate_->GetHelpBubbleCloseButtonInkDropColorId()));
    views::FocusRing::Get(this)->SetColorId(
        delegate_->GetHelpBubbleForegroundColorId());
  }

 private:
  const raw_ptr<const HelpBubbleDelegate> delegate_;
};

BEGIN_METADATA(ClosePromoButton)
END_METADATA

class DotView : public views::View {
  METADATA_HEADER(DotView, views::View)

 public:
  DotView(const HelpBubbleDelegate* delegate, gfx::Size size, bool should_fill)
      : delegate_(delegate), size_(size), should_fill_(should_fill) {
    // In order to anti-alias properly, we'll grow by the stroke width and then
    // have the excess space be subtracted from the margins by the layout.
    SetProperty(views::kInternalPaddingKey, gfx::Insets(kStrokeWidth));
  }
  ~DotView() override = default;

  // views::View:
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    gfx::Size size = size_;
    const gfx::Insets* const insets = GetProperty(views::kInternalPaddingKey);
    size.Enlarge(insets->width(), insets->height());
    return size;
  }

  void OnPaint(gfx::Canvas* canvas) override {
    gfx::RectF local_bounds = gfx::RectF(GetLocalBounds());
    DCHECK_GT(local_bounds.width(), size_.width());
    DCHECK_GT(local_bounds.height(), size_.height());
    const gfx::PointF center_point = local_bounds.CenterPoint();
    const float radius = (size_.width() - kStrokeWidth) / 2.0f;

    const SkColor color = GetColorProvider()->GetColor(
        delegate_->GetHelpBubbleForegroundColorId());
    if (should_fill_) {
      cc::PaintFlags fill_flags;
      fill_flags.setStyle(cc::PaintFlags::kFill_Style);
      fill_flags.setAntiAlias(true);
      fill_flags.setColor(color);
      canvas->DrawCircle(center_point, radius, fill_flags);
    }

    cc::PaintFlags stroke_flags;
    stroke_flags.setStyle(cc::PaintFlags::kStroke_Style);
    stroke_flags.setStrokeWidth(kStrokeWidth);
    stroke_flags.setAntiAlias(true);
    stroke_flags.setColor(color);
    canvas->DrawCircle(center_point, radius, stroke_flags);
  }

 private:
  static constexpr int kStrokeWidth = 1;

  raw_ptr<const HelpBubbleDelegate> delegate_;
  const gfx::Size size_;
  const bool should_fill_;
};

constexpr int DotView::kStrokeWidth;

BEGIN_METADATA(DotView)
END_METADATA

}  // namespace

DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView,
                                      kHelpBubbleElementIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView,
                                      kDefaultButtonIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView,
                                      kFirstNonDefaultButtonIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView, kCloseButtonIdForTesting);

DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView, kBodyTextIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(HelpBubbleView, kTitleTextIdForTesting);

// TODO(https://crbug.com/382611284): Temporarily handle the case when the
// primary window is minimized by closing the help bubble. Remove this code when
// the issue is solved at the Views framework level.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC)
class HelpBubbleView::PrimaryWidgetObserver : public views::WidgetObserver {
 public:
  explicit PrimaryWidgetObserver(HelpBubbleView& help_bubble)
      : help_bubble_(help_bubble) {
    observation_.Observe(help_bubble_->GetWidget()->GetPrimaryWindowWidget());
  }
  ~PrimaryWidgetObserver() override = default;

 private:
  void OnWidgetDestroying(views::Widget*) override { observation_.Reset(); }
  void OnWidgetShowStateChanged(views::Widget* widget) override {
    if (widget->IsMinimized()) {
      help_bubble_->GetWidget()->CloseWithReason(
          views::Widget::ClosedReason::kLostFocus);
    }
  }

  const raw_ref<HelpBubbleView> help_bubble_;
  base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
      this};
};
#endif

// Watches for the anchor view to be destroyed or removed from its widget.
// Used in cases where the anchor element is not the same as the anchor view.
// Prevents the help bubble from lingering after its anchor is invalid, which
// can cause crashes and other strange behavior.
class HelpBubbleView::AnchorViewObserver : public views::ViewObserver {
 public:
  AnchorViewObserver(views::View* anchor_view, HelpBubbleView* help_bubble)
      : help_bubble_(help_bubble) {
    observation_.Observe(anchor_view);
  }

 private:
  // views::ViewObserver:
  void OnViewRemovedFromWidget(views::View*) override { Detach(); }
  void OnViewIsDeleting(views::View*) override { Detach(); }

  void Detach() {
    observation_.Reset();
    if (help_bubble_->GetWidget() && !help_bubble_->GetWidget()->IsClosed()) {
      help_bubble_->GetWidget()->Close();
    }
  }

  const raw_ptr<HelpBubbleView> help_bubble_;
  base::ScopedObservation<View, ViewObserver> observation_{this};
};

// static
HelpBubbleViewInfo HelpBubbleView::Create(
    const HelpBubbleDelegate* delegate,
    const internal::HelpBubbleAnchorParams& anchor,
    HelpBubbleParams params,
    std::unique_ptr<HelpBubbleEventRelay> event_relay) {
  const bool visible_arrow =
      anchor.show_arrow && params.arrow != HelpBubbleArrow::kNone;
  const bool show_active =
      params.focus_on_show_hint.value_or(!params.buttons.empty()) &&
      !event_relay;
  auto bubble = base::WrapUnique(new HelpBubbleView(
      delegate, anchor, std::move(params), std::move(event_relay)));
  auto* const bubble_ptr = bubble.get();
  std::unique_ptr<views::Widget> widget =
      views::BubbleDialogDelegate::CreateBubble(std::move(bubble).release());
  bubble_ptr->InitializeAndShow(visible_arrow, show_active);
  return HelpBubbleViewInfo(std::move(widget), bubble_ptr);
}

HelpBubbleView::HelpBubbleView(
    const HelpBubbleDelegate* delegate,
    const internal::HelpBubbleAnchorParams& anchor,
    HelpBubbleParams params,
    std::unique_ptr<HelpBubbleEventRelay> event_relay)
    : BubbleDialogDelegateView(anchor.view,
                               HelpBubbleViews::TranslateArrow(params.arrow),
                               HelpBubbleFactoryViews::GetDefaultBubbleShadow(),
                               true),
      delegate_(delegate),
      event_relay_(std::move(event_relay)) {
  SetBackgroundColor(delegate_->GetHelpBubbleBackgroundColorId());

  if (anchor.rect.has_value()) {
    SetForceAnchorRect(anchor.rect.value());
    anchor_observer_ = std::make_unique<AnchorViewObserver>(anchor.view, this);
  } else {
    // When hosted within a `views::ScrollView`, the anchor view may be
    // (partially) outside the viewport. Ensure that the anchor view is visible.
    anchor.view->ScrollViewToVisible();
  }
  DCHECK(anchor.view)
      << "A bubble that closes on blur must be initially focused.";
  UseCompactMargins();

  // Default timeout depends on whether non-close buttons are present.
  timeout_ = params.timeout.value_or(params.buttons.empty()
                                         ? kDefaultTimeoutWithoutButtons
                                         : kDefaultTimeoutWithButtons);
  timeout_callback_ = std::move(params.timeout_callback);
  SetCancelCallback(std::move(params.dismiss_callback));

  accessible_name_ = params.title_text;
  if (!accessible_name_.empty())
    accessible_name_ += u". ";
  accessible_name_ += params.screenreader_text.empty()
                          ? params.body_text
                          : params.screenreader_text;
  screenreader_hint_text_ = params.keyboard_navigation_hint;

  // Since we don't have any controls for the user to interact with (we're just
  // an information bubble), override our role to kAlert.
  SetAccessibleWindowRole(ax::mojom::Role::kAlert);

  // Layout structure:
  //
  // [***ooo      x]  <--- progress container
  // [@ TITLE     x]  <--- top text container
  //    body text
  // [    cancel ok]  <--- button container
  //
  // Notes:
  // - The close button's placement depends on the presence of a progress
  //   indicator.
  // - The body text takes the place of TITLE if there is no title.
  // - If there is both a title and icon, the body text is manually indented to
  //   align with the title; this avoids having to nest an additional vertical
  //   container.
  // - Unused containers are set to not be visible.
  views::View* const progress_container =
      AddChildView(std::make_unique<views::View>());
  views::View* const top_text_container =
      AddChildView(std::make_unique<views::View>());
  views::View* const button_container =
      AddChildView(std::make_unique<views::View>());

  // Add progress indicator (optional) and its container.
  if (params.progress) {
    DCHECK(params.progress->second);
    // TODO(crbug.com/40176811): surface progress information in a11y tree
    for (int i = 0; i < params.progress->second; ++i) {
      // TODO(crbug.com/40176811): formalize dot size
      progress_container->AddChildView(std::make_unique<DotView>(
          delegate, gfx::Size(8, 8), i < params.progress->first));
    }
  } else {
    progress_container->SetVisible(false);
  }

  // Add the body icon (optional).
  constexpr int kBodyIconSize = 20;
  constexpr int kBodyIconBackgroundSize = 24;
  if (params.body_icon) {
    icon_view_ = top_text_container->AddChildViewAt(
        std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
            *params.body_icon, delegate->GetHelpBubbleBackgroundColorId(),
            kBodyIconSize)),
        0);
    icon_view_->SetPreferredSize(
        gfx::Size(kBodyIconBackgroundSize, kBodyIconBackgroundSize));
    icon_view_->GetViewAccessibility().SetName(params.body_icon_alt_text);
  }

  // Add title (optional) and body label.
  if (!params.title_text.empty()) {
    views::Label* const title_label =
        top_text_container->AddChildView(std::make_unique<views::Label>(
            params.title_text, delegate->GetTitleTextContext()));
    title_label->SetProperty(views::kElementIdentifierKey,
                             kTitleTextIdForTesting);
    labels_.push_back(title_label);
    views::Label* const body_label =
        AddChildViewAt(std::make_unique<views::Label>(
                           params.body_text, delegate->GetBodyTextContext()),
                       GetIndexOf(button_container).value());
    labels_.push_back(body_label);
    body_label->SetProperty(views::kElementIdentifierKey,
                            kBodyTextIdForTesting);
  } else {
    views::Label* const body_label =
        top_text_container->AddChildView(std::make_unique<views::Label>(
            params.body_text, delegate->GetBodyTextContext()));
    labels_.push_back(body_label);
    body_label->SetProperty(views::kElementIdentifierKey,
                            kBodyTextIdForTesting);
  }

  // Set common label properties.
  for (views::Label* label : labels_) {
    label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    label->SetMultiLine(true);
    label->SetElideBehavior(gfx::NO_ELIDE);
  }

  // Add close button.
  std::u16string alt_text = params.close_button_alt_text;

  // This can be empty if a test doesn't set it. Set a reasonable default to
  // avoid an assertion (generated when a button with no text has no
  // accessible name).
  if (alt_text.empty()) {
    alt_text = l10n_util::GetStringUTF16(IDS_CLOSE);
  }

  // Since we set the cancel callback, we will use CancelDialog() to dismiss.
  close_button_ = (params.progress ? progress_container : top_text_container)
                      ->AddChildView(std::make_unique<ClosePromoButton>(
                          delegate, alt_text,
                          base::BindRepeating(&DialogDelegate::CancelDialog,
                                              base::Unretained(this))));
  close_button_->SetProperty(views::kElementIdentifierKey,
                             kCloseButtonIdForTesting);

  // Add other buttons.
  if (!params.buttons.empty()) {
    auto run_callback_and_close = [](HelpBubbleView* bubble_view,
                                     base::OnceClosure callback) {
      // We want to call the button callback before deleting the bubble in case
      // the caller needs to do something with it, but the callback itself
      // could close the bubble. Therefore, we need to ensure that the
      // underlying bubble view is not deleted before trying to close it.
      views::ViewTracker tracker(bubble_view);
      std::move(callback).Run();
      auto* const view = tracker.view();
      if (view && view->GetWidget() && !view->GetWidget()->IsClosed())
        view->GetWidget()->Close();
    };

    // We will hold the default button to add later, since where we add it in
    // the sequence depends on platform style.
    std::unique_ptr<MdIPHBubbleButton> default_button;
    for (HelpBubbleButtonParams& button_params : params.buttons) {
      auto button = std::make_unique<MdIPHBubbleButton>(
          delegate,
          base::BindOnce(run_callback_and_close, base::Unretained(this),
                         std::move(button_params.callback)),
          button_params.text, button_params.is_default);
      button->SetMinSize(gfx::Size(0, 0));
      if (button_params.is_default) {
        DCHECK(!default_button);
        default_button = std::move(button);
        default_button->SetProperty(views::kElementIdentifierKey,
                                    kDefaultButtonIdForTesting);
      } else {
        non_default_buttons_.push_back(
            button_container->AddChildView(std::move(button)));
      }
    }

    if (!non_default_buttons_.empty()) {
      non_default_buttons_.front()->SetProperty(
          views::kElementIdentifierKey, kFirstNonDefaultButtonIdForTesting);
    }

    // Add the default button if there is one based on platform style.
    if (default_button) {
      if constexpr (views::PlatformStyle::kIsOkButtonLeading) {
        default_button_ =
            button_container->AddChildViewAt(std::move(default_button), 0);
      } else {
        default_button_ =
            button_container->AddChildView(std::move(default_button));
      }
    }
  } else {
    button_container->SetVisible(false);
  }

  // Set up layouts. This is the default vertical spacing that is also used to
  // separate progress indicators for symmetry.
  // TODO(dfried): consider whether we could take font ascender and descender
  // height and factor them into margin calculations.
  const views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
  const int default_spacing = layout_provider->GetDistanceMetric(
      views::DISTANCE_RELATED_CONTROL_VERTICAL);

  // The insets from the bubble border to the text inside.
  const gfx::Insets contents_insets = gfx::Insets(20);

  // Create primary layout (vertical).
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kVertical)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetInteriorMargin(contents_insets)
      .SetCollapseMargins(true)
      .SetDefault(views::kMarginsKey,
                  gfx::Insets::TLBR(0, 0, default_spacing, 0))
      .SetIgnoreDefaultMainAxisMargins(true);

  // Set up top row container layout.
  const int kCloseButtonHeight = 20;
  auto& progress_layout =
      progress_container
          ->SetLayoutManager(std::make_unique<views::FlexLayout>())
          ->SetOrientation(views::LayoutOrientation::kHorizontal)
          .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
          .SetMinimumCrossAxisSize(kCloseButtonHeight)
          .SetDefault(views::kMarginsKey,
                      gfx::Insets::TLBR(0, default_spacing, 0, 0))
          .SetIgnoreDefaultMainAxisMargins(true);
  progress_container->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(progress_layout.GetDefaultFlexRule()));

  // Close button should float right in whatever container it's in.
  if (close_button_) {
    close_button_->SetProperty(
        views::kFlexBehaviorKey,
        views::FlexSpecification(views::LayoutOrientation::kHorizontal,
                                 views::MinimumFlexSizeRule::kPreferred,
                                 views::MaximumFlexSizeRule::kUnbounded)
            .WithAlignment(views::LayoutAlignment::kEnd));
    close_button_->SetProperty(views::kMarginsKey,
                               gfx::Insets::TLBR(0, default_spacing, 0, 0));
    close_button_->SetProperty(views::kElementIdentifierKey,
                               kCloseButtonIdForTesting);
  }

  // Icon view should have padding between it and the title or body label.
  if (icon_view_) {
    // When there is no title, distance from icon and body text to buttons can
    // appear cramped, so add a small bit of extra margin.
    const int bottom_margin =
        !params.buttons.empty() && params.title_text.empty() ? 2 : 0;
    icon_view_->SetProperty(
        views::kMarginsKey,
        gfx::Insets::TLBR(0, 0, bottom_margin, default_spacing));
  }

  // Set label flex properties. This ensures that if the width of the bubble
  // maxes out the text will shrink on the cross-axis and grow to multiple
  // lines without getting cut off.
  const views::FlexSpecification text_flex(
      views::LayoutOrientation::kVertical,
      views::MinimumFlexSizeRule::kPreferred,
      views::MaximumFlexSizeRule::kPreferred,
      /* adjust_height_for_width = */ true,
      views::MinimumFlexSizeRule::kScaleToMinimum);

  for (views::Label* label : labels_)
    label->SetProperty(views::kFlexBehaviorKey, text_flex);

  auto& top_text_layout =
      top_text_container
          ->SetLayoutManager(std::make_unique<views::FlexLayout>())
          ->SetOrientation(views::LayoutOrientation::kHorizontal)
          .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
          .SetIgnoreDefaultMainAxisMargins(true);
  top_text_container->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(top_text_layout.GetDefaultFlexRule()));

  // If the body icon is present, labels after the first are not parented to
  // the top text container, but still need to be inset to align with the
  // title.
  if (icon_view_) {
    const int indent =
        contents_insets.left() + kBodyIconBackgroundSize + default_spacing;
    for (size_t i = 1; i < labels_.size(); ++i) {
      labels_[i]->SetProperty(views::kMarginsKey,
                              gfx::Insets::TLBR(0, indent, 0, 0));
    }
  }

  // Set up button container layout.

  // Add in spacing between bubble content and bottom/buttons.
  button_container->SetProperty(
      views::kMarginsKey,
      gfx::Insets::TLBR(layout_provider->GetDistanceMetric(
                            views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_TEXT),
                        0, 0, 0));

  // Create button container internal layout.
  auto& button_layout =
      button_container->SetLayoutManager(std::make_unique<views::FlexLayout>())
          ->SetOrientation(views::LayoutOrientation::kHorizontal)
          .SetMainAxisAlignment(views::LayoutAlignment::kEnd)
          .SetDefault(
              views::kMarginsKey,
              gfx::Insets::TLBR(0,
                                layout_provider->GetDistanceMetric(
                                    views::DISTANCE_RELATED_BUTTON_HORIZONTAL),
                                0, 0))
          .SetIgnoreDefaultMainAxisMargins(true);

  // In a handful of (mostly South-Asian) languages, button text can exceed the
  // available width in the bubble if buttons are aligned horizontally. In those
  // cases - and only those cases - the bubble can switch to a vertical button
  // alignment.
  if (button_container->GetMinimumSize().width() >
      kMaxWidthDip - contents_insets.width()) {
    button_layout.SetOrientation(views::LayoutOrientation::kVertical)
        .SetCrossAxisAlignment(views::LayoutAlignment::kEnd)
        .SetDefault(views::kMarginsKey, gfx::Insets::VH(default_spacing, 0))
        .SetIgnoreDefaultMainAxisMargins(true);

    // Calculate the closest the bubble can be to the normal max width without
    // cutting off an especially long button caption.
    max_bubble_width_ =
        std::max(kMaxWidthDip, button_container->GetMinimumSize().width() +
                                   contents_insets.width());
  }

  button_container->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(button_layout.GetDefaultFlexRule()));

  // Want a consistent initial focused view if one is available.
  if (!button_container->children().empty()) {
    SetInitiallyFocusedView(button_container->children()[0]);
  } else if (close_button_) {
    SetInitiallyFocusedView(close_button_);
  }

  SetProperty(views::kElementIdentifierKey, kHelpBubbleElementIdForTesting);
  set_frame_margins({.contents = gfx::Insets(), .title = gfx::Insets()});
  SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
  set_close_on_deactivate(false);
  set_focus_traversable_from_anchor_view(false);

  if (event_relay_ && !event_relay_->ShouldHelpBubbleProcessEvents()) {
    CHECK_LE(params.buttons.size(), 1U)
        << "Help bubbles that cannot activate cannot have multiple interactive "
           "buttons due to accessibility constraints.";
    SetCanActivate(false);
    set_accept_events(false);
  }
}

void HelpBubbleView::InitializeAndShow(bool visible_arrow, bool show_active) {
  views::Widget* const widget = GetWidget();

  // This gets reset to the platform default when we call CreateBubble(), so we
  // have to change it afterwards:
  set_adjust_if_offscreen(true);
  auto* const frame_view = GetBubbleFrameView();
  frame_view->SetDisplayVisibleArrow(visible_arrow);

  // If the primary window widget is not the anchor widget, do not use the
  // window anchor bounds.
  if (anchor_widget()->GetPrimaryWindowWidget() != anchor_widget()) {
    SetUseAnchorWindowBounds(false);
  }

  // Bubbles get a 1-dip border that's either light or dark depending on system
  // light or dark mode, but this does not match with the help bubble (see
  // b/303069420).
  frame_view->bubble_border()->set_draw_border_stroke(false);

  // TODO(crbug.com/41493925) Remove this InvalidateLayout() once the border
  // invalidate itself when it changes.
  InvalidateLayout();

  // Setup that should happen after the widget is constructed:
  if (event_relay_ && !event_relay_->ShouldHelpBubbleProcessEvents()) {
    // This is required on Windows because of the way events are routed.
    GetBubbleFrameView()->set_hit_test_transparent(true);
  }
  if (auto* const anchor_bubble =
          anchor_widget()->widget_delegate()->AsBubbleDialogDelegate()) {
    // Make sure that if the help bubble is attaching to a dialog, the dialog
    // does not immediately dismiss when the help bubble is shown or focused.
    anchor_pin_ = anchor_bubble->PreventCloseOnDeactivate();
  }

  // Most help bubbles with buttons take focus when they show.
  if (show_active) {
    widget->Show();
  } else {
    widget->ShowInactive();
  }

  // Begin event-forwarding if appropriate.
  if (event_relay_) {
    event_relay_->Init(this);
  }

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC)
  primary_widget_observer_ = std::make_unique<PrimaryWidgetObserver>(*this);
#endif

  MaybeStartAutoCloseTimer();
}

HelpBubbleView::~HelpBubbleView() = default;

void HelpBubbleView::MaybeStartAutoCloseTimer() {
  if (timeout_.is_zero())
    return;

  auto_close_timer_.Start(FROM_HERE, timeout_, this,
                          &HelpBubbleView::OnTimeout);
}

void HelpBubbleView::OnTimeout() {
  // The callback could destroy the widget, so grab a weak pointer.
  base::WeakPtr<views::Widget> widget =
      GetWidget() ? GetWidget()->GetWeakPtr() : nullptr;
  if (timeout_callback_) {
    std::move(timeout_callback_).Run();
  }
  if (widget) {
    widget->Close();
  }
}

std::u16string HelpBubbleView::GetAccessibleWindowTitle() const {
  std::u16string result = accessible_name_;

  // If there's a keyboard navigation hint, append it after a full stop.
  if (!screenreader_hint_text_.empty() && activate_count_ <= 1)
    result += u". " + screenreader_hint_text_;

  return result;
}

void HelpBubbleView::OnWidgetActivationChanged(views::Widget* widget,
                                               bool active) {
  if (widget == GetWidget()) {
    if (active) {
      ++activate_count_;
      auto_close_timer_.Stop();
      widget->UpdateAccessibleNameForRootView();
    } else {
      MaybeStartAutoCloseTimer();
    }
  }
}

void HelpBubbleView::OnThemeChanged() {
  views::BubbleDialogDelegateView::OnThemeChanged();

  const auto* color_provider = GetColorProvider();
  const SkColor foreground_color =
      color_provider->GetColor(delegate_->GetHelpBubbleForegroundColorId());
  if (icon_view_) {
    icon_view_->SetBackground(views::CreateRoundedRectBackground(
        foreground_color, icon_view_->GetPreferredSize({}).height() / 2));
  }

  const SkColor background_color =
      color_provider->GetColor(delegate_->GetHelpBubbleBackgroundColorId());
  for (views::Label* label : labels_) {
    label->SetBackgroundColor(background_color);
    label->SetEnabledColor(foreground_color);
  }
}

gfx::Size HelpBubbleView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  const gfx::Size layout_manager_preferred_size =
      View::CalculatePreferredSize(available_size);

  // Wrap if the width is larger than |kBubbleMaxWidthDip|.
  if (layout_manager_preferred_size.width() > max_bubble_width_) {
    return gfx::Size(max_bubble_width_,
                     GetLayoutManager()->GetPreferredHeightForWidth(
                         this, max_bubble_width_));
  }

  if (layout_manager_preferred_size.width() < kMinWidthDip) {
    return gfx::Size(kMinWidthDip, layout_manager_preferred_size.height());
  }

  return layout_manager_preferred_size;
}

gfx::Rect HelpBubbleView::GetAnchorRect() const {
  gfx::Rect default_anchor_rect = BubbleDialogDelegateView::GetAnchorRect();
  if (!local_anchor_bounds_) {
    return default_anchor_rect;
  }

  // Ensure that we are not trying to clamp the anchor bounds to a completely
  // empty bounds.
  gfx::Size size = default_anchor_rect.size();
  size.SetToMax({1, 1});

  // Clamp the local bounds to the size of the anchor view.
  const int left = std::clamp(local_anchor_bounds_->x(), 0, size.width() - 1);
  const int right = std::clamp(local_anchor_bounds_->right(), 1, size.width());
  const int top = std::clamp(local_anchor_bounds_->y(), 0, size.height() - 1);
  const int bottom =
      std::clamp(local_anchor_bounds_->bottom(), 1, size.height());
  gfx::Rect result(left, top, right - left, bottom - top);

  // Translate back to screen coordinates.
  result.Offset(default_anchor_rect.OffsetFromOrigin());
  return result;
}

void HelpBubbleView::OnBeforeBubbleWidgetInit(views::Widget::InitParams* params,
                                              views::Widget* widget) const {
  BubbleDialogDelegateView::OnBeforeBubbleWidgetInit(params, widget);
#if BUILDFLAG(IS_LINUX)
  // Help bubbles anchored to menus may be clipped to their anchors' bounds,
  // resulting in visual errors, unless they use accelerated rendering. See
  // crbug.com/1445770 for details. This also applies to bubbles anchored to
  // all accelerated windows below a certain size, especially those which are
  // not top-level application windows (see crbug.com/340523110).
  //
  // Because it is not possible to know exactly if a bubble will correctly fit
  // in the bounds of its ancestor accelerator widget, due to things like
  // anchor positioning and the possibility that a window size could change,
  // make all Linux help bubbles accelerated.
  //
  // Note: accelerated widgets are "desktop native" widgets and interact with
  // the OS window activation system; this is, in turn, a problem for Linux
  // because of known technical limitations around window activation. See the
  // following bug for more information regarding window activation issues in
  // Weston, the windowing environment used on chrome's Wayland test-bots:
  // https://gitlab.freedesktop.org/wayland/weston/-/issues/669
  params->use_accelerated_widget_override = true;
#endif
}

// static
bool HelpBubbleView::IsHelpBubble(views::DialogDelegate* dialog) {
  auto* const contents = dialog->GetContentsView();
  return contents && views::IsViewClass<HelpBubbleView>(contents);
}

views::LabelButton* HelpBubbleView::GetDefaultButtonForTesting() const {
  return default_button_;
}

views::LabelButton* HelpBubbleView::GetNonDefaultButtonForTesting(
    int index) const {
  return non_default_buttons_[index];
}

void HelpBubbleView::SetForceAnchorRect(gfx::Rect force_anchor_rect) {
  force_anchor_rect.Offset(
      -views::BubbleDialogDelegateView::GetAnchorRect().OffsetFromOrigin());
  local_anchor_bounds_ = force_anchor_rect;
}

BEGIN_METADATA(HelpBubbleView)
END_METADATA

}  // namespace user_education
