/* gst-python
 * Copyright (C) 2009 Edward Hervey <bilboed@bilboed.com>
 *               2005 Benjamin Otte <otte@gnome.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

/* include this first, before NO_IMPORT_PYGOBJECT is defined */
#include <pygobject.h>
#include <gst/gst.h>
#include <Python.h>
/*
 * We need to call dlopen() directly on macOS to workaround a macOS runtime
 * linker bug. When there are nested dlopen() calls and the second dlopen() is
 * called from another library (such as gmodule), @loader_path is resolved as
 * @executable_path and RPATHs are read from the executable (gst-plugin-scanner)
 * instead of the library itself (libgstges.dylib). This doesn't happen if the
 * second dlopen() call is directly in the source code of the library.
 * Previously seen at:
 * https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1171#note_2290789
 */
#ifdef G_OS_WIN32
#include <gmodule.h>
#define gstpy_module_open(fname) g_module_open(fname,0)
#define gstpy_module_error g_module_error
#define gstpy_module_symbol(module,name,symbol) g_module_symbol(module,name,symbol)
#else
#include <dlfcn.h>
#define gstpy_module_open(fname) dlopen(fname,RTLD_NOW | RTLD_GLOBAL)
#define gstpy_module_error dlerror
static inline gboolean
gstpy_module_symbol (gpointer handle, const char *name, gpointer * symbol)
{
  *symbol = dlsym (handle, name);
  return *symbol != NULL;
}
#endif

void *_PyGstElement_Type;

GST_DEBUG_CATEGORY_STATIC (pyplugindebug);
#define GST_CAT_DEFAULT pyplugindebug

#define GST_ORIGIN "http://gstreamer.freedesktop.org"

static gboolean
gst_python_plugin_load_file (GstPlugin * plugin, const char *name)
{
  PyObject *main_module, *main_locals;
  PyObject *elementfactory;
  PyObject *module;
  const gchar *facname;
  guint rank;
  PyObject *class;

  GST_DEBUG ("loading plugin %s", name);

  main_module = PyImport_AddModule ("__main__");
  if (main_module == NULL) {
    GST_WARNING ("Could not get __main__, ignoring plugin %s", name);
    PyErr_Print ();
    PyErr_Clear ();
    return FALSE;
  }

  main_locals = PyModule_GetDict (main_module);
  module =
      PyImport_ImportModuleEx ((char *) name, main_locals, main_locals, NULL);
  if (!module) {
    GST_DEBUG ("Could not load module, ignoring plugin %s", name);
    PyErr_Print ();
    PyErr_Clear ();
    return FALSE;
  }

  /* Get __gstelementfactory__ from file */
  elementfactory = PyObject_GetAttrString (module, "__gstelementfactory__");
  if (!elementfactory) {
    GST_DEBUG ("python file doesn't contain __gstelementfactory__");
    PyErr_Clear ();
    return FALSE;
  }

  /* parse tuple : name, rank, gst.ElementClass */
  if (!PyArg_ParseTuple (elementfactory, "sIO", &facname, &rank, &class)) {
    GST_WARNING ("__gstelementfactory__ isn't correctly formatted");
    PyErr_Print ();
    PyErr_Clear ();
    Py_DECREF (elementfactory);
    return FALSE;
  }

  if (!PyObject_IsSubclass (class, (PyObject *) & PyGObject_Type)) {
    GST_WARNING ("the class provided isn't a subclass of GObject.Object");
    PyErr_Print ();
    PyErr_Clear ();
    Py_DECREF (elementfactory);
    Py_DECREF (class);
    return FALSE;
  }

  if (!g_type_is_a (pyg_type_from_object (class), GST_TYPE_ELEMENT)) {
    GST_WARNING ("the class provided isn't a subclass of Gst.Element");
    PyErr_Print ();
    PyErr_Clear ();
    Py_DECREF (elementfactory);
    Py_DECREF (class);
    return FALSE;
  }

  GST_INFO ("Valid plugin");
  Py_DECREF (elementfactory);

  return gst_element_register (plugin, facname, rank,
      pyg_type_from_object (class));
}

