// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// SPDX-FileCopyrightText: Copyright Contributors to the Kokkos project

#include <impl/Kokkos_StringManipulation.hpp>
#include <impl/Kokkos_HostSharedPtr.hpp>
#include <Kokkos_Macros.hpp>
#ifdef KOKKOS_ENABLE_EXPERIMENTAL_CXX20_MODULES
import kokkos.core;
#else
#include <Kokkos_Core.hpp>
#endif

#include <new>
#include <gtest/gtest.h>

using Kokkos::Impl::HostSharedPtr;

namespace {

class Data {
  char d[64];

 public:
  KOKKOS_FUNCTION void write(char const* s) {
    Kokkos::Impl::strncpy(d, s, sizeof(d));
  }
};

template <class SmartPtr>
struct CheckAccessStoredPointerAndDereferenceOnDevice {
  SmartPtr m_device_ptr;
  using ElementType = typename SmartPtr::element_type;
  static_assert(std::is_same_v<ElementType, Data>);

  CheckAccessStoredPointerAndDereferenceOnDevice(SmartPtr device_ptr)
      : m_device_ptr(device_ptr) {
    int errors;
    Kokkos::parallel_reduce(Kokkos::RangePolicy<TEST_EXECSPACE>(0, 1), *this,
                            errors);
    EXPECT_EQ(errors, 0);
  }

  KOKKOS_FUNCTION void operator()(int, int& e) const {
    auto raw_ptr = m_device_ptr.get();  // get

    auto tmp = new (raw_ptr) ElementType();

    auto& obj = *m_device_ptr;  // operator*
    if (&obj != raw_ptr) ++e;

    m_device_ptr->write("hello world");  // operator->

    tmp->~ElementType();
  }
};

template <class Ptr>
CheckAccessStoredPointerAndDereferenceOnDevice<Ptr>
check_access_stored_pointer_and_dereference_on_device(Ptr p) {
  return {p};
}

template <class SmartPtr>
struct CheckSpecialMembersOnDevice {
  SmartPtr m_device_ptr;

  KOKKOS_FUNCTION void operator()(int, int& e) const {
    SmartPtr p1 = m_device_ptr;   // copy construction
    SmartPtr p2 = std::move(p1);  // move construction

    p1 = p2;             // copy assignment
    p2 = std::move(p1);  // move assignment

    SmartPtr p3;  // default constructor
    if (p3) ++e;
    SmartPtr p4{nullptr};
    if (p4) ++e;
  }

  CheckSpecialMembersOnDevice(SmartPtr device_ptr) : m_device_ptr(device_ptr) {
    int errors;
    Kokkos::parallel_reduce(Kokkos::RangePolicy<TEST_EXECSPACE>(0, 1), *this,
                            errors);
    EXPECT_EQ(errors, 0);
  }
};

template <class Ptr>
CheckSpecialMembersOnDevice<Ptr> check_special_members_on_device(Ptr p) {
  return {p};
}

}  // namespace

TEST(TEST_CATEGORY, host_shared_ptr_dereference_on_device) {
  using T = Data;

  using MemorySpace = TEST_EXECSPACE::memory_space;

  HostSharedPtr<T> device_ptr(
      static_cast<T*>(Kokkos::kokkos_malloc<MemorySpace>(sizeof(T))),
      [](T* p) { Kokkos::kokkos_free<MemorySpace>(p); });

  check_access_stored_pointer_and_dereference_on_device(device_ptr);
}

TEST(TEST_CATEGORY, host_shared_ptr_special_members_on_device) {
  using T = Data;

  using MemorySpace = TEST_EXECSPACE::memory_space;

  HostSharedPtr<T> device_ptr(
      static_cast<T*>(Kokkos::kokkos_malloc<MemorySpace>(sizeof(T))),
      [](T* p) { Kokkos::kokkos_free<MemorySpace>(p); });

  check_special_members_on_device(device_ptr);
}

