#pragma once

#include <jni/functions.hpp>
#include <jni/tagging.hpp>

#include <cstddef>

namespace jni
   {
    template < class TheTag > class Class;
    template < class TheTag, class > class Field;
    template < class TheTag, class > class Method;

    class ObjectBase
       {
        protected:
            jobject* ptr = nullptr;

            explicit ObjectBase(std::nullptr_t = nullptr)
               {}

            explicit ObjectBase(jobject* p)
               : ptr(p)
               {}

            ~ObjectBase() = default;

            void reset(jobject* p) { ptr = p; }

        public:
            explicit operator bool() const { return ptr; }

            friend bool operator==(const ObjectBase& a, const ObjectBase& b) { return a.ptr == b.ptr; }
            friend bool operator!=(const ObjectBase& a, const ObjectBase& b) { return !( a == b ); }

            template < class OtherTag >
            bool IsInstanceOf(JNIEnv& env, const Class<OtherTag>& clazz) const
               {
                return jni::IsInstanceOf(env, ptr, *clazz);
               }
       };

    template < class TheTag = ObjectTag >
    class Object : public TagTraits<TheTag>::SuperType
       {
        public:
            using TagType = TheTag;
            using SuperType = typename TagTraits<TheTag>::SuperType;
            using UntaggedType = typename TagTraits<TheTag>::UntaggedType;

            explicit Object(UntaggedType* p)
               : SuperType(p)
               {}

        protected:
            explicit Object(std::nullptr_t = nullptr)
               {}

            Object(const Object&) = delete;
            Object& operator=(const Object&) = delete;

        public:
            UntaggedType* get() const { return reinterpret_cast<UntaggedType*>(this->ptr); }
            UntaggedType& operator*() const { return *get(); }

            template < class T >
            auto Get(JNIEnv& env, const Field<TagType, T>& field) const
               -> std::enable_if_t< IsPrimitive<T>::value, T >
               {
                return GetField<T>(env, get(), field);
               }

            template < class T >
            auto Get(JNIEnv& env, const Field<TagType, T>& field) const
               -> std::enable_if_t< !IsPrimitive<T>::value, Local<T> >
               {
                return Local<T>(env, reinterpret_cast<typename T::UntaggedType*>(GetField<jobject*>(env, get(), field)));
               }

            template < class T >
            auto Set(JNIEnv& env, const Field<TagType, T>& field, T value) const
               -> std::enable_if_t< IsPrimitive<T>::value >
               {
                SetField<T>(env, get(), field, value);
               }

            template < class Expected, class Actual >
            auto Set(JNIEnv& env, const Field<TagType, Expected>& field, const Actual& value) const
               -> std::enable_if_t< !IsPrimitive<Expected>::value
                                 && std::is_convertible<const Actual&, const Expected&>::value >
               {
                SetField<jobject*>(env, get(), field, value.get());
               }

            template < class R, class... ExpectedArgs, class... ActualArgs >
            auto Call(JNIEnv& env, const Method<TagType, R (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< IsPrimitive<R>::value
                                 && Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value, R >
               {
                return CallMethod<R>(env, get(), method, Untag(args)...);
               }

            template < class R, class... ExpectedArgs, class... ActualArgs >
            auto Call(JNIEnv& env, const Method<TagType, R (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< !IsPrimitive<R>::value
                                 && !std::is_void<R>::value
                                 && Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value, Local<R> >
               {
                return Local<R>(env, reinterpret_cast<typename R::UntaggedType*>(CallMethod<jobject*>(env, get(), method, Untag(args)...)));
               }

            template < class... ExpectedArgs, class... ActualArgs >
            auto Call(JNIEnv& env, const Method<TagType, void (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value >
               {
                CallMethod<void>(env, get(), method, Untag(args)...);
               }

            template < class R, class... ExpectedArgs, class... ActualArgs >
            auto CallNonvirtual(JNIEnv& env, const Class<TagType>& clazz, const Method<TagType, R (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< IsPrimitive<R>::value
                                 && Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value, R >
               {
                return CallNonvirtualMethod<R>(env, get(), clazz, method, Untag(args)...);
               }

            template < class R, class... ExpectedArgs, class... ActualArgs >
            auto CallNonvirtual(JNIEnv& env, const Class<TagType>& clazz, const Method<TagType, R (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< !IsPrimitive<R>::value
                                 && !std::is_void<R>::value
                                 && Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value, Local<R> >
               {
                return Local<R>(env, reinterpret_cast<typename R::UntaggedType*>(CallNonvirtualMethod<jobject*>(env, get(), clazz, method, Untag(args)...)));
               }

            template < class... ExpectedArgs, class... ActualArgs >
            auto CallNonvirtual(JNIEnv& env, const Class<TagType>& clazz, const Method<TagType, void (ExpectedArgs...)>& method, const ActualArgs&... args) const
               -> std::enable_if_t< Conjunction<std::is_convertible<const ActualArgs&, const ExpectedArgs&>...>::value >
               {
                CallNonvirtualMethod<void>(env, get(), clazz, method, Untag(args)...);
               }
       };

    template < class OutTagType, class T >
    Local<Object<OutTagType>> Cast(JNIEnv& env, const Class<OutTagType>& clazz, const T& object)
       {
        if (!object.IsInstanceOf(env, clazz))
           {
            ThrowNew(env, FindClass(env, "java/lang/ClassCastException"));
           }
        return Local<Object<OutTagType>>(env, reinterpret_cast<typename Object<OutTagType>::UntaggedType*>(NewLocal(env, object).release()));
       }
   }