static gboolean
gst_python_load_directory (GstPlugin * plugin, const gchar * path)
{
  GDir *dir;
  const gchar *file;
  GError *error = NULL;

  dir = g_dir_open (path, 0, &error);
  if (!dir) {
    /*retval should probably be depending on error, but since we ignore it... */
    GST_DEBUG ("Couldn't open Python plugin dir: %s", error->message);
    g_error_free (error);
    return FALSE;
  }
  while ((file = g_dir_read_name (dir))) {
    /* FIXME : go down in subdirectories */
    if (g_str_has_suffix (file, ".py")) {
      gsize len = strlen (file) - 3;
      gchar *name = g_strndup (file, len);
      gst_python_plugin_load_file (plugin, name);
      g_free (name);
    }
  }
  return TRUE;
}

static gboolean
gst_python_plugin_load (GstPlugin * plugin)
{
  PyObject *sys_path;
  const gchar *plugin_path;
  gboolean ret = TRUE;

  sys_path = PySys_GetObject ("path");

  /* Mimic the order in which the registry is checked in core */

  /* 1. check env_variable GST_PLUGIN_PATH */
  plugin_path = g_getenv ("GST_PLUGIN_PATH_1_0");
  if (plugin_path == NULL)
    plugin_path = g_getenv ("GST_PLUGIN_PATH");
  if (plugin_path) {
    char **list;
    int i;

    GST_DEBUG ("GST_PLUGIN_PATH set to %s", plugin_path);
    list = g_strsplit (plugin_path, G_SEARCHPATH_SEPARATOR_S, 0);
    for (i = 0; list[i]; i++) {
      gchar *sysdir = g_build_filename (list[i], "python", NULL);
      PyList_Insert (sys_path, 0, PyUnicode_FromString (sysdir));
      gst_python_load_directory (plugin, sysdir);
      g_free (sysdir);
    }

    g_strfreev (list);
  }

  /* 2. Check for GST_PLUGIN_SYSTEM_PATH */
  plugin_path = g_getenv ("GST_PLUGIN_SYSTEM_PATH_1_0");
  if (plugin_path == NULL)
    plugin_path = g_getenv ("GST_PLUGIN_SYSTEM_PATH");
  if (plugin_path == NULL) {
    char *home_plugins;

    /* 2.a. Scan user and system-wide plugin directory */
    GST_DEBUG ("GST_PLUGIN_SYSTEM_PATH not set");

    /* plugins in the user's home directory take precedence over
     * system-installed ones */
    home_plugins = g_build_filename (g_get_home_dir (),
        ".gstreamer-" GST_API_VERSION, "plugins", "python", NULL);
    PyList_Insert (sys_path, 0, PyUnicode_FromString (home_plugins));
    gst_python_load_directory (plugin, home_plugins);
    g_free (home_plugins);

    /* add the main (installed) library path */
    PyList_Insert (sys_path, 0, PyUnicode_FromString (PLUGINDIR "/python"));
    gst_python_load_directory (plugin, PLUGINDIR "/python");
  } else {
    gchar **list;
    gint i;

    /* 2.b. Scan GST_PLUGIN_SYSTEM_PATH */
    GST_DEBUG ("GST_PLUGIN_SYSTEM_PATH set to %s", plugin_path);
    list = g_strsplit (plugin_path, G_SEARCHPATH_SEPARATOR_S, 0);
    for (i = 0; list[i]; i++) {
      gchar *sysdir;

      sysdir = g_build_filename (list[i], "python", NULL);

      PyList_Insert (sys_path, 0, PyUnicode_FromString (sysdir));
      gst_python_load_directory (plugin, sysdir);
      g_free (sysdir);
    }
    g_strfreev (list);
  }


  return ret;
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  PyGILState_STATE state = 0;
  PyObject *gi, *require_version, *args, *gst, *dict, *pyplugin;
  gboolean we_initialized = FALSE;
  gpointer has_python = NULL;
  const gchar *override_path;

  GST_DEBUG_CATEGORY_INIT (pyplugindebug, "pyplugin", 0,
      "Python plugin loader");

  gst_plugin_add_dependency_simple (plugin,
      "HOME/.gstreamer-" GST_API_VERSION
      "/plugins/python:GST_PLUGIN_SYSTEM_PATH/python:GST_PLUGIN_PATH/python",
      PLUGINDIR "/python:HOME/.gstreamer-" GST_API_VERSION "/plugins/python:"
      "GST_PLUGIN_SYSTEM_PATH/python:GST_PLUGIN_PATH/python", NULL,
      GST_PLUGIN_DEPENDENCY_FLAG_NONE);

  GST_LOG ("Checking to see if libpython is already loaded");
  if (gstpy_module_symbol (gstpy_module_open (NULL),
          "_Py_NoneStruct", &has_python) && has_python) {
    GST_LOG ("libpython is already loaded");
  } else {
    GST_LOG ("loading libpython by name: %s", PY_LIB_FNAME);
    if (!gstpy_module_open (PY_LIB_FNAME)) {
      GST_ERROR ("Couldn't load libpython. Reason: %s", gstpy_module_error ());
      return FALSE;
    }
  }

  if (!Py_IsInitialized ()) {
    GST_LOG ("python wasn't already initialized");
    /* set the correct plugin for registering stuff */
    Py_Initialize ();
    we_initialized = TRUE;
  } else {
    GST_LOG ("python was already initialized");
    state = PyGILState_Ensure ();
  }

  if ((override_path = g_getenv ("GST_OVERRIDE_SRC_PATH"))) {
    gchar *overrides_setup =
        g_build_filename (override_path, "..", "..", "testsuite",
        "overrides_hack.py", NULL);
    FILE *fd = fopen (overrides_setup, "rb");

    if (!fd || PyRun_SimpleFileExFlags (fd, overrides_setup, 1, 0)) {
      g_free (overrides_setup);

      return FALSE;
    } else {
      g_free (overrides_setup);
      GST_INFO ("Imported overrides setup");
    }
  }

  GST_LOG ("Running with python version '%s'", Py_GetVersion ());

  GST_LOG ("initializing pygobject");
  if (!pygobject_init (3, 0, 0)) {
    if (PyErr_Occurred ()) {
      PyObject *type, *value, *traceback;
      PyErr_Fetch (&type, &value, &traceback);
      PyObject *str = PyObject_Str (value);
      if (str) {
        g_critical ("pygobject initialization failed: %s\n",
            PyUnicode_AsUTF8 (str));
        Py_DECREF (str);
      }
      Py_XDECREF (type);
      Py_XDECREF (value);
      Py_XDECREF (traceback);
      PyErr_Clear ();
    }
    return FALSE;
  }

  gi = PyImport_ImportModule ("gi");
  if (!gi) {
    g_critical ("can't find gi");
    return FALSE;
  }


  require_version = PyObject_GetAttrString (gi, (char *) "require_version");
  args = PyTuple_Pack (2, PyUnicode_FromString ("Gst"),
      PyUnicode_FromString ("1.0"));
  PyObject_CallObject (require_version, args);
  Py_DECREF (require_version);
  Py_DECREF (args);
  Py_DECREF (gi);

  gst = PyImport_ImportModule ("gi.repository.Gst");
  if (!gst) {
    g_critical ("can't find gi.repository.Gst");
    return FALSE;
  }

  if (we_initialized) {
    PyObject *tmp;

    dict = PyModule_GetDict (gst);
    if (!dict) {
      g_critical ("gi.repository.Gst is no dict");
      return FALSE;
    }

    tmp =
        PyObject_GetAttr (PyMapping_GetItemString (dict,
            "_introspection_module"), PyUnicode_FromString ("__dict__"));

    _PyGstElement_Type = PyMapping_GetItemString (tmp, "Element");

    if (!_PyGstElement_Type) {
      g_critical ("Could not get Gst.Element");
      return FALSE;
    }

    pyplugin = pygobject_new (G_OBJECT (plugin));
    if (!pyplugin || PyModule_AddObject (gst, "__plugin__", pyplugin) != 0) {
      g_critical ("Couldn't set __plugin__ attribute");
      if (pyplugin)
        Py_DECREF (pyplugin);
      return FALSE;
    }
  }

  gst_python_plugin_load (plugin);

  if (we_initialized) {
    /* We need to release the GIL since we're going back to C land */
    PyEval_SaveThread ();
  } else
    PyGILState_Release (state);
  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR, python,
    "loader for plugins written in python",
    plugin_init, VERSION, "LGPL", PACKAGE_NAME, GST_ORIGIN)