/*
 * Copyright 2015 Brockmann Consult GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file was modified by Deephaven Data Labs.
 *
 */

#include "jpy_module.h"
#include "jpy_diag.h"
#include "jpy_verboseexcept.h"
#include "jpy_jtype.h"
#include "jpy_jmethod.h"
#include "jpy_jfield.h"
#include "jpy_jobj.h"
#include "jpy_conv.h"
#include "jpy_compat.h"


#include <stdlib.h>
#include <string.h>


PyObject* JPy_has_jvm(PyObject* self);
PyObject* JPy_create_jvm(PyObject* self, PyObject* args, PyObject* kwds);
PyObject* JPy_destroy_jvm(PyObject* self, PyObject* args);
PyObject* JPy_get_type(PyObject* self, PyObject* args, PyObject* kwds);
PyObject* JPy_cast(PyObject* self, PyObject* args);
PyObject* JPy_array(PyObject* self, PyObject* args);


static PyMethodDef JPy_Functions[] = {

    {"has_jvm",     (PyCFunction) JPy_has_jvm, METH_NOARGS,
                    "has_jvm() - Check if the JVM is available."},

    {"create_jvm",  (PyCFunction) JPy_create_jvm, METH_VARARGS|METH_KEYWORDS,
                    "create_jvm(options) - Create the Java VM from the given list of options."},

    {"destroy_jvm", JPy_destroy_jvm, METH_VARARGS,
                    "destroy_jvm() - Destroy the current Java VM."},

    {"get_type",    (PyCFunction) JPy_get_type, METH_VARARGS|METH_KEYWORDS,
                    "get_type(name, resolve=True) - Return the Java class with the given name, e.g. 'java.io.File'. "
                    "Loads the Java class from the JVM if not already done. Optionally avoids resolving the class' methods."},

    {"cast",        JPy_cast, METH_VARARGS,
                    "cast(obj, type) - Cast the given Java object to the given Java type (type name or type object). "
                    "Returns None if the cast is not possible."},

    {"array",       JPy_array, METH_VARARGS,
                    "array(name, init) - Return a new Java array of given Java type (type name or type object) and initializer (array length or sequence). "
                    "Possible primitive types are 'boolean', 'byte', 'char', 'short', 'int', 'long', 'float', and 'double'."},

    {NULL, NULL, 0, NULL} /*Sentinel*/
};

void JPy_free_ptr(void* unused);

#define JPY_MODULE_NAME "jpy"
#define JPY_MODULE_DOC  "Bi-directional Python-Java Bridge"

#if defined(JPY_COMPAT_33P)
static struct PyModuleDef JPy_ModuleDef =
{
    PyModuleDef_HEAD_INIT,
    JPY_MODULE_NAME,   /* Name of the Python JPy_Module */
    JPY_MODULE_DOC,    /* Module documentation */
    -1,                /* Size of per-interpreter state of the JPy_Module, or -1 if the JPy_Module keeps state in global variables. */
    JPy_Functions,     /* Structure containing global jpy-functions */
    NULL,         // m_reload
    NULL,         // m_traverse
    NULL,         // m_clear
    JPy_free_ptr  // m_free
};
#endif

PyObject* JPy_Module = NULL;
PyObject* JPy_Types = NULL;
PyObject* JPy_Type_Callbacks = NULL;
PyObject* JPy_Type_Translations = NULL;
PyObject* JException_Type = NULL;

// A global reference to a Java VM singleton.
JavaVM* JPy_JVM = NULL;

// If true, this JVM structure has been initialised from Python jpy.create_jvm()
jboolean JPy_MustDestroyJVM = JNI_FALSE;


// Global VM Information (maybe better place this in the JPy_JVM structure later)
// {{{

JPy_JType* JPy_JBoolean = NULL;
JPy_JType* JPy_JChar = NULL;
JPy_JType* JPy_JByte = NULL;
JPy_JType* JPy_JShort = NULL;
JPy_JType* JPy_JInt = NULL;
JPy_JType* JPy_JLong = NULL;
JPy_JType* JPy_JFloat = NULL;
JPy_JType* JPy_JDouble = NULL;
JPy_JType* JPy_JVoid = NULL;
JPy_JType* JPy_JBooleanObj = NULL;
JPy_JType* JPy_JCharacterObj = NULL;
JPy_JType* JPy_JByteObj = NULL;
JPy_JType* JPy_JShortObj = NULL;
JPy_JType* JPy_JIntegerObj = NULL;
JPy_JType* JPy_JLongObj = NULL;
JPy_JType* JPy_JFloatObj = NULL;
JPy_JType* JPy_JDoubleObj = NULL;
JPy_JType* JPy_JObject = NULL;
JPy_JType* JPy_JClass = NULL;
JPy_JType* JPy_JString = NULL;
JPy_JType* JPy_JPyObject = NULL;
JPy_JType* JPy_JPyModule = NULL;
JPy_JType* JPy_JThrowable = NULL;
JPy_JType* JPy_JStackTraceElement = NULL;


// java.lang.Comparable
jclass JPy_Comparable_JClass = NULL;
jmethodID JPy_Comparable_CompareTo_MID = NULL;

// java.lang.Object
jclass JPy_Object_JClass = NULL;
jmethodID JPy_Object_ToString_MID = NULL;
jmethodID JPy_Object_HashCode_MID = NULL;
jmethodID JPy_Object_Equals_MID = NULL;

// java.lang.Class
jclass JPy_Class_JClass = NULL;
jmethodID JPy_Class_GetName_MID = NULL;
jmethodID JPy_Class_GetDeclaredConstructors_MID = NULL;
jmethodID JPy_Class_GetDeclaredFields_MID = NULL;
jmethodID JPy_Class_GetDeclaredMethods_MID = NULL;
jmethodID JPy_Class_GetFields_MID = NULL;
jmethodID JPy_Class_GetMethods_MID = NULL;
jmethodID JPy_Class_GetComponentType_MID = NULL;
jmethodID JPy_Class_IsPrimitive_MID = NULL;
jmethodID JPy_Class_IsInterface_MID = NULL;

// java.lang.reflect.Constructor
jclass JPy_Constructor_JClass = NULL;
jmethodID JPy_Constructor_GetModifiers_MID = NULL;
jmethodID JPy_Constructor_GetParameterTypes_MID = NULL;

// java.lang.reflect.Method
jclass JPy_Method_JClass = NULL;
jmethodID JPy_Method_GetName_MID = NULL;
jmethodID JPy_Method_GetReturnType_MID = NULL;
jmethodID JPy_Method_GetParameterTypes_MID = NULL;
jmethodID JPy_Method_GetModifiers_MID = NULL;

// java.lang.reflect.Field
jclass JPy_Field_JClass = NULL;
jmethodID JPy_Field_GetName_MID = NULL;
jmethodID JPy_Field_GetModifiers_MID = NULL;
jmethodID JPy_Field_GetType_MID = NULL;

