// Copyright 2021 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/tabs/hovercard/tab_hover_card_controller.h"

#include <algorithm>
#include <optional>

#include "base/callback_list.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_widget_sublevel.h"
#include "chrome/browser/ui/views/frame/vertical_tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/hovercard/hover_card_anchor_target.h"
#include "chrome/browser/ui/views/tabs/hovercard/tab_hover_card_bubble_view.h"
#include "chrome/browser/ui/views/tabs/hovercard/tab_hover_card_thumbnail_observer.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "ui/events/event.h"
#include "ui/events/event_observer.h"
#include "ui/events/types/event_type.h"
#include "ui/views/event_monitor.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace {

base::TimeDelta GetPreviewImageCaptureDelay(
    ThumbnailImage::CaptureReadiness readiness) {
  int ms = 0;
  switch (readiness) {
    case ThumbnailImage::CaptureReadiness::kNotReady: {
      static const int not_ready_delay = base::GetFieldTrialParamByFeatureAsInt(
          features::kTabHoverCardImages,
          features::kTabHoverCardImagesNotReadyDelayParameterName, 800);
      ms = not_ready_delay;
      break;
    }
    case ThumbnailImage::CaptureReadiness::kReadyForInitialCapture: {
      static const int loading_delay = base::GetFieldTrialParamByFeatureAsInt(
          features::kTabHoverCardImages,
          features::kTabHoverCardImagesLoadingDelayParameterName, 300);
      ms = loading_delay;
      break;
    }
    case ThumbnailImage::CaptureReadiness::kReadyForFinalCapture: {
      static const int loaded_delay = base::GetFieldTrialParamByFeatureAsInt(
          features::kTabHoverCardImages,
          features::kTabHoverCardImagesLoadedDelayParameterName, 300);
      ms = loaded_delay;
      break;
    }
  }
  DCHECK_GE(ms, 0);
  return base::Milliseconds(ms);
}

base::TimeDelta GetShowDelay(BrowserWindowInterface* browser,
                             TabStripRegionView* tab_strip,
                             HoverCardAnchorTarget* anchor_target) {
  static const int max_width_additional_delay =
      base::GetFieldTrialParamByFeatureAsInt(
          features::kTabHoverCardImages,
          features::kTabHoverCardAdditionalMaxWidthDelay, 500);

  // Delay is calculated as a logarithmic scale and bounded by a minimum width
  // based on the width of a pinned tab and a maximum of the standard width.
  // Once we reach standard width for the tab, we add an additional
  // `max_width_additional_delay` delay to the computed delay value since the
  // standard width should provide enough information of the tab reducing the
  // overall value provided by the hovercard.
  //
  //  delay (ms)
  //           |
  // max delay-|                                    *
  //           |                          *
  //           |                    *
  //           |                *
  //           |            *
  //           |         *
  //           |       *
  //           |     *
  //           |    *
  // min delay-|****
  //           |___________________________________________ tab width
  //               |                                |
  //       minimum tab width               standard tab width
  int tab_width = 0, tab_min_width = 0, tab_standard_width = 0;
  auto* vertical_tab_strip_state_controller =
      tabs::VerticalTabStripStateController::From(browser);
  if (vertical_tab_strip_state_controller &&
      vertical_tab_strip_state_controller->ShouldDisplayVerticalTabs()) {
    // For vertical tabs, we use tab strip width instead of tab widths. The
    // minimum width is the collapsed state while the standard width is tied to
    // the default uncollapsed width.
    tab_width = tab_strip->width();

    tab_min_width = VerticalTabStripRegionView::kCollapsedWidth;
    tab_standard_width = tabs::kVerticalTabStripDefaultUncollapsedWidth;
  } else {
    // Use the largest tab in the tab strip when determining the delay so that
    // the delay is consistent for all tabs within the tab strip.
    tab_width = anchor_target->GetView()->width();
    for (int i = 0; i < browser->GetTabStripModel()->count(); i++) {
      tabs::TabInterface* tab = browser->GetTabStripModel()->GetTabAtIndex(i);
      if (!tab) {
        continue;
      }
      views::View* tab_anchor = tab_strip->GetTabAnchorView(tab->GetHandle());
      if (tab_anchor) {
        tab_width = std::max(tab_width, tab_anchor->width());
      }
    }

    const TabStyle* tab_style = TabStyle::Get();
    tab_min_width = tab_style->GetPinnedWidth(/*is_split=*/false);
    tab_standard_width = tab_style->GetStandardWidth(/*is_split=*/false);
  }

  constexpr base::TimeDelta kMinimumTriggerDelay = base::Milliseconds(300);
  if (tab_width <= tab_min_width) {
    return kMinimumTriggerDelay;
  }
  constexpr base::TimeDelta kMaximumTriggerDelay = base::Milliseconds(800);
  double logarithmic_fraction =
      std::log(tab_width - tab_min_width + 1) /
      std::log(tab_standard_width - tab_min_width + 1);
  base::TimeDelta scaling_factor = kMaximumTriggerDelay - kMinimumTriggerDelay;
  base::TimeDelta delay =
      logarithmic_fraction * scaling_factor + kMinimumTriggerDelay;
  if (tab_width >= tab_standard_width) {
    delay += base::Milliseconds(max_width_additional_delay);
  }
  return delay;
}

