/* SPDX-FileCopyrightText: 2002-2005 - Paolo Maggi
 * SPDX-FileCopyrightText: 2015-2025 - Sébastien Wilmet
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "gedit-spell-window-activatable.h"
#include <glib/gi18n.h>
#include <gedit/gedit-app.h>
#include <gedit/gedit-window.h>
#include <gedit/gedit-window-activatable.h>
#include <gspell/gspell.h>
#include "gedit-spell-document.h"

static void gedit_window_activatable_iface_init (GeditWindowActivatableInterface *iface);

struct _GeditSpellWindowActivatablePrivate
{
	GeditWindow *window;
	GSettings *settings;
};

enum
{
	PROP_0,
	PROP_WINDOW
};

G_DEFINE_DYNAMIC_TYPE_EXTENDED (GeditSpellWindowActivatable,
				gedit_spell_window_activatable,
				G_TYPE_OBJECT,
				0,
				G_ADD_PRIVATE_DYNAMIC (GeditSpellWindowActivatable)
				G_IMPLEMENT_INTERFACE_DYNAMIC (GEDIT_TYPE_WINDOW_ACTIVATABLE,
							       gedit_window_activatable_iface_init))

static void
gedit_spell_window_activatable_set_property (GObject      *object,
					     guint         prop_id,
					     const GValue *value,
					     GParamSpec   *pspec)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (object);

	switch (prop_id)
	{
		case PROP_WINDOW:
			g_assert (self->priv->window == NULL);
			self->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_window_activatable_get_property (GObject    *object,
					     guint       prop_id,
					     GValue     *value,
					     GParamSpec *pspec)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (object);

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

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

static void
gedit_spell_window_activatable_dispose (GObject *object)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (object);

	g_clear_object (&self->priv->window);
	g_clear_object (&self->priv->settings);

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

static void
gedit_spell_window_activatable_class_init (GeditSpellWindowActivatableClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->set_property = gedit_spell_window_activatable_set_property;
	object_class->get_property = gedit_spell_window_activatable_get_property;
	object_class->dispose = gedit_spell_window_activatable_dispose;

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

static void
gedit_spell_window_activatable_class_finalize (GeditSpellWindowActivatableClass *klass)
{
}

static void
gedit_spell_window_activatable_init (GeditSpellWindowActivatable *self)
{
	self->priv = gedit_spell_window_activatable_get_instance_private (self);
	self->priv->settings = g_settings_new ("org.gnome.gedit.plugins.spell");
}

static void
check_spell_activate_cb (GSimpleAction *action,
			 GVariant      *parameter,
			 gpointer       user_data)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (user_data);
	GeditView *view;
	GspellNavigator *navigator;
	GtkWidget *dialog;

	view = gedit_window_get_active_view (self->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 (self->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
config_spell_activate_cb (GSimpleAction *action,
			  GVariant      *parameter,
			  gpointer       user_data)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (user_data);
	GeditDocument *document;
	GspellChecker *checker;
	const GspellLanguage *language;
	GtkWidget *dialog;
	GtkWindowGroup *window_group;

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

	checker = gedit_spell_document_get_spell_checker (document);
	g_return_if_fail (checker != NULL);

	language = gspell_checker_get_language (checker);

	dialog = gspell_language_chooser_dialog_new (GTK_WINDOW (self->priv->window),
						     language,
						     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 (self->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_spell_checker_activate_cb (GSimpleAction *action,
				  GVariant      *parameter,
				  gpointer       user_data)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (user_data);
	GVariant *state;
	gboolean active;
	GeditView *view;

	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 (self->priv->window);
	if (view != NULL)
	{
		GeditDocument *document;

		document = 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 state to the default value specified in the GActionEntry.
		 */
		gedit_spell_document_set_metadata_for_inline_spell_checking (document, active);
	}
}