// java.util.Map
jclass JPy_Map_JClass = NULL;
jclass JPy_Map_Entry_JClass = NULL;
jmethodID JPy_Map_entrySet_MID = NULL;
jmethodID JPy_Map_put_MID = NULL;
jmethodID JPy_Map_clear_MID = NULL;
jmethodID JPy_Map_Entry_getKey_MID = NULL;
jmethodID JPy_Map_Entry_getValue_MID = NULL;
// java.util.Set
jclass JPy_Set_JClass = NULL;
jmethodID JPy_Set_Iterator_MID = NULL;
// java.util.Iterator
jclass JPy_Iterator_JClass = NULL;
jmethodID JPy_Iterator_next_MID = NULL;
jmethodID JPy_Iterator_hasNext_MID = NULL;

jclass JPy_RuntimeException_JClass = NULL;
jclass JPy_OutOfMemoryError_JClass = NULL;
jclass JPy_UnsupportedOperationException_JClass = NULL;
jclass JPy_FileNotFoundException_JClass = NULL;
jclass JPy_KeyError_JClass = NULL;
jclass JPy_StopIteration_JClass = NULL;

// java.lang.Boolean
jclass JPy_Boolean_JClass = NULL;
jmethodID JPy_Boolean_ValueOf_SMID = NULL;
jmethodID JPy_Boolean_BooleanValue_MID = NULL;

jclass JPy_Character_JClass = NULL;
jmethodID JPy_Character_ValueOf_SMID;
jmethodID JPy_Character_CharValue_MID = NULL;

jclass JPy_Byte_JClass = NULL;
jmethodID JPy_Byte_ValueOf_SMID = NULL;

jclass JPy_Short_JClass = NULL;
jmethodID JPy_Short_ValueOf_SMID = NULL;

jclass JPy_Integer_JClass = NULL;
jmethodID JPy_Integer_ValueOf_SMID = NULL;

jclass JPy_Long_JClass = NULL;
jmethodID JPy_Long_ValueOf_SMID = NULL;

jclass JPy_Float_JClass = NULL;
jmethodID JPy_Float_ValueOf_SMID = NULL;

jclass JPy_Double_JClass = NULL;
jmethodID JPy_Double_ValueOf_SMID = NULL;

// java.lang.Number
jclass JPy_Number_JClass = NULL;
jmethodID JPy_Number_IntValue_MID = NULL;
jmethodID JPy_Number_LongValue_MID = NULL;
jmethodID JPy_Number_DoubleValue_MID = NULL;

jclass JPy_Void_JClass = NULL;
jclass JPy_String_JClass = NULL;
jclass JPy_PyObject_JClass = NULL;
jclass JPy_PyDictWrapper_JClass = NULL;

jmethodID JPy_PyObject_GetPointer_MID = NULL;
jmethodID JPy_PyObject_UnwrapProxy_SMID = NULL;
jmethodID JPy_PyObject_Init_MID = NULL;
jmethodID JPy_PyModule_Init_MID = NULL;

jmethodID JPy_PyDictWrapper_GetPointer_MID = NULL;

// java.lang.Throwable
jclass JPy_Throwable_JClass = NULL;
jmethodID JPy_Throwable_getStackTrace_MID = NULL;
jmethodID JPy_Throwable_getCause_MID = NULL;

// stack trace element
jclass JPy_StackTraceElement_JClass = NULL;

// java.util.function.Supplier
jclass JPy_Supplier_JClass = NULL;
jmethodID JPy_Supplier_get_MID = NULL;

// }}}


JNIEnv* JPy_GetJNIEnv(void)
{
    JavaVM* jvm;
    JNIEnv* jenv;
    jint status;

    jvm = JPy_JVM;
    if (jvm == NULL) {
        PyErr_SetString(PyExc_RuntimeError, "jpy: No JVM available.");
        return NULL;
    }

    status = (*jvm)->GetEnv(jvm, (void**) &jenv, JPY_JNI_VERSION);
    if (status == JNI_EDETACHED) {
        if ((*jvm)->AttachCurrentThread(jvm, (void**) &jenv, NULL) == 0) {
            JPy_DIAG_PRINT(JPy_DIAG_F_JVM, "JPy_GetJNIEnv: Attached current thread to JVM: jenv=%p\n", jenv);
        } else {
            PyErr_SetString(PyExc_RuntimeError, "jpy: Failed to attach current thread to JVM.");
            return NULL;
        }
    } else if (status == JNI_EVERSION) {
        PyErr_SetString(PyExc_RuntimeError, "jpy: Failed to attach current thread to JVM: Java version not supported.");
        return NULL;
    } else if (status == JNI_OK) {
        // ok!
        JPy_DIAG_PRINT(JPy_DIAG_F_JVM, "JPy_GetJNIEnv: jenv=%p\n", jenv);
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_JVM + JPy_DIAG_F_ERR, "JPy_GetJNIEnv: Received unhandled status code from JVM GetEnv(): status=%d\n", status);
    }

    return jenv;
}

#if defined(JPY_COMPAT_33P)
#define JPY_RETURN(V) return V
#define JPY_MODULE_INIT_FUNC PyInit_jpy
#elif defined(JPY_COMPAT_27)
#define JPY_RETURN(V) return
#define JPY_MODULE_INIT_FUNC initjpy
#else
#error JPY_VERSION_ERROR
#endif

/**
 * Called by the Python interpreter's import machinery, e.g. using 'import jpy'.
 */
