// 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/thumbnails/thumbnail_tab_helper.h"

#include <stdint.h>

#include <algorithm>
#include <optional>
#include <set>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.h"
#include "chrome/browser/ui/thumbnails/thumbnail_capture_driver.h"
#include "chrome/browser/ui/thumbnails/thumbnail_readiness_tracker.h"
#include "chrome/browser/ui/thumbnails/thumbnail_scheduler.h"
#include "chrome/browser/ui/thumbnails/thumbnail_scheduler_impl.h"
#include "components/tabs/public/tab_interface.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/native_theme/native_theme.h"

namespace {

// Minimum scale factor to capture thumbnail images at. At 1.0x we want to
// slightly over-sample the image so that it looks good for multiple uses and
// cropped to different dimensions.
constexpr float kMinThumbnailScaleFactor = 1.5f;

gfx::Size GetMinimumThumbnailSize() {
  // Minimum thumbnail dimension (in DIP) for tablet tabstrip previews.
  constexpr int kMinThumbnailDimensionForTablet = 175;

  // Compute minimum sizes for multiple uses of the thumbnail - currently,
  // tablet tabstrip previews and tab hover card preview images.
  gfx::Size min_target_size = TabStyle::Get()->GetPreviewImageSize();
  min_target_size.SetToMax(
      {kMinThumbnailDimensionForTablet, kMinThumbnailDimensionForTablet});

  return min_target_size;
}

// When a tab is discarded, the `WebContents` associated with it is destroyed
// and a new, empty one is created. This also means that all of the
// `WebContentsUserData` associated with the original `WebContents` are
// destroyed. In order for the thumbnail image data to remain available after a
// tab discard, an instance of DiscardedTabThumbnailData is created and attached
// to the new `WebContents` right after its creation. Then, when that
// `WebContents` is attached to a tab strip, it gets all of the `TabHelper`s
// attached to it, including `ThumbnailTabHelper` which can query the
// `WebContents` to pick up any persisted thumbnail data if its creation is the
// result of a discard.
// The order of operations during a discard is:
// 1. A new `WebContents` is created
// 2. The old `WebContents`'s observers receive a call to
// `AboutToBeDiscarded()`, receiving the new `WebContents` as a parameter.
// 3. The old `WebContents` is attached to the tab strip, replacing the old one
// 4. The old `WebContents` is deleted
class DiscardedTabThumbnailData
    : public content::WebContentsUserData<DiscardedTabThumbnailData> {
 public:
  static ThumbnailImage::CompressedThumbnailData TakeThumbnailDataIfAvailable(
      content::WebContents* web_contents) {
    DiscardedTabThumbnailData* existing_thumbnail_data =
        DiscardedTabThumbnailData::FromWebContents(web_contents);
    if (existing_thumbnail_data) {
      ThumbnailImage::CompressedThumbnailData thumbnail =
          std::move(existing_thumbnail_data->thumbnail_);
      // It's safe to delete the `DiscardedTabThumbnailData` because the
      // `CompressedThumbnailData` we've just taken is a `scoped_refptr`.
      web_contents->RemoveUserData(UserDataKey());
      return thumbnail;
    }
    return nullptr;
  }

  explicit DiscardedTabThumbnailData(
      content::WebContents* web_contents,
      ThumbnailImage::CompressedThumbnailData thumbnail)
      : content::WebContentsUserData<DiscardedTabThumbnailData>(*web_contents),
        thumbnail_(thumbnail) {}

  WEB_CONTENTS_USER_DATA_KEY_DECL();

 private:
  ThumbnailImage::CompressedThumbnailData thumbnail_;
};

WEB_CONTENTS_USER_DATA_KEY_IMPL(DiscardedTabThumbnailData);

}  // anonymous namespace

// ThumbnailTabHelper::CaptureType ---------------------------------------

enum class ThumbnailTabHelper::CaptureType {
  // The image was copied directly from a visible RenderWidgetHostView.
  kCopyFromView = 0,
  // The image is a frame from a background tab video capturer.
  kVideoFrame = 1,

  kMaxValue = kVideoFrame,
};

// ThumbnailTabHelper::TabStateTracker ---------------------------

