/*
 * Copyright (c) 2024, 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 <memory>
#include <new>

#include "config/av1_rtcd.h"
#include "aom_ports/aom_timer.h"
#include "aom_ports/bitops.h"
#include "gtest/gtest.h"
#include "test/acm_random.h"
#include "test/util.h"

namespace {

using ::testing::Combine;
using ::testing::Values;
using ::testing::ValuesIn;

using std::make_tuple;
using std::tuple;

const int kIters = 1000;

using FrameDimension = tuple<int, int>;

// Check that two 8-bit output buffers are identical.
void AssertOutputBufferEq(const uint8_t *p1, const uint8_t *p2, int width,
                          int height) {
  ASSERT_TRUE(p1 != p2) << "Buffers must be at different memory locations";
  for (int j = 0; j < height; ++j) {
    if (memcmp(p1, p2, sizeof(*p1) * width) == 0) {
      p1 += width;
      p2 += width;
      continue;
    }
    for (int i = 0; i < width; ++i) {
      ASSERT_EQ(p1[i], p2[i])
          << width << "x" << height << " Pixel mismatch at (" << i << ", " << j
          << ")";
    }
  }
}

using LowBDResizeFunc = bool (*)(uint8_t *intbuf, uint8_t *output,
                                 int out_stride, int height, int height2,
                                 int stride, int start_wd);
// Test parameter list:
//  <tst_fun, dims>
using ResizeTestParams = tuple<LowBDResizeFunc, FrameDimension>;

class AV1ResizeYTest : public ::testing::TestWithParam<ResizeTestParams> {
 public:
  void SetUp() {
    test_fun_ = GET_PARAM(0);
    frame_dim_ = GET_PARAM(1);
    width_ = std::get<0>(frame_dim_);
    height_ = std::get<1>(frame_dim_);
    const int msb = get_msb(AOMMIN(width_, height_));
    n_levels_ = AOMMAX(msb - MIN_PYRAMID_SIZE_LOG2, 1);
    const int src_buf_size = (width_ / 2) * height_;
    const int dest_buf_size = (width_ * height_) / 4;
    src_ = std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[src_buf_size]);
    ASSERT_NE(src_, nullptr);

    ref_dest_ =
        std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[dest_buf_size]);
    ASSERT_NE(ref_dest_, nullptr);

    test_dest_ =
        std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[dest_buf_size]);
    ASSERT_NE(test_dest_, nullptr);
  }

  void RunTest() {
    for (int i = 0; i < (width_ / 2) * height_; i++) src_[i] = rng_.Rand8();
    for (int level = 1; level < n_levels_; level++) {
      const int width2 = (width_ >> level);
      const int height2 = (height_ >> level);
      av1_resize_vert_dir_c(src_.get(), ref_dest_.get(), width2, height2 << 1,
                            height2, width2, 0);
      test_fun_(src_.get(), test_dest_.get(), width2, height2 << 1, height2,
                width2, 0);

      AssertOutputBufferEq(ref_dest_.get(), test_dest_.get(), width2, height2);
    }
  }

  void SpeedTest() {
    for (int i = 0; i < (width_ / 2) * height_; i++) src_[i] = rng_.Rand8();
    for (int level = 1; level < n_levels_; level++) {
      const int width2 = (width_ >> level);
      const int height2 = (height_ >> level);
      aom_usec_timer ref_timer;
      aom_usec_timer_start(&ref_timer);
      for (int j = 0; j < kIters; j++) {
        av1_resize_vert_dir_c(src_.get(), ref_dest_.get(), width2, height2 << 1,
                              height2, width2, 0);
      }
      aom_usec_timer_mark(&ref_timer);
      const int64_t ref_time = aom_usec_timer_elapsed(&ref_timer);

      aom_usec_timer tst_timer;
      aom_usec_timer_start(&tst_timer);
      for (int j = 0; j < kIters; j++) {
        test_fun_(src_.get(), test_dest_.get(), width2, height2 << 1, height2,
                  width2, 0);
      }
      aom_usec_timer_mark(&tst_timer);
      const int64_t tst_time = aom_usec_timer_elapsed(&tst_timer);

      std::cout << "level: " << level << " [" << width2 << " x " << height2
                << "] C time = " << ref_time << " , SIMD time = " << tst_time
                << " scaling=" << float(1.00) * ref_time / tst_time << "x \n";
    }
  }

 private:
  LowBDResizeFunc test_fun_;
  FrameDimension frame_dim_;
  int width_;
  int height_;
  int n_levels_;
  std::unique_ptr<uint8_t[]> src_;
  std::unique_ptr<uint8_t[]> ref_dest_;
  std::unique_ptr<uint8_t[]> test_dest_;
  libaom_test::ACMRandom rng_;
};

GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AV1ResizeYTest);

TEST_P(AV1ResizeYTest, RunTest) { RunTest(); }

TEST_P(AV1ResizeYTest, DISABLED_SpeedTest) { SpeedTest(); }

#if HAVE_AVX2 || HAVE_SSE2
// Resolutions (width x height) to be tested for resizing.
const FrameDimension kFrameDim[] = {
  make_tuple(3840, 2160), make_tuple(2560, 1440), make_tuple(1920, 1080),
  make_tuple(1280, 720),  make_tuple(640, 480),   make_tuple(640, 360),
  make_tuple(286, 286),   make_tuple(284, 284),   make_tuple(282, 282),
  make_tuple(280, 280),   make_tuple(262, 262),   make_tuple(258, 258),
  make_tuple(256, 256),   make_tuple(34, 34),
};
#endif

#if HAVE_AVX2
INSTANTIATE_TEST_SUITE_P(
    AVX2, AV1ResizeYTest,
    ::testing::Combine(::testing::Values(av1_resize_vert_dir_avx2),
                       ::testing::ValuesIn(kFrameDim)));
#endif

#if HAVE_SSE2
INSTANTIATE_TEST_SUITE_P(
    SSE2, AV1ResizeYTest,
    ::testing::Combine(::testing::Values(av1_resize_vert_dir_sse2),
                       ::testing::ValuesIn(kFrameDim)));
#endif

using LowBDResize_x_Func = void (*)(const uint8_t *const input, int in_stride,
                                    uint8_t *intbuf, int height,
                                    int filtered_length, int width2);

using Resize_x_TestParams = tuple<LowBDResize_x_Func, FrameDimension>;

class AV1ResizeXTest : public ::testing::TestWithParam<Resize_x_TestParams> {
 public:
  void SetUp() {
    test_fun_ = GET_PARAM(0);
    frame_dim_ = GET_PARAM(1);
    width_ = std::get<0>(frame_dim_);
    height_ = std::get<1>(frame_dim_);
    const int msb = get_msb(AOMMIN(width_, height_));
    n_levels_ = AOMMAX(msb - MIN_PYRAMID_SIZE_LOG2, 1);
    const int src_buf_size = width_ * height_;
    const int dest_buf_size = (width_ * height_) / 2;
    src_ = std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[src_buf_size]);
    ASSERT_NE(src_, nullptr);

    ref_dest_ =
        std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[dest_buf_size]);
    ASSERT_NE(ref_dest_, nullptr);

    test_dest_ =
        std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[dest_buf_size]);
    ASSERT_NE(test_dest_, nullptr);
  }

  void RunTest() {
    for (int i = 0; i < width_ * height_; ++i) src_[i] = rng_.Rand8();

    for (int level = 1; level < n_levels_; ++level) {
      const int width2 = (width_ >> level);
      av1_resize_horz_dir_c(src_.get(), width_, ref_dest_.get(), height_,
                            width2 << 1, width2);
      test_fun_(src_.get(), width_, test_dest_.get(), height_, width2 << 1,
                width2);
      AssertOutputBufferEq(ref_dest_.get(), test_dest_.get(), width2, height_);
    }
  }

  void SpeedTest() {
    for (int i = 0; i < width_ * height_; ++i) src_[i] = rng_.Rand8();

    for (int level = 1; level < n_levels_; ++level) {
      const int width2 = (width_ >> level);
      aom_usec_timer ref_timer;
      aom_usec_timer_start(&ref_timer);
      for (int j = 0; j < kIters; ++j) {
        av1_resize_horz_dir_c(src_.get(), width_, ref_dest_.get(), height_,
                              width2 << 1, width2);
      }
      aom_usec_timer_mark(&ref_timer);
      const int64_t ref_time = aom_usec_timer_elapsed(&ref_timer);

      aom_usec_timer tst_timer;
      aom_usec_timer_start(&tst_timer);
      for (int j = 0; j < kIters; ++j) {
        test_fun_(src_.get(), width_, test_dest_.get(), height_, width2 << 1,
                  width2);
      }
      aom_usec_timer_mark(&tst_timer);
      const int64_t tst_time = aom_usec_timer_elapsed(&tst_timer);

      std::cout << "level: " << level << " [" << width2 << " x " << height_
                << "] C time = " << ref_time << " , SIMD time = " << tst_time
                << " scaling=" << float(1.00) * ref_time / tst_time << "x \n";
    }
  }

 private:
  LowBDResize_x_Func test_fun_;
  FrameDimension frame_dim_;
  int width_;
  int height_;
  int n_levels_;
  std::unique_ptr<uint8_t[]> src_;
  std::unique_ptr<uint8_t[]> ref_dest_;
  std::unique_ptr<uint8_t[]> test_dest_;
  libaom_test::ACMRandom rng_;
};

GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AV1ResizeXTest);

TEST_P(AV1ResizeXTest, RunTest) { RunTest(); }

TEST_P(AV1ResizeXTest, DISABLED_SpeedTest) { SpeedTest(); }

#if HAVE_SSE2
INSTANTIATE_TEST_SUITE_P(
    SSE2, AV1ResizeXTest,
    ::testing::Combine(::testing::Values(av1_resize_horz_dir_sse2),
                       ::testing::ValuesIn(kFrameDim)));
#endif

#if HAVE_AVX2
INSTANTIATE_TEST_SUITE_P(
    AVX2, AV1ResizeXTest,
    ::testing::Combine(::testing::Values(av1_resize_horz_dir_avx2),
                       ::testing::ValuesIn(kFrameDim)));
#endif

}  // namespace