PyMODINIT_FUNC JPY_MODULE_INIT_FUNC(void)
{
    //printf("PyInit_jpy: JPy_JVM=%p\n", JPy_JVM);

    /////////////////////////////////////////////////////////////////////////

#if defined(JPY_COMPAT_33P)
    JPy_Module = PyModule_Create(&JPy_ModuleDef);
    if (JPy_Module == NULL) {
        JPY_RETURN(NULL);
    }
#elif defined(JPY_COMPAT_27)
    JPy_Module = Py_InitModule3(JPY_MODULE_NAME, JPy_Functions, JPY_MODULE_DOC);
    if (JPy_Module == NULL) {
        JPY_RETURN(NULL);
    }
#else
    #error JPY_VERSION_ERROR
#endif

    /////////////////////////////////////////////////////////////////////////

    if (PyType_Ready(&JType_Type) < 0) {
        JPY_RETURN(NULL);
    }
    JPy_INCREF(&JType_Type);
    PyModule_AddObject(JPy_Module, "JType", (PyObject*) &JType_Type);

    /////////////////////////////////////////////////////////////////////////

    if (PyType_Ready(&JMethod_Type) < 0) {
        JPY_RETURN(NULL);
    }
    JPy_INCREF(&JMethod_Type);
    PyModule_AddObject(JPy_Module, "JMethod", (PyObject*) &JMethod_Type);

    /////////////////////////////////////////////////////////////////////////

    if (PyType_Ready(&JOverloadedMethod_Type) < 0) {
        JPY_RETURN(NULL);
    }
    JPy_INCREF(&JOverloadedMethod_Type);
    PyModule_AddObject(JPy_Module, "JOverloadedMethod", (PyObject*) &JOverloadedMethod_Type);

    /////////////////////////////////////////////////////////////////////////

    if (PyType_Ready(&JField_Type) < 0) {
        JPY_RETURN(NULL);
    }
    JPy_INCREF(&JField_Type);
    PyModule_AddObject(JPy_Module, "JField", (PyObject*) &JField_Type);

    /////////////////////////////////////////////////////////////////////////

    JException_Type = PyErr_NewException("jpy.JException", NULL, NULL);
    JPy_INCREF(JException_Type);
    PyModule_AddObject(JPy_Module, "JException", JException_Type);

    /////////////////////////////////////////////////////////////////////////

    JPy_Types = PyDict_New();
    JPy_INCREF(JPy_Types);
    PyModule_AddObject(JPy_Module, JPy_MODULE_ATTR_NAME_TYPES, JPy_Types);

    /////////////////////////////////////////////////////////////////////////

    JPy_Type_Callbacks = PyDict_New();
    JPy_INCREF(JPy_Type_Callbacks);
    PyModule_AddObject(JPy_Module, JPy_MODULE_ATTR_NAME_TYPE_CALLBACKS, JPy_Type_Callbacks);

    /////////////////////////////////////////////////////////////////////////

    JPy_Type_Translations = PyDict_New();
    JPy_INCREF(JPy_Type_Translations);
    PyModule_AddObject(JPy_Module, JPy_MODULE_ATTR_NAME_TYPE_TRANSLATIONS, JPy_Type_Translations);

    /////////////////////////////////////////////////////////////////////////

    if (PyType_Ready(&Diag_Type) < 0) {
        JPY_RETURN(NULL);
    }
    //JPy_INCREF(&DiagFlags_Type);
    {
        PyObject* pyDiag = Diag_New();
        JPy_INCREF(pyDiag);
        PyModule_AddObject(JPy_Module, "diag", pyDiag);
    }

    if (PyType_Ready(&VerboseExceptions_Type) < 0) {
        JPY_RETURN(NULL);
    }
    {
        PyObject* pyVerboseExceptions = VerboseExceptions_New();
        JPy_INCREF(pyVerboseExceptions);
        PyModule_AddObject(JPy_Module, "VerboseExceptions", pyVerboseExceptions);
    }

    /////////////////////////////////////////////////////////////////////////

    if (JPy_JVM != NULL) {
        JNIEnv* jenv;
        jenv = JPy_GetJNIEnv();
        if (jenv == NULL) {
            JPY_RETURN(NULL);
        }
        // If we have already a running VM, initialize global variables
        if (JPy_InitGlobalVars(jenv) < 0) {
            JPY_RETURN(NULL);
        }
    }

    /////////////////////////////////////////////////////////////////////////

    //printf("PyInit_jpy: exit\n");

    JPY_RETURN(JPy_Module);
}

PyObject* JPy_has_jvm(PyObject* self)
{
    return PyBool_FromLong(JPy_JVM != NULL);
}

PyObject* JPy_create_jvm(PyObject* self, PyObject* args, PyObject* kwds)
{
    static char* keywords[] = {"options", NULL};
    PyObject*   options;
    Py_ssize_t  optionCount;
    PyObject*   option;
    JavaVMOption* jvmOptions;
    JavaVMInitArgs jvmInitArgs;
    jint        jvmErrorCode;
    JNIEnv*     jenv;
    Py_ssize_t  i;

    //printf("JPy_create_jvm: JPy_JVM=%p\n", JPy_JVM);

    options = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:create_jvm", keywords, &options)) {
        return NULL;
    }

    if (JPy_JVM != NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_JVM + JPy_DIAG_F_ERR, "JPy_create_jvm: WARNING: Java VM is already running.\n");
        JPy_DECREF(options);
        return Py_BuildValue("");
    }

    if (!PySequence_Check(options)) {
        PyErr_SetString(PyExc_ValueError, "create_jvm: argument 1 (options) must be a sequence of Java VM option strings");
        return NULL;
    }

    optionCount = PySequence_Length(options);
    if (optionCount == -1) {
        PyErr_SetString(PyExc_ValueError, "create_jvm: failed to determine sequence length of argument 1 (options)");
        return NULL;
    }

    jvmOptions = PyMem_New(JavaVMOption, optionCount);
    if (jvmOptions == NULL) {
        return PyErr_NoMemory();
    }

    for (i = 0; i < optionCount; i++) {
        option = PySequence_GetItem(options, i);
        if (option == NULL) {
            PyMem_Del(jvmOptions);
            return NULL;
        }
        jvmOptions[i].optionString = (char*) JPy_AS_UTF8(option);
        jvmOptions[i].extraInfo = NULL;
        JPy_DIAG_PRINT(JPy_DIAG_F_JVM, "JPy_create_jvm: jvmOptions[%d].optionString = '%s'\n", i, jvmOptions[i].optionString);
        if (jvmOptions[i].optionString == NULL) {
            PyMem_Del(jvmOptions);
            return NULL;
        }
        JPy_DECREF(option);
    }

    jvmInitArgs.version = JPY_JNI_VERSION;
    jvmInitArgs.options = jvmOptions;
    jvmInitArgs.nOptions = (size_t) optionCount;
    jvmInitArgs.ignoreUnrecognized = 0;
    jvmErrorCode = JNI_CreateJavaVM(&JPy_JVM, (void**) &jenv, &jvmInitArgs);
    JPy_MustDestroyJVM = JNI_TRUE;

    JPy_DIAG_PRINT(JPy_DIAG_F_JVM, "JPy_create_jvm: res=%d, JPy_JVM=%p, jenv=%p, JPy_MustDestroyJVM=%d\n", jvmErrorCode, JPy_JVM, jenv, JPy_MustDestroyJVM);

    PyMem_Del(jvmOptions);

    if (jvmErrorCode != JNI_OK) {
        JPy_DIAG_PRINT(JPy_DIAG_F_JVM + JPy_DIAG_F_ERR,
                       "JPy_create_jvm: INTERNAL ERROR: Failed to create Java VM (JNI error code %d). Possible reasons are:\n"
                       "* The Java heap space setting is too high (option -Xmx). Try '256M' first, then increment.\n"
                       "* The JVM shared library (Unix: libjvm.so, Windows: jvm.dll) cannot be found or cannot be loaded.\n"
                       "  Make sure the shared library can be found via the 'PATH' environment variable.\n"
                       "  Also make sure that the JVM is compiled for the same architecture as Python.\n",
                       jvmErrorCode);
        PyErr_SetString(PyExc_RuntimeError, "jpy: failed to create Java VM");
        return NULL;
    }

    if (JPy_InitGlobalVars(jenv) < 0) {
        return NULL;
    }

    return Py_BuildValue("");
}

PyObject* JPy_destroy_jvm(PyObject* self, PyObject* args)
{
    JPy_DIAG_PRINT(JPy_DIAG_F_JVM, "JPy_destroy_jvm: JPy_JVM=%p, JPy_MustDestroyJVM=%d\n", JPy_JVM, JPy_MustDestroyJVM);

    if (JPy_JVM != NULL && JPy_MustDestroyJVM) {
        JPy_ClearGlobalVars(JPy_GetJNIEnv());
        (*JPy_JVM)->DestroyJavaVM(JPy_JVM);
        JPy_JVM = NULL;
    }

    return Py_BuildValue("");
}

