#pragma once

#include <cstddef>
#include <cassert>
#include <limits>
#include <memory>
#include <utility>

#include <jni.h>

namespace jni
   {
    template < class W > struct Wrapper;

    template < class W, class U >
    auto Wrap(U&& u)
       {
        return Wrapper<W>().Wrap(std::forward<U>(u));
       }

    template < class W >
    auto Unwrap(W&& w)
       {
        return Wrapper<typename std::decay<W>::type>().Unwrap(std::forward<W>(w));
       }

    template < class T >
    using UnwrappedType = decltype(Unwrap<T>(std::declval<T>()));


    template < class T >
    struct PrimitiveTypeWrapper
       {
        T Wrap(T t) const { return t; }
        T Unwrap(T t) const { return t; }
       };

    template <> struct Wrapper< jboolean > : PrimitiveTypeWrapper< jboolean > {};
    template <> struct Wrapper< jbyte    > : PrimitiveTypeWrapper< jbyte    > {};
    template <> struct Wrapper< jchar    > : PrimitiveTypeWrapper< jchar    > {};
    template <> struct Wrapper< jshort   > : PrimitiveTypeWrapper< jshort   > {};
    template <> struct Wrapper< jint     > : PrimitiveTypeWrapper< jint     > {};
    template <> struct Wrapper< jlong    > : PrimitiveTypeWrapper< jlong    > {};
    template <> struct Wrapper< jfloat   > : PrimitiveTypeWrapper< jfloat   > {};
    template <> struct Wrapper< jdouble  > : PrimitiveTypeWrapper< jdouble  > {};


    template <>
    struct Wrapper<const char16_t*>
       {
        const char16_t* Wrap(const jchar* s) const { return reinterpret_cast<const char16_t*>(s); }
        const jchar* Unwrap(const char16_t* s) const { return reinterpret_cast<const jchar*>(s); }
       };

    template <>
    struct Wrapper<char16_t*>
       {
        char16_t* Wrap(jchar* s) const { return reinterpret_cast<char16_t*>(s); }
        jchar* Unwrap(char16_t* s) const { return reinterpret_cast<jchar*>(s); }
       };


    template <>
    struct Wrapper<jsize>
       {
        jsize Wrap(::jsize s) const
           {
            if (s < 0)
                throw std::range_error("::jsize < 0");
            return static_cast<jsize>(s);
           }

        ::jsize Unwrap(jsize s) const
           {
            if (s > jsize(std::numeric_limits<::jsize>::max()))
                throw std::range_error("jsize > max");
            return static_cast<::jsize>(s);
           }
       };


    template < class W, class U >
    struct ReferenceTypeWrapper
       {
        W* Wrap(U u) const { return reinterpret_cast<W*>(u); }
        U Unwrap(W* w) const { return reinterpret_cast<U>(w); }
       };

    template <> struct Wrapper< jobject*    > : ReferenceTypeWrapper< jobject,    ::jobject    > {};
    template <> struct Wrapper< jclass*     > : ReferenceTypeWrapper< jclass,     ::jclass     > {};
    template <> struct Wrapper< jstring*    > : ReferenceTypeWrapper< jstring,    ::jstring    > {};
    template <> struct Wrapper< jthrowable* > : ReferenceTypeWrapper< jthrowable, ::jthrowable > {};

    template <> struct Wrapper< jarray< jobject  >* > : ReferenceTypeWrapper< jarray< jobject  >, ::jobjectArray  > {};
    template <> struct Wrapper< jarray< jboolean >* > : ReferenceTypeWrapper< jarray< jboolean >, ::jbooleanArray > {};
    template <> struct Wrapper< jarray< jbyte    >* > : ReferenceTypeWrapper< jarray< jbyte    >, ::jbyteArray    > {};
    template <> struct Wrapper< jarray< jchar    >* > : ReferenceTypeWrapper< jarray< jchar    >, ::jcharArray    > {};
    template <> struct Wrapper< jarray< jshort   >* > : ReferenceTypeWrapper< jarray< jshort   >, ::jshortArray   > {};
    template <> struct Wrapper< jarray< jint     >* > : ReferenceTypeWrapper< jarray< jint     >, ::jintArray     > {};
    template <> struct Wrapper< jarray< jlong    >* > : ReferenceTypeWrapper< jarray< jlong    >, ::jlongArray    > {};
    template <> struct Wrapper< jarray< jfloat   >* > : ReferenceTypeWrapper< jarray< jfloat   >, ::jfloatArray   > {};
    template <> struct Wrapper< jarray< jdouble  >* > : ReferenceTypeWrapper< jarray< jdouble  >, ::jdoubleArray  > {};

    template <> struct Wrapper< jfieldID*  > : ReferenceTypeWrapper< jfieldID,  ::jfieldID  > {};
    template <> struct Wrapper< jmethodID* > : ReferenceTypeWrapper< jmethodID, ::jmethodID > {};


    // Allow references to be unwrapped to pointers to the underlying type, but not the
    // reverse, because dereferences should be explicit, with null checks where necessary.

    template < class W, class U >
    struct ReferenceTypeUnwrapper
       {
        U Unwrap(W& w) const { return reinterpret_cast<U>(&w); }
       };

    template <> struct Wrapper< jobject    > : ReferenceTypeUnwrapper< jobject,    ::jobject    > {};
    template <> struct Wrapper< jclass     > : ReferenceTypeUnwrapper< jclass,     ::jclass     > {};
    template <> struct Wrapper< jstring    > : ReferenceTypeUnwrapper< jstring,    ::jstring    > {};
    template <> struct Wrapper< jthrowable > : ReferenceTypeUnwrapper< jthrowable, ::jthrowable > {};

    template <> struct Wrapper< jarray< jobject  > > : ReferenceTypeUnwrapper< jarray< jobject  >, ::jobjectArray  > {};
    template <> struct Wrapper< jarray< jboolean > > : ReferenceTypeUnwrapper< jarray< jboolean >, ::jbooleanArray > {};
    template <> struct Wrapper< jarray< jbyte    > > : ReferenceTypeUnwrapper< jarray< jbyte    >, ::jbyteArray    > {};
    template <> struct Wrapper< jarray< jchar    > > : ReferenceTypeUnwrapper< jarray< jchar    >, ::jcharArray    > {};
    template <> struct Wrapper< jarray< jshort   > > : ReferenceTypeUnwrapper< jarray< jshort   >, ::jshortArray   > {};
    template <> struct Wrapper< jarray< jint     > > : ReferenceTypeUnwrapper< jarray< jint     >, ::jintArray     > {};
    template <> struct Wrapper< jarray< jlong    > > : ReferenceTypeUnwrapper< jarray< jlong    >, ::jlongArray    > {};
    template <> struct Wrapper< jarray< jfloat   > > : ReferenceTypeUnwrapper< jarray< jfloat   >, ::jfloatArray   > {};
    template <> struct Wrapper< jarray< jdouble  > > : ReferenceTypeUnwrapper< jarray< jdouble  >, ::jdoubleArray  > {};

    template <> struct Wrapper< jfieldID  > : ReferenceTypeUnwrapper< jfieldID,  ::jfieldID  > {};
    template <> struct Wrapper< jmethodID > : ReferenceTypeUnwrapper< jmethodID, ::jmethodID > {};


    template < class T, class R, class... Args >
    struct Wrapper< JNINativeMethod< R (JNIEnv*, T*, Args...) > >
       {
        ::JNINativeMethod Unwrap(JNINativeMethod<R (JNIEnv*, T*, Args...)> method) const
           {
            return { const_cast<char*>(method.name), const_cast<char*>(method.signature), reinterpret_cast<void*>(method.fnPtr) };
           }
       };


    template <>
    struct Wrapper<version>
       {
        version Wrap(::jint v) const { return static_cast<version>(v); }
        ::jint Unwrap(version v) const { return static_cast<::jint>(v); }
       };
   }
