// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/gpu/chromeos/platform_video_frame_utils.h"

#include <linux/memfd.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/mman.h>
#include <unistd.h>

#include <optional>
#include <utility>
#include <vector>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h"
#include "gpu/command_buffer/client/test_shared_image_interface.h"
#include "media/base/color_plane_layout.h"
#include "media/base/format_utils.h"
#include "media/base/video_frame.h"
#include "media/base/video_frame_layout.h"
#include "media/base/video_types.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/buffer_types.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/linux/native_pixmap_dmabuf.h"
#include "ui/gfx/native_pixmap_handle.h"

namespace media {

namespace {

// Creates mock FDs and wrap them into a VideoFrame.
scoped_refptr<VideoFrame> CreateMockDmaBufVideoFrame(
    VideoPixelFormat pixel_format,
    const gfx::Size& coded_size,
    const gfx::Rect& visible_rect,
    const gfx::Size& natural_size) {
  size_t num_planes = VideoFrame::NumPlanes(pixel_format);
  std::vector<ColorPlaneLayout> planes;
  std::vector<base::ScopedFD> dmabuf_fds;
  for (size_t i = 0; i < num_planes; i++) {
    const gfx::Size plane_size_in_bytes =
        VideoFrame::PlaneSize(pixel_format, i, coded_size);
    // Placeholder plane fd.
    base::ScopedFD fd(memfd_create("test_shared_image", MFD_CLOEXEC));
    CHECK(fd.is_valid());
    CHECK_EQ(ftruncate(fd.get(), plane_size_in_bytes.GetArea()), 0);
    dmabuf_fds.emplace_back(std::move(fd));
    if (!dmabuf_fds.back().is_valid()) {
      LOG(ERROR) << "The FD taken from file is not valid";
      return nullptr;
    }

    planes.emplace_back(plane_size_in_bytes.width(), /*offset=*/0,
                        plane_size_in_bytes.GetArea());
  }
  const std::optional<VideoFrameLayout> layout =
      VideoFrameLayout::CreateWithPlanes(pixel_format, coded_size, planes);
  if (!layout) {
    LOG(ERROR) << "Failed to create video frame layout";
    return nullptr;
  }

  return VideoFrame::WrapExternalDmabufs(*layout, visible_rect, natural_size,
                                         std::move(dmabuf_fds),
                                         base::TimeDelta());
}
}  // namespace

TEST(PlatformVideoFrameUtilsTest, UniqueTrackingTokenHelperTests) {
  UniqueTrackingTokenHelper helper;

  VideoFrameMetadata metadata;
  CHECK(!metadata.tracking_token.has_value());

  // Tests that SetUniqueTrackingToken will set |tracking_token| when passed a
  // VideoFrameMetadata without |tracking_token| set.
  helper.SetUniqueTrackingToken(metadata);
  ASSERT_TRUE(metadata.tracking_token.has_value());
  ASSERT_FALSE(metadata.tracking_token->is_empty());

  // Tests that SetUniqueTrackingToken will set |tracking_token| when passed a
  // VideoFrameMetadata an empty, but set |tracking_token|. The constructor
  // add an empty base::UnguessableToken to avoid users being able to register
  // empty tokens.
  metadata.tracking_token = base::UnguessableToken();
  CHECK(metadata.tracking_token->is_empty());
  helper.SetUniqueTrackingToken(metadata);
  EXPECT_FALSE(metadata.tracking_token->is_empty());

  // Check that inserting an already inserted token results in a new token
  // getting generated.
  base::UnguessableToken token = *metadata.tracking_token;
  helper.SetUniqueTrackingToken(metadata);
  ASSERT_TRUE(metadata.tracking_token.has_value());
  EXPECT_NE(*metadata.tracking_token, token);
}

TEST(PlatformVideoFrameUtilsTest, CreateNativePixmapDmaBuf) {
  constexpr VideoPixelFormat kPixelFormat = PIXEL_FORMAT_NV12;
  constexpr gfx::Size kCodedSize(320, 240);

  const std::optional<viz::SharedImageFormat> si_format =
      VideoPixelFormatToSharedImageFormat(kPixelFormat);
  ASSERT_TRUE(si_format) << "Invalid pixel format: " << kPixelFormat;

  scoped_refptr<VideoFrame> video_frame = CreateMockDmaBufVideoFrame(
      kPixelFormat, kCodedSize, gfx::Rect(kCodedSize), kCodedSize);
  ASSERT_TRUE(video_frame);

  // Create a native pixmap and verify its metadata.
  scoped_refptr<gfx::NativePixmapDmaBuf> native_pixmap =
      CreateNativePixmapDmaBuf(video_frame.get());
  ASSERT_TRUE(native_pixmap);
  EXPECT_EQ(native_pixmap->GetSharedImageFormat(), *si_format);
  EXPECT_EQ(native_pixmap->GetFormatModifier(),
            video_frame->layout().modifier());

  // Verify the DMA Buf layouts are the same.
  const size_t num_planes = video_frame->layout().num_planes();
  ASSERT_EQ(native_pixmap->ExportHandle().planes.size(), num_planes);
  for (size_t i = 0; i < num_planes; i++) {
    const ColorPlaneLayout& plane = video_frame->layout().planes()[i];
    // The original and duplicated FDs should be different.
    EXPECT_NE(native_pixmap->GetDmaBufFd(i), video_frame->GetDmabufFd(i));
    EXPECT_EQ(native_pixmap->GetDmaBufPitch(i),
              base::checked_cast<uint32_t>(plane.stride));
    EXPECT_EQ(native_pixmap->GetDmaBufOffset(i), plane.offset);
    EXPECT_EQ(native_pixmap->GetDmaBufPlaneSize(i), plane.size);
  }
}

// TODO(b/230370976): remove this #if/#endif guard. To do so, we need to be able
// to mock/fake the allocator used by CreatePlatformVideoFrame() and
// CreateMappableSharedImageVideoFrame() so that those functions return a
// non-nullptr frame on platforms where allocating NV12 buffers is not
// supported.
#if BUILDFLAG(IS_CHROMEOS)
TEST(PlatformVideoFrameUtilsTest, CreateVideoFrame) {
  auto test_sii = base::MakeRefCounted<gpu::TestSharedImageInterface>();
  constexpr VideoPixelFormat kPixelFormat = PIXEL_FORMAT_NV12;
  constexpr gfx::Size kCodedSize(320, 240);
  constexpr gfx::Rect kVisibleRect(kCodedSize);
  constexpr gfx::Size kNaturalSize(kCodedSize);
  constexpr gfx::ColorSpace kColorSpace(gfx::ColorSpace::CreateREC709());
  constexpr auto kTimeStamp = base::Milliseconds(1234);
  constexpr gfx::BufferUsage kBufferUsage =
      gfx::BufferUsage::VEA_READ_CAMERA_AND_CPU_READ_WRITE;

  const VideoFrame::StorageType storage_types[] = {
      VideoFrame::STORAGE_DMABUFS,
      VideoFrame::STORAGE_MAPPABLE_SHARED_IMAGE,
  };
  for (const auto& storage_type : storage_types) {
    scoped_refptr<VideoFrame> frame;
    switch (storage_type) {
      case VideoFrame::STORAGE_DMABUFS:
        frame =
            CreatePlatformVideoFrame(kPixelFormat, kCodedSize, kVisibleRect,
                                     kNaturalSize, kTimeStamp, kBufferUsage);
        break;
      case VideoFrame::STORAGE_MAPPABLE_SHARED_IMAGE:
        frame = CreateMappableSharedImageVideoFrame(
            kPixelFormat, kColorSpace, kCodedSize, kVisibleRect, kNaturalSize,
            kTimeStamp, kBufferUsage, test_sii.get());
        break;
      default:
        NOTREACHED();
    };

    ASSERT_TRUE(frame);
    EXPECT_EQ(frame->format(), kPixelFormat);
    EXPECT_EQ(frame->coded_size(), kCodedSize);
    EXPECT_EQ(frame->visible_rect(), kVisibleRect);
    EXPECT_EQ(frame->natural_size(), kNaturalSize);
    EXPECT_EQ(frame->timestamp(), kTimeStamp);
    EXPECT_EQ(frame->storage_type(), storage_type);

    switch (storage_type) {
      case VideoFrame::STORAGE_DMABUFS:
        EXPECT_FALSE(frame->NumDmabufFds() == 0);
        break;
      case VideoFrame::STORAGE_MAPPABLE_SHARED_IMAGE:
        EXPECT_FALSE(frame->GetGpuMemoryBufferHandle().is_null());
        break;
      default:
        NOTREACHED();
    };
  }
}
#endif  // BUILDFLAG(IS_CHROMEOS)
}  // namespace media
