#ifndef TILE_BASE_REF_PTR_H
#define TILE_BASE_REF_PTR_H

#pragma once

#include "tile/base/casting.h"
#include "tile/base/down_cast.h"
#include "tile/base/internal/logging.h"
#include "tile/base/internal/meta.h"
#include "tile/base/internal/utility.h"
#include "tile/base/likely.h"

#include <atomic>
#include <utility>

namespace tile {
// namespace detail {
// template <typename T> struct has_after_ref {
//   template <typename U, decltype(std::declval<U>().AfterRef(
//                             std::declval<U *>(),
//                             std::declval<std::uint32_t>())) * = nullptr>
//   static int Test(int);
//   template <typename...> static char Test(...);
//   static constexpr bool value = std::is_same<decltype(Test<T>(0)),
//   int>::value;
// };
//
// template <typename T> struct has_before_deref {
//   template <typename U, decltype(std::declval<U>().BeforeDeref(
//                             std::declval<U *>(),
//                             std::declval<std::uint32_t>())) * = nullptr>
//   static int Test(int);
//   template <typename...> static char Test(...);
//   static constexpr bool value = std::is_same<decltype(Test<T>(0)),
//   int>::value;
// };

// } // namespace detail

struct ref_ptr_t {
    explicit ref_ptr_t() = default;
};

struct adopt_ptr_t {
    explicit adopt_ptr_t() = default;
};

constexpr auto ref_ptr   = ref_ptr_t();
constexpr auto adopt_ptr = adopt_ptr_t();

template<typename T, typename = void>
struct RefTraits {
    static void Reference(T *ptr) noexcept
    {
        static_assert(sizeof(T) == 0,
                      "To use `RefPtr<T>`, you need either inherit from `RefCounted<T>` "
                      "(which you dind't), or specialize `RefTraits<T>`");
    }

    static void Dereference(T *ptr) noexcept
    {
        static_assert(sizeof(T) == 0,
                      "To use `RefPtr<T>`, you need either inherit from `RefCounted<T>` "
                      "(which you dind't), or specialize `RefTraits<T>`");
    }
};

template<typename T, typename Deleter = std::default_delete<T>>
class RefCounted {
public:
    void Ref() noexcept
    {
        auto was = ref_count_.fetch_add(1, std::memory_order_relaxed);
        TILE_DCHECK_GT(was, 0);
    }

    void Deref() noexcept
    {
        auto was = ref_count_.fetch_sub(1, std::memory_order_acq_rel);
        if (was == 1) {
            Deleter()(static_cast<T *>(this));
        } else {
            TILE_DCHECK_GT(was, 1);
        }
    }

    std::uint32_t UnsafeRefCount() const noexcept { return ref_count_.load(std::memory_order_relaxed); }

protected:
    RefCounted() = default;