PyObject* JPy_get_type_internal(JNIEnv* jenv, PyObject* self, PyObject* args, PyObject* kwds)
{
    static char* keywords[] = {"name", "resolve", NULL};
    const char* className;
    int resolve;

    resolve = 1; // True
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i:get_type", keywords, &className, &resolve)) {
        return NULL;
    }

    return (PyObject*) JType_GetTypeForName(jenv, className, (jboolean) (resolve != 0 ? JNI_TRUE : JNI_FALSE));
}

PyObject* JPy_get_type(PyObject* self, PyObject* args, PyObject* kwds)
{
    JPy_FRAME(PyObject*, NULL, JPy_get_type_internal(jenv, self, args, kwds), 16)
}

PyObject* JPy_cast_internal(JNIEnv* jenv, PyObject* self, PyObject* args)
{
    PyObject* obj;
    PyObject* objType;
    JPy_JType* type;
    jboolean inst;

    if (!PyArg_ParseTuple(args, "OO:cast", &obj, &objType)) {
        return NULL;
    }

    if (obj == Py_None) {
        return Py_BuildValue("");
    }

    if (!JObj_Check(obj)) {
        PyErr_SetString(PyExc_ValueError, "cast: argument 1 (obj) must be a Java object");
        return NULL;
    }

    if (JPy_IS_STR(objType)) {
        const char* typeName = JPy_AS_UTF8(objType);
        type = JType_GetTypeForName(jenv, typeName, JNI_FALSE);
        if (type == NULL) {
            return NULL;
        }
    } else if (JType_Check(objType)) {
        type = (JPy_JType*) objType;
    } else {
        PyErr_SetString(PyExc_ValueError, "cast: argument 2 (obj_type) must be a Java type name or Java type object");
        return NULL;
    }

    inst = (*jenv)->IsInstanceOf(jenv, ((JPy_JObj*) obj)->objectRef, type->classRef);
    if (inst) {
        return (PyObject*) JObj_FromType(jenv, (JPy_JType*) objType, ((JPy_JObj*) obj)->objectRef);
    } else {
        return Py_BuildValue("");
    }
}

PyObject* JPy_cast(PyObject* self, PyObject* args)
{
    JPy_FRAME(PyObject*, NULL, JPy_cast_internal(jenv, self, args), 16)
}

PyObject* JPy_array_internal(JNIEnv* jenv, PyObject* self, PyObject* args)
{
    JPy_JType* componentType;
    jarray arrayRef;
    PyObject* objType;
    PyObject* objInit;

    if (!PyArg_ParseTuple(args, "OO:array", &objType, &objInit)) {
        return NULL;
    }

    if (JPy_IS_STR(objType)) {
        const char* typeName;
        typeName = JPy_AS_UTF8(objType);
        componentType = JType_GetTypeForName(jenv, typeName, JNI_FALSE);
        if (componentType == NULL) {
            return NULL;
        }
    } else if (JType_Check(objType)) {
        componentType = (JPy_JType*) objType;
    } else {
        PyErr_SetString(PyExc_ValueError, "array: argument 1 (type) must be a type name or Java type object");
        return NULL;
    }

    if (componentType == JPy_JVoid) {
        PyErr_SetString(PyExc_ValueError, "array: argument 1 (type) must not be 'void'");
        return NULL;
    }

    if (JPy_IS_CLONG(objInit)) {
        jint length = JPy_AS_CLONG(objInit);
        if (length < 0) {
            PyErr_SetString(PyExc_ValueError, "array: argument 2 (init) must be either an integer array length or any sequence");
            return NULL;
        }
        if (componentType == JPy_JBoolean) {
            arrayRef = (*jenv)->NewBooleanArray(jenv, length);
        } else if (componentType == JPy_JChar) {
            arrayRef = (*jenv)->NewCharArray(jenv, length);
        } else if (componentType == JPy_JByte) {
            arrayRef = (*jenv)->NewByteArray(jenv, length);
        } else if (componentType == JPy_JShort) {
            arrayRef = (*jenv)->NewShortArray(jenv, length);
        } else if (componentType == JPy_JInt) {
            arrayRef = (*jenv)->NewIntArray(jenv, length);
        } else if (componentType == JPy_JLong) {
            arrayRef = (*jenv)->NewLongArray(jenv, length);
        } else if (componentType == JPy_JFloat) {
            arrayRef = (*jenv)->NewFloatArray(jenv, length);
        } else if (componentType == JPy_JDouble) {
            arrayRef = (*jenv)->NewDoubleArray(jenv, length);
        } else {
            arrayRef = (*jenv)->NewObjectArray(jenv, length, componentType->classRef, NULL);
        }
        if (arrayRef == NULL) {
            return PyErr_NoMemory();
        }
        return JObj_New(jenv, arrayRef);
    } else if (PySequence_Check(objInit)) {
        if (JType_CreateJavaArray(jenv, componentType, objInit, &arrayRef, JNI_FALSE) < 0) {
            return NULL;
        }
        return JObj_New(jenv, arrayRef);
    } else {
        PyErr_SetString(PyExc_ValueError, "array: argument 2 (init) must be either an integer array length or any sequence");
        return NULL;
    }
}

PyObject* JPy_array(PyObject* self, PyObject* args)
{
    JPy_FRAME(PyObject*, NULL, JPy_array_internal(jenv, self, args), 16)
}

JPy_JType* JPy_GetNonObjectJType(JNIEnv* jenv, jclass classRef)
{
    jclass primClassRef;
    jfieldID fid;
    JPy_JType* type;

    if (classRef == NULL) {
        PyErr_SetString(PyExc_RuntimeError, "jpy: internal error: classRef == NULL");
    }

    fid = (*jenv)->GetStaticFieldID(jenv, classRef, "TYPE", "Ljava/lang/Class;");
    if (fid == NULL) {
        PyErr_SetString(PyExc_RuntimeError, "field 'TYPE' not found");
        return NULL;
    }

    primClassRef = (*jenv)->GetStaticObjectField(jenv, classRef, fid);
    if (primClassRef == NULL) {
        PyErr_SetString(PyExc_RuntimeError, "failed to access field 'TYPE'");
        return NULL;
    }

    type = JType_GetType(jenv, primClassRef, JNI_FALSE);
    JPy_DELETE_LOCAL_REF(primClassRef);
    if (type == NULL) {
        return NULL;
    }

    type->isResolved = JNI_TRUE; // Primitive types are always resolved.
    JPy_INCREF((PyObject*) type);

    return type;
}