namespace {

struct Bar {
  double val;
};

struct Foo {
  Foo(bool allocate = false) : ptr(allocate ? new Bar : nullptr) {}
  Kokkos::Impl::HostSharedPtr<Bar> ptr;
  int use_count() { return ptr.use_count(); }
};

template <class DevMemSpace, class HostMemSpace>
void host_shared_ptr_test_reference_counting() {
  using ExecSpace = typename DevMemSpace::execution_space;
  bool is_gpu =
      !Kokkos::SpaceAccessibility<ExecSpace, Kokkos::HostSpace>::accessible;

  // Create two tracked instances
  Foo f1(true), f2(true);
  // Scope Views
  {
    Foo* fp_d_ptr =
        static_cast<Foo*>(Kokkos::kokkos_malloc<DevMemSpace>(sizeof(Foo)));
    Kokkos::View<Foo, DevMemSpace> fp_d(fp_d_ptr);
    // If using UVM or on the CPU don't make an extra HostCopy
    Foo* fp_h_ptr = std::is_same_v<DevMemSpace, HostMemSpace>
                        ? fp_d_ptr
                        : static_cast<Foo*>(
                              Kokkos::kokkos_malloc<HostMemSpace>(sizeof(Foo)));
    Kokkos::View<Foo, HostMemSpace> fp_h(fp_h_ptr);
    ASSERT_EQ(1, f1.use_count());
    ASSERT_EQ(1, f2.use_count());

    // Just for the sake of it initialize the data of the host copy
    new (fp_h.data()) Foo();
    // placement new in kernel
    //  if on GPU: should not increase use_count, fp_d will not be tracked
    //  if on Host: refcount will increase fp_d is tracked
    Kokkos::parallel_for(
        Kokkos::RangePolicy<ExecSpace>(0, 1),
        KOKKOS_LAMBDA(int) { new (fp_d.data()) Foo(f1); });
    Kokkos::fence();
    Kokkos::deep_copy(fp_h, fp_d);

    if (is_gpu)
      ASSERT_EQ(1, f1.use_count());
    else
      ASSERT_EQ(2, f1.use_count());
    ASSERT_EQ(1, f2.use_count());

    // assignment operator on host, will increase f2 use_count
    //   if default device is GPU: fp_h was untracked
    //   if default device is CPU: fp_h was tracked and use_count was 2 for
    //   aliasing f1, in which case use_count will be decreased here
    fp_h() = f2;
    ASSERT_EQ(1, f1.use_count());
    ASSERT_EQ(2, f2.use_count());

    Kokkos::deep_copy(fp_d, fp_h);
    ASSERT_EQ(1, f1.use_count());
    ASSERT_EQ(2, f2.use_count());

    // assignment in kernel:
    //  If on GPU: should not increase use_count of f1 and fp_d will not be
    //  tracked.
    //  If on Host: use_count will increase of f1, fp_d is tracked,
    //  use_count of f2 goes down.
    //  Since we are messing with the use count on the device: make host copy
    //  untracked first. Note if fp_d and fp_h alias each other (e.g. compiling
    //  for CPU only) that means fp_d() will be untracked too during assignemnt
    fp_h() = Foo();
    Kokkos::parallel_for(
        Kokkos::RangePolicy<ExecSpace>(0, 1),
        KOKKOS_LAMBDA(int) { fp_d() = f1; });
    Kokkos::fence();
    Kokkos::deep_copy(fp_h, fp_d);

    if (is_gpu)
      ASSERT_EQ(1, f1.use_count());
    else
      ASSERT_EQ(2, f1.use_count());
    ASSERT_EQ(1, f2.use_count());

    // Assign non-tracked ptr
    //   if  if_gpu will not change use_count
    //   if !is_gpu will decrease use_count of f1
    fp_h() = Foo();
    ASSERT_EQ(1, f1.use_count());
    ASSERT_EQ(1, f2.use_count());
    fp_h() = f2;
    ASSERT_EQ(1, f1.use_count());
    ASSERT_EQ(2, f2.use_count());

    // before deleting host version make sure its not tracked
    fp_h() = Foo();
    if (fp_h_ptr != fp_d_ptr) Kokkos::kokkos_free<HostMemSpace>(fp_h_ptr);
    Kokkos::kokkos_free<DevMemSpace>(fp_d_ptr);
  }

  ASSERT_EQ(1, f1.use_count());
  ASSERT_EQ(1, f2.use_count());
}
}  // namespace

TEST(TEST_CATEGORY, host_shared_ptr_tracking) {
  host_shared_ptr_test_reference_counting<typename TEST_EXECSPACE::memory_space,
                                          Kokkos::HostSpace>();
#ifdef KOKKOS_ENABLE_CUDA
  if (std::is_same_v<TEST_EXECSPACE, Kokkos::Cuda>)
    host_shared_ptr_test_reference_counting<Kokkos::CudaUVMSpace,
                                            Kokkos::CudaUVMSpace>();
#endif
#ifdef KOKKOS_ENABLE_SYCL
  if (std::is_same_v<TEST_EXECSPACE, Kokkos::SYCL>)
    host_shared_ptr_test_reference_counting<Kokkos::SYCLSharedUSMSpace,
                                            Kokkos::SYCLSharedUSMSpace>();
#endif
#ifdef KOKKOS_ENABLE_HIP
  if (std::is_same_v<TEST_EXECSPACE, Kokkos::HIP>) {
    host_shared_ptr_test_reference_counting<Kokkos::HIPHostPinnedSpace,
                                            Kokkos::HIPHostPinnedSpace>();
    host_shared_ptr_test_reference_counting<Kokkos::HIPManagedSpace,
                                            Kokkos::HIPManagedSpace>();
  }
#endif
}
