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

#include <algorithm>
#include <cstring>
#include <tuple>

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

namespace libavif {
namespace {

enum class InputType { kStillImage, kGrid, kAnimation };

// Generates a valid AVIF stream with the video range flag set to the same value
// in the "colr" box and in the AV1 OBU payload (in the "mdat" box).
avifResult GenerateEncodedData(avifRange range, InputType input_type,
                               testutil::AvifRwData* encoded) {
  testutil::AvifImagePtr image =
      testutil::CreateImage(/*width=*/64, /*height=*/64, /*depth=*/8,
                            AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL, range);
  AVIF_CHECKERR(image != nullptr, AVIF_RESULT_OUT_OF_MEMORY);
  testutil::FillImageGradient(image.get());  // Pixel values do not matter.

  testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
  AVIF_CHECKERR(encoder != nullptr, AVIF_RESULT_OUT_OF_MEMORY);
  if (input_type == InputType::kStillImage) {
    AVIF_CHECKRES(avifEncoderWrite(encoder.get(), image.get(), encoded));
  } else if (input_type == InputType::kGrid) {
    const avifImage* cellImages[2] = {image.get(), image.get()};
    AVIF_CHECKRES(avifEncoderAddImageGrid(encoder.get(), /*gridCols=*/2,
                                          /*gridRows=*/1, cellImages,
                                          AVIF_ADD_IMAGE_FLAG_SINGLE));
    AVIF_CHECKRES(avifEncoderFinish(encoder.get(), encoded));
  } else {
    assert(input_type == InputType::kAnimation);
    AVIF_CHECKRES(avifEncoderAddImage(encoder.get(), image.get(),
                                      /*durationInTimescales=*/1,
                                      AVIF_ADD_IMAGE_FLAG_NONE));
    AVIF_CHECKRES(avifEncoderAddImage(encoder.get(), image.get(),
                                      /*durationInTimescales=*/1,
                                      AVIF_ADD_IMAGE_FLAG_NONE));
    AVIF_CHECKRES(avifEncoderFinish(encoder.get(), encoded));
  }
  return AVIF_RESULT_OK;
}

class RangeTest
    : public testing::TestWithParam<std::tuple<avifRange, InputType>> {};

TEST_P(RangeTest, DifferentVideoRangeInColrAndMdat) {
  const avifRange obu_range = std::get<0>(GetParam());
  const InputType input_type = std::get<1>(GetParam());
  testutil::AvifRwData encoded;
  ASSERT_EQ(GenerateEncodedData(obu_range, input_type, &encoded),
            AVIF_RESULT_OK);

  // Set full_range_flag in the "colr" box to a different value.
  // This creates an invalid bitstream according to AV1-ISOBMFF v1.2.0 but
  // libavif still allows it.
  const avifRange colr_range =
      (obu_range == AVIF_RANGE_LIMITED) ? AVIF_RANGE_FULL : AVIF_RANGE_LIMITED;
  const uint8_t kColrBoxTag[] = "colr";
  uint8_t* colr_box = std::search(encoded.data, encoded.data + encoded.size,
                                  kColrBoxTag, kColrBoxTag + 4);
  do {
    ASSERT_GT(colr_box, encoded.data + 4);
    ASSERT_LT(colr_box, encoded.data + encoded.size);
    const uint32_t colr_box_size = (colr_box[-4] << 24) | (colr_box[-3] << 16) |
                                   (colr_box[-2] << 8) | colr_box[-1];
    ASSERT_EQ(colr_box_size, 19u);
    ASSERT_LT(colr_box + colr_box_size - 4, encoded.data + encoded.size);
    ASSERT_TRUE(std::equal(colr_box + 4, colr_box + 8,
                           reinterpret_cast<const uint8_t*>("nclx")));
    // full_range_flag(1bit)=colr_range, reserved(7bits)=0
    colr_box[colr_box_size - 5] = static_cast<uint8_t>(colr_range << 7);
    colr_box = std::search(colr_box + 4, encoded.data + encoded.size,
                           kColrBoxTag, kColrBoxTag + 4);
  } while (colr_box != encoded.data + encoded.size);

  // Section 12.1.5.1 of ISO 14496-12 (ISOBMFF) says:
  //   If colour information is supplied in both this [colr] box, and also in
  //   the video bitstream, this box takes precedence, and over-rides the
  //   information in the bitstream.
  testutil::AvifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy);
  ASSERT_NE(decoded, nullptr);
  testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
  ASSERT_NE(decoder, nullptr);
  ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
                                  encoded.size),
            AVIF_RESULT_OK);
  ASSERT_EQ(decoded->yuvRange, colr_range);
}

TEST_P(RangeTest, MissingColr) {
  const avifRange obu_range = std::get<0>(GetParam());
  const InputType input_type = std::get<1>(GetParam());
  testutil::AvifRwData encoded;
  ASSERT_EQ(GenerateEncodedData(obu_range, input_type, &encoded),
            AVIF_RESULT_OK);

  // Remove the "colr" box (by replacing it with a placeholder).
  // This creates an invalid bitstream according to AV1-ISOBMFF v1.2.0 but
  // libavif still allows it.
  const uint8_t kColrBoxTag[] = "colr";
  uint8_t* colr_box = std::search(encoded.data, encoded.data + encoded.size,
                                  kColrBoxTag, kColrBoxTag + 4);
  do {
    ASSERT_LT(colr_box + 4, encoded.data + encoded.size);
    std::memcpy(colr_box, "free", 4);
    colr_box = std::search(colr_box + 4, encoded.data + encoded.size,
                           kColrBoxTag, kColrBoxTag + 4);
  } while (colr_box != encoded.data + encoded.size);

  // Make sure the AV1 OBU range is kept.
  testutil::AvifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy);
  ASSERT_NE(decoded, nullptr);
  testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
  ASSERT_NE(decoder, nullptr);
  ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
                                  encoded.size),
            AVIF_RESULT_OK);
  ASSERT_EQ(decoded->yuvRange, obu_range);
}

INSTANTIATE_TEST_SUITE_P(
    All, RangeTest,
    testing::Combine(testing::Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
                     testing::Values(InputType::kStillImage, InputType::kGrid,
                                     InputType::kAnimation)));

}  // namespace
}  // namespace libavif