bool IsBrowserForSystemWebApp(
    const BrowserWindowInterface* browser_window_interface) {
#if BUILDFLAG(IS_CHROMEOS)
  return web_app::GetSystemWebAppType(browser_window_interface).has_value();
#else
  return false;
#endif
}

views::WidgetFadeAnimator::SlideDirection ComputeSlideDirection(
    const BrowserWindowInterface* browser) {
  if (!features::IsTabStripDeclutterEnabled()) {
    return views::WidgetFadeAnimator::SlideDirection::kNone;
  }

  auto* controller = tabs::VerticalTabStripStateController::From(browser);

  if (controller && controller->ShouldDisplayVerticalTabs()) {
    return views::WidgetFadeAnimator::SlideDirection::kTrailing;
  } else {
    return views::WidgetFadeAnimator::SlideDirection::kDown;
  }
}

}  // anonymous namespace

//-------------------------------------------------------------------
// TabHoverCardController::EventSniffer

// Listens in on the browser event stream and hides an associated hover card
// on any keypress, mouse click, or gesture.
class TabHoverCardController::EventSniffer : public ui::EventObserver {
 public:
  explicit EventSniffer(TabHoverCardController* controller)
      : controller_(controller) {
    // Note that null is a valid value for the second parameter here; if for
    // some reason there is no native window it simply falls back to
    // application-wide event-sniffing, which for this case is better than not
    // watching events at all.
    event_monitor_ = views::EventMonitor::CreateWindowMonitor(
        this,
        controller_->tab_strip_->GetWidget()
            ->GetTopLevelWidget()
            ->GetNativeWindow(),
        {ui::EventType::kKeyPressed, ui::EventType::kKeyReleased,
         ui::EventType::kMousePressed, ui::EventType::kMouseReleased,
         ui::EventType::kGestureBegin, ui::EventType::kGestureEnd});
  }

  ~EventSniffer() override = default;

 protected:
  // ui::EventObserver:
  void OnEvent(const ui::Event& event) override {
    bool close_hover_card = true;
    if (event.IsKeyEvent()) {
      bool is_tab_strip_focused = IsTabStripFocused();
      // Hover card needs to be dismissed (and regenerated) if the keypress
      // would select the tab (this also takes focus out of the tabstrip).
      close_hover_card = event.AsKeyEvent()->key_code() == ui::VKEY_RETURN ||
                         event.AsKeyEvent()->key_code() == ui::VKEY_ESCAPE ||
                         !is_tab_strip_focused;
    }

    if (close_hover_card) {
      controller_->UpdateHoverCard(
          nullptr, TabSlotController::HoverCardUpdateType::kEvent);
    }
  }

 private:
  bool IsTabStripFocused() const {
    views::View* tab_strip_view = controller_->tab_strip_->GetTabStripView();
    return tab_strip_view && tab_strip_view->GetFocusManager() &&
           tab_strip_view->Contains(
               tab_strip_view->GetFocusManager()->GetFocusedView());
  }

  const raw_ptr<TabHoverCardController> controller_;
  std::unique_ptr<views::EventMonitor> event_monitor_;
};

//-------------------------------------------------------------------
// TabHoverCardController

