/*
python-javabridge is licensed under the BSD license.  See the
accompanying file LICENSE for details.

Copyright (c) 2003-2009 Massachusetts Institute of Technology
Copyright (c) 2009-2013 Broad Institute
All rights reserved.
*/

#include <jni.h>
#include <stdio.h>
#include <Python.h>
#ifdef __linux__
#include <dlfcn.h>
#endif
#include "org_cellprofiler_javabridge_CPython.h"

int initialized = 0;

#ifdef __linux__
/*
 * On Linux, it appears that Python's symbols cannot be found by other
 * native libraries if we let the JVM load libpython... so we have to load it
 * explicitly, with the correct flag (RTLD_GLOBAL).
 */

static char *get_property(JavaVM *vm, const char *key)
{
	JNIEnv *pEnv;
	jclass system;
	jmethodID get;
	jstring string;
	const char *characters;
	char *result;

	if ((*vm)->GetEnv(vm, (void **)&pEnv, JNI_VERSION_1_2) != JNI_OK) {
		fprintf(stderr, "Could not obtain JNI environment\n");
		return NULL;
	}

	if (!(system = (*pEnv)->FindClass(pEnv, "java/lang/System"))) {
		fprintf(stderr, "Could not access System class\n");
		return NULL;
	}

	if (!(get = (*pEnv)->GetStaticMethodID(pEnv, system, "getProperty",
			"(Ljava/lang/String;)Ljava/lang/String;"))) {
		fprintf(stderr, "Could not find getProperty method\n");
		return NULL;
	}

	if (!(string = (jstring)(*pEnv)->CallStaticObjectMethod(pEnv, system,
			get, (*pEnv)->NewStringUTF(pEnv, key))))
		return NULL;

	characters = (*pEnv)->GetStringUTFChars(pEnv, string, NULL);
	result = strdup(characters);
	(*pEnv)->ReleaseStringUTFChars(pEnv, string, characters);

	(*vm)->DetachCurrentThread(vm);

	return result;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
	const char *python_location = get_property(vm, "python.location");

	if (!python_location) {
		if ((initialized == 0) && ! Py_IsInitialized()) {
			Py_Initialize();
			PyObject* result = PyRun_String("from distutils.sysconfig import get_config_var; from os.path import join; join(get_config_var(\"LIBDIR\"), get_config_var(\"LDLIBRARY\"))\n", Py_single_input, NULL, NULL);
			python_location = PyString_AsString(result);
			initialized = 1;
		}
	}
	if (!dlopen(python_location, RTLD_LAZY | RTLD_GLOBAL))
		fprintf(stderr, "Warning: Error loading %s\n", python_location);
	return JNI_VERSION_1_2;
}
#endif

#ifdef _WIN32
/*
 * If MSVC90.dll is on the path, we will perish horribly in Windows
 * with an R6034 exception on loading it from some of the numerous
 * pyd files that include it.
 */
const char *pCleanPath = "import os;os.environ['path']=';'.join([path for path in os.environ['path'].split(';') if 'msvcr90.dll' not in map((lambda x:x.lower()), os.listdir(path))])";
#endif
static void check_init() {
    if ((initialized == 0) && ! Py_IsInitialized()) {
        Py_Initialize();
        #ifdef _WIN32
        PyRun_SimpleString(pCleanPath);
        #endif
        initialized = 1;
    }
}

/*
   throwError
   
   Throw an error that indicates a problem beyond what would be caused
   by the evaluation or execution of Python code
   
   pEnv - JNI environment
   message - message to report
*/
static void throwError(JNIEnv *pEnv, char *message) {
    jclass exClass;
    char *className = "java/lang/Error";

    exClass = (*pEnv)->FindClass(pEnv, className);
    if (exClass == NULL) {
        return;
    }
    (*pEnv)->ThrowNew(pEnv, exClass, message);
    PyErr_Clear();
}

