/*
 * Copyright (C) 2012 - Juan Ferrer Toribio
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "vn-gui.h"
#include <gdome.h>
#include <stdlib.h>
#include <glib/gstdio.h>

#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/valid.h>

#define MAIN_UI			_GUI_DIR"/main.glade"
#define MENU_UI			_GUI_DIR"/menu.glade"
#define MODULE_DTD		_DTD_DIR"/module.dtd"

#define S(string)						(gdome_str_mkref (string))
#define gtk_builder_get(builder, id)	((gpointer) gtk_builder_get_object (builder, id))

/**
 * SECTION: vn-gui
 * @Short_description: GUI manager
 * @Title: VnGui
 * 
 * Manages most of the GUI operations.
 **/
G_DEFINE_TYPE (VnGui, vn_gui, G_TYPE_OBJECT);

struct _VnWindow
{
	VnGui * obj;
	GtkWindow * widget;
	GtkDialog * about;
	GtkHeaderBar * header;
	GtkWidget * menu_button;
	GtkWidget * spinner;
	GtkNotebook * notebook;
	VnForm * active_form;
	guint merge_id;
	gboolean maximized;
};

enum {
	 COL_ICON
	,COL_NAME
	,COL_TITLE
	,COL_TYPE
	,COL_MODULE
	,COL_COUNT
};

enum {
	 LOGOUT
	,EXIT
	,LAST_SIGNAL
};

typedef struct
{
	VnGui * obj;
	gboolean aux;
	GError * error;
	GThread * thread;
}
GuiData;

void vn_gui_on_new_window_activated (GSimpleAction * a, GVariant * v, gpointer obj); 
void vn_gui_on_logout_activated (GSimpleAction * a, GVariant * v, gpointer obj);
void vn_gui_on_open_activated (GSimpleAction * a, GVariant * v, gpointer obj);
void vn_gui_on_about_activated (GSimpleAction * a, GVariant * v, gpointer obj);
void vn_gui_on_exit_activated (GSimpleAction * a, GVariant * v, gpointer obj);
void vn_gui_on_close_tab_activated (GSimpleAction * a, GVariant * v, gpointer obj);
void vn_gui_on_open_form_activated (GSimpleAction * action, GVariant * v, gpointer obj);

static void	vn_gui_reconnect				(VnGui * obj);
static void	vn_gui_on_conn_error			(DbConn * conn, const GError * error, VnGui * obj);
static void	vn_gui_on_conn_status_changed	(DbConn * conn, DbConnStatus status, VnGui * obj);
void		vn_gui_on_window_destroyed		(GtkWindow * widget, VnWindow * window);
void		vn_gui_on_page_removed			(GtkNotebook * notebook, GtkWidget * page, guint num, VnWindow * window);

static guint signals[LAST_SIGNAL] = {0};

static const GActionEntry app_entries[] =
{
	 {"new-window", vn_gui_on_new_window_activated}
	,{"logout", vn_gui_on_logout_activated}
	,{"connect", vn_gui_on_open_activated}
	,{"about", vn_gui_on_about_activated}
	,{"quit", vn_gui_on_exit_activated}
};

static const GActionEntry win_entries[] =
{
	 {"close", vn_gui_on_close_tab_activated}
};

static const GActionEntry open_action[] =
{
	{"open-form", vn_gui_on_open_form_activated, "s"}
};

/**
 * vn_gui_new:
 * @app: a #GtkApplication
 * @conn: the #VnLogin
 * 
 * Creates a new Gui object.
 *
 * Return value: the created #VnGui
 **/