static void
inline_spell_checker_change_state_cb (GSimpleAction *action,
				      GVariant      *state,
				      gpointer       user_data)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (user_data);
	GeditView *view;
	gboolean active;

	active = g_variant_get_boolean (state);

	view = gedit_window_get_active_view (self->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_actions_state (GeditSpellWindowActivatable *self)
{
	GeditTab *active_tab;
	GeditView *active_view = NULL;
	gboolean active_view_is_editable;
	GAction *check_spell_action;
	GAction *config_spell_action;
	GAction *inline_checker_action;

	active_tab = gedit_window_get_active_tab (self->priv->window);
	if (active_tab != NULL)
	{
		active_view = gedit_tab_get_view (active_tab);
	}

	active_view_is_editable = (active_view != NULL &&
				   gtk_text_view_get_editable (GTK_TEXT_VIEW (active_view)));

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

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

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

	/* Update only on normal state to avoid garbage changes during e.g. file
	 * loading.
	 */
	if (active_tab != NULL &&
	    gedit_tab_get_state (active_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 (active_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
restore_inline_checker_state (GeditSpellWindowActivatable *self,
			      GeditView                   *view)
{
	GeditDocument *document;
	gboolean inline_spell_checking_metadata_exists = FALSE;
	gboolean inline_spell_checking_metadata_value = FALSE;
	gboolean enabled;
	GspellTextView *gspell_view;
	GeditView *active_view;

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

	gedit_spell_document_get_metadata_for_inline_spell_checking (document,
								     &inline_spell_checking_metadata_exists,
								     &inline_spell_checking_metadata_value);

	if (inline_spell_checking_metadata_exists)
	{
		enabled = inline_spell_checking_metadata_value;
	}
	else
	{
		enabled = g_settings_get_boolean (self->priv->settings, "highlight-misspelled-words");
	}

	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 (self->priv->window);
	if (active_view == view)
	{
		GAction *action;

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

static void
document_loaded_cb (GeditDocument               *document,
		    GeditSpellWindowActivatable *self)
{
	GeditTab *tab;
	GeditView *view;

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

	gedit_spell_document_restore_language (document);
	restore_inline_checker_state (self, view);
}

static void
document_saved_cb (GeditDocument *document,
		   gpointer       user_data)
{
	/* Make sure to save the metadata here too */
	{
		GspellChecker *checker;
		const GspellLanguage *language = NULL;

		checker = gedit_spell_document_get_spell_checker (document);
		if (checker != NULL)
		{
			language = gspell_checker_get_language (checker);
		}

		gedit_spell_document_set_metadata_for_language (document, language);
	}
	{
		GeditTab *tab;
		GeditView *view;
		GspellTextView *gspell_view;
		gboolean inline_spell_checking;

		tab = gedit_tab_get_from_document (document);
		view = gedit_tab_get_view (tab);
		gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (view));
		inline_spell_checking = gspell_text_view_get_inline_spell_checking (gspell_view);

		gedit_spell_document_set_metadata_for_inline_spell_checking (document, inline_spell_checking);
	}
}

static void
activate_spell_checking_in_view (GeditSpellWindowActivatable *self,
				 GeditView                   *view)
{
	GeditDocument *document;

	document = 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 (gedit_spell_document_get_spell_checker (document) == NULL)
	{
		gedit_spell_document_setup_spell_checker (document);
		restore_inline_checker_state (self, view);
	}

	g_signal_connect_object (document,
				 "loaded",
				 G_CALLBACK (document_loaded_cb),
				 self,
				 G_CONNECT_DEFAULT);

	g_signal_connect_object (document,
				 "saved",
				 G_CALLBACK (document_saved_cb),
				 self,
				 G_CONNECT_DEFAULT);
}

static void
disconnect_view (GeditSpellWindowActivatable *self,
		 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, document_loaded_cb, self);
	g_signal_handlers_disconnect_by_func (buffer, document_saved_cb, self);
}

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

	disconnect_view (self, 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,
	      GeditSpellWindowActivatable *self)
{
	activate_spell_checking_in_view (self, gedit_tab_get_view (tab));
}

static void
tab_removed_cb (GeditWindow                 *window,
		GeditTab                    *tab,
		GeditSpellWindowActivatable *self)
{
	/* 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 (self, gedit_tab_get_view (tab));
}

static void
gedit_spell_window_activatable_activate (GeditWindowActivatable *activatable)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (activatable);
	GList *views;
	GList *l;

	const GActionEntry action_entries[] =
	{
		{ "check-spell", check_spell_activate_cb },
		{ "config-spell", config_spell_activate_cb },
		{ "inline-spell-checker",
		  inline_spell_checker_activate_cb,
		  NULL,
		  "false",
		  inline_spell_checker_change_state_cb }
	};

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

	update_actions_state (self);

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

	g_signal_connect_object (self->priv->window,
				 "tab-added",
				 G_CALLBACK (tab_added_cb),
				 self,
				 G_CONNECT_DEFAULT);

	g_signal_connect_object (self->priv->window,
				 "tab-removed",
				 G_CALLBACK (tab_removed_cb),
				 self,
				 G_CONNECT_DEFAULT);
}

static void
gedit_spell_window_activatable_deactivate (GeditWindowActivatable *activatable)
{
	GeditSpellWindowActivatable *self = GEDIT_SPELL_WINDOW_ACTIVATABLE (activatable);
	GList *views;
	GList *l;

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

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

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

static void
gedit_spell_window_activatable_update_state (GeditWindowActivatable *activatable)
{
	update_actions_state (GEDIT_SPELL_WINDOW_ACTIVATABLE (activatable));
}

static void
gedit_window_activatable_iface_init (GeditWindowActivatableInterface *iface)
{
	iface->activate = gedit_spell_window_activatable_activate;
	iface->deactivate = gedit_spell_window_activatable_deactivate;
	iface->update_state = gedit_spell_window_activatable_update_state;
}

void
gedit_spell_window_activatable_register (PeasObjectModule *module)
{
	gedit_spell_window_activatable_register_type (G_TYPE_MODULE (module));

	peas_object_module_register_extension_type (module,
						    GEDIT_TYPE_WINDOW_ACTIVATABLE,
						    GEDIT_TYPE_SPELL_WINDOW_ACTIVATABLE);
}