    // Recycle dead object for custom deleter
    void Save() noexcept
    {
        bool is_dead = ref_count_.fetch_add(1, std::memory_order_relaxed) == 0;
        TILE_CHECK(is_dead);
    }

private:
    friend Deleter;
    std::atomic<std::uint32_t> ref_count_{1};
};

static_assert(sizeof(RefCounted<int>) == sizeof(std::atomic<std::uint32_t>), "");

namespace detail {

struct not_ref_counted_tag_t {
    explicit not_ref_counted_tag_t() = default;
};

struct not_ref_counted_type_tag_t {
    explicit not_ref_counted_type_tag_t() = default;
};

template<class T, class... Us>
RefCounted<T, Us...> GetRefCountedType(const RefCounted<T, Us...> *);
template<class T>
int GetRefCountedType(...);
int GetRefCountedType(...);// Match failure.

// Extract `T` from `RefCounted<T, ...>`.
//
// We're returning `std::common_type<T>` instead of `T` so as not to require `T`
// to be complete.
template<class T, class... Us>
std::common_type<T> GetRefeeType(const RefCounted<T, Us...> *);
std::common_type<int> GetRefeeType(...);

// - if `T` is explicitly specified, it tests if `T` inherits from
// `RefCounted<T, ...>`
//   and returns `RefCounted<T, ...>`
//
// - if `T` is not specified, it tests if `T` inherits from some `RefCounted<U,
// ...>`
//   and returns `RefCounted<U, ...>`
//
// - Otherwise `not_ref_counted_tag_t` is returned.

// template <typename T, typename... Args> struct is_ref_counted {
//   template <typename U, typename... Ts>
//   static int Test(const RefCounted<U, Ts...> *);
//
//   template <typename...> static char Test(...);
//
//   static constexpr bool value =
//       std::is_same<decltype(Test<T, Args...>(reinterpret_cast<const T
//       *>(0))),
//                    int>::value;
// };

template<class T>
using refee_t = typename decltype(GetRefeeType(reinterpret_cast<const T *>(0)))::type;

// If `T` inherits from `RefCounted<T, ...>` directly, `T` is returned,
// otherwise `int` (which can never inherits `RefCounted<int, ...>`) is
// returned.
template<class T>
using direct_refee_t = refee_t<decltype(GetRefCountedType<T>(reinterpret_cast<const T *>(0)))>;

// Test if `T` is a subclass of some `RefCounted<T, ...>`.
template<class T>
struct is_ref_counted_directly {
    static constexpr bool value = !std::is_same<int, direct_refee_t<T>>::value;
};

// If `T` is inheriting from some `RefCounted<U, ...>` unambiguously, this
// method returns `U`. Otherwise it returns `int`.
template<class T>
using indirect_refee_t = refee_t<decltype(GetRefCountedType(reinterpret_cast<const T *>(0)))>;

// `T` is inherited from `RefCounted<U, ...>` indirectly;   (1)
// `T` is inherited from `RefCounted<U, ...>` directly;     (2)
// `U` has a virtual destructor                             (3)
template<typename T>
struct is_ref_counted_indirectly_safe {
    static constexpr bool value = !is_ref_counted_directly<T>::value &&// 1
        is_ref_counted_directly<indirect_refee_t<T>>::value &&         // 2
        std::has_virtual_destructor<indirect_refee_t<T>>::value;       // 3
};

template<class T>
using as_ref_counted_t = decltype(GetRefCountedType(reinterpret_cast<const T *>(0)));

// - `T` is directly inheriting `RefCounted<T,...>`
//   (destructor does not have to be `virtual`)
// - `T` inherits from `RefCounted<U, ...>` and `U`'s destructor is virtual
template<typename T>
struct is_default_ref_traits_safe {
    static constexpr bool value =
        detail::is_ref_counted_directly<T>::value || detail::is_ref_counted_indirectly_safe<T>::value;
};

template<typename T>
struct is_ref_type {
    template<typename U = T,
             typename   = decltype(std::declval<U>().Ref()),
             typename   = decltype(std::declval<U>().Deref()),
             typename   = decltype(std::declval<U>().UnsafeRefCount())>
    static int Test(int);
    template<typename>
    static char Test(...);
    static constexpr bool value = std::is_same<decltype(Test<T>(0)), int>::value;
};

}// namespace detail

// template <typename T>
// struct RefTraits<T, enable_if_t<detail::is_ref_type<T>::value>> {
//   static void Reference(T *ptr) noexcept {
//     TILE_DCHECK_GT(ptr->UnsafeRefCount(), 0);
//     ptr->Ref();
//   }
//
//   static void Dereference(T *ptr) noexcept {
//     TILE_DCHECK_GT(ptr->UnsafeRefCount(), 0);
//     ptr->Deref();
//   }
// };

// RefTraits for `RefCounted`
template<typename T, typename Deleter>
struct RefTraits<RefCounted<T, Deleter>> {
    static void Reference(RefCounted<T, Deleter> *ptr) noexcept
    {
        TILE_DCHECK_GT(ptr->UnsafeRefCount(), 0);
        ptr->Ref();
    }

