/* gtkspell - a spell-checking addon for GtkText
 * Copyright (c) 2000 Evan Martin.
 * vim: ts=4 sw=4
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

#include <gtk/gtk.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/poll.h>
#include <unistd.h>   
#include <stdio.h>    
#include <signal.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

/* TODO:
 * handle dictionary changes
 * asynchronous lookups
 */

/* size of the text buffer used in various word-processing routines. */
#define BUFSIZE 1024
/* number of suggestions to display on each menu. */
#define MENUCOUNT 10
#define BUGEMAIL "gtkspell-devel@lists.sourceforge.net"

/* because we keep only one copy of the spell program running, 
 * all ispell-related variables can be static.  
 */
static pid_t spell_pid = -1;
static int fd_write[2], fd_read[2];
static int signal_set_up = 0;

/* FIXME? */
static GdkColor highlight = { 0, 255*256, 0, 0 };

static void entry_insert_cb(GtkText *gtktext, 
		gchar *newtext, guint len, guint *ppos, gpointer d);
static void set_up_signal();

int gtkspell_running() {
	return (spell_pid > 0);
}

static void error_print(const char *fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	fprintf(stderr, "gtkspell: ");
	vfprintf(stderr, fmt, ap);
	va_end(ap);
}

/* functions to interface with pipe */
static void writetext(char *text) {
	write(fd_write[1], text, strlen(text));
}
static int readpipe(char *buf, int bufsize) {
	int len;
	len = read(fd_read[0], buf, bufsize-1);
	if (len < 0) {
		error_print("read: %s\n", strerror(errno));
		return -1;
	} else if (len == 0) {
		error_print("pipe closed.\n");
		return -1;
	} else if (len == bufsize-1) {
		error_print("buffer overflowed?\n");
	}

	buf[len] = 0;
	return len;
}
static int readline(char *buf) {
	return readpipe(buf, BUFSIZE);
}

static int readresponse(char *buf) {
	int len;
	len = readpipe(buf, BUFSIZE);

	/* all ispell responses of any reasonable length should end in \n\n.
	 * depending on the speed of the spell checker, this may require more
	 * reading. */
	if (len >= 2 && (buf[len-1] != '\n' || buf[len-2] != '\n')) {
		len += readpipe(buf+len, BUFSIZE-len);
	}

	/* now we can remove all of the the trailing newlines. */
	while (len > 0 && buf[len-1] == '\n')
		buf[--len] = 0;

	return len;
}


void gtkspell_stop() {
	if (gtkspell_running()) {
		kill(spell_pid, SIGQUIT); /* FIXME: is this the correct signal? */
	}
}

int gtkspell_start(char *path, char * args[]) {
	int fd_error[2];
	char buf[BUFSIZE];

	if (gtkspell_running()) {
		error_print("gtkspell_start called while already running.\n");
		gtkspell_stop();
	}

	if (!signal_set_up) {
		set_up_signal();
		signal_set_up = 1;
	}

	pipe(fd_write);
	pipe(fd_read);
	pipe(fd_error);

	spell_pid = fork();
	if (spell_pid < 0) {
		error_print("fork: %s\n", strerror(errno));
		return -1;
	} else if (spell_pid == 0) {
		dup2(fd_write[0], 0);
		dup2(fd_read[1], 1);
		dup2(fd_error[1], 2);
		close(fd_read[0]);
		close(fd_error[0]);
		close(fd_write[1]);

		if (path == NULL) {
			if (execvp(args[0], args) < 0) 
				error_print("execvp('%s'): %s\n", args[0], strerror(errno));
		} else {
			if (execv(path, args) < 0) 
				error_print("execv('%s'): %s\n", path, strerror(errno));
		}
		/* if we get here, we failed.
		 * send some text on the pipe to indicate status.
		 */
		write(fd_read[1], "!", 1);

		_exit(0);
	} else {
		/* there are at least two ways to fail:
		 * - the exec() can fail
		 * - the exec() can succeed, but the program can dump the help screen
		 * we must check for both.
		 */
		struct pollfd fds[2];

		fds[0].fd = fd_error[0];
		fds[0].events = POLLIN | POLLERR;
		fds[1].fd = fd_read[0];
		fds[1].events = POLLIN | POLLERR;
		if (poll(fds, 2, 2000) <= 0) {
			/* FIXME: is this needed? */
			error_print("Timed out waiting for spell command.\n");
			gtkspell_stop();
			return -1;
		}

		if (fds[0].revents) { /* stderr readable? */
			error_print("Spell command printed on stderr -- probably failed.\n");
			gtkspell_stop();
			return -1;
		}
		readline(buf);
		/* ispell should print something like this:
		 * @(#) International Ispell Version 3.1.20 10/10/95
		 * if it doesn't, it's an error. */
		if (buf[0] != '@') {
			gtkspell_stop();
			return -1;
		}
	}

	/* put ispell into terse mode.  
	 * this makes it not respond on correctly spelled words. */
	sprintf(buf, "!\n");
	writetext(buf);
	return 0;
}

