// Copyright 2022 Alibaba Group Holding Limited.
//
// 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.

#include "Pemja.h"
#include "python_class/PythonClass.h"
#include "java_class/JavaClass.h"

static PyThreadState *JcpMainThreadState = NULL;

/*
 * Create redirection module.
 */

static PyObject *stdout_redirection(PyObject *self, PyObject *args) {
  const char *string;

  if (!PyArg_ParseTuple(args, "s", &string)) return NULL;

  fprintf(stdout, "%s", string);
  fflush(stdout);

  Py_INCREF(Py_None);
  return Py_None;
}

static PyObject *stderr_redirection(PyObject *self, PyObject *args) {
  const char *string;

  if (!PyArg_ParseTuple(args, "s", &string)) return NULL;

  fprintf(stderr, "%s", string);
  fflush(stderr);

  Py_INCREF(Py_None);
  return Py_None;
}

static PyMethodDef redirectionMethods[] = {
    {"stdout_redirect", (PyCFunction)stdout_redirection, METH_VARARGS, ""},
    {"stderr_redirect", (PyCFunction)stderr_redirection, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL} /*sentinel */
};

static struct PyModuleDef redirection_module_def = {
    PyModuleDef_HEAD_INIT,
    "redirection",      /* m_name */
    NULL,               /* m_doc */
    -1,                 /* m_size */
    redirectionMethods, /* m_methods */
    NULL,               /* m_reload */
    NULL,               /* m_traverse */
    NULL,               /* m_clear */
    NULL,               /* m_free */
};

/*
 * Create pemja module.
 */

static PyObject *pemja_find_class(PyObject *self, PyObject *args) {
  JcpThread *jcp_thread;
  PyObject *result;

  char *name, *p;

  JNIEnv *env;
  jclass clazz;

  if (!PyArg_ParseTuple(args, "s", &name)) {
    return NULL;
  }

  // get JcpThread
  jcp_thread = JcpThread_Get();
  if (!jcp_thread) {
    if (!PyErr_Occurred()) {
      PyErr_Format(PyExc_RuntimeError, "Invalid JcpThread pointer.");
    }
    return NULL;
  }

  // get JNIEnv*
  env = jcp_thread->env;

  // convert python class name to java class name. e.g. java.lang.Object ->
  // java/lang/Object
  for (p = name; *p != '\0'; p++) {
    if (*p == '.') {
      *p = '/';
    }
  }

  clazz = (*env)->FindClass(env, name);
  // failed to find the class
  if ((*env)->ExceptionOccurred(env)) {
    return NULL;
  }

  result = JcpPyJClass_New(env, clazz);

  (*env)->DeleteLocalRef(env, clazz);
  return result;
}

static PyMethodDef pemja_methods[] = {
    {"findClass", (PyCFunction)pemja_find_class, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL} /*sentinel */
};

static struct PyModuleDef pemja_module_def = {
    PyModuleDef_HEAD_INIT,
    "_pemja",      /* m_name */
    NULL,          /* m_doc */
    -1,            /* m_size */
    pemja_methods, /* m_methods */
    NULL,          /* m_reload */
    NULL,          /* m_traverse */
    NULL,          /* m_clear */
    NULL,          /* m_free */
};

/*
 * Initialize pemja module
 */
static PyObject *pemja_module_init(JNIEnv *env) {
  PyObject *sys_modules = NULL;
  PyObject *pemja_module = NULL;

  // create _pemja module
  pemja_module = PyModule_Create(&pemja_module_def);
  if (pemja_module == NULL) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to create `_pemja` module.");
    return NULL;
  }

  sys_modules = PyImport_GetModuleDict();
  if (PyDict_SetItemString(sys_modules, "_pemja", pemja_module) == -1) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to add `_pemja` module to sys.modules");
    return NULL;
  }

  // import _pemja module
  pemja_module = PyImport_ImportModule("_pemja");
  if (!pemja_module) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to import `_pemja` module");
    return NULL;
  }

  // stuff for making new pyjarray objects
  PyModule_AddIntConstant(pemja_module, "JBOOLEAN_ID", JBOOLEAN_ID);
  PyModule_AddIntConstant(pemja_module, "JINT_ID", JINT_ID);
  PyModule_AddIntConstant(pemja_module, "JLONG_ID", JLONG_ID);
  PyModule_AddIntConstant(pemja_module, "JSTRING_ID", JSTRING_ID);
  PyModule_AddIntConstant(pemja_module, "JDOUBLE_ID", JDOUBLE_ID);
  PyModule_AddIntConstant(pemja_module, "JSHORT_ID", JSHORT_ID);
  PyModule_AddIntConstant(pemja_module, "JFLOAT_ID", JFLOAT_ID);
  PyModule_AddIntConstant(pemja_module, "JCHAR_ID", JCHAR_ID);
  PyModule_AddIntConstant(pemja_module, "JBYTE_ID", JBYTE_ID);

  return pemja_module;
}

