/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * pluma-automatic-spell-checker.c
 * This file is part of pluma
 *
 * Copyright (C) 2002 Paolo Maggi
 * Copyright (C) 2012-2021 MATE Developers
 *
 * 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.
 */

/*
 * Modified by the pluma Team, 2002. See the AUTHORS file for a
 * list of people on the pluma Team.
 * See the ChangeLog files for a list of changes.
 */

/* This is a modified version of gtkspell 2.0.5  (gtkspell.sf.net) */
/* gtkspell - a spell-checking addon for GTK's TextView widget
 * Copyright (c) 2002 Evan Martin.
 */

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

#include <string.h>

#include <glib/gi18n.h>

#include "pluma-automatic-spell-checker.h"
#include "pluma-spell-utils.h"

struct _PlumaAutomaticSpellChecker {
	PlumaDocument		*doc;
	GSList 			*views;

	GtkTextMark 		*mark_insert_start;
	GtkTextMark		*mark_insert_end;
	gboolean 		 deferred_check;

	GtkTextTag 		*tag_highlight;
	GtkTextMark		*mark_click;

       	PlumaSpellChecker	*spell_checker;
};

static GQuark automatic_spell_checker_id = 0;
static GQuark suggestion_id = 0;

static void pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell);

static void
view_destroy (PlumaView *view, PlumaAutomaticSpellChecker *spell)
{
	pluma_automatic_spell_checker_detach_view (spell, view);
}

static void
check_word (PlumaAutomaticSpellChecker *spell, GtkTextIter *start, GtkTextIter *end)
{
	gchar *word;

	word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), start, end, FALSE);

	/*
	g_print ("Check word: %s [%d - %d]\n", word, gtk_text_iter_get_offset (start),
						gtk_text_iter_get_offset (end));
	*/

	if (!pluma_spell_checker_check_word (spell->spell_checker, word, -1))
	{
		/*
		g_print ("Apply tag: [%d - %d]\n", gtk_text_iter_get_offset (start),
						gtk_text_iter_get_offset (end));
		*/
		gtk_text_buffer_apply_tag (GTK_TEXT_BUFFER (spell->doc),
					   spell->tag_highlight,
					   start,
					   end);
	}

	g_free (word);
}

static void
check_range (PlumaAutomaticSpellChecker *spell,
	     GtkTextIter                 start,
	     GtkTextIter                 end,
	     gboolean                    force_all)
{
	/* we need to "split" on word boundaries.
	 * luckily, Pango knows what "words" are
	 * so we don't have to figure it out. */

	GtkTextIter wstart;
	GtkTextIter wend;
	GtkTextIter cursor;
	GtkTextIter precursor;
  	gboolean    highlight;

	/*
	g_print ("Check range: [%d - %d]\n", gtk_text_iter_get_offset (&start),
						gtk_text_iter_get_offset (&end));
	*/

	if (gtk_text_iter_inside_word (&end))
		gtk_text_iter_forward_word_end (&end);

	if (!gtk_text_iter_starts_word (&start))
	{
		if (gtk_text_iter_inside_word (&start) ||
		    gtk_text_iter_ends_word (&start))
		{
			gtk_text_iter_backward_word_start (&start);
		}
		else
		{
			/* if we're neither at the beginning nor inside a word,
			 * me must be in some spaces.
			 * skip forward to the beginning of the next word. */

			if (gtk_text_iter_forward_word_end (&start))
				gtk_text_iter_backward_word_start (&start);
		}
	}

	gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
					  &cursor,
					  gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (spell->doc)));

	precursor = cursor;
	gtk_text_iter_backward_char (&precursor);

  	highlight = gtk_text_iter_has_tag (&cursor, spell->tag_highlight) ||
  	            gtk_text_iter_has_tag (&precursor, spell->tag_highlight);

	gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
				    spell->tag_highlight,
				    &start,
				    &end);

	/* Fix a corner case when replacement occurs at beginning of buffer:
	 * An iter at offset 0 seems to always be inside a word,
  	 * even if it's not.  Possibly a pango bug.
	 */
  	if (gtk_text_iter_get_offset (&start) == 0)
	{
		gtk_text_iter_forward_word_end(&start);
		gtk_text_iter_backward_word_start(&start);
	}

	wstart = start;

	while (pluma_spell_utils_skip_no_spell_check (&wstart, &end) &&
	       gtk_text_iter_compare (&wstart, &end) < 0)
	{
		gboolean inword;

		/* move wend to the end of the current word. */
		wend = wstart;

		gtk_text_iter_forward_word_end (&wend);

		inword = (gtk_text_iter_compare (&wstart, &cursor) < 0) &&
			 (gtk_text_iter_compare (&cursor, &wend) <= 0);

		if (inword && !force_all)
		{
			/* this word is being actively edited,
			 * only check if it's already highligted,
			 * otherwise defer this check until later. */
			if (highlight)
				check_word (spell, &wstart, &wend);
			else
				spell->deferred_check = TRUE;
		}
		else
		{
			check_word (spell, &wstart, &wend);
			spell->deferred_check = FALSE;
		}

		/* now move wend to the beginning of the next word, */
		gtk_text_iter_forward_word_end (&wend);
		gtk_text_iter_backward_word_start (&wend);

		/* make sure we've actually advanced
		 * (we don't advance in some corner cases), */
		if (gtk_text_iter_equal (&wstart, &wend))
			break; /* we're done in these cases.. */

		/* and then pick this as the new next word beginning. */
		wstart = wend;
	}
}

