// Copyright 2022 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
//
// Compares two files and returns whether they are the same once decoded.
//
// Usage:
//   are_images_equal file1 file2 ignore_alpha_flag [psnr_threshold]
//   [ignore_gain_map_flag]
// If psnr_threshold is not provided or is 0, the images are compared for strict
// equality, including all metadata. If psnr_threshold is greater than 0, the
// images are compared for similarity using PSNR. Most metadata is NOT checked.
// If a gain map is present and ignore_gain_map_flag is absent or 0, the
// tone-mapped images are also compared using PSNR. ignore_alpha_flag and
// ignore_gain_map_flag are booleans (0 or 1).

#include <cstdint>
#include <cstring>
#include <iostream>
#include <limits>
#include <string>

#include "aviftest_helpers.h"
#include "avifutil.h"

using avif::ImagePtr;
using avif::RGBImageCleanup;

namespace {

ImagePtr ToneMapToAlternate(const ImagePtr& image) {
  avif::testutil::AvifRgbImage toneMappedRgb(image.get(), image->depth,
                                             AVIF_RGB_FORMAT_RGB);
  RGBImageCleanup rgbCleanup(&toneMappedRgb);

  avif::ImagePtr toneMapped(
      avifImageCreate(toneMappedRgb.width, toneMappedRgb.height,
                      toneMappedRgb.depth, AVIF_PIXEL_FORMAT_YUV444));
  toneMapped->colorPrimaries = image->gainMap->altColorPrimaries;
  toneMapped->transferCharacteristics =
      image->gainMap->altTransferCharacteristics;
  toneMapped->matrixCoefficients = image->gainMap->altMatrixCoefficients;
  toneMapped->yuvRange = image->yuvRange;

  float altHeadroom = 0.0f;
  if (image->gainMap->alternateHdrHeadroom.d != 0) {
    altHeadroom = (float)image->gainMap->alternateHdrHeadroom.n /
                  image->gainMap->alternateHdrHeadroom.d;
  }

  avifDiagnostics diag;
  avifResult res = avifImageApplyGainMap(
      image.get(), image->gainMap, altHeadroom, toneMapped->colorPrimaries,
      toneMapped->transferCharacteristics, &toneMappedRgb, &toneMapped->clli,
      &diag);
  if (res != AVIF_RESULT_OK) {
    std::cerr << "Failed to apply gain map: " << diag.error << std::endl;
    return nullptr;
  }

  res = avifImageRGBToYUV(toneMapped.get(), &toneMappedRgb);
  if (res != AVIF_RESULT_OK) {
    return nullptr;
  }
  return toneMapped;
}

}  // namespace

