// Copyright 2012 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/infobars/infobar_view.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/grit/generated_resources.h"
#include "components/infobars/core/infobar_delegate.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/text_constants.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.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/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

template <typename T>
void InfoBarView::AssignLabelDetails(T* view) const {
  view->SizeToPreferredSize();
  view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  view->SetProperty(
      views::kMarginsKey,
      gfx::Insets::VH(ChromeLayoutProvider::Get()->GetDistanceMetric(
                          DISTANCE_TOAST_LABEL_VERTICAL),
                      0));
}

template void InfoBarView::AssignLabelDetails(views::Label*) const;
template void InfoBarView::AssignLabelDetails(views::StyledLabel*) const;
template void InfoBarView::AssignLabelDetails(views::Link*) const;

// Helpers --------------------------------------------------------------------

namespace {
const int kInfobarIconSize = 24;
int GetElementSpacing() {
  return ChromeLayoutProvider::Get()->GetDistanceMetric(
      views::DISTANCE_UNRELATED_INFOBAR_CONTAINER_HORIZONTAL);
}

gfx::Insets GetCloseButtonSpacing() {
  auto* provider = ChromeLayoutProvider::Get();
  const gfx::Insets vector_button_insets =
      provider->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON);
  return gfx::Insets::VH(
             provider->GetDistanceMetric(DISTANCE_TOAST_CONTROL_VERTICAL),
             GetElementSpacing()) -
         vector_button_insets;
}

}  // namespace

// InfoBarView ----------------------------------------------------------------

DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(InfoBarView, kInfoBarElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(InfoBarView, kDismissButtonElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(InfoBarView, kLeftBalancerElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(InfoBarView, kRightSpacerElementId);

InfoBarView::InfoBarView(std::unique_ptr<infobars::InfoBarDelegate> delegate)
    : infobars::InfoBar(std::move(delegate)),
      views::ExternalFocusTracker(this, nullptr) {
  // Make Infobar animation aligned to the Compositor.
  SetNotifier(std::make_unique<
              gfx::AnimationDelegateNotifier<views::AnimationDelegateViews>>(
      this, this));

  set_owned_by_client(OwnedByClientPassKey());  // InfoBar deletes itself at the
                                                // appropriate time.

  // Clip child layers; without this, buttons won't look correct during
  // animation.
  SetPaintToLayer();
  layer()->SetMasksToBounds(true);

  const int kRefreshMargin = ChromeLayoutProvider::Get()->GetDistanceMetric(
      views::DISTANCE_UNRELATED_CONTROL_HORIZONTAL);

  const views::FlexSpecification kSpacerFlex =
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                               views::MaximumFlexSizeRule::kUnbounded)
          .WithWeight(1);

  const views::FlexSpecification kRigidFlex =
      views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                               views::MaximumFlexSizeRule::kPreferred)
          .WithWeight(0);

  auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
  layout->SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetInteriorMargin(gfx::Insets::VH(0, kRefreshMargin));

  // Add a balancer for elements for centered layout.
  left_balancer_ = AddChildView(std::make_unique<views::View>());
  left_balancer_->SetProperty(views::kElementIdentifierKey,
                              kLeftBalancerElementId);
  left_balancer_->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                               views::MaximumFlexSizeRule::kPreferred)
          .WithWeight(1));
  left_balancer_->SetPreferredSize(gfx::Size(0, 1));

  // Add a spacer for centered layout.
  auto* primary_space = AddChildView(std::make_unique<views::View>());
  primary_space->SetProperty(views::kFlexBehaviorKey, kSpacerFlex);

  const ui::ImageModel& image = this->delegate()->GetIcon();
  if (!image.IsEmpty()) {
    icon_ = new views::ImageView;
    icon_->SetImage(image);
    icon_->SetImageSize(gfx::Size(kInfobarIconSize, kInfobarIconSize));
    icon_->SizeToPreferredSize();
    icon_->SetProperty(
        views::kMarginsKey,
        gfx::Insets::VH(ChromeLayoutProvider::Get()->GetDistanceMetric(
                            DISTANCE_TOAST_LABEL_VERTICAL),
                        0));
    AddChildViewRaw(icon_.get());

    // Set the flex property for icon.
    // Add margin between icon and content for flex layout.
    icon_->SetProperty(
        views::kMarginsKey,
        std::make_unique<gfx::Insets>(gfx::Insets::TLBR(
            0, 0, 0,
            ChromeLayoutProvider::Get()->GetDistanceMetric(
                DISTANCE_INFOBAR_HORIZONTAL_ICON_LABEL_PADDING))));
  }

  // Create the content container for flex layout.
  content_container_ = AddChildView(std::make_unique<views::View>());
  content_container_->SetLayoutManager(std::make_unique<views::FlexLayout>());
  content_container_->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                               views::MaximumFlexSizeRule::kPreferred)
          .WithWeight(0));

  // Add the second spacer and the right-side container.
  right_spacer_ = AddChildView(std::make_unique<views::View>());
  right_spacer_->SetProperty(views::kElementIdentifierKey,
                             kRightSpacerElementId);
  right_spacer_->SetProperty(views::kFlexBehaviorKey, kSpacerFlex);

  // Create the container for right-aligned elements.
  right_side_container_ = AddChildView(std::make_unique<views::View>());
  auto* right_layout = right_side_container_->SetLayoutManager(
      std::make_unique<views::FlexLayout>());
  right_layout->SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetCrossAxisAlignment(views::LayoutAlignment::kEnd);
  right_side_container_->SetProperty(views::kFlexBehaviorKey, kRigidFlex);

  if (this->delegate()->IsCloseable()) {
    auto close_button = views::CreateVectorImageButton(base::BindRepeating(
        &InfoBarView::CloseButtonPressed, base::Unretained(this)));
    views::SetImageFromVectorIconWithColor(
        close_button.get(),
        features::IsRoundedIconsEnabled()
            ? vector_icons::kCloseIcon
            : vector_icons::kCloseChromeRefreshOldIcon,
        {kColorInfoBarButtonIcon, kColorInfoBarForeground,
         kColorInfoBarButtonIconHovered});
    close_button->SetTooltipText(l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
    close_button->SetProperty(views::kElementIdentifierKey,
                              kDismissButtonElementId);

    // Add to container if Refresh is enabled.
    if (right_side_container_) {
      close_button_ =
          right_side_container_->AddChildView(std::move(close_button));
      close_button_->SetProperty(views::kFlexBehaviorKey, kRigidFlex);
      close_button_->SetProperty(
          views::kMarginsKey, std::make_unique<gfx::Insets>(
                                  gfx::Insets::TLBR(0, kRefreshMargin, 0, 0)));
    }
    InstallCircleHighlightPathGenerator(close_button_);
  }
  SetTargetHeight(
      ChromeLayoutProvider::Get()->GetDistanceMetric(DISTANCE_INFOBAR_HEIGHT));

  GetViewAccessibility().SetRole(ax::mojom::Role::kAlertDialog);
  GetViewAccessibility().SetName(l10n_util::GetStringUTF8(IDS_ACCNAME_INFOBAR));
  GetViewAccessibility().SetKeyShortcuts("Alt+Shift+A");

  // Initial balancing.
  RecalculateLayoutBalancing();
}

InfoBarView::~InfoBarView() {
  // We should have closed any open menus in PlatformSpecificHide(), then
  // subclasses' RunMenu() functions should have prevented opening any new ones
  // once we became unowned.
  DCHECK(!menu_runner_.get());
}

void InfoBarView::RecalculateLayoutBalancing() {
  // We need both the balancer and the container to exist.
  if (!left_balancer_ || !right_side_container_) {
    return;
  }
  // Get the width of the container (this includes the close button + margins).
  const int right_side_width =
      right_side_container_->GetPreferredSize().width();

  // Make sure the left balancer matces the right side width, otherwise set the
  // size.
  if (left_balancer_->GetPreferredSize().width() != right_side_width) {
    left_balancer_->SetPreferredSize(gfx::Size(right_side_width, 1));

    // Trigger a re-layout so the spacers can adjust to the new balance.
    InvalidateLayout();
  }
}