// static
bool TabHoverCardController::disable_animations_for_testing_ = false;

TabHoverCardController::TabHoverCardController(
    TabStripRegionView* tab_strip,
    BrowserWindowInterface* browser_window_interface)
    : tab_strip_(tab_strip),
      browser_window_interface_(browser_window_interface),
      tab_resource_usage_collector_(TabResourceUsageCollector::Get()) {
  if (PrefService* pref_service = g_browser_process->local_state()) {
    // Hovercard image previews are still not fully rolled out to all platforms
    // so we default the pref to the state of the feature rollout.
    pref_service->SetDefaultPrefValue(prefs::kHoverCardImagesEnabled,
                                      base::Value(base::FeatureList::IsEnabled(
                                          features::kTabHoverCardImages)));

    pref_change_registrar_.Init(pref_service);

    // Register for previews enabled pref change events.
    hover_card_image_previews_enabled_ = AreHoverCardImagesEnabled();
    pref_change_registrar_.Add(
        prefs::kHoverCardImagesEnabled,
        base::BindRepeating(
            &TabHoverCardController::OnHovercardImagesEnabledChanged,
            base::Unretained(this)));

    // Register for memory usage enabled pref change events. Exclude
    // tracking them for system web apps (e.g. ChromeOS terminal app).
    if (!browser_window_interface) {
      CHECK_IS_TEST();
    } else if (!IsBrowserForSystemWebApp(browser_window_interface)) {
      OnHovercardMemoryUsageEnabledChanged();
      pref_change_registrar_.Add(
          prefs::kHoverCardMemoryUsageEnabled,
          base::BindRepeating(
              &TabHoverCardController::OnHovercardMemoryUsageEnabledChanged,
              base::Unretained(this)));
    }
  }
}

TabHoverCardController::ScopedHideHoverCardLock::ScopedHideHoverCardLock(
    TabHoverCardController* controller)
    : controller_(controller) {
  controller_->OnLockCreated();
}

TabHoverCardController::ScopedHideHoverCardLock::~ScopedHideHoverCardLock() {
  controller_->OnLockDestroyed();
}

TabHoverCardController::~TabHoverCardController() = default;

bool TabHoverCardController::IsHoverCardVisible() const {
  return hover_card_ && GetCardWidget() && !GetCardWidget()->IsClosed();
}

bool TabHoverCardController::IsHoverCardShowingForTab(
    HoverCardAnchorTarget* anchor_target) const {
  return IsHoverCardVisible() && !fade_animator_->IsFadingOut() &&
         target_tab_ == anchor_target;
}

void TabHoverCardController::UpdateHoverCard(
    HoverCardAnchorTarget* anchor_target,
    TabSlotController::HoverCardUpdateType update_type) {
  if (hide_hover_card_lock_count_ > 0) {
    anchor_target = nullptr;
    update_type = TabSlotController::HoverCardUpdateType::kAnimating;
  }

  // Never display a hover card for an invalid tab.
  if (anchor_target && !anchor_target->IsValidHoverCardTarget()) {
    anchor_target = nullptr;
  }

  // Update this ASAP so that if we try to fade-in and we have the wrong target
  // then when the fade timer elapses we won't incorrectly try to fade in on the
  // wrong tab.
  if (target_tab_ != anchor_target) {
    delayed_show_timer_.Stop();
    target_tab_observation_.Reset();
    if (anchor_target) {
      // If the hover card is transitioning between tabs, we need to do a
      // cross-fade.
      start_hover_card_fade_ = target_tab_ != nullptr;
      target_tab_observation_.Observe(anchor_target->GetView());
    }
    target_tab_ = anchor_target;
  }

  // If there's nothing to attach to then there's no point in creating a card.
  // Note that this includes a check for whether the tab strip widget is
  // visible (see crbug.com/454057267).
  if (!hover_card_ && (!anchor_target || !tab_strip_->GetWidget() ||
                       !tab_strip_->GetWidget()->IsVisibleOnScreen())) {
    return;
  }

  switch (update_type) {
    case TabSlotController::HoverCardUpdateType::kSelectionChanged:
      ResetCardsSeenCount();
      break;
    case TabSlotController::HoverCardUpdateType::kHover:
      if (!anchor_target) {
        last_mouse_exit_timestamp_ = base::TimeTicks::Now();
      }
      break;
    case TabSlotController::HoverCardUpdateType::kTabDataChanged:
      DCHECK(anchor_target && IsHoverCardShowingForTab(anchor_target));
      break;
    case TabSlotController::HoverCardUpdateType::kTabRemoved:
    case TabSlotController::HoverCardUpdateType::kAnimating:
      // Neither of these cases should have a tab associated.
      DCHECK(!anchor_target);
      break;
    case TabSlotController::HoverCardUpdateType::kEvent:
    case TabSlotController::HoverCardUpdateType::kFocus:
      // No special action taken for this type of even (yet).
      break;
  }

  if (anchor_target) {
    UpdateOrShowCard(anchor_target, update_type);
  } else {
    HideHoverCard();
  }
}