VnGui * vn_gui_new (GtkApplication * app, DbConn * conn)
{	
	return g_object_new (VN_TYPE_GUI, "app", app, "conn", conn, NULL);
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Private

/*
 * Frees the #GuiData struct.
 */
static void gui_data_free (GuiData * gui_data)
{
	if (gui_data->error)
		g_error_free (gui_data->error);

	g_object_unref (gui_data->obj);
	g_thread_unref (gui_data->thread);
	g_free (gui_data);
}

static void vn_gui_free_window (VnGui * obj, VnWindow * window)
{
	g_signal_handlers_disconnect_by_func (window->widget,
		vn_gui_on_window_destroyed, window);
	g_signal_handlers_disconnect_by_func (window->notebook,
		vn_gui_on_page_removed, window);

	gtk_widget_destroy (GTK_WIDGET (window->widget));
	g_free (window);
}

/*
 * Validates a module definition file at @path against the DTD
 */
static gboolean vn_gui_validate_module_definition (VnGui * obj, const gchar * path)
{
	gboolean valid = FALSE;
	xmlValidCtxtPtr ctxt = xmlNewValidCtxt ();

	if (ctxt)
	{
		xmlDocPtr doc = xmlParseFile (path);

		if (doc)
		{
			xmlDtdPtr dtd = xmlParseDTD	(NULL, (const xmlChar *) MODULE_DTD);

			if (!dtd)
				g_error ("The DTD is not well formed!");

			valid = xmlValidateDtd (ctxt, doc, dtd);
			xmlFreeDtd (dtd);
		}

		xmlFreeValidCtxt (ctxt);
		xmlFreeDoc (doc);
	}

	return valid;
}

/*
 * Loads a module and all of its forms.
 */
static void vn_gui_load_module (VnGui * obj, const gchar * dir, const gchar * file)
{
	gint n;
	gchar * mod_title_strip, * mod_name;
	const gchar * text_dom = NULL;
	GModule * module = NULL;
	VnMod * mod = NULL;
	GdomeException e;
	GdomeDocument * doc;
	GdomeDOMImplementation * di;
	GdomeNodeList * nl;
	GdomeElement * el;
	GdomeNode * node;
	GdomeDOMString * mod_title;
	GdomeDOMString * library;
	GdomeDOMString * name_str;
	gchar * path = g_strdup_printf ("%s/%s", dir, file);

	// Getting the module info from XML file

	di = gdome_di_mkref ();
	doc = gdome_di_createDocFromURI (di, path, 0, &e);
	gdome_di_unref (di, &e);

	name_str = S("name");

	if (doc)
	{
		gboolean is_module = FALSE;
		GdomeDocumentType * dt = gdome_doc_doctype (doc, &e);

		if (dt)
		{
			GdomeDOMString * dt_name = gdome_dt_name (dt, &e);
			is_module = dt_name && !g_strcmp0 (dt_name->str, "hedera-module");
			gdome_str_unref (dt_name);
			gdome_dt_unref (dt, &e);
		}

		if (is_module)
		{
			if (vn_gui_validate_module_definition (obj, path))
			{
				GdomeDOMString * library_str = S("library"),
					* form_str = S("form");

				nl = gdome_doc_getElementsByTagName (doc, library_str, &e);
				el = (GdomeElement *) gdome_nl_item (nl, 0, &e);
				gdome_str_unref (library_str);
				gdome_nl_unref (nl, &e);

				library = gdome_el_getAttribute (el, name_str, &e);
				mod_name = g_strdup (library->str);

				node = gdome_el_firstChild (el, &e);
				mod_title = gdome_n_nodeValue (node, &e);
				mod_title_strip = g_strstrip (g_strdup (mod_title->str));
				nl = gdome_doc_getElementsByTagName (doc, form_str, &e);

				gdome_str_unref (form_str);
				gdome_str_unref (library);
				gdome_doc_unref (doc, &e);
				gdome_n_unref (node, &e);
				gdome_el_unref (el, &e);
			}
			else
			{
				g_warning ("VnGui: The module definition at \"%s\" is not valid", path);
				gdome_doc_unref (doc, &e);
				gdome_str_unref (name_str);
				g_free (path);
				return;
			}
		}
		else
		{
			gdome_doc_unref (doc, &e);
			gdome_str_unref (name_str);
			g_free (path);
			return;
		}
	}
	else
	{
		g_warning ("VnGui: Error loading module info file: %s", (gchar *) file);
		gdome_str_unref (name_str);
		g_free (path);
		return;
	}

	g_free (path);

	// Loading the module dynamically

	for (n = 0; obj->lib_dirs[n] && !module; n++)
	{
		path = g_module_build_path (obj->lib_dirs[n], mod_name);
		module = g_module_open (path, 0);
		g_free (path);
	}

	if (module)
	{
		GType mod_type = VN_TYPE_MOD;
		VnModGetTypeFunc mod_get_type_func;
		gchar * c_name = g_strdelimit (g_strdup (mod_name), "-. ", '_'),
			* symbol_name = g_strdup_printf ("vn_%s_get_type", c_name);

		g_module_make_resident (module);

		if (g_module_symbol (module, symbol_name, (gpointer) &mod_get_type_func)
		&& g_type_is_a (mod_get_type_func (), VN_TYPE_MOD))
			mod_type = mod_get_type_func ();

		mod = g_object_new (mod_type
			,"name"     ,mod_name
			,"data-dir" ,dir
			,"module"   ,module
			,"title"    ,mod_title_strip
			,"gui"		,obj
			,NULL
		);

		g_free (c_name);
		g_free (symbol_name);
	}
	else
		g_warning ("VnGui: Can't load module %s: %s", mod_name, g_module_error ());

	// If successful, load forms

	if (mod)
	{
		gulong len, n;
		GtkTreeIter parent_iter;
		GdomeDOMString * icon_str = S("icon");

		// Creating folder to put forms inside

		text_dom = vn_mod_get_text_domain (mod);
		gtk_tree_store_append (obj->tree, &parent_iter, NULL);
		gtk_tree_store_set (obj->tree, &parent_iter
			,COL_ICON	,"gtk-directory"
			,COL_TITLE	,g_dgettext (text_dom, mod_title_strip)
			,COL_TYPE	,G_TYPE_NONE
			,COL_NAME	,NULL
			,-1
		);

		len = gdome_nl_length (nl, &e);

		for (n = 0; n < len; n++)
		{
			gchar * c_name, * title_strip, * symbol_name;
			GdomeDOMString * icon, * name, * title;
			VnFormGetTypeFunc form_get_type_func;

			el = (GdomeElement *) gdome_nl_item (nl, n, &e);
			icon = gdome_el_getAttribute (el, icon_str, &e);

			name = gdome_el_getAttribute (el, name_str, &e);
			c_name = g_strdelimit (g_strdup (name->str), "-. ", '_');

			node = gdome_el_firstChild (el, &e);
			title = gdome_n_nodeValue (node, &e);
			title_strip = g_strstrip (g_strdup (g_dgettext (text_dom, title->str)));

			gdome_n_unref (node, &e);
			gdome_el_unref (el, &e);

			symbol_name = g_strdup_printf ("vn_%s_get_type", c_name);

			if (g_module_symbol (module, symbol_name, (gpointer) &form_get_type_func))
			{
				GType type = form_get_type_func ();

				if (g_type_is_a (type, VN_TYPE_FORM))
				{
					GtkTreeIter * iter = g_new (GtkTreeIter, 1);
					gtk_tree_store_append (obj->tree, iter, &parent_iter);
					gtk_tree_store_set (obj->tree, iter
						,COL_NAME		,name->str
						,COL_ICON		,icon->str
						,COL_TITLE		,title_strip
						,COL_TYPE		,type
						,COL_MODULE		,mod
						,-1
					);

					g_hash_table_replace (obj->forms, g_strdup (name->str), iter);					
				}
				else
					g_warning ("VnGui: %s isn't a VnForm", g_type_name (type));
			}
			else
				g_warning ("VnGui: Error loading form: %s", g_module_error ());

			g_free (c_name);
			g_free (title_strip);
			g_free (symbol_name);
			gdome_str_unref (name);
			gdome_str_unref (icon);
			gdome_str_unref (title);
		}

		obj->modules = g_slist_prepend (obj->modules, mod);

		gdome_str_unref (icon_str);
	}

	g_free (mod_title_strip);
	g_free (mod_name);
	gdome_str_unref (name_str);
	gdome_str_unref (mod_title);
	gdome_nl_unref (nl, &e);
}

static void set_accelerator (VnGui * obj, GMenuModel * model, gint item, gboolean enable)
{
	GMenuAttributeIter * iter;
	GVariant * value;
	GVariant * target = NULL;
	const char * key;
	const char * accel = NULL;
	const char * action = NULL;

	iter = g_menu_model_iterate_item_attributes (model, item);

	while (g_menu_attribute_iter_get_next (iter, &key, &value))
	{
		if (g_str_equal (key, "action") && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
			action = g_variant_get_string (value, NULL);
		else if (g_str_equal (key, "accel") && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
			accel = g_variant_get_string (value, NULL);
		else if (g_str_equal (key, "target"))
			target = g_variant_ref (value);

		g_variant_unref (value);
	}

	g_object_unref (iter);

	if (accel && action)
	{
		if (enable)
			gtk_application_add_accelerator (obj->app, accel, action, target);
		else
			gtk_application_remove_accelerator (obj->app, action, target);
	}

	if (target)
		   g_variant_unref (target);
}

static void vn_gui_set_menu_accels (VnGui * obj, GMenuModel * menu, gboolean enable)
{
	gint i;

	for (i = 0; i < g_menu_model_get_n_items (menu); i++)
	{
		GMenuLinkIter * iter;
		GMenuModel * more;
		const gchar * key;

		set_accelerator (obj, menu, i, enable);

		iter = g_menu_model_iterate_item_links (menu, i);

		while (g_menu_link_iter_get_next (iter, &key, &more))
		{
			vn_gui_set_menu_accels (obj, more, enable);
			g_object_unref (more);
		}

		g_object_unref (iter);
	}
}

static VnWindow * vn_gui_add_window (VnGui * obj, GtkWindow * widget, GtkNotebook * notebook)
{
	GSList * n;
	GtkWidget * button;
	VnWindow * window = g_new (VnWindow, 1);

	window->obj = obj;
	window->widget = widget;
	window->notebook = notebook;
	window->active_form = NULL;
	window->merge_id = 0;

	obj->windows = g_slist_prepend (obj->windows, window);

	gtk_application_add_window (obj->app, widget);
	gtk_notebook_set_group_name (notebook,
		g_application_get_application_id (G_APPLICATION (obj->app)));

	// Setting header and window menu

	window->header = g_object_new (GTK_TYPE_HEADER_BAR
		,"show-close-button", TRUE
		,"title", obj->app_title
		,"subtitle", db_conn_get_user (obj->conn)
		,NULL);
	gtk_window_set_titlebar (widget, GTK_WIDGET (window->header));

	button = gtk_menu_button_new ();
	g_action_map_add_action_entries (G_ACTION_MAP (window->widget),
		win_entries, G_N_ELEMENTS (win_entries), window);
	gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (button), obj->main_menu);
	gtk_button_set_image (GTK_BUTTON (button),
		gtk_image_new_from_icon_name ("emblem-system-symbolic",
			GTK_ICON_SIZE_BUTTON));
	gtk_header_bar_pack_end (window->header, button);

	button = gtk_menu_button_new ();
	gtk_widget_set_no_show_all (button, TRUE);
	gtk_header_bar_pack_start (window->header, button);
	window->menu_button = button;

	window->spinner = gtk_spinner_new ();
	gtk_widget_set_opacity (window->spinner, 0);
	gtk_header_bar_pack_end (window->header, window->spinner);

	// Loading the modules actions

	g_action_map_add_action_entries (G_ACTION_MAP (window->widget),
		open_action, G_N_ELEMENTS (open_action), window);

	for (n = obj->modules; n; n = n->next)
	{
		gint size;
		VnMod * mod = VN_MOD (n->data);
		const GActionEntry * actions = vn_mod_get_actions (mod, &size);
		g_action_map_add_action_entries (G_ACTION_MAP (window->widget),
			actions, size, mod);
	}

	vn_gui_set_menu_accels (obj, obj->main_menu, TRUE);

	gtk_widget_show_all (GTK_WIDGET (widget));
	return window;
}

/*
 * Shows an error dialog.
 */
static void vn_gui_show_error (VnGui * obj, const GError * error)
{
	GtkWidget * dialog;
	GtkWindow * window = obj->active_window ? obj->active_window->widget : NULL;

	if (error && error->code == DB_CONN_ERROR_LOST)
		dialog = gtk_message_dialog_new (window
			,GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT
			,GTK_MESSAGE_QUESTION
			,GTK_BUTTONS_YES_NO
			,_("Connection has been lost. Do you want to reconnect?")
		);	
	else
		dialog = gtk_message_dialog_new (window
			,GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT
			,GTK_MESSAGE_WARNING
			,GTK_BUTTONS_CLOSE
			,_("An error occurred in the connection.")
		);

	gtk_window_set_title (GTK_WINDOW (dialog), _("Database error"));
	
	if (error)
		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
			"%s", error->message);
	else
		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
			_("Unknown error"));

	if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES)
		vn_gui_reconnect (obj);

	gtk_widget_destroy (GTK_WIDGET (dialog));	
}