void InfoBarView::Layout(PassKey) {
  if (GetLayoutManager()) {
    // Since we are using flex layout, simply return as no manual calculations
    // are needed for the layout.
    LayoutSuperclass<views::View>(this);
    return;
  }
}

gfx::Size InfoBarView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // With the refresh, the infobar has a fixed height and centers its content.
  // The preferred width is the width of the content.
  int width = 0;

  const int spacing = GetElementSpacing();
  if (icon_) {
    width += spacing + icon_->width();
  }

  const int content_width = GetContentPreferredWidth();
  if (content_width) {
    width += spacing + content_width;
  }

  const int trailing_space =
      close_button_ ? GetCloseButtonSpacing().width() + close_button_->width()
                    : GetElementSpacing();
  return gfx::Size(width + trailing_space, computed_height());
}

void InfoBarView::OnThemeChanged() {
  views::View::OnThemeChanged();
  const auto* cp = GetColorProvider();
  const SkColor background_theme_color = cp->GetColor(kColorInfoBarBackground);
  SetBackground(views::CreateSolidBackground(background_theme_color));

  const SkColor text_color = cp->GetColor(kColorInfoBarForeground);
  for (views::View* child : content_container_->children()) {
    auto* label = views::AsViewClass<views::Label>(child);
    if (label) {
      label->SetBackgroundColor(background_theme_color);
      if (!views::IsViewClass<views::Link>(child)) {
        label->SetEnabledColor(text_color);
        label->SetAutoColorReadabilityEnabled(false);
      }
    }
    auto* styled_label = views::AsViewClass<views::StyledLabel>(child);
    if (styled_label) {
      styled_label->SetDisplayedOnBackgroundColor(background_theme_color);
    }
  }

  // Set dark mode status so that it can be used to set a different icon image
  // that is more suitable for a dark background.
  delegate()->set_dark_mode(
      color_utils::IsDark(cp->GetColor(kColorInfoBarBackground)));
  if (icon_) {
    icon_->SetImage(delegate()->GetIcon());
  }
}

void InfoBarView::OnWillChangeFocus(View* focused_before, View* focused_now) {
  views::ExternalFocusTracker::OnWillChangeFocus(focused_before, focused_now);

  // This will trigger some screen readers to read the entire contents of this
  // infobar.
  if (focused_before && focused_now && !Contains(focused_before) &&
      Contains(focused_now)) {
    NotifyAccessibilityEventDeprecated(ax::mojom::Event::kAlert, true);
  }
}

std::unique_ptr<views::Label> InfoBarView::CreateLabel(
    const std::u16string& text) const {
  auto label = std::make_unique<views::Label>(
      text, views::style::CONTEXT_DIALOG_BODY_TEXT);
  AssignLabelDetails(label.get());
  return label;
}

std::unique_ptr<views::StyledLabel> InfoBarView::CreateStyledLabel(
    const std::u16string& text) const {
  auto label = std::make_unique<views::StyledLabel>();
  label->SetText(text);
  label->SetTextContext(views::style::CONTEXT_DIALOG_BODY_TEXT);
  AssignLabelDetails(label.get());
  return label;
}

std::unique_ptr<views::Link> InfoBarView::CreateLink(
    const std::u16string& text,
    const std::optional<std::u16string>& accessible_text) {
  auto link = std::make_unique<views::Link>(
      text, views::style::CONTEXT_DIALOG_BODY_TEXT);
  AssignLabelDetails(link.get());
  link->SetCallback(
      base::BindRepeating(&InfoBarView::LinkClicked, base::Unretained(this)));

  if (accessible_text.has_value()) {
    link->SetAccessibleName(accessible_text.value());
  }

  return link;
}

void InfoBarView::AddViewBeforeCloseButton(std::unique_ptr<views::View> view) {
  // We can call AddChildView and ReorderChildView here
  // because we are inside the InfoBarView class.
  if (close_button_) {
    if (!right_side_container_) {
      return;
    }
    std::optional<size_t> index =
        right_side_container_->GetIndexOf(close_button_);
    if (index.has_value()) {
      right_side_container_->AddChildViewAt(std::move(view), index.value());
      // Re-balance the layout to account for new view on the right side.
      RecalculateLayoutBalancing();
    }
  } else {
    right_side_container_->AddChildView(std::move(view));
    RecalculateLayoutBalancing();
  }
}