void TabHoverCardController::PreventImmediateReshow() {
  last_mouse_exit_timestamp_ = base::TimeTicks();
}

std::unique_ptr<TabHoverCardController::ScopedHideHoverCardLock>
TabHoverCardController::GetHoverCardHideLock() {
  return std::make_unique<ScopedHideHoverCardLock>(this);
}

void TabHoverCardController::OnLockCreated() {
  hide_hover_card_lock_count_++;
  delayed_show_timer_.Stop();
  HideHoverCard();
}

void TabHoverCardController::OnLockDestroyed() {
  hide_hover_card_lock_count_--;
}

// static
bool TabHoverCardController::AreHoverCardImagesEnabled() {
  if (base::FeatureList::IsEnabled(features::kTabHoverCardImages)) {
    PrefService* pref_service = g_browser_process->local_state();
    return pref_service->GetBoolean(prefs::kHoverCardImagesEnabled);
  }
  return false;
}

// static
bool TabHoverCardController::UseAnimations() {
  return !disable_animations_for_testing_ &&
         gfx::Animation::ShouldRenderRichAnimation();
}

void TabHoverCardController::OnViewIsDeleting(views::View* observed_view) {
  if (hover_card_ == observed_view) {
    OnCardClosing();
  } else if (target_tab_ && target_tab_->GetView() == observed_view) {
    UpdateHoverCard(nullptr,
                    TabSlotController::HoverCardUpdateType::kTabRemoved);
    // These postconditions should always be met after calling
    // UpdateHoverCard(nullptr, ...)
    DCHECK(!target_tab_);
    DCHECK(!target_tab_observation_.IsObserving());
  }
}

void TabHoverCardController::OnViewVisibilityChanged(views::View* observed_view,
                                                     views::View* starting_view,
                                                     bool visible) {
  // Only care about target tab becoming invisible.
  if (!target_tab_ || target_tab_->GetView() != observed_view) {
    return;
  }
  // If visibility anywhere in the hierarchy changed to false, then the target
  // view is not visible, so treat it as if it is going away.
  if (!visible) {
    OnViewIsDeleting(observed_view);
  }
}

void TabHoverCardController::OnTabResourceMetricsRefreshed() {
  if (hover_card_ && target_tab_) {
    UpdateHoverCard(target_tab_,
                    TabSlotController::HoverCardUpdateType::kTabDataChanged);
  }
}

bool TabHoverCardController::ArePreviewsEnabled() const {
  return static_cast<bool>(thumbnail_observer_);
}

