// Copyright (c) Sleipnir contributors

#pragma once

#include <concepts>
#include <cstddef>
#include <memory>
#include <utility>

namespace slp {

/// A custom intrusive shared pointer implementation without thread
/// synchronization overhead.
///
/// Types used with this class should have three things:
///
/// 1. A zero-initialized public counter variable that serves as the shared
///    pointer's reference count.
/// 2. A free function `void inc_ref_count(T*)` that increments the reference
///    count.
/// 3. A free function `void dec_ref_count(T*)` that decrements the reference
///    count and deallocates the pointed to object if the reference count
///    reaches zero.
///
/// @tparam T The type of the object to be reference counted.
template <typename T>
class IntrusiveSharedPtr {
 public:
  template <typename>
  friend class IntrusiveSharedPtr;

  /// Constructs an empty intrusive shared pointer.
  constexpr IntrusiveSharedPtr() noexcept = default;

  /// Constructs an empty intrusive shared pointer.
  // NOLINTNEXTLINE (google-explicit-constructor)
  constexpr IntrusiveSharedPtr(std::nullptr_t) noexcept {}

  /// Constructs an intrusive shared pointer from the given pointer and takes
  /// ownership.
  ///
  /// @param ptr The pointer of which to take ownership.
  explicit constexpr IntrusiveSharedPtr(T* ptr) noexcept : m_ptr{ptr} {
    if (m_ptr != nullptr) {
      inc_ref_count(m_ptr);
    }
  }

  constexpr ~IntrusiveSharedPtr() {
    if (m_ptr != nullptr) {
      dec_ref_count(m_ptr);
    }
  }

  /// Copy constructs from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  constexpr IntrusiveSharedPtr(const IntrusiveSharedPtr<T>& rhs) noexcept
      : m_ptr{rhs.m_ptr} {
    if (m_ptr != nullptr) {
      inc_ref_count(m_ptr);
    }
  }

  /// Copy constructs from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  template <typename U>
    requires(!std::same_as<T, U> && std::convertible_to<U*, T*>)
  // NOLINTNEXTLINE (google-explicit-constructor)
  constexpr IntrusiveSharedPtr(const IntrusiveSharedPtr<U>& rhs) noexcept
      : m_ptr{rhs.m_ptr} {
    if (m_ptr != nullptr) {
      inc_ref_count(m_ptr);
    }
  }

  /// Makes a copy of the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  /// @return This intrusive shared pointer.
  // NOLINTNEXTLINE (google-explicit-constructor)
  constexpr IntrusiveSharedPtr<T>& operator=(
      const IntrusiveSharedPtr<T>& rhs) noexcept {
    if (m_ptr == rhs.m_ptr) {
      return *this;
    }

    if (m_ptr != nullptr) {
      dec_ref_count(m_ptr);
    }

    m_ptr = rhs.m_ptr;

    if (m_ptr != nullptr) {
      inc_ref_count(m_ptr);
    }

    return *this;
  }

  /// Makes a copy of the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  /// @return This intrusive shared pointer.
  template <typename U>
    requires(!std::same_as<T, U> && std::convertible_to<U*, T*>)
  // NOLINTNEXTLINE (google-explicit-constructor)
  constexpr IntrusiveSharedPtr<T>& operator=(
      const IntrusiveSharedPtr<U>& rhs) noexcept {
    if (m_ptr == rhs.m_ptr) {
      return *this;
    }

    if (m_ptr != nullptr) {
      dec_ref_count(m_ptr);
    }

    m_ptr = rhs.m_ptr;

    if (m_ptr != nullptr) {
      inc_ref_count(m_ptr);
    }

    return *this;
  }

  /// Move constructs from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  constexpr IntrusiveSharedPtr(IntrusiveSharedPtr<T>&& rhs) noexcept
      : m_ptr{std::exchange(rhs.m_ptr, nullptr)} {}

  /// Move constructs from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  template <typename U>
    requires(!std::same_as<T, U> && std::convertible_to<U*, T*>)
  // NOLINTNEXTLINE (google-explicit-constructor)
  constexpr IntrusiveSharedPtr(IntrusiveSharedPtr<U>&& rhs) noexcept
      : m_ptr{std::exchange(rhs.m_ptr, nullptr)} {}

  /// Move assigns from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  /// @return This intrusive shared pointer.
  constexpr IntrusiveSharedPtr<T>& operator=(
      IntrusiveSharedPtr<T>&& rhs) noexcept {
    if (m_ptr == rhs.m_ptr) {
      return *this;
    }

    std::swap(m_ptr, rhs.m_ptr);

    return *this;
  }