static void
check_deferred_range (PlumaAutomaticSpellChecker *spell,
		      gboolean                    force_all)
{
	GtkTextIter start, end;

	gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
					  &start,
					  spell->mark_insert_start);
	gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
					  &end,
					  spell->mark_insert_end);

	check_range (spell, start, end, force_all);
}

/* insertion works like this:
 *  - before the text is inserted, we mark the position in the buffer.
 *  - after the text is inserted, we see where our mark is and use that and
 *    the current position to check the entire range of inserted text.
 *
 * this may be overkill for the common case (inserting one character). */

static void
insert_text_before (GtkTextBuffer *buffer, GtkTextIter *iter,
		gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
{
	gtk_text_buffer_move_mark (buffer, spell->mark_insert_start, iter);
}

static void
insert_text_after (GtkTextBuffer *buffer, GtkTextIter *iter,
                  gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
{
	GtkTextIter start;

	/* we need to check a range of text. */
	gtk_text_buffer_get_iter_at_mark (buffer, &start, spell->mark_insert_start);

	check_range (spell, start, *iter, FALSE);

	gtk_text_buffer_move_mark (buffer, spell->mark_insert_end, iter);
}

/* deleting is more simple:  we're given the range of deleted text.
 * after deletion, the start and end iters should be at the same position
 * (because all of the text between them was deleted!).
 * this means we only really check the words immediately bounding the
 * deletion.
 */

static void
delete_range_after (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end,
		PlumaAutomaticSpellChecker *spell)
{
	check_range (spell, *start, *end, FALSE);
}

static void
mark_set (GtkTextBuffer              *buffer,
	  GtkTextIter                *iter,
	  GtkTextMark                *mark,
	  PlumaAutomaticSpellChecker *spell)
{
	/* if the cursor has moved and there is a deferred check so handle it now */
	if ((mark == gtk_text_buffer_get_insert (buffer)) && spell->deferred_check)
		check_deferred_range (spell, FALSE);
}

static void
get_word_extents_from_mark (GtkTextBuffer *buffer,
			    GtkTextIter   *start,
			    GtkTextIter   *end,
			    GtkTextMark   *mark)
{
	gtk_text_buffer_get_iter_at_mark(buffer, start, mark);

	if (!gtk_text_iter_starts_word (start))
		gtk_text_iter_backward_word_start (start);

	*end = *start;

	if (gtk_text_iter_inside_word (end))
		gtk_text_iter_forward_word_end (end);
}

static void
remove_tag_to_word (PlumaAutomaticSpellChecker *spell, const gchar *word)
{
	GtkTextIter iter;
	GtkTextIter match_start, match_end;

	gboolean found;

	gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (spell->doc), &iter, 0);

	found = TRUE;

	while (found)
	{
		found = gtk_text_iter_forward_search (&iter,
				word,
				GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
				&match_start,
				&match_end,
				NULL);

		if (found)
		{
			if (gtk_text_iter_starts_word (&match_start) &&
			    gtk_text_iter_ends_word (&match_end))
			{
				gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
						spell->tag_highlight,
						&match_start,
						&match_end);
			}

			iter = match_end;
		}
	}
}