/*
 * Closes and saves the GUI interface.
 */
static void vn_gui_close (VnGui * obj)
{
	GSList * n;

	g_return_if_fail (VN_IS_GUI (obj));

	if (obj->windows)
	{
		vn_gui_save (obj);
		g_object_disconnect (obj->conn
			,"any_signal", vn_gui_on_conn_error, obj
			,"any_signal", vn_gui_on_conn_status_changed, obj
			,NULL
		);

		for (n = obj->windows; n; n = n->next)
			vn_gui_free_window (obj, n->data);

		g_slist_free (obj->windows);
		obj->windows = NULL;
		obj->active_window = NULL;
	}
}

/*
 * Idle function that completes the reopen thread.
 */
static gboolean vn_gui_reconnect_idle (GuiData * gui_data)
{
	if (!gui_data->aux)
		vn_gui_show_error (gui_data->obj, gui_data->error);

	return G_SOURCE_REMOVE;
}

/*
 * Thread function that tries to reopen the connection asynchronously.
 */
static void vn_gui_reconnect_thread (GuiData * gui_data)
{
	gui_data->aux = db_conn_reconnect (gui_data->obj->conn, &gui_data->error);
	g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
		(GSourceFunc) vn_gui_reconnect_idle, gui_data, (GDestroyNotify) gui_data_free);
}

