/*
 *  Copyright 2022 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "modules/desktop_capture/screen_capturer_fuchsia.h"

#include <fuchsia/sysmem2/cpp/fidl.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/ui/display/singleton/cpp/fidl.h>
#include <lib/sys/cpp/component_context.h>

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>

#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "modules/desktop_capture/blank_detector_desktop_capturer_wrapper.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include "modules/desktop_capture/desktop_capture_types.h"
#include "modules/desktop_capture/desktop_capturer.h"
#include "modules/desktop_capture/desktop_frame.h"
#include "modules/desktop_capture/desktop_geometry.h"
#include "modules/desktop_capture/fallback_desktop_capturer_wrapper.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/divide_round.h"

namespace webrtc {

namespace {

static constexpr uint32_t kMinBufferCount = 2;
static constexpr uint32_t kFuchsiaBytesPerPixel = 4;
static constexpr DesktopCapturer::SourceId kFuchsiaScreenId = 1;
// 500 milliseconds
static constexpr zx::duration kEventDelay = zx::msec(500);
static constexpr fuchsia::images2::ColorSpace kSRGBColorSpace =
    fuchsia::images2::ColorSpace::SRGB;
static constexpr fuchsia::images2::PixelFormat kBGRA32PixelFormatType =
    fuchsia::images2::PixelFormat::B8G8R8A8;

// Round |value| up to the closest multiple of |multiple|
size_t RoundUpToMultiple(size_t value, size_t multiple) {
  return DivideRoundUp(value, multiple) * multiple;
}

}  // namespace

std::unique_ptr<DesktopCapturer> DesktopCapturer::CreateRawScreenCapturer(
    const DesktopCaptureOptions& options) {
  RTC_LOG(LS_INFO) << "DesktopCapturer::CreateRawScreenCapturer creates "
                      "DesktopCapturer of type ScreenCapturerFuchsia";
  std::unique_ptr<ScreenCapturerFuchsia> capturer(
      new ScreenCapturerFuchsia(options));
  return capturer;
}

ScreenCapturerFuchsia::ScreenCapturerFuchsia(
    const DesktopCaptureOptions& options)
    : component_context_(sys::ComponentContext::Create()),
      clock_(options.clock()) {}

ScreenCapturerFuchsia::~ScreenCapturerFuchsia() {
  // unmap virtual memory mapped pointers
  uint32_t virt_mem_bytes =
      buffer_collection_info_.settings().buffer_settings().size_bytes();
  for (uint32_t buffer_index = 0;
       buffer_index < buffer_collection_info_.buffers().size();
       buffer_index++) {
    uintptr_t address =
        reinterpret_cast<uintptr_t>(virtual_memory_mapped_addrs_[buffer_index]);
    zx_status_t status = zx::vmar::root_self()->unmap(address, virt_mem_bytes);
    RTC_DCHECK(status == ZX_OK);
  }
}

void ScreenCapturerFuchsia::Start(Callback* callback) {
  RTC_DCHECK(!callback_);
  RTC_DCHECK(callback);
  callback_ = callback;

  fatal_error_ = false;

  SetupBuffers();
}

void ScreenCapturerFuchsia::CaptureFrame() {
  if (fatal_error_) {
    callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr);
    return;
  }

  Timestamp capture_start_time = clock_.CurrentTime();

  zx::event event;
  zx::event dup;
  zx_status_t status = zx::event::create(0, &event);
  if (status != ZX_OK) {
    RTC_LOG(LS_ERROR) << "Failed to create event: " << status;
    callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr);
    return;
  }
  event.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup);

  fuchsia::ui::composition::GetNextFrameArgs next_frame_args;
  next_frame_args.set_event(std::move(dup));

  fuchsia::ui::composition::ScreenCapture_GetNextFrame_Result result;
  screen_capture_->GetNextFrame(std::move(next_frame_args), &result);
  if (result.is_err()) {
    RTC_LOG(LS_ERROR) << "fuchsia.ui.composition.GetNextFrame() failed: "
                      << result.err() << "\n";
    callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr);
    return;
  }

  status = event.wait_one(ZX_EVENT_SIGNALED, zx::deadline_after(kEventDelay),
                          nullptr);
  if (status != ZX_OK) {
    RTC_LOG(LS_ERROR) << "Timed out waiting for ScreenCapture to render frame: "
                      << status;
    callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr);
    return;
  }
  uint32_t buffer_index = result.response().buffer_id();

  // TODO(bugs.webrtc.org/14097): Use SharedMemoryDesktopFrame and
  // ScreenCaptureFrameQueue
  std::unique_ptr<BasicDesktopFrame> frame(
      new BasicDesktopFrame(DesktopSize(width_, height_)));

  uint32_t pixels_per_row = GetPixelsPerRow(
      buffer_collection_info_.settings().image_format_constraints());
  uint32_t stride = kFuchsiaBytesPerPixel * pixels_per_row;
  frame->CopyPixelsFrom(virtual_memory_mapped_addrs_[buffer_index], stride,
                        DesktopRect::MakeWH(width_, height_));
  // Mark the whole screen as having been updated.
  frame->mutable_updated_region()->SetRect(
      DesktopRect::MakeWH(width_, height_));

  fuchsia::ui::composition::ScreenCapture_ReleaseFrame_Result release_result;
  screen_capture_->ReleaseFrame(buffer_index, &release_result);
  if (release_result.is_err()) {
    RTC_LOG(LS_ERROR) << "fuchsia.ui.composition.ReleaseFrame() failed: "
                      << release_result.err();
  }

  int capture_time_ms = (clock_.CurrentTime() - capture_start_time).ms();
  frame->set_capture_time_ms(capture_time_ms);
  callback_->OnCaptureResult(Result::SUCCESS, std::move(frame));
}

bool ScreenCapturerFuchsia::GetSourceList(SourceList* screens) {
  RTC_DCHECK(screens->size() == 0);
  // Fuchsia only supports single monitor display at this point
  screens->push_back({kFuchsiaScreenId, std::string("Fuchsia monitor")});
  return true;
}

bool ScreenCapturerFuchsia::SelectSource(SourceId id) {
  if (id == kFuchsiaScreenId || id == kFullDesktopScreenId) {
    return true;
  }
  return false;
}

fuchsia::sysmem2::BufferCollectionConstraints
ScreenCapturerFuchsia::GetBufferConstraints() {
  fuchsia::sysmem2::BufferCollectionConstraints constraints;
  constraints.mutable_usage()->set_cpu(fuchsia::sysmem2::CPU_USAGE_READ |
                                       fuchsia::sysmem2::CPU_USAGE_WRITE);
  constraints.set_min_buffer_count(kMinBufferCount);

  auto& memory_constraints = *constraints.mutable_buffer_memory_constraints();
  memory_constraints.set_ram_domain_supported(true);
  memory_constraints.set_cpu_domain_supported(true);

  fuchsia::sysmem2::ImageFormatConstraints& image_constraints =
      constraints.mutable_image_format_constraints()->emplace_back();
  image_constraints.mutable_color_spaces()->emplace_back(kSRGBColorSpace);
  image_constraints.set_pixel_format(kBGRA32PixelFormatType);
  image_constraints.set_pixel_format_modifier(
      fuchsia::images2::PixelFormatModifier::LINEAR);

  image_constraints.set_required_min_size(
      fuchsia::math::SizeU{width_, height_});
  image_constraints.set_required_max_size(
      fuchsia::math::SizeU{width_, height_});

  image_constraints.set_bytes_per_row_divisor(kFuchsiaBytesPerPixel);

  return constraints;
}

void ScreenCapturerFuchsia::SetupBuffers() {
  fuchsia::ui::display::singleton::InfoSyncPtr display_info;
  zx_status_t status =
      component_context_->svc()->Connect(display_info.NewRequest());
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "Failed to connect to fuchsia.ui.display.singleton.Info: " << status;
    return;
  }

  fuchsia::ui::display::singleton::Metrics metrics;
  status = display_info->GetMetrics(&metrics);
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to connect to get display dimensions: "
                      << status;
    return;
  }
  width_ = metrics.extent_in_px().width;
  height_ = metrics.extent_in_px().height;

  status = component_context_->svc()->Connect(sysmem_allocator_.NewRequest());
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to connect to fuchsia.sysmem2.Allocator: "
                      << status;
    return;
  }

  fuchsia::sysmem2::BufferCollectionTokenSyncPtr sysmem_token;
  status = sysmem_allocator_->AllocateSharedCollection(
      std::move(fuchsia::sysmem2::AllocatorAllocateSharedCollectionRequest{}
                    .set_token_request(sysmem_token.NewRequest())));
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "fuchsia.sysmem2.Allocator.AllocateSharedCollection() failed: "
        << status;
    return;
  }

  fuchsia::sysmem2::BufferCollectionTokenSyncPtr flatland_token;
  status = sysmem_token->Duplicate(
      std::move(fuchsia::sysmem2::BufferCollectionTokenDuplicateRequest{}
                    .set_rights_attenuation_mask(ZX_RIGHT_SAME_RIGHTS)
                    .set_token_request(flatland_token.NewRequest())));
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "fuchsia.sysmem2.BufferCollectionToken.Duplicate() failed: "
        << status;
    return;
  }

  fuchsia::sysmem2::Node_Sync_Result sync_result;
  status = sysmem_token->Sync(&sync_result);
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "fuchsia.sysmem2.BufferCollectionToken.Sync() failed: "
                      << status;
    return;
  }

  status = sysmem_allocator_->BindSharedCollection(
      std::move(fuchsia::sysmem2::AllocatorBindSharedCollectionRequest{}
                    .set_token(std::move(sysmem_token))
                    .set_buffer_collection_request(collection_.NewRequest())));
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "fuchsia.sysmem2.Allocator.BindSharedCollection() failed: "
        << status;
    return;
  }

  status = collection_->SetConstraints(std::move(
      fuchsia::sysmem2::BufferCollectionSetConstraintsRequest{}.set_constraints(
          GetBufferConstraints())));
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "fuchsia.sysmem2.BufferCollection.SetConstraints() failed: "
        << status;
    return;
  }

  fuchsia::ui::composition::BufferCollectionImportToken import_token;
  fuchsia::ui::composition::BufferCollectionExportToken export_token;
  status = zx::eventpair::create(0, &export_token.value, &import_token.value);
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "Failed to create BufferCollection import and export tokens: "
        << status;
    return;
  }

  status = component_context_->svc()->Connect(flatland_allocator_.NewRequest());
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to connect to Flatland Allocator: " << status;
    return;
  }

  fuchsia::ui::composition::RegisterBufferCollectionArgs buffer_collection_args;
  buffer_collection_args.set_export_token(std::move(export_token));
  buffer_collection_args.set_buffer_collection_token2(
      std::move(flatland_token));
  buffer_collection_args.set_usage(
      fuchsia::ui::composition::RegisterBufferCollectionUsage::SCREENSHOT);

  fuchsia::ui::composition::Allocator_RegisterBufferCollection_Result
      buffer_collection_result;
  flatland_allocator_->RegisterBufferCollection(
      std::move(buffer_collection_args), &buffer_collection_result);
  if (buffer_collection_result.is_err()) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "fuchsia.ui.composition.Allocator."
                         "RegisterBufferCollection() failed.";
    return;
  }

  fuchsia::sysmem2::BufferCollection_WaitForAllBuffersAllocated_Result
      wait_result;
  status = collection_->WaitForAllBuffersAllocated(&wait_result);
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to wait for buffer collection info: "
                      << status;
    return;
  }
  if (!wait_result.is_response()) {
    if (wait_result.is_framework_err()) {
      RTC_LOG(LS_ERROR)
          << "Failed to allocate buffer collection (framework_err): "
          << fidl::ToUnderlying(wait_result.framework_err());
    } else {
      RTC_LOG(LS_ERROR) << "Failed to allocate buffer collection (err): "
                        << static_cast<uint32_t>(wait_result.err());
    }
    fatal_error_ = true;
    return;
  }
  buffer_collection_info_ =
      std::move(*wait_result.response().mutable_buffer_collection_info());

  status = collection_->Release();
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to close buffer collection token: " << status;
    return;
  }

  status = component_context_->svc()->Connect(screen_capture_.NewRequest());
  if (status != ZX_OK) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR) << "Failed to connect to Screen Capture: " << status;
    return;
  }

  // Configure buffers in ScreenCapture client.
  fuchsia::ui::composition::ScreenCaptureConfig configure_args;
  configure_args.set_import_token(std::move(import_token));
  configure_args.set_buffer_count(buffer_collection_info_.buffers().size());
  configure_args.set_size({width_, height_});

  fuchsia::ui::composition::ScreenCapture_Configure_Result configure_result;
  screen_capture_->Configure(std::move(configure_args), &configure_result);
  if (configure_result.is_err()) {
    fatal_error_ = true;
    RTC_LOG(LS_ERROR)
        << "fuchsia.ui.composition.ScreenCapture.Configure() failed: "
        << configure_result.err();
    return;
  }

  // We have a collection of virtual memory objects which the ScreenCapture
  // client will write the frame data to when requested. We map each of these
  // onto a pointer stored in virtual_memory_mapped_addrs_ which we can use to
  // access this data.
  uint32_t virt_mem_bytes =
      buffer_collection_info_.settings().buffer_settings().size_bytes();
  RTC_DCHECK(virt_mem_bytes > 0);
  for (uint32_t buffer_index = 0;
       buffer_index < buffer_collection_info_.buffers().size();
       buffer_index++) {
    const zx::vmo& virt_mem =
        buffer_collection_info_.buffers()[buffer_index].vmo();
    virtual_memory_mapped_addrs_[buffer_index] = nullptr;
    status = zx::vmar::root_self()->map(
        ZX_VM_PERM_READ, /*vmar_offset*/ 0, virt_mem,
        /*vmo_offset*/ 0, virt_mem_bytes,
        reinterpret_cast<uintptr_t*>(
            &virtual_memory_mapped_addrs_[buffer_index]));
    if (status != ZX_OK) {
      fatal_error_ = true;
      RTC_LOG(LS_ERROR) << "Failed to map virtual memory: " << status;
      return;
    }
  }
}

uint32_t ScreenCapturerFuchsia::GetPixelsPerRow(
    const fuchsia::sysmem2::ImageFormatConstraints& constraints) {
  uint32_t stride = RoundUpToMultiple(
      std::max(constraints.min_bytes_per_row(), width_ * kFuchsiaBytesPerPixel),
      constraints.bytes_per_row_divisor());
  uint32_t pixels_per_row = stride / kFuchsiaBytesPerPixel;

  return pixels_per_row;
}

}  // namespace webrtc
