/*
 * Copyright (c) 2021, Alliance for Open Media. All rights reserved
 *
 * This source code is subject to the terms of the BSD 3-Clause Clear License
 * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear
 * License was not distributed with this source code in the LICENSE file, you
 * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/.  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
 * aomedia.org/license/patent-license/.
 */

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <tuple>

#include "third_party/googletest/src/googletest/include/gtest/gtest.h"

#include "config/avm_config.h"
#include "config/av2_rtcd.h"

#include "avm_ports/avm_timer.h"
#include "avm_ports/mem.h"
#include "av2/common/av2_common_int.h"
#include "av2/common/idct.h"
#include "av2/common/scan.h"
#include "av2/common/txb_common.h"
#include "test/acm_random.h"
#include "test/clear_system_state.h"
#include "test/register_state_check.h"
#include "test/util.h"

namespace {
using libavm_test::ACMRandom;

typedef void (*GetNzMapContextsFunc)(const uint8_t *const levels,
                                     const int16_t *const scan,
                                     const uint16_t eob, const TX_SIZE tx_size,
                                     const TX_CLASS tx_class,
                                     int8_t *const coeff_contexts,
                                     const int plane);

class EncodeTxbTest : public ::testing::TestWithParam<GetNzMapContextsFunc> {
 public:
  EncodeTxbTest() : get_nz_map_contexts_func_(GetParam()) {}

  virtual ~EncodeTxbTest() {}

  virtual void SetUp() {
    coeff_contexts_ref_ = reinterpret_cast<int8_t *>(
        avm_memalign(16, sizeof(*coeff_contexts_ref_) * MAX_TX_SQUARE));
    ASSERT_TRUE(coeff_contexts_ref_ != NULL);
    coeff_contexts_ = reinterpret_cast<int8_t *>(
        avm_memalign(16, sizeof(*coeff_contexts_) * MAX_TX_SQUARE));
    ASSERT_TRUE(coeff_contexts_ != NULL);
  }

  virtual void TearDown() {
    avm_free(coeff_contexts_ref_);
    avm_free(coeff_contexts_);
    libavm_test::ClearSystemState();
  }

  void GetNzMapContextsRun() {
    const int kNumTests = 10;
    int result = 0;
    for (int plane = 0; plane < 3; ++plane) {
      for (int is_inter = 0; is_inter < 2; ++is_inter) {
        for (int tx_type = DCT_DCT; tx_type < TX_TYPES; ++tx_type) {
          const TX_CLASS tx_class = tx_type_to_class[tx_type];
          for (int tx_size = TX_4X4; tx_size < TX_SIZES_ALL; ++tx_size) {
            const int bwl = get_txb_bwl((TX_SIZE)tx_size);
            const int width = get_txb_wide((TX_SIZE)tx_size);
            const int height = get_txb_high((TX_SIZE)tx_size);
            const int real_width = tx_size_wide[tx_size];
            const int real_height = tx_size_high[tx_size];
            const int16_t *const scan = av2_scan_orders[tx_size][tx_type].scan;

            levels_ = set_levels(levels_buf_, width);
            for (int i = 0; i < kNumTests && !result; ++i) {
              for (int eob = 1; eob <= width * height && !result; ++eob) {
                InitDataWithEob(scan, bwl, eob);

                av2_get_nz_map_contexts_c(levels_, scan, eob, (TX_SIZE)tx_size,
                                          tx_class, coeff_contexts_ref_, plane);
                get_nz_map_contexts_func_(levels_, scan, eob, (TX_SIZE)tx_size,
                                          tx_class, coeff_contexts_, plane);

                result = Compare(scan, eob);

                EXPECT_EQ(result, 0)
                    << " tx_class " << tx_class << " width " << real_width
                    << " height " << real_height << " eob " << eob;
              }
            }
          }
        }
      }
    }
  }

