#include <gtest/gtest.h>

#include <ATen/ATen.h>
#include <ATen/core/Reduction.h>
#include <torch/cuda.h>
#include <ATen/test/test_assert.h>
#include <c10/util/irange.h>
#include <c10/util/CallOnce.h>

// for TH compat test only...
struct THFloatTensor;

#include <iostream>
#include <chrono>
// NOLINTNEXTLINE(modernize-deprecated-headers)
#include <string.h>
#include <sstream>
#include <thread>
#include <mutex>

#define ASSERT_EQ_RESOLVED(X, Y) \
  {                              \
    bool isEQ = X == Y;          \
    ASSERT_TRUE(isEQ);           \
  }

using namespace at;

void TestResize(DeprecatedTypeProperties& type) {
  auto a = at::empty({0}, type.options());
  a.resize_({3, 4});
  ASSERT_EQ_RESOLVED(a.numel(), 12);
  a.resize_({5, 7});
  ASSERT_EQ_RESOLVED(a.numel(), 35);
}

void TestOnesAndDot(DeprecatedTypeProperties& type) {
  Tensor b0 = ones({1, 1}, type);
  ASSERT_EQ_RESOLVED((b0 + b0).sum().item<double>(), 2);

  Tensor b1 = ones({1, 2}, type);
  ASSERT_EQ_RESOLVED((b1 + b1).sum().item<double>(), 4);

  Tensor b = ones({3, 4}, type);
  ASSERT_EQ_RESOLVED((b + b).sum().item<double>(), 24);
  ASSERT_EQ_RESOLVED(b.numel(), 12);
  if (type.backend() != Backend::CPU || type.scalarType() != kHalf) {
    ASSERT_EQ_RESOLVED(b.view(-1).dot(b.view(-1)).item<double>(), 12);
  }
}

void TestSort(DeprecatedTypeProperties& type) {
  Tensor b = rand({3, 4}, type);

  auto z = b.sort(1);
  auto z_sorted = std::get<0>(z);

  bool isLT = z_sorted[0][0].item<float>() < z_sorted[0][1].item<float>();
  ASSERT_TRUE(isLT);
}

void TestRandperm(DeprecatedTypeProperties& type) {
  if (type.backend() != Backend::CUDA) {
    Tensor b = randperm(15, type);
    auto [rv, ri] = sort(b, 0);
    bool isLE = (rv[0].item<float>() <= rv[1].item<float>());
    ASSERT_TRUE(isLE);
  }
}

void SendContext() {
  std::stringstream ss;
  ss << "context: " << std::hex << (int64_t)&globalContext() << std::endl;
}

void TestAdd(DeprecatedTypeProperties& type) {
  Tensor a = rand({3, 4}, type);
  Tensor b = rand({3, 4}, type);
  Tensor c = add(a, add(a, b));
  // TODO:0-dim Tensor d(3.f);
  Scalar d = 3.f;
  if (type.backend() == Backend::CPU && type.scalarType() == kHalf) {
      ASSERT_TRUE(add(c, d).allclose(a + a + b + d, 1e-2));
  } else {
      ASSERT_TRUE(add(c, d).allclose(a + a + b + d));
  }
}

void TestZeros(DeprecatedTypeProperties& type) {
  auto begin = std::chrono::high_resolution_clock::now();
  Tensor a = zeros({1024, 1024}, type);
  for ([[maybe_unused]] const auto i : c10::irange(1, 1000)) {
    a = zeros({128, 128}, type);
  }
  auto end = std::chrono::high_resolution_clock::now();
  std::cout << std::dec << "   "
            << std::chrono::duration_cast<std::chrono::milliseconds>(
                   end - begin)
                   .count()
            << " ms" << std::endl;

   std::srand(std::time(nullptr));
   ASSERT_EQ(norm(a).item<double>(), 0.0);
}

void TestLoadsOfAdds(DeprecatedTypeProperties& type) {
  auto begin = std::chrono::high_resolution_clock::now();
  Tensor d = ones({3, 4}, type);
  Tensor r = zeros({3, 4}, type);
  for ([[maybe_unused]] const auto i : c10::irange(1000)) {
    add_out(r, r, d);
  }
  auto end = std::chrono::high_resolution_clock::now();
  // TODO TEST PERF?
  std::cout << std::dec << "   "
            << std::chrono::duration_cast<std::chrono::milliseconds>(
                   end - begin)
                   .count()
            << " ms" << std::endl;
  ASSERT_EQ_RESOLVED(norm(1000 * d).item<double>(), norm(r).item<double>());
}