/*
 * Reconnects to database.
 */
void vn_gui_reconnect (VnGui * obj)
{
	GuiData * gui_data = g_new (GuiData, 1);
	gui_data->obj = g_object_ref (obj);
	gui_data->aux = FALSE;
	gui_data->error = NULL;
	gui_data->thread = g_thread_new ("vn-gui-reconnect",
		(GThreadFunc) vn_gui_reconnect_thread, gui_data);
}

/*
 * Saves the login information and closes the main GUI. 
 */
static gboolean vn_gui_logout_idle (GuiData * gui_data)
{
	vn_gui_close (gui_data->obj);

	if (gui_data->aux)
		g_signal_emit (gui_data->obj, signals[EXIT], 0);
	else
		g_signal_emit (gui_data->obj, signals[LOGOUT], 0);

	return G_SOURCE_REMOVE;
}

/*
 * Thread function that tryes to close the connection asynchronously.
 */
static void vn_gui_logout_thread (GuiData * gui_data)
{
	db_conn_close (gui_data->obj->conn, TRUE);
	g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
		(GSourceFunc) vn_gui_logout_idle, gui_data, (GDestroyNotify) gui_data_free);
}

/*
 * Closes the connection and the user interface.
 */
void vn_gui_logout (VnGui * obj, gboolean exit)
{
	GuiData * gui_data = g_new (GuiData, 1);
	gui_data->obj = g_object_ref (obj);
	gui_data->aux = exit;
	gui_data->error = NULL;
	gui_data->thread = g_thread_new ("vn-gui-close",
		(GThreadFunc) vn_gui_logout_thread, gui_data);
}

static void vn_gui_hide_form (VnWindow * window)
{
	VnForm * form = window->active_form;

	if (form)
	{
		gint i, size;
		const GActionEntry * actions = vn_form_get_actions (form, &size);
		GMenuModel * menu = vn_form_get_menu_model (form);

		for (i = 0; i < size; i++)
			g_action_map_remove_action (G_ACTION_MAP (window->widget),
				actions[i].name);

		if (menu) 
			vn_gui_set_menu_accels (window->obj, menu, FALSE);

		gtk_widget_hide (window->menu_button);
		window->active_form = NULL;
	}
}

static void vn_gui_set_show_tabs (VnWindow * window)
{
	gtk_notebook_set_show_tabs (window->notebook,
		gtk_notebook_get_n_pages (window->notebook) > 1);
}

//--------------------------------------------------- Window handlers

/*
 * Called when the last window is closed.
 */
gboolean vn_gui_on_last_window_deleted (GtkWidget * widget, GdkEvent * event, VnWindow * window)
{
	VnGui * obj = window->obj;

	if (obj->windows->next)
		return FALSE;

	vn_gui_logout (obj, TRUE);
	return TRUE;
}

/*
 * Called when a window is closed.
 */
void vn_gui_on_window_destroyed (GtkWindow * widget, VnWindow * window)
{
	VnGui * obj = window->obj;
	vn_gui_free_window (obj, window);
	obj->windows = g_slist_remove (obj->windows, window);
}

/*
 * Called when any window of the application gets the focus. 
 */
gboolean vn_gui_on_window_focused (GtkWidget * widget, GdkEvent * event, VnWindow * window)
{
	window->obj->active_window = window;
	return FALSE;
}

//--------------------------------------------------- Notebook handlers

/*
 * Called when a page is detached from a notebook. This function creates a new
 * window with a notebook and puts the page in.
 * Connected to the "create-window" signal of GtkNotebook.
 */