/**
 * Get the JcpThread.
 */
JcpThread *JcpThread_Get(void) {
  PyObject *tdict, *t, *key;
  JcpThread *ret = NULL;

  key = PyUnicode_FromString(DICT_KEY);
  if ((tdict = PyThreadState_GetDict()) != NULL && key != NULL) {
    t = PyDict_GetItem(tdict, key); /* borrowed */
    if (t != NULL && !PyErr_Occurred()) {
      ret = (JcpThread *)PyCapsule_GetPointer(t, NULL);
    }
  }
  Py_XDECREF(key);
  if (!ret && !PyErr_Occurred()) {
    PyErr_Format(PyExc_RuntimeError,
                 "No JcpThread instance available on current thread.");
  }
  return ret;
}

JNIEnv *JcpThreadEnv_Get(void) {
  JavaVM *jvm;
  JNIEnv *env;
  jsize nVMs;

  JNI_GetCreatedJavaVMs(&jvm, 1, &nVMs);

  (*jvm)->AttachCurrentThreadAsDaemon(jvm, (void **)&env, NULL);

  return env;
}

/*
 * Initialize Python main Interpreter and this method will be called at startup
 * and be called only once.
 */

static int pyjtypes_init() {
  if (PyType_Ready(&PyJObject_Type) < 0) {
    return -1;
  }

  // class
  if (!PyJClass_Type.tp_base) {
    PyJClass_Type.tp_base = &PyJObject_Type;
  }

  if (PyType_Ready(&PyJClass_Type) < 0) {
    return -1;
  }

  // iterable
  if (!PyJIterable_Type.tp_base) {
    PyJIterable_Type.tp_base = &PyJObject_Type;
  }

  if (PyType_Ready(&PyJIterable_Type) < 0) {
    return -1;
  }

  // iterator
  if (!PyJIterator_Type.tp_base) {
    PyJIterator_Type.tp_base = &PyJObject_Type;
  }

  if (PyType_Ready(&PyJIterator_Type) < 0) {
    return -1;
  }

  // collection
  if (!PyJCollection_Type.tp_base) {
    PyJCollection_Type.tp_base = &PyJIterable_Type;
  }

    if (PyType_Ready(&PyJCollection_Type) < 0) {
        return -1;
    }

  return 0;
}

void JcpPy_setPythonHome(JNIEnv *env, jstring home) {
  const char *home_as_utf = (*env)->GetStringUTFChars(env, home, NULL);
  wchar_t *home_for_python = Py_DecodeLocale(home_as_utf, NULL);
  (*env)->ReleaseStringUTFChars(env, home, home_as_utf);
  Py_SetPythonHome(home_for_python);
}