static void
add_to_dictionary (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
{
	gchar *word;

	GtkTextIter start, end;

	get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);

	word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
					 &start,
					 &end,
					 FALSE);

	pluma_spell_checker_add_word_to_personal (spell->spell_checker, word, -1);

	g_free (word);
}

static void
ignore_all (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
{
	gchar *word;

	GtkTextIter start, end;

	get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);

	word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
					 &start,
					 &end,
					 FALSE);

	pluma_spell_checker_add_word_to_session (spell->spell_checker, word, -1);

	g_free (word);
}

static void
replace_word (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
{
	gchar *oldword;
	const gchar *newword;

	GtkTextIter start, end;

	get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);

	oldword = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);

	newword =  g_object_get_qdata (G_OBJECT (menuitem), suggestion_id);
	g_return_if_fail (newword != NULL);

	gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (spell->doc));

	gtk_text_buffer_delete (GTK_TEXT_BUFFER (spell->doc), &start, &end);
	gtk_text_buffer_insert (GTK_TEXT_BUFFER (spell->doc), &start, newword, -1);

	gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (spell->doc));

	pluma_spell_checker_set_correction (spell->spell_checker,
				oldword, strlen (oldword),
				newword, strlen (newword));

	g_free (oldword);
}

static GtkWidget *
build_suggestion_menu (PlumaAutomaticSpellChecker *spell, const gchar *word)
{
	GtkWidget *topmenu, *menu;
	GtkWidget *mi;
	GSList *suggestions;
	GSList *list;
	gchar *label_text;

	topmenu = menu = gtk_menu_new();

	suggestions = pluma_spell_checker_get_suggestions (spell->spell_checker, word, -1);

	list = suggestions;

	if (suggestions == NULL)
	{
		/* no suggestions.  put something in the menu anyway... */
		GtkWidget *label;
		/* Translators: Displayed in the "Check Spelling" dialog if there are no suggestions for the current misspelled word */
		label = gtk_label_new (_("(no suggested words)"));

		mi = gtk_menu_item_new ();
		gtk_widget_set_sensitive (mi, FALSE);
		gtk_container_add (GTK_CONTAINER(mi), label);
		gtk_widget_show_all (mi);
		gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
	}
	else
	{
		gint count = 0;

		/* build a set of menus with suggestions. */
		while (suggestions != NULL)
		{
			GtkWidget *label;

			if (count == 10)
			{
				/* Separator */
				mi = gtk_menu_item_new ();
				gtk_widget_show (mi);
				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);

				mi = gtk_menu_item_new_with_mnemonic (_("_More..."));
				gtk_widget_show (mi);
				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);

				menu = gtk_menu_new ();
				gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
				count = 0;
			}

			label_text = g_strdup_printf ("<b>%s</b>", (gchar*) suggestions->data);

			label = gtk_label_new (label_text);
			gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
			gtk_label_set_xalign (GTK_LABEL (label), 0.0);

			mi = gtk_menu_item_new ();
			gtk_container_add (GTK_CONTAINER(mi), label);

			gtk_widget_show_all (mi);
			gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);

			g_object_set_qdata_full (G_OBJECT (mi),
				 suggestion_id,
				 g_strdup (suggestions->data),
				 (GDestroyNotify)g_free);

			g_free (label_text);
			g_signal_connect (mi,
					  "activate",
					  G_CALLBACK (replace_word),
					  spell);

			count++;

			suggestions = g_slist_next (suggestions);
		}
	}

	/* free the suggestion list */
	suggestions = list;

	while (list)
	{
		g_free (list->data);
		list = g_slist_next (list);
	}

	g_slist_free (suggestions);

	/* Separator */
	mi = gtk_menu_item_new ();
	gtk_widget_show (mi);
	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);

	/* Ignore all */
	mi = gtk_image_menu_item_new_with_mnemonic (_("_Ignore All"));
	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
				       gtk_image_new_from_icon_name ("go-bottom",
					       			     GTK_ICON_SIZE_MENU));

	g_signal_connect (mi,
			  "activate",
			  G_CALLBACK(ignore_all),
			  spell);

	gtk_widget_show_all (mi);

	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);

	/* + Add to Dictionary */
	mi = gtk_image_menu_item_new_with_mnemonic (_("_Add"));
	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
				       gtk_image_new_from_icon_name ("list-add",
					       			     GTK_ICON_SIZE_MENU));

	g_signal_connect (mi,
			  "activate",
			  G_CALLBACK (add_to_dictionary),
			  spell);

	gtk_widget_show_all (mi);

	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);

	return topmenu;
}