  void SpeedTestGetNzMapContextsRun() {
    const int kNumTests = 20000000;
    avm_usec_timer timer;
    avm_usec_timer timer_ref;

    printf("Note: Only test the largest possible eob case!\n");
    for (int tx_size = TX_4X4; tx_size < TX_SIZES_ALL; ++tx_size) {
      const int bwl = get_txb_bwl((TX_SIZE)tx_size);
      const int width = get_txb_wide((TX_SIZE)tx_size);
      const int height = get_txb_high((TX_SIZE)tx_size);
      const int real_width = tx_size_wide[tx_size];
      const int real_height = tx_size_high[tx_size];
      const TX_TYPE tx_type = DCT_DCT;
      const TX_CLASS tx_class = tx_type_to_class[tx_type];
      const int16_t *const scan = av2_scan_orders[tx_size][tx_type].scan;
      const int eob = width * height;
      const int numTests = kNumTests;

      levels_ = set_levels(levels_buf_, width);
      InitDataWithEob(scan, bwl, eob);

      avm_usec_timer_start(&timer_ref);
      for (int i = 0; i < numTests; ++i) {
        av2_get_nz_map_contexts_c(levels_, scan, eob, (TX_SIZE)tx_size,
                                  tx_class, coeff_contexts_ref_, 0);
      }
      avm_usec_timer_mark(&timer_ref);

      levels_ = set_levels(levels_buf_, width);
      InitDataWithEob(scan, bwl, eob);

      avm_usec_timer_start(&timer);
      for (int i = 0; i < numTests; ++i) {
        get_nz_map_contexts_func_(levels_, scan, eob, (TX_SIZE)tx_size,
                                  tx_class, coeff_contexts_, 0);
      }
      avm_usec_timer_mark(&timer);

      const int elapsed_time_ref =
          static_cast<int>(avm_usec_timer_elapsed(&timer_ref));
      const int elapsed_time = static_cast<int>(avm_usec_timer_elapsed(&timer));

      printf("get_nz_map_contexts_%2dx%2d: %7.1f ms ref %7.1f ms gain %4.2f\n",
             real_width, real_height, elapsed_time / 1000.0,
             elapsed_time_ref / 1000.0,
             (elapsed_time_ref * 1.0) / (elapsed_time * 1.0));
    }
  }

 private:
  void InitDataWithEob(const int16_t *const scan, const int bwl,
                       const int eob) {
    memset(levels_buf_, 0, sizeof(levels_buf_));
    memset(coeff_contexts_, 0, sizeof(*coeff_contexts_) * MAX_TX_SQUARE);

    for (int c = 0; c < eob; ++c) {
      levels_[get_padded_idx(scan[c], bwl)] =
          static_cast<uint8_t>(clamp(rnd_.Rand8(), 0, INT8_MAX));
      coeff_contexts_[scan[c]] = static_cast<int8_t>(rnd_.Rand16() >> 1);
    }

    memcpy(coeff_contexts_ref_, coeff_contexts_,
           sizeof(*coeff_contexts_) * MAX_TX_SQUARE);
  }

  bool Compare(const int16_t *const scan, const int eob) const {
    bool result = false;
    if (memcmp(coeff_contexts_, coeff_contexts_ref_,
               sizeof(*coeff_contexts_ref_) * MAX_TX_SQUARE)) {
      for (int i = 0; i < eob; i++) {
        const int pos = scan[i];
        if (coeff_contexts_ref_[pos] != coeff_contexts_[pos]) {
          printf("coeff_contexts_[%d] diff:%6d (ref),%6d (opt)\n", pos,
                 coeff_contexts_ref_[pos], coeff_contexts_[pos]);
          result = true;
          break;
        }
      }
    }
    return result;
  }

  GetNzMapContextsFunc get_nz_map_contexts_func_;
  ACMRandom rnd_;
  uint8_t levels_buf_[TX_PAD_2D];
  uint8_t *levels_;
  int8_t *coeff_contexts_ref_;
  int8_t *coeff_contexts_;
};
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EncodeTxbTest);

TEST_P(EncodeTxbTest, GetNzMapContexts) { GetNzMapContextsRun(); }

TEST_P(EncodeTxbTest, DISABLED_SpeedTestGetNzMapContexts) {
  SpeedTestGetNzMapContextsRun();
}

