// 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/location_bar/zoom_bubble_view.h"

#include <cmath>
#include <memory>
#include <optional>

#include "base/auto_reset.h"
#include "base/check.h"
#include "base/i18n/number_formatting.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/location_bar/zoom_bubble_manager.h"
#include "chrome/browser/ui/views/zoom/zoom_view_controller.h"
#include "chrome/grit/generated_resources.h"
#include "components/vector_icons/vector_icons.h"
#include "components/zoom/page_zoom.h"
#include "components/zoom/zoom_controller.h"
#include "extensions/browser/extension_zoom_request_client.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/skia/include/core/SkPath.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/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

namespace {

// The default time that the bubble should stay on the screen if it will close
// automatically.
constexpr base::TimeDelta kBubbleCloseDelayDefault = base::Milliseconds(1500);

// A longer timeout used for how long the bubble should stay on the screen
// before it will close automatically after + or - buttons have been used.
constexpr base::TimeDelta kBubbleCloseDelayLong = base::Milliseconds(5000);

class ZoomButtonHighlightPathGenerator : public views::HighlightPathGenerator {
 public:
  ZoomButtonHighlightPathGenerator() = default;

  SkPath GetHighlightPath(const views::View* view) override {
    constexpr int kCircleRadiusDp = 24 / 2;
    const gfx::Point center = view->GetLocalBounds().CenterPoint();
    return SkPath::Circle(center.x(), center.y(), kCircleRadiusDp);
  }
};

std::unique_ptr<views::ImageButton> CreateZoomButton(
    views::Button::PressedCallback callback,
    const gfx::VectorIcon& icon,
    int tooltip_id,
    std::optional<int> icon_size = std::nullopt) {
  auto zoom_button = views::CreateVectorImageButtonWithNativeTheme(
      std::move(callback), icon, icon_size);
  zoom_button->SetTooltipText(l10n_util::GetStringUTF16(tooltip_id));
  views::HighlightPathGenerator::Install(
      zoom_button.get(), std::make_unique<ZoomButtonHighlightPathGenerator>());
  return zoom_button;
}

class ZoomValue : public views::Label {
  METADATA_HEADER(ZoomValue, views::Label)

 public:
  explicit ZoomValue(const content::WebContents* web_contents)
      : Label(std::u16string(),
              views::style::CONTEXT_LABEL,
              views::style::STYLE_PRIMARY),
        max_width_(GetLabelMaxWidth(web_contents)) {
    SetHorizontalAlignment(gfx::ALIGN_LEFT);
  }
  ZoomValue(const ZoomValue&) = delete;
  ZoomValue& operator=(const ZoomValue&) = delete;
  ~ZoomValue() override = default;

  // views::Label:
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    gfx::Size size =
        views::Label::CalculatePreferredSize(views::SizeBounds(max_width_, {}));
    // When the initial value of the text width is small(eg: 80%), the
    // `ZoomBubbleView` will be smaller. Then after we use a larger value(eg:
    // 200%), the text will not be fully displayed. It needs to be set to the
    // maximum value to ensure that the size of `ZoomBubbleView` is fixed.
    size.set_width(max_width_);
    return size;
  }

 private:
  int GetLabelMaxWidth(const content::WebContents* web_contents) const {
    const int border_width = GetInsets().width();
    int max_w = 0;
    auto* zoom_controller = zoom::ZoomController::FromWebContents(web_contents);
    DCHECK(zoom_controller);
    // Enumerate all zoom factors that can be used in PageZoom::Zoom.
    std::vector<double> zoom_factors =
        zoom::PageZoom::PresetZoomFactors(zoom_controller->GetZoomPercent());
    for (auto zoom : zoom_factors) {
      int w = gfx::GetStringWidth(
          base::FormatPercent(static_cast<int>(std::round(zoom * 100))),
          font_list());
      max_w = std::max(w, max_w);
    }
    return max_w + border_width;
  }

  const int max_width_;
};