GtkNotebook * vn_gui_on_page_detached (GtkNotebook * old_notebook,
	GtkWidget * page, gint x, gint y, VnWindow * window)
{
	VnWindow * new_window = vn_gui_create_window (window->obj, x, y);
	return (window) ? new_window->notebook : NULL;
}

/*
 * Called when the focus changes from a page to another. It is also used in
 * newly opened pages.
 */
void vn_gui_on_switch_page (GtkNotebook * notebook, VnForm * form, guint num, VnWindow * window)
{
	GtkTreeIter * iter;
	GMenuModel * menu;
	VnGui * obj = window->obj;

	vn_gui_hide_form (window);
	window->active_form = form;

	if ((iter = g_hash_table_lookup (obj->forms, vn_form_get_name (form))))
	{
		gchar * form_title;

		gtk_tree_model_get (GTK_TREE_MODEL (obj->tree),
			iter, COL_TITLE, &form_title, -1);
		gtk_header_bar_set_title (window->header, form_title);
		g_free (form_title);
	}

	// Set active form Menu

	menu = vn_form_get_menu_model (form);

	if (menu)
	{
		gint size;
		const GActionEntry * actions = vn_form_get_actions (form, &size);

		g_action_map_add_action_entries (G_ACTION_MAP (window->widget),
			actions, size, form);

		vn_gui_set_menu_accels (obj, menu, TRUE);

		gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (window->menu_button), menu);
		gtk_widget_show (window->menu_button);
	}
}

/*
 * Called when a page is removed from its notebook. If the window is not the
 * last one and its the last page destroys the window too.
 */
void vn_gui_on_page_removed (GtkNotebook * notebook,
	GtkWidget * page, guint num, VnWindow * window)
{
	if (gtk_notebook_get_n_pages (notebook) < 1)
	{
		if (!window->obj->windows->next)
		{
			vn_gui_hide_form (window);
			gtk_header_bar_set_title (window->header, window->obj->app_title);
		}
		else
			gtk_widget_destroy (GTK_WIDGET (window->widget));
	}

	vn_gui_set_show_tabs (window);
}

/*
 * Called each time a page is added to a notebook.
 */
void vn_gui_on_page_added (GtkNotebook * notebook, GtkWidget * page, guint num,
	VnWindow * window)
{
	vn_gui_set_show_tabs (window);
}

//--------------------------------------------------- Action handlers

/*
 * Opens a form when the action associated to it is activated.
 */
void vn_gui_on_open_form_activated (GSimpleAction * a, GVariant * p, gpointer obj)
{
	VnWindow * window = obj;
	vn_gui_open_form_at_window (window->obj, g_variant_get_string (p, NULL), window);
}

/*
 * New empty window action handler.
 */
void vn_gui_on_new_window_activated (GSimpleAction * a, GVariant * v, gpointer obj)
{
	gint x , y;
	VnWindow * w;
	VnGui * o = obj;

	gtk_window_get_position (o->active_window->widget, &x, &y);
	w = vn_gui_create_window (obj, x + 100, y + 100);
	gtk_window_resize (w->widget, 500, 500);
}

/*
 * Logout action handler.
 */
void vn_gui_on_logout_activated (GSimpleAction * a, GVariant * v, gpointer obj)
{
	vn_gui_logout (obj, FALSE);
}

/*
 * Reconnects to database.
 */
void vn_gui_on_open_activated (GSimpleAction * a, GVariant * v, gpointer obj)
{
	vn_gui_reconnect (obj);
}

/*
 * Shows a window with program information.
 */
void vn_gui_on_about_activated (GSimpleAction * a, GVariant * v, gpointer obj)
{
	gtk_dialog_run (VN_GUI (obj)->active_window->about);
}

/*
 * Exit action handler.
 */
void vn_gui_on_exit_activated (GSimpleAction * a, GVariant * v, gpointer obj)
{
	vn_gui_logout (obj, TRUE);
}

/*
 * Closes the current tab when the close-tab action is activated.
 */
void vn_gui_on_close_tab_activated (GSimpleAction * a, GVariant * v, gpointer w)
{
	VnWindow * window = w;
	VnGui * obj = window->obj;

	if (window->active_form)
		vn_gui_close_form (window->obj, window->active_form);
	else if (!obj->windows->next)
		vn_gui_logout (obj, TRUE);
	else
		gtk_widget_destroy (GTK_WIDGET (window->widget));
}

//--------------------------------------------------- Connection handlers

/*
 * Called when there is a query error in the connection.
 */
static void vn_gui_on_conn_error (DbConn * conn, const GError * error, VnGui * obj)
{
	vn_gui_show_error (obj, error);
}

/*
 * Enables/disables the #GtkSpinner when connection is loading.
 */