void JcpPy_Initialize(JNIEnv *env, jstring python_home, jstring working_dir) {
  PyObject *sys_modules = NULL;
  PyObject *redirection_module = NULL;

  if (JcpMainThreadState != NULL) {
    // this shouldn't happen but to be safe, don't initialize twice
    return;
  }

  // Cache java classes
  Jcp_CacheClasses(env);

  // Initialize Python
  Py_Initialize();

#if PY_MINOR_VERSION < 9
  // Initialize Python thread support
  PyEval_InitThreads();
#endif

  if (pyjtypes_init() < 0) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to initialize pyjtypes.");
    return;
  }

  // save a pointer to the main PyThreadState object
  JcpMainThreadState = PyThreadState_Get();

  // create redirection module
  redirection_module = PyModule_Create(&redirection_module_def);
  if (redirection_module == NULL) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to create `redirection` module.");
    goto EXIT;
  }

  sys_modules = PyImport_GetModuleDict();
  if (PyDict_SetItemString(sys_modules, "redirection", redirection_module) ==
      -1) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to add `redirection` module to sys.modules");
    goto EXIT;
  }

  // import redirection module
  redirection_module = PyImport_ImportModule("redirection");
  if (!redirection_module) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to import `redirection` module");
    goto EXIT;
  }
  Py_DECREF(redirection_module);

  // set sys.argv
  wchar_t *argv[] = {L"jcp", 0};

  PySys_SetArgv(1, argv);

  if (python_home) {
    const char *python_home_chars =
        (*env)->GetStringUTFChars(env, python_home, NULL);
    Py_SetPythonHome(Py_DecodeLocale(python_home_chars, NULL));
    (*env)->ReleaseStringUTFChars(env, python_home, python_home_chars);
  }

  if (working_dir) {
    const char *working_dir_chars =
        (*env)->GetStringUTFChars(env, working_dir, NULL);
    chdir(working_dir_chars);
    (*env)->ReleaseStringUTFChars(env, working_dir, working_dir_chars);
  }

EXIT:
  PyEval_ReleaseThread(JcpMainThreadState);
}

/*
 * Finalize Python Interpreter and release resources.
 */

void JcpPy_Finalize(JavaVM *vm) {
  JNIEnv *env;

  // shutdown python
  PyEval_AcquireThread(JcpMainThreadState);
  Py_Finalize();

  if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK) {
    // failed to get a JNIEnv*, we can hope it's just shutting down fast
    return;
  } else {
    // delete global references
    Jcp_UnRefCacheClasses(env);
  }
}

/*
 * Initialize JcpThread and attach a new PyThreadState to it.
 */

intptr_t JcpPy_InitThread(JNIEnv *env, int type) {
  JcpThread *jcp_thread;

  PyObject *tdict, *globals = NULL, *key, *t;

  jcp_thread = malloc(sizeof(JcpThread));
  if (!jcp_thread) {
    return 0;
  }

  if (type == 0) {
    // create new ThreadState
    jcp_thread->tstate = PyThreadState_New(JcpMainThreadState->interp);

  } else if (type == 1) {
    // create sub interpreter
    PyEval_AcquireThread(JcpMainThreadState);

    jcp_thread->tstate = Py_NewInterpreter();

    PyEval_SaveThread();
  } else {
    PyErr_Format(PyExc_RuntimeError, "Unknown exec type `%d` ", type);
  }

  PyEval_AcquireThread(jcp_thread->tstate);

  if (type == 0) {
    globals = PyDict_New();
    PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
  } else if (type == 1) {
    PyObject *mod_main = PyImport_AddModule("__main__"); /* borrowed */
    if (mod_main == NULL) {
      PyEval_ReleaseThread(jcp_thread->tstate);
      return 0;
    }
    globals = PyModule_GetDict(mod_main);
    Py_INCREF(globals);
  }

  if ((tdict = PyThreadState_GetDict()) != NULL) {
    // Capsule objects let you wrap a C "void *" pointer in a Python object.
    t = PyCapsule_New((void *)jcp_thread, NULL, NULL);
    key = PyUnicode_FromString(DICT_KEY);

    PyDict_SetItem(tdict, key, t); /* takes ownership */

    Py_DECREF(key);
    Py_DECREF(t);
  }

  // Init JcpThread
  jcp_thread->globals = globals;
  jcp_thread->env = env;
  jcp_thread->cache_function_name = NULL;
  jcp_thread->cache_object_name = NULL;
  jcp_thread->cache_method_name = NULL;
  jcp_thread->cache_callable = NULL;
  jcp_thread->name_to_attrs = NULL;
  jcp_thread->pemja_module = pemja_module_init(env);

  PyEval_ReleaseThread(jcp_thread->tstate);

  return (intptr_t)jcp_thread;
}

