//
// This file is part of MorphoGraphX - http://www.MorphoGraphX.org
// Copyright (C) 2012-2016 Richard S. Smith and collaborators.
//
// If you use MorphoGraphX in your work, please cite:
//   http://dx.doi.org/10.7554/eLife.05864
//
// MorphoGraphX is free software, and is licensed under under the terms of the 
// GNU General (GPL) Public License version 2.0, http://www.gnu.org/licenses.
// 
#include <PythonProcess.hpp>

#include <Dir.hpp>
#include <Forall.hpp>
#include <Information.hpp>

#include <frameobject.h>
#include <QTextStream>
#include <stddef.h>
#include <stdio.h>
#include <structmember.h>
#include <QFileDialog>

#pragma GCC diagnostic ignored "-Wwrite-strings"

namespace mgx
{
  static PyObject* printFactory(PyObject* self)
  {
    QStringList procs = listProcesses();
    forall(QString& p, procs)
      p = QString("'%1'").arg(p);
  
    QString result = QString("<factory - %1>").arg(procs.join(", "));
    return Py_BuildValue("s", result.toLocal8Bit().data());
  }
  
  static PyObject* showProc(PyObject* self)
  {
    mgx_Process& process = reinterpret_cast<mgx_Process_Python*>(self)->intern;
    QString name = process.name;
    QString result = QString("%1[%2]").arg(name).arg(process.numParms);
    return Py_BuildValue("s", result.toLocal8Bit().data());
  }
  
  static PyObject* runProcess(PyObject* self, PyObject* args, PyObject*)
  {
    mgx_Process& proc = reinterpret_cast<mgx_Process_Python*>(self)->intern;
    if(not proc.process) {
      PyErr_SetString(PyExc_ValueError, "You can't use this process for launching. "
                            "It hasn't been created using of the process repository.");
      return NULL;
    }
    Py_ssize_t s = PyTuple_Size(args);
    int expected = proc.numParms;
    if(s < expected) {
      QString err = (QString("Argument count mismatch for process %1:\n "
      " Expected: %2\n  Provided: %3").arg(proc.name).arg(expected).arg(s));
      PyErr_SetString(PyExc_ValueError, err.toLocal8Bit().data());
      return NULL;
    }
    QStringList parms;
    bool parsing_strings = true;
    for(int i = 0; i < s; ++i) {
      PyObject* obj = PyTuple_GetItem(args, i);
      if(obj) {
        if(PyFloat_Check(obj)) {
          parsing_strings = false;
          float f = (float)PyFloat_AsDouble(obj);
          if(PyErr_Occurred())
            return NULL;
          parms << QString::number(f);
        } else if(PyInt_Check(obj)) {
          parsing_strings = false;
          long l = PyInt_AsLong(obj);
          if(l == -1 and PyErr_Occurred())
            return NULL;
          float f = (float)l;
          parms << QString::number(f);
        } else {
          if(!parsing_strings) {
            PyErr_SetString(PyExc_ValueError, "Error, string arguments should be put first, "
                               "and then floating points ones");
            return NULL;
          }
          PyObject* str = PyObject_Str(obj);
          if(!str)
            return NULL;
          char* val = PyString_AsString(str);
          if(!val)
            return NULL;
          parms << QString::fromLocal8Bit(val);
          Py_DECREF(str);
        }
      } else
        return NULL;
    }
    if(parms.size() < proc.numParms) {
      QString err = 
           QString("Error, the process expect at least %1 strings, and only %2 where given")
        .arg(proc.numParms).arg(parms.size());
      PyErr_SetString(PyExc_ValueError, err.toLocal8Bit().data());
      return NULL;
    }
    if(!proc.process->runProcess(proc.name, parms)) {
      PyErr_SetString(PyExc_RuntimeError, proc.process->errorMessage().toLocal8Bit().data());
      return NULL;
    }
    proc.process->updateState();
    proc.process->updateViewer();
    Py_INCREF(Py_None);
    return Py_None;
  }
  
  static void Factory_dealloc(mgx_Factory_Python* factory)
  {
    Py_XDECREF(factory->fact_dict);
    factory->fact_dict = 0;
    factory->intern.~mgx_Factory();
    factory->ob_type->tp_free((PyObject*)factory);
  }
  
  static void Process_dealloc(mgx_Process_Python* proc)
  {
    Py_XDECREF(proc->descr);
    proc->descr = 0;
    proc->intern.~mgx_Process();
    proc->ob_type->tp_free((PyObject*)proc);
  }
  
  static PyMethodDef module_methods[] = { { 0, 0, 0, 0 } };
  