void TestLoadOfAddsWithCopy(DeprecatedTypeProperties& type) {
  auto begin = std::chrono::high_resolution_clock::now();
  Tensor d = ones({3, 4}, type);
  Tensor r = zeros({3, 4}, type);
  for ([[maybe_unused]] const auto i : c10::irange(1000)) {
    r = add(r, d);
  }
  auto end = std::chrono::high_resolution_clock::now();
  // TODO TEST PERF?
  std::cout << std::dec << "   "
            << std::chrono::duration_cast<std::chrono::milliseconds>(
                   end - begin)
                   .count()
            << " ms" << std::endl;
  ASSERT_EQ_RESOLVED(norm(1000 * d).item<double>(), norm(r).item<double>());
}

void TestIsContiguous(DeprecatedTypeProperties& type) {
  Tensor a = rand({3, 4}, type);
  ASSERT_TRUE(a.is_contiguous());
  a = a.transpose(0, 1);
  ASSERT_FALSE(a.is_contiguous());
}

void TestPermute(DeprecatedTypeProperties& type) {
  Tensor a = rand({3, 4, 5}, type);
  Tensor b = a.permute({1, 2, 0});
  ASSERT_TRUE(b.sizes().equals({4, 5, 3}));
  ASSERT_TRUE(b.strides().equals({5, 1, 20}));
}

void TestMm(DeprecatedTypeProperties& type) {
  if (type.backend() != Backend::CPU || type.scalarType() != kHalf) {
    Tensor a = rand({3, 4}, type);
    Tensor b = rand({4}, type);
    Tensor c = mv(a, b);
    ASSERT_TRUE(c.equal(addmv(zeros({3}, type), a, b, 0, 1)));
  }
}

void TestSqueeze(DeprecatedTypeProperties& type) {
  Tensor a = rand({2, 1}, type);
  Tensor b = squeeze(a);
  ASSERT_EQ_RESOLVED(b.dim(), 1);
  a = rand({1}, type);
  b = squeeze(a);
  ASSERT_TRUE(a[0].equal(b));
  Tensor c = at::scalar_tensor(1, type.options());
  Tensor d = squeeze(c);
  ASSERT_TRUE(c.equal(d));
}

void TestCopy(DeprecatedTypeProperties& type) {
  Tensor a = zeros({4, 3}, type);
  Tensor e = rand({4, 3}, type);
  a.copy_(e);
  ASSERT_TRUE(a.equal(e));
}

void TestCopyBroadcasting(DeprecatedTypeProperties& type) {
  Tensor a = zeros({4, 3}, type);
  Tensor e = rand({3}, type);
  a.copy_(e);
  for (const auto i : c10::irange(4)) {
    ASSERT_TRUE(a[i].equal(e));
  }
}
void TestAbsValue(DeprecatedTypeProperties& type) {
  Tensor r = at::abs(at::scalar_tensor(-3, type.options()));
  ASSERT_EQ_RESOLVED(r.item<int32_t>(), 3);
}
/*
   TODO(zach): operator overloads
#if 0
{
std::cout << "eq (value):" << std::endl;
Tensor a = Tensor(10.f);
std::cout << (a == 11_i64) << " -- should be 0" << std::endl;
std::cout << (a == 10_i64) << " -- should be 1" << std::endl;
std::cout << (a == 10.) << " -- should be 1" << std::endl;
}
#endif
*/

void TestAddingAValueWithScalar(DeprecatedTypeProperties& type) {
  Tensor a = rand({4, 3}, type);
  ASSERT_TRUE((ones({4, 3}, type) + a).equal(add(a, 1)));
}

void TestSelect(DeprecatedTypeProperties& type) {
  Tensor a = rand({3, 7}, type);
  auto a_13 = select(a, 1, 3);
  auto a_13_02 = select(select(a, 1, 3), 0, 2);
  ASSERT_TRUE(a[0][3].equal(a_13[0]));
  ASSERT_TRUE(a[2][3].equal(a_13_02));
}