// Stores information about the state of the current WebContents and renderer.
class ThumbnailTabHelper::TabStateTracker
    : public content::WebContentsObserver,
      public ThumbnailCaptureDriver::Client,
      public ThumbnailImage::Delegate {
 public:
  TabStateTracker(ThumbnailTabHelper* thumbnail_tab_helper,
                  content::WebContents* contents)
      : content::WebContentsObserver(contents),
        thumbnail_tab_helper_(thumbnail_tab_helper),
        readiness_tracker_(
            contents,
            base::BindRepeating(&TabStateTracker::PageReadinessChanged,
                                base::Unretained(this))) {}
  ~TabStateTracker() override = default;

  // Returns the host view associated with the current web contents, or null if
  // none.
  content::RenderWidgetHostView* GetView() {
    auto* const contents = web_contents();
    return contents ? contents->GetPrimaryMainFrame()
                          ->GetRenderViewHost()
                          ->GetWidget()
                          ->GetView()
                    : nullptr;
  }

  // Returns true if we are capturing thumbnails from a tab and should continue
  // to do so, false if we should stop.
  bool ShouldContinueVideoCapture() const { return !!scoped_capture_; }

  // Tells our scheduling logic that a frame was received.
  void OnFrameCaptured(CaptureType capture_type) {
    if (capture_type == CaptureType::kVideoFrame) {
      capture_driver_.GotFrame();
    }
  }

  bool is_ready() const {
    return page_readiness_ != CaptureReadiness::kNotReady;
  }

 private:
  using CaptureReadiness = ThumbnailImage::CaptureReadiness;

  // ThumbnailCaptureDriver::Client:
  void RequestCapture() override {
    if (!scoped_capture_) {
      scoped_capture_ = web_contents()->IncrementCapturerCount(
          gfx::Size(), /*stay_hidden=*/true,
          /*stay_awake=*/false, /*is_activity=*/true);
    }
  }

  void StartCapture() override {
    DCHECK(scoped_capture_);
    thumbnail_tab_helper_->StartVideoCapture();
  }

  void StopCapture() override {
    thumbnail_tab_helper_->StopVideoCapture();
    scoped_capture_.RunAndReset();
  }

  // content::WebContentsObserver:
  void RenderViewReady() override { capture_driver_.SetCanCapture(true); }

  void PrimaryMainFrameRenderProcessGone(
      base::TerminationStatus status) override {
    // TODO(crbug.com/40686155): determine if there are other ways to
    // lose the view.
    capture_driver_.SetCanCapture(false);
  }

  // ThumbnailImage::Delegate:
  void ThumbnailImageBeingObservedChanged(bool is_being_observed) override {
    capture_driver_.UpdateThumbnailVisibility(is_being_observed);
    // Do not attempt to reload discarded tabs for thumbnail observation events.
    if (is_being_observed && !web_contents()->WasDiscarded()) {
      web_contents()->GetController().LoadIfNecessary();
    }
  }

  ThumbnailImage::CaptureReadiness GetCaptureReadiness() const override {
    return page_readiness_;
  }

  void PageReadinessChanged(CaptureReadiness readiness) {
    if (page_readiness_ == readiness) {
      return;
    }

    // If we transition back to a kNotReady state, clear any existing thumbnail,
    // as it will contain an old snapshot, possibly from a different domain.
    // Readiness will be reset to kNotReady when a tab is discarded. In this
    // specific case we do not clear thumbnail data to ensure the existing
    // preview remains available while discarded tabs are hovered.
    if (readiness == CaptureReadiness::kNotReady &&
        !web_contents()->WasDiscarded()) {
      thumbnail_tab_helper_->ClearData();
    }
    page_readiness_ = readiness;
    capture_driver_.UpdatePageReadiness(readiness);
  }

  const raw_ptr<ThumbnailTabHelper> thumbnail_tab_helper_;

  ThumbnailCaptureDriver capture_driver_{
      this, &thumbnail_tab_helper_->GetScheduler()};
  ThumbnailReadinessTracker readiness_tracker_;

  // Where we are in the page lifecycle.
  CaptureReadiness page_readiness_ = CaptureReadiness::kNotReady;

  // Scoped request for video capture.
  base::ScopedClosureRunner scoped_capture_;
};

// ThumbnailTabHelper ----------------------------------------------------