BEGIN_METADATA(ZoomValue)
END_METADATA

}  // namespace

void ZoomBubbleView::Refresh() {
  UpdateZoomPercent();
  StartTimerIfNecessary();
}

ZoomBubbleView::ZoomBubbleView(BrowserWindowInterface* browser,
                               ZoomBubbleManager* manager,
                               views::BubbleAnchor anchor,
                               content::WebContents* web_contents,
                               DisplayReason reason)
    : LocationBarBubbleDelegateView(anchor, web_contents),
      browser_(browser),
      manager_(manager),
      auto_close_duration_(kBubbleCloseDelayDefault),
      auto_close_(reason == AUTOMATIC) {
  SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
  SetNotifyEnterExitOnChild(true);
  UseCompactMargins();
}

ZoomBubbleView::~ZoomBubbleView() = default;

std::u16string ZoomBubbleView::GetAccessibleWindowTitle() const {
  return manager_->GetZoomActionAccessibleName();
}

void ZoomBubbleView::OnFocus() {
  LocationBarBubbleDelegateView::OnFocus();
  StopTimer();
}

void ZoomBubbleView::OnBlur() {
  LocationBarBubbleDelegateView::OnBlur();

  const views::FocusManager* focus_manager = GetFocusManager();
  if (focus_manager && Contains(focus_manager->GetFocusedView())) {
    return;
  }

  StartTimerIfNecessary();
}

void ZoomBubbleView::OnGestureEvent(ui::GestureEvent* event) {
  if (!auto_close_ || event->type() != ui::EventType::kGestureTap) {
    return;
  }

  auto_close_ = false;
  StopTimer();
  event->SetHandled();
}

void ZoomBubbleView::OnKeyEvent(ui::KeyEvent* event) {
  if (!auto_close_) {
    return;
  }

  const views::FocusManager* focus_manager = GetFocusManager();
  if (focus_manager && Contains(focus_manager->GetFocusedView())) {
    StopTimer();
  } else {
    StartTimerIfNecessary();
  }
}

void ZoomBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
  StopTimer();
}

void ZoomBubbleView::OnMouseExited(const ui::MouseEvent& event) {
  StartTimerIfNecessary();
}

