// Copyright 2022 Google LLC
// SPDX-License-Identifier: BSD-2-Clause

#include <algorithm>
#include <cmath>
#include <memory>
#include <tuple>

#include "avif/avif.h"
#include "aviftest_helpers.h"
#include "gtest/gtest.h"

using ::testing::Bool;
using ::testing::Combine;
using ::testing::Range;
using ::testing::Values;

namespace libavif {
namespace {

//------------------------------------------------------------------------------

constexpr uint32_t kModifierSize = 4 * 4;

// Modifies the pixel values of a channel in image by modifier[] (row-ordered).
template <typename PixelType>
void ModifyImageChannel(avifRGBImage* image, uint32_t channel_offset,
                        const uint8_t modifier[kModifierSize]) {
  const uint32_t channel_count = avifRGBFormatChannelCount(image->format);
  assert(channel_offset < channel_count);
  for (uint32_t y = 0, i = 0; y < image->height; ++y) {
    PixelType* pixel =
        reinterpret_cast<PixelType*>(image->pixels + image->rowBytes * y);
    for (uint32_t x = 0; x < image->width; ++x, ++i) {
      pixel[channel_offset] += modifier[i % kModifierSize];
      pixel += channel_count;
    }
  }
}

void ModifyImageChannel(avifRGBImage* image, uint32_t channel_offset,
                        const uint8_t modifier[kModifierSize]) {
  if (image->depth <= 8) {
    ModifyImageChannel<uint8_t>(image, channel_offset, modifier);
  } else {
    ModifyImageChannel<uint16_t>(image, channel_offset, modifier);
  }
}

// Accumulates stats about the differences between the images a and b.
template <typename PixelType>
void GetDiffSumAndSqDiffSum(const avifRGBImage& a, const avifRGBImage& b,
                            int64_t* diff_sum, int64_t* abs_diff_sum,
                            int64_t* sq_diff_sum, int64_t* max_abs_diff) {
  const uint32_t channel_count = avifRGBFormatChannelCount(a.format);
  for (uint32_t y = 0; y < a.height; ++y) {
    const PixelType* row_a =
        reinterpret_cast<PixelType*>(a.pixels + a.rowBytes * y);
    const PixelType* row_b =
        reinterpret_cast<PixelType*>(b.pixels + b.rowBytes * y);
    for (uint32_t x = 0; x < a.width * channel_count; ++x) {
      const int64_t diff =
          static_cast<int64_t>(row_b[x]) - static_cast<int64_t>(row_a[x]);
      *diff_sum += diff;
      *abs_diff_sum += std::abs(diff);
      *sq_diff_sum += diff * diff;
      *max_abs_diff = std::max(*max_abs_diff, std::abs(diff));
    }
  }
}

void GetDiffSumAndSqDiffSum(const avifRGBImage& a, const avifRGBImage& b,
                            int64_t* diff_sum, int64_t* abs_diff_sum,
                            int64_t* sq_diff_sum, int64_t* max_abs_diff) {
  (a.depth <= 8) ? GetDiffSumAndSqDiffSum<uint8_t>(a, b, diff_sum, abs_diff_sum,
                                                   sq_diff_sum, max_abs_diff)
                 : GetDiffSumAndSqDiffSum<uint16_t>(
                       a, b, diff_sum, abs_diff_sum, sq_diff_sum, max_abs_diff);
}

// Returns the Peak Signal-to-Noise Ratio from accumulated stats.
double GetPsnr(double sq_diff_sum, double num_diffs, double max_abs_diff) {
  if (sq_diff_sum == 0.) {
    return 99.;  // Lossless.
  }
  const double distortion =
      sq_diff_sum / (num_diffs * max_abs_diff * max_abs_diff);
  return (distortion > 0.) ? std::min(-10 * std::log10(distortion), 98.9)
                           : 98.9;  // Not lossless.
}

//------------------------------------------------------------------------------

// To exercise the chroma subsampling loss, the input samples must differ in
// each of the RGB channels. Chroma subsampling expects the input RGB channels
// to be correlated to minimize the quality loss.
constexpr uint8_t kRedNoise[kModifierSize] = {
    7,  14, 11, 5,   // Random permutation of 16 values.
    4,  6,  8,  15,  //
    2,  9,  13, 3,   //
    12, 1,  10, 0};
constexpr uint8_t kGreenNoise[kModifierSize] = {
    3,  2,  12, 15,  // Random permutation of 16 values
    14, 10, 7,  13,  // that is somewhat close to kRedNoise.
    5,  1,  9,  0,   //
    8,  4,  11, 6};
constexpr uint8_t kBlueNoise[kModifierSize] = {
    0,  8,  14, 9,   // Random permutation of 16 values
    13, 12, 2,  7,   // that is somewhat close to kGreenNoise.
    3,  1,  11, 10,  //
    6,  15, 5,  4};

//------------------------------------------------------------------------------

// Converts from RGB to YUV and back to RGB for all RGB combinations, separated
// by a color step for reasonable timing. If add_noise is true, also applies
// some noise to the input samples to exercise chroma subsampling.
void ConvertWholeRange(int rgb_depth, int yuv_depth, avifRGBFormat rgb_format,
                       avifPixelFormat yuv_format, avifRange yuv_range,
                       avifMatrixCoefficients matrix_coefficients,
                       avifChromaDownsampling chroma_downsampling,
                       bool add_noise, uint32_t rgb_step,
                       double max_abs_average_diff, double min_psnr, bool log) {
  // Deduced constants.
  const bool is_monochrome =
      (yuv_format == AVIF_PIXEL_FORMAT_YUV400);  // If so, only test grey input.
  const uint32_t rgb_max = (1 << rgb_depth) - 1;

  // The YUV upsampling treats the first and last rows and columns differently
  // than the remaining pairs of rows and columns. An image of 16 pixels is used
  // to test all these possibilities.
  static constexpr int width = 4;
  static constexpr int height = 4;
  testutil::AvifImagePtr yuv(
      avifImageCreate(width, height, yuv_depth, yuv_format), avifImageDestroy);
  ASSERT_NE(yuv, nullptr);
  yuv->matrixCoefficients = matrix_coefficients;
  yuv->yuvRange = yuv_range;
  testutil::AvifRgbImage src_rgb(yuv.get(), rgb_depth, rgb_format);
  src_rgb.chromaDownsampling = chroma_downsampling;
  testutil::AvifRgbImage dst_rgb(yuv.get(), rgb_depth, rgb_format);
  const testutil::RgbChannelOffsets offsets =
      testutil::GetRgbChannelOffsets(rgb_format);

  // Alpha values are not tested here. Keep it opaque.
  if (avifRGBFormatHasAlpha(src_rgb.format)) {
    testutil::FillImageChannel(&src_rgb, offsets.a, rgb_max);
  }

  // Estimate the loss from converting RGB values to YUV and back.
  int64_t diff_sum = 0, abs_diff_sum = 0, sq_diff_sum = 0, max_abs_diff = 0;
  int64_t num_diffs = 0;
  const uint32_t max_value = rgb_max - (add_noise ? 15 : 0);
  for (uint32_t r = 0; r < max_value + rgb_step; r += rgb_step) {
    r = std::min(r, max_value);  // Test the maximum sample value even if it is
                                 // not a multiple of rgb_step.
    testutil::FillImageChannel(&src_rgb, offsets.r, r);
    if (add_noise) {
      ModifyImageChannel(&src_rgb, offsets.r, kRedNoise);
    }

    if (is_monochrome) {
      // Test only greyish input when converting to a single channel.
      testutil::FillImageChannel(&src_rgb, offsets.g, r);
      testutil::FillImageChannel(&src_rgb, offsets.b, r);
      if (add_noise) {
        ModifyImageChannel(&src_rgb, offsets.g, kGreenNoise);
        ModifyImageChannel(&src_rgb, offsets.b, kBlueNoise);
      }

      ASSERT_EQ(avifImageRGBToYUV(yuv.get(), &src_rgb), AVIF_RESULT_OK);
      ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &dst_rgb), AVIF_RESULT_OK);
      GetDiffSumAndSqDiffSum(src_rgb, dst_rgb, &diff_sum, &abs_diff_sum,
                             &sq_diff_sum, &max_abs_diff);
      num_diffs += src_rgb.width * src_rgb.height * 3;  // Alpha is lossless.
    } else {
      for (uint32_t g = 0; g < max_value + rgb_step; g += rgb_step) {
        g = std::min(g, max_value);
        testutil::FillImageChannel(&src_rgb, offsets.g, g);
        if (add_noise) {
          ModifyImageChannel(&src_rgb, offsets.g, kGreenNoise);
        }
        for (uint32_t b = 0; b < max_value + rgb_step; b += rgb_step) {
          b = std::min(b, max_value);
          testutil::FillImageChannel(&src_rgb, offsets.b, b);
          if (add_noise) {
            ModifyImageChannel(&src_rgb, offsets.b, kBlueNoise);
          }

          const avifResult result = avifImageRGBToYUV(yuv.get(), &src_rgb);
          if (result == AVIF_RESULT_NOT_IMPLEMENTED &&
              src_rgb.chromaDownsampling ==
                  AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV) {
            GTEST_SKIP() << "libsharpyuv unavailable, skip test.";
          }
          ASSERT_EQ(result, AVIF_RESULT_OK);
          ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &dst_rgb), AVIF_RESULT_OK);
          GetDiffSumAndSqDiffSum(src_rgb, dst_rgb, &diff_sum, &abs_diff_sum,
                                 &sq_diff_sum, &max_abs_diff);
          num_diffs +=
              src_rgb.width * src_rgb.height * 3;  // Alpha is lossless.
        }
      }
    }
  }

  // Stats and thresholds.
  // Note: The thresholds defined in this test are calibrated for libyuv fast
  //       paths. See reformat_libyuv.c. Slower non-libyuv conversions in
  //       libavif have a higher precision (using floating point operations).
  const double average_diff =
      static_cast<double>(diff_sum) / static_cast<double>(num_diffs);
  const double average_abs_diff =
      static_cast<double>(abs_diff_sum) / static_cast<double>(num_diffs);
  const double psnr = GetPsnr(static_cast<double>(sq_diff_sum),
                              static_cast<double>(num_diffs), rgb_max);
  EXPECT_LE(std::abs(average_diff), max_abs_average_diff);
  EXPECT_GE(psnr, min_psnr);

  if (log) {
    // Print stats for convenience and easier threshold tuning.
    static constexpr const char* kAvifRgbFormatToString[] = {
        "RGB", "RGBA", "ARGB", "BGR", "BGRA", "ABGR"};
    std::cout << " RGB " << rgb_depth << " bits, YUV " << yuv_depth << " bits, "
              << kAvifRgbFormatToString[rgb_format] << ", "
              << avifPixelFormatToString(yuv_format) << ", "
              << (yuv_range ? "full" : "lmtd") << ", MC " << matrix_coefficients
              << ", " << (add_noise ? "noisy" : "plain") << ", avg "
              << average_diff << ", abs avg " << average_abs_diff << ", max "
              << max_abs_diff << ", PSNR " << psnr << "dB" << std::endl;
  }
}