  static int mgx_Factory_init(mgx_Factory_Python* self, PyObject* args, PyObject*)
  {
    PyObject* f_dict = PyDict_New();
  
    self->fact_dict = f_dict;
    self->intern.process = 0;
  
    return 0;
  }
  
  static int mgx_Process_init(mgx_Process_Python* self, PyObject* args, PyObject*)
  {
    const char* proc_name;
  
    if(!PyArg_ParseTuple(args, "s", &proc_name))
      return -1;
  
    QString name = QString::fromLocal8Bit(proc_name);
    name.replace("__", "/");
    name.replace('_', " ");
  
    ProcessDefinition* def = getProcessDefinition(name);
    if(not def) {
      PyErr_SetString(PyExc_NameError, "No such process");
      return -1;
    }
    self->intern.name = name;
    self->intern.numParms = def->parmNames.size();
    self->intern.process = 0;
  
    QStringList arguments, defaults;
    forall(const QString& arg, def->parmNames)
      arguments << QString("'%1'").arg(arg);
    forall(const QString& str, def->parms)
      defaults << QString("'%1'").arg(str);
  
    QString descr = (QString("%1\nParameters: %2\nDefaults: %3")
                .arg(def->description).arg(arguments.join(", ")).arg(defaults.join(", ")));
    PyObject* doc = PyString_FromString(descr.toLocal8Bit().data());
    self->descr = doc;
  
    return 0;
  }
  
  static PyObject* mgx_Factory_new(PyTypeObject* type, PyObject*, PyObject*)
  {
    mgx_Factory_Python* self;
  
    self = (mgx_Factory_Python*)type->tp_alloc(type, 0);
    if(self != NULL) {
      new (&self->intern)mgx_Factory();
    }
  
    return (PyObject*)self;
  }
  
  static PyObject* mgx_Process_new(PyTypeObject* type, PyObject*, PyObject*)
  {
    mgx_Process_Python* self;
  
    self = (mgx_Process_Python*)type->tp_alloc(type, 0);
    if(self != NULL) {
      new (&self->intern)mgx_Process();
    }
  
    return (PyObject*)self;
  }
  
  static PyObject* fact_getdict(mgx_Factory_Python* self)
  {
    if(self->fact_dict == NULL) {
      self->fact_dict = PyDict_New();
      if(self->fact_dict == NULL)
        return NULL;
    }
    Py_INCREF(self->fact_dict);
    return self->fact_dict;
  }
  
  static PyGetSetDef fact_getsetlist[] = { { "__dict__", (getter)fact_getdict, NULL, "Dictionnary" }, { NULL } };
  
  PyTypeObject factory_type = { PyObject_HEAD_INIT(NULL) 0,                /*ob_size*/
                                "mgx.Factory",                            /*tp_name*/
                                sizeof(mgx_Factory_Python),               /*tp_basicsize*/
                                0,                                        /*tp_itemsize*/
                                (destructor)Factory_dealloc,              /*tp_dealloc*/
                                0,                                        /*tp_print*/
                                0,                                        /*tp_getattr*/
                                0,                                        /*tp_setattr*/
                                0,                                        /*tp_compare*/
                                0,                                        /*tp_repr*/
                                0,                                        /*tp_as_number*/
                                0,                                        /*tp_as_sequence*/
                                0,                                        /*tp_as_mapping*/
                                0,                                        /*tp_hash */
                                0,                                        /*tp_call*/
                                printFactory,                             /*tp_str*/
                                PyObject_GenericGetAttr,                  /*tp_getattro*/
                                PyObject_GenericSetAttr,                  /*tp_setattro*/
                                0,                                        /*tp_as_buffer*/
                                Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
                                "Access to MorphoGraphX processes",       /* tp_doc */
                                0,                                        /* tp_traverse */
                                0,                                        /* tp_clear */
                                0,                                        /* tp_richcompare */
                                0,                                        /* tp_weaklistoffset */
                                0,                                        /* tp_iter */
                                0,                                        /* tp_iternext */
                                0,                                        /* tp_methods */
                                0,                                        /* tp_members */
                                fact_getsetlist,                          /* tp_getset */
                                0,                                        /* tp_base */
                                0,                                        /* tp_dict */
                                0,                                        /* tp_descr_get */
                                0,                                        /* tp_descr_set */
                                offsetof(mgx_Factory_Python, fact_dict),  /* tp_dictoffset */
                                (initproc)mgx_Factory_init,               /* tp_init */
                                0,                                        /* tp_alloc */
                                mgx_Factory_new,                          /* tp_new */
                                0,                                        0, 0, 0, 0, 0, 0, 0, 0 };
  