/*
 * Finalize JcpThread.
 */

void JcpPy_FinalizeThread(intptr_t ptr) {
  JcpThread *jcp_thread;

  PyObject *tdict, *key;

  jcp_thread = (JcpThread *)ptr;
  if (!jcp_thread) {
    printf("WARNING: thread_close, invalid JcpThread pointer.\n");
    return;
  }

  PyEval_AcquireThread(jcp_thread->tstate);

  key = PyUnicode_FromString(DICT_KEY);
  if ((tdict = PyThreadState_GetDict()) != NULL && key != NULL) {
    PyDict_DelItem(tdict, key);
  }

  Py_DECREF(key);

  Py_CLEAR(jcp_thread->globals);
  Py_CLEAR(jcp_thread->name_to_attrs);
  Py_CLEAR(jcp_thread->pemja_module);

  if (jcp_thread->cache_function_name) {
    free(jcp_thread->cache_function_name);
  }

  if (jcp_thread->cache_object_name) {
    free(jcp_thread->cache_object_name);
  }

  if (jcp_thread->cache_method_name) {
    free(jcp_thread->cache_method_name);
  }

  Py_XDECREF(jcp_thread->cache_callable);

  if (jcp_thread->tstate->interp == JcpMainThreadState->interp) {
    PyThreadState_Clear(jcp_thread->tstate);
    PyEval_ReleaseThread(jcp_thread->tstate);
    PyThreadState_Delete(jcp_thread->tstate);
  } else {
    Py_EndInterpreter(jcp_thread->tstate);
    PyThreadState_Swap(JcpMainThreadState);
    PyEval_ReleaseThread(JcpMainThreadState);
  }

  free(jcp_thread);
}

/* Add path to search path of Main Interpreter */

void JcpPy_AddSearchPath(JNIEnv *env, jstring path) {
  PyObject *path_list;
  PyObject *py_path;

  PyEval_AcquireThread(JcpMainThreadState);

  path_list = PySys_GetObject("path");

  if (path_list != NULL) {
    Py_INCREF(path_list);

    if (path != NULL) {
      py_path = JcpPyString_FromJString(env, path);

      if (py_path != NULL) {
        PyList_Insert(path_list, 0, py_path);
        Py_DECREF(py_path);
      }
    }
  }

  PyEval_ReleaseThread(JcpMainThreadState);
}

/* Import module to the Main Interpreter so that the module can be shared by all
 * interpreters */

void JcpPy_ImportModule(JNIEnv *env, jstring name) {
  const char *module_name;

  PyObject *sys_modules;
  PyObject *module;

  PyEval_AcquireThread(JcpMainThreadState);

  sys_modules = PyImport_GetModuleDict();

  if (sys_modules == NULL) {
    (*env)->ThrowNew(env, JILLEGAL_STATE_EXEC_TYPE,
                     "Failed to import `sys` module.");
    goto exit;
  }

  module_name = JcpString_FromJString(env, name);
  module = PyImport_ImportModule(module_name);

  if (module) {
    Py_DECREF(module);
  } else {
    PyErr_Format(PyExc_RuntimeError, "Failed to import module `%s` ",
                 module_name);
    JcpPyErr_Throw(env);
  }

  JcpString_Clear(env, name, module_name);

exit:
  PyEval_ReleaseThread(JcpMainThreadState);
}

// ------------------------------ set/get parameters
// methods--------------------------------

/* Function to set a Java boolean value in the specified PyThreadState */

void JcpPyObject_SetJBoolean(JNIEnv *env, intptr_t ptr, const char *name,
                             jboolean value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyBool_FromLong((long)value));

  Jcp_END_ALLOW_THREADS
}

/* Function to set a Java int value in the specified PyThreadState */

void JcpPyObject_SetJInt(JNIEnv *env, intptr_t ptr, const char *name,
                         jint value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyInt_FromInt((int)value));

  Jcp_END_ALLOW_THREADS
}

/* Function to set a Java long value in the specified PyThreadState */