// Converts from RGB to YUV and back to RGB for multiple buffer dimensions to
// exercise stride computation and subsampling edge cases.
void ConvertWholeBuffer(int rgb_depth, int yuv_depth, avifRGBFormat rgb_format,
                        avifPixelFormat yuv_format, avifRange yuv_range,
                        avifMatrixCoefficients matrix_coefficients,
                        avifChromaDownsampling chroma_downsampling,
                        bool add_noise, double min_psnr) {
  // Deduced constants.
  const bool is_monochrome =
      (yuv_format == AVIF_PIXEL_FORMAT_YUV400);  // If so, only test grey input.
  const uint32_t rgb_max = (1 << rgb_depth) - 1;

  // Estimate the loss from converting RGB values to YUV and back.
  int64_t diff_sum = 0, abs_diff_sum = 0, sq_diff_sum = 0, max_abs_diff = 0;
  int64_t num_diffs = 0;
  for (int width : {1, 2, 127}) {
    for (int height : {1, 2, 251}) {
      testutil::AvifImagePtr yuv(
          avifImageCreate(width, height, yuv_depth, yuv_format),
          avifImageDestroy);
      ASSERT_NE(yuv, nullptr);
      yuv->matrixCoefficients = matrix_coefficients;
      yuv->yuvRange = yuv_range;
      testutil::AvifRgbImage src_rgb(yuv.get(), rgb_depth, rgb_format);
      src_rgb.chromaDownsampling = chroma_downsampling;
      testutil::AvifRgbImage dst_rgb(yuv.get(), rgb_depth, rgb_format);
      const testutil::RgbChannelOffsets offsets =
          testutil::GetRgbChannelOffsets(rgb_format);

      // Fill the input buffer with whatever content.
      testutil::FillImageChannel(&src_rgb, offsets.r, /*value=*/0);
      testutil::FillImageChannel(&src_rgb, offsets.g, /*value=*/0);
      testutil::FillImageChannel(&src_rgb, offsets.b, /*value=*/0);
      if (add_noise) {
        ModifyImageChannel(&src_rgb, offsets.r, kRedNoise);
        ModifyImageChannel(&src_rgb, offsets.g,
                           is_monochrome ? kRedNoise : kGreenNoise);
        ModifyImageChannel(&src_rgb, offsets.b,
                           is_monochrome ? kRedNoise : kBlueNoise);
      }
      // Alpha values are not tested here. Keep it opaque.
      if (avifRGBFormatHasAlpha(src_rgb.format)) {
        testutil::FillImageChannel(&src_rgb, offsets.a, rgb_max);
      }

      const avifResult result = avifImageRGBToYUV(yuv.get(), &src_rgb);
      if (result == AVIF_RESULT_NOT_IMPLEMENTED &&
          src_rgb.chromaDownsampling == AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV) {
        GTEST_SKIP() << "libsharpyuv unavailable, skip test.";
      }
      ASSERT_EQ(result, AVIF_RESULT_OK);
      ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &dst_rgb), AVIF_RESULT_OK);
      GetDiffSumAndSqDiffSum(src_rgb, dst_rgb, &diff_sum, &abs_diff_sum,
                             &sq_diff_sum, &max_abs_diff);
      num_diffs += src_rgb.width * src_rgb.height * 3;
    }
  }
  // max_abs_average_diff is not tested here because it is not meaningful for
  // only 3*3 conversions as it takes the maximum difference per conversion.
  // PSNR is averaged on all pixels so it can be tested here.
  EXPECT_GE(GetPsnr(static_cast<double>(sq_diff_sum),
                    static_cast<double>(num_diffs), rgb_max),
            min_psnr);
}

