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

#include <math.h>

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

namespace avif {
namespace {

// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;

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

void CheckGainMapMetadata(
    const avifGainMap& gm, std::array<double, 3> gain_map_min,
    std::array<double, 3> gain_map_max, std::array<double, 3> gain_map_gamma,
    std::array<double, 3> base_offset, std::array<double, 3> alternate_offset,
    double base_hdr_headroom, double alternate_hdr_headroom) {
  const double kEpsilon = 1e-8;

  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[0].n) / gm.gainMapMin[0].d,
              gain_map_min[0], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[1].n) / gm.gainMapMin[1].d,
              gain_map_min[1], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[2].n) / gm.gainMapMin[2].d,
              gain_map_min[2], kEpsilon);

  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[0].n) / gm.gainMapMax[0].d,
              gain_map_max[0], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[1].n) / gm.gainMapMax[1].d,
              gain_map_max[1], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[2].n) / gm.gainMapMax[2].d,
              gain_map_max[2], kEpsilon);

  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[0].n) / gm.gainMapGamma[0].d,
              gain_map_gamma[0], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[1].n) / gm.gainMapGamma[1].d,
              gain_map_gamma[1], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[2].n) / gm.gainMapGamma[2].d,
              gain_map_gamma[2], kEpsilon);

  EXPECT_NEAR(static_cast<double>(gm.baseOffset[0].n) / gm.baseOffset[0].d,
              base_offset[0], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.baseOffset[1].n) / gm.baseOffset[1].d,
              base_offset[1], kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.baseOffset[2].n) / gm.baseOffset[2].d,
              base_offset[2], kEpsilon);

  EXPECT_NEAR(
      static_cast<double>(gm.alternateOffset[0].n) / gm.alternateOffset[0].d,
      alternate_offset[0], kEpsilon);
  EXPECT_NEAR(
      static_cast<double>(gm.alternateOffset[1].n) / gm.alternateOffset[1].d,
      alternate_offset[1], kEpsilon);
  EXPECT_NEAR(
      static_cast<double>(gm.alternateOffset[2].n) / gm.alternateOffset[2].d,
      alternate_offset[2], kEpsilon);

  EXPECT_NEAR(static_cast<double>(gm.baseHdrHeadroom.n) / gm.baseHdrHeadroom.d,
              base_hdr_headroom, kEpsilon);
  EXPECT_NEAR(static_cast<double>(gm.alternateHdrHeadroom.n) /
                  gm.alternateHdrHeadroom.d,
              alternate_hdr_headroom, kEpsilon);
}

TEST(JpegTest, ReadJpegWithGainMap) {
  for (const char* filename : {"paris_exif_xmp_gainmap_bigendian.jpg",
                               "paris_exif_xmp_gainmap_littleendian.jpg"}) {
    SCOPED_TRACE(filename);

    const ImagePtr image =
        testutil::ReadImage(data_path, filename, AVIF_PIXEL_FORMAT_YUV444, 8,
                            AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
                            /*ignore_icc=*/false, /*ignore_exif=*/false,
                            /*ignore_xmp=*/true, /*allow_changing_cicp=*/true,
                            /*ignore_gain_map=*/false);
    ASSERT_NE(image, nullptr);
    ASSERT_NE(image->gainMap, nullptr);
    ASSERT_NE(image->gainMap->image, nullptr);
    EXPECT_EQ(image->gainMap->image->width, 512u);
    EXPECT_EQ(image->gainMap->image->height, 384u);
    // Since ignore_xmp is true, there should be no XMP, even if it had to
    // be read to parse the gain map.
    EXPECT_EQ(image->xmp.size, 0u);

    CheckGainMapMetadata(*image->gainMap,
                         /*gain_map_min=*/{0.0, 0.0, 0.0},
                         /*gain_map_max=*/{3.5, 3.6, 3.7},
                         /*gain_map_gamma=*/{1.0, 1.0, 1.0},
                         /*base_offset=*/{0.0, 0.0, 0.0},
                         /*alternate_offset=*/{0.0, 0.0, 0.0},
                         /*base_hdr_headroom=*/0.0,
                         /*alternate_hdr_headroom=*/3.5);
  }
}

TEST(JpegTest, IgnoreGainMap) {
  const ImagePtr image = testutil::ReadImage(
      data_path, "paris_exif_xmp_gainmap_littleendian.jpg",
      AVIF_PIXEL_FORMAT_YUV444, 8, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
      /*ignore_icc=*/false, /*ignore_exif=*/false,
      /*ignore_xmp=*/false, /*allow_changing_cicp=*/true,
      /*ignore_gain_map=*/true);
  ASSERT_NE(image, nullptr);
  ASSERT_EQ(image->gainMap, nullptr);
  // Check there is xmp since ignore_xmp is false (just making sure that
  // ignore_gain_map=true has no impact on this).
  EXPECT_GT(image->xmp.size, 0u);
}