    static void Dereference(RefCounted<T, Deleter> *ptr) noexcept
    {
        TILE_DCHECK_GT(ptr->UnsafeRefCount(), 0);
        ptr->Deref();
    }
};

// template <typename T>
// struct RefTraits<
//     T, enable_if_t<std::is_same<detail::as_ref_counted_t<T>, T>::value &&
//                    detail::is_default_ref_traits_safe<T>::value>> {
//   static void Reference(T *ptr) noexcept { ptr->Ref(); }
//   static void Dereference(T *ptr) noexcept { ptr->Deref(); }
// };

// RefTraits for the subclass of `RefCounted`
template<typename T>
struct RefTraits<
    T,
    enable_if_t<!std::is_same<detail::as_ref_counted_t<T>, T>::value && detail::is_default_ref_traits_safe<T>::value>>
    : public RefTraits<detail::as_ref_counted_t<T>> {};

template<typename T>
class RefPtr final {
    using Traits = RefTraits<T>;

public:
    constexpr RefPtr() noexcept : ptr_(nullptr) {}

    constexpr RefPtr(std::nullptr_t) noexcept : ptr_(nullptr) {}

    ~RefPtr() noexcept
    {
        if (ptr_) { Traits::Dereference(ptr_); }
    }

    RefPtr(ref_ptr_t, T *ptr) noexcept : ptr_(ptr)
    {
        if (ptr_) { Traits::Reference(ptr); }
    }

    RefPtr(adopt_ptr_t, T *ptr) noexcept : ptr_(ptr) {}

    template<typename U, typename = enable_if_t<std::is_convertible<U *, T *>::value>>
    constexpr RefPtr(RefPtr<U> ptr) noexcept : ptr_(ptr.Leak())
    {}

    RefPtr(const RefPtr &other) noexcept : ptr_(other.ptr_)
    {
        if (ptr_) { Traits::Reference(ptr_); }
    }

    RefPtr(RefPtr &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; }

    RefPtr &operator=(const RefPtr &other) noexcept
    {
        if (TILE_UNLIKELY(&other == this)) { return *this; }

        if (ptr_) { Traits::Dereference(ptr_); }

        ptr_ = other.ptr_;

        if (ptr_) { Traits::Reference(ptr_); }
        return *this;
    }

    RefPtr &operator=(RefPtr &&ptr) noexcept
    {
        if (TILE_UNLIKELY(&ptr == this)) { return *this; }
        if (ptr_) { Traits::Dereference(ptr_); }
        ptr_     = ptr.ptr_;
        ptr.ptr_ = nullptr;
        return *this;
    }

    RefPtr &operator=(std::nullptr_t) noexcept
    {
        Reset();
        return *this;
    }

    constexpr T *operator->() const noexcept { return ptr_; }

    constexpr T &operator*() const noexcept { return *ptr_; }

    constexpr T *Get() const noexcept { return ptr_; }

    constexpr explicit operator bool() const noexcept { return !!ptr_; }

    void Reset() noexcept
    {
        if (ptr_) {
            Traits::Dereference(ptr_);
            ptr_ = nullptr;
        }
    }

    void Reset(ref_ptr_t, T *ptr) noexcept
    {
        Reset();
        ptr_ = ptr;
        Traits::Reference(ptr);
    }

    void Reset(adopt_ptr_t, T *ptr) noexcept
    {
        Reset();
        ptr_ = ptr;
    }

    TILE_NODISCARD T *Leak() noexcept { return internal::Exchange(ptr_, nullptr); }

    template<typename U>
    bool Is() const noexcept
    {
        return isa<U>(ptr_);
    }

    template<typename U>
    RefPtr<U> As() const noexcept
    {
        return RefPtr<U>(ref_ptr, down_cast<U>(ptr_));
    }

private:
    T *ptr_;
};

template<typename T, typename... Args>
RefPtr<T>
MakeRefCounted(Args &&...args)
{
    return RefPtr<T>(adopt_ptr, new T(std::forward<Args>(args)...));
}

template<typename T>
constexpr bool
operator==(const RefPtr<T> &lhs, const RefPtr<T> &rhs) noexcept
{
    return lhs.Get() == rhs.Get();
}

template<typename T>
constexpr bool
operator==(const RefPtr<T> &lhs, std::nullptr_t) noexcept
{
    return lhs.Get() == nullptr;
}

template<typename T>
constexpr bool
operator==(std::nullptr_t, const RefPtr<T> &rhs) noexcept
{
    return nullptr == rhs.Get();
}
};// namespace tile

#endif// TILE_BASE_REF_PTR_H