//------------------------------------------------------------------------------
// Exhaustive settings
// These tests would generate too many GoogleTest instances as parameterized
// tests (TEST_P) so loops are used instead.

TEST(RGBToYUVTest, ExhaustiveSettings) {
  // Coverage of all configurations with all min/max input combinations.
  for (int rgb_depth : {8, 10, 12, 16}) {
    for (int yuv_depth : {8, 10, 12, 16}) {
      for (avifRGBFormat rgb_format :
           {AVIF_RGB_FORMAT_RGB, AVIF_RGB_FORMAT_RGBA, AVIF_RGB_FORMAT_ARGB,
            AVIF_RGB_FORMAT_BGR, AVIF_RGB_FORMAT_BGRA, AVIF_RGB_FORMAT_ABGR}) {
        for (avifPixelFormat yuv_format :
             {AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
              AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400}) {
          for (avifRange yuv_range : {AVIF_RANGE_LIMITED, AVIF_RANGE_FULL}) {
            for (decltype(AVIF_MATRIX_COEFFICIENTS_IDENTITY)
                     matrix_coefficients : {AVIF_MATRIX_COEFFICIENTS_IDENTITY,
                                            AVIF_MATRIX_COEFFICIENTS_BT601}) {
              if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
                  yuv_format != AVIF_PIXEL_FORMAT_YUV444) {
                // See avifPrepareReformatState().
                continue;
              }
              for (avifChromaDownsampling chroma_downsampling :
                   {AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
                    AVIF_CHROMA_DOWNSAMPLING_FASTEST,
                    AVIF_CHROMA_DOWNSAMPLING_BEST_QUALITY,
                    AVIF_CHROMA_DOWNSAMPLING_AVERAGE,
                    AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV}) {
                if (chroma_downsampling == AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV &&
                    yuv_depth > 12) {
                  // SharpYuvConvert() only supports YUV bit depths up to 12.
                  continue;
                }
                ConvertWholeRange(
                    rgb_depth, yuv_depth, rgb_format, yuv_format, yuv_range,
                    static_cast<avifMatrixCoefficients>(matrix_coefficients),
                    chroma_downsampling,
                    /*add_noise=*/true,
                    // Just try min and max values.
                    /*rgb_step=*/(1u << rgb_depth) - 1u,
                    // Barely check the results, this is mostly for coverage.
                    /*max_abs_average_diff=*/(1u << rgb_depth) - 1u,
                    /*min_psnr=*/5.0,
                    // Avoid spam.
                    /*log=*/false);
              }
            }
          }
        }
      }
    }
  }
}

