// 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 "content/browser/media/capture/web_contents_frame_tracker.h"

#include <algorithm>
#include <utility>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "components/viz/common/surfaces/video_capture_target.h"
#include "content/browser/gpu/gpu_data_manager_impl.h"
#include "content/browser/media/capture/web_contents_auto_scaler.h"
#include "content/browser/media/capture/web_contents_video_capture_device.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_media_capture_id.h"
#include "content/public/browser/web_contents_observer.h"
#include "media/base/video_util.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video_capture_types.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/native_ui_types.h"

#if !BUILDFLAG(IS_IOS)
#include "content/browser/media/capture/mouse_cursor_overlay_controller.h"
#endif

namespace content {

namespace {

void SetScaleOverrideForCapture(RenderFrameHost* rfh, float scale_override) {
  if (rfh) {
    // Inside content, down-casting from the public interface class is safe.
    auto* view = static_cast<RenderWidgetHostViewBase*>(rfh->GetView());
    if (view) {
      view->SetScaleOverrideForCapture(scale_override);
    }
  }
}

}  // namespace

// Note on lifetime: this context is deleted via WebContentsObserver's
// WebContentsDestroyed() method when the WebContents is destroyed.
class WebContentsContext : public WebContentsFrameTracker::Context {
 public:
  explicit WebContentsContext(WebContents* contents) : contents_(contents) {
    DCHECK(contents_);
  }
  ~WebContentsContext() override = default;

  // WebContextFrameTracker::Context overrides.
  std::optional<gfx::Rect> GetScreenBounds() override {
    if (auto* view = GetCurrentView()) {
      // If we know the available size of the screen, we don't want to exceed
      // it as it may result in strange capture behavior in some cases.
      return view->GetScreenInfo().rect;
    }
    return std::nullopt;
  }

  WebContentsImpl::CaptureTarget GetCaptureTarget() override {
    return static_cast<WebContentsImpl*>(contents_)->GetCaptureTarget();
  }

  void IncrementCapturerCount(const gfx::Size& capture_size) override {
    capture_handle_ = contents_->IncrementCapturerCount(
        capture_size, /*stay_hidden=*/false,
        /*stay_awake=*/true, /*is_activity=*/true);
  }

  void DecrementCapturerCount() override { capture_handle_.RunAndReset(); }

 private:
  // WebContentsAutoScaler::Delegate:
  void SetCaptureScaleOverride(float scale) override {
    if (auto* view = GetCurrentView()) {
      view->SetScaleOverrideForCapture(scale);
    }
  }

  float GetCaptureScaleOverride() const override {
    if (const auto* view = GetCurrentView()) {
      return view->GetScaleOverrideForCapture();
    }
    // Otherwise we can assume it's unset and return the default value.
    return 1.0f;
  }

  RenderWidgetHostViewBase* GetCurrentView() const {
    RenderWidgetHostView* view = contents_->GetRenderWidgetHostView();
    // Make sure the RWHV is still associated with a RWH before considering the
    // view "alive." This is because a null RWH indicates the RWHV has had its
    // Destroy() method called.
    if (!view || !view->GetRenderWidgetHost()) {
      return nullptr;
    }
    // Inside content, down-casting from the public interface class is safe.
    return static_cast<RenderWidgetHostViewBase*>(view);
  }

  base::ScopedClosureRunner capture_handle_;

  // The backing WebContents.
  raw_ptr<WebContents, DanglingUntriaged> contents_;
};

WebContentsFrameTracker::WebContentsFrameTracker(
    scoped_refptr<base::SequencedTaskRunner> device_task_runner,
    base::WeakPtr<WebContentsVideoCaptureDevice> device,
    MouseCursorOverlayController* cursor_controller)
    : device_(std::move(device)),
      device_task_runner_(std::move(device_task_runner))
#if !BUILDFLAG(IS_IOS)
      ,
      cursor_controller_(cursor_controller->GetWeakPtr())
#endif
{
  // Verify on construction that this object is created on the UI thread.  After
  // this, depend on the sequence checker to ensure consistent execution.
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(device_task_runner_);

#if !BUILDFLAG(IS_IOS)
  CHECK(cursor_controller_);
#endif
}

WebContentsFrameTracker::~WebContentsFrameTracker() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_capturing_) {
    DidStopCapturingWebContents();
  }
}

void WebContentsFrameTracker::WillStartCapturingWebContents(
    const gfx::Size& capture_size,
    bool is_high_dpi_enabled) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!is_capturing_);
  if (!web_contents()) {
    return;
  }

  capture_size_ = capture_size;
  if (is_high_dpi_enabled &&
      !GpuDataManagerImpl::GetInstance()->IsGpuCompositingDisabled()) {
    auto_scaler_ =
        std::make_unique<WebContentsAutoScaler>(*context_, capture_size);
  }
  context_->IncrementCapturerCount(CalculatePreferredSize(capture_size));
  is_capturing_ = true;
}

