// 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/bookmarks/bookmark_button.h"

#include <string_view>

#include "base/metrics/histogram_functions.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/predictors/loading_predictor_config.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/preloading/bookmarkbar_preload/bookmarkbar_preload_pipeline_manager.h"
#include "chrome/browser/preloading/chrome_preloading.h"
#include "chrome/browser/preloading/prerender/prerender_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_button_util.h"
#include "chrome/browser/ui/views/event_utils.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/preloading_data.h"
#include "content/public/browser/web_contents.h"
#include "services/network/public/cpp/constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class PreloadBookmarkMetricsEvent {
  kMouseOver = 0,
  kMouseDown = 1,
  kMouseClick = 2,
  kMaxValue = kMouseClick,
};

// These are used as control the behavior of kBookmarkTriggerForPrerender2.
const base::FeatureParam<int> kPreconnectStartDelayOnMouseHoverByMilliseconds{
    &features::kBookmarkTriggerForPreconnect,
    "preconnect_start_delay_on_mouse_hover_ms", 100};
const base::FeatureParam<int> kPrefetchStartDelayOnMouseHoverByMilliseconds{
    &features::kBookmarkTriggerForPrefetch,
    "prefetch_start_delay_on_mouse_hover_ms", 300};

// BookmarkButtonBase -----------------------------------------------

// Base class for non-menu hosting buttons used on the bookmark bar.

BookmarkButtonBase::BookmarkButtonBase(PressedCallback callback,
                                       std::u16string_view title)
    : LabelButton(std::move(callback), title) {
  ConfigureInkDrop(this);

  SetImageLabelSpacing(
      GetLayoutConstant(LayoutConstant::kBookmarkBarButtonImageLabelPadding));

  views::InstallPillHighlightPathGenerator(this);

  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);

  show_animation_ = std::make_unique<gfx::SlideAnimation>(this);
  if (!BookmarkBarView::GetAnimationsEnabled()) {
    // For some reason during testing the events generated by animating
    // throw off the test. So, don't animate while testing.
    show_animation_->Reset(1);
  } else {
    show_animation_->Show();
  }
  GetViewAccessibility().SetRoleDescription(
      l10n_util::GetStringUTF8(IDS_ACCNAME_BOOKMARK_BUTTON_ROLE_DESCRIPTION));
}

BookmarkButtonBase::~BookmarkButtonBase() = default;

views::View* BookmarkButtonBase::GetTooltipHandlerForPoint(
    const gfx::Point& point) {
  return HitTestPoint(point) && GetCanProcessEventsWithinSubtree() ? this
                                                                   : nullptr;
}

bool BookmarkButtonBase::IsTriggerableEvent(const ui::Event& e) {
  return e.type() == ui::EventType::kGestureTap ||
         e.type() == ui::EventType::kGestureTapDown ||
         event_utils::IsPossibleDispositionEvent(e);
}

std::unique_ptr<views::LabelButtonBorder>
BookmarkButtonBase::CreateDefaultBorder() const {
  return bookmark_button_util::CreateBookmarkButtonBorder();
}

BEGIN_METADATA(BookmarkButtonBase)
END_METADATA
// BookmarkButton -------------------------------------------------------------

// Buttons used for the bookmarks on the bookmark bar.
BookmarkButton::BookmarkButton(PressedCallback callback,
                               const GURL& url,
                               std::u16string_view title,
                               const raw_ptr<BrowserWindowInterface> browser)
    : BookmarkButtonBase(base::BindRepeating(&BookmarkButton::OnButtonPressed,
                                             base::Unretained(this)),
                         title),
      callback_(std::move(callback)),
      url_(url),
      browser_(browser) {}

BookmarkButton::~BookmarkButton() = default;

void BookmarkButton::OnButtonPressed(const ui::Event& event) {
  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPrefetch) &&
      browser_) {
    browser_->GetProfile()->GetPrefs()->SetInt64(
        prefs::kBookmarkBarNavigationCount,
        browser_->GetProfile()->GetPrefs()->GetInt64(
            prefs::kBookmarkBarNavigationCount) +
            1);
  }
  callback_.Run(event);
}

void BookmarkButton::AddedToWidget() {
  BookmarkButtonBase::AddedToWidget();

  widget_observation_.Observe(GetWidget());

  UpdateMaxTooltipWidth();
}