void TestZeroDim(DeprecatedTypeProperties& type) {
  Tensor a = at::scalar_tensor(4, type.options()); // rand(type, {1});

  Tensor b = rand({3, 4}, type);
  ASSERT_EQ_RESOLVED((a + a).dim(), 0);
  ASSERT_EQ_RESOLVED((1 + a).dim(), 0);
  ASSERT_EQ_RESOLVED((b + a).dim(), 2);
  ASSERT_EQ_RESOLVED((a + b).dim(), 2);
  auto c = rand({3, 4}, type);
  ASSERT_EQ_RESOLVED(c[1][2].dim(), 0);

  auto f = rand({3, 4}, type);
  f[2] = zeros({4}, type);
  f[1][0] = -1;
  ASSERT_EQ_RESOLVED(f[2][0].item<double>(), 0);
}

void TestToCFloat() {
  Tensor a = zeros({3, 4});
  Tensor b = ones({3, 7});
  Tensor c = cat({a, b}, 1);
  ASSERT_EQ_RESOLVED(c.size(1), 11);

  Tensor e = rand({});
  ASSERT_EQ_RESOLVED(*e.data_ptr<float>(), e.sum().item<float>());
}
void TestToString() {
  Tensor b = ones({3, 7}) * .0000001f;
  std::stringstream s;
  s << b << "\n";
  std::string expect = "1e-07 *";
  ASSERT_EQ_RESOLVED(s.str().substr(0, expect.size()), expect);
}

void TestIndexingByScalar() {
  Tensor tensor = arange(0, 10, kInt);
  Tensor one = ones({}, kInt);
  for (const auto i : c10::irange(tensor.numel())) {
    ASSERT_TRUE(tensor[i].equal(one * i));
  }
  for (size_t i = 0; i < static_cast<uint64_t>(tensor.numel()); ++i) {
    ASSERT_TRUE(tensor[i].equal(one * static_cast<int64_t>(i)));
  }
  for (const auto i : c10::irange(tensor.numel())) {
    ASSERT_TRUE(tensor[i].equal(one * i));
  }
  // NOLINTNEXTLINE(bugprone-too-small-loop-variable)
  for (int16_t i = 0; i < tensor.numel(); ++i) {
    ASSERT_TRUE(tensor[i].equal(one * i));
  }
  // NOLINTNEXTLINE(bugprone-too-small-loop-variable)
  for (int8_t i = 0; i < tensor.numel(); ++i) {
    ASSERT_TRUE(tensor[i].equal(one * i));
  }
  // Throw StartsWith("Can only index tensors with integral scalars")
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-magic-numbers,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(tensor[Scalar(3.14)].equal(one));
}

void TestIndexingByZerodimTensor() {
  Tensor tensor = arange(0, 10, kInt);
  Tensor one = ones({}, kInt);
  for (const auto i : c10::irange(tensor.numel())) {
    ASSERT_TRUE(tensor[one * i].equal(one * i));
  }
  // Throw StartsWith(
  //            "Can only index tensors with integral scalars")
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-magic-numbers,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(tensor[ones({}) * 3.14].equal(one));
  // Throw StartsWith("Can only index with tensors that are defined")
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(tensor[Tensor()].equal(one));
  // Throw StartsWith("Can only index with tensors that are scalars (zero-dim)")
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(tensor[ones({2, 3, 4}, kInt)].equal(one));
}
void TestIndexingMixedDevice(DeprecatedTypeProperties& type) {
  Tensor tensor = randn({20, 20}, type);
  Tensor index = arange(10, kLong).cpu();
  Tensor result = tensor.index({index});
  ASSERT_TRUE(result[0].equal(tensor[0]));
}
void TestDispatch() {
  Tensor tensor = randn({20, 20});
  Tensor other = randn({20, 20});
  auto result = tensor.m(relu).m(mse_loss, other, at::Reduction::Mean);
  ASSERT_TRUE(result.allclose(mse_loss(relu(tensor), other)));
}

void TestNegativeDim(DeprecatedTypeProperties& type) {
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(empty({5, -5, 5}, type.options()));
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(empty({5, -5, -5}, type.options()));
  Tensor tensor = empty({5, 5}, type.options());
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_ANY_THROW(tensor.reshape({-5, -5}));
}

void TestView(DeprecatedTypeProperties& type) {
  // Testing the tensor view path, which is different from
  // the Variable view path, see https://github.com/pytorch/pytorch/pull/23452
  // for details
  Tensor tensor = randn({3, 4}, type);;
  Tensor viewed = tensor.view({3, 4});
  tensor.resize_({6, 2});
  ASSERT_TRUE(tensor.sizes().equals({6, 2}));
  ASSERT_TRUE(viewed.sizes().equals({3, 4}));
}

