// Copyright 2020 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/background_thumbnail_video_capturer.h"

#include <stdint.h>

#include <utility>

#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/trace_id_helper.h"
#include "content/public/browser/render_frame_host.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 "media/capture/mojom/video_capture_buffer.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"
#include "third_party/perfetto/include/perfetto/tracing/track_event_args.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "ui/gfx/geometry/skia_conversions.h"

BackgroundThumbnailVideoCapturer::BackgroundThumbnailVideoCapturer(
    content::WebContents* contents,
    GotFrameCallback got_frame_callback)
    : contents_(contents), got_frame_callback_(std::move(got_frame_callback)) {
  DCHECK(contents_);
  DCHECK(got_frame_callback_);
}

BackgroundThumbnailVideoCapturer::~BackgroundThumbnailVideoCapturer() {
  if (video_capturer_) {
    Stop();
  }
}

void BackgroundThumbnailVideoCapturer::Start(
    const ThumbnailCaptureInfo& capture_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (video_capturer_) {
    return;
  }

  content::RenderWidgetHostView* const source_view =
      contents_->GetPrimaryMainFrame()
          ->GetRenderViewHost()
          ->GetWidget()
          ->GetView();
  if (!source_view) {
    return;
  }

  capture_info_ = capture_info;

  {
    // Assign IDs to each capture session for tracing. IDs are unique
    // throughout the lifetime of this process. Using a static here is
    // safe since this is only invoked from the UI thread.
    static uint64_t capture_num GUARDED_BY_CONTEXT(sequence_checker_) = 0;
    cur_capture_num_ = ++capture_num;
    TRACE_EVENT_BEGIN("ui", "Tab.Preview.VideoCapture",
                      perfetto::Track(cur_capture_num_));
  }

  start_time_ = base::TimeTicks::Now();
  num_received_frames_ = 0;

  constexpr int kMaxFrameRate = 2;
  video_capturer_ = source_view->CreateVideoCapturer();
  video_capturer_->SetResolutionConstraints(capture_info_.target_size,
                                            capture_info_.target_size, false);
  video_capturer_->SetAutoThrottlingEnabled(false);
  video_capturer_->SetMinSizeChangePeriod(base::TimeDelta());
  video_capturer_->SetFormat(media::PIXEL_FORMAT_ARGB);
  video_capturer_->SetMinCapturePeriod(base::Seconds(1) / kMaxFrameRate);
  video_capturer_->Start(this, viz::mojom::BufferFormatPreference::kDefault);
}

void BackgroundThumbnailVideoCapturer::Stop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!video_capturer_) {
    return;
  }

  video_capturer_->Stop();
  video_capturer_.reset();

  TRACE_EVENT_END("ui", perfetto::Track(cur_capture_num_));
  start_time_ = base::TimeTicks();
  cur_capture_num_ = 0;
}

void BackgroundThumbnailVideoCapturer::OnFrameCaptured(
    ::media::mojom::VideoBufferHandlePtr data,
    ::media::mojom::VideoFrameInfoPtr info,
    const gfx::Rect& content_rect,
    mojo::PendingRemote<::viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
        callbacks) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CHECK(video_capturer_);

  mojo::Remote<::viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
      callbacks_remote(std::move(callbacks));

  CHECK(data->is_read_only_shmem_region());
  base::ReadOnlySharedMemoryRegion& shmem_region =
      data->get_read_only_shmem_region();

  // The |data| parameter is not nullable and mojo type mapping for
  // `base::ReadOnlySharedMemoryRegion` defines that nullable version of it is
  // the same type, with null check being equivalent to IsValid() check. Given
  // the above, we should never be able to receive a read only shmem region that
  // is not valid - mojo will enforce it for us.
  DCHECK(shmem_region.IsValid());

  // Process captured image.
  base::ReadOnlySharedMemoryMapping mapping = shmem_region.Map();
  if (!mapping.IsValid()) {
    DLOG(ERROR) << "Shared memory mapping failed.";
    return;
  }
  if (mapping.size() <
      media::VideoFrame::AllocationSize(info->pixel_format, info->coded_size)) {
    DLOG(ERROR) << "Shared memory size was less than expected.";
    return;
  }

  TRACE_EVENT_INSTANT("ui", "Tab.Preview.VideoCaptureFrameReceived",
                      "frame_number", num_received_frames_);
  ++num_received_frames_;

  uint64_t frame_id = base::trace_event::GetNextGlobalTraceId();
  TRACE_EVENT("ui", "Tab.Preview.ProcessVideoCaptureFrame",
              perfetto::Flow::ProcessScoped(frame_id));

  // The SkBitmap's pixels will be marked as immutable, but the installPixels()
  // API requires a non-const pointer. So, cast away the const.
  void* const pixels = const_cast<void*>(mapping.memory());

  // Call installPixels() with a |releaseProc| that: 1) notifies the capturer
  // that this consumer has finished with the frame, and 2) releases the shared
  // memory mapping.
  struct FramePinner {
    // Keeps the shared memory that backs |frame_| mapped.
    base::ReadOnlySharedMemoryMapping mapping;
    // Prevents FrameSinkVideoCapturer from recycling the shared memory that
    // backs |frame_|.
    mojo::PendingRemote<viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
        releaser;
  };

  // Subtract back out the scroll bars if we decided there was enough canvas to
  // account for them and still have a decent preview image.
  const float scale_ratio = static_cast<float>(content_rect.width()) /
                            capture_info_.copy_rect.width();

  const gfx::Insets original_scroll_insets = capture_info_.scrollbar_insets;
  const auto scroll_insets = gfx::Insets::TLBR(
      0, 0, std::round(original_scroll_insets.width() * scale_ratio),
      std::round(original_scroll_insets.height() * scale_ratio));
  gfx::Rect effective_content_rect = content_rect;
  effective_content_rect.Inset(scroll_insets);

  const gfx::Size bitmap_size(content_rect.right(), content_rect.bottom());
  const SkImageInfo image_info = SkImageInfo::MakeN32(
      bitmap_size.width(), bitmap_size.height(), kPremul_SkAlphaType,
      info->color_space.ToSkColorSpace());
  const size_t row_bytes =
      media::VideoFrame::RowBytes(media::VideoFrame::Plane::kARGB,
                                  info->pixel_format, info->coded_size.width());
  CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes));
  SkBitmap frame;
  frame.installPixels(
      image_info, pixels, row_bytes,
      [](void* addr, void* context) {
        delete static_cast<FramePinner*>(context);
      },
      new FramePinner{std::move(mapping), callbacks_remote.Unbind()});
  frame.setImmutable();

  SkBitmap cropped_frame;
  if (!frame.extractSubset(&cropped_frame,
                           gfx::RectToSkIRect(effective_content_rect))) {
    return;
  }

  got_frame_callback_.Run(cropped_frame, frame_id);
}

void BackgroundThumbnailVideoCapturer::OnNewCaptureVersion(
    const media::CaptureVersion& capture_version) {}

void BackgroundThumbnailVideoCapturer::OnFrameWithEmptyRegionCapture() {}

void BackgroundThumbnailVideoCapturer::OnStopped() {}

void BackgroundThumbnailVideoCapturer::OnLog(const std::string& /*message*/) {}