void JcpPyObject_SetJLong(JNIEnv *env, intptr_t ptr, const char *name,
                          jlong value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyInt_FromLong(value));

  Jcp_END_ALLOW_THREADS
}

/* Function to set a Java double value in the specified PyThreadState */

void JcpPyObject_SetJDouble(JNIEnv *env, intptr_t ptr, const char *name,
                            jdouble value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyFloat_FromDouble((double)value));

  Jcp_END_ALLOW_THREADS
}

/* Function to set a Java String in the specified PyThreadState */

void JcpPyObject_SetJString(JNIEnv *env, intptr_t ptr, const char *name,
                            jstring value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyString_FromJString(env, value));

  Jcp_END_ALLOW_THREADS
}

/* Function to set a Java Object in the specified PyThreadState */

void JcpPyObject_SetJObject(JNIEnv *env, intptr_t ptr, const char *name,
                            jobject value) {
  Jcp_BEGIN_ALLOW_THREADS

      _JcpPyObject_SetPyObject(jcp_thread->globals, name,
                               JcpPyObject_FromJObject(env, value));

  Jcp_END_ALLOW_THREADS
}

/* Function to get the Java Object by the name in the specified PyThreadState */

jobject JcpPyObject_GetJObject(JNIEnv *env, intptr_t ptr, const char *name,
                               jclass clazz) {
  PyObject *pyobject;

  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      pyobject = PyDict_GetItemString(jcp_thread->globals, name);

  if (pyobject != NULL) {
    result = JcpPyObject_AsJObject(env, pyobject, clazz);
  }

  Jcp_END_ALLOW_THREADS

      return result;
}

// ----------------------------------------------------------------------------------------

// ------------------------------ Core JcpPyObject call functions
// -------------------------

/* Call a callable Python object without any arguments */

