/*
 * gedit-spell-plugin.c
 *
 * Copyright (C) 2002-2005 Paolo Maggi
 * Copyright (C) 2015-2016 Sébastien Wilmet
 *
 * 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, 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 "gedit-spell-plugin.h"

#include <glib/gi18n.h>
#include <gedit/gedit-debug.h>
#include <gedit/gedit-app.h>
#include <gedit/gedit-window.h>
#include <gedit/gedit-window-activatable.h>
#include <gspell/gspell.h>

#include "gedit-spell-app-activatable.h"

#ifdef G_OS_WIN32
#define GEDIT_METADATA_ATTRIBUTE_SPELL_LANGUAGE "spell-language"
#define GEDIT_METADATA_ATTRIBUTE_SPELL_ENABLED  "spell-enabled"
#else
#define GEDIT_METADATA_ATTRIBUTE_SPELL_LANGUAGE "metadata::gedit-spell-language"
#define GEDIT_METADATA_ATTRIBUTE_SPELL_ENABLED  "metadata::gedit-spell-enabled"
#endif

#define SPELL_ENABLED_STR "1"

static void gedit_window_activatable_iface_init (GeditWindowActivatableInterface *iface);

struct _GeditSpellPluginPrivate
{
	GeditWindow *window;
};

enum
{
	PROP_0,
	PROP_WINDOW
};

G_DEFINE_DYNAMIC_TYPE_EXTENDED (GeditSpellPlugin,
				gedit_spell_plugin,
				PEAS_TYPE_EXTENSION_BASE,
				0,
				G_IMPLEMENT_INTERFACE_DYNAMIC (GEDIT_TYPE_WINDOW_ACTIVATABLE,
							       gedit_window_activatable_iface_init)
				G_ADD_PRIVATE_DYNAMIC (GeditSpellPlugin))

static void
gedit_spell_plugin_set_property (GObject      *object,
				 guint         prop_id,
				 const GValue *value,
				 GParamSpec   *pspec)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (object);

	switch (prop_id)
	{
		case PROP_WINDOW:
			plugin->priv->window = GEDIT_WINDOW (g_value_dup_object (value));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
gedit_spell_plugin_get_property (GObject    *object,
				 guint       prop_id,
				 GValue     *value,
				 GParamSpec *pspec)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (object);

	switch (prop_id)
	{
		case PROP_WINDOW:
			g_value_set_object (value, plugin->priv->window);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
gedit_spell_plugin_dispose (GObject *object)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (object);

	gedit_debug_message (DEBUG_PLUGINS, "GeditSpellPlugin disposing");

	g_clear_object (&plugin->priv->window);

	G_OBJECT_CLASS (gedit_spell_plugin_parent_class)->dispose (object);
}

static void
gedit_spell_plugin_class_init (GeditSpellPluginClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->set_property = gedit_spell_plugin_set_property;
	object_class->get_property = gedit_spell_plugin_get_property;
	object_class->dispose = gedit_spell_plugin_dispose;

	g_object_class_override_property (object_class, PROP_WINDOW, "window");
}

static void
gedit_spell_plugin_class_finalize (GeditSpellPluginClass *klass)
{
}

static void
gedit_spell_plugin_init (GeditSpellPlugin *plugin)
{
	gedit_debug_message (DEBUG_PLUGINS, "GeditSpellPlugin initializing");

	plugin->priv = gedit_spell_plugin_get_instance_private (plugin);
}

static GspellChecker *
get_spell_checker (GeditDocument *doc)
{
	GspellTextBuffer *gspell_buffer;

	gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (GTK_TEXT_BUFFER (doc));
	return gspell_text_buffer_get_spell_checker (gspell_buffer);
}

static const GspellLanguage *
get_language_from_metadata (GeditDocument *doc)
{
	const GspellLanguage *lang = NULL;
	gchar *language_code = NULL;

	language_code = gedit_document_get_metadata (doc, GEDIT_METADATA_ATTRIBUTE_SPELL_LANGUAGE);

	if (language_code != NULL)
	{
		lang = gspell_language_lookup (language_code);
		g_free (language_code);
	}

	return lang;
}

static void
check_spell_cb (GSimpleAction *action,
		GVariant      *parameter,
		gpointer       data)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (data);
	GeditSpellPluginPrivate *priv;
	GeditView *view;
	GspellNavigator *navigator;
	GtkWidget *dialog;

	gedit_debug (DEBUG_PLUGINS);

	priv = plugin->priv;

	view = gedit_window_get_active_view (priv->window);
	g_return_if_fail (view != NULL);

	navigator = gspell_navigator_text_view_new (GTK_TEXT_VIEW (view));
	dialog = gspell_checker_dialog_new (GTK_WINDOW (priv->window), navigator);

	gtk_widget_show (dialog);
}

static void
language_dialog_response_cb (GtkDialog *dialog,
			     gint       response_id,
			     gpointer   user_data)
{
	if (response_id == GTK_RESPONSE_HELP)
	{
		gedit_app_show_help (GEDIT_APP (g_application_get_default ()),
				     GTK_WINDOW (dialog),
				     NULL,
				     "gedit-spellcheck");
		return;
	}

	gtk_widget_destroy (GTK_WIDGET (dialog));
}

static void
set_language_cb (GSimpleAction *action,
		 GVariant      *parameter,
		 gpointer       data)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (data);
	GeditSpellPluginPrivate *priv;
	GeditDocument *doc;
	GspellChecker *checker;
	const GspellLanguage *lang;
	GtkWidget *dialog;
	GtkWindowGroup *window_group;

	gedit_debug (DEBUG_PLUGINS);

	priv = plugin->priv;

	doc = gedit_window_get_active_document (priv->window);
	g_return_if_fail (doc != NULL);

	checker = get_spell_checker (doc);
	g_return_if_fail (checker != NULL);

	lang = gspell_checker_get_language (checker);

	dialog = gspell_language_chooser_dialog_new (GTK_WINDOW (priv->window),
						     lang,
						     GTK_DIALOG_MODAL |
						     GTK_DIALOG_DESTROY_WITH_PARENT);

	g_object_bind_property (dialog, "language",
				checker, "language",
				G_BINDING_DEFAULT);

	window_group = gedit_window_get_group (priv->window);

	gtk_window_group_add_window (window_group, GTK_WINDOW (dialog));

	gtk_dialog_add_button (GTK_DIALOG (dialog),
			       _("_Help"),
			       GTK_RESPONSE_HELP);

	g_signal_connect (dialog,
			  "response",
			  G_CALLBACK (language_dialog_response_cb),
			  NULL);

	gtk_widget_show (dialog);
}

static void
inline_checker_activate_cb (GSimpleAction *action,
			    GVariant      *parameter,
			    gpointer       data)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (data);
	GeditSpellPluginPrivate *priv = plugin->priv;
	GVariant *state;
	gboolean active;
	GeditView *view;

	gedit_debug (DEBUG_PLUGINS);

	state = g_action_get_state (G_ACTION (action));
	g_return_if_fail (state != NULL);

	active = g_variant_get_boolean (state);
	g_variant_unref (state);

	/* We must toggle ourself the value. */
	active = !active;
	g_action_change_state (G_ACTION (action), g_variant_new_boolean (active));

	view = gedit_window_get_active_view (priv->window);
	if (view != NULL)
	{
		GeditDocument *doc;

		doc = GEDIT_DOCUMENT (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));

		/* Set metadata in the "activate" handler, not in "change-state"
		 * because "change-state" is called every time the state
		 * changes, not specifically when the user has changed the state
		 * herself. For example "change-state" is called to initialize
		 * the sate to the default value specified in the GActionEntry.
		 */
		gedit_document_set_metadata (doc,
					     GEDIT_METADATA_ATTRIBUTE_SPELL_ENABLED,
					     active ? SPELL_ENABLED_STR : NULL,
					     NULL);
	}
}