void ThumbnailTabHelper::CaptureThumbnailOnTabBackgrounded() {
  if (!state_->is_ready()) {
    return;
  }

  const base::TimeTicks time_of_call = base::TimeTicks::Now();

  // Ignore previous requests to capture a thumbnail on tab switch.
  weak_factory_for_thumbnail_on_tab_hidden_.InvalidateWeakPtrs();

  // Get the WebContents' main view. Note that during shutdown there may not be
  // a view to capture, and views are sometimes not available for capture even
  // when they are present.
  content::RenderWidgetHostView* const source_view = state_->GetView();
  if (!source_view || !source_view->IsSurfaceAvailableForCopy()) {
    return;
  }

  // Note: this is the size in pixels on-screen, not the size in DIPs.
  gfx::Size source_size = source_view->GetViewBounds().size();
  if (source_size.IsEmpty()) {
    return;
  }

  const float scale_factor = source_view->GetDeviceScaleFactor();
  ThumbnailCaptureInfo copy_info = GetInitialCaptureInfo(
      source_size, scale_factor, /* include_scrollbars_in_capture */ false);

  source_view->CopyFromSurface(
      copy_info.copy_rect, copy_info.target_size, base::TimeDelta(),
      base::BindOnce(&ThumbnailTabHelper::StoreThumbnailForTabSwitch,
                     weak_factory_for_thumbnail_on_tab_hidden_.GetWeakPtr(),
                     time_of_call));
}

DEFINE_USER_DATA(ThumbnailTabHelper);

ThumbnailTabHelper::ThumbnailTabHelper(tabs::TabInterface& tab,
                                       content::WebContents* contents)
    : content::WebContentsObserver(contents),
      state_(std::make_unique<TabStateTracker>(this, contents)),
      background_capturer_(std::make_unique<BackgroundThumbnailVideoCapturer>(
          contents,
          base::BindRepeating(
              &ThumbnailTabHelper::StoreThumbnailForBackgroundCapture,
              base::Unretained(this)))),
      thumbnail_(base::MakeRefCounted<ThumbnailImage>(
          state_.get(),
          DiscardedTabThumbnailData::TakeThumbnailDataIfAvailable(contents))),
      scoped_unowned_user_data_(tab.GetUnownedUserDataHost(), *this) {
  is_tab_discarded_ = contents->WasDiscarded();
}

ThumbnailTabHelper::~ThumbnailTabHelper() {
  StopVideoCapture();
}

// static
ThumbnailTabHelper* ThumbnailTabHelper::From(tabs::TabInterface* tab) {
  return Get(tab->GetUnownedUserDataHost());
}

// static
ThumbnailScheduler& ThumbnailTabHelper::GetScheduler() {
  static base::NoDestructor<ThumbnailSchedulerImpl> instance;
  return *instance.get();
}

void ThumbnailTabHelper::StoreThumbnailForTabSwitch(
    base::TimeTicks start_time,
    const content::CopyFromSurfaceResult& result) {
  UMA_HISTOGRAM_CUSTOM_TIMES("Tab.Preview.TimeToStoreAfterTabSwitch",
                             base::TimeTicks::Now() - start_time,
                             base::Milliseconds(1), base::Seconds(1), 50);
  if (!result.has_value()) {
    return;
  }

  StoreThumbnail(CaptureType::kCopyFromView, result->bitmap, std::nullopt);
}

void ThumbnailTabHelper::StoreThumbnailForBackgroundCapture(
    const SkBitmap& bitmap,
    uint64_t frame_id) {
  // If this is the first thumbnail being stored, record the time it took from
  // capturing to storing the frame.
  if (!thumbnail_->has_data() &&
      start_video_capture_time_ != base::TimeTicks()) {
    UMA_HISTOGRAM_TIMES(
        "Tab.Preview.TimeToStoreFirstUsableFrameAfterStartCapture",
        base::TimeTicks::Now() - start_video_capture_time_);
  }
  StoreThumbnail(CaptureType::kVideoFrame, bitmap, frame_id);
}

void ThumbnailTabHelper::StoreThumbnail(CaptureType type,
                                        const SkBitmap& bitmap,
                                        std::optional<uint64_t> frame_id) {
  // Failed requests will return an empty bitmap. In tests this can be triggered
  // on threads other than the UI thread.
  if (bitmap.drawsNothing()) {
    return;
  }

  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  state_->OnFrameCaptured(type);
  thumbnail_->AssignSkBitmap(bitmap, frame_id);
}

