/*
 * peas-python-internal.c
 * This file is part of libpeas
 *
 * Copyright (C) 2014-2015 - Garrett Regier
 *
 * libpeas 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 2.1 of the License, or (at your option) any later version.
 *
 * libpeas 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "peas-python-internal.h"

#include <gio/gio.h>


static PyObject *internal_module = NULL;
static PyObject *internal_hooks = NULL;
static PyObject *FailedError = NULL;


static PyObject *
failed_fn (PyObject *self,
           PyObject *args)
{
  const char *msg;
  char *clean_msg;

  if (!PyArg_ParseTuple (args, "s:Hooks.failed", &msg))
    return NULL;

  /* Python tracebacks have a trailing newline */
  clean_msg = g_strchomp (g_strdup (msg));

  g_warning ("%s", clean_msg);

  /* peas_python_internal_call() knows to check for this exception */
  PyErr_SetObject (FailedError, NULL);

  g_free (clean_msg);
  return NULL;
}

static PyMethodDef failed_method_def = {
  "failed", (PyCFunction) failed_fn, METH_VARARGS | METH_STATIC,
  "Prints warning and raises an Exception"
};

gboolean
peas_python_internal_setup (gboolean already_initialized)
{
  const char *prgname;
  GBytes *internal_python = NULL;
  PyObject *builtins_module, *globals, *result;
  PyObject *code = NULL, *failed_method = NULL;
  gboolean success = FALSE;

#define goto_error_if_failed(cond) \
  G_STMT_START { \
    if (G_UNLIKELY (!(cond))) \
      { \
        g_warn_if_fail (cond); \
        goto error; \
      } \
  } G_STMT_END

  prgname = g_get_prgname ();
  prgname = prgname == NULL ? "" : prgname;

  builtins_module = PyImport_ImportModule ("builtins");

  goto_error_if_failed (builtins_module != NULL);

  /* We don't use the byte-compiled Python source
   * because glib-compile-resources cannot output
   * depends for generated files.
   *
   * https://bugzilla.gnome.org/show_bug.cgi?id=673101
   */
  internal_python = g_resources_lookup_data ("/org/gnome/libpeas/loaders/"
                                             "python/"
                                             "internal.py",
                                             G_RESOURCE_LOOKUP_FLAGS_NONE,
                                             NULL);
  goto_error_if_failed (internal_python != NULL);

  /* Compile it manually so the filename is available */
  code = Py_CompileString (g_bytes_get_data (internal_python, NULL),
                           "peas-python-internal.py",
                           Py_file_input);
  goto_error_if_failed (code != NULL);

  internal_module = PyModule_New ("libpeas-internal");
  goto_error_if_failed (internal_module != NULL);

  goto_error_if_failed (PyModule_AddStringConstant (internal_module, "__file__",
                                                    "peas-python-internal.py") == 0);
  goto_error_if_failed (PyModule_AddObject (internal_module, "__builtins__",
                                            builtins_module) == 0);
  goto_error_if_failed (PyModule_AddObject (internal_module,
                                            "ALREADY_INITIALIZED",
                                            already_initialized ?
                                            Py_True : Py_False) == 0);
  goto_error_if_failed (PyModule_AddStringConstant (internal_module,
                                                    "PRGNAME",
                                                    prgname) == 0);
  goto_error_if_failed (PyModule_AddStringMacro (internal_module,
                                                 GETTEXT_PACKAGE) == 0);

#ifndef G_OS_WIN32
  goto_error_if_failed (PyModule_AddStringMacro (internal_module,
                                                 PEAS_LOCALEDIR) == 0);
#endif

  globals = PyModule_GetDict (internal_module);
  result = PyEval_EvalCode ((gpointer) code, globals, globals);
  Py_XDECREF (result);

  if (PyErr_Occurred ())
    {
      g_warning ("Failed to run internal Python code");
      goto error;
    }

  internal_hooks = PyDict_GetItemString (globals, "hooks");
  goto_error_if_failed (internal_hooks != NULL);

  FailedError = PyDict_GetItemString (globals, "FailedError");
  goto_error_if_failed (FailedError != NULL);

  failed_method = PyCFunction_NewEx (&failed_method_def,
                                     NULL, internal_module);
  goto_error_if_failed (failed_method != NULL);
  goto_error_if_failed (PyObject_SetAttrString (internal_hooks, "failed",
                                                failed_method) == 0);

  success = TRUE;

#undef goto_error_if_failed

error:

  Py_XDECREF (failed_method);
  Py_XDECREF (code);
  g_clear_pointer (&internal_python, g_bytes_unref);

  if (!success)
    {
      FailedError = NULL;
      internal_hooks = NULL;

      if (internal_module != NULL)
        {
          PyDict_Clear (PyModule_GetDict (internal_module));
          Py_DECREF (internal_module);
        }
    }

  return success;
}

void
peas_python_internal_shutdown (void)
{
  peas_python_internal_call ("exit", NULL, NULL);

  FailedError = NULL;
  internal_hooks = NULL;
  PyDict_Clear (PyModule_GetDict (internal_module));
  Py_DECREF (internal_module);
}

PyObject *
peas_python_internal_call (const char   *name,
                           PyTypeObject *return_type,
                           const char   *format,
                           ...)
{
  PyObject *args;
  PyObject *result = NULL;
  va_list var_args;

  /* The PyTypeObject for Py_None is not exposed directly */
  if (return_type == NULL)
    return_type = Py_None->ob_type;

  va_start (var_args, format);
  args = Py_VaBuildValue (format == NULL ? "()" : format, var_args);
  va_end (var_args);

  if (args != NULL)
    {
      result = PyObject_CallMethod (internal_hooks, "call", "(sOO)",
                                    name, args, return_type);
      Py_DECREF (args);
    }

  if (PyErr_Occurred ())
    {
      /* Raised by failed_fn() to prevent printing the exception */
      if (PyErr_ExceptionMatches (FailedError))
        {
          PyErr_Clear ();
        }
      else
        {
          g_warning ("Failed to run internal Python hook 'call'");
          PyErr_Print ();
        }

      return NULL;
    }

  if (result == Py_None)
    Py_CLEAR (result);

  return result;
}
