/*
 * Copyright (c) 2021, 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 <stdint.h>
#include <vector>

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

#include "av1/encoder/block.h"
#include "av1/encoder/encodemb.h"
#include "av1/common/scan.h"

namespace {

// Reorders 'qcoeff_lexico', which is in lexicographic order (row by row), into
// scan order (zigzag) in 'qcoeff_scan'.
void ToScanOrder(TX_SIZE tx_size, TX_TYPE tx_type, tran_low_t *qcoeff_lexico,
                 tran_low_t *qcoeff_scan) {
  const int max_eob = av1_get_max_eob(tx_size);
  const SCAN_ORDER *const scan_order = get_scan(tx_size, tx_type);
  for (int i = 0; i < max_eob; ++i) {
    qcoeff_scan[i] = qcoeff_lexico[scan_order->scan[i]];
  }
}

// Reorders 'qcoeff_scan', which is in scan order (zigzag), into lexicographic
// order (row by row) in 'qcoeff_lexico'.
void ToLexicoOrder(TX_SIZE tx_size, TX_TYPE tx_type, tran_low_t *qcoeff_scan,
                   tran_low_t *qcoeff_lexico) {
  const int max_eob = av1_get_max_eob(tx_size);
  const SCAN_ORDER *const scan_order = get_scan(tx_size, tx_type);
  for (int i = 0; i < max_eob; ++i) {
    qcoeff_lexico[scan_order->scan[i]] = qcoeff_scan[i];
  }
}

// Runs coefficient dropout on 'qcoeff_scan'.
void Dropout(TX_SIZE tx_size, TX_TYPE tx_type, int dropout_num_before,
             int dropout_num_after, tran_low_t *qcoeff_scan) {
  tran_low_t qcoeff[MAX_TX_SQUARE];
  // qcoeff_scan is assumed to be in scan order, since tests are easier to
  // understand this way, but av1_dropout_qcoeff expects coeffs in lexico order
  // so we convert to lexico then back to scan afterwards.
  ToLexicoOrder(tx_size, tx_type, qcoeff_scan, qcoeff);

  const int max_eob = av1_get_max_eob(tx_size);
  const int kDequantFactor = 10;
  tran_low_t dqcoeff[MAX_TX_SQUARE];
  for (int i = 0; i < max_eob; ++i) {
    dqcoeff[i] = qcoeff[i] * kDequantFactor;
  }

  uint16_t eob = max_eob;
  while (eob > 0 && qcoeff_scan[eob - 1] == 0) --eob;

  MACROBLOCK mb;
  const int kPlane = 0;
  const int kBlock = 0;
  memset(&mb, 0, sizeof(mb));
  uint16_t eobs[] = { eob };
  mb.plane[kPlane].eobs = eobs;
  mb.plane[kPlane].qcoeff = qcoeff;
  mb.plane[kPlane].dqcoeff = dqcoeff;
  uint8_t txb_entropy_ctx[1];
  mb.plane[kPlane].txb_entropy_ctx = txb_entropy_ctx;

  av1_dropout_qcoeff_num(&mb, kPlane, kBlock, tx_size, tx_type,
                         dropout_num_before, dropout_num_after);

  ToScanOrder(tx_size, tx_type, qcoeff, qcoeff_scan);

  // Check updated eob value is valid.
  uint16_t new_eob = max_eob;
  while (new_eob > 0 && qcoeff_scan[new_eob - 1] == 0) --new_eob;
  EXPECT_EQ(new_eob, mb.plane[kPlane].eobs[0]);

  // Check dqcoeff is still valid.
  for (int i = 0; i < max_eob; ++i) {
    EXPECT_EQ(qcoeff[i] * kDequantFactor, dqcoeff[i]);
  }
}

void ExpectArrayEq(tran_low_t *actual, std::vector<tran_low_t> expected) {
  for (size_t i = 0; i < expected.size(); ++i) {
    EXPECT_EQ(expected[i], actual[i]) << "Arrays differ at index " << i;
  }
}

static constexpr TX_TYPE kTxType = DCT_DCT;

TEST(DropoutTest, KeepsLargeCoeffs) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Large isolated coeffs should be preserved.
  tran_low_t qcoeff_scan[] = { 0, 0, 0, 0, 0, 0, 42, 0,    // should be kept
                               0, 0, 0, 0, 0, 0, 0,  0,    //
                               0, 0, 0, 0, 0, 0, 0,  -30,  // should be kept
                               0, 0, 0, 0, 0, 0, 0,  0 };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 0, 0, 0, 0, 0, 0, 42, 0,    //
                               0, 0, 0, 0, 0, 0, 0,  0,    //
                               0, 0, 0, 0, 0, 0, 0,  -30,  //
                               0, 0, 0, 0, 0, 0, 0,  0 });
}

TEST(DropoutTest, RemovesSmallIsolatedCoeffs) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Small isolated coeffs should be removed.
  tran_low_t qcoeff_scan[] = { 0, 0, 0, 0, 1,  0, 0, 0,  // should be removed
                               0, 0, 0, 0, 0,  0, 0, 0,  //
                               0, 0, 0, 0, -2, 0, 0, 0,  // should be removed
                               0, 0, 0, 0, 0,  0, 0, 0 };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0 });
}

TEST(DropoutTest, KeepsSmallCoeffsAmongLargeOnes) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Small coeffs that are not isolated (not enough zeros before/after should be
  // kept).
  tran_low_t qcoeff_scan[] = {
    1, 0,  0, 0,  -5, 0, 0, -1,  // should be kept
    0, 0,  0, 10, 0,  0, 2, 0,   // should be kept
    0, 0,  0, 0,  0,  0, 0, 0,   //
    0, -2, 0, 0,  0,  0, 0, 0    // should be removed
  };                             // should be removed
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 1, 0, 0, 0,  -5, 0, 0, -1,  //
                               0, 0, 0, 10, 0,  0, 2, 0,   //
                               0, 0, 0, 0,  0,  0, 0, 0,   //
                               0, 0, 0, 0,  0,  0, 0, 0 });
}

TEST(DropoutTest, KeepsSmallCoeffsCloseToStartOrEnd) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Small coeffs that are too close to the beginning or end of the block
  // should also be kept (not enough zeroes before/after).
  tran_low_t qcoeff_scan[] = { 0, 0, -1, 0,  0, 0, 0,  0,  // should be kept
                               0, 0, 0,  10, 0, 0, 0,  0,  // should be kept
                               0, 0, 0,  2,  0, 0, 0,  0,  // should be removed
                               0, 0, 0,  0,  0, 0, -1, 0 };  // should be kept
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 0, 0, -1, 0,  0, 0, 0,  0,  //
                               0, 0, 0,  10, 0, 0, 0,  0,  //
                               0, 0, 0,  0,  0, 0, 0,  0,  //
                               0, 0, 0,  0,  0, 0, -1, 0 });
}

TEST(DropoutTest, RemovesSmallClusterOfCoeffs) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Small clusters (<= kDropoutContinuityMax) of small coeffs should be
  // removed.
  tran_low_t qcoeff_scan_two[] = {
    0, 0, 0, 0, 1, 0, 0, -1,  // should be removed
    0, 0, 0, 0, 0, 0, 0, 0,   //
    0, 0, 0, 0, 0, 0, 1, 0,   // should be removed
    0, 0, 0, 0, 0, 0, 0, 0
  };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after,
          qcoeff_scan_two);
  ExpectArrayEq(qcoeff_scan_two, { 0, 0, 0, 0, 0, 0, 0, 0,  //
                                   0, 0, 0, 0, 0, 0, 0, 0,  //
                                   0, 0, 0, 0, 0, 0, 0, 0,  //
                                   0, 0, 0, 0, 0, 0, 0, 0 });
}

TEST(DropoutTest, KeepsLargeClusterOfCoeffs) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 6;
  // Large clusters (> kDropoutContinuityMax) of small coeffs should be kept.
  tran_low_t qcoeff_scan[] = { 0, 0, 0, 0, 1, 0,  1, -1,  // should be kept
                               0, 0, 0, 0, 0, 0,  0, 0,   //
                               0, 0, 0, 0, 0, -2, 0, 0,   // should be removed
                               0, 0, 0, 0, 0, 0,  0, 0 };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 0, 0, 0, 0, 1, 0, 1, -1,  //
                               0, 0, 0, 0, 0, 0, 0, 0,   //
                               0, 0, 0, 0, 0, 0, 0, 0,   //
                               0, 0, 0, 0, 0, 0, 0, 0 });
}

TEST(DropoutTest, NumBeforeLargerThanNumAfter) {
  const TX_SIZE tx_size = TX_8X4;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 2;
  // The second coeff (-2) doesn't seem to meet the dropout_num_before
  // criteria. But since the first coeff (1) will be dropped, it will meet
  // the criteria and should be dropped too.
  tran_low_t qcoeff_scan[] = { 0,  0, 0, 0, 1, 0, 0, 0,  // should be removed
                               -2, 0, 0, 0, 0, 0, 0, 0,  // should be removed
                               0,  0, 0, 0, 0, 0, 0, 0,  //
                               0,  0, 0, 0, 0, 0, 0, 0 };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0,  //
                               0, 0, 0, 0, 0, 0, 0, 0 });
}

// More complex test combining other test cases.
TEST(DropoutTest, ComplexTest) {
  const TX_SIZE tx_size = TX_8X8;
  const uint32_t dropout_num_before = 4;
  const uint32_t dropout_num_after = 2;
  tran_low_t qcoeff_scan[] = { 1, 12, 0,  0,   0, 0, 1,  0,   //
                               0, 0,  0,  -12, 0, 0, 0,  1,   //
                               0, 0,  -2, 0,   1, 0, 0,  1,   //
                               0, 0,  0,  0,   5, 0, -1, 0,   //
                               0, 0,  0,  1,   0, 0, 0,  -1,  //
                               0, 0,  0,  0,   2, 0, 0,  0,   //
                               0, 1,  0,  0,   0, 5, 0,  0,   //
                               0, 0,  1,  1,   0, 0, 0,  -2 };
  Dropout(tx_size, kTxType, dropout_num_before, dropout_num_after, qcoeff_scan);
  ExpectArrayEq(qcoeff_scan, { 1, 12, 0,  0,   0, 0, 0,  0,  //
                               0, 0,  0,  -12, 0, 0, 0,  1,  //
                               0, 0,  -2, 0,   1, 0, 0,  1,  //
                               0, 0,  0,  0,   5, 0, -1, 0,  //
                               0, 0,  0,  0,   0, 0, 0,  0,  //
                               0, 0,  0,  0,   0, 0, 0,  0,  //
                               0, 0,  0,  0,   0, 5, 0,  0,  //
                               0, 0,  0,  0,   0, 0, 0,  -2 });
}

}  // namespace