void TabHoverCardController::CreateHoverCard(
    HoverCardAnchorTarget* anchor_target) {
  TabHoverCardBubbleView::InitParams params;
  params.use_animation = UseAnimations();
  // In some browser types (e.g. ChromeOS terminal app) hide the domain label.
  params.show_domain = !IsBrowserForSystemWebApp(browser_window_interface_);
  params.show_memory_usage = hover_card_memory_usage_enabled_;
  params.show_image_preview = hover_card_image_previews_enabled_;

  hover_card_ = new TabHoverCardBubbleView(anchor_target, params);
  hover_card_observation_.Observe(hover_card_.get());
  event_sniffer_ = std::make_unique<EventSniffer>(this);
  slide_animator_ = std::make_unique<views::BubbleSlideAnimator>(hover_card_);
  slide_animator_->SetSlideDuration(
      TabHoverCardBubbleView::kHoverCardSlideDuration);
  slide_progressed_subscription_ = slide_animator_->AddSlideProgressedCallback(
      base::BindRepeating(&TabHoverCardController::OnSlideAnimationProgressed,
                          weak_ptr_factory_.GetWeakPtr()));
  slide_complete_subscription_ = slide_animator_->AddSlideCompleteCallback(
      base::BindRepeating(&TabHoverCardController::OnSlideAnimationComplete,
                          weak_ptr_factory_.GetWeakPtr()));
  fade_animator_ = std::make_unique<views::WidgetFadeAnimator>(GetCardWidget());
  fade_complete_subscription_ = fade_animator_->AddFadeCompleteCallback(
      base::BindRepeating(&TabHoverCardController::OnFadeAnimationEnded,
                          weak_ptr_factory_.GetWeakPtr()));

  if (!thumbnail_observer_ && hover_card_image_previews_enabled_) {
    thumbnail_observer_ = std::make_unique<TabHoverCardThumbnailObserver>();
    thumbnail_subscription_ = thumbnail_observer_->AddCallback(
        base::BindRepeating(&TabHoverCardController::OnPreviewImageAvailable,
                            weak_ptr_factory_.GetWeakPtr()));
  }

  tab_resource_usage_collector_observation_.Observe(
      tab_resource_usage_collector_);
}

void TabHoverCardController::UpdateCardContent(
    HoverCardAnchorTarget* anchor_target) {
  if (start_hover_card_fade_) {
    hover_card_->SetTextFade(0.0);
    start_hover_card_fade_ = false;
  }

  hover_card_->UpdateCardContent(anchor_target);
}

void TabHoverCardController::MaybeStartThumbnailObservation(
    HoverCardAnchorTarget* anchor_target,
    bool is_initial_show) {
  // If the preview image feature is not enabled, `thumbnail_observer_` will be
  // null.
  if (!thumbnail_observer_) {
    return;
  }

  if (!anchor_target->NeedsToShowThumbnail()) {
    thumbnail_observer_->Observe(nullptr);
    return;
  }

  // Only Tabs have associated thumbnails.
  if (std::holds_alternative<GroupCardData>(anchor_target->data())) {
    return;
  }

  // Discarded tabs that don't already have a thumbnail won't get one.
  const TabCardData& tab_card_data =
      std::get<TabCardData>(anchor_target->data());
  bool has_thumbnail =
      tab_card_data.thumbnail && tab_card_data.thumbnail->has_data();
  if (tab_card_data.is_tab_discarded && !has_thumbnail) {
    thumbnail_observer_->Observe(nullptr);
    return;
  }

  if (tab_card_data.is_crashed) {
    hover_card_->SetCrashedImage();
    thumbnail_observer_->Observe(nullptr);
    return;
  }

  auto thumbnail = tab_card_data.thumbnail;
  if (!thumbnail) {
    hover_card_->SetPlaceholderImage();
    thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;
    return;
  }

  if (thumbnail == thumbnail_observer_->current_image()) {
    return;
  }

  // We're probably going ask for a preview image, so figure out whether we
  // want to capture now, later, or at all, and whether to show a placeholder
  // in the meantime.

  // The crossfade parameter determines when a placeholder image is displayed.
  const auto crossfade_at =
      TabHoverCardBubbleView::GetPreviewImageCrossfadeStart();
  if (UseAnimations() && crossfade_at.has_value() &&
      crossfade_at.value() == 0.0) {
    hover_card_->SetPlaceholderImage();
    thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
  } else {
    thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithoutPlaceholder;
  }

  // For the first show there has already been a delay, so it's fine to ask for
  // the image immediately; same is true if we already have a thumbnail.
  // Otherwise the delay is based on the capture readiness.
  base::TimeDelta capture_delay =
      is_initial_show || thumbnail->has_data()
          ? base::TimeDelta()
          : GetPreviewImageCaptureDelay(thumbnail->GetCaptureReadiness());

  if (capture_delay.is_zero()) {
    thumbnail_observer_->Observe(thumbnail);
    return;
  }

  // If we've already waiting on this tab, we're done.
  if (delayed_show_timer_.IsRunning()) {
    return;
  }

  // Stop updating the preview image unless/until we re-enable capture.
  thumbnail_observer_->Observe(nullptr);
  if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
    hover_card_->SetPlaceholderImage();
    thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
  }

  CHECK(!capture_delay.is_inf());

  // Start a delayed capture.
  delayed_show_timer_.Start(
      FROM_HERE, capture_delay,
      base::BindOnce(&TabHoverCardController::StartThumbnailObservation,
                     base::Unretained(this), anchor_target));
}