TEST(RGBToYUVTest, AllMatrixCoefficients) {
  // Coverage of all configurations with all min/max input combinations.
  for (int rgb_depth : {8, 10, 12, 16}) {
    for (int yuv_depth : {8, 10, 12, 16}) {
      for (avifPixelFormat yuv_format :
           {AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
            AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400}) {
        for (avifRange yuv_range : {AVIF_RANGE_LIMITED, AVIF_RANGE_FULL}) {
          for (decltype(AVIF_MATRIX_COEFFICIENTS_IDENTITY) matrix_coefficients :
               {
                   AVIF_MATRIX_COEFFICIENTS_BT709,
                   AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED,
                   AVIF_MATRIX_COEFFICIENTS_FCC,
                   AVIF_MATRIX_COEFFICIENTS_BT470BG,
                   AVIF_MATRIX_COEFFICIENTS_BT601,
                   AVIF_MATRIX_COEFFICIENTS_SMPTE240,
                   AVIF_MATRIX_COEFFICIENTS_YCGCO,
                   AVIF_MATRIX_COEFFICIENTS_BT2020_NCL,
                   AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL
                   // These are unsupported. See avifPrepareReformatState().
                   // AVIF_MATRIX_COEFFICIENTS_BT2020_CL
                   // AVIF_MATRIX_COEFFICIENTS_SMPTE2085
                   // AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_CL
                   // AVIF_MATRIX_COEFFICIENTS_ICTCP
               }) {
            if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO &&
                yuv_range == AVIF_RANGE_LIMITED) {
              // See avifPrepareReformatState().
              continue;
            }
            for (avifChromaDownsampling chroma_downsampling :
                 {AVIF_CHROMA_DOWNSAMPLING_FASTEST,
                  AVIF_CHROMA_DOWNSAMPLING_BEST_QUALITY}) {
              ConvertWholeRange(
                  rgb_depth, yuv_depth, AVIF_RGB_FORMAT_RGBA, yuv_format,
                  yuv_range,
                  static_cast<avifMatrixCoefficients>(matrix_coefficients),
                  chroma_downsampling,
                  /*add_noise=*/true,
                  // Just try min and max values.
                  /*rgb_step=*/(1u << rgb_depth) - 1u,
                  // Barely check the results, this is mostly for coverage.
                  /*max_abs_average_diff=*/(1u << rgb_depth) - 1u,
                  /*min_psnr=*/5.0,
                  // Avoid spam.
                  /*log=*/false);
            }
          }
        }
      }
    }
  }
}