static void vn_gui_on_conn_status_changed (DbConn * conn, DbConnStatus status, VnGui * obj)
{
	GSList * l;
	gchar * status_text;

	if (status & DB_CONN_CLOSING)
		status_text = _("Closing connection");
	else if (status & DB_CONN_TRANSACTION)
		status_text = _("Transaction started");
	else if (status & DB_CONN_OPENING)
		status_text = _("Connecting");
	else if (status & DB_CONN_LOST)
		status_text = _("Connection lost");
	else if (status == DB_CONN_CLOSED)
		status_text = _("Connection closed");
	else if (status & DB_CONN_LOADING)
		status_text = _("Loading");
	else
		status_text = _("Ready");

	for (l = obj->windows; l; l = l->next)
	{
		VnWindow * win = l->data;
		gtk_widget_set_tooltip_text (win->spinner, status_text);

		if (status & DB_CONN_LOADING)
		{
			gtk_widget_set_opacity (win->spinner, 1);
			gtk_spinner_start (GTK_SPINNER (win->spinner));
		}
		else
		{
			gtk_widget_set_opacity (win->spinner, 0);
			gtk_spinner_stop (GTK_SPINNER (win->spinner));
		}
	}
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Public

/**
 * vn_gui_open:
 * @obj: a #VnGui
 * 
 * Shows the main GUI.
 **/
void vn_gui_open (VnGui * obj)
{
	GtkBuilder * builder;
	GKeyFile * config;
	GError * err = NULL;

	g_return_if_fail (VN_IS_GUI (obj));

	builder = gtk_builder_new ();

	// Loading Application Menu

	if (gtk_builder_add_from_file (builder, MENU_UI, &err))
	{
		GSList * n;
		GMenu * section;
		GMenuModel * menu = G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu"));

		g_action_map_add_action_entries (G_ACTION_MAP (obj->app), app_entries,
			G_N_ELEMENTS (app_entries), obj);
		gtk_application_set_app_menu (obj->app, menu);

		obj->main_menu = gtk_builder_get (builder, "win-menu");
		section = gtk_builder_get (builder, "modules");

		for (n = obj->modules; n; n = n->next)
		{
			VnMod * mod = VN_MOD (n->data);
			GMenuModel * mod_menu = vn_mod_get_menu_model (mod);

			if (mod_menu)
			{
				g_menu_prepend_submenu (section,
					g_strdup (vn_mod_get_title (mod)), mod_menu);
				g_object_unref (mod_menu);
			}
		}
	}
	else if (err)
	{
		g_warning ("VnGui: %s", err->message);
		g_error_free (err);
	}

	// Restoring interface

	config = g_key_file_new ();

	if (g_key_file_load_from_file (config, obj->config_file, 0, NULL))
	{
		gsize m, n, len;
		gint x, y, width, height;
		gboolean maximized;
		gchar ** windows, ** forms;
		VnWindow * window;

		windows = g_key_file_get_groups (config, &len);

		for (m = 0; m < len; m++)
		{
			x = g_key_file_get_integer (config, windows[m], "x", NULL);
			y = g_key_file_get_integer (config, windows[m], "y", NULL);
			width = g_key_file_get_integer (config, windows[m], "width", NULL);
			height = g_key_file_get_integer (config, windows[m], "height", NULL);
			maximized = g_key_file_get_boolean (config, windows[m], "maximized", NULL);

			window = vn_gui_create_window (obj, x, y);
			gtk_window_resize (window->widget, width, height);

			if (maximized)
				gtk_window_maximize (window->widget);

			forms = g_key_file_get_string_list (config, windows[m], "forms", NULL, NULL);

			for (n = 0; forms[n]; n++)
				vn_gui_open_form_at_window (obj, forms[n], window);

			g_key_file_remove_group (config, windows[m], NULL);
			g_strfreev (forms);
		}

		g_strfreev (windows);
	}
	else
	{
		VnWindow * window = vn_gui_create_window (obj, 100, 100);
		gtk_window_resize (window->widget, 500, 400);
	}

	g_key_file_free (config);

	g_object_connect (obj->conn
		,"signal::error", vn_gui_on_conn_error, obj
		,"signal::status-changed", vn_gui_on_conn_status_changed, obj
		,NULL
	);

	g_object_unref (builder);
}

/**
 * vn_gui_save:
 * @obj: the #VnGui
 *
 * Saves the GUI configuration.
 **/
void vn_gui_save (VnGui * obj)
{
	gint len;
	gint j, i = 0;
	gchar * group;
	const gchar ** forms;
	GSList * m;
	GList * nb_pages, * n;
	VnWindow * window;
	gint x, y, width, height;
	GdkWindow * win;
	gboolean maximized;
	GKeyFile * config;

	g_return_if_fail (VN_IS_GUI (obj));

	// Saving the interface configuration

	config = g_key_file_new ();

	for (m = obj->windows; m; m = m->next)
	{
		window = (VnWindow *) m->data;
		group = g_strdup_printf ("window%d", i++);
		
		// Saving the window position and size
		
		gtk_window_get_position (window->widget, &x, &y);
		gtk_window_get_size (window->widget, &width, &height);

		g_key_file_set_integer (config, group, "x", x);
		g_key_file_set_integer (config, group, "y", y);
		g_key_file_set_integer (config, group, "width", width);
		g_key_file_set_integer (config, group, "height", height);

		g_object_get (window->widget, "window", &win, NULL);
		maximized = (gdk_window_get_state (win) & GDK_WINDOW_STATE_MAXIMIZED);
		g_object_unref (win);
		g_key_file_set_boolean (config, group, "maximized", maximized);

		// Saving the forms opened at window

		nb_pages = gtk_container_get_children (GTK_CONTAINER (window->notebook));
		len = gtk_notebook_get_n_pages (window->notebook);
		forms = g_new (const gchar *, len);
		
		for (j = 0, n = nb_pages; n; n = n->next)
			forms[j++] = vn_form_get_name (n->data);

		g_key_file_set_string_list (config, group, "forms", forms, len);

		g_list_free (nb_pages);
		g_free (forms);
		g_free (group);
	}

	gvn_key_file_save (config, obj->config_file);
	g_key_file_free (config);
}

/**
 * vn_gui_create_window:
 * @obj: the #VnGui
 * @x: the x coordinate
 * @y: the y coordinate
 *
 * Creates a new window.
 * 
 * Return value: VnWindow.
 **/
VnWindow * vn_gui_create_window (VnGui * obj, gint x, gint y)
{
	GtkBuilder * builder;
	VnWindow * window = NULL;
	GError * err = NULL;

	g_return_val_if_fail (VN_IS_GUI (obj), NULL);

	builder = gtk_builder_new ();

	if (gtk_builder_add_from_file (builder, MAIN_UI, &err))
	{
		GtkWindow * widget = gtk_builder_get (builder, "window");
		GtkNotebook * notebook = gtk_builder_get (builder, "notebook");
		gtk_window_move (widget, x, y);

		window = vn_gui_add_window (obj, widget, notebook);
		window->about = gtk_builder_get (builder, "about");
		gtk_builder_connect_signals (builder, window);
	}
	else
	{
		g_warning ("VnGui: %s", err->message);
		g_error_free (err);
	}

	g_object_unref (builder);
	return window;
}

/**
 * vn_gui_open_form_at_window:
 * @obj: the #VnGui
 * @form_name: the name of the form that you want to open
 * @window: the #VnWindow where the form will be opened
 * 
 * Opens a new form at the specified window.
 *
 * Return value: (transfer none): the created #VnForm
 **/
VnForm * vn_gui_open_form_at_window (VnGui * obj, const gchar * form_name, VnWindow * window)
{
	gchar * icon;
	gchar * title;
	VnMod * module;
	GType form_type;
	GtkBox * hbox;
	GtkWidget * form;
	GtkWidget * widget;
	GtkWidget * button;
	GtkTreeIter * iter;
	GtkNotebook * notebook = NULL;

	g_return_val_if_fail (VN_IS_GUI (obj), NULL);

	iter = g_hash_table_lookup (obj->forms, form_name);

	if (!iter)
	{
		g_warning ("VnGui: Form %s doesn't exist", form_name);
		return NULL;
	}

	gtk_tree_model_get (GTK_TREE_MODEL (obj->tree), iter
		,COL_ICON		,&icon
		,COL_TITLE		,&title
		,COL_MODULE		,&module
		,COL_TYPE		,&form_type
		,-1
	);

	form = g_object_new (form_type
		,"name"		,form_name
		,"gui"		,obj
		,"module"	,module
		,NULL
	);
	vn_form_open (VN_FORM (form));

	hbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5));

	widget = gtk_image_new_from_icon_name (icon, GTK_ICON_SIZE_MENU);
	gtk_box_pack_start (hbox, widget, FALSE, FALSE, 0);

	widget = gtk_label_new (title);
	gtk_box_pack_start (hbox, widget, TRUE, TRUE, 0);

	button = gtk_button_new ();
	g_signal_connect (button, "clicked",
		G_CALLBACK (vn_gui_close_form), form);
	gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
	gtk_box_pack_start (hbox, button, FALSE, FALSE, 0);

	widget = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
	gtk_button_set_image (GTK_BUTTON (button), widget);

	if (!window)
		window = obj->active_window;

	notebook = window->notebook;
	gtk_notebook_set_current_page (notebook,
		gtk_notebook_append_page (notebook, form, GTK_WIDGET (hbox)));
	gtk_notebook_set_tab_detachable (notebook, form, TRUE);
	gtk_notebook_set_tab_reorderable (notebook, form, TRUE);
	gtk_container_child_set (GTK_CONTAINER (notebook), form,
		"tab-expand", TRUE, NULL);
	gtk_widget_show_all (GTK_WIDGET (hbox));
	gtk_widget_show (form);

	g_free (icon);
	g_free (title);

	return VN_FORM (form);
}