  /// Move assigns from the given intrusive shared pointer.
  ///
  /// @param rhs The other intrusive shared pointer.
  /// @return This intrusive shared pointer.
  template <typename U>
    requires(!std::same_as<T, U> && std::convertible_to<U*, T*>)
  constexpr IntrusiveSharedPtr<T>& operator=(
      IntrusiveSharedPtr<U>&& rhs) noexcept {
    if (m_ptr == rhs.m_ptr) {
      return *this;
    }

    std::swap(m_ptr, rhs.m_ptr);

    return *this;
  }

  /// Returns the internal pointer.
  ///
  /// @return The internal pointer.
  constexpr T* get() const noexcept { return m_ptr; }

  /// Returns the object pointed to by the internal pointer.
  ///
  /// @return The object pointed to by the internal pointer.
  constexpr T& operator*() const noexcept { return *m_ptr; }

  /// Returns the internal pointer.
  ///
  /// @return The internal pointer.
  constexpr T* operator->() const noexcept { return m_ptr; }

  /// Returns true if the internal pointer isn't nullptr.
  ///
  /// @return True if the internal pointer isn't nullptr.
  explicit constexpr operator bool() const noexcept { return m_ptr != nullptr; }

  /// Returns true if the given intrusive shared pointers point to the same
  /// object.
  ///
  /// @param lhs The left-hand side.
  /// @param rhs The right-hand side.
  friend constexpr bool operator==(const IntrusiveSharedPtr<T>& lhs,
                                   const IntrusiveSharedPtr<T>& rhs) noexcept {
    return lhs.m_ptr == rhs.m_ptr;
  }

  /// Returns true if the given intrusive shared pointers point to different
  /// objects.
  ///
  /// @param lhs The left-hand side.
  /// @param rhs The right-hand side.
  friend constexpr bool operator!=(const IntrusiveSharedPtr<T>& lhs,
                                   const IntrusiveSharedPtr<T>& rhs) noexcept {
    return lhs.m_ptr != rhs.m_ptr;
  }

  /// Returns true if the left-hand intrusive shared pointer points to nullptr.
  ///
  /// @param lhs The left-hand side.
  friend constexpr bool operator==(const IntrusiveSharedPtr<T>& lhs,
                                   std::nullptr_t) noexcept {
    return lhs.m_ptr == nullptr;
  }

  /// Returns true if the right-hand intrusive shared pointer points to nullptr.
  ///
  /// @param rhs The right-hand side.
  friend constexpr bool operator==(std::nullptr_t,
                                   const IntrusiveSharedPtr<T>& rhs) noexcept {
    return nullptr == rhs.m_ptr;
  }

  /// Returns true if the left-hand intrusive shared pointer doesn't point to
  /// nullptr.
  ///
  /// @param lhs The left-hand side.
  friend constexpr bool operator!=(const IntrusiveSharedPtr<T>& lhs,
                                   std::nullptr_t) noexcept {
    return lhs.m_ptr != nullptr;
  }

  /// Returns true if the right-hand intrusive shared pointer doesn't point to
  /// nullptr.
  ///
  /// @param rhs The right-hand side.
  friend constexpr bool operator!=(std::nullptr_t,
                                   const IntrusiveSharedPtr<T>& rhs) noexcept {
    return nullptr != rhs.m_ptr;
  }

 private:
  T* m_ptr = nullptr;
};

/// Constructs an object of type T and wraps it in an intrusive shared pointer
/// using args as the parameter list for the constructor of T.
///
/// @tparam T Type of object for intrusive shared pointer.
/// @tparam Args Types of constructor arguments.
/// @param args Constructor arguments for T.
template <typename T, typename... Args>
IntrusiveSharedPtr<T> make_intrusive_shared(Args&&... args) {
  return IntrusiveSharedPtr<T>{new T(std::forward<Args>(args)...)};
}

/// Constructs an object of type T and wraps it in an intrusive shared pointer
/// using alloc as the storage allocator of T and args as the parameter list for
/// the constructor of T.
///
/// @tparam T Type of object for intrusive shared pointer.
/// @tparam Alloc Type of allocator for T.
/// @tparam Args Types of constructor arguments.
/// @param alloc The allocator for T.
/// @param args Constructor arguments for T.
template <typename T, typename Alloc, typename... Args>
IntrusiveSharedPtr<T> allocate_intrusive_shared(Alloc alloc, Args&&... args) {
  auto ptr = std::allocator_traits<Alloc>::allocate(alloc, sizeof(T));
  std::allocator_traits<Alloc>::construct(alloc, ptr,
                                          std::forward<Args>(args)...);
  return IntrusiveSharedPtr<T>{ptr};
}

}  // namespace slp
