#pragma once
#include <luna/Alloc.h>
#include <luna/Atomic.h>
#include <luna/Hash.h>
#include <luna/OwnedPtr.h>
#include <luna/Result.h>
#include <luna/ScopeGuard.h>
#include <luna/TypeTraits.h>

template <typename T> class SharedPtr;
template <typename T> SharedPtr<T> adopt_shared(T*);

struct Shareable
{
    void ref()
    {
        m_ref_count++;
    }

    bool unref()
    {
        m_ref_count--;
        return m_ref_count == 0;
    }

    Atomic<int> m_ref_count { 0 };
};

template <typename T> class SharedPtr
{
  public:
    SharedPtr()
    {
        m_ptr = nullptr;
    }

    SharedPtr(T* ptr) : m_ptr(ptr)
    {
        if (m_ptr) shareable()->ref();
    }

    SharedPtr(const SharedPtr<T>& other) : m_ptr(other.m_ptr)
    {
        if (m_ptr) shareable()->ref();
    }

    SharedPtr(SharedPtr<T>&& other) : m_ptr(other.m_ptr)
    {
        other.m_ptr = nullptr;
    }

    template <typename Tp> operator SharedPtr<Tp>()
    {
        return { (Tp*)m_ptr };
    }

    ~SharedPtr()
    {
        if (m_ptr && shareable()->unref()) { delete m_ptr; }
    }

    SharedPtr<T>& operator=(const SharedPtr<T>& other)
    {
        if (&other == this) return *this;

        if (m_ptr && shareable()->unref()) { delete m_ptr; }

        m_ptr = other.m_ptr;

        if (m_ptr) shareable()->ref();

        return *this;
    }

    bool operator==(const SharedPtr<T>& other)
    {
        return m_ptr == other.m_ptr;
    }

    T* ptr() const
    {
        return m_ptr;
    }

    T* operator->() const
    {
        return m_ptr;
    }

    T& operator*() const
    {
        return *m_ptr;
    }

    operator bool() const
    {
        return m_ptr != nullptr;
    }

  private:
    T* m_ptr;

    Shareable* shareable()
    {
        static_assert(IsBaseOf<Shareable, T>);
        return (Shareable*)m_ptr;
    }
};

template <typename T> SharedPtr<T> adopt_shared(T* ptr)
{
    return SharedPtr<T> { ptr };
}

template <typename T, class... Args> Result<SharedPtr<T>> make_shared(Args... args)
{
    T* raw_ptr = TRY(make<T>(args...));
    return adopt_shared(raw_ptr);
}

template <typename T> Result<SharedPtr<T>> adopt_shared_if_nonnull(T* ptr)
{
    if (ptr) return adopt_shared(ptr);
    else
        return err(ENOMEM);
}

template <typename T> SharedPtr<T> adopt_shared_from_owned(OwnedPtr<T>&& other)
{
    T* ptr = other.m_ptr;
    other.m_ptr = nullptr;

    return SharedPtr<T> { ptr };
}