void ZoomBubbleView::Init() {
  // Set up the layout of the zoom bubble.
  constexpr int kPercentLabelPadding = 64;
  const ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
  const int spacing =
      provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_HORIZONTAL);
  gfx::Insets inset_border_insets =
      provider->GetInsetsMetric(INSETS_TOAST) - margins();
  inset_border_insets.set_top_bottom(0, 0);
  auto box_layout = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, inset_border_insets, spacing);
  box_layout->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
  box_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  SetLayoutManager(std::move(box_layout));

  // Calculate child views margins in |this| client view.
  const int label_vertical_spacing =
      provider->GetDistanceMetric(DISTANCE_TOAST_LABEL_VERTICAL);
  const auto label_margin =
      gfx::Insets::TLBR(label_vertical_spacing - margins().top(), 0,
                        label_vertical_spacing - margins().bottom(),
                        kPercentLabelPadding - spacing);

  // Account for the apparent margins that vector buttons have around icons.
  const int control_vertical_spacing =
      provider->GetDistanceMetric(DISTANCE_TOAST_CONTROL_VERTICAL);
  const auto control_vertical_margin =
      gfx::Insets::TLBR(control_vertical_spacing - margins().top(), 0,
                        control_vertical_spacing - margins().bottom(), 0);
  const gfx::Insets vector_button_margin(
      control_vertical_margin -
      provider->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON));

  const auto button_pressed_callback = [this](base::RepeatingClosure closure) {
    return base::BindRepeating(&ZoomBubbleView::ButtonPressed,
                               base::Unretained(this), std::move(closure));
  };

  // If this zoom change was initiated by an extension, that extension will be
  // attributed by showing its icon in the zoom bubble.
  if (extension_info_.icon_image) {
    auto image_button = std::make_unique<views::ImageButton>(
        button_pressed_callback(base::BindRepeating(
            &ZoomBubbleView::ImageButtonPressed, base::Unretained(this))));
    image_button->SetTooltipText(
        l10n_util::GetStringFUTF16(IDS_TOOLTIP_ZOOM_EXTENSION_ICON,
                                   base::UTF8ToUTF16(extension_info_.name)));
    image_button->SetImageModel(views::Button::STATE_NORMAL,
                                ui::ImageModel::FromImageSkia(
                                    extension_info_.icon_image->image_skia()));
    image_button_ = AddChildView(std::move(image_button));
  }

  // Add zoom label with the new zoom percent.
  auto label = std::make_unique<ZoomValue>(web_contents());
  label->SetProperty(views::kMarginsKey, gfx::Insets(label_margin));
  label->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
  label_ = label.get();
  AddChildView(std::move(label));

  const auto zoom_callback = [button_pressed_callback,
                              web_contents =
                                  web_contents()](content::PageZoom zoom) {
    return button_pressed_callback(base::BindRepeating(
        &zoom::PageZoom::Zoom, base::Unretained(web_contents), zoom));
  };

  // Add Zoom Out ("-") button.
  const int zoom_out_icon_size = 20;
  zoom_out_button_ = AddChildView(CreateZoomButton(
      zoom_callback(content::PAGE_ZOOM_OUT),
      features::IsRoundedIconsEnabled() ? kRemoveIcon : kRemoveOldIcon,
      IDS_ACCNAME_ZOOM_MINUS2,
      features::IsRoundedIconsEnabled() ? std::make_optional(zoom_out_icon_size)
                                        : std::nullopt));
  zoom_out_button_->SetProperty(views::kMarginsKey,
                                gfx::Insets(vector_button_margin));

  // Add Zoom In ("+") button.
  zoom_in_button_ = AddChildView(CreateZoomButton(
      zoom_callback(content::PAGE_ZOOM_IN),
      features::IsRoundedIconsEnabled() ? vector_icons::kAdd2Icon : kAddOldIcon,
      IDS_ACCNAME_ZOOM_PLUS2));
  zoom_in_button_->SetProperty(views::kMarginsKey,
                               gfx::Insets(vector_button_margin));

  // Add "Reset" button.
  auto reset_button = std::make_unique<views::MdTextButton>(
      zoom_callback(content::PAGE_ZOOM_RESET),
      l10n_util::GetStringUTF16(IDS_ZOOM_SET_DEFAULT));
  reset_button->SetTooltipText(
      l10n_util::GetStringUTF16(IDS_ACCNAME_ZOOM_SET_DEFAULT));
  reset_button_ = AddChildView(std::move(reset_button));

  UpdateZoomPercent();
  StartTimerIfNecessary();
}


void ZoomBubbleView::OnExtensionIconImageChanged(
    extensions::IconImage* /* image */) {
  image_button_->SetImageModel(
      views::Button::STATE_NORMAL,
      ui::ImageModel::FromImageSkia(extension_info_.icon_image->image_skia()));
  image_button_->SchedulePaint();
}

void ZoomBubbleView::SetExtensionInfo(const extensions::Extension* extension) {
  DCHECK(extension);
  extension_info_.id = extension->id();
  extension_info_.name = extension->name();

  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  const gfx::ImageSkia& default_extension_icon_image =
      *rb.GetImageSkiaNamed(IDR_EXTENSIONS_FAVICON);
  int icon_size = gfx::kFaviconSize;

  // We give first preference to an icon from the extension's icon set that
  // matches the size of the default. But not all extensions will declare an
  // icon set, or may not have an icon of the default size (we don't want the
  // bubble to display, for example, a very large icon). In that case, if there
  // is an action icon (size-16) this is an acceptable alternative.
  const ExtensionIconSet* icons = &extensions::IconsInfo::GetIcons(extension);
  bool has_default_sized_icon =
      !icons->Get(gfx::kFaviconSize, ExtensionIconSet::Match::kExactly).empty();

  if (!has_default_sized_icon) {
    const extensions::ActionInfo* action =
        extensions::ActionInfo::GetExtensionActionInfo(extension);
    if (!action || action->default_icon.empty()) {
      return;  // Out of options.
    }

    icons = &action->default_icon;
    icon_size = icons->map().begin()->first;
  }

  extension_info_.icon_image = std::make_unique<extensions::IconImage>(
      web_contents()->GetBrowserContext(), extension, *icons, icon_size,
      default_extension_icon_image, this);
}

