#pragma once

#include <memory>
#include <stdint.h>
#include <atomic>

namespace Raybyte {

	template<typename T>
	using Scope = std::unique_ptr<T>;
	
	template<typename T, typename ... Args>
	constexpr Scope<T> CreateScope(Args&& ... args)
	{
		return std::make_unique<T>(std::forward<Args>(args)...);
	}

	class ReferenceCounted
	{
	public:
		void IncrementRefCount() const { m_RefCount++; }
		void DecrementRefCount() const { m_RefCount--; }

		uint32 GetRefCount() const { return m_RefCount.load(); }
		
		virtual bool operator==(const ReferenceCounted& other) const
		{
			return this == &other;
		}

		virtual bool operator!=(const ReferenceCounted& other) const
		{
			return this != &other;
		}
	private:
		mutable std::atomic<uint32> m_RefCount = 0;
	};
	
	template<typename T>
	class Ref
	{
	public:
		Ref()
			: m_Instance(nullptr)
		{
		}

		Ref(std::nullptr_t n)
			: m_Instance(nullptr)
		{
		}

		Ref(T* instance)
			: m_Instance(instance)
		{
			static_assert(std::is_base_of<ReferenceCounted, T>::value, "Class is not ReferenceCounted!");

			IncRef();
		}

		Ref(const Ref<T>& other)
			: m_Instance(other.m_Instance)
		{
			IncRef();
		}

		template<typename T2>
		Ref(const Ref<T2>& other)
		{
			m_Instance = (T*)other.m_Instance;
			IncRef();
		}
		
		template<typename T2>
		Ref(Ref<T2>&& other)
		{
			m_Instance = (T*)other.m_Instance;
			other.m_Instance = nullptr;
		}

		~Ref()
		{
			DecRef();
		}

		Ref& operator=(std::nullptr_t)
		{
			DecRef();
			m_Instance = nullptr;
			return *this;
		}

		Ref& operator=(const Ref<T>& other)
		{
			other.IncRef();
			DecRef();
			m_Instance = other.m_Instance;
			return *this;
		}

		template<typename T2>
		Ref& operator=(const Ref<T2>& other)
		{
			DecRef();
			m_Instance = other.m_Instance;
			other.m_Instance = nullptr;
			return *this;
		}

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

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

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

		T* Get() { return m_Instance; }
		const T* Get() const { return m_Instance; }

		template<typename T2>
		Ref<T2> As() const
		{
			return Ref<T2>(*this);
		}

		void Reset(T* instance = nullptr)
		{
			DecRef();
			m_Instance = instance;
		}
		
		template<typename... Args>
		static Ref<T> Create(Args&&... args)
		{
			return Ref<T>(new T(std::forward<Args>(args)...));
		}

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

		bool operator!=(const Ref<T>& other) const
		{
			return !(*this == other);
		}

		bool Equals(const Ref<T>& other)
		{
			if (!m_Instance || !other.m_Instance)
				return false;
			return *m_Instance == *other.m_Instance;
		}
	private:
		void IncRef() const
		{
			if (m_Instance)
				m_Instance->IncrementRefCount();
		}

		void DecRef() const
		{
			if (m_Instance)
			{
				m_Instance->DecrementRefCount();

				if (m_Instance->GetRefCount() == 0)
				{
					delete m_Instance;
					m_Instance = nullptr;
				}
			}
		}

		template<class T2>
		friend class Ref;
		mutable T* m_Instance;
	};

}