//------------------------------------------------------------------------------
// Selected configurations

class RGBToYUVTest
    : public testing::TestWithParam<std::tuple<
          /*rgb_depth=*/int, /*yuv_depth=*/int, avifRGBFormat, avifPixelFormat,
          avifRange, avifMatrixCoefficients, avifChromaDownsampling,
          /*add_noise=*/bool, /*rgb_step=*/uint32_t,
          /*max_abs_average_diff=*/double, /*min_psnr=*/double>> {};

TEST_P(RGBToYUVTest, ConvertWholeRange) {
  ConvertWholeRange(
      /*rgb_depth=*/std::get<0>(GetParam()),
      /*yuv_depth=*/std::get<1>(GetParam()),
      /*rgb_format=*/std::get<2>(GetParam()),
      /*yuv_format=*/std::get<3>(GetParam()),
      /*yuv_range=*/std::get<4>(GetParam()),
      /*matrix_coefficients=*/std::get<5>(GetParam()),
      /*chroma_downsampling=*/std::get<6>(GetParam()),
      // Whether to add noise to the input RGB samples.
      // Should only impact subsampled chroma (4:2:2 and 4:2:0).
      /*add_noise=*/std::get<7>(GetParam()),
      // Testing each RGB combination would be more accurate but results are
      // similar with faster settings.
      /*rgb_step=*/std::get<8>(GetParam()),
      // Thresholds to pass.
      /*max_abs_average_diff=*/std::get<9>(GetParam()),
      /*min_psnr=*/std::get<10>(GetParam()),
      // Useful to see surrounding results when there is a failure.
      /*log=*/true);
}

TEST_P(RGBToYUVTest, ConvertWholeBuffer) {
  ConvertWholeBuffer(
      /*rgb_depth=*/std::get<0>(GetParam()),
      /*yuv_depth=*/std::get<1>(GetParam()),
      /*rgb_format=*/std::get<2>(GetParam()),
      /*yuv_format=*/std::get<3>(GetParam()),
      /*yuv_range=*/std::get<4>(GetParam()),
      /*matrix_coefficients=*/std::get<5>(GetParam()),
      /*chroma_downsampling=*/std::get<6>(GetParam()),
      // Whether to add noise to the input RGB samples.
      /*add_noise=*/std::get<7>(GetParam()),
      // Threshold to pass.
      /*min_psnr=*/std::get<10>(GetParam()));
}

// avifMatrixCoefficients-typed constants for testing::Values() to work on MSVC.
// typedef or using decltype(AVIF_MATRIX_COEFFICIENTS_IDENTITY) does not work
// (GTest template "declared using unnamed type, is used but never defined").
constexpr avifMatrixCoefficients kMatrixCoefficientsBT601 =
    AVIF_MATRIX_COEFFICIENTS_BT601;
constexpr avifMatrixCoefficients kMatrixCoefficientsBT709 =
    AVIF_MATRIX_COEFFICIENTS_BT709;
constexpr avifMatrixCoefficients kMatrixCoefficientsIdentity =
    AVIF_MATRIX_COEFFICIENTS_IDENTITY;

// This is the default avifenc setup when encoding from 8b PNG files to AVIF.
INSTANTIATE_TEST_SUITE_P(
    DefaultFormat, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(8),
            /*yuv_depth=*/Values(8), Values(AVIF_RGB_FORMAT_RGBA),
            Values(AVIF_PIXEL_FORMAT_YUV420), Values(AVIF_RANGE_FULL),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Values(true),
            /*rgb_step=*/Values(3),
            /*max_abs_average_diff=*/Values(0.1),  // The color drift is almost
                                                   // centered.
            /*min_psnr=*/Values(36.)  // Subsampling distortion is acceptable.
            ));