TEST(JpegTest, ParseXMP) {
  const std::string xmp = R"(
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <foo:myelement> <!--  7.3 "Other XMP elements may appear around the rdf:RDF element." -->
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
      hdrgm:Version="1.0"
      hdrgm:BaseRenditionIsHDR="True"
      hdrgm:OffsetSDR="0.046983"
      hdrgm:OffsetHDR="0.046983"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="3.9">
      <hdrgm:GainMapMin>
        <rdf:Seq>
        <rdf:li>0.025869</rdf:li>
        <rdf:li>0.075191</rdf:li>
        <rdf:li>0.142298</rdf:li>
        </rdf:Seq>
      </hdrgm:GainMapMin>
      <hdrgm:GainMapMax>
        <rdf:Seq>
        <rdf:li>3.527605</rdf:li>
        <rdf:li>2.830234</rdf:li>
        <!-- should work even with some whitespace -->
        <rdf:li>
          1.537243
        </rdf:li>
        </rdf:Seq>
      </hdrgm:GainMapMax>
      <hdrgm:Gamma>
        <rdf:Seq>
        <rdf:li>0.506828</rdf:li>
        <rdf:li>0.590032</rdf:li>
        <rdf:li>1.517708</rdf:li>
        </rdf:Seq>
      </hdrgm:Gamma>
      </rdf:Description>
    </rdf:RDF>
  </foo:myelement>
</x:xmpmeta>
<?xpacket end="w"?>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                      gainMap.get()));

  CheckGainMapMetadata(*gainMap,
                       /*gain_map_min=*/{0.025869, 0.075191, 0.142298},
                       /*gain_map_max=*/{3.527605, 2.830234, 1.537243},
                       /*gain_map_gamma=*/{0.506828, 0.590032, 1.517708},
                       /*base_offset=*/{0.046983, 0.046983, 0.046983},
                       /*alternate_offset=*/{0.046983, 0.046983, 0.046983},
                       /*base_hdr_headroom=*/3.9,
                       /*alternate_hdr_headroom=*/0.0);
}

TEST(JpegTest, ParseXMPAllDefaultValues) {
  const std::string xmp = R"(
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about="stuff"
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/" hdrgm:Version="1.0">
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                      gainMap.get()));

  CheckGainMapMetadata(
      *gainMap,
      /*gain_map_min=*/{0.0, 0.0, 0.0},
      /*gain_map_max=*/{1.0, 1.0, 1.0},
      /*gain_map_gamma=*/{1.0, 1.0, 1.0},
      /*base_offset=*/{1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0},
      /*alternate_offset=*/{1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0},
      /*base_hdr_headroom=*/0.0,
      /*alternate_hdr_headroom=*/1.0);
}

TEST(JpegTest, ExtendedXmp) {
  const std::string xmp = R"(
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about="stuff"
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/" hdrgm:Version="1.0"
      hdrgm:BaseRenditionIsHDR="False"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="3.9">
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>

<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <!-- Imagine this is some extended xmp that avifenc concatenated to
      the main XMP. As a result we have invalid XMP but should still be
      able to parse it. -->
    <stuff></stuff>
  </rdf:RDF>
</x:xmpmeta>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                      gainMap.get()));

  // Note that this test passes because the gain map metadata is in the primary
  // XMP. If it was in the extended part, we wouldn't detect it (but probably
  // should).
  CheckGainMapMetadata(
      *gainMap,
      /*gain_map_min=*/{0.0, 0.0, 0.0},
      /*gain_map_max=*/{1.0, 1.0, 1.0},
      /*gain_map_gamma=*/{1.0, 1.0, 1.0},
      /*base_offset=*/{1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0},
      /*alternate_offset=*/{1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0},
      /*base_hdr_headroom=*/0.0,
      /*alternate_hdr_headroom=*/3.9);
}

TEST(JpegTest, InvalidNumberOfValues) {
  const std::string xmp = R"(
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
    hdrgm:Version="1.0"
    hdrgm:BaseRenditionIsHDR="False"
    hdrgm:OffsetSDR="0.046983"
    hdrgm:OffsetHDR="0.046983"
    hdrgm:HDRCapacityMin="0"
    hdrgm:HDRCapacityMax="3.9">
    <hdrgm:GainMapMin>
      <rdf:Seq><!--invalid! only two values-->
      <rdf:li>0.023869</rdf:li>
      <rdf:li>0.075191</rdf:li>
      </rdf:Seq>
    </hdrgm:GainMapMin>
  </rdf:RDF>
</x:xmpmeta>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                       gainMap.get()));
}

TEST(JpegTest, WrongVersion) {
  const std::string xmp = R"(
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/" hdrgm:Version="2.0">
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                       gainMap.get()));
}

TEST(JpegTest, InvalidXMP) {
  const std::string xmp = R"(
<x:xmpmeta xmlns:x="adobe:ns:meta/">
    <rdf:Description rdf:about=""
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/" hdrgm:Version="2.0">
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
  )";
  GainMapPtr gainMap(avifGainMapCreate());
  EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                       gainMap.get()));
}

TEST(JpegTest, EmptyXMP) {
  const std::string xmp = "";
  GainMapPtr gainMap(avifGainMapCreate());
  EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
                                       gainMap.get()));
}

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

}  // namespace
}  // namespace avif

int main(int argc, char** argv) {
  ::testing::InitGoogleTest(&argc, argv);
  if (argc != 2) {
    std::cerr << "There must be exactly one argument containing the path to "
                 "the test data folder"
              << std::endl;
    return 1;
  }
  avif::data_path = argv[1];
  return RUN_ALL_TESTS();
}