int main(int argc, char** argv) {
  if (argc < 4 || argc > 6) {
    std::cerr << "Wrong argument: " << argv[0]
              << " file1 file2 ignore_alpha_flag [psnr_threshold] "
                 "[ignore_gain_map_flag]"
              << std::endl;
    return 2;
  }
  const bool ignore_gain_map = argc > 5 && std::stoi(argv[5]) != 0;
  const double psnr_threshold = argc > 4 ? std::stod(argv[4]) : 0;

  ImagePtr decoded[2] = {ImagePtr(avifImageCreateEmpty()),
                         ImagePtr(avifImageCreateEmpty())};
  if (!decoded[0] || !decoded[1]) {
    std::cerr << "Cannot create AVIF images." << std::endl;
    return 2;
  }
  uint32_t depth[2];
  // Request the bit depth closest to the bit depth of the input file.
  constexpr int kRequestedDepth = 0;
  constexpr avifPixelFormat requestedFormat = AVIF_PIXEL_FORMAT_NONE;
  for (int i : {0, 1}) {
    // Make sure no color conversion happens.
    decoded[i]->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
    if (avifReadImage(
            argv[i + 1], AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */,
            requestedFormat, kRequestedDepth,
            AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
            /*ignoreColorProfile==*/AVIF_FALSE, /*ignoreExif=*/AVIF_FALSE,
            /*ignoreXMP=*/AVIF_FALSE, ignore_gain_map,
            /*imageSizeLimit=*/std::numeric_limits<uint32_t>::max(),
            decoded[i].get(), &depth[i], nullptr,
            nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
      std::cerr << "Image " << argv[i + 1] << " cannot be read." << std::endl;
      return 2;
    }
  }

  if (depth[0] != depth[1]) {
    std::cerr << "Images " << argv[1] << " and " << argv[2]
              << " have different depths." << std::endl;
    return 1;
  }

  if (!ignore_gain_map &&
      ((decoded[0]->gainMap != nullptr) != (decoded[1]->gainMap != nullptr))) {
    std::cerr << "Images " << argv[1] << " and " << argv[2]
              << " have different gainmap presence" << std::endl;
    return 1;
  }

  bool ignore_alpha = std::stoi(argv[3]) != 0;

  if (psnr_threshold <= 0) {  // Check for strict equality.
    if (!avif::testutil::AreImagesEqual(*decoded[0], *decoded[1],
                                        ignore_alpha)) {
      auto psnr =
          avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha);
      std::cerr << "Images " << argv[1] << " and " << argv[2]
                << " are different (base image PSNR: " << psnr << ")."
                << std::endl;
      return 1;
    }
    std::cout << "Images " << argv[1] << " and " << argv[2] << " are identical."
              << std::endl;
  } else {
    // If there is a gain map, this is the PSNR of the base images only.
    auto psnr = avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha);
    if (psnr < psnr_threshold) {
      std::cerr << "PSNR: " << psnr << ", images " << argv[1] << " and "
                << argv[2] << " are not similar enough (threshold: " << argv[4]
                << ")." << std::endl;
      return 1;
    }
    std::cout << "PSNR: " << psnr << ", images " << argv[1] << " and "
              << argv[2] << " are similar." << std::endl;

    // If there is a gain map, tone map both images to their alternate headroom
    // and check the PSNR of the tone mapped images.
    if (decoded[0]->gainMap && decoded[1]->gainMap &&
        decoded[0]->gainMap->image && decoded[1]->gainMap->image) {
      // We don't support ICC profiles when tonemapping, so clear them after
      // checking for equality.
      if (decoded[0]->icc.size != decoded[1]->icc.size ||
          (decoded[0]->icc.size > 0 &&
           std::memcmp(decoded[0]->icc.data, decoded[1]->icc.data,
                       decoded[0]->icc.size) != 0)) {
        std::cerr << "Gainmap PSNR: Images " << argv[1] << " and " << argv[2]
                  << " have different ICC profiles." << std::endl;
        return 1;
      }
      if (decoded[0]->gainMap->altICC.size !=
              decoded[1]->gainMap->altICC.size ||
          (decoded[0]->gainMap->altICC.size > 0 &&
           std::memcmp(decoded[0]->gainMap->altICC.data,
                       decoded[1]->gainMap->altICC.data,
                       decoded[0]->gainMap->altICC.size) != 0)) {
        std::cerr << "Gainmap PSNR: Images " << argv[1] << " and " << argv[2]
                  << " have different ICC profiles." << std::endl;
        return 1;
      }
      avifRWDataFree(&decoded[0]->icc);
      avifRWDataFree(&decoded[1]->icc);
      avifRWDataFree(&decoded[0]->gainMap->altICC);
      avifRWDataFree(&decoded[1]->gainMap->altICC);

      avif::ImagePtr toneMapped0 = ToneMapToAlternate(decoded[0]);
      avif::ImagePtr toneMapped1 = ToneMapToAlternate(decoded[1]);
      if (!toneMapped0 || !toneMapped1) {
        std::cerr << "Failed to tone map images." << std::endl;
        return 1;
      }
      psnr = avif::testutil::GetPsnr(*toneMapped0, *toneMapped1);
      if (psnr < psnr_threshold) {
        std::cerr << "Tone-mapped PSNR: " << psnr << ", images " << argv[1]
                  << " and " << argv[2]
                  << " are not similar enough (threshold: " << argv[4] << ")."
                  << std::endl;
        return 1;
      }
      std::cout << "Tone-mapped PSNR: " << psnr << ", images " << argv[1]
                << " and " << argv[2] << " are similar." << std::endl;
    }
  }

  return 0;
}