// Keeping RGB samples in full range and same or higher bit depth should not
// bring any loss in the roundtrip.
INSTANTIATE_TEST_SUITE_P(Identity8b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(8),
                                 /*yuv_depth=*/Values(8, 10, 12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV444),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsIdentity),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(true),
                                 /*rgb_step=*/Values(31),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(Identity10b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(10),
                                 /*yuv_depth=*/Values(10, 12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV444),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsIdentity),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(true),
                                 /*rgb_step=*/Values(101),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(Identity12b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(12),
                                 /*yuv_depth=*/Values(12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV444),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsIdentity),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(true),
                                 /*rgb_step=*/Values(401),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(Identity16b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(16),
                                 /*yuv_depth=*/Values(16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV444),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsIdentity),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(true),
                                 /*rgb_step=*/Values(6421),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));

// 4:4:4 and chroma subsampling have similar distortions on plain color inputs.
INSTANTIATE_TEST_SUITE_P(
    PlainAnySubsampling8b, RGBToYUVTest,
    Combine(
        /*rgb_depth=*/Values(8),
        /*yuv_depth=*/Values(8), Values(AVIF_RGB_FORMAT_RGBA),
        Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
               AVIF_PIXEL_FORMAT_YUV420),
        Values(AVIF_RANGE_FULL), Values(kMatrixCoefficientsBT601),
        Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
        /*add_noise=*/Values(false),
        /*rgb_step=*/Values(17),
        /*max_abs_average_diff=*/Values(0.02),  // The color drift is centered.
        /*min_psnr=*/Values(45.)  // RGB>YUV>RGB distortion is barely
                                  // noticeable.
        ));

// Converting grey RGB samples to full-range monochrome of same or greater bit
// depth should be lossless.
INSTANTIATE_TEST_SUITE_P(MonochromeLossless8b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(8),
                                 /*yuv_depth=*/Values(8, 10, 12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV400),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsBT601),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(false),
                                 /*rgb_step=*/Values(1),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(MonochromeLossless10b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(10),
                                 /*yuv_depth=*/Values(10, 12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV400),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsBT601),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(false),
                                 /*rgb_step=*/Values(1),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(MonochromeLossless12b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(12),
                                 /*yuv_depth=*/Values(12, 16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV400),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsBT601),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(false),
                                 /*rgb_step=*/Values(1),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));
INSTANTIATE_TEST_SUITE_P(MonochromeLossless16b, RGBToYUVTest,
                         Combine(/*rgb_depth=*/Values(16),
                                 /*yuv_depth=*/Values(16),
                                 Values(AVIF_RGB_FORMAT_RGBA),
                                 Values(AVIF_PIXEL_FORMAT_YUV400),
                                 Values(AVIF_RANGE_FULL),
                                 Values(kMatrixCoefficientsBT601),
                                 Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
                                 /*add_noise=*/Values(false),
                                 /*rgb_step=*/Values(401),
                                 /*max_abs_average_diff=*/Values(0.),
                                 /*min_psnr=*/Values(99.)));

// Coverage for reformat_libsharpyuv.c.
INSTANTIATE_TEST_SUITE_P(
    SharpYuv8Bit, RGBToYUVTest,
    Combine(
        /*rgb_depth=*/Values(8),
        /*yuv_depth=*/Values(8, 10, 12), Values(AVIF_RGB_FORMAT_RGBA),
        Values(AVIF_PIXEL_FORMAT_YUV420),
        Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
        Values(kMatrixCoefficientsBT601, kMatrixCoefficientsBT709),
        Values(AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV),
        /*add_noise=*/Values(true),
        /*rgb_step=*/Values(17),
        /*max_abs_average_diff=*/Values(1.2),  // Sharp YUV introduces some
                                               // color shift.
        /*min_psnr=*/Values(34.)  // SharpYuv distortion is acceptable.
        ));
INSTANTIATE_TEST_SUITE_P(
    SharpYuv10Bit, RGBToYUVTest,
    Combine(
        /*rgb_depth=*/Values(10),
        /*yuv_depth=*/Values(8, 10, 12), Values(AVIF_RGB_FORMAT_RGBA),
        Values(AVIF_PIXEL_FORMAT_YUV420),
        Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
        Values(kMatrixCoefficientsBT601),
        Values(AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV),
        /*add_noise=*/Values(true),
        /*rgb_step=*/Values(211),              // High or it would be too slow.
        /*max_abs_average_diff=*/Values(1.2),  // Sharp YUV introduces some
                                               // color shift.
        /*min_psnr=*/Values(34.)  // SharpYuv distortion is acceptable.
        ));
INSTANTIATE_TEST_SUITE_P(
    SharpYuv12Bit, RGBToYUVTest,
    Combine(
        /*rgb_depth=*/Values(12),
        /*yuv_depth=*/Values(8, 10, 12), Values(AVIF_RGB_FORMAT_RGBA),
        Values(AVIF_PIXEL_FORMAT_YUV420),
        Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
        Values(kMatrixCoefficientsBT601),
        Values(AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV),
        /*add_noise=*/Values(true),
        /*rgb_step=*/Values(840),              // High or it would be too slow.
        /*max_abs_average_diff=*/Values(1.2),  // Sharp YUV introduces some
                                               // color shift.
        /*min_psnr=*/Values(34.)  // SharpYuv distortion is acceptable.
        ));
