/*
 * eom-python-module.c
 * This file is part of eom
 *
 * Copyright (C) 2005 Raphael Slinckx
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/* This needs to be included before any standard header
 * see http://docs.python.org/c-api/intro.html#include-files */
#include <Python.h>

#include <pygobject.h>
#include <pygtk/pygtk.h>

#include <signal.h>

#include <gmodule.h>

#include "eom-python-module.h"
#include "eom-python-plugin.h"
#include "eom-debug.h"

#if PY_VERSION_HEX < 0x02050000
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif

#define EOM_PYTHON_MODULE_GET_PRIVATE(object) \
	(G_TYPE_INSTANCE_GET_PRIVATE ((object), EOM_TYPE_PYTHON_MODULE, EomPythonModulePrivate))

struct _EomPythonModulePrivate {
	gchar *module;
	gchar *path;
	GType type;
};

enum {
	PROP_0,
	PROP_PATH,
	PROP_MODULE
};

void pyeom_register_classes (PyObject *d);
void pyeom_add_constants (PyObject *module, const gchar *strip_prefix);
extern PyMethodDef pyeom_functions[];

static PyTypeObject *PyEomPlugin_Type;

G_DEFINE_TYPE (EomPythonModule, eom_python_module, G_TYPE_TYPE_MODULE)

static gboolean
eom_python_module_load (GTypeModule *gmodule)
{
	EomPythonModulePrivate *priv = EOM_PYTHON_MODULE_GET_PRIVATE (gmodule);
	PyObject *main_module, *main_locals, *locals, *key, *value;
	PyObject *module, *fromlist;
	Py_ssize_t pos = 0;

	g_return_val_if_fail (Py_IsInitialized (), FALSE);

	main_module = PyImport_AddModule ("__main__");

	if (main_module == NULL) {
		g_warning ("Could not get __main__.");
		return FALSE;
	}

	/* If we have a special path, we register it */
	if (priv->path != NULL) {
		PyObject *sys_path = PySys_GetObject ("path");
		PyObject *path = PyString_FromString (priv->path);

		if (PySequence_Contains(sys_path, path) == 0)
			PyList_Insert (sys_path, 0, path);

		Py_DECREF(path);
	}

	main_locals = PyModule_GetDict (main_module);

	/* We need a fromlist to be able to import modules with
         * a '.' in the name. */
	fromlist = PyTuple_New(0);

	module = PyImport_ImportModuleEx (priv->module, main_locals, main_locals, fromlist);

	Py_DECREF(fromlist);

	if (!module) {
		PyErr_Print ();
		return FALSE;
	}

	locals = PyModule_GetDict (module);

	while (PyDict_Next (locals, &pos, &key, &value)) {
		if (!PyType_Check(value))
			continue;

		if (PyObject_IsSubclass (value, (PyObject*) PyEomPlugin_Type)) {
			priv->type = eom_python_plugin_get_type (gmodule, value);
			return TRUE;
		}
	}

	return FALSE;
}

static void
eom_python_module_unload (GTypeModule *module)
{
	EomPythonModulePrivate *priv = EOM_PYTHON_MODULE_GET_PRIVATE (module);

	eom_debug_message (DEBUG_PLUGINS, "Unloading Python module");

	priv->type = 0;
}

GObject *
eom_python_module_new_object (EomPythonModule *module)
{
	EomPythonModulePrivate *priv = EOM_PYTHON_MODULE_GET_PRIVATE (module);

	eom_debug_message (DEBUG_PLUGINS, "Creating object of type %s", g_type_name (priv->type));

	if (priv->type == 0)
		return NULL;

	return g_object_new (priv->type, NULL);
}

static void
eom_python_module_init (EomPythonModule *module)
{
	eom_debug_message (DEBUG_PLUGINS, "Init of Python module");
}

static void
eom_python_module_finalize (GObject *object)
{
	EomPythonModulePrivate *priv = EOM_PYTHON_MODULE_GET_PRIVATE (object);

	eom_debug_message (DEBUG_PLUGINS, "Finalizing Python module %s", g_type_name (priv->type));

	g_free (priv->module);
	g_free (priv->path);

	G_OBJECT_CLASS (eom_python_module_parent_class)->finalize (object);
}

static void
eom_python_module_get_property (GObject    *object,
				guint       prop_id,
				GValue     *value,
				GParamSpec *pspec)
{
	g_return_if_reached ();
}