void WebContentsFrameTracker::DidStopCapturingWebContents() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (web_contents()) {
    DCHECK(is_capturing_);
    context_->DecrementCapturerCount();
    is_capturing_ = false;

    if (auto_scaler_) {
      UMA_HISTOGRAM_COUNTS_1000("Media.VideoCapture.ScaleOverrideChangeCount",
                                auto_scaler_->GetScaleOverrideChangeCount());
      context_->SetCaptureScaleOverride(1.0f);
      auto_scaler_.reset();
    }
  }
  DCHECK(!is_capturing_);
}

void WebContentsFrameTracker::SetCapturedContentSize(
    const gfx::Size& content_size) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!web_contents()) {
    return;
  }

  // For efficiency, this function should only be called when the captured
  // content size changes. The caller is responsible for enforcing that.
  TRACE_EVENT_INSTANT("gpu.capture",
                      "WebContentsFrameTracker::SetCapturedContentSize",
                      "content_size", content_size.ToString());

  if (auto_scaler_) {
    auto_scaler_->SetCapturedContentSize(content_size);
  }
}

// We provide the WebContents with a preferred size override during its capture.
// The preferred size is a strong suggestion to UI layout code to size the view
// such that its physical rendering size matches the exact capture size. This
// helps to eliminate redundant scaling operations during capture. Note that if
// there are multiple capturers, a "first past the post" system is used and
// the first capturer's preferred size is set.
gfx::Size WebContentsFrameTracker::CalculatePreferredSize(
    const gfx::Size& capture_size) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (capture_size.IsEmpty()) {
    // NOTE: An empty preferred size will cause the WebContents to keep its
    // previous size preference.
    return {};
  }
  gfx::Size preferred_size = capture_size;

  // If we know the available size of the screen, we don't want to exceed
  // it as it may result in strange capture behavior in some cases.
  if (context_) {
    const std::optional<gfx::Rect> screen_bounds = context_->GetScreenBounds();
    if (screen_bounds) {
      if (screen_bounds->size().IsEmpty()) {
        return {};
      }

      // We want to honor the aspect ratio of the capture size request while
      // also limiting it to the screen bounds of the view.
      // For motivation, see https://crbug.com/1194803.
      const double x_ratio = static_cast<double>(capture_size.width()) /
                             static_cast<double>(screen_bounds->size().width());
      const double y_ratio =
          static_cast<double>(capture_size.height()) /
          static_cast<double>(screen_bounds->size().height());

      const double scale_ratio = std::max(x_ratio, y_ratio);
      if (scale_ratio > 1.0) {
        preferred_size = gfx::ScaleToFlooredSize(
            preferred_size, static_cast<float>(1 / scale_ratio));
      }

      DVLOG(3) << __func__ << ": x_ratio=" << x_ratio << " y_ratio=" << y_ratio
               << " scale_ratio=" << scale_ratio
               << " preferred_size=" << preferred_size.ToString();
    }
  }
  return preferred_size;
}

void WebContentsFrameTracker::OnUtilizationReport(
    media::VideoCaptureFeedback feedback) {
  TRACE_EVENT_INSTANT("gpu.capture",
                      "WebContentsFrameTracker::OnUtilizationReport",
                      "utilization", feedback.resource_utilization,
                      "max_pixels", feedback.max_pixels);
  if (auto_scaler_) {
    auto_scaler_->OnUtilizationReport(std::move(feedback));
  }
}

void WebContentsFrameTracker::RenderFrameCreated(
    RenderFrameHost* render_frame_host) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OnPossibleTargetChange();
  if (auto_scaler_) {
    SetScaleOverrideForCapture(render_frame_host,
                               auto_scaler_->GetDesiredCaptureScaleOverride());
  }
}

void WebContentsFrameTracker::RenderFrameDeleted(
    RenderFrameHost* render_frame_host) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::RenderFrameHostChanged(
    RenderFrameHost* old_host,
    RenderFrameHost* new_host) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OnPossibleTargetChange();
  SetScaleOverrideForCapture(old_host, 1.0f);
  if (auto_scaler_) {
    SetScaleOverrideForCapture(new_host,
                               auto_scaler_->GetDesiredCaptureScaleOverride());
  }
}