void ZoomBubbleView::UpdateZoomPercent() {
  label_->SetText(base::FormatPercent(
      zoom::ZoomController::FromWebContents(web_contents())->GetZoomPercent()));
  label_->GetViewAccessibility().SetName(GetAccessibleWindowTitle());
  label_->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kAlert, true);

  // Disable buttons at min, max and default
  auto* zoom_controller = zoom::ZoomController::FromWebContents(web_contents());
  double current_zoom_level = zoom_controller->GetZoomLevel();
  double default_zoom_level = zoom_controller->GetDefaultZoomLevel();
  std::vector<double> zoom_levels =
      zoom::PageZoom::PresetZoomLevels(default_zoom_level);
  CHECK(zoom_out_button_);
  zoom_out_button_->SetEnabled(
      !blink::ZoomValuesEqual(zoom_levels.front(), current_zoom_level));
  CHECK(zoom_in_button_);
  zoom_in_button_->SetEnabled(
      !blink::ZoomValuesEqual(zoom_levels.back(), current_zoom_level));
  CHECK(reset_button_);
  reset_button_->SetEnabled(
      !blink::ZoomValuesEqual(current_zoom_level, default_zoom_level));
}

void ZoomBubbleView::StartTimerIfNecessary() {
  if (!auto_close_) {
    return;
  }

  auto_close_timer_.Start(
      FROM_HERE, auto_close_duration_,
      base::BindOnce(&ZoomBubbleView::Close, base::Unretained(this)));
}

void ZoomBubbleView::Close() {
  if (GetWidget()) {
    GetWidget()->Close();
  }
}

void ZoomBubbleView::StopTimer() {
  auto_close_timer_.Stop();
}

void ZoomBubbleView::ButtonPressed(base::RepeatingClosure closure) {
  // No button presses in this dialog should cause the dialog to close,
  // including when the zoom level is set to 100% as a result of a button press.
  base::AutoReset<bool> auto_ignore_close_bubble(&ignore_close_bubble_, true);

  // Extend the timer to give a user more time after any button is pressed.
  auto_close_duration_ = kBubbleCloseDelayLong;
  StartTimerIfNecessary();

  closure.Run();
}

void ZoomBubbleView::ImageButtonPressed() {
  DCHECK(extension_info_.icon_image) << "Invalid button press.";
  chrome::AddSelectedTabWithURL(
      browser_,
      GURL(base::StringPrintf("chrome://extensions?id=%s",
                              extension_info_.id.c_str())),
      ui::PAGE_TRANSITION_FROM_API);
}

std::u16string_view ZoomBubbleView::GetLabelForTesting() const {
  return label_->GetText();
}

base::OneShotTimer* ZoomBubbleView::GetAutoCloseTimerForTesting() {
  return &auto_close_timer_;
}
views::Button* ZoomBubbleView::GetResetButtonForTesting() {
  return reset_button_;
}

views::Button* ZoomBubbleView::GetZoomInButtonForTesting() {
  return zoom_in_button_;
}

void ZoomBubbleView::OnKeyEventForTesting(ui::KeyEvent* event) {
  OnKeyEvent(event);
}

ZoomBubbleView::ZoomBubbleExtensionInfo::ZoomBubbleExtensionInfo() = default;

ZoomBubbleView::ZoomBubbleExtensionInfo::~ZoomBubbleExtensionInfo() = default;

BEGIN_METADATA(ZoomBubbleView)
END_METADATA