/**
 * vn_gui_open_form:
 * @obj: the #VnGui
 * @form_name: the name of the form that you want to open
 * 
 * Opens a new form, creating a new page for it at the main notebook.
 *
 * Return value: (transfer none): the created #VnForm
 **/
VnForm * vn_gui_open_form (VnGui * obj, const gchar * form_name)
{
	g_return_val_if_fail (VN_IS_GUI (obj), NULL);
	g_return_val_if_fail (form_name, NULL);

	return vn_gui_open_form_at_window (obj, form_name, NULL);
}

/**
 * vn_gui_close_form:
 * @obj: the #VnGui
 * @form: the #VnForm that you want to close
 * 
 * Closes a form.
 **/
void vn_gui_close_form (VnGui * obj, VnForm * form)
{
	gint num;
	GtkNotebook * notebook;

	g_return_if_fail (VN_IS_FORM (form));

	notebook = GTK_NOTEBOOK (gtk_widget_get_ancestor (
		GTK_WIDGET (form), GTK_TYPE_NOTEBOOK));
	num = gtk_notebook_page_num (notebook, GTK_WIDGET (form));
	gtk_notebook_remove_page (notebook, num);
}

/**
 * vn_gui_get_conn:
 * @obj: a #VnGui
 * 
 * Gets the Data Base connection used by @obj
 *
 * Return value: (transfer none): the #DbConn
 **/