static void
inline_checker_change_state_cb (GSimpleAction *action,
				GVariant      *state,
				gpointer       data)
{
	GeditSpellPlugin *plugin = GEDIT_SPELL_PLUGIN (data);
	GeditSpellPluginPrivate *priv = plugin->priv;
	GeditView *view;
	gboolean active;

	gedit_debug (DEBUG_PLUGINS);

	active = g_variant_get_boolean (state);

	gedit_debug_message (DEBUG_PLUGINS, active ? "Inline Checker activated" : "Inline Checker deactivated");

	view = gedit_window_get_active_view (priv->window);
	if (view != NULL)
	{
		GspellTextView *gspell_view;

		gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
		gspell_text_view_set_inline_spell_checking (gspell_view, active);

		g_simple_action_set_state (action, g_variant_new_boolean (active));
	}
}

static void
update_ui (GeditSpellPlugin *plugin)
{
	GeditSpellPluginPrivate *priv;
	GeditTab *tab;
	GeditView *view = NULL;
	gboolean editable_view;
	GAction *check_spell_action;
	GAction *config_spell_action;
	GAction *inline_checker_action;

	gedit_debug (DEBUG_PLUGINS);

	priv = plugin->priv;

	tab = gedit_window_get_active_tab (priv->window);
	if (tab != NULL)
	{
		view = gedit_tab_get_view (tab);
	}

	editable_view = (view != NULL) && gtk_text_view_get_editable (GTK_TEXT_VIEW (view));

	check_spell_action = g_action_map_lookup_action (G_ACTION_MAP (priv->window),
	                                                 "check-spell");
	g_simple_action_set_enabled (G_SIMPLE_ACTION (check_spell_action),
				     editable_view);

	config_spell_action = g_action_map_lookup_action (G_ACTION_MAP (priv->window),
	                                                  "config-spell");
	g_simple_action_set_enabled (G_SIMPLE_ACTION (config_spell_action),
				     editable_view);

	inline_checker_action = g_action_map_lookup_action (G_ACTION_MAP (priv->window),
							    "inline-spell-checker");
	g_simple_action_set_enabled (G_SIMPLE_ACTION (inline_checker_action),
				     editable_view);

	/* Update only on normal state to avoid garbage changes during e.g. file
	 * loading.
	 */
	if (tab != NULL &&
	    gedit_tab_get_state (tab) == GEDIT_TAB_STATE_NORMAL)
	{
		GspellTextView *gspell_view;
		gboolean inline_checking_enabled;

		gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
		inline_checking_enabled = gspell_text_view_get_inline_spell_checking (gspell_view);

		g_action_change_state (inline_checker_action,
				       g_variant_new_boolean (inline_checking_enabled));
	}
}

