/*
 * Copyright (c) 2016, Alliance for Open Media. All rights reserved.
 *
 * This source code is subject to the terms of the BSD 2 Clause License and
 * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
 * was not distributed with this source code in the LICENSE file, you can
 * obtain it at www.aomedia.org/license/software. If the Alliance for Open
 * Media Patent License 1.0 was not distributed with this source code in the
 * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
 */

#include <math.h>
#include <stdlib.h>
#include <new>
#include <tuple>

#include "gtest/gtest.h"
#include "test/acm_random.h"
#include "test/util.h"

#include "config/aom_config.h"

#include "aom_dsp/psnr.h"
#include "aom_dsp/ssim.h"
#include "aom_ports/mem.h"
#include "aom_scale/yv12config.h"

using libaom_test::ACMRandom;

namespace {

using LBDMetricFunc = double (*)(const YV12_BUFFER_CONFIG *source,
                                 const YV12_BUFFER_CONFIG *dest);
using HBDMetricFunc = double (*)(const YV12_BUFFER_CONFIG *source,
                                 const YV12_BUFFER_CONFIG *dest, uint32_t in_bd,
                                 uint32_t bd);

double compute_hbd_psnr(const YV12_BUFFER_CONFIG *source,
                        const YV12_BUFFER_CONFIG *dest, uint32_t in_bd,
                        uint32_t bd) {
  PSNR_STATS psnr;
  aom_calc_highbd_psnr(source, dest, &psnr, bd, in_bd);
  return psnr.psnr[0];
}

double compute_psnr(const YV12_BUFFER_CONFIG *source,
                    const YV12_BUFFER_CONFIG *dest) {
  PSNR_STATS psnr;
  aom_calc_psnr(source, dest, &psnr);
  return psnr.psnr[0];
}

double compute_hbd_psnrhvs(const YV12_BUFFER_CONFIG *source,
                           const YV12_BUFFER_CONFIG *dest, uint32_t in_bd,
                           uint32_t bd) {
  double tempy, tempu, tempv;
  return aom_psnrhvs(source, dest, &tempy, &tempu, &tempv, bd, in_bd);
}

double compute_psnrhvs(const YV12_BUFFER_CONFIG *source,
                       const YV12_BUFFER_CONFIG *dest) {
  double tempy, tempu, tempv;
  return aom_psnrhvs(source, dest, &tempy, &tempu, &tempv, 8, 8);
}

double compute_hbd_fastssim(const YV12_BUFFER_CONFIG *source,
                            const YV12_BUFFER_CONFIG *dest, uint32_t in_bd,
                            uint32_t bd) {
  double tempy, tempu, tempv;
  return aom_calc_fastssim(source, dest, &tempy, &tempu, &tempv, bd, in_bd);
}

double compute_fastssim(const YV12_BUFFER_CONFIG *source,
                        const YV12_BUFFER_CONFIG *dest) {
  double tempy, tempu, tempv;
  return aom_calc_fastssim(source, dest, &tempy, &tempu, &tempv, 8, 8);
}

double compute_hbd_aomssim(const YV12_BUFFER_CONFIG *source,
                           const YV12_BUFFER_CONFIG *dest, uint32_t in_bd,
                           uint32_t bd) {
  double ssim[2], weight[2];
  aom_highbd_calc_ssim(source, dest, weight, bd, in_bd, ssim);
  return 100 * pow(ssim[0] / weight[0], 8.0);
}

double compute_aomssim(const YV12_BUFFER_CONFIG *source,
                       const YV12_BUFFER_CONFIG *dest) {
  double ssim, weight;
  aom_lowbd_calc_ssim(source, dest, &weight, &ssim);
  return 100 * pow(ssim / weight, 8.0);
}

class HBDMetricsTestBase {
 public:
  virtual ~HBDMetricsTestBase() = default;