void BookmarkButton::RemovedFromWidget() {
  BookmarkButtonBase::RemovedFromWidget();

  widget_observation_.Reset();
}

void BookmarkButton::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  BookmarkButtonBase::OnBoundsChanged(previous_bounds);
  UpdateMaxTooltipWidth();
}

std::u16string BookmarkButton::GetRenderedTooltipText(
    const gfx::Point& p) const {
  if (tooltip_text_needs_update_) {
    const_cast<BookmarkButton*>(this)->MaybeUpdateTooltipText();
  }
  return GetTooltipText();
}

void BookmarkButton::AdjustAccessibleName(std::u16string& new_name,
                                          ax::mojom::NameFrom& name_from) {
  if (new_name.empty()) {
    new_name = l10n_util::GetStringFUTF16(
        IDS_UNNAMED_BOOKMARK_BUTTON_ACCESSIBLE_NAME,
        url_formatter::FormatUrl(
            url_.get(), url_formatter::kFormatUrlOmitDefaults,
            base::UnescapeRule::NORMAL, nullptr, nullptr, nullptr));
    name_from = ax::mojom::NameFrom::kContents;
  }
}

void BookmarkButton::SetText(std::u16string_view text) {
  BookmarkButtonBase::SetText(text);
  tooltip_text_needs_update_ = true;
}

void BookmarkButton::MaybeUpdateTooltipText() {
  if (!tooltip_text_needs_update_) {
    return;
  }
  tooltip_text_needs_update_ = false;
  if (!GetWidget()) {
    return;
  }
  const views::TooltipManager* tooltip_manager =
      GetWidget()->GetTooltipManager();
  if (tooltip_manager) {
    SetTooltipText(BookmarkBarView::CreateToolTipForURLAndTitle(
        max_tooltip_width_, tooltip_manager->GetFontList(), *url_, GetText()));
  }
}

void BookmarkButton::OnMouseEntered(const ui::MouseEvent& event) {
  // Compute the tooltip text before it's queried by the tooltip manager.
  MaybeUpdateTooltipText();

  // Reset source information for taking metrics for following mouse events.

  BookmarkButtonBase::OnMouseEntered(event);

  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPrefetch) &&
      browser_) {
    browser_->GetProfile()->GetPrefs()->SetInt64(
        prefs::kBookmarkBarHoverCount,
        browser_->GetProfile()->GetPrefs()->GetInt64(
            prefs::kBookmarkBarHoverCount) +
            1);
  }

  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPreconnect)) {
    preconnect_timer_.Start(
        FROM_HERE,
        base::Milliseconds(
            kPreconnectStartDelayOnMouseHoverByMilliseconds.Get()),
        base::BindRepeating(&BookmarkButton::StartPreconnecting,
                            base::Unretained(this), *url_));
  }

  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPrefetch)) {
    prefetch_timer_.Start(
        FROM_HERE,
        base::Milliseconds(kPrefetchStartDelayOnMouseHoverByMilliseconds.Get()),
        base::BindRepeating(&BookmarkButton::StartPreloading,
                            base::Unretained(this), *url_,
                            content::PreloadingType::kPrefetch));
  }

  // Now we should register the callback function that will be used to
  // compute the preloading recall.
  if (auto* web_contents =
          browser_->GetTabStripModel()->GetActiveWebContents()) {
    content::PreloadingData* preloading_data =
        content::PreloadingData::GetOrCreateForWebContents(web_contents);
    preloading_data->SetIsNavigationInDomainCallback(
        chrome_preloading_predictor::kMouseHoverOrMouseDownOnBookmarkBar,
        base::BindRepeating(
            [](content::NavigationHandle* navigation_handle) -> bool {
              return ui::PageTransitionCoreTypeIs(
                         navigation_handle->GetPageTransition(),
                         ui::PAGE_TRANSITION_AUTO_BOOKMARK) &&
                     ui::PageTransitionIsNewNavigation(
                         navigation_handle->GetPageTransition());
            }));
  }
}

void BookmarkButton::StopPreloadingTimers() {
  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPreconnect)) {
    preconnect_timer_.Stop();
  }

  if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPrefetch)) {
    prefetch_timer_.Stop();
  }
}

void BookmarkButton::OnMouseExited(const ui::MouseEvent& event) {
  BookmarkButtonBase::OnMouseExited(event);
  StopPreloadingTimers();

  if (!GetBookmarkBarPreloadPipelineManager()) {
    return;
  }

  GetBookmarkBarPreloadPipelineManager()->ResetPrerender();
}