  static PyMemberDef proc_memberlist[] = { 
     { "doc", T_OBJECT, offsetof(mgx_Process_Python, descr), READONLY, "" },
     { "description", T_OBJECT, offsetof(mgx_Process_Python, descr), READONLY, "" },
     { NULL } };
  
  PyTypeObject process_type = { PyObject_HEAD_INIT(NULL) 0,          /*ob_size*/
                                "mgx.Process",                      /*tp_name*/
                                sizeof(mgx_Process_Python),         /*tp_basicsize*/
                                0,                                  /*tp_itemsize*/
                                (destructor)Process_dealloc,        /*tp_dealloc*/
                                0,                                  /*tp_print*/
                                0,                                  /*tp_getattr*/
                                0,                                  /*tp_setattr*/
                                0,                                  /*tp_compare*/
                                0,                                  /*tp_repr*/
                                0,                                  /*tp_as_number*/
                                0,                                  /*tp_as_sequence*/
                                0,                                  /*tp_as_mapping*/
                                0,                                  /*tp_hash */
                                runProcess,                         /*tp_call*/
                                showProc,                           /*tp_str*/
                                0,                                  /*tp_getattro*/
                                0,                                  /*tp_setattro*/
                                0,                                  /*tp_as_buffer*/
                                Py_TPFLAGS_DEFAULT,                 /*tp_flags*/
                                "Access to MorphoGraphX processes", /* tp_doc */
                                0,                                  /* tp_traverse */
                                0,                                  /* tp_clear */
                                0,                                  /* tp_richcompare */
                                0,                                  /* tp_weaklistoffset */
                                0,                                  /* tp_iter */
                                0,                                  /* tp_iternext */
                                0,                                  /* tp_methods */
                                proc_memberlist,                    /* tp_members */
                                0,                                  /* tp_getset */
                                0,                                  /* tp_base */
                                0,                                  /* tp_dict */
                                0,                                  /* tp_descr_get */
                                0,                                  /* tp_descr_set */
                                0,                                  /* tp_dictoffset */
                                (initproc)mgx_Process_init,         /* tp_init */
                                0,                                  /* tp_alloc */
                                mgx_Process_new,                    /* tp_new */
                                0,                                  0, 0, 0, 0, 0, 0, 0, 0 };
  
  bool PythonProcess::addFactory(char* name)
  {
    PyObject* fact_arg = Py_BuildValue("(s)", name);
    if(!fact_arg) {
      setErrorMessage("Could not create arguments to create factory");
      return false;
    }
    PyObject* pyFactory = PyObject_Call((PyObject*)&factory_type, fact_arg, 0);
    Py_DECREF(fact_arg);
    if(!pyFactory) {
      setErrorMessage(QString("Cannot create %1 factory object").arg(name));
      return false;
    }
    mgx_Factory& factory = reinterpret_cast<mgx_Factory_Python*>(pyFactory)->intern;
    factory.process = this;
    // Get list of processes and add them to the factory
    QStringList processNames = listProcesses();
    forall(QString processName, processNames) {
      mgx_Process_Python* proc = createProcess(processName);
      proc->intern.process = this;
      processName.replace('/', "__");
      processName.replace(' ', "_");
      if(PyObject_SetAttrString(pyFactory, processName.toLocal8Bit().data(), (PyObject*)proc) == -1) {
        PyErr_Print();
        setErrorMessage(QString("Could not set attribute %1 to factory").arg(processName));
        return false;
      }
    }
    // Add the factory to the modules (main and mgx)
    Py_INCREF(pyFactory);
    if(PyModule_AddObject(module, name, pyFactory)) {
      Py_DECREF(pyFactory);
      setErrorMessage(QString("Cannot add %1 factory object to the mgx module").arg(name));
      return false;
    }
    if(PyModule_AddObject(main, name, pyFactory)) {
      Py_DECREF(pyFactory);
      setErrorMessage(QString("Cannot add %1 factory object to the __main__ module").arg(name));
      return false;
    }
    return true;
  }
  
  bool PythonProcess::removeFactory(char* name)
  {
    PyObject* d1 = PyModule_GetDict(module);
    PyObject* d2 = PyModule_GetDict(main);
    PyDict_DelItemString(d1, name);
    PyDict_DelItemString(d2, name);
    return true;
  }
  