void TabHoverCardController::StartThumbnailObservation(
    HoverCardAnchorTarget* anchor_target) {
  if (anchor_target != target_tab_) {
    return;
  }

  // If the preview image feature is not enabled, `thumbnail_observer_` will be
  // null.
  if (!thumbnail_observer_) {
    return;
  }

  DCHECK(anchor_target);
  DCHECK(hover_card_);
  DCHECK(waiting_for_preview());

  const TabCardData& card_data = std::get<TabCardData>(anchor_target->data());
  auto thumbnail = card_data.thumbnail;
  if (!thumbnail || thumbnail == thumbnail_observer_->current_image()) {
    return;
  }

  thumbnail_observer_->Observe(thumbnail);
}

void TabHoverCardController::UpdateOrShowCard(
    HoverCardAnchorTarget* anchor_target,
    TabSlotController::HoverCardUpdateType update_type) {
  // Close is asynchronous, so make sure that if we're closing we clear out all
  // of our data *now* rather than waiting for the deletion message.
  if (hover_card_ && GetCardWidget()->IsClosed()) {
    OnViewIsDeleting(hover_card_);
  }

  // If a hover card is being updated because of a data change, the hover card
  // had better already be showing for the affected tab.
  if (update_type == TabSlotController::HoverCardUpdateType::kTabDataChanged) {
    if (!IsHoverCardShowingForTab(anchor_target)) {
      return;
    }

    UpdateCardContent(anchor_target);

    // When a tab has been discarded, the thumbnail is moved to a new
    // ThumbnailTabHelper so it must be observed again.
    if (std::holds_alternative<GroupCardData>(anchor_target->data())) {
      return;
    }

    const TabCardData& card_data = std::get<TabCardData>(anchor_target->data());
    if (card_data.is_tab_discarded) {
      MaybeStartThumbnailObservation(anchor_target,
                                     /* is_initial_show */ false);
    }

    slide_animator_->UpdateTargetBounds(anchor_target->GetAnchor());
    return;
  }

  if (hover_card_) {
    // Cancel any pending fades.
    if (fade_animator_->IsFadingOut()) {
      fade_animator_->CancelFadeOut();
    }

    fade_animator_->CancelSlide(false);

    // If the card was visible we need to update the card now, before any slide
    // or snap occurs.
    UpdateCardContent(anchor_target);
    MaybeStartThumbnailObservation(anchor_target, /* is_initial_show */ false);

    auto arrow_position = anchor_target->GetAnchorPosition();
    if (hover_card_->arrow() != arrow_position) {
      hover_card_->SetArrowWithoutResizing(arrow_position);
    }

    // If widget is already visible and anchored to the correct tab we should
    // not try to reset the anchor view or reshow.
    if (!UseAnimations() ||
        (hover_card_->IsSameAnchor(anchor_target->GetAnchor()) &&
         !slide_animator_->is_animating())) {
      slide_animator_->SnapToAnchor(anchor_target->GetAnchor());
    } else {
      slide_animator_->AnimateToAnchor(anchor_target->GetAnchor());
    }
    return;
  }

  // Maybe make hover card visible. Disabling animations for testing also
  // eliminates the show timer, lest the tests have to be significantly more
  // complex and time-consuming.
  const bool is_initial = !ShouldShowImmediately(anchor_target);
  if (is_initial) {
    ResetCardsSeenCount();
  }
  if (is_initial && !disable_animations_for_testing_) {
    delayed_show_timer_.Start(
        FROM_HERE,
        GetShowDelay(browser_window_interface_, tab_strip_, anchor_target),
        base::BindOnce(&TabHoverCardController::ShowHoverCard,
                       weak_ptr_factory_.GetWeakPtr(), true, anchor_target));
  } else {
    // Just in case, cancel the timer. This shouldn't cancel a delayed capture
    // since delayed capture only happens when the hover card already exists,
    // and this code is only invoked if there is no hover card yet.
    delayed_show_timer_.Stop();
    DCHECK_EQ(target_tab_, anchor_target);
    ShowHoverCard(is_initial, anchor_target);
  }
}