bool BookmarkButton::OnMousePressed(const ui::MouseEvent& event) {
  bool result = BookmarkButtonBase::OnMousePressed(event);
  if (GetState() == ButtonState::STATE_PRESSED) {
    base::UmaHistogramEnumeration("Prerender.Experimental.BookmarkMetrics",
                                  PreloadBookmarkMetricsEvent::kMouseDown);
  }

  if (event.IsOnlyLeftMouseButton()) {
    // Stopping preloading timers to avoid starting preconnect/prefetch after
    // prerender has been started.
    StopPreloadingTimers();
    // Starting prefetch before prerender. With
    // `UsePrefetchPrerenderIntegration()` being true, preloading can fall back
    // to prefetch if prerender fails. If a prefetch has been started, this
    // function call will do nothing.
    if (base::FeatureList::IsEnabled(features::kBookmarkTriggerForPrefetch)) {
      StartPreloading(*url_, content::PreloadingType::kPrefetch);
    }
    // Starting prerender. If a prerender has been started, this function call
    // will do nothing.
    StartPreloading(*url_, content::PreloadingType::kPrerender);
  }
  return result;
}

void BookmarkButton::OnMouseMoved(const ui::MouseEvent& event) {
  return BookmarkButtonBase::OnMouseMoved(event);
}

void BookmarkButton::OnWidgetBoundsChanged(views::Widget* widget,
                                           const gfx::Rect& new_bounds) {
  UpdateMaxTooltipWidth();
}

void BookmarkButton::StartPreconnecting(GURL url) {
  CHECK(base::FeatureList::IsEnabled(features::kBookmarkTriggerForPreconnect));
  // TODO(crbug.com/413259638): Introduce preconnect related tests once the
  // related infrastructure is completed.
  if (!GetBookmarkBarPreloadPipelineManager() ||
      GetBookmarkBarPreloadPipelineManager()->IsPreloadingStarted()) {
    return;
  }

  auto* loading_predictor = predictors::LoadingPredictorFactory::GetForProfile(
      browser_->GetProfile());
  if (loading_predictor) {
    loading_predictor->PrepareForPageLoad(
        /*initiator_origin=*/std::nullopt, url,
        predictors::HintOrigin::BOOKMARK_BAR,
        network::GetNoOpNetworkRestrictionsId(), true);
  }
}

void BookmarkButton::StartPreloading(const GURL& url,
                                     content::PreloadingType preloadingType) {
  if (!GetBookmarkBarPreloadPipelineManager()) {
    return;
  }

  switch (preloadingType) {
    case content::PreloadingType::kPrerender:
      GetBookmarkBarPreloadPipelineManager()->StartPrerender(url);
      break;
    case content::PreloadingType::kPrefetch:
      GetBookmarkBarPreloadPipelineManager()->StartPrefetch(url);
      break;
    case content::PreloadingType::kUnspecified:
    case content::PreloadingType::kPreconnect:
    case content::PreloadingType::kNoStatePrefetch:
    case content::PreloadingType::kPrerenderUntilScript:
      NOTREACHED();
  }
}

void BookmarkButton::UpdateMaxTooltipWidth() {
  if (!GetWidget()) {
    return;
  }

  const views::TooltipManager* tooltip_manager =
      GetWidget()->GetTooltipManager();
  gfx::Point p;
  ConvertPointToScreen(this, &p);
  int max_tooltip_width = tooltip_manager->GetMaxWidth(p);
  if (max_tooltip_width != max_tooltip_width_) {
    max_tooltip_width_ = max_tooltip_width;
    tooltip_text_needs_update_ = true;
  }
}

BookmarkBarPreloadPipelineManager*
BookmarkButton::GetBookmarkBarPreloadPipelineManager() {
  tabs::TabInterface* active_tab = browser_->GetTabStripModel()->GetActiveTab();
  // TODO(crbug.com/413259638): active_tab is only expected to be null if the
  // tab_strip is being initialized or destroyed, but putting a CHECK had caused
  // crbug.com/448228076.
  if (!active_tab) {
    return nullptr;
  }
  return active_tab->GetTabFeatures()->bookmarkbar_preload_pipeline_manager();
}

BEGIN_METADATA(BookmarkButton)
END_METADATA