INSTANTIATE_TEST_SUITE_P(
    SharpYuv16Bit, RGBToYUVTest,
    Combine(
        /*rgb_depth=*/Values(16),
        // TODO(yguyon): Why max_abs_average_diff>28 if RGB16 to YUV10 full rng?
        /*yuv_depth=*/Values(8, /*10,*/ 12), Values(AVIF_RGB_FORMAT_RGBA),
        Values(AVIF_PIXEL_FORMAT_YUV420),
        Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
        Values(kMatrixCoefficientsBT601),
        Values(AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV),
        /*add_noise=*/Values(true),
        /*rgb_step=*/Values(4567),             // High or it would be too slow.
        /*max_abs_average_diff=*/Values(2.5),  // Sharp YUV introduces some
                                               // color shift.
        /*min_psnr=*/Values(49.)  // SharpYuv distortion is acceptable.
        ));

// Can be used to print the drift of all RGB to YUV conversion possibilities.
// Also used for coverage.
INSTANTIATE_TEST_SUITE_P(
    All8b, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(8),
            /*yuv_depth=*/Values(8, 10, 12, 16),
            Values(AVIF_RGB_FORMAT_RGBA, AVIF_RGB_FORMAT_BGR),
            Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
                   AVIF_PIXEL_FORMAT_YUV420),
            Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Bool(),
            /*rgb_step=*/Values(61),  // High or it would be too slow.
            /*max_abs_average_diff=*/Values(1.),  // Not very accurate because
                                                  // of high rgb_step.
            /*min_psnr=*/Values(36.)));
INSTANTIATE_TEST_SUITE_P(
    All10b, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(10),
            /*yuv_depth=*/Values(8, 10, 12, 16), Values(AVIF_RGB_FORMAT_RGBA),
            Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
                   AVIF_PIXEL_FORMAT_YUV420),
            Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Bool(),
            /*rgb_step=*/Values(211),  // High or it would be too slow.
            /*max_abs_average_diff=*/Values(0.2),  // Not very accurate because
                                                   // of high rgb_step.
            /*min_psnr=*/Values(47.)));
INSTANTIATE_TEST_SUITE_P(
    All12b, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(12),
            /*yuv_depth=*/Values(8, 10, 12, 16), Values(AVIF_RGB_FORMAT_RGBA),
            Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
                   AVIF_PIXEL_FORMAT_YUV420),
            Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Bool(),
            /*rgb_step=*/Values(809),  // High or it would be too slow.
            /*max_abs_average_diff=*/Values(0.3),  // Not very accurate because
                                                   // of high rgb_step.
            /*min_psnr=*/Values(52.)));
INSTANTIATE_TEST_SUITE_P(
    All16b, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(16),
            /*yuv_depth=*/Values(16), Values(AVIF_RGB_FORMAT_RGBA),
            Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
                   AVIF_PIXEL_FORMAT_YUV420),
            Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Bool(),
            /*rgb_step=*/Values(16001),  // High or it would be too slow.
            /*max_abs_average_diff=*/Values(0.05),
            /*min_psnr=*/Values(80.)));

// This was used to estimate the quality loss of libyuv for RGB-to-YUV.
// Disabled because it takes a few minutes.
INSTANTIATE_TEST_SUITE_P(
    DISABLED_All8bTo8b, RGBToYUVTest,
    Combine(/*rgb_depth=*/Values(8),
            /*yuv_depth=*/Values(8), Values(AVIF_RGB_FORMAT_RGBA),
            Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
                   AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400),
            Values(AVIF_RANGE_FULL, AVIF_RANGE_LIMITED),
            Values(kMatrixCoefficientsBT601),
            Values(AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC),
            /*add_noise=*/Bool(),
            /*rgb_step=*/Values(3),  // way faster and 99% similar to rgb_step=1
            /*max_abs_average_diff=*/Values(10.),
            /*min_psnr=*/Values(10.)));

// Converts YUV pixels to RGB using one thread and multiple threads and checks
// whether the results of both are identical.
class YUVToRGBThreadingTest
    : public testing::TestWithParam<std::tuple<
          /*rgb_depth=*/int, /*yuv_depth=*/int,
          /*width=*/int, /*height=*/int, avifRGBFormat, avifPixelFormat,
          /*threads=*/int, /*avoidLibYUV=*/bool, avifChromaUpsampling,
          /*has_alpha=*/bool>> {};