void TabHoverCardController::ShowHoverCard(
    bool is_initial,
    HoverCardAnchorTarget* intended_target) {
  // Make sure the hover card isn't accidentally shown if it's already visible
  // or if the anchor is gone or changed.
  if (hover_card_ || target_tab_ != intended_target || !TargetTabIsValid()) {
    return;
  }

  // Note: `target_tab_` can be nullified via reentreant callbacks invoked
  // throughout the HoverCard creation process. The doc mentioned at
  // crbug.com/40865488#comment23 discusses proper fixes for this. Until then,
  // early-return after vulnerable calls here if `target_tab_` has become null.
  // See also: crbug.com/40821222, crbug.com/40837791, crbug.com/40855586
  CreateHoverCard(target_tab_);
  if (!TargetTabIsValid()) {
    HideHoverCard();
    return;
  }

  UpdateCardContent(target_tab_);
  if (!TargetTabIsValid()) {
    HideHoverCard();
    return;
  }

  slide_animator_->UpdateTargetBounds(intended_target->GetAnchor());
  MaybeStartThumbnailObservation(target_tab_, is_initial);
  GetCardWidget()->SetZOrderSublevel(ChromeWidgetSublevel::kSublevelHoverable);

  if (!is_initial || !UseAnimations()) {
    OnCardFullyVisible();
    GetCardWidget()->Show();
    return;
  }

  fade_animator_->FadeIn(GetLayoutConstant(LayoutConstant::kTabStripPadding),
                         ComputeSlideDirection(browser_window_interface_));
}

void TabHoverCardController::HideHoverCard() {
  if (!hover_card_ || GetCardWidget()->IsClosed()) {
    return;
  }

  // Required for test metrics.
  hover_card_last_seen_on_tab_ = nullptr;

  if (thumbnail_observer_) {
    thumbnail_observer_->Observe(nullptr);
    thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;
  }

  bool slide_on_fade_out = !slide_animator_->is_animating();

  // Cancel any pending fade-in.
  if (fade_animator_->IsFadingIn()) {
    slide_on_fade_out = false;
    fade_animator_->CancelFadeIn();
  }

  // This needs to be called whether we're doing a fade or a pop out.
  slide_animator_->StopAnimation();
  if (!UseAnimations()) {
    CloseCardWidget();
    return;
  }
  if (fade_animator_->IsFadingOut()) {
    return;
  }

  fade_animator_->FadeOut(
      GetLayoutConstant(LayoutConstant::kTabStripPadding),
      slide_on_fade_out ? ComputeSlideDirection(browser_window_interface_)
                        : views::WidgetFadeAnimator::SlideDirection::kNone);
}

bool TabHoverCardController::ShouldShowImmediately(
    const HoverCardAnchorTarget* anchor_target) const {
  // If less than `kShowWithoutDelayTimeBuffer` time has passed since the hover
  // card was last visible then it is shown immediately. This is to account for
  // if hover unintentionally leaves the tab strip.
  constexpr base::TimeDelta kShowWithoutDelayTimeBuffer =
      base::Milliseconds(300);
  base::TimeDelta elapsed_time =
      base::TimeTicks::Now() - last_mouse_exit_timestamp_;

  bool within_delay_time_buffer = !last_mouse_exit_timestamp_.is_null() &&
                                  elapsed_time <= kShowWithoutDelayTimeBuffer;
  // Hover cards should be shown without delay if triggered within the time
  // buffer or if the tab or its children have focus which indicates that the
  // tab is keyboard focused.
  const views::FocusManager* const tab_focus_manager =
      anchor_target->GetView()->GetFocusManager();
  return within_delay_time_buffer || anchor_target->GetView()->HasFocus() ||
         (tab_focus_manager && anchor_target->GetView()->Contains(
                                   tab_focus_manager->GetFocusedView()));
}