static void
populate_popup (GtkTextView *textview, GtkMenu *menu, PlumaAutomaticSpellChecker *spell)
{
	GtkWidget *img, *mi;
	GtkTextIter start, end;
	char *word;

	/* we need to figure out if they picked a misspelled word. */
	get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);

	/* if our highlight algorithm ever messes up,
	 * this isn't correct, either. */
	if (!gtk_text_iter_has_tag (&start, spell->tag_highlight))
		return; /* word wasn't misspelled. */

	/* menu separator comes first. */
	mi = gtk_menu_item_new ();
	gtk_widget_show (mi);
	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);

	/* then, on top of it, the suggestions menu. */
	img = gtk_image_new_from_icon_name ("tools-check-spelling", GTK_ICON_SIZE_MENU);
	mi = gtk_image_menu_item_new_with_mnemonic (_("_Spelling Suggestions..."));
	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), img);

	word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);
	gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi),
				   build_suggestion_menu (spell, word));
	g_free(word);

	gtk_widget_show_all (mi);
	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
}

void
pluma_automatic_spell_checker_recheck_all (PlumaAutomaticSpellChecker *spell)
{
	GtkTextIter start, end;

	g_return_if_fail (spell != NULL);

	gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc), &start, &end);

	check_range (spell, start, end, TRUE);
}

static void
add_word_signal_cb (PlumaSpellChecker          *checker,
		    const gchar                *word,
		    gint                        len,
		    PlumaAutomaticSpellChecker *spell)
{
	gchar *w;

	if (len < 0)
		w = g_strdup (word);
	else
		w = g_strndup (word, len);

	remove_tag_to_word (spell, w);

	g_free (w);
}

static void
set_language_cb (PlumaSpellChecker               *checker,
		 const PlumaSpellCheckerLanguage *lang,
		 PlumaAutomaticSpellChecker      *spell)
{
	pluma_automatic_spell_checker_recheck_all (spell);
}

static void
clear_session_cb (PlumaSpellChecker          *checker,
		  PlumaAutomaticSpellChecker *spell)
{
	pluma_automatic_spell_checker_recheck_all (spell);
}

/* When the user right-clicks on a word, they want to check that word.
 * Here, we do NOT  move the cursor to the location of the clicked-upon word
 * since that prevents the use of edit functions on the context menu.
 */
static gboolean
button_press_event (GtkTextView *view,
		    GdkEventButton *event,
		    PlumaAutomaticSpellChecker *spell)
{
	if (event->button == 3)
	{
		gint x, y;
		GtkTextIter iter;

		GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);

		/* handle deferred check if it exists */
  	        if (spell->deferred_check)
			check_deferred_range (spell, TRUE);

		gtk_text_view_window_to_buffer_coords (view,
				GTK_TEXT_WINDOW_TEXT,
				event->x, event->y,
				&x, &y);

		gtk_text_view_get_iter_at_location (view, &iter, x, y);

		gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
	}

	return FALSE; /* false: let gtk process this event, too.
			 we don't want to eat any events. */
}

/* Move the insert mark before popping up the menu, otherwise it
 * will contain the wrong set of suggestions.
 */
static gboolean
popup_menu_event (GtkTextView *view, PlumaAutomaticSpellChecker *spell)
{
	GtkTextIter iter;
	GtkTextBuffer *buffer;

	buffer = gtk_text_view_get_buffer (view);

	/* handle deferred check if it exists */
	if (spell->deferred_check)
		check_deferred_range (spell, TRUE);

	gtk_text_buffer_get_iter_at_mark (buffer, &iter,
					  gtk_text_buffer_get_insert (buffer));
	gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);

	return FALSE;
}

static void
tag_table_changed (GtkTextTagTable            *table,
		   PlumaAutomaticSpellChecker *spell)
{
	g_return_if_fail (spell->tag_highlight !=  NULL);

	gtk_text_tag_set_priority (spell->tag_highlight,
				   gtk_text_tag_table_get_size (table) - 1);
}

static void
tag_added_or_removed (GtkTextTagTable            *table,
		      GtkTextTag                 *tag,
		      PlumaAutomaticSpellChecker *spell)
{
	tag_table_changed (table, spell);
}

