/* Copyright (C) 2017 The authors of Py-ChemShell

   This file is part of Py-ChemShell.

   Py-ChemShell is free software: you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as
   published by the Free Software Foundation, either version 3 of the
   License, or (at your option) any later version.

   Py-ChemShell is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with Py-ChemShell.  If not, see
   <http://www.gnu.org/licenses/>.  */


#include "Python.h"
#include <libgen.h>
#include <stdio.h>
#include <string.h>

char *get_arch(), *get_path(), *get_test(), *get_pythonpath(), *get_site_packages();

int chemsh_c_init_py(int argc, char** argv, int debug) {

    int i, ierror = 0;
    int interactive = 1;
    FILE *inputfile;
    char *prefix;
  
    PyObject *chemshMod;
    PyObject *globalsDict;
    PyObject *sanityEx, *runEx, *runTB;
    PyObject *typeObj, *valueObj, *tracebackObj;
    PyObject *ioMod = NULL, *tracebackMod = NULL, *StringIOObj = NULL, *bytesObj = NULL;

    // set the architecture "chemsh.CHEMSH_ARCH" for which ChemShell is built
    // get_arch() is generated at compile time, see chemsh/app/CMakeLists.txt
    const char *chemsh_arch = (char *) get_arch();
    const char *chemsh_path = (char *) get_path();
    const char *chemsh_test = (char *) get_test();
    char *site_packages = (char *) get_site_packages();
    char *pythonpath    = (char *) get_pythonpath();

    const char delimiter[2] = ":";
    const char *cempty = "";
    char *token, *cbuff, *sbuff;
    
    // start python interpreter
    Py_Initialize();

    // YL 30/10/2018: we append the current ChemShell path to sys.path instead of setting PYTHONPATH which is not a good practice
    PyObject *sysMod  = PyImport_ImportModule("sys");
    PyObject *pathList = PyObject_GetAttrString(sysMod, "path");
    PyObject *stdoutObj = PyObject_GetAttrString(sysMod, "stdout");
    PyList_Append(pathList, PyUnicode_FromString(chemsh_path));

    // YL 21/02/2023: now reversed the order and as a result sys.path contains:
    //                [ '.', [PREPENDEDPATH,] on-site ($PYTHONUSERBASE), $PYTHONPATH ]

    // YL 26/02/2021: enabled additional compiletime PYTHONPATH to be added
    //                to sys.path; multiple paths are separated by ":"
    token = strtok(pythonpath, delimiter);
    while(token) {
        PyList_Insert(pathList, 0, PyUnicode_FromString(token));
        token = strtok(NULL, delimiter);
    }

    // YL 19/03/2021: on-site PYTHONPATH for dependencies automatically installed at compiletime
    PyList_Insert(pathList, 0, PyUnicode_FromString(site_packages));

    // PyImport_AddModule() does NOT load/import module (namely, __init__.py is not run); do NOT use PyImport_ImportModule
    chemshMod = PyImport_AddModule("chemsh");
    PyModule_AddStringConstant(chemshMod, "CHEMSH_ARCH", chemsh_arch);

    // add debugging level
    PyModule_AddIntConstant(chemshMod, "CHEMSH_DEBUG", debug);

    // reload to actually load/import: this calls __init__.py
    PyImport_ReloadModule(chemshMod);

    // run a sanity check
    inputfile = fopen(chemsh_test, "r");
    if(inputfile) {
        // YL 10/03/2021: we now can print traceback if the sanity check fails
        PyErr_Print();
        ierror = PyRun_SimpleFile(inputfile, chemsh_test);
        if(ierror) {
            printf("\n >>> ERROR: ChemShell cannot pass the sanity check.\n\n");
            PyErr_Print();
            fflush(stdout);
            fflush(stderr);
            return 999;
        }
        fclose(inputfile);
    }

    if(argc) {

        // loop over command line arguments to deal with --prepend-pythonpath
        // YL 11/03/2021: this step must be separated to avoid adding path repeatedly
        for(i=1; i<argc; i++) {

            // YL 27/02/2021: enabled additional runtime sys.path;
            //                multiple paths are separated by ":"
            if(strcmp(argv[i], "--prepend-pythonpath") == 0) {
                token = strtok(argv[i+1], delimiter);
                while(token) {
                    PyList_Insert(pathList, 0, PyUnicode_FromString(token));
                    token = strtok(NULL, delimiter);
                }
            }
        }

        // YL 18/02/2021: we enabled importing within the working directory so that other
        //                Python files under the same folder can be connveniently used.
        //                we assume when importing external files the user knows what s/he
        //                is doing because built-in ChemShell might be overridden
        int size_syspath = PyList_Size(pathList);
        int cwd_in_syspath = 0;
        // YL 11/03/2021: loop over sys.path to check if "" is already added (it shouldn't!), if not we prepend a ""...
        for(i=0; i<size_syspath; i++) {
            PyObject *path_j = PyList_GetItem(pathList, i);
            if(PyArg_Parse(path_j, "s", &cbuff)) {
                // YL 11/03/2021: do not use strcmp to check empty string!
                if(!*cbuff) {
                    cwd_in_syspath = 1;
                    break;
                }
            }
        }
        // ...because just an empty string "" means the current working directory (where the script is being executed)
        if(!cwd_in_syspath) {
            PyList_Insert(pathList, 0, PyUnicode_FromString(cempty));
        }

        PyErr_Clear();
        // now loop over command line arguments to execute input script(s)
        for(i=1; i<argc; i++) {
            // YL 11/03/2021: use "r+" (read/write) rather than "r" because otherwise fopen doesn't distinguish a directory, for example when `--prepend-pythonpath /home` is used;
            //                but don't worry! it won't change the timestamp
            inputfile = fopen(argv[i], "r+");
            if(inputfile) {
                // run
                globalsDict = PyModule_GetDict(chemshMod);
                // Py_file_input is a Python built-in constant
                runEx = PyRun_File(inputfile, argv[i], Py_file_input, globalsDict, globalsDict);
                // stdout.flush()
                PyObject_CallMethod(stdoutObj, (const char*) "flush", NULL);
                if(!runEx) {
                    printf("\n >>> chemsh.x ERROR: Failed running %s\n", argv[i]);
                    if(PyErr_Occurred()) {

                        // YL 01/07/2021: doesn't work strangely; commented out otherwise segfaults below
//                        PyErr_Print();

                        // YL 01/07/2021: let's fix the issue that tracebacks are not printed by PyErr_Print()
                        // fetch the traceback object
                        PyErr_Fetch(&typeObj, &valueObj, &tracebackObj);
                        PyErr_NormalizeException(&typeObj, &valueObj, &tracebackObj);

                        // import `io.StringIO` and create an instance
                        ioMod = PyImport_ImportModule("io");
                        if(!ioMod) {
                            printf("\nchemsh_c_init_py: failed importing io\n");
                        }
                        StringIOObj = PyObject_CallMethod(ioMod, (const char*) "StringIO", NULL);
                        if(!StringIOObj) {
                            printf("\nchemsh_c_init_py: failed ctreating StringIO()\n");
                        }

                        // import `traceback`
                        tracebackMod = PyImport_ImportModule( "traceback" );
                        if(!tracebackMod) {
                            printf("\nchemsh_c_init_py: failed importing traceback\n");
                        }

                        // call `traceback.print_exception()` and save output to `StringIOObj`
                        bytesObj = PyObject_CallMethod( tracebackMod, (const char*) "print_exception",
                                                                 (const char*) "OOOOO",
                                                                  typeObj,
                                                                  valueObj ? valueObj : Py_None,
                                                                  tracebackObj ? tracebackObj : Py_None,
                                                                  Py_None,
                                                                  StringIOObj);
                        if(!bytesObj) {
                            printf("\nchemsh_c_init_py: failed calling traceback.print_exception()\n");
                        }

                        // fetch the Bytes value via StringIOObj.getvalue()
                        Py_DECREF(bytesObj);
                        bytesObj = PyObject_CallMethod(StringIOObj, (const char*) "getvalue", NULL);
                        if(!bytesObj) {
                            printf("\nchemsh_c_init_py: failed calling StringIO.getvalue()\n");
                        }

                        // print the traceback
                        printf("\n%s\n", PyUnicode_AsUTF8(PyObject_Str(bytesObj)));
                        // i don't know why PyObject_Bytes doesn't work but PyObject_Str does because getvalue should return a Bytes value
//                        sbuff = PyBytes_AsString(PyObject_Bytes(bytesObj));

                        Py_DECREF(ioMod);
                        Py_DECREF(tracebackMod);
                        Py_DECREF(StringIOObj);
                        Py_DECREF(bytesObj);

                        fflush(stdout);

                    }
                    return 112;
                } else {
                    // successful
                    
                }
                Py_DECREF(runEx);
                PyErr_Clear();
//                ierror = PyRun_SimpleFile(inputfile, argv[i]);
//                // YL 10/03/2021: with this we can print traceback if the run fails
//                if(PyErr_Occurred()) {
// YL 01/07/2021: PyErr_Print just doesn't work
//                    PyErr_Print();
//                }
//                PyErr_Print();
//                if(ierror) {
//                    return 112;
//                }
                fclose(inputfile);
                interactive = 0;
            }
        }
    }

    // YL 08/11/2018: enabled triggering the Python shell mode with arguments but no input script given
    if(interactive) {

        // YL 24/03/2021: this enables using arrow keys in the interactive mode
        if(!PyImport_ImportModule("readline")) {
            printf("\n>>> ChemShell: Python standard library `readline` not available\n");
            PyErr_Clear();
        }

        if(debug>3) {
            printf("\n Entering interactive Python shell...\n\n");
        }
        /* interactive mode */
        ierror = PyRun_InteractiveLoop(stdin, "-");
        if(debug>=5) {
            printf("\n\n Interactive Python shell exit code: %d\n", ierror);
        }
    }
  
    // shut down interpreter
    Py_DECREF(chemshMod);
    Py_Finalize();
  
    /* PyRun_SimpleFile returns -1 on error. We return 1
       to be consistent with error return value from the
       standard python interpreter */
    if(ierror != 0) {
        return 1;
    } else {
        return 0;
    }
}





// YL 10/03/2021: here is the method of calling PyRun_File() but i find only traceback
//                from ChemShell is printed, not from the input script somehow
//                (so it's not helpful for the user to debug)
//globalsDict = PyModule_GetDict(chemshMod);
//// Py_file_input is a Python built-in constant
//runEx = PyRun_File(inputfile, argv[i], Py_file_input, globalsDict, globalsDict);
//if(!runEx) {
//    printf("\n >>> ERROR: Failed running %s.\n\n", argv[i]);
//    return 112;
//} else {
//    // do something with the traceback (not demonstrated here)
//    PyObject* runTB = PyException_GetTraceback(runEx);
//}
//Py_DECREF(runEx);

// YL 10/03/2021: an example of adding an absolute path
//                char *abspath = realpath(argv[i], NULL);
//                // make a copy because dirname() will change the value of abspath (just in case for future)
//                char *cbuff   = strdup(abspath);
//                char *dir     = dirname(cbuff);