void TestIntArrayRefExpansion(DeprecatedTypeProperties& type) {
  if (type.backend() != Backend::CPU || type.scalarType() != kHalf) {
    max_pool2d(randn({3, 3, 3, 3}, type.options()), 2, 1, 1, 1);
    max_pool3d(randn({3, 3, 3, 3, 3}, type.options()), 2, 1, 1, 1);
    avg_pool2d(randn({3, 3, 3, 3}, type.options()), 2, 1, 1);
    avg_pool3d(randn({3, 3, 3, 3, 3}, type.options()), 2, 1, 1);
  }
}

void test(DeprecatedTypeProperties& type) {
  TestResize(type);
  TestOnesAndDot(type);

  TestSort(type);
  TestRandperm(type);
  TestAdd(type);
  TestZeros(type);
  TestLoadsOfAdds(type);
  TestLoadOfAddsWithCopy(type);
  TestIsContiguous(type);
  TestPermute(type);
  TestMm(type);
  TestSqueeze(type);
  TestCopy(type);
  TestCopyBroadcasting(type);
  TestAbsValue(type);
  TestAddingAValueWithScalar(type);
  TestSelect(type);
  TestZeroDim(type);
  TestToCFloat();
  TestToString();
  TestIndexingByScalar();
  TestIndexingByZerodimTensor();
  TestIndexingMixedDevice(type);
  TestDispatch();
  TestNegativeDim(type);
  TestView(type);
  TestIntArrayRefExpansion(type);
}

TEST(BasicTest, BasicTestCPU) {
  manual_seed(123);

  test(CPU(kFloat));
}

TEST(BasicTest, BasicTestHalfCPU) {
  manual_seed(234);

  test(CPU(kHalf));
}

TEST(BasicTest, BasicTestCUDA) {
  manual_seed(123);

  if (at::hasCUDA()) {
    test(CUDA(kFloat));
  }
}

TEST(BasicTest, FactoryMethodsTest) {
  // Test default values
  at::Tensor tensor0 = at::empty({4});
  ASSERT_EQ(tensor0.dtype(), at::kFloat);
  ASSERT_EQ(tensor0.layout(), at::kStrided);
  ASSERT_EQ(tensor0.device(), at::kCPU);
  ASSERT_FALSE(tensor0.requires_grad());
  ASSERT_FALSE(tensor0.is_pinned());

  // Test setting requires_grad to false.
  tensor0 = at::empty({4}, at::TensorOptions().requires_grad(false));
  ASSERT_EQ(tensor0.dtype(), at::kFloat);
  ASSERT_EQ(tensor0.layout(), at::kStrided);
  ASSERT_EQ(tensor0.device(), at::kCPU);
  ASSERT_FALSE(tensor0.requires_grad());
  ASSERT_FALSE(tensor0.is_pinned());

  // Test setting requires_grad to true.
  // This is a bug. Requires_grad was set to TRUE but this is not implemented.
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  EXPECT_ANY_THROW(at::empty({4}, at::TensorOptions().requires_grad(true)));

  // Test setting dtype
  at::Tensor tensor1 = at::empty({4}, at::TensorOptions().dtype(at::kHalf));
  ASSERT_EQ(tensor1.dtype(), at::kHalf);
  ASSERT_EQ(tensor1.layout(), at::kStrided);
  ASSERT_EQ(tensor1.device(), at::kCPU);
  ASSERT_FALSE(tensor1.requires_grad());
  ASSERT_FALSE(tensor1.is_pinned());

  // Sparse tensor CPU test to avoid requiring CUDA to catch simple bugs.
  // Sparse tensors do not work with static CPU dispatch.
#ifndef ATEN_CPU_STATIC_DISPATCH
  tensor1 = at::empty({4}, at::TensorOptions().dtype(at::kHalf).layout(at::kSparse));
  ASSERT_EQ(tensor1.dtype(), at::kHalf);
  ASSERT_EQ(tensor1.layout(), at::kSparse);
  ASSERT_EQ(tensor1.device(), at::kCPU);
  ASSERT_FALSE(tensor1.requires_grad());
  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  ASSERT_FALSE(tensor1.is_pinned());
#endif // ATEN_CPU_STATIC_DISPATCH

  if (torch::cuda::is_available()) {
    // Test setting pin memory
    tensor1 = at::empty({4}, at::TensorOptions().pinned_memory(true));
    ASSERT_EQ(tensor1.dtype(), at::kFloat);
    ASSERT_EQ(tensor1.layout(), at::kStrided);
    ASSERT_EQ(tensor1.device(), at::kCPU);
    ASSERT_EQ(tensor1.requires_grad(), false);
    ASSERT_FALSE(tensor1.device().is_cuda());
    ASSERT_TRUE(tensor1.is_pinned());

    // Test setting device
    tensor1 = at::empty({4}, at::TensorOptions().device(at::kCUDA));
    ASSERT_EQ(tensor1.dtype(), at::kFloat);
    ASSERT_EQ(tensor1.layout(), at::kStrided);
    ASSERT_TRUE(tensor1.device().is_cuda());
    ASSERT_FALSE(tensor1.requires_grad());
    ASSERT_FALSE(tensor1.is_pinned());

    // Test set everything
    tensor1 = at::empty({4}, at::TensorOptions().dtype(at::kHalf).device(at::kCUDA).layout(at::kSparse).requires_grad(false));
    ASSERT_EQ(tensor1.dtype(), at::kHalf);
    ASSERT_EQ(tensor1.layout(), at::kSparse);
    ASSERT_TRUE(tensor1.device().is_cuda());
    ASSERT_THROWS(tensor1.nbytes());

    // This is a bug
    // Issue https://github.com/pytorch/pytorch/issues/30405
    ASSERT_FALSE(tensor1.requires_grad());
    ASSERT_FALSE(tensor1.is_pinned());
  }

  // Test _like variants
  if (torch::cuda::is_available()) {
    // Issue https://github.com/pytorch/pytorch/issues/28093
    at::Tensor proto = at::empty({1}, at::kDouble);
    tensor0 = at::empty_like(proto, at::kCUDA);
    ASSERT_EQ(tensor0.dtype(), at::kDouble);
    ASSERT_EQ(tensor0.layout(), at::kStrided);
    ASSERT_TRUE(tensor0.device().is_cuda());
    ASSERT_FALSE(tensor0.requires_grad());
    ASSERT_FALSE(tensor0.is_pinned());
  }
}