  bool PythonProcess::initPython(const QString& filename)
  {
    main = PyImport_AddModule("__main__");
    if(main == NULL) {
      setErrorMessage("Cannot import __main__ module");
      return false;
    }
    main_dict = PyModule_GetDict(main);
    PyObject* f = PyString_FromString(filename.toLocal8Bit().data());
    if(f == NULL) {
      setErrorMessage("Cannot create the Python's string for the file name");
      return false;
    }
    if(PyDict_SetItemString(main_dict, "__file__", f) < 0) {
      Py_DECREF(f);
      setErrorMessage("Cannot set the __main__ module's filename");
      return false;
    }
    Py_DECREF(f);
    if(PyType_Ready(&factory_type) < 0) {
      setErrorMessage("Factory type is not ready");
      return false;
    }
  
    if(PyType_Ready(&process_type) < 0) {
      setErrorMessage("Process type is not ready");
      return false;
    }
  
    module = Py_InitModule3("mgx", module_methods, "Module to access MorphoGraphX processes.");
    if(module == NULL) {
      setErrorMessage("Could not create mgx module");
      return false;
    }
    Py_INCREF(&factory_type);
    PyModule_AddObject(module, "Factory", (PyObject*)&factory_type);
    Py_INCREF(&process_type);
    PyModule_AddObject(module, "Process", (PyObject*)&process_type);
  
    if(!addFactory("Process"))
      return false;
    return true;
  }
  
  bool PythonProcess::finalizePython()
  {
    removeFactory("Process");
    return true;
  }
  
  mgx_Process_Python* PythonProcess::createProcess(const QString& name)
  {
    PyObject* args = Py_BuildValue("(s)", name.toLocal8Bit().data());
    if(!args)
      return NULL;
    PyObject* obj = PyObject_Call((PyObject*)&process_type, args, 0);
    Py_DECREF(args);
    return (mgx_Process_Python*)obj;
  }
  
  QString PythonProcess::getTraceBack(PyObject* tb, int limit)
  {
    QString err;
    PyTracebackObject* tb1 = (PyTracebackObject*)tb;
    int depth = 0;
    while(tb1 != NULL) {
      depth++;
      tb1 = tb1->tb_next;
    }
    tb1 = (PyTracebackObject*)tb;
    while(tb1 != NULL) {
      if(depth <= limit) {
        const char* fn = PyString_AsString(tb1->tb_frame->f_code->co_filename);
        int lo = tb1->tb_lineno;
        const char* co = PyString_AsString(tb1->tb_frame->f_code->co_name);
        err += QString("  File \"%1\", line %2, in %3\n").arg(fn).arg(lo).arg(co);
      }
      depth--;
      tb1 = tb1->tb_next;
    }
    return err;
  }
  
  bool PythonProcess::initialize(QWidget* parent)
  {
    QString filename = parm("Script");

    if(filename.isEmpty() and parent)
      filename = QFileDialog::getOpenFileName(parent, "Choose python script to execute", filename,
                                                    "Python scripts (*.py);;All files (*.*)");
    // check file
    if(filename.isEmpty())
      return false;
    // ok all good
    setParm("Script", filename);
  
    return true;
  }
  
  bool PythonProcess::run(QString filename)
  {
    bool success = false;
    filename = resolvePath(filename);
    FILE* file = fopen(filename.toLocal8Bit().data(), "rt");
    if(not file) {
      setErrorMessage(QString("Cannot open file '%1' for reading").arg(filename));
      return false;
    }
    Py_Initialize();
    do {
      // First, add the __main__ module and set its file name to the file loaded
      if(!initPython(filename))
        break;
      PyObject* res
        = PyRun_FileExFlags(file, filename.toLocal8Bit().data(), 
                                     Py_file_input, main_dict, main_dict, true, 0);
      if(res == NULL) {
        PyObject* exc, *val, *tb;
        PyErr_Fetch(&exc, &val, &tb);
        if(!exc)
          setErrorMessage("Unknown error in Python code");
        else {
          // Now, get the exception string
          QString err;
          PyErr_NormalizeException(&exc, &val, &tb);
          if(tb and tb != Py_None) {
            err += "Traceback (most recent call last):\n";
            err += getTraceBack(tb, 10);
          }
          PyObject* str = PyObject_Str(val);
          if(str) {
            PyObject* exc_str = PyObject_GetAttrString(exc, "__name__");
            char* s_exc = PyString_AsString(exc_str);
            char* s_val = PyString_AsString(str);
            setErrorMessage(err + QString("%1: %2").arg(s_exc).arg(s_val));
            Py_DECREF(exc_str);
            Py_DECREF(str);
          } else
            setErrorMessage("Unknown exception in Python code");
        }
      } else {
        Py_DECREF(res);
        success = true;
      }
    } while(false);
    PyErr_Clear();
    if(PyDict_DelItemString(main_dict, "__file__"))
      PyErr_Clear();
    finalizePython();
    Py_Finalize();

    return success;
  }
  REGISTER_PROCESS(PythonProcess);

}