static void
tag_changed (GtkTextTagTable            *table,
	     GtkTextTag                 *tag,
	     gboolean                    size_changed,
	     PlumaAutomaticSpellChecker *spell)
{
	tag_table_changed (table, spell);
}

static void
highlight_updated (GtkSourceBuffer            *buffer,
                   GtkTextIter                *start,
                   GtkTextIter                *end,
                   PlumaAutomaticSpellChecker *spell)
{
	check_range (spell, *start, *end, FALSE);
}

static void
spell_tag_destroyed (PlumaAutomaticSpellChecker *spell,
                     GObject                    *where_the_object_was)
{
	spell->tag_highlight = NULL;
}

PlumaAutomaticSpellChecker *
pluma_automatic_spell_checker_new (PlumaDocument     *doc,
				   PlumaSpellChecker *checker)
{
	PlumaAutomaticSpellChecker *spell;
	GtkTextTagTable *tag_table;
	GtkTextIter start, end;

	g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);
	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (checker), NULL);
	g_return_val_if_fail ((spell = pluma_automatic_spell_checker_get_from_document (doc)) == NULL,
			      spell);

	/* attach to the widget */
	spell = g_new0 (PlumaAutomaticSpellChecker, 1);

	spell->doc = doc;
	spell->spell_checker = g_object_ref (checker);

	if (automatic_spell_checker_id == 0)
	{
		automatic_spell_checker_id =
			g_quark_from_string ("PlumaAutomaticSpellCheckerID");
	}
	if (suggestion_id == 0)
	{
		suggestion_id = g_quark_from_string ("PlumaAutoSuggestionID");
	}

	g_object_set_qdata_full (G_OBJECT (doc),
				 automatic_spell_checker_id,
				 spell,
				 (GDestroyNotify)pluma_automatic_spell_checker_free_internal);

	g_signal_connect (doc,
			  "insert-text",
			  G_CALLBACK (insert_text_before),
			  spell);
	g_signal_connect_after (doc,
			  "insert-text",
			  G_CALLBACK (insert_text_after),
			  spell);
	g_signal_connect_after (doc,
			  "delete-range",
			  G_CALLBACK (delete_range_after),
			  spell);
	g_signal_connect (doc,
			  "mark-set",
			  G_CALLBACK (mark_set),
			  spell);

	g_signal_connect (doc,
	                  "highlight-updated",
	                  G_CALLBACK (highlight_updated),
	                  spell);

	g_signal_connect (spell->spell_checker,
			  "add_word_to_session",
			  G_CALLBACK (add_word_signal_cb),
			  spell);
	g_signal_connect (spell->spell_checker,
			  "add_word_to_personal",
			  G_CALLBACK (add_word_signal_cb),
			  spell);
	g_signal_connect (spell->spell_checker,
			  "clear_session",
			  G_CALLBACK (clear_session_cb),
			  spell);
	g_signal_connect (spell->spell_checker,
			  "set_language",
			  G_CALLBACK (set_language_cb),
			  spell);

	spell->tag_highlight = gtk_text_buffer_create_tag (
				GTK_TEXT_BUFFER (doc),
				"gtkspell-misspelled",
				"underline", PANGO_UNDERLINE_ERROR,
				NULL);

	g_object_weak_ref (G_OBJECT (spell->tag_highlight),
	                   (GWeakNotify)spell_tag_destroyed,
	                   spell);

	tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (doc));

	gtk_text_tag_set_priority (spell->tag_highlight,
				   gtk_text_tag_table_get_size (tag_table) - 1);

	g_signal_connect (tag_table,
			  "tag-added",
			  G_CALLBACK (tag_added_or_removed),
			  spell);
	g_signal_connect (tag_table,
			  "tag-removed",
			  G_CALLBACK (tag_added_or_removed),
			  spell);
	g_signal_connect (tag_table,
			  "tag-changed",
			  G_CALLBACK (tag_changed),
			  spell);

	/* we create the mark here, but we don't use it until text is
	 * inserted, so we don't really care where iter points.  */
	gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (doc), &start, &end);

	spell->mark_insert_start = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
					"pluma-automatic-spell-checker-insert-start");

	if (spell->mark_insert_start == NULL)
	{
		spell->mark_insert_start =
			gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
						     "pluma-automatic-spell-checker-insert-start",
						     &start,
						     TRUE);
	}
	else
	{
		gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
					   spell->mark_insert_start,
					   &start);
	}

	spell->mark_insert_end = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
					"pluma-automatic-spell-checker-insert-end");

	if (spell->mark_insert_end == NULL)
	{
		spell->mark_insert_end =
			gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
						     "pluma-automatic-spell-checker-insert-end",
						     &start,
						     TRUE);
	}
	else
	{
		gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
					   spell->mark_insert_end,
					   &start);
	}

	spell->mark_click = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
					"pluma-automatic-spell-checker-click");

	if (spell->mark_click == NULL)
	{
		spell->mark_click =
			gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
						     "pluma-automatic-spell-checker-click",
						     &start,
						     TRUE);
	}
	else
	{
		gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
					   spell->mark_click,
					   &start);
	}

	spell->deferred_check = FALSE;

	return spell;
}