static void
setup_inline_checker_from_metadata (GeditSpellPlugin *plugin,
				    GeditView        *view)
{
	GeditDocument *doc;
	gboolean enabled = FALSE;
	gchar *enabled_str;
	GspellTextView *gspell_view;
	GeditView *active_view;

	doc = GEDIT_DOCUMENT (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));

	enabled_str = gedit_document_get_metadata (doc, GEDIT_METADATA_ATTRIBUTE_SPELL_ENABLED);
	if (enabled_str != NULL)
	{
		enabled = g_str_equal (enabled_str, SPELL_ENABLED_STR);
		g_free (enabled_str);
	}

	gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
	gspell_text_view_set_inline_spell_checking (gspell_view, enabled);

	/* In case that the view is the active one we mark the spell action */
	active_view = gedit_window_get_active_view (plugin->priv->window);

	if (active_view == view)
	{
		GAction *action;

		action = g_action_map_lookup_action (G_ACTION_MAP (plugin->priv->window),
		                                     "inline-spell-checker");
		g_action_change_state (action, g_variant_new_boolean (enabled));
	}
}

static void
language_notify_cb (GspellChecker *checker,
		    GParamSpec    *pspec,
		    GeditDocument *doc)
{
	const GspellLanguage *lang;
	const gchar *language_code;

	g_return_if_fail (GEDIT_IS_DOCUMENT (doc));

	lang = gspell_checker_get_language (checker);
	g_return_if_fail (lang != NULL);

	language_code = gspell_language_get_code (lang);
	g_return_if_fail (language_code != NULL);

	gedit_document_set_metadata (doc,
				     GEDIT_METADATA_ATTRIBUTE_SPELL_LANGUAGE, language_code,
				     NULL);
}