void WebContentsFrameTracker::WebContentsDestroyed() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  is_capturing_ = false;
  auto_scaler_.reset();
  context_.reset();
  Observe(nullptr);
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::OnVisibilityChanged(Visibility visibility) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::CaptureTargetChanged() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::SetWebContentsAndContextFromRoutingId(
    const GlobalRenderFrameHostId& id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Observe(WebContents::FromRenderFrameHost(RenderFrameHost::FromID(id)));
  if (web_contents()) {
    // If the routing ID was invalid, don't set up a context.
    context_ = std::make_unique<WebContentsContext>(web_contents());
  }
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::ApplySubCaptureTarget(
    media::mojom::SubCaptureTargetType type,
    const base::Token& target_token,
    uint32_t sub_capture_target_version,
    base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)>
        callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(callback);

  if (sub_capture_target_version_ >= sub_capture_target_version) {
    std::move(callback).Run(
        media::mojom::ApplySubCaptureTargetResult::kNonIncreasingVersion);
    return;
  }

  sub_capture_target_ =
      target_token.is_zero()
          ? std::nullopt
          : std::make_optional<SubCaptureTargetInfo>(type, target_token);

  sub_capture_target_version_ = sub_capture_target_version;

  // If we don't have a target yet, we can store the sub-capture target,
  // but cannot actually apply it yet.
  if (!target_frame_sink_id_.is_valid()) {
    return;
  }

  const viz::VideoCaptureTarget target(target_frame_sink_id_,
                                       DeriveSubTarget());
  device_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](const viz::VideoCaptureTarget& target,
             uint32_t sub_capture_target_version,
             base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)>
                 callback,
             base::WeakPtr<WebContentsVideoCaptureDevice> device) {
            if (!device) {
              std::move(callback).Run(
                  media::mojom::ApplySubCaptureTargetResult::kErrorGeneric);
              return;
            }
            device->OnTargetChanged(target, sub_capture_target_version);
            std::move(callback).Run(
                media::mojom::ApplySubCaptureTargetResult::kSuccess);
          },
          target, sub_capture_target_version_, std::move(callback), device_));
}

void WebContentsFrameTracker::SetWebContentsAndContextForTesting(
    WebContents* web_contents,
    std::unique_ptr<WebContentsFrameTracker::Context> context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Observe(web_contents);
  context_ = std::move(context);
  OnPossibleTargetChange();
}

void WebContentsFrameTracker::OnPossibleTargetChange() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!web_contents()) {
    DCHECK(!context_);
    device_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsVideoCaptureDevice::OnTargetPermanentlyLost,
                       device_));
    SetTargetView({});
    return;
  }

  const WebContentsImpl::CaptureTarget capture_target =
      context_ ? context_->GetCaptureTarget()
               : WebContentsImpl::CaptureTarget{};

  // TODO(crbug.com/40203554): Clear |sub_capture_target_| when
  // share-this-tab-instead is clicked.
  if (capture_target.sink_id != target_frame_sink_id_) {
    target_frame_sink_id_ = capture_target.sink_id;
    std::optional<viz::VideoCaptureTarget> target;
    if (capture_target.sink_id.is_valid()) {
      target =
          viz::VideoCaptureTarget(capture_target.sink_id, DeriveSubTarget());
    }

    // The target may change to an invalid one, but we don't consider it
    // permanently lost here yet.
    device_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsVideoCaptureDevice::OnTargetChanged, device_,
                       std::move(target), sub_capture_target_version_));
  }

  // Note: MouseCursorOverlayController runs on the UI thread. SetTargetView()
  // must be called synchronously since the NativeView pointer is not valid
  // across task switches, cf. https://crbug.com/818679
  //
  // We only want to do a mouse overlay if this not a background tab, so
  // we disregard the view if it is. This is only needed if multiple tabs
  // share the same native view (which happens with SurfaceEmbed).
  SetTargetView(web_contents()->GetVisibility() != Visibility::HIDDEN
                    ? capture_target.view
                    : gfx::NativeView());
}

void WebContentsFrameTracker::SetTargetView(gfx::NativeView view) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // We don't have to worry about web_contents() changing other than to null
  // since SetWebContentsAndContextFromRoutingId() is only called once.
  if (view == target_native_view_) {
    return;
  }
  target_native_view_ = view;
#if !BUILDFLAG(IS_IOS)
  if (cursor_controller_) {
    cursor_controller_->SetTargetView(view, web_contents());
  }
#endif
}

viz::VideoCaptureSubTarget WebContentsFrameTracker::DeriveSubTarget() const {
  if (!sub_capture_target_.has_value()) {
    return base::Token();
  }

  const SubCaptureTargetInfo& sub_capture_target = sub_capture_target_.value();
  switch (sub_capture_target.type) {
    case media::mojom::SubCaptureTargetType::kCropTarget:
      return sub_capture_target.token;
    case media::mojom::SubCaptureTargetType::kRestrictionTarget:
      return viz::SubtreeCaptureId(sub_capture_target.token);
  }

  NOTREACHED();
}

}  // namespace content