bool TabHoverCardController::TargetTabIsValid() const {
  // There are a bunch of conditions under which a tab may no longer be valid,
  // including no longer belonging to the same tabstrip, being dragged or
  // detached, or just not being visible. We need to be vigilant about invalid
  // tabs due to e.g. crbug.com/40821222.
  return target_tab_ && target_tab_->IsValidHoverCardTarget();
}

void TabHoverCardController::OnCardFullyVisible() {
  DCHECK(target_tab_);
  if (target_tab_ == hover_card_last_seen_on_tab_.get()) {
    return;
  }
  hover_card_last_seen_on_tab_ = target_tab_;
  ++hover_cards_seen_count_;
}

void TabHoverCardController::ResetCardsSeenCount() {
  hover_card_last_seen_on_tab_ = nullptr;
  hover_cards_seen_count_ = 0;
}

void TabHoverCardController::OnFadeAnimationEnded(
    views::WidgetFadeAnimator* animator,
    views::WidgetFadeAnimator::FadeType fade_type) {
  // There's a potential race condition where we get the fade in complete signal
  // just as we've decided to fade out, so check for null.
  // See: crbug.com/40174890
  if (target_tab_ &&
      fade_type == views::WidgetFadeAnimator::FadeType::kFadeIn) {
    OnCardFullyVisible();
  }

  if (fade_type == views::WidgetFadeAnimator::FadeType::kFadeOut) {
    CloseCardWidget();
  }
}

void TabHoverCardController::OnSlideAnimationProgressed(
    views::BubbleSlideAnimator* animator,
    double value) {
  if (hover_card_) {
    hover_card_->SetTextFade(value);
    hover_card_->SetSliding(value > 0);
  }
  if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
    const auto crossfade_start =
        TabHoverCardBubbleView::GetPreviewImageCrossfadeStart();
    if (crossfade_start.has_value() && value >= crossfade_start.value()) {
      hover_card_->SetPlaceholderImage();
      thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
    }
  }
}

void TabHoverCardController::OnSlideAnimationComplete(
    views::BubbleSlideAnimator* animator) {
  // Make sure we're displaying the new text at 100% opacity, and none of the
  // old text.
  hover_card_->SetTextFade(1.0);
  hover_card_->SetSliding(false);

  // If we were waiting for a preview image with data to load, we don't want to
  // keep showing the old image while hovering on the new tab, so clear it. This
  // shouldn't happen very often for slide animations, but could on slower
  // computers.
  if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
    hover_card_->SetPlaceholderImage();
    thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
  }

  OnCardFullyVisible();
}

void TabHoverCardController::OnPreviewImageAvailable(
    TabHoverCardThumbnailObserver* observer,
    gfx::ImageSkia thumbnail_image) {
  DCHECK_EQ(thumbnail_observer_.get(), observer);

  thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;

  // The hover card could be destroyed before the preview image is delivered.
  if (!hover_card_) {
    return;
  }
  // Can still set image on a fading-out hover card (we can change this behavior
  // later if we want).
  hover_card_->SetTargetTabImage(thumbnail_image);
}

void TabHoverCardController::OnHovercardImagesEnabledChanged() {
  hover_card_image_previews_enabled_ = AreHoverCardImagesEnabled();
  if (!hover_card_image_previews_enabled_) {
    thumbnail_subscription_ = base::CallbackListSubscription();
    thumbnail_observer_.reset();
  }
}

void TabHoverCardController::OnHovercardMemoryUsageEnabledChanged() {
  hover_card_memory_usage_enabled_ =
      g_browser_process->local_state()->GetBoolean(
          prefs::kHoverCardMemoryUsageEnabled);
}

views::Widget* TabHoverCardController::GetCardWidget() const {
  return hover_card_->GetWidget();
}

void TabHoverCardController::CloseCardWidget() {
  GetCardWidget()->Close();
}

void TabHoverCardController::OnCardClosing() {
  tab_resource_usage_collector_observation_.Reset();
  delayed_show_timer_.Stop();
  hover_card_observation_.Reset();
  event_sniffer_.reset();
  slide_progressed_subscription_ = base::CallbackListSubscription();
  slide_complete_subscription_ = base::CallbackListSubscription();
  fade_complete_subscription_ = base::CallbackListSubscription();
  slide_animator_.reset();
  fade_animator_.reset();
  hover_card_ = nullptr;
}