static void
on_document_loaded (GeditDocument    *doc,
		    GeditSpellPlugin *plugin)
{
	GspellChecker *checker;
	GeditTab *tab;
	GeditView *view;

	checker = get_spell_checker (doc);

	if (checker != NULL)
	{
		const GspellLanguage *lang;

		lang = get_language_from_metadata (doc);

		if (lang != NULL)
		{
			g_signal_handlers_block_by_func (checker, language_notify_cb, doc);
			gspell_checker_set_language (checker, lang);
			g_signal_handlers_unblock_by_func (checker, language_notify_cb, doc);
		}
	}

	tab = gedit_tab_get_from_document (doc);
	view = gedit_tab_get_view (tab);
	setup_inline_checker_from_metadata (plugin, view);
}

static void
on_document_saved (GeditDocument *doc,
		   gpointer       user_data)
{
	GeditTab *tab;
	GeditView *view;
	GspellChecker *checker;
	const gchar *language_code = NULL;
	GspellTextView *gspell_view;
	gboolean inline_checking_enabled;

	/* Make sure to save the metadata here too */

	checker = get_spell_checker (doc);

	if (checker != NULL)
	{
		const GspellLanguage *lang;

		lang = gspell_checker_get_language (checker);
		if (lang != NULL)
		{
			language_code = gspell_language_get_code (lang);
		}
	}

	tab = gedit_tab_get_from_document (doc);
	view = gedit_tab_get_view (tab);

	gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
	inline_checking_enabled = gspell_text_view_get_inline_spell_checking (gspell_view);

	gedit_document_set_metadata (doc,
	                             GEDIT_METADATA_ATTRIBUTE_SPELL_ENABLED,
				     inline_checking_enabled ? SPELL_ENABLED_STR : NULL,
	                             GEDIT_METADATA_ATTRIBUTE_SPELL_LANGUAGE,
	                             language_code,
	                             NULL);
}

static void
activate_spell_checking_in_view (GeditSpellPlugin *plugin,
				 GeditView        *view)
{
	GeditDocument *doc;

	doc = GEDIT_DOCUMENT (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));

	/* It is possible that a GspellChecker has already been set, for example
	 * if a GeditTab has moved to another window.
	 */
	if (get_spell_checker (doc) == NULL)
	{
		const GspellLanguage *lang;
		GspellChecker *checker;
		GspellTextBuffer *gspell_buffer;

		lang = get_language_from_metadata (doc);
		checker = gspell_checker_new (lang);

		g_signal_connect_object (checker,
					 "notify::language",
					 G_CALLBACK (language_notify_cb),
					 doc,
					 0);

		gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (GTK_TEXT_BUFFER (doc));
		gspell_text_buffer_set_spell_checker (gspell_buffer, checker);
		g_object_unref (checker);

		setup_inline_checker_from_metadata (plugin, view);
	}

	g_signal_connect_object (doc,
				 "loaded",
				 G_CALLBACK (on_document_loaded),
				 plugin,
				 0);

	g_signal_connect_object (doc,
				 "saved",
				 G_CALLBACK (on_document_saved),
				 plugin,
				 0);
}

static void
disconnect_view (GeditSpellPlugin *plugin,
		 GeditView        *view)
{
	GtkTextBuffer *buffer;

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));

	/* It should still be the same buffer as the one where the signal
	 * handlers were connected. If not, we assume that the old buffer is
	 * finalized. And it is anyway safe to call
	 * g_signal_handlers_disconnect_by_func() if no signal handlers are
	 * found.
	 */
	g_signal_handlers_disconnect_by_func (buffer, on_document_loaded, plugin);
	g_signal_handlers_disconnect_by_func (buffer, on_document_saved, plugin);
}

static void
deactivate_spell_checking_in_view (GeditSpellPlugin *plugin,
				   GeditView        *view)
{
	GtkTextBuffer *gtk_buffer;
	GspellTextBuffer *gspell_buffer;
	GspellTextView *gspell_view;

	disconnect_view (plugin, view);

	gtk_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
	gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
	gspell_text_buffer_set_spell_checker (gspell_buffer, NULL);

	gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
	gspell_text_view_set_inline_spell_checking (gspell_view, FALSE);
}