 protected:
  void RunAccuracyCheck() {
    const int width = 1920;
    const int height = 1080;
    size_t i = 0;
    const uint8_t kPixFiller = 128;
    YV12_BUFFER_CONFIG lbd_src, lbd_dst;
    YV12_BUFFER_CONFIG hbd_src, hbd_dst;
    ACMRandom rnd(ACMRandom::DeterministicSeed());
    double lbd_db, hbd_db;

    memset(&lbd_src, 0, sizeof(lbd_src));
    memset(&lbd_dst, 0, sizeof(lbd_dst));
    memset(&hbd_src, 0, sizeof(hbd_src));
    memset(&hbd_dst, 0, sizeof(hbd_dst));

    aom_alloc_frame_buffer(&lbd_src, width, height, 1, 1, 0, 32, 16, false, 0);
    aom_alloc_frame_buffer(&lbd_dst, width, height, 1, 1, 0, 32, 16, false, 0);
    aom_alloc_frame_buffer(&hbd_src, width, height, 1, 1, 1, 32, 16, false, 0);
    aom_alloc_frame_buffer(&hbd_dst, width, height, 1, 1, 1, 32, 16, false, 0);

    memset(lbd_src.buffer_alloc, kPixFiller, lbd_src.buffer_alloc_sz);
    while (i < lbd_src.buffer_alloc_sz) {
      uint16_t spel, dpel;
      spel = lbd_src.buffer_alloc[i];
      // Create some distortion for dst buffer.
      dpel = rnd.Rand8();
      lbd_dst.buffer_alloc[i] = (uint8_t)dpel;
      ((uint16_t *)(hbd_src.buffer_alloc))[i] = spel << (bit_depth_ - 8);
      ((uint16_t *)(hbd_dst.buffer_alloc))[i] = dpel << (bit_depth_ - 8);
      i++;
    }

    lbd_db = lbd_metric_(&lbd_src, &lbd_dst);
    hbd_db = hbd_metric_(&hbd_src, &hbd_dst, input_bit_depth_, bit_depth_);
    EXPECT_LE(fabs(lbd_db - hbd_db), threshold_);

    i = 0;
    while (i < lbd_src.buffer_alloc_sz) {
      uint16_t dpel;
      // Create some small distortion for dst buffer.
      dpel = 120 + (rnd.Rand8() >> 4);
      lbd_dst.buffer_alloc[i] = (uint8_t)dpel;
      ((uint16_t *)(hbd_dst.buffer_alloc))[i] = dpel << (bit_depth_ - 8);
      i++;
    }

    lbd_db = lbd_metric_(&lbd_src, &lbd_dst);
    hbd_db = hbd_metric_(&hbd_src, &hbd_dst, input_bit_depth_, bit_depth_);
    EXPECT_LE(fabs(lbd_db - hbd_db), threshold_);

    i = 0;
    while (i < lbd_src.buffer_alloc_sz) {
      uint16_t dpel;
      // Create some small distortion for dst buffer.
      dpel = 126 + (rnd.Rand8() >> 6);
      lbd_dst.buffer_alloc[i] = (uint8_t)dpel;
      ((uint16_t *)(hbd_dst.buffer_alloc))[i] = dpel << (bit_depth_ - 8);
      i++;
    }

    lbd_db = lbd_metric_(&lbd_src, &lbd_dst);
    hbd_db = hbd_metric_(&hbd_src, &hbd_dst, input_bit_depth_, bit_depth_);
    EXPECT_LE(fabs(lbd_db - hbd_db), threshold_);

    aom_free_frame_buffer(&lbd_src);
    aom_free_frame_buffer(&lbd_dst);
    aom_free_frame_buffer(&hbd_src);
    aom_free_frame_buffer(&hbd_dst);
  }

  int input_bit_depth_;
  int bit_depth_;
  double threshold_;
  LBDMetricFunc lbd_metric_;
  HBDMetricFunc hbd_metric_;
};

using MetricTestTParam =
    std::tuple<LBDMetricFunc, HBDMetricFunc, int, int, double>;
class HBDMetricsTest : public HBDMetricsTestBase,
                       public ::testing::TestWithParam<MetricTestTParam> {
 public:
  void SetUp() override {
    lbd_metric_ = GET_PARAM(0);
    hbd_metric_ = GET_PARAM(1);
    input_bit_depth_ = GET_PARAM(2);
    bit_depth_ = GET_PARAM(3);
    threshold_ = GET_PARAM(4);
  }
};

TEST_P(HBDMetricsTest, RunAccuracyCheck) { RunAccuracyCheck(); }

// Allow small variation due to floating point operations.
static const double kSsim_thresh = 0.001;
// Allow some additional errors accumulated in floating point operations.
static const double kFSsim_thresh = 0.03;
// Allow some extra variation due to rounding error accumulated in dct.
static const double kPhvs_thresh = 0.3;

INSTANTIATE_TEST_SUITE_P(
    AOMSSIM, HBDMetricsTest,
    ::testing::Values(MetricTestTParam(&compute_aomssim, &compute_hbd_aomssim,
                                       8, 10, kSsim_thresh),
                      MetricTestTParam(&compute_aomssim, &compute_hbd_aomssim,
                                       10, 10, kPhvs_thresh),
                      MetricTestTParam(&compute_aomssim, &compute_hbd_aomssim,
                                       8, 12, kSsim_thresh),
                      MetricTestTParam(&compute_aomssim, &compute_hbd_aomssim,
                                       12, 12, kPhvs_thresh)));
INSTANTIATE_TEST_SUITE_P(
    FASTSSIM, HBDMetricsTest,
    ::testing::Values(MetricTestTParam(&compute_fastssim, &compute_hbd_fastssim,
                                       8, 10, kFSsim_thresh),
                      MetricTestTParam(&compute_fastssim, &compute_hbd_fastssim,
                                       10, 10, kFSsim_thresh),
                      MetricTestTParam(&compute_fastssim, &compute_hbd_fastssim,
                                       8, 12, kFSsim_thresh),
                      MetricTestTParam(&compute_fastssim, &compute_hbd_fastssim,
                                       12, 12, kFSsim_thresh)));
INSTANTIATE_TEST_SUITE_P(
    PSNRHVS, HBDMetricsTest,
    ::testing::Values(MetricTestTParam(&compute_psnrhvs, &compute_hbd_psnrhvs,
                                       8, 10, kPhvs_thresh),
                      MetricTestTParam(&compute_psnrhvs, &compute_hbd_psnrhvs,
                                       10, 10, kPhvs_thresh),
                      MetricTestTParam(&compute_psnrhvs, &compute_hbd_psnrhvs,
                                       8, 12, kPhvs_thresh),
                      MetricTestTParam(&compute_psnrhvs, &compute_hbd_psnrhvs,
                                       12, 12, kPhvs_thresh)));
INSTANTIATE_TEST_SUITE_P(
    PSNR, HBDMetricsTest,
    ::testing::Values(
        MetricTestTParam(&compute_psnr, &compute_hbd_psnr, 8, 10, kPhvs_thresh),
        MetricTestTParam(&compute_psnr, &compute_hbd_psnr, 10, 10,
                         kPhvs_thresh),
        MetricTestTParam(&compute_psnr, &compute_hbd_psnr, 8, 12, kPhvs_thresh),
        MetricTestTParam(&compute_psnr, &compute_hbd_psnr, 12, 12,
                         kPhvs_thresh)));
}  // namespace