static GList* misspelled_suggest(char *word) {
	char buf[BUFSIZE];
	char *newword;
	GList *l = NULL;
	int count;

	sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
	writetext(buf);
	readresponse(buf);

	switch (buf[0]) { /* first char is ispell command. */
		case 0: /* no response: word is ok. */
			return NULL;
		case '&': /* misspelled, with suggestions */
			/* & <orig> <count> <ofs>: <miss>, <miss>, <guess>, ... */
			strtok(buf, " "); /* & */
			newword = strtok(NULL, " "); /* orig */
			l = g_list_append(l, g_strdup(newword));
			newword = strtok(NULL, " "); /* count */
			count = atoi(newword);
			strtok(NULL, " "); /* ofs: */

			while ((newword = strtok(NULL, ",")) != NULL) {
				int len = strlen(newword);
				if (newword[len-1] == ' ' || newword[len-1] == '\n') 
					newword[len-1] = 0;
				if (count == 0) {
					g_list_append(l, NULL); /* signal the "suggestions" */
				}
				/* add it to the list, skipping the initial space. */
				l = g_list_append(l, 
						g_strdup(newword[0] == ' ' ? newword+1 : newword));

				count--;
			}
			return l;

		case '#': /* misspelled, no suggestions */
			/* # <orig> <ofs> */
			strtok(buf, " "); /* & */
			newword = strtok(NULL, " "); /* orig */
			l = g_list_append(l, g_strdup(newword));
			return l;
		default:
			error_print("Unsupported spell command '%c'.\n"
					"This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
	}
	return NULL;
}

static int misspelled_test(char *word) {
	char buf[BUFSIZE];
	sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
	writetext(buf);
	readresponse(buf);

	if (buf[0] == 0) {
		return 0;
	} else if (buf[0] == '&' || buf[0] == '#') {
		return 1;
	}
	
	error_print("Unsupported spell command '%c'.\n"
			"This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
	return -1;
}

static gboolean iswordsep(char c) {
	return !isalpha(c) && c != '\'';
}

static gboolean get_word_from_pos(GtkText* gtktext, int pos, char* buf, 
		int *pstart, int *pend) {
	gint start, end;

	if (iswordsep(GTK_TEXT_INDEX(gtktext, pos))) return FALSE;

	for (start = pos; start >= 0; --start) {
		if (iswordsep(GTK_TEXT_INDEX(gtktext, start))) break;
	}
	start++;

	for (end = pos; end <= gtk_text_get_length(gtktext); end++) {
		if (iswordsep(GTK_TEXT_INDEX(gtktext, end))) break;
	}

	if (buf) {
		for (pos = start; pos < end; pos++) 
			buf[pos-start] = GTK_TEXT_INDEX(gtktext, pos);
		buf[pos-start] = 0;
	}

	if (pstart) *pstart = start;
	if (pend) *pend = end;

	return TRUE;
}

static gboolean get_curword(GtkText* gtktext, char* buf, 
		int *pstart, int *pend) {
	int pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	return get_word_from_pos(gtktext, pos, buf, pstart, pend);
}

static void change_color(GtkText *gtktext, 
		int start, int end, GdkColor *color) {
	char * newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
	gtk_text_freeze(gtktext);
	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	
	gtk_text_set_point(gtktext, start);
	gtk_text_forward_delete(gtktext, end-start);

	if (newtext && end-start > 0)
		gtk_text_insert(gtktext, NULL, color, NULL, newtext, end-start);

	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_text_thaw(gtktext);
}

static gboolean check_at(GtkText *gtktext, int from_pos) {
	int start, end;
	char buf[BUFSIZE];

	if (!get_word_from_pos(gtktext, from_pos, buf, &start, &end)) {
		return FALSE;
	}

	if (misspelled_test(buf)) {
		if (highlight.pixel == 0) {
			/* add an entry for the highlight in the color map. */
			GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
			gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);;
		}
		change_color(gtktext, start, end, &highlight);
		return TRUE;
	} else { 
		change_color(gtktext, start, end, 
				&(GTK_WIDGET(gtktext)->style->fg[0]));
		return FALSE;
	}
}

void gtkspell_check_all(GtkText *gtktext) {
	guint origpos;
	guint pos = 0;
	guint len;
	float adj_value;
	
	if (!gtkspell_running()) return;
	
	len = gtk_text_get_length(gtktext);

	adj_value = gtktext->vadj->value;
	gtk_text_freeze(gtktext);
	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	while (pos < len) {
		while (pos < len && iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
			pos++;
		while (pos < len && !iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
			pos++;
		if (pos > 0)
			check_at(gtktext, pos-1);
	}
	gtk_text_thaw(gtktext);
	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
}

static void entry_insert_cb(GtkText *gtktext, 
		gchar *newtext, guint len, guint *ppos, gpointer d) {
	int origpos;

	if (!gtkspell_running()) return;

	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
									 GTK_SIGNAL_FUNC(entry_insert_cb),
									 NULL);
	gtk_text_insert(GTK_TEXT(gtktext), NULL,
			&(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
									 GTK_SIGNAL_FUNC(entry_insert_cb),
									 NULL);
	gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
	*ppos += len;

	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));

	if (iswordsep(newtext[0])) {
		/* did we just end a word? */
		if (*ppos >= 2) check_at(gtktext, *ppos-2);

		/* did we just split a word? */
		if (*ppos < gtk_text_get_length(gtktext))
			check_at(gtktext, *ppos+1);
	} else {
		/* check as they type, *except* if they're typing at the end (the most
		 * common case.
		 */
		if (*ppos < gtk_text_get_length(gtktext) && 
				!iswordsep(GTK_TEXT_INDEX(gtktext, *ppos)))
			check_at(gtktext, *ppos-1);
	}

	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
}

static void entry_delete_cb(GtkText *gtktext,
		gint start, gint end, gpointer d) {
	int origpos;

	if (!gtkspell_running()) return;

	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	check_at(gtktext, start-1);
	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
	gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
	/* this is to *UNDO* the selection, in case they were holding shift
	 * while hitting backspace. */
}

static void replace_word(GtkWidget *w, gpointer d) {
	int start, end;
	char *newword;
	char buf[BUFSIZE];

	/* we don't save their position, 
	 * because the cursor is moved by the click. */

	gtk_text_freeze(GTK_TEXT(d));

	gtk_label_get(GTK_LABEL(GTK_BIN(w)->child), &newword);
	get_curword(GTK_TEXT(d), buf, &start, &end);

	gtk_text_set_point(GTK_TEXT(d), end);
	gtk_text_backward_delete(GTK_TEXT(d), end-start);
	gtk_text_insert(GTK_TEXT(d), NULL, NULL, NULL, newword, strlen(newword));

	gtk_text_thaw(GTK_TEXT(d));
}

static GtkMenu *make_menu(GList *l, GtkText *gtktext) {
	GtkWidget *menu, *item;
	char *caption;
	menu = gtk_menu_new(); {
		caption = g_strdup_printf("Not in dictionary: %s", (char*)l->data);
		item = gtk_menu_item_new_with_label(caption);
		/* I'd like to make it so this item is never selectable, like
		 * the menu titles in the GNOME panel... unfortunately, the GNOME
		 * panel creates their own custom widget to do this! */
		gtk_widget_show(item);
		gtk_menu_append(GTK_MENU(menu), item);

		item = gtk_menu_item_new();
		gtk_widget_show(item);
		gtk_menu_append(GTK_MENU(menu), item);

		l = l->next;
		if (l == NULL) {
			item = gtk_menu_item_new_with_label("(no suggestions)");
			gtk_widget_show(item);
			gtk_menu_append(GTK_MENU(menu), item);
		} else {
			GtkWidget *curmenu = menu;
			int count = 0;
			do {
				if (l->data == NULL && l->next != NULL) {
					count = 0;
					curmenu = gtk_menu_new();
					item = gtk_menu_item_new_with_label("Other Possibilities...");
					gtk_widget_show(item);
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
					gtk_menu_append(GTK_MENU(curmenu), item);
					l = l->next;
				} else if (count > MENUCOUNT) {
					count -= MENUCOUNT;
					item = gtk_menu_item_new_with_label("More...");
					gtk_widget_show(item);
					gtk_menu_append(GTK_MENU(curmenu), item);
					curmenu = gtk_menu_new();
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
				}
				item = gtk_menu_item_new_with_label((char*)l->data);
				gtk_signal_connect(GTK_OBJECT(item), "activate",
						GTK_SIGNAL_FUNC(replace_word), gtktext);
				gtk_widget_show(item);
				gtk_menu_append(GTK_MENU(curmenu), item);
				count++;
			} while ((l = l->next) != NULL);
		}
	}
	return GTK_MENU(menu);
}

static void popup_menu(GtkText *gtktext, GdkEventButton *eb) {
	char buf[BUFSIZE];
	GList *list, *l;

	get_curword(gtktext, buf, NULL, NULL);

	list = misspelled_suggest(buf);
	if (list != NULL) {
		gtk_menu_popup(make_menu(list, gtktext), NULL, NULL, NULL, NULL,
				eb->button, eb->time);
		for (l = list; l != NULL; l = l->next)
			g_free(l->data);
		g_list_free(list);
	}
}

/* ok, this is pretty wacky:
 * we need to let the right-mouse-click go through, so it moves the cursor, 
 * but we *can't* let it go through, because GtkText interprets rightclicks as
 * weird selection modifiers.
 *
 * so what do we do?  forge rightclicks as leftclicks, then popup the menu. 
 * HACK HACK HACK. 
 */
static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d) {
	GdkEventButton *eb;
	gboolean retval;

	if (!gtkspell_running()) return FALSE;

	if (e->type != GDK_BUTTON_PRESS) return FALSE;
	eb = (GdkEventButton*) e;

	if (eb->button != 3) return FALSE;

	/* forge the leftclick */
	eb->button = 1;

	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
	gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
			e, &retval);
	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
	gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");

	/* now do the menu wackiness */
	popup_menu(gtktext, eb);
	return TRUE;
}