#if HAVE_SSE2
INSTANTIATE_TEST_SUITE_P(SSE2, EncodeTxbTest,
                         ::testing::Values(av2_get_nz_map_contexts_sse2));
#endif

typedef void (*av2_txb_init_levels_func)(const tran_low_t *const coeff,
                                         const int width, const int height,
                                         uint8_t *const levels);

typedef std::tuple<av2_txb_init_levels_func, int> TxbInitLevelParam;

class EncodeTxbInitLevelTest
    : public ::testing::TestWithParam<TxbInitLevelParam> {
 public:
  virtual ~EncodeTxbInitLevelTest() {}
  virtual void TearDown() { libavm_test::ClearSystemState(); }
  void RunTest(av2_txb_init_levels_func test_func, int tx_size, int is_speed);
};
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EncodeTxbInitLevelTest);

void EncodeTxbInitLevelTest::RunTest(av2_txb_init_levels_func test_func,
                                     int tx_size, int is_speed) {
  const int width = get_txb_wide((TX_SIZE)tx_size);
  const int height = get_txb_high((TX_SIZE)tx_size);
  tran_low_t coeff[MAX_TX_SQUARE];

  uint8_t levels_buf[2][TX_PAD_2D];
  uint8_t *const levels0 = set_levels(levels_buf[0], width);
  uint8_t *const levels1 = set_levels(levels_buf[1], width);

  ACMRandom rnd(ACMRandom::DeterministicSeed());
  for (int i = 0; i < width * height; i++) {
    coeff[i] = rnd.Rand15Signed() + rnd.Rand15Signed();
  }
  for (int i = 0; i < TX_PAD_2D; i++) {
    levels_buf[0][i] = rnd.Rand8();
    levels_buf[1][i] = rnd.Rand8();
  }
  const int run_times = is_speed ? (width * height) * 10000 : 1;
  avm_usec_timer timer;
  avm_usec_timer_start(&timer);
  for (int i = 0; i < run_times; ++i) {
    av2_txb_init_levels_c(coeff, width, height, levels0);
  }
  const double t1 = get_time_mark(&timer);
  avm_usec_timer_start(&timer);
  for (int i = 0; i < run_times; ++i) {
    test_func(coeff, width, height, levels1);
  }
  const double t2 = get_time_mark(&timer);
  if (is_speed) {
    printf("init %3dx%-3d:%7.2f/%7.2fns", width, height, t1, t2);
    printf("(%3.2f)\n", t1 / t2);
  }
  const int stride = width + TX_PAD_HOR;
  for (int r = TX_PAD_TOP; r < height + TX_PAD_VER; ++r) {
    for (int c = 0; c < stride; ++c) {
      ASSERT_EQ(levels_buf[0][c + r * stride], levels_buf[1][c + r * stride])
          << "[" << r << "," << c << "] " << run_times << width << "x"
          << height;
    }
  }
}

TEST_P(EncodeTxbInitLevelTest, match) {
  RunTest(GET_PARAM(0), GET_PARAM(1), 0);
}

TEST_P(EncodeTxbInitLevelTest, DISABLED_Speed) {
  RunTest(GET_PARAM(0), GET_PARAM(1), 1);
}

#if HAVE_SSE4_1
INSTANTIATE_TEST_SUITE_P(
    SSE4_1, EncodeTxbInitLevelTest,
    ::testing::Combine(::testing::Values(&av2_txb_init_levels_sse4_1),
                       ::testing::Range(0, static_cast<int>(TX_SIZES_ALL), 1)));
#endif
#if HAVE_AVX2
INSTANTIATE_TEST_SUITE_P(
    AVX2, EncodeTxbInitLevelTest,
    ::testing::Combine(::testing::Values(&av2_txb_init_levels_avx2),
                       ::testing::Range(0, static_cast<int>(TX_SIZES_ALL), 1)));