TEST(BasicTest, BasicStdTestCPU) {
  c10::once_flag flag1, flag2;

  auto simple_do_once = [&]()
  {
      c10::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
  };

  auto may_throw_function = [&](bool do_throw)
  {
    if (do_throw) {
      std::cout << "throw: call_once will retry\n"; // this may appear more than once
      TORCH_CHECK(false, "throw exception");
    }
    std::cout << "Didn't throw, call_once will not attempt again\n"; // guaranteed once
  };

  auto do_once = [&](bool do_throw)
  {
    try {
      c10::call_once(flag2, may_throw_function, do_throw);
    }
    catch (...) {
    }
  };

  std::thread st1(simple_do_once);
  std::thread st2(simple_do_once);
  std::thread st3(simple_do_once);
  std::thread st4(simple_do_once);
  st1.join();
  st2.join();
  st3.join();
  st4.join();

  std::thread t1(do_once, true);
  std::thread t2(do_once, true);
  std::thread t3(do_once, false);
  std::thread t4(do_once, true);
  t1.join();
  t2.join();
  t3.join();
  t4.join();
}

TEST(BasicTest, TestForBlobResizeCPU) {
  // Checks that for_blob can correctly create tensors with non-empty offset and resize them
  std::array<int32_t, 6> storage;
  std::iota(storage.begin(), storage.end(), 1);
  auto t = at::for_blob(storage.data(), {3,}).storage_offset(3).options(c10::TensorOptions(kInt)).make_tensor();
  auto te = *at::expand_size(t, {3, 3});
  ASSERT_EQ(te[1][1].item<int32_t>(), 5);
}

TEST(BasicTest, TestForBlobStridesResizeCPU) {
  // Checks that for_blob can correctly create tensors with non-empty offset and resize them
  std::array<int32_t, 6> storage;
  std::iota(storage.begin(), storage.end(), 1);
  auto t = at::for_blob(storage.data(), {3,}).strides({1,}).storage_offset(3).options(c10::TensorOptions(kInt)).make_tensor();
  auto te = *at::expand_size(t, {3, 3});
  ASSERT_EQ(te[1][1].item<int32_t>(), 5);
}