void gtkspell_uncheck_all(GtkText *gtktext) {
	int origpos;
	char *text;
	float adj_value;

	adj_value = gtktext->vadj->value;
	gtk_text_freeze(gtktext);
	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
	gtk_text_set_point(gtktext, 0);
	gtk_text_forward_delete(gtktext, gtk_text_get_length(gtktext));
	gtk_text_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
	gtk_text_thaw(gtktext);

	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
	gtk_adjustment_set_value(gtktext->vadj, adj_value);
}

void gtkspell_attach(GtkText *gtktext) {
	gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
		GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
		GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
	gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
			GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
}

void gtkspell_detach(GtkText *gtktext) {
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
		GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
		GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);

	gtkspell_uncheck_all(gtktext);
}

static void sigchld(int param) {
	if (gtkspell_running() &&
		(waitpid(spell_pid, NULL, WNOHANG) == spell_pid)) {
		spell_pid = 0;
	} else {
		/* a default SIGCHLD handler.
		 * what else to do here? */
		waitpid(-1, NULL, WNOHANG);
	}
}

static void set_up_signal() {
	struct sigaction sigact;
	memset(&sigact, 0, sizeof(struct sigaction));

	sigact.sa_handler = sigchld;
	sigaction(SIGCHLD, &sigact, NULL);
}