void ThumbnailTabHelper::ClearData() {
  thumbnail_->ClearData();
}

void ThumbnailTabHelper::StartVideoCapture() {
  content::RenderWidgetHostView* const source_view = state_->GetView();
  if (!source_view) {
    return;
  }

  const float scale_factor = source_view->GetDeviceScaleFactor();
  const gfx::Size source_size = source_view->GetViewBounds().size();
  if (source_size.IsEmpty()) {
    return;
  }

  start_video_capture_time_ = base::TimeTicks::Now();

  last_frame_capture_info_ = GetInitialCaptureInfo(
      source_size, scale_factor, /* include_scrollbars_in_capture */ true);
  background_capturer_->Start(last_frame_capture_info_);
}

void ThumbnailTabHelper::StopVideoCapture() {
  background_capturer_->Stop();
  start_video_capture_time_ = base::TimeTicks();
}

// static
ThumbnailCaptureInfo ThumbnailTabHelper::GetInitialCaptureInfo(
    const gfx::Size& source_size,
    float scale_factor,
    bool include_scrollbars_in_capture) {
  ThumbnailCaptureInfo capture_info;
  capture_info.source_size = source_size;

  scale_factor = std::max(scale_factor, kMinThumbnailScaleFactor);

  // Minimum thumbnail dimension (in DIP) for tablet tabstrip previews.
  const gfx::Size smallest_thumbnail = GetMinimumThumbnailSize();
  const int smallest_dimension =
      scale_factor *
      std::min(smallest_thumbnail.width(), smallest_thumbnail.height());

  // Clip the pixels that will commonly hold a scrollbar, which looks bad in
  // thumbnails - but only if that wouldn't make the thumbnail too small. We
  // can't just use gfx::scrollbar_size() because that reports default system
  // scrollbar width which is different from the width used in web rendering.
  const int scrollbar_size_dip =
      ui::NativeTheme::GetInstanceForWeb()
          ->GetPartSize(ui::NativeTheme::Part::kScrollbarVerticalTrack,
                        ui::NativeTheme::State::kNormal,
                        ui::NativeTheme::ExtraParams(
                            std::in_place_type<
                                ui::NativeTheme::ScrollbarTrackExtraParams>))
          .width();
  // Round up to make sure any scrollbar pixls are eliminated. It's better to
  // lose a single pixel of content than having a single pixel of scrollbar.
  const int scrollbar_size = std::ceil(scale_factor * scrollbar_size_dip);
  if (source_size.width() - scrollbar_size > smallest_dimension) {
    capture_info.scrollbar_insets.set_right(scrollbar_size);
  }
  if (source_size.height() - scrollbar_size > smallest_dimension) {
    capture_info.scrollbar_insets.set_bottom(scrollbar_size);
  }

  // Calculate the region to copy from.
  capture_info.copy_rect = gfx::Rect(source_size);
  if (!include_scrollbars_in_capture) {
    capture_info.copy_rect.Inset(capture_info.scrollbar_insets);
  }

  // Compute minimum sizes for multiple uses of the thumbnail - currently,
  // tablet tabstrip previews and tab hover card preview images.
  const gfx::Size min_target_size =
      gfx::ScaleToFlooredSize(smallest_thumbnail, scale_factor);

  // Calculate the target size to be the smallest size which meets the minimum
  // requirements but has the same aspect ratio as the source (with or without
  // scrollbars).
  const float width_ratio = static_cast<float>(capture_info.copy_rect.width()) /
                            min_target_size.width();
  const float height_ratio =
      static_cast<float>(capture_info.copy_rect.height()) /
      min_target_size.height();
  const float scale_ratio = std::min(width_ratio, height_ratio);
  capture_info.target_size =
      scale_ratio <= 1.0f
          ? capture_info.copy_rect.size()
          : gfx::ScaleToCeiledSize(capture_info.copy_rect.size(),
                                   1.0f / scale_ratio);

  return capture_info;
}

void ThumbnailTabHelper::AboutToBeDiscarded(
    content::WebContents* new_contents) {
  DiscardedTabThumbnailData::CreateForWebContents(new_contents,
                                                  thumbnail_->data());
}

void ThumbnailTabHelper::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  is_tab_discarded_ = false;
}