static void
eom_python_module_set_property (GObject      *object,
				  guint         prop_id,
				  const GValue *value,
				  GParamSpec   *pspec)
{
	EomPythonModule *mod = EOM_PYTHON_MODULE (object);

	switch (prop_id) {
	case PROP_MODULE:
		EOM_PYTHON_MODULE_GET_PRIVATE (mod)->module = g_value_dup_string (value);
		break;

	case PROP_PATH:
		EOM_PYTHON_MODULE_GET_PRIVATE (mod)->path = g_value_dup_string (value);
		break;

	default:
		g_return_if_reached ();
	}
}

static void
eom_python_module_class_init (EomPythonModuleClass *class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (class);
	GTypeModuleClass *module_class = G_TYPE_MODULE_CLASS (class);

	object_class->finalize = eom_python_module_finalize;
	object_class->get_property = eom_python_module_get_property;
	object_class->set_property = eom_python_module_set_property;

	g_object_class_install_property
			(object_class,
			 PROP_MODULE,
			 g_param_spec_string ("module",
					      "Module Name",
					      "The Python module to load for this plugin",
					      NULL,
					      G_PARAM_WRITABLE | G_PARAM_READABLE | G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property
			(object_class,
			 PROP_PATH,
			 g_param_spec_string ("path",
					      "Path",
					      "The Python path to use when loading this module",
					      NULL,
					      G_PARAM_WRITABLE | G_PARAM_READABLE | G_PARAM_CONSTRUCT_ONLY));

	g_type_class_add_private (object_class, sizeof (EomPythonModulePrivate));

	module_class->load = eom_python_module_load;
	module_class->unload = eom_python_module_unload;
}

EomPythonModule *
eom_python_module_new (const gchar *path,
		       const gchar *module)
{
	EomPythonModule *result;

	if (module == NULL || module[0] == '\0')
		return NULL;

	result = g_object_new (EOM_TYPE_PYTHON_MODULE,
			       "module", module,
			       "path", path,
			       NULL);

	g_type_module_set_name (G_TYPE_MODULE (result), module);

	return result;
}


static gint idle_garbage_collect_id = 0;

/* C equivalent of
 *    import pygtk
 *    pygtk.require ("2.0")
 */
static gboolean
check_pygtk2 (void)
{
	PyObject *pygtk, *mdict, *require;

	/* pygtk.require("2.0") */
	pygtk = PyImport_ImportModule ("pygtk");

	if (pygtk == NULL) {
		g_warning ("Error initializing Python interpreter: could not import pygtk.");
		return FALSE;
	}

	mdict = PyModule_GetDict (pygtk);

	require = PyDict_GetItemString (mdict, "require");

	PyObject_CallObject (require,
			     Py_BuildValue ("(S)", PyString_FromString ("2.0")));

	if (PyErr_Occurred()) {
		g_warning ("Error initializing Python interpreter: pygtk 2 is required.");
		return FALSE;
	}

	return TRUE;
}

/* Note: the following two functions are needed because
 * init_pyobject and init_pygtk which are *macros* which in case
 * case of error set the PyErr and then make the calling
 * function return behind our back.
 * It's up to the caller to check the result with PyErr_Occurred()
 */
static void
eom_init_pygobject (void)
{
	init_pygobject_check (2, 11, 5); /* FIXME: get from config */
}

static void
eom_init_pygtk (void)
{
	PyObject *gtk, *mdict, *version, *required_version;

	init_pygtk ();

	/* There isn't init_pygtk_check(), do the version
	 * check ourselves */
	gtk = PyImport_ImportModule("gtk");

	mdict = PyModule_GetDict(gtk);

	version = PyDict_GetItemString (mdict, "pygtk_version");

	if (!version) {
		PyErr_SetString (PyExc_ImportError,
				 "PyGObject version too old");
		return;
	}

	required_version = Py_BuildValue ("(iii)", 2, 4, 0); /* FIXME */

	if (PyObject_Compare (version, required_version) == -1) {
		PyErr_SetString (PyExc_ImportError,
				 "PyGObject version too old");

		Py_DECREF (required_version);
		return;
	}

	Py_DECREF (required_version);
}

gboolean
eom_python_init (void)
{
	PyObject *mdict, *path, *tuple;
	PyObject *sys_path, *eom;
	PyObject *gettext, *install, *gettext_args;
	struct sigaction old_sigint;
	gint res;
	/* Workaround for python bug. See #569228. */
	char *argv[] = { "/dev/null/python/is/buggy/eom", NULL };

	static gboolean init_failed = FALSE;

	if (init_failed) {
		/* We already failed to initialized Python, don't need to
		 * retry again */
		return FALSE;
	}

	if (Py_IsInitialized ()) {
		/* Python has already been successfully initialized */
		return TRUE;
	}

	/* We are trying to initialize Python for the first time,
	   set init_failed to FALSE only if the entire initialization process
	   ends with success */
	init_failed = TRUE;

	/* Hack to make python not overwrite SIGINT: this is needed to avoid
	 * the crash reported on bug #326191 */

	/* CHECK: can't we use Py_InitializeEx instead of Py_Initialize in order
          to avoid to manage signal handlers ? - Paolo (Dec. 31, 2006) */

	/* Save old handler */
	res = sigaction (SIGINT, NULL, &old_sigint);

	if (res != 0) {
		g_warning ("Error initializing Python interpreter: cannot get "
		           "handler to SIGINT signal (%s)", strerror (errno));

		return FALSE;
	}

	/* Python initialization */
	Py_Initialize ();

	/* Restore old handler */
	res = sigaction (SIGINT, &old_sigint, NULL);

	if (res != 0) {
		g_warning ("Error initializing Python interpreter: cannot restore "
		           "handler to SIGINT signal (%s).", strerror (errno));

		goto python_init_error;
	}

	PySys_SetArgv (1, argv);

	/* Sanitize sys.path */
	PyRun_SimpleString("import sys; sys.path = filter(None, sys.path)");

	if (!check_pygtk2 ()) {
		/* Warning message already printed in check_pygtk2 */
		goto python_init_error;
	}

	/* import gobject */
	eom_init_pygobject ();

	if (PyErr_Occurred ()) {
		g_warning ("Error initializing Python interpreter: could not import pygobject.");

		goto python_init_error;
	}

	/* import gtk */
	eom_init_pygtk ();

	if (PyErr_Occurred ()) {
		g_warning ("Error initializing Python interpreter: could not import pygtk.");

		goto python_init_error;
	}

	/* sys.path.insert(0, ...) for system-wide plugins */
	sys_path = PySys_GetObject ("path");
	path = PyString_FromString (EOM_PLUGIN_DIR "/");
	PyList_Insert (sys_path, 0, path);
	Py_DECREF(path);

	/* import eom */
	eom = Py_InitModule ("eom", pyeom_functions);
	mdict = PyModule_GetDict (eom);

	pyeom_register_classes (mdict);
	pyeom_add_constants (eom, "EOM_");

	/* eom version */
	tuple = Py_BuildValue("(iii)",
			      EOM_MAJOR_VERSION,
			      EOM_MINOR_VERSION,
			      EOM_MICRO_VERSION);
	PyDict_SetItemString(mdict, "version", tuple);
	Py_DECREF(tuple);

	/* Retrieve the Python type for eom.Plugin */
	PyEomPlugin_Type = (PyTypeObject *) PyDict_GetItemString (mdict, "Plugin");

	if (PyEomPlugin_Type == NULL) {
		PyErr_Print ();

		goto python_init_error;
	}

	/* i18n support */
	gettext = PyImport_ImportModule ("gettext");

	if (gettext == NULL) {
		g_warning ("Error initializing Python interpreter: could not import gettext.");

		goto python_init_error;
	}

	mdict = PyModule_GetDict (gettext);
	install = PyDict_GetItemString (mdict, "install");
	gettext_args = Py_BuildValue ("ss", GETTEXT_PACKAGE, EOM_LOCALE_DIR);
	PyObject_CallObject (install, gettext_args);
	Py_DECREF (gettext_args);

	/* Python has been successfully initialized */
	init_failed = FALSE;

	return TRUE;

python_init_error:

	g_warning ("Please check the installation of all the Python related packages required "
	           "by eom and try again.");

	PyErr_Clear ();

	eom_python_shutdown ();

	return FALSE;
}

void
eom_python_shutdown (void)
{
	if (Py_IsInitialized ()) {
		if (idle_garbage_collect_id != 0) {
			g_source_remove (idle_garbage_collect_id);
			idle_garbage_collect_id = 0;
		}

		while (PyGC_Collect ())
			;

		Py_Finalize ();
	}
}

static gboolean
run_gc (gpointer data)
{
	while (PyGC_Collect ())
		;

	idle_garbage_collect_id = 0;

	return FALSE;
}

void
eom_python_garbage_collect (void)
{
	if (Py_IsInitialized()) {
		/*
		 * We both run the GC right now and we schedule
		 * a further collection in the main loop.
		 */

		while (PyGC_Collect ())
			;

		if (idle_garbage_collect_id == 0)
			idle_garbage_collect_id = g_idle_add (run_gc, NULL);
	}
}