/*
    throwWrappedError
    
    Throw an exception that reflects the current Python exception into
    Java.
*/
static void throwWrappedError(JNIEnv *pEnv, int linenumber)
{
    char buffer[1000];
    /* TODO: implement*/
    PyErr_Print();
    snprintf(buffer, sizeof(buffer), 
             "Python exception at %s:%d", __FILE__, linenumber);
    throwError(pEnv, buffer);
}
/*
   attach_env
   
   Attach the supplied environment to the javabridge thread-local context.
   
   pEnv - the environment passed via the JNI call
   
   Prerequisites: The GIL must be taken.
   
   Returns: 0 if successful, negative value on failure indicating that a
            Java exception has been thrown.
*/
static int attach_env(JNIEnv *pEnv){
    PyObject *pPyEnv;
    PyObject *pJavabridge;
    PyObject *pJNIEnter;
    PyObject *pArgs;
    PyObject *pResult;
    /*
    Equivalent to:
    import javabridge
    javabridge.jni_enter(env)
    */
    
    pPyEnv = PyCapsule_New((void *)pEnv, NULL, NULL);
    if (PyErr_Occurred()) {
        throwWrappedError(pEnv, __LINE__);
        return -1;
    }
    pJavabridge = PyImport_ImportModule("javabridge");
    if (PyErr_Occurred()) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pPyEnv);
        return -1;
    }
    pJNIEnter = PyObject_GetAttrString(pJavabridge, "jni_enter");
    if (PyErr_Occurred()) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pJavabridge);
        Py_DECREF(pPyEnv);
        return -1;
    }
    pArgs = PyTuple_Pack(1, pPyEnv);
    if (! pArgs) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pJNIEnter);
        Py_DECREF(pJavabridge);
        Py_DECREF(pPyEnv);
        return -1;
    }
    pResult = PyObject_CallObject(pJNIEnter, pArgs);
    if (! pResult) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pArgs);
        Py_DECREF(pJNIEnter);
        Py_DECREF(pJavabridge);
        Py_DECREF(pPyEnv);
        return -1;
    }
    Py_DECREF(pResult);
    Py_DECREF(pArgs);
    Py_DECREF(pJNIEnter);
    Py_DECREF(pJavabridge);
    Py_DECREF(pPyEnv);
    return 0;    
}

/*
     detach_env
     
     Detach an environment previously attached using attach_env
*/
static int detach_env(JNIEnv *pEnv) {
    PyObject *pJavabridge;
    PyObject *pJNIExit;
    PyObject *pArgs;
    PyObject *pResult;

    pJavabridge = PyImport_ImportModule("javabridge");
    if (! pJavabridge) {
        throwWrappedError(pEnv, __LINE__);
        return -1;
    }
    pJNIExit = PyObject_GetAttrString(pJavabridge, "jni_exit");
    if (! pJNIExit) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pJavabridge);
        return -1;
    }
    pArgs = PyTuple_New(0);
    if (! pArgs) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pJNIExit);
        Py_DECREF(pJavabridge);
    }
    pResult = PyObject_CallObject(pJNIExit, pArgs);
    if (! pResult) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pArgs);
        Py_DECREF(pJNIExit);
        Py_DECREF(pJavabridge);
        return -1;
    }
    Py_DECREF(pResult);
    Py_DECREF(pArgs);   
    Py_DECREF(pJNIExit);
    Py_DECREF(pJavabridge);
    return 0;
}

static PyObject *wrapJObject(JNIEnv *pEnv, jobject j) {
    PyObject *pJavabridge;
    PyObject *pGetEnv;
    PyObject *pTheEnv;
    PyObject *pCapsule;
    PyObject *pResult;
    
    if (! j) {
        Py_RETURN_NONE;
    }
    pJavabridge = PyImport_ImportModule("javabridge");
    if (! pJavabridge) {
        throwWrappedError(pEnv, __LINE__);
        return NULL;
    }
    pGetEnv = PyObject_GetAttrString(pJavabridge, "get_env");
    if (! pGetEnv) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pJavabridge);
        return NULL;
    }
    pTheEnv = PyObject_CallObject(pGetEnv, NULL);
    if (! pTheEnv) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pGetEnv);
        Py_DECREF(pJavabridge);
        return NULL;
    }
    pCapsule = PyCapsule_New((void *)j, NULL, NULL);
    if (! pCapsule) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pTheEnv);
        Py_DECREF(pGetEnv);
        Py_DECREF(pJavabridge);
        return NULL;
    }
    pResult = PyObject_CallMethod(pTheEnv, "make_jb_object", "O", pCapsule);
    if (! pResult) {
        throwWrappedError(pEnv, __LINE__);
    }
    Py_DECREF(pCapsule);
    Py_DECREF(pTheEnv);
    Py_DECREF(pGetEnv);
    Py_DECREF(pJavabridge);
    return pResult;    
}