jobject JcpPyObject_CallNoArgs(JNIEnv *env, intptr_t ptr, const char *name) {
  PyObject *callable = NULL;
  PyObject *py_ret = NULL;

  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      callable = _JcpPyFunction_Load(env, jcp_thread, name);

  if (callable) {
#if PY_MINOR_VERSION >= 9
    py_ret = PyObject_CallNoArgs(callable);
#else
    py_ret = PyObject_CallFunctionObjArgs(callable, NULL);
#endif

    if (!JcpPyErr_Throw(env)) {
      result = JcpPyObject_AsJObject(env, py_ret, JOBJECT_TYPE);
      Py_DECREF(py_ret);
    }
  }

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jboolean argument */

jobject JcpPyObject_CallOneJBooleanArg(JNIEnv *env, intptr_t ptr,
                                       const char *name, jboolean arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_CallOneArg(env, jcp_thread, name,
                                       JcpPyBool_FromLong((long)arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jint argument */

jobject JcpPyObject_CallOneJIntArg(JNIEnv *env, intptr_t ptr, const char *name,
                                   jint arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result =
          _JcpPyObject_CallOneArg(env, jcp_thread, name, JcpPyInt_FromInt(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jlong argument */

jobject JcpPyObject_CallOneJLongArg(JNIEnv *env, intptr_t ptr, const char *name,
                                    jlong arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_CallOneArg(env, jcp_thread, name,
                                       JcpPyInt_FromLong(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jdouble argument */

jobject JcpPyObject_CallOneJDoubleArg(JNIEnv *env, intptr_t ptr,
                                      const char *name, jdouble arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_CallOneArg(env, jcp_thread, name,
                                       JcpPyFloat_FromDouble(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jstring argument */

jobject JcpPyObject_CallOneJStringArg(JNIEnv *env, intptr_t ptr,
                                      const char *name, jstring arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_CallOneArg(env, jcp_thread, name,
                                       JcpPyString_FromJString(env, arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object with only one jobject argument */

jobject JcpPyObject_CallOneJObjectArg(JNIEnv *env, intptr_t ptr,
                                      const char *name, jobject arg) {
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_CallOneArg(env, jcp_thread, name,
                                       JcpPyObject_FromJObject(env, arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call a callable Python object */

jobject JcpPyObject_Call(JNIEnv *env, intptr_t ptr, const char *name,
                         jobjectArray args, jobject kwargs) {
  int arg_len = 0;

  PyObject *py_arg1;
  PyObject *py_arg2;
  PyObject *py_args = NULL;
  PyObject *py_kwargs = NULL;
  PyObject *py_ret = NULL;
  PyObject *callable = NULL;

  jobject element;
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      if (args != NULL) {
    arg_len = (*env)->GetArrayLength(env, args);
  }

  callable = _JcpPyFunction_Load(env, jcp_thread, name);

  if (!callable) {
    JcpPyErr_Throw(env);
    goto exit;
  }

  if (kwargs != NULL) {
    py_args = PyTuple_New(arg_len);

    for (int i = 0; i < arg_len; i++) {
      element = (*env)->GetObjectArrayElement(env, args, i);
      PyTuple_SetItem(py_args, i, JcpPyObject_FromJObject(env, element));
      (*env)->DeleteLocalRef(env, element);
    }

    py_kwargs = JcpPyDict_FromJMap(env, kwargs);
    py_ret = PyObject_Call(callable, py_args, py_kwargs);
    Py_DECREF(py_args);
    Py_DECREF(py_kwargs);

  } else {
    // create a Tuple to store args will lost some performance, so for the
    // function which only have less than two arguments, we will call
    // PyObject_CallFunctionObjArgs to speed up.
    if (arg_len == 0) {
      py_ret = PyObject_CallFunctionObjArgs(callable, NULL);
    } else if (arg_len == 1) {
      element = (*env)->GetObjectArrayElement(env, args, 0);
      PyObject *py_arg = JcpPyObject_FromJObject(env, element);
      (*env)->DeleteLocalRef(env, element);

      // call the callable function
      py_ret = PyObject_CallFunctionObjArgs(callable, py_arg, NULL);

      Py_DECREF(py_arg);
    } else if (arg_len == 2) {
      // get 1st input argument
      element = (*env)->GetObjectArrayElement(env, args, 0);
      py_arg1 = JcpPyObject_FromJObject(env, element);
      (*env)->DeleteLocalRef(env, element);
      // get 2nd input argument
      element = (*env)->GetObjectArrayElement(env, args, 1);
      py_arg2 = JcpPyObject_FromJObject(env, element);
      (*env)->DeleteLocalRef(env, element);

      // call the callable function
      py_ret = PyObject_CallFunctionObjArgs(callable, py_arg1, py_arg2, NULL);

      Py_DECREF(py_arg1);
      Py_DECREF(py_arg2);
    } else {
      py_args = PyTuple_New(arg_len);

      for (int i = 0; i < arg_len; i++) {
        element = (*env)->GetObjectArrayElement(env, args, i);
        PyTuple_SetItem(py_args, i, JcpPyObject_FromJObject(env, element));
        (*env)->DeleteLocalRef(env, element);
      }

      // call the callable function
      py_ret = PyObject_CallObject(callable, py_args);
      Py_DECREF(py_args);
    }
  }

  if (JcpPyErr_Throw(env) || !py_ret) {
    goto exit;
  }

  result = JcpPyObject_AsJObject(env, py_ret, JOBJECT_TYPE);

  if (!result) {
    JcpPyErr_Throw(env);
  }

exit:
  Py_XDECREF(py_ret);

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' without arguments */

jobject JcpPyObject_CallMethodNoArgs(JNIEnv *env, intptr_t ptr, const char *obj,
                                     const char *cname) {
  PyObject *self;
  PyObject *name;
  PyObject *py_ret = NULL;

  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

      self = PyDict_GetItemString(jcp_thread->globals, obj);

  if (self) {
    name = PyUnicode_FromString(cname);

#if PY_MINOR_VERSION >= 9
    py_ret = PyObject_CallMethodNoArgs(self, name);
#else
    py_ret = PyObject_CallMethodObjArgs(self, name, NULL);
#endif

    Py_DECREF(name);

    if (!JcpPyErr_Throw(env)) {
      result = JcpPyObject_AsJObject(env, py_ret, JOBJECT_TYPE);
      Py_DECREF(py_ret);
    }
  }

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jboolean argument
 */

jobject JcpPyObject_CallMethodOneJBooleanArg(JNIEnv *env, intptr_t ptr,
                                             const char *obj, const char *name,
                                             jboolean arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_Call_MethodOneArg(env, jcp_thread, obj, name,
                                              JcpPyBool_FromLong(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jint argument */

jobject JcpPyObject_CallMethodOneJIntArg(JNIEnv *env, intptr_t ptr,
                                         const char *obj, const char *name,
                                         jint arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_Call_MethodOneArg(env, jcp_thread, obj, name,
                                              JcpPyInt_FromInt(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jlong argument */

jobject JcpPyObject_CallMethodOneJLongArg(JNIEnv *env, intptr_t ptr,
                                          const char *obj, const char *name,
                                          jlong arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

  result = _JcpPyObject_Call_MethodOneArg(env, jcp_thread, obj, name,
                                              JcpPyInt_FromLong(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jdouble argument
 */

jobject JcpPyObject_CallMethodOneJDoubleArg(JNIEnv *env, intptr_t ptr,
                                            const char *obj, const char *name,
                                            jdouble arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_Call_MethodOneArg(env, jcp_thread, obj, name,
                                              JcpPyFloat_FromDouble(arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jstring argument
 */

jobject JcpPyObject_CallMethodOneJStringArg(JNIEnv *env, intptr_t ptr,
                                            const char *obj, const char *name,
                                            jstring arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_Call_MethodOneArg(
          env, jcp_thread, obj, name, JcpPyString_FromJString(env, arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with only one jobject argument
 */

jobject JcpPyObject_CallMethodOneJObjectArg(JNIEnv *env, intptr_t ptr,
                                            const char *obj, const char *name,
                                            jobject arg) {
  jobject result;

  Jcp_BEGIN_ALLOW_THREADS

      result = _JcpPyObject_Call_MethodOneArg(
          env, jcp_thread, obj, name, JcpPyObject_FromJObject(env, arg));

  Jcp_END_ALLOW_THREADS

      return result;
}

/* Call the method named 'name' of object 'obj' with a variable number of Java
 * arguments */

jobject JcpPyObject_CallMethod(JNIEnv *env, intptr_t ptr, const char *obj,
                               const char *name, jobjectArray args) {
  int arg_len = 0;

  PyObject *callable = NULL;
  PyObject *py_args = NULL;
  PyObject *py_ret = NULL;

  jobject element;
  jobject result = NULL;

  Jcp_BEGIN_ALLOW_THREADS

  if (args != NULL) {
    arg_len = (*env)->GetArrayLength(env, args);
  }

  callable = _JcpPyObjectMethod_Load(jcp_thread, obj, name);

  if (callable == NULL) {
    JcpPyErr_Throw(env);
    goto exit;
  }

  py_args = PyTuple_New(arg_len);

  for (int i = 0; i < arg_len; i++) {
    element = (*env)->GetObjectArrayElement(env, args, i);
    PyTuple_SetItem(py_args, i, JcpPyObject_FromJObject(env, element));
    (*env)->DeleteLocalRef(env, element);
  }

  py_ret = PyObject_Call(callable, py_args, NULL);
  Py_DECREF(py_args);

  if (JcpPyErr_Throw(env) || !py_ret) {
    goto exit;
  }

  result = JcpPyObject_AsJObject(env, py_ret, JOBJECT_TYPE);

  if (!result) {
    JcpPyErr_Throw(env);
  }

exit:
  Py_XDECREF(py_ret);
  Jcp_END_ALLOW_THREADS

      return result;
}

// ----------------------------------------------------------------------------------------

/* Exec python code */

void JcpExec(JNIEnv *env, intptr_t ptr, const char *code) {
  PyObject *result;

  if (code == NULL) {
    return;
  }

  Jcp_BEGIN_ALLOW_THREADS

      result = PyRun_String(code, Py_file_input, jcp_thread->globals,
                            jcp_thread->globals);

  if (result) {
    Py_DECREF(result);
  } else {
    JcpPyErr_Throw(env);
  }

  Jcp_END_ALLOW_THREADS
}