PlumaAutomaticSpellChecker *
pluma_automatic_spell_checker_get_from_document (const PlumaDocument *doc)
{
	g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);

	if (automatic_spell_checker_id == 0)
		return NULL;

	return g_object_get_qdata (G_OBJECT (doc), automatic_spell_checker_id);
}

void
pluma_automatic_spell_checker_free (PlumaAutomaticSpellChecker *spell)
{
	g_return_if_fail (spell != NULL);
	g_return_if_fail (pluma_automatic_spell_checker_get_from_document (spell->doc) == spell);

	if (automatic_spell_checker_id == 0)
		return;

	g_object_set_qdata (G_OBJECT (spell->doc), automatic_spell_checker_id, NULL);
}

static void
pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell)
{
	GtkTextTagTable *table;
	GtkTextIter start, end;
	GSList *list;

	g_return_if_fail (spell != NULL);

	table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (spell->doc));

	if (table != NULL && spell->tag_highlight != NULL)
	{
		gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc),
					    &start,
					    &end);
		gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
					    spell->tag_highlight,
					    &start,
					    &end);

		g_signal_handlers_disconnect_matched (G_OBJECT (table),
					G_SIGNAL_MATCH_DATA,
					0, 0, NULL, NULL,
					spell);

		gtk_text_tag_table_remove (table, spell->tag_highlight);
	}

	g_signal_handlers_disconnect_matched (G_OBJECT (spell->doc),
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);

	g_signal_handlers_disconnect_matched (G_OBJECT (spell->spell_checker),
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);

	g_object_unref (spell->spell_checker);

	list = spell->views;
	while (list != NULL)
	{
		PlumaView *view = PLUMA_VIEW (list->data);

		g_signal_handlers_disconnect_matched (G_OBJECT (view),
				G_SIGNAL_MATCH_DATA,
				0, 0, NULL, NULL,
				spell);

		g_signal_handlers_disconnect_matched (G_OBJECT (view),
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);

		list = g_slist_next (list);
	}

	g_slist_free (spell->views);

	g_free (spell);
}

void
pluma_automatic_spell_checker_attach_view (
		PlumaAutomaticSpellChecker *spell,
		PlumaView *view)
{
	g_return_if_fail (spell != NULL);
	g_return_if_fail (PLUMA_IS_VIEW (view));

	g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
			  GTK_TEXT_BUFFER (spell->doc));

	g_signal_connect (view,
			  "button-press-event",
			  G_CALLBACK (button_press_event),
			  spell);
	g_signal_connect (view,
			  "popup-menu",
			  G_CALLBACK (popup_menu_event),
			  spell);
	g_signal_connect (view,
			  "populate-popup",
			  G_CALLBACK (populate_popup),
			  spell);
	g_signal_connect (view,
			  "destroy",
			  G_CALLBACK (view_destroy),
			  spell);

	spell->views = g_slist_prepend (spell->views, view);
}

void
pluma_automatic_spell_checker_detach_view (
		PlumaAutomaticSpellChecker *spell,
		PlumaView *view)
{
	g_return_if_fail (spell != NULL);
	g_return_if_fail (PLUMA_IS_VIEW (view));

	g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
			  GTK_TEXT_BUFFER (spell->doc));
	g_return_if_fail (spell->views != NULL);

	g_signal_handlers_disconnect_matched (G_OBJECT (view),
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);

	g_signal_handlers_disconnect_matched (G_OBJECT (view),
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);

	spell->views = g_slist_remove (spell->views, view);
}