DbConn * vn_gui_get_conn (VnGui * obj)
{
	g_return_val_if_fail (VN_IS_GUI (obj), NULL);
	
	return obj->conn;
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Properties

enum
{
	  PROP_CONN = 1
	 ,PROP_APP
};

static void vn_gui_set_property (VnGui * obj, guint id,
	const GValue * value, GParamSpec * pspec)
{
	switch (id)
	{
		case PROP_CONN:
		{
			gchar * query_path = g_build_path (G_SEARCHPATH_SEPARATOR_S
				,_VN_MODULE_QUERY_DIR
				,g_getenv ("VN_MODULE_QUERY_PATH")
				,NULL
			);

			obj->conn = g_value_dup_object (value);
			db_conn_set_query_path (obj->conn, query_path);

			g_free (query_path);
			break;
		}
		case PROP_APP:
		{
			gint n;
			const gchar * app_id;
			gchar * app_name;
			gchar * config_dir;
			gchar * lib_path, * data_path;

			obj->app = g_value_dup_object (value);

			app_id = g_application_get_application_id (G_APPLICATION (obj->app));
			app_name = g_strrstr (app_id, ".") + 1;

			obj->app_title = g_strdup (app_name);
			obj->app_title[0] = g_ascii_toupper (obj->app_title[0]);

			config_dir = g_build_filename (g_get_user_config_dir (), app_name, NULL);
			g_mkdir_with_parents (config_dir, 0700);
			obj->config_file = g_build_filename (config_dir, "gui.ini", NULL);
			g_free (config_dir);

			// Setting module search paths
			lib_path = g_build_path (G_SEARCHPATH_SEPARATOR_S
				,_VN_MODULE_LIB_DIR
				,g_getenv ("VN_MODULE_LIB_PATH")
				,NULL
			);
			data_path = g_build_path (G_SEARCHPATH_SEPARATOR_S
				,_VN_MODULE_DATA_DIR
				,g_getenv ("VN_MODULE_DATA_PATH")
				,NULL
			);

			obj->lib_dirs = g_strsplit (lib_path, G_SEARCHPATH_SEPARATOR_S, 0);
			obj->data_dirs = g_strsplit (data_path, G_SEARCHPATH_SEPARATOR_S, 0);

			g_free (data_path);
			g_free (lib_path);

			// Initializing modules

			for (n = 0; obj->data_dirs[n]; n++)
			{
				const gchar * file;
				GError * err = NULL;
				GDir * dir = g_dir_open (obj->data_dirs[n], 0, &err);

				if (dir)
				{
					while ((file = g_dir_read_name (dir)))	
					if (!g_strcmp0 (".xml", g_strrstr (file, ".")))
						vn_gui_load_module (obj, obj->data_dirs[n], file);

					g_dir_close (dir);
				}
				else
				{
					g_warning ("VnGui: Error opening directory at module data path: %s"
						,err->message);
					g_error_free (err);
				}
			}

			break;
		}
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec);
	}
}

static void vn_gui_get_property (VnGui * obj, guint id,
	GValue * value, GParamSpec * pspec)
{	
	switch (id)
	{
		case PROP_CONN:
			g_value_set_object (value, obj->conn);
			break;
		case PROP_APP:
			g_value_set_object (value, obj->app);
			break;
		default:		
			G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec);
	}
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Class

static void vn_gui_init (VnGui * obj)
{
	obj->active_window = NULL;
	obj->windows = NULL;
	obj->conn = NULL;
	obj->app = NULL;
	obj->app_title = NULL;
	obj->modules = NULL;
	obj->lib_dirs = NULL;
	obj->data_dirs = NULL;
	obj->config_file = NULL;
	obj->main_menu = NULL;
	obj->modules = NULL;

	obj->forms = g_hash_table_new_full (
		 (GHashFunc) g_str_hash
		,(GEqualFunc) g_str_equal
		,(GDestroyNotify) g_free
		,(GDestroyNotify) g_free
	);
	obj->tree = gtk_tree_store_new (COL_COUNT
		,G_TYPE_STRING		// COL_ICON
		,G_TYPE_STRING		// COL_NAME
		,G_TYPE_STRING		// COL_TITLE
		,G_TYPE_GTYPE		// COL_TYPE
		,G_TYPE_OBJECT		// COL_MODULE
	);
}

static void vn_gui_finalize (VnGui * obj)
{
	vn_gui_close (obj);
	db_conn_close (obj->conn, FALSE);

	g_hash_table_unref (obj->forms);
	g_slist_free_full (obj->modules, g_object_unref);
	g_clear_object (&obj->conn);
	g_clear_object (&obj->tree);
	g_clear_object (&obj->app);
	g_strfreev (obj->lib_dirs);
	g_strfreev (obj->data_dirs);
	g_free (obj->config_file);
	g_free (obj->app_title);
	G_OBJECT_CLASS (vn_gui_parent_class)->finalize (G_OBJECT (obj));
}

static void vn_gui_class_init (VnGuiClass * k)
{
	GObjectClass * klass = G_OBJECT_CLASS (k);
	klass->finalize = (GObjectFinalizeFunc) vn_gui_finalize;
	klass->set_property = (GObjectSetPropertyFunc) vn_gui_set_property;
	klass->get_property = (GObjectGetPropertyFunc) vn_gui_get_property;

	signals[LOGOUT] = g_signal_new ("logout",
		VN_TYPE_GUI, G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
		g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0
	);
	signals[EXIT] = g_signal_new ("exit",
		VN_TYPE_GUI, G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
		g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0
	);

	g_object_class_install_property (klass, PROP_CONN,
		g_param_spec_object ("conn"
			,_("Connection")
			,_("The connection used by Gui")
			,DB_TYPE_CONN
			,G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
	));
	g_object_class_install_property (klass, PROP_APP,
		g_param_spec_object ("app"
			,_("Application")
			,_("The application handler for the entire program")
			,GTK_TYPE_APPLICATION
			,G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
	));
}