/* 
 * Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "grtpp_module_python.h"
#include "grtpp_util.h"
#include "base/file_utilities.h"
#include "base/string_utilities.h"
#include "base/file_functions.h"
#include "base/log.h"

DEFAULT_LOG_DOMAIN("ModulePython");

using namespace grt;
using namespace base;

PythonModule::PythonModule(PythonModuleLoader *loader, PyObject *module)
: Module(loader), _module(module)
{
}


PythonModule::~PythonModule()
{
  Py_XDECREF(_module);
}


static TypeSpec parse_type(PyObject *type)
{
  if (PyString_Check(type))
  {
    TypeSpec s;
    s.base.type= str_to_type(PyString_AsString(type));
    return s;
  }
  PyErr_Clear();
  
  if (PyTuple_Check(type))
  {
    TypeSpec s;
    PyObject *b, *c;
    
    if (PyTuple_Size(type) != 2)
    {
      PythonContext::log_python_error("Invalid type specification for Python module function. Tuple must be in form (<container type>, <content type>)");
      throw std::runtime_error("Invalid type specification. Tuple must be in form (<container type>, <content type>)");
    }
    
    b= PyTuple_GetItem(type, 0);
    if (!b)
    {
      PythonContext::log_python_error("Invalid type specification for Python module function");
      throw std::runtime_error("Invalid type specification 0");
    }
    c= PyTuple_GetItem(type, 1);
    if (!c)
    {
      PythonContext::log_python_error("Invalid type specification for Python module function");
      throw std::runtime_error("Invalid type specification 1");
    }

    s.base.type= str_to_type(PyString_AsString(b));
    if (s.base.type == grt::ObjectType)
    {
      if (PyString_Check(c))
        s.base.object_class= PyString_AsString(c);
      else
        throw std::runtime_error("Invalid object type specification");
    }
    else
    {
      if (PyString_Check(c))
        s.content.type= str_to_type(PyString_AsString(c));
      else if (PyTuple_Check(c) && PyTuple_Size(c) == 2)
      {
        s.content.type= grt::ObjectType;
        s.content.object_class= PyString_AsString(PyTuple_GetItem(c, 1));
      }
      else
        throw std::runtime_error("Invalid type specification");
    }
    return s;
  }
  PyErr_Clear();
  
  throw std::runtime_error("Invalid type specification");
}


void PythonModule::add_parse_function(const std::string &name, PyObject *return_type, PyObject *arguments, PyObject *callable)
{
  Function func;
  
  func.name= name;
  try
  {
    func.ret_type= parse_type(return_type);
  }
  catch (std::exception &exc)
  {
    log_error("Invalid return type specification in %s.%s: %s\n", _name.c_str(), name.c_str(), exc.what());
    throw std::runtime_error(strfmt("Invalid return type specification in %s.%s: %s", _name.c_str(), name.c_str(), exc.what()));
  }
  
  for (Py_ssize_t c= PyList_Size(arguments), i= 0; i < c; i++)
  {
    PyObject *spec= PyList_GetItem(arguments, i);
    ArgSpec arg;
    PyObject *tmp;
    
    if (!PyTuple_Check(spec))
    {
      PythonContext::log_python_error("Invalid argument specification for Python module function (not a tuple)");
      throw std::runtime_error("Invalid argument specification (argument spec must be list of tuples)");
    }
  
    tmp= PyTuple_GetItem(spec, 0);
    if (!tmp || !PyString_Check(tmp))
    {
      PythonContext::log_python_error("Invalid argument name specification for Python module function");
      throw std::runtime_error("Invalid argument name specification");
    }
    arg.name= PyString_AsString(tmp);

    tmp= PyTuple_GetItem(spec, 1);
    if (!tmp)
    {
      PythonContext::log_python_error("Invalid argument type specification for Python module function");
      throw std::runtime_error("Invalid argument type specification");
    }
    try
    {
      arg.type= parse_type(tmp);
    }
    catch (std::exception &exc)
    {
      log_error("Invalid argument type specification in %s.%s: %s\n", _name.c_str(), name.c_str(), exc.what());
      throw std::runtime_error(strfmt("Invalid argument type specification in %s.%s", _name.c_str(), name.c_str()));
    }
    func.arg_types.push_back(arg);
  }
  
  PyObject *doc = PyObject_GetAttrString(callable, "__doc__");
  if (doc && doc != Py_None)
    func.description = PyString_AsString(doc);
  
  func.call= boost::bind(&PythonModule::call_function, this, _1, callable, func);
  
  add_function(func);
}


static std::string exception_detail()
{
  PyObject *exc_class = NULL, *exc = NULL, *exc_tb = NULL;
  PyErr_Fetch(&exc_class, &exc, &exc_tb);
  if (exc)
  {
    PyObject *str = PyObject_Str(exc);
    if (str)
    {
      char *s = PyString_AsString(str);
      if (s)
        return s;
      Py_DECREF(str);
    }
    Py_DECREF(exc);
  }
  Py_XDECREF(exc_class);
  Py_XDECREF(exc_tb);
  return "";
}

ValueRef PythonModule::call_function(const BaseListRef &args, PyObject *function, const Function &funcdef)
{
  WillEnterPython lock;

  PythonContext *ctx= ((PythonModuleLoader*)get_loader())->get_python_context();
  PyObject *argtuple;
    
  if (args.is_valid())
  {
    argtuple= PyTuple_New(args.count());
    int i= 0;
    
    // convert arguments to a tuple that can be passed to the function
    for (BaseListRef::raw_const_iterator iter= args.begin(); iter != args.end(); ++iter)
      PyTuple_SetItem(argtuple, i++, ctx->from_grt(*iter));
  }
  else
  {
    argtuple= PyTuple_New(0);
  }
  
  // call the function
  PyObject *ret= PyObject_Call(function, argtuple, NULL);
  Py_DECREF(argtuple);

  if (!ret || PyErr_Occurred())
  {
    if (PyErr_ExceptionMatches(ctx->user_interrupted_error()))
    {
      std::string what = exception_detail();
      if (what.empty())
        what = "Interrupted by user";
      
      throw grt::user_cancelled(what);
    }
    if (PyErr_ExceptionMatches(ctx->db_access_denied_error()))
    {
      std::string what = exception_detail();
      if (what.empty())
        what = "Access denied";
      throw grt::db_access_denied(what);
    }
    if (PyErr_ExceptionMatches(ctx->db_login_error()))
    {
      std::string what = exception_detail();
      if (what.empty())
        what = "Login error, check username and password";
      throw grt::db_login_error(what);
    }
    PyObject *etype;
    PyObject *evalue;
    PyObject *etrace;
    PyErr_Fetch(&etype, &evalue, &etrace);

    PyObject *s = PyObject_GetAttrString(etype, "__name__");
    std::string ename, exc;
    if (!ctx->pystring_to_string(s, ename))
      ename = "???";
    s = PyObject_Str(evalue);
    if (!ctx->pystring_to_string(s, exc))
      exc = "???";
    if (s)
    {
      // Explicit braces here as Py_DECREF is a macro with an if/else part.
      Py_DECREF(s);
    }

    PyErr_Restore(etype, evalue, etrace);
    PythonContext::log_python_error(strfmt("error calling %s.%s", name().c_str(), funcdef.name.c_str()).c_str());
    throw grt::module_error(strfmt("error calling Python module function %s.%s", name().c_str(), funcdef.name.c_str()),
                            strfmt("%s(\"%s\")", ename.c_str(), exc.c_str()));
  }

  ValueRef result= ctx->from_pyobject(ret, funcdef.ret_type);
  
  Py_XDECREF(ret);
  
  return result;
}






PythonModuleLoader::PythonModuleLoader(GRT *grt, const std::string &module_path)
: ModuleLoader(grt), _pycontext(grt, module_path)
{
}


PythonModuleLoader::~PythonModuleLoader()
{
}


Module *PythonModuleLoader::init_module(const std::string &path)
{
  PyObject *mod;
  std::string name;
  
  WillEnterPython lock;
  
  if (path.rfind('.') != std::string::npos)
    name= path.substr(0, path.rfind('.')); // strip extension
  else
    name= path;
  
  name = base::basename(name);
  
  {
    PyObject *old_path, *sysmod, *path_list;
    
    // temporarily add the file's path to the module lookup pat
    sysmod= PyImport_AddModule("sys");
  
    path_list= PyDict_GetItemString(PyModule_GetDict(sysmod), "path");
    old_path= PyList_GetSlice(path_list, 0, PyList_Size(path_list));
    {
      PyObject *tmp= PyString_FromString(base::dirname(path).c_str());
      PyList_Append(path_list, tmp);
      Py_DECREF(tmp);
    }
    // import module
    mod= PyImport_ImportModule((char*)name.c_str());
    
    // restore path
    PyDict_SetItemString(PyModule_GetDict(sysmod), "path", old_path);
    Py_DECREF(old_path);
    if (mod == NULL)
    {
      PythonContext::log_python_error(base::strfmt("Error importing Python module %s\n", path.c_str()).c_str());
      return 0;
    }
  }

  {
    PyObject *module_dict= PyModule_GetDict(mod);
    PyObject *moduleInfo= NULL;

    moduleInfo= PyDict_GetItemString(module_dict, "ModuleInfo");
    if (!moduleInfo)
    {
      PyErr_Print();
      return 0;
    }
    if (!PyDict_Check(moduleInfo))
    {
      Py_XDECREF(moduleInfo);
      PyErr_Clear();
      throw grt::module_error("ModuleInfo is not an object");
    }
    
    grt::PythonModule *module= new grt::PythonModule(this, mod);

    module->_path= path;
    {
      PyObject *name;
      name= PyObject_GetAttrString(moduleInfo, "name");
      if (name && PyString_Check(name))
        module->_name= PyString_AsString(name);
      else
      {
        PyErr_Print();
        Py_XDECREF(moduleInfo);
        delete module;
        throw grt::module_error("ModuleInfo incorrectly defined (name attribute missing)");
      }
    }
    
    
    PyObject *functions= PyObject_GetAttrString(moduleInfo, "functions");
    if (functions && PyList_Check(functions))
    {
      for (Py_ssize_t c= PyList_Size(functions), i= 0; i < c; i++)
      {
        PyObject *item= PyList_GetItem(functions, i);
        const char *name= 0;
        PyObject *rettype;
        PyObject *argtypes;
        PyObject *callable;

        if (!PyArg_ParseTuple(item, "z(OO!)O!", &name, &rettype, &PyList_Type, &argtypes, &PyFunction_Type, &callable))
        {
          PySys_WriteStderr("ERROR: Invalid module function specification in %s\n",
                            path.c_str());
          PyErr_Print();
          PyObject *tmp= PyTuple_GetItem(item, 0);
          if (tmp && PyString_Check(tmp))
          {
            PySys_WriteStderr("  for function %s.%s\n",
                              module->_name.c_str(), PyString_AsString(tmp));
          }
          PyErr_Clear();
          delete module;
          return 0;
        }

        try
        {
          module->add_parse_function(name?name:"", rettype, argtypes, callable);
        }
        catch (std::exception &exc)
        {
          delete module;
          throw grt::module_error(strfmt("Error registering Python module function: %s", exc.what()));
        }
      }
    }
    else
      PyErr_Print();

    PyObject *implements= PyObject_GetAttrString(moduleInfo, "implements");
    if (!implements || !PyList_Check(implements))
    {
      PyErr_Print();
      delete module;
      throw grt::module_error("Invalid value for 'implements' list");
    }
    
    for (Py_ssize_t c= PyList_Size(implements), i= 0; i < c; i++)
    {
      PyObject *name= PyList_GetItem(implements, i);
      if (!PyString_Check(name))
      {
        PyErr_Print();
        delete module;
        throw grt::module_error("Invalid value for 'implements' list");
      }
      
      module->_interfaces.push_back(PyString_AsString(name));
    }
    
    PyObject *meta;

    meta= PyObject_GetAttrString(moduleInfo, "author");
    if (meta && PyString_Check(meta))
      module->_meta_author= PyString_AsString(meta);
    else
      PyErr_Print();
    
    meta= PyObject_GetAttrString(moduleInfo, "version");
    if (meta && PyString_Check(meta))
      module->_meta_version= PyString_AsString(meta);
    else
      PyErr_Print();
    
    meta= PyObject_GetAttrString(moduleInfo, "description");
    if (meta && PyString_Check(meta))
      module->_meta_description= PyString_AsString(meta);
    else
      PyErr_Print();
    
    {
      if (g_str_has_suffix(base::dirname(path).c_str(), ".mwbplugin"))
        module->_is_bundle= true;
    }
    return module;
  }
  
  return 0;
}


void PythonModuleLoader::refresh()
{
  _pycontext.refresh();
}


void PythonModuleLoader::add_module_dir(const std::string &dirpath)
{
  WillEnterPython lock;

  PyObject *sysmod, *path_list;
  PyObject *path= PyString_FromString(dirpath.c_str());
  
  sysmod= PyImport_AddModule("sys");

  path_list= PyDict_GetItemString(PyModule_GetDict(sysmod), "path");
  Py_ssize_t i;

  // check if the path is already in it
  for (i= PyList_Size(path_list)-1; i >= 0; --i)
  {
    if (PyObject_Compare(PyList_GetItem(path_list, i), path) == 0)
      break;
  }

  if (i < 0) // not found
    PyList_Append(path_list, path);
  
  Py_DECREF(path);
}


bool PythonModuleLoader::load_library(const std::string &file)
{
  // add the path to the search path so that it can be imported
  {
    add_module_dir(base::dirname(file));
  }

  return true;
}


bool PythonModuleLoader::run_script_file(const std::string &path)
{
  if (!g_file_test(path.c_str(), G_FILE_TEST_EXISTS))
    return false;

  WillEnterPython lock;
  
  // Execute in the global environment will make it available to the interactive shell.
  return _pycontext.run_file(path, true) == 0;
}

bool PythonModuleLoader::run_script(const std::string &script)
{
  return _pycontext.run_buffer(script, 0) == 0;
}

bool PythonModuleLoader::check_file_extension(const std::string &path)
{
  if (g_str_has_suffix(path.c_str(), ".py"))
    return true;

  return false;
}