static void
tab_added_cb (GeditWindow      *window,
	      GeditTab         *tab,
	      GeditSpellPlugin *plugin)
{
	activate_spell_checking_in_view (plugin, gedit_tab_get_view (tab));
}

static void
tab_removed_cb (GeditWindow      *window,
		GeditTab         *tab,
		GeditSpellPlugin *plugin)
{
	/* Don't deactivate completely the spell checking in @tab, since the tab
	 * can be moved to another window and we don't want to loose the spell
	 * checking settings (they are not saved in metadata for unsaved
	 * documents).
	 */
	disconnect_view (plugin, gedit_tab_get_view (tab));
}

static void
gedit_spell_plugin_activate (GeditWindowActivatable *activatable)
{
	GeditSpellPlugin *plugin;
	GeditSpellPluginPrivate *priv;
	GList *views;
	GList *l;

	const GActionEntry action_entries[] =
	{
		{ "check-spell", check_spell_cb },
		{ "config-spell", set_language_cb },
		{ "inline-spell-checker",
		  inline_checker_activate_cb,
		  NULL,
		  "false",
		  inline_checker_change_state_cb }
	};

	gedit_debug (DEBUG_PLUGINS);

	plugin = GEDIT_SPELL_PLUGIN (activatable);
	priv = plugin->priv;

	g_action_map_add_action_entries (G_ACTION_MAP (priv->window),
	                                 action_entries,
	                                 G_N_ELEMENTS (action_entries),
	                                 activatable);

	update_ui (plugin);

	views = gedit_window_get_views (priv->window);
	for (l = views; l != NULL; l = l->next)
	{
		activate_spell_checking_in_view (plugin, GEDIT_VIEW (l->data));
	}

	g_signal_connect (priv->window,
			  "tab-added",
			  G_CALLBACK (tab_added_cb),
			  activatable);

	g_signal_connect (priv->window,
			  "tab-removed",
			  G_CALLBACK (tab_removed_cb),
			  activatable);
}

static void
gedit_spell_plugin_deactivate (GeditWindowActivatable *activatable)
{
	GeditSpellPlugin *plugin;
	GeditSpellPluginPrivate *priv;
	GList *views;
	GList *l;

	gedit_debug (DEBUG_PLUGINS);

	plugin = GEDIT_SPELL_PLUGIN (activatable);
	priv = plugin->priv;

	g_action_map_remove_action (G_ACTION_MAP (priv->window), "check-spell");
	g_action_map_remove_action (G_ACTION_MAP (priv->window), "config-spell");
	g_action_map_remove_action (G_ACTION_MAP (priv->window), "inline-spell-checker");

	g_signal_handlers_disconnect_by_func (priv->window, tab_added_cb, activatable);
	g_signal_handlers_disconnect_by_func (priv->window, tab_removed_cb, activatable);

	views = gedit_window_get_views (priv->window);
	for (l = views; l != NULL; l = l->next)
	{
		deactivate_spell_checking_in_view (plugin, GEDIT_VIEW (l->data));
	}
}

static void
gedit_spell_plugin_update_state (GeditWindowActivatable *activatable)
{
	gedit_debug (DEBUG_PLUGINS);

	update_ui (GEDIT_SPELL_PLUGIN (activatable));
}

static void
gedit_window_activatable_iface_init (GeditWindowActivatableInterface *iface)
{
	iface->activate = gedit_spell_plugin_activate;
	iface->deactivate = gedit_spell_plugin_deactivate;
	iface->update_state = gedit_spell_plugin_update_state;
}

G_MODULE_EXPORT void
peas_register_types (PeasObjectModule *module)
{
	gedit_spell_plugin_register_type (G_TYPE_MODULE (module));
	gedit_spell_app_activatable_register (G_TYPE_MODULE (module));

	peas_object_module_register_extension_type (module,
						    GEDIT_TYPE_WINDOW_ACTIVATABLE,
						    GEDIT_TYPE_SPELL_PLUGIN);
}

/* ex:set ts=8 noet: */