jclass JPy_GetClass(JNIEnv* jenv, const char* name)
{
    jclass localClassRef;
    jclass globalClassRef;

    localClassRef = (*jenv)->FindClass(jenv, name);
    if (localClassRef == NULL) {
        PyErr_Format(PyExc_RuntimeError, "jpy: internal error: Java class '%s' not found", name);
        return NULL;
    }

    globalClassRef = (*jenv)->NewGlobalRef(jenv, localClassRef);
    JPy_DELETE_LOCAL_REF(localClassRef);
    if (globalClassRef == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    return globalClassRef;
}

jmethodID JPy_GetMethod(JNIEnv* jenv, jclass classRef, const char* name, const char* sig)
{
    jmethodID methodID;
    methodID = (*jenv)->GetMethodID(jenv, classRef, name, sig);
    if (methodID == NULL) {
        PyErr_Format(PyExc_RuntimeError, "jpy: internal error: method not found: %s%s", name, sig);
        return NULL;
    }
    return methodID;
}

jmethodID JPy_GetStaticMethod(JNIEnv *jenv, jclass classRef, const char *name, const char *sig)
{
    jmethodID methodID;
    methodID = (*jenv)->GetStaticMethodID(jenv, classRef, name, sig);
    if (methodID == NULL) {
        PyErr_Format(PyExc_RuntimeError, "jpy: internal error: static method not found: %s%s", name, sig);
        return NULL;
    }
    return methodID;
}

#define DEFINE_CLASS(C, N) \
    C = JPy_GetClass(jenv, N); \
    if (C == NULL) { \
        return -1; \
    }


#define DEFINE_METHOD(M, C, N, S) \
    M = JPy_GetMethod(jenv, C, N, S); \
    if (M == NULL) { \
        return -1; \
    }


#define DEFINE_STATIC_METHOD(M, C, N, S) \
    M = JPy_GetStaticMethod(jenv, C, N, S); \
    if (M == NULL) { \
        return -1; \
    }


#define DEFINE_NON_OBJECT_TYPE(T, C) \
    T = JPy_GetNonObjectJType(jenv, C); \
    if (T == NULL) { \
        return -1; \
    }


#define DEFINE_OBJECT_TYPE(T, C) \
    T = JType_GetType(jenv, C, JNI_FALSE); \
    if (T == NULL) { \
        return -1; \
    }


int initGlobalPyObjectVars(JNIEnv* jenv)
{
    JPy_JType *dictType;
    JPy_JType *keyErrorType;
    JPy_JType *stopIterationType;

    JPy_JPyObject = JType_GetTypeForName(jenv, "org.jpy.PyObject", JNI_FALSE);
    if (JPy_JPyObject == NULL) {
        // org.jpy.PyObject may not be on the classpath, which is ok
        PyErr_Clear();
        return -1;
    } else {
        JPy_PyObject_JClass = JPy_JPyObject->classRef;
        DEFINE_METHOD(JPy_PyObject_GetPointer_MID, JPy_PyObject_JClass, "getPointer", "()J");
        DEFINE_STATIC_METHOD(JPy_PyObject_UnwrapProxy_SMID, JPy_PyObject_JClass, "unwrapProxy", "(Ljava/lang/Object;)Lorg/jpy/PyObject;");
        DEFINE_METHOD(JPy_PyObject_Init_MID, JPy_PyObject_JClass, "<init>", "(JZ)V");
    }

    JPy_JPyModule = JType_GetTypeForName(jenv, "org.jpy.PyModule", JNI_FALSE);
    if (JPy_JPyModule == NULL) {
        // org.jpy.PyModule may not be on the classpath, which is ok
        PyErr_Clear();
        return -1;
    }

    dictType = JType_GetTypeForName(jenv, "org.jpy.PyDictWrapper", JNI_FALSE);
    if (dictType == NULL) {
        PyErr_Clear();
        return -1;
    } else {
        JPy_PyDictWrapper_JClass = dictType->classRef;
        DEFINE_METHOD(JPy_PyDictWrapper_GetPointer_MID, JPy_PyDictWrapper_JClass, "getPointer", "()J");
    }

    keyErrorType = JType_GetTypeForName(jenv, "org.jpy.KeyError", JNI_FALSE);
    if (keyErrorType == NULL) {
        PyErr_Clear();
        return -1;
    } else {
        JPy_KeyError_JClass = keyErrorType->classRef;
    }

    stopIterationType = JType_GetTypeForName(jenv, "org.jpy.StopIteration", JNI_FALSE);
    if (stopIterationType == NULL) {
        PyErr_Clear();
        return -1;
    } else {
        JPy_StopIteration_JClass = stopIterationType->classRef;
    }

    return 0;
}


int JPy_InitGlobalVars(JNIEnv* jenv)
{
    if (JPy_Comparable_JClass != NULL) {
        return 0;
    }

    DEFINE_CLASS(JPy_Comparable_JClass, "java/lang/Comparable");
    DEFINE_METHOD(JPy_Comparable_CompareTo_MID, JPy_Comparable_JClass, "compareTo", "(Ljava/lang/Object;)I");

    DEFINE_CLASS(JPy_Object_JClass, "java/lang/Object");
    DEFINE_METHOD(JPy_Object_ToString_MID, JPy_Object_JClass, "toString", "()Ljava/lang/String;");
    DEFINE_METHOD(JPy_Object_HashCode_MID, JPy_Object_JClass, "hashCode", "()I");
    DEFINE_METHOD(JPy_Object_Equals_MID, JPy_Object_JClass, "equals", "(Ljava/lang/Object;)Z");

    DEFINE_CLASS(JPy_Class_JClass, "java/lang/Class");
    DEFINE_METHOD(JPy_Class_GetName_MID, JPy_Class_JClass, "getName", "()Ljava/lang/String;");
    DEFINE_METHOD(JPy_Class_GetDeclaredConstructors_MID, JPy_Class_JClass, "getDeclaredConstructors", "()[Ljava/lang/reflect/Constructor;");
    DEFINE_METHOD(JPy_Class_GetDeclaredMethods_MID, JPy_Class_JClass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
    DEFINE_METHOD(JPy_Class_GetDeclaredFields_MID, JPy_Class_JClass, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
    DEFINE_METHOD(JPy_Class_GetMethods_MID, JPy_Class_JClass, "getMethods", "()[Ljava/lang/reflect/Method;");
    DEFINE_METHOD(JPy_Class_GetFields_MID, JPy_Class_JClass, "getFields", "()[Ljava/lang/reflect/Field;");
    DEFINE_METHOD(JPy_Class_GetComponentType_MID, JPy_Class_JClass, "getComponentType", "()Ljava/lang/Class;");
    DEFINE_METHOD(JPy_Class_IsPrimitive_MID, JPy_Class_JClass, "isPrimitive", "()Z");
    DEFINE_METHOD(JPy_Class_IsInterface_MID, JPy_Class_JClass, "isInterface", "()Z");

    DEFINE_CLASS(JPy_Constructor_JClass, "java/lang/reflect/Constructor");
    DEFINE_METHOD(JPy_Constructor_GetModifiers_MID, JPy_Constructor_JClass, "getModifiers", "()I");
    DEFINE_METHOD(JPy_Constructor_GetParameterTypes_MID, JPy_Constructor_JClass, "getParameterTypes", "()[Ljava/lang/Class;");

    DEFINE_CLASS(JPy_Field_JClass, "java/lang/reflect/Field");
    DEFINE_METHOD(JPy_Field_GetName_MID, JPy_Field_JClass, "getName", "()Ljava/lang/String;");
    DEFINE_METHOD(JPy_Field_GetModifiers_MID, JPy_Field_JClass, "getModifiers", "()I");
    DEFINE_METHOD(JPy_Field_GetType_MID, JPy_Field_JClass, "getType", "()Ljava/lang/Class;");

    DEFINE_CLASS(JPy_Method_JClass, "java/lang/reflect/Method");
    DEFINE_METHOD(JPy_Method_GetName_MID, JPy_Method_JClass, "getName", "()Ljava/lang/String;");
    DEFINE_METHOD(JPy_Method_GetModifiers_MID, JPy_Method_JClass, "getModifiers", "()I");
    DEFINE_METHOD(JPy_Method_GetParameterTypes_MID, JPy_Method_JClass, "getParameterTypes", "()[Ljava/lang/Class;");
    DEFINE_METHOD(JPy_Method_GetReturnType_MID, JPy_Method_JClass, "getReturnType", "()Ljava/lang/Class;");

    DEFINE_CLASS(JPy_Map_JClass, "java/util/Map");
    DEFINE_METHOD(JPy_Map_entrySet_MID, JPy_Map_JClass, "entrySet", "()Ljava/util/Set;");
    DEFINE_METHOD(JPy_Map_put_MID, JPy_Map_JClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    DEFINE_METHOD(JPy_Map_clear_MID, JPy_Map_JClass, "clear", "()V");

    DEFINE_CLASS(JPy_Map_Entry_JClass, "java/util/Map$Entry");
    DEFINE_METHOD(JPy_Map_Entry_getKey_MID, JPy_Map_Entry_JClass, "getKey", "()Ljava/lang/Object;");
    DEFINE_METHOD(JPy_Map_Entry_getValue_MID, JPy_Map_Entry_JClass, "getValue", "()Ljava/lang/Object;");


    // java.util.Set
    DEFINE_CLASS(JPy_Set_JClass, "java/util/Set");
    DEFINE_METHOD(JPy_Set_Iterator_MID, JPy_Set_JClass, "iterator", "()Ljava/util/Iterator;");
    // java.util.Iterator
    DEFINE_CLASS(JPy_Iterator_JClass, "java/util/Iterator");
    DEFINE_METHOD(JPy_Iterator_next_MID, JPy_Iterator_JClass, "next", "()Ljava/lang/Object;");
    DEFINE_METHOD(JPy_Iterator_hasNext_MID, JPy_Iterator_JClass, "hasNext", "()Z");

    DEFINE_CLASS(JPy_RuntimeException_JClass, "java/lang/RuntimeException");
    DEFINE_CLASS(JPy_OutOfMemoryError_JClass, "java/lang/OutOfMemoryError");
    DEFINE_CLASS(JPy_FileNotFoundException_JClass, "java/io/FileNotFoundException");
    DEFINE_CLASS(JPy_UnsupportedOperationException_JClass, "java/lang/UnsupportedOperationException");

    DEFINE_CLASS(JPy_Boolean_JClass, "java/lang/Boolean");
    DEFINE_STATIC_METHOD(JPy_Boolean_ValueOf_SMID, JPy_Boolean_JClass, "valueOf", "(Z)Ljava/lang/Boolean;");
    DEFINE_METHOD(JPy_Boolean_BooleanValue_MID, JPy_Boolean_JClass, "booleanValue", "()Z");

    DEFINE_CLASS(JPy_Character_JClass, "java/lang/Character");
    DEFINE_STATIC_METHOD(JPy_Character_ValueOf_SMID, JPy_Character_JClass, "valueOf", "(C)Ljava/lang/Character;");
    DEFINE_METHOD(JPy_Character_CharValue_MID, JPy_Character_JClass, "charValue", "()C");

    DEFINE_CLASS(JPy_Number_JClass, "java/lang/Number");

    DEFINE_CLASS(JPy_Byte_JClass, "java/lang/Byte");
    DEFINE_STATIC_METHOD(JPy_Byte_ValueOf_SMID, JPy_Byte_JClass, "valueOf", "(B)Ljava/lang/Byte;");

    DEFINE_CLASS(JPy_Short_JClass, "java/lang/Short");
    DEFINE_STATIC_METHOD(JPy_Short_ValueOf_SMID, JPy_Short_JClass, "valueOf", "(S)Ljava/lang/Short;");

    DEFINE_CLASS(JPy_Integer_JClass, "java/lang/Integer");
    DEFINE_STATIC_METHOD(JPy_Integer_ValueOf_SMID, JPy_Integer_JClass, "valueOf", "(I)Ljava/lang/Integer;");

    DEFINE_CLASS(JPy_Long_JClass, "java/lang/Long");
    DEFINE_STATIC_METHOD(JPy_Long_ValueOf_SMID, JPy_Long_JClass, "valueOf", "(J)Ljava/lang/Long;");

    DEFINE_CLASS(JPy_Float_JClass, "java/lang/Float");
    DEFINE_STATIC_METHOD(JPy_Float_ValueOf_SMID, JPy_Float_JClass, "valueOf", "(F)Ljava/lang/Float;");

    DEFINE_CLASS(JPy_Double_JClass, "java/lang/Double");
    DEFINE_STATIC_METHOD(JPy_Double_ValueOf_SMID, JPy_Double_JClass, "valueOf", "(D)Ljava/lang/Double;");

    DEFINE_CLASS(JPy_Number_JClass, "java/lang/Number");
    DEFINE_METHOD(JPy_Number_IntValue_MID, JPy_Number_JClass, "intValue", "()I");
    DEFINE_METHOD(JPy_Number_LongValue_MID , JPy_Number_JClass, "longValue", "()J");
    DEFINE_METHOD(JPy_Number_DoubleValue_MID, JPy_Number_JClass, "doubleValue", "()D");

    DEFINE_CLASS(JPy_Void_JClass, "java/lang/Void");

    DEFINE_CLASS(JPy_String_JClass, "java/lang/String");
    DEFINE_CLASS(JPy_Throwable_JClass, "java/lang/Throwable");
    DEFINE_CLASS(JPy_StackTraceElement_JClass, "java/lang/StackTraceElement");

    // Non-Object types: Primitive types and void.
    DEFINE_NON_OBJECT_TYPE(JPy_JBoolean, JPy_Boolean_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JChar, JPy_Character_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JByte, JPy_Byte_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JShort, JPy_Short_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JInt, JPy_Integer_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JLong, JPy_Long_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JFloat, JPy_Float_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JDouble, JPy_Double_JClass);
    DEFINE_NON_OBJECT_TYPE(JPy_JVoid, JPy_Void_JClass);
    // The Java root object. Make sure, JPy_JObject is the first object type to be initialized.
    DEFINE_OBJECT_TYPE(JPy_JObject, JPy_Object_JClass);
    // Immediately after JPy_JObject, we define JPy_JClass. From now on JType_AddClassAttribute() works.
    DEFINE_OBJECT_TYPE(JPy_JClass, JPy_Class_JClass);
    // Primitive-Wrapper Objects.
    DEFINE_OBJECT_TYPE(JPy_JBooleanObj, JPy_Boolean_JClass);
    DEFINE_OBJECT_TYPE(JPy_JCharacterObj, JPy_Character_JClass);
    DEFINE_OBJECT_TYPE(JPy_JByteObj, JPy_Byte_JClass);
    DEFINE_OBJECT_TYPE(JPy_JShortObj, JPy_Short_JClass);
    DEFINE_OBJECT_TYPE(JPy_JIntegerObj, JPy_Integer_JClass);
    DEFINE_OBJECT_TYPE(JPy_JLongObj, JPy_Long_JClass);
    DEFINE_OBJECT_TYPE(JPy_JFloatObj, JPy_Float_JClass);
    DEFINE_OBJECT_TYPE(JPy_JDoubleObj, JPy_Double_JClass);
    // Other objects.
    DEFINE_OBJECT_TYPE(JPy_JString, JPy_String_JClass);
    DEFINE_OBJECT_TYPE(JPy_JThrowable, JPy_Throwable_JClass);
    DEFINE_OBJECT_TYPE(JPy_JStackTraceElement, JPy_StackTraceElement_JClass);
    DEFINE_METHOD(JPy_Throwable_getCause_MID, JPy_Throwable_JClass, "getCause", "()Ljava/lang/Throwable;");
    DEFINE_METHOD(JPy_Throwable_getStackTrace_MID, JPy_Throwable_JClass, "getStackTrace", "()[Ljava/lang/StackTraceElement;");

    DEFINE_CLASS(JPy_Supplier_JClass, "java/util/function/Supplier");
    DEFINE_METHOD(JPy_Supplier_get_MID, JPy_Supplier_JClass, "get", "()Ljava/lang/Object;")

    // JType_AddClassAttribute is actually called from within JType_GetType(), but not for
    // JPy_JObject and JPy_JClass for an obvious reason. So we do it now:
    JType_AddClassAttribute(jenv, JPy_JObject);
    JType_AddClassAttribute(jenv, JPy_JClass);

    if (initGlobalPyObjectVars(jenv) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JPy_InitGlobalVars: JPy_JPyObject=%p, JPy_JPyModule=%p\n", JPy_JPyObject, JPy_JPyModule);
    }

    return 0;
}

void JPy_ClearGlobalVars(JNIEnv* jenv)
{
    if (jenv != NULL) {
        (*jenv)->DeleteGlobalRef(jenv, JPy_Comparable_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Object_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Class_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Constructor_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Method_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Field_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_RuntimeException_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Boolean_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Character_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Byte_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Short_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Integer_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Long_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Float_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Double_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Number_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_Void_JClass);
        (*jenv)->DeleteGlobalRef(jenv, JPy_String_JClass);
    }

    JPy_Comparable_JClass = NULL;
    JPy_Object_JClass = NULL;
    JPy_Class_JClass = NULL;
    JPy_Constructor_JClass = NULL;
    JPy_Method_JClass = NULL;
    JPy_Field_JClass = NULL;
    JPy_RuntimeException_JClass = NULL;
    JPy_Boolean_JClass = NULL;
    JPy_Character_JClass = NULL;
    JPy_Byte_JClass = NULL;
    JPy_Short_JClass = NULL;
    JPy_Integer_JClass = NULL;
    JPy_Long_JClass = NULL;
    JPy_Float_JClass = NULL;
    JPy_Double_JClass = NULL;
    JPy_Number_JClass = NULL;
    JPy_Void_JClass = NULL;
    JPy_String_JClass = NULL;

    JPy_Object_ToString_MID = NULL;
    JPy_Object_HashCode_MID = NULL;
    JPy_Object_Equals_MID = NULL;
    JPy_Class_GetName_MID = NULL;
    JPy_Class_GetDeclaredConstructors_MID = NULL;
    JPy_Class_GetDeclaredFields_MID = NULL;
    JPy_Class_GetDeclaredMethods_MID = NULL;
    JPy_Class_GetFields_MID = NULL;
    JPy_Class_GetMethods_MID = NULL;
    JPy_Class_GetComponentType_MID = NULL;
    JPy_Class_IsPrimitive_MID = NULL;
    JPy_Class_IsInterface_MID = NULL;
    JPy_Constructor_GetModifiers_MID = NULL;
    JPy_Constructor_GetParameterTypes_MID = NULL;
    JPy_Method_GetName_MID = NULL;
    JPy_Method_GetReturnType_MID = NULL;
    JPy_Method_GetParameterTypes_MID = NULL;
    JPy_Method_GetModifiers_MID = NULL;
    JPy_Field_GetName_MID = NULL;
    JPy_Field_GetModifiers_MID = NULL;
    JPy_Field_GetType_MID = NULL;
    JPy_Boolean_ValueOf_SMID = NULL;
    JPy_Boolean_BooleanValue_MID = NULL;
    JPy_Character_ValueOf_SMID = NULL;
    JPy_Character_CharValue_MID = NULL;
    JPy_Byte_ValueOf_SMID = NULL;
    JPy_Short_ValueOf_SMID = NULL;
    JPy_Integer_ValueOf_SMID = NULL;
    JPy_Long_ValueOf_SMID = NULL;
    JPy_Float_ValueOf_SMID = NULL;
    JPy_Double_ValueOf_SMID = NULL;
    JPy_Number_IntValue_MID = NULL;
    JPy_Number_LongValue_MID = NULL;
    JPy_Number_DoubleValue_MID = NULL;
    JPy_PyObject_GetPointer_MID = NULL;
    JPy_PyObject_UnwrapProxy_SMID = NULL;

    JPy_XDECREF(JPy_JBoolean);
    JPy_XDECREF(JPy_JChar);
    JPy_XDECREF(JPy_JByte);
    JPy_XDECREF(JPy_JShort);
    JPy_XDECREF(JPy_JInt);
    JPy_XDECREF(JPy_JLong);
    JPy_XDECREF(JPy_JFloat);
    JPy_XDECREF(JPy_JDouble);
    JPy_XDECREF(JPy_JVoid);
    JPy_XDECREF(JPy_JBooleanObj);
    JPy_XDECREF(JPy_JCharacterObj);
    JPy_XDECREF(JPy_JByteObj);
    JPy_XDECREF(JPy_JShortObj);
    JPy_XDECREF(JPy_JIntegerObj);
    JPy_XDECREF(JPy_JLongObj);
    JPy_XDECREF(JPy_JFloatObj);
    JPy_XDECREF(JPy_JDoubleObj);
    JPy_XDECREF(JPy_JPyObject);
    JPy_XDECREF(JPy_JPyModule);

    JPy_JBoolean = NULL;
    JPy_JChar = NULL;
    JPy_JByte = NULL;
    JPy_JShort = NULL;
    JPy_JInt = NULL;
    JPy_JLong = NULL;
    JPy_JFloat = NULL;
    JPy_JDouble = NULL;
    JPy_JVoid = NULL;
    JPy_JString = NULL;
    JPy_JBooleanObj = NULL;
    JPy_JCharacterObj = NULL;
    JPy_JByteObj = NULL;
    JPy_JShortObj = NULL;
    JPy_JIntegerObj = NULL;
    JPy_JLongObj = NULL;
    JPy_JFloatObj = NULL;
    JPy_JDoubleObj = NULL;
    JPy_JPyObject = NULL;
    JPy_JPyModule = NULL;
}

#define AT_STRING "\tat "
#define AT_STRLEN 4
#define CAUSED_BY_STRING "caused by "
#define CAUSED_BY_STRLEN 10
#define ELIDED_STRING_MAX_SIZE 30

void JPy_HandleJavaException(JNIEnv* jenv)
{
    jthrowable error = (*jenv)->ExceptionOccurred(jenv);
    if (error != NULL) {
        jstring message;
        int allocError = 0;

        if (JPy_DiagFlags != 0) {
            (*jenv)->ExceptionDescribe(jenv);
        }

        if (JPy_VerboseExceptions) {
            char *stackTraceString;
            size_t stackTraceLength = 0;
            jthrowable cause = error;
            jarray enclosingElements = NULL;
            jint enclosingSize = 0;

            stackTraceString = strdup("");

            do {
                /* We want the type and the detail string, which is actually what a Throwable toString() does by
                 * default, as does the default printStackTrace(). */
                jint ii;

                jarray stackTrace;
                jint stackTraceElements;
                jint lastElementToPrint;
                jint enclosingIndex;

                if (stackTraceLength > 0) {
                    char *newStackString;

                    newStackString = realloc(stackTraceString, CAUSED_BY_STRLEN + 1 + stackTraceLength);
                    if (newStackString == NULL) {
                        allocError = 1;
                        break;
                    }
                    stackTraceString = newStackString;
                    strcat(stackTraceString, CAUSED_BY_STRING);
                    stackTraceLength += CAUSED_BY_STRLEN;
                }

                message = (jstring) (*jenv)->CallObjectMethod(jenv, cause, JPy_Object_ToString_MID);
                if (message != NULL) {
                    const char *messageChars = (*jenv)->GetStringUTFChars(jenv, message, NULL);
                    if (messageChars != NULL) {
                        char *newStackString;
                        size_t len = strlen(messageChars);

                        newStackString = realloc(stackTraceString, len + 2 + stackTraceLength);
                        if (newStackString == NULL) {
                            (*jenv)->ReleaseStringUTFChars(jenv, message, messageChars);
                            allocError = 1;
                            break;
                        }

                        stackTraceString = newStackString;
                        strcat(stackTraceString, messageChars);
                        stackTraceString[stackTraceLength + len] = '\n';
                        stackTraceString[stackTraceLength + len + 1] = '\0';
                        stackTraceLength += (len + 1);

                        (*jenv)->ReleaseStringUTFChars(jenv, message, messageChars);
                    } else {
                        allocError = 1;
                        break;
                    }
                    JPy_DELETE_LOCAL_REF(message);
                }

                /* We should assemble a string based on the stack trace. */
                stackTrace = (*jenv)->CallObjectMethod(jenv, cause, JPy_Throwable_getStackTrace_MID);
                stackTraceElements = (*jenv)->GetArrayLength(jenv, stackTrace);
                lastElementToPrint = stackTraceElements - 1;
                enclosingIndex = enclosingSize - 1;

                while (lastElementToPrint >= 0 && enclosingIndex >= 0) {
                    jobject thisElement = (*jenv)->GetObjectArrayElement(jenv, stackTrace, lastElementToPrint);
                    jobject thatElement = (*jenv)->GetObjectArrayElement(jenv, enclosingElements, enclosingIndex);

                    // if they are equal, let's decrement, otherwise we break
                    jboolean  equal = (*jenv)->CallBooleanMethod(jenv, thisElement, JPy_Object_Equals_MID, thatElement);
                    if (!equal) {
                        break;
                    }

                    lastElementToPrint--;
                    enclosingIndex--;
                }

                for (ii = 0; ii <= lastElementToPrint; ++ii) {
                    jobject traceElement = (*jenv)->GetObjectArrayElement(jenv, stackTrace, ii);
                    if (traceElement != NULL) {
                        message = (jstring) (*jenv)->CallObjectMethod(jenv, traceElement, JPy_Object_ToString_MID);
                        if (message != NULL) {
                            size_t len;
                            char *newStackString;
                            const char *messageChars = (*jenv)->GetStringUTFChars(jenv, message, NULL);
                            if (messageChars == NULL) {
                                allocError = 1;
                                break;
                            }

                            len = strlen(messageChars);

                            newStackString = realloc(stackTraceString, len + 2 + AT_STRLEN + stackTraceLength);
                            if (newStackString == NULL) {
                                (*jenv)->ReleaseStringUTFChars(jenv, message, messageChars);
                                allocError = 1;
                                break;
                            }

                            stackTraceString = newStackString;
                            strcat(stackTraceString, AT_STRING);
                            strcat(stackTraceString, messageChars);
                            stackTraceString[stackTraceLength + len + AT_STRLEN] = '\n';
                            stackTraceString[stackTraceLength + len + AT_STRLEN + 1] = '\0';
                            stackTraceLength += (len + 1 + AT_STRLEN);

                            (*jenv)->ReleaseStringUTFChars(jenv, message, messageChars);
                        }

                    }
                }

                if (lastElementToPrint < stackTraceElements - 1) {
                    int written;
                    char *newStackString = realloc(stackTraceString, stackTraceLength + ELIDED_STRING_MAX_SIZE);
                    if (newStackString == NULL) {
                        allocError = 1;
                        break;
                    }

                    stackTraceString = newStackString;
                    stackTraceString[stackTraceLength + ELIDED_STRING_MAX_SIZE - 1] = '\0';

                    written = snprintf(stackTraceString + stackTraceLength, ELIDED_STRING_MAX_SIZE - 1, "\t... %d more\n", (stackTraceElements - lastElementToPrint) - 1);
                    if (written > (ELIDED_STRING_MAX_SIZE - 1)) {
                        stackTraceLength += (ELIDED_STRING_MAX_SIZE - 1);
                    } else {
                        stackTraceLength += written;
                    }
                }

                /** So we can eliminate extra entries. */
                enclosingElements = stackTrace;
                enclosingSize = stackTraceElements;

                /** Now the next cause. */
                cause = (*jenv)->CallObjectMethod(jenv, cause, JPy_Throwable_getCause_MID);
            } while (cause != NULL && !allocError);

            if (allocError == 0 && stackTraceString != NULL) {
                PyErr_Format(PyExc_RuntimeError, "%s", stackTraceString);
            } else {
                PyErr_SetString(PyExc_RuntimeError,
                                "Java VM exception occurred, but failed to allocate message text");
            }
            free(stackTraceString);
        } else {
            message = (jstring) (*jenv)->CallObjectMethod(jenv, error, JPy_Object_ToString_MID);
            if (message != NULL) {
                const char *messageChars;

                messageChars = (*jenv)->GetStringUTFChars(jenv, message, NULL);
                if (messageChars != NULL) {
                    PyErr_Format(PyExc_RuntimeError, "%s", messageChars);
                    (*jenv)->ReleaseStringUTFChars(jenv, message, messageChars);
                } else {
                    PyErr_SetString(PyExc_RuntimeError,
                                    "Java VM exception occurred, but failed to allocate message text");
                }
                JPy_DELETE_LOCAL_REF(message);
            } else {
                PyErr_SetString(PyExc_RuntimeError, "Java VM exception occurred, no message");
            }
        }

        JPy_DELETE_LOCAL_REF(error);
        (*jenv)->ExceptionClear(jenv);
    }
}

void JPy_free()
{
    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JPy_free: freeing module data...\n");
    JPy_ClearGlobalVars(NULL);

    JPy_Module = NULL;
    JPy_Types = NULL;
    JPy_Type_Callbacks = NULL;
    JPy_Type_Translations = NULL;
    JException_Type = NULL;

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JPy_free: done freeing module data\n");
}

void JPy_free_ptr(void* unused)
{
    JPy_free();
}