static PyObject *mapToDictionary(JNIEnv *pEnv, jobject map) {
    PyObject *pMap;
    PyObject *pJavabridge;
    PyObject *pFn;
    PyObject *pArgs;
    PyObject *pResult;
    
    if (! map) {
        pResult = PyDict_New();
        if (! pResult) {
            throwWrappedError(pEnv, __LINE__);
        }
        return pResult;
    }
    pMap = wrapJObject(pEnv, map);
    if (! pMap) {
        return NULL;
    }
    pJavabridge = PyImport_ImportModule("javabridge.jutil");
    if (! pJavabridge) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pMap);
        return NULL;
    }
    pFn = PyObject_GetAttrString(pJavabridge, "make_run_dictionary");
    Py_DECREF(pJavabridge);
    if (! pFn) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pMap);
        return NULL;
    }
    pArgs = PyTuple_Pack(1, pMap);
    Py_DECREF(pMap);
    if (! pArgs) {
        throwWrappedError(pEnv, __LINE__);
        Py_DECREF(pFn);
        return NULL;
    }
    pResult = PyObject_CallObject(pFn, pArgs);
    Py_DECREF(pFn);
    Py_DECREF(pArgs);
    if (! pResult){
        throwWrappedError(pEnv, __LINE__);
        return NULL;
    }
    return pResult;
}

/*
 * Add globals from __main__ to a dictionary of them.
 */
static int add_globals(JNIEnv *pEnv, PyObject *pGlobals) {
    PyObject *pMain;
    PyObject *pMainDict;
    int result;
    pMain = PyImport_AddModule("__main__");
    if (! pMain) {
        throwWrappedError(pEnv, __LINE__);
        return -1;
    }
    pMainDict = PyModule_GetDict(pMain);
    result = PyDict_Merge(pGlobals, pMainDict, 0);
    if (result) {
        throwWrappedError(pEnv, __LINE__);
    }
    return result;
}

JNIEXPORT void JNICALL Java_org_cellprofiler_javabridge_CPython_exec
  (JNIEnv *pEnv, jobject thiss, jstring script, jobject locals, jobject globals) {
    PyGILState_STATE state;
    PyObject *pLocals;
    PyObject *pGlobals;
    const char *pScript;
    PyObject *pResult;
    
    if (! pEnv) {
        throwError(pEnv, "JNIEnv was null.");
        return;
    }
    if (! script) {
        throwError(pEnv, "Script was null.");
        return;
    }
    check_init();
    state = PyGILState_Ensure();
    if (attach_env(pEnv) == 0) {
        pLocals = mapToDictionary(pEnv, locals);
        if (pLocals) {
            if ((locals != NULL) && 
                ((*pEnv)->IsSameObject(pEnv, locals, globals))) {
                pGlobals = pLocals;
                Py_INCREF(pGlobals);
            } else {
                pGlobals = mapToDictionary(pEnv, globals);
            }
            if (pGlobals) {
                if (! add_globals(pEnv, pGlobals)) {
                    pScript = (*pEnv)->GetStringUTFChars(pEnv, script, NULL);
                    pResult = PyRun_String(
                        pScript, Py_file_input, pGlobals, pLocals);
                    (*pEnv)->ReleaseStringUTFChars(pEnv, script, NULL);
                    if (pResult) {
                        Py_DECREF(pResult);
                    } else {
                        throwWrappedError(pEnv, __LINE__);
                    }
                }
                Py_DECREF(pGlobals);
            }
            Py_DECREF(pLocals);
        }
    }
    detach_env(pEnv);
    PyGILState_Release(state);
}