TEST_P(YUVToRGBThreadingTest, TestIdentical) {
  const int rgb_depth = std::get<0>(GetParam());
  const int yuv_depth = std::get<1>(GetParam());
  const int width = std::get<2>(GetParam());
  const int height = std::get<3>(GetParam());
  const avifRGBFormat rgb_format = std::get<4>(GetParam());
  const avifPixelFormat yuv_format = std::get<5>(GetParam());
  const int maxThreads = std::get<6>(GetParam());
  const bool avoidLibYUV = std::get<7>(GetParam());
  const avifChromaUpsampling chromaUpsampling = std::get<8>(GetParam());
  const bool has_alpha = std::get<9>(GetParam());

  if (rgb_depth > 8 && rgb_format == AVIF_RGB_FORMAT_RGB_565) {
    return;
  }

  testutil::AvifImagePtr yuv(
      avifImageCreate(width, height, yuv_depth, yuv_format), avifImageDestroy);
  ASSERT_NE(yuv, nullptr);
  yuv->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
  yuv->yuvRange = AVIF_RANGE_FULL;

  // Fill YUVA planes with random values.
  srand(0xAABBCCDD);
  const int yuv_max = (1 << yuv_depth);
  ASSERT_EQ(avifImageAllocatePlanes(
                yuv.get(), has_alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV),
            AVIF_RESULT_OK);
  for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_A; ++plane) {
    const uint32_t plane_width = avifImagePlaneWidth(yuv.get(), plane);
    if (plane_width == 0) continue;
    const uint32_t plane_height = avifImagePlaneHeight(yuv.get(), plane);
    const uint32_t rowBytes = avifImagePlaneRowBytes(yuv.get(), plane);
    uint8_t* row = avifImagePlane(yuv.get(), plane);
    for (uint32_t y = 0; y < plane_height; ++y, row += rowBytes) {
      for (uint32_t x = 0; x < plane_width; ++x) {
        if (yuv_depth == 8) {
          row[x] = (uint8_t)(rand() % yuv_max);
        } else {
          ((uint16_t*)row)[x] = (uint16_t)(rand() % yuv_max);
        }
      }
    }
  }

  // Convert to RGB with 1 thread.
  testutil::AvifRgbImage rgb(yuv.get(), rgb_depth, rgb_format);
  rgb.avoidLibYUV = avoidLibYUV;
  rgb.chromaUpsampling = chromaUpsampling;
  ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &rgb), AVIF_RESULT_OK);

  // Convert to RGB with multiple threads.
  testutil::AvifRgbImage rgb_threaded(yuv.get(), rgb_depth, rgb_format);
  rgb_threaded.avoidLibYUV = avoidLibYUV;
  rgb_threaded.chromaUpsampling = chromaUpsampling;
  rgb_threaded.maxThreads = maxThreads;
  ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &rgb_threaded), AVIF_RESULT_OK);

  EXPECT_TRUE(testutil::AreImagesEqual(rgb, rgb_threaded));
}

INSTANTIATE_TEST_SUITE_P(
    YUVToRGBThreadingTestInstance, YUVToRGBThreadingTest,
    Combine(/*rgb_depth=*/Values(8, 16),
            /*yuv_depth=*/Values(8, 10),
            /*width=*/Values(1, 2, 127, 200),
            /*height=*/Values(1, 2, 127, 200),
            Values(AVIF_RGB_FORMAT_RGB, AVIF_RGB_FORMAT_RGBA),
            Range(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_COUNT),
            // Test an odd and even number for threads. Not adding all possible
            // thread values to keep the number of test instances low.
            /*threads=*/Values(2, 7),
            /*avoidLibYUV=*/Bool(),
            Values(AVIF_CHROMA_UPSAMPLING_FASTEST,
                   AVIF_CHROMA_UPSAMPLING_BILINEAR),
            /*has_alpha=*/Bool()));

// This will generate a large number of test instances and hence it is disabled
// by default. It can be run manually if necessary.
INSTANTIATE_TEST_SUITE_P(
    DISABLED_ExhaustiveYUVToRGBThreadingTestInstance, YUVToRGBThreadingTest,
    Combine(/*rgb_depth=*/Values(8, 10, 12, 16),
            /*yuv_depth=*/Values(8, 10, 12),
            /*width=*/Values(1, 2, 127, 200),
            /*height=*/Values(1, 2, 127, 200),
            Range(AVIF_RGB_FORMAT_RGB, AVIF_RGB_FORMAT_COUNT),
            Range(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_COUNT),
            /*threads=*/Range(0, 9),
            /*avoidLibYUV=*/Bool(),
            Values(AVIF_CHROMA_UPSAMPLING_AUTOMATIC,
                   AVIF_CHROMA_UPSAMPLING_FASTEST,
                   AVIF_CHROMA_UPSAMPLING_NEAREST,
                   AVIF_CHROMA_UPSAMPLING_BILINEAR),
            /*has_alpha=*/Bool()));

}  // namespace
}  // namespace libavif