#endif
#if HAVE_NEON
INSTANTIATE_TEST_SUITE_P(
    NEON, EncodeTxbInitLevelTest,
    ::testing::Combine(::testing::Values(&av2_txb_init_levels_neon),
                       ::testing::Range(0, static_cast<int>(TX_SIZES_ALL), 1)));
#endif

typedef void (*av2_txb_init_levels_signs_func)(const tran_low_t *const coeff,
                                               const int width,
                                               const int height,
                                               uint8_t *const levels,
                                               int8_t *const signs);

typedef std::tuple<av2_txb_init_levels_signs_func, int> TxbInitLevelSignsParam;

class EncodeTxbInitLevelSignsTest
    : public ::testing::TestWithParam<TxbInitLevelSignsParam> {
 public:
  virtual ~EncodeTxbInitLevelSignsTest() {}
  virtual void TearDown() { libavm_test::ClearSystemState(); }
  void RunTest(av2_txb_init_levels_signs_func test_func, int tx_size,
               int is_speed);
};
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EncodeTxbInitLevelSignsTest);

void EncodeTxbInitLevelSignsTest::RunTest(
    av2_txb_init_levels_signs_func test_func, int tx_size, int is_speed) {
  const int width = get_txb_wide((TX_SIZE)tx_size);
  const int height = get_txb_high((TX_SIZE)tx_size);
  tran_low_t coeff[MAX_TX_SQUARE];

  uint8_t levels_buf[2][TX_PAD_2D];
  int8_t signs_buf[2][TX_PAD_2D];

  const int run_times = is_speed ? 1000000 : 1;

  ACMRandom rnd(ACMRandom::DeterministicSeed());
  for (int i = 0; i < width * height; i++) {
    coeff[i] = rnd.Rand15Signed() + rnd.Rand15Signed();
  }
  avm_usec_timer timer;
  avm_usec_timer_start(&timer);
  for (int i = 0; i < run_times; ++i) {
    av2_txb_init_levels_signs_c(coeff, width, height, levels_buf[0],
                                signs_buf[0]);
  }
  const double t1 = get_time_mark(&timer);
  avm_usec_timer_start(&timer);
  for (int i = 0; i < run_times; ++i) {
    test_func(coeff, width, height, levels_buf[1], signs_buf[1]);
  }
  const double t2 = get_time_mark(&timer);
  if (is_speed) {
    printf("init %3dx%-3d:%7.2f/%7.2fns", width, height, t1, t2);
    printf("(%3.2f)\n", t1 / t2);
  }
  const int stride = width + TX_PAD_HOR;
  for (int r = TX_PAD_TOP; r < height + TX_PAD_VER; ++r) {
    for (int c = 0; c < stride; ++c) {
      ASSERT_EQ(levels_buf[0][c + r * stride], levels_buf[1][c + r * stride])
          << "[" << r << "," << c << "] " << run_times << width << "x"
          << height;

      ASSERT_EQ(signs_buf[0][c + r * stride], signs_buf[1][c + r * stride])
          << "[" << r << "," << c << "] " << run_times << width << "x"
          << height;
    }
  }
}

TEST_P(EncodeTxbInitLevelSignsTest, match) {
  RunTest(GET_PARAM(0), GET_PARAM(1), 0);
}

TEST_P(EncodeTxbInitLevelSignsTest, DISABLED_Speed) {
  RunTest(GET_PARAM(0), GET_PARAM(1), 1);
}

#if HAVE_SSE4_1
INSTANTIATE_TEST_SUITE_P(
    SSE4_1, EncodeTxbInitLevelSignsTest,
    ::testing::Combine(::testing::Values(&av2_txb_init_levels_signs_sse4_1),
                       ::testing::Range(0, static_cast<int>(TX_SIZES_ALL), 1)));
#endif
#if HAVE_AVX2
INSTANTIATE_TEST_SUITE_P(
    AVX2, EncodeTxbInitLevelSignsTest,
    ::testing::Combine(::testing::Values(&av2_txb_init_levels_signs_avx2),
                       ::testing::Range(0, static_cast<int>(TX_SIZES_ALL), 1)));
#endif

}  // namespace