// static
void InfoBarView::AssignWidths(Views* views, int available_width) {
  // Sort by width decreasing.
  std::sort(views->begin(), views->end(),
            [](views::View* view_1, views::View* view_2) {
              return view_1->GetPreferredSize().width() >
                     view_2->GetPreferredSize().width();
            });
  AssignWidthsSorted(views, available_width);
}

int InfoBarView::GetContentMinimumWidth() const {
  return 0;
}

int InfoBarView::GetContentPreferredWidth() const {
  return 0;
}

int InfoBarView::GetStartX() const {
  // Ensure we don't return a value greater than GetEndX(), so children can
  // safely set something's width to "GetEndX() - GetStartX()" without risking
  // that being negative.
  return std::min((icon_ ? icon_->bounds().right() : 0) + GetElementSpacing(),
                  GetEndX());
}

int InfoBarView::GetEndX() const {
  return close_button_ ? close_button_->x() - GetCloseButtonSpacing().left()
                       : width() - GetElementSpacing();
}

int InfoBarView::OffsetY(views::View* view) const {
  return std::max((target_height() - view->height()) / 2, 0) -
         (target_height() - height());
}

void InfoBarView::PlatformSpecificShow(bool animate) {
  // If we gain focus, we want to restore it to the previously-focused element
  // when we're hidden. So when we're in a Widget, create a focus tracker so
  // that if we gain focus we'll know what the previously-focused element was.
  SetFocusManager(GetFocusManager());

  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kAlert, true);
}

void InfoBarView::PlatformSpecificHide(bool animate) {
  // Cancel any menus we may have open.  It doesn't make sense to leave them
  // open while we're hidden, and if we're going to become unowned, we can't
  // allow the user to choose any options and potentially call functions that
  // try to access the owner.
  menu_runner_.reset();

  // It's possible to be called twice (once with |animate| true and once with it
  // false); in this case the second SetFocusManager() call will silently no-op.
  SetFocusManager(nullptr);

  if (!animate) {
    return;
  }

  // Do not restore focus (and active state with it) if some other top-level
  // window became active.
  views::Widget* widget = GetWidget();
  if (!widget || widget->IsActive()) {
    FocusLastFocusedExternalView();
  }
}

void InfoBarView::PlatformSpecificOnHeightRecalculated() {
  // Ensure that notifying our container of our size change will result in a
  // re-layout.
  InvalidateLayout();
}

// static
void InfoBarView::AssignWidthsSorted(Views* views, int available_width) {
  if (views->empty()) {
    return;
  }
  gfx::Size back_view_size(views->back()->GetPreferredSize());
  back_view_size.set_width(
      std::min(back_view_size.width(),
               available_width / static_cast<int>(views->size())));
  views->back()->SetSize(back_view_size);
  views->pop_back();
  AssignWidthsSorted(views, available_width - back_view_size.width());
}

void InfoBarView::SetInteriorMargin(const gfx::Insets& margin) {
  static_cast<views::FlexLayout*>(GetLayoutManager())
      ->SetInteriorMargin(margin);
}

void InfoBarView::LinkClicked(const ui::Event& event) {
  if (!owner()) {
    return;  // We're closing; don't call anything, it might access the owner.
  }
  if (delegate()->LinkClicked(ui::DispositionFromEventFlags(event.flags()))) {
    RemoveSelf();
  }
}

void InfoBarView::CloseButtonPressed() {
  if (!owner()) {
    return;  // We're closing; don't call anything, it might access the owner.
  }
  delegate()->InfoBarDismissed();
  RemoveSelf();
}

BEGIN_METADATA(InfoBarView)
ADD_READONLY_PROPERTY_METADATA(int, ContentMinimumWidth)
ADD_READONLY_PROPERTY_METADATA(int, StartX)
ADD_READONLY_PROPERTY_METADATA(int, EndX)
END_METADATA
