#pragma once

#include <Core/Common/CoreUtils.h>
#include <type_traits>

namespace Juliet
{
    template <typename Type, typename OtherType>
    concept NonNullPtr_Convertible = std::is_convertible_v<OtherType*, Type*>;

    template <typename Type, typename OtherType>
    concept NonNullPtr_SameType = std::is_same_v<OtherType*, Type*>;

    template <typename Type>
    class NonNullPtr
    {
      public:
        inline NonNullPtr(Type* ptr)
          : InternalPtr(ptr)
        {
            Assert(ptr, "Tried to initialize a NonNullPtr with a null pointer");
        }

        template <typename OtherType>
            requires NonNullPtr_Convertible<OtherType*, Type*>
        NonNullPtr(const NonNullPtr<OtherType>& otherPtr)
          : InternalPtr(otherPtr.Get())
        {
            Assert(InternalPtr, "Fatal Error: Assigned a non null ptr using another NonNullPtr but its was null.");
        }

        // Assignment
        NonNullPtr& operator=(Type* ptr)
        {
            Assert(ptr, "Tried to assign a null pointer to a NonNullPtr!");
            InternalPtr = ptr;
            return *this;
        }

        template <typename OtherType>
            requires NonNullPtr_Convertible<OtherType*, Type*>
        NonNullPtr& operator=(const NonNullPtr<OtherType>& otherPtr)
        {
            InternalPtr = otherPtr.Get();
            return *this;
        }

        // Accessors
        operator Type*() const
        {
            Assert(InternalPtr, "NonNullPtr: Internal Pointer is Null");
            return InternalPtr;
        }

        Type* Get() const
        {
            Assert(InternalPtr, "NonNullPtr: Internal Pointer is Null");
            return InternalPtr;
        }

        Type& operator*() const
        {
            Assert(InternalPtr, "NonNullPtr: Internal Pointer is Null");
            return *InternalPtr;
        }

        inline Type* operator->() const
        {
            Assert(InternalPtr, "NonNullPtr: Internal Pointer is Null");
            return InternalPtr;
        }

        // Comparisons
        bool operator==(const NonNullPtr& otherPtr) const { return InternalPtr == otherPtr.InternalPtr; }

        template <typename OtherType>
            requires NonNullPtr_SameType<Type, OtherType>
        bool operator==(OtherType* otherRawPtr) const
        {
            return InternalPtr == otherRawPtr;
        }

        template <typename OtherType>
            requires NonNullPtr_SameType<Type, OtherType>
        friend bool operator==(OtherType* otherRawPtr, const NonNullPtr& nonNullPtr)
        {
            return otherRawPtr == nonNullPtr.InternalPtr;
        }

        // Forbid assigning a nullptr at compile time
        NonNullPtr(std::nullptr_t)
          : InternalPtr(nullptr)
        {
            static_assert(sizeof(Type) == 0, "Trying to initialize a NonNullPtr with a nullptr value");
        }

        NonNullPtr& operator=(std::nullptr_t)
        {
            static_assert(sizeof(Type) == 0, "Trying to assign a NonNullPtr with a nullptr value");
            return *this;
        }

        explicit operator bool() const { return true; }

      private:
        Type* InternalPtr;
    };

    template <typename T>
    NonNullPtr(T*) -> NonNullPtr<T>;
} // namespace Juliet
