/*
 * Copyright (c) 1999 by The XFree86 Project, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *  
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Except as contained in this notice, the name of the XFree86 Project shall
 * not be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization from the
 * XFree86 Project.
 *
 * Author: Paulo César Pereira de Andrade
 */

/* $XFree86: xc/programs/xedit/ispell.c,v 1.17 2001/12/03 21:04:40 herrb Exp $ */

#include "xedit.h"
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <X11/Xaw/Toggle.h>
#include <X11/Xaw/MenuButton.h>
#include <X11/Xaw/SmeBSB.h>
#include <X11/Xaw/SimpleMenu.h>

#define RECEIVE		1
#define SEND		2

#define CHECK		0
#define	ADD		1
#define REMOVE		2

#define	ASIS		1
#define UNCAP		2

/*
 * Types
 */
#define UNDO_DEPTH	16
typedef struct _ispell_undo {
    char *undo_str;
    int undo_count;
    XawTextPosition undo_pos;
    Boolean repeat;	/* two (misspelled?) words together */
    Boolean terse;
    int format;		/* remember text formatting style */
    struct _ispell_undo *next, *prev;
} ispell_undo;

typedef struct _ispell_dict {
    Widget sme;
    char *wchars;
    struct _ispell_dict *next;
} ispell_dict;

#define	TEXT	0
#define HTML	1
struct _ispell_format {
    char *name;
    int value;
    Widget sme;
};

static struct _ispell_format ispell_format[] = {
    {"text",	TEXT},
    {"html",	HTML},
};

struct _ispell {
    Widget shell, form, mispelled, repeated, word, replacement, text,
	   suggestions, viewport, list, commands, replace, status,
	   replaceAll, undo, ignore, ignoreAll, add, addUncap, suspend,
	   cancel, check, look, terse, options, dict, dictMenu,
	   format, formatMenu;

    Widget ascii, source;
    XtInputId id;
    int pid, ifd[2], ofd[2];
    XawTextPosition left, right;
    char *item;
    Bool lock;
    Bool repeat;
    Bool checkit;
    int stat;
    char *buf;
    int bufsiz;
    int buflen;
    char sendbuf[1024];
    char sentbuf[1024];

    int undo_depth;
    ispell_undo *undo_head, *undo_base;
    char *undo_for;

    char *wchars;
    char *cmd;
    char *skip;
    char *command;
    Boolean terse_mode, undo_terse_mode;
    char *guess_label, *miss_label, *root_label, *none_label, *eof_label,
	 *compound_label, *ok_label, *repeat_label, *working_label, *look_label;
    char *look_cmd;
    char *words_file;

    char *dictionary;
    char *dict_list;
    ispell_dict *dict_info;

    int format_mode;	/* to undo correctly */
    char *formatting;
    struct _ispell_format *format_info;
};

typedef struct _ReplaceList {
    char *word;
    char *replace;
    struct _ReplaceList *next;
} ReplaceList;

typedef struct _IgnoreList {
    char *word;
    int add;
    struct _IgnoreList *next;
} IgnoreList;

/*
 * Prototypes
 */
static void AddIspell(Widget, XtPointer, XtPointer);
static void ChangeDictionaryIspell(Widget, XtPointer, XtPointer);
static void ChangeFormatIspell(Widget, XtPointer, XtPointer);
static void CheckIspell(Widget, XtPointer, XtPointer);
static void IgnoreIspell(Widget, XtPointer, XtPointer);
static Bool InitIspell(void);
static void IspellCheckUndo(void);
static int IspellConvertHtmlAmp(char*);
static Bool IspellDoIgnoredWord(char*, int, int);
static Bool IspellIgnoredWord(char*, int, int);
static void IspellInputCallback(XtPointer, int*, XtInputId*);
static void IspellKillUndoBuffer(void);
static Bool IspellReceive(void);
static char *IspellReplacedWord(char*, char*);
static int IspellSend(void);
static void IspellSetSelection(XawTextPosition, XawTextPosition);
static void IspellSetRepeated(Bool);
static void IspellSetSensitive(Bool);
static void IspellSetStatus(char*);
static void IspellSetTerseMode(Bool);
static Bool IspellStartProcess(void);
static Bool IspellEndProcess(Bool, Bool);
static void LookIspell(Widget, XtPointer, XtPointer);
static void PopdownIspell(Widget, XtPointer, XtPointer);
static void ReplaceIspell(Widget, XtPointer, XtPointer);
static void RevertIspell(Widget, XtPointer, XtPointer);
static void SelectIspell(Widget, XtPointer, XtPointer);
static void ToggleTerseIspell(Widget, XtPointer, XtPointer);
#ifndef SIGNALRETURNSINT
static void timeout_signal(int);
static void (*old_timeout)(int);
#else
static int timeout_signal(int);
static int (*old_timeout)(int);
#endif
static void UndoIspell(Widget, XtPointer, XtPointer);

Bool _XawTextSrcUndo(TextSrcObject, XawTextPosition*);

/*
 * Initialization
 */
static struct _ispell ispell;

#define RSTRTBLSZ	23
#define ISTRTBLSZ	71
static ReplaceList *replace_list[RSTRTBLSZ];
static IgnoreList *ignore_list[ISTRTBLSZ];

#ifndef XtCStatus
#define XtCStatus	"Status"
#endif

#define Offset(field) XtOffsetOf(struct _ispell, field)
static XtResource resources[] = {
    {"wordChars", "Chars", XtRString, sizeof(char*),
	Offset(wchars), XtRString, ""},
    {"ispellCommand", "CommandLine", XtRString, sizeof(char*),
	Offset(cmd), XtRString, "/usr/local/bin/ispell"},
    {"terseMode", "Terse", XtRBoolean, sizeof(Boolean),
	Offset(terse_mode), XtRImmediate, (XtPointer)False},
    {"guessLabel", XtCStatus, XtRString, sizeof(String),
	Offset(guess_label), XtRString, "Guess"},
    {"missLabel", XtCStatus, XtRString, sizeof(String),
	Offset(miss_label), XtRString, "Miss"},
    {"rootLabel", XtCStatus, XtRString, sizeof(String),
	Offset(root_label), XtRString, "Root:"},
    {"noneLabel", XtCStatus, XtRString, sizeof(String),
	Offset(none_label), XtRString, "None"},
    {"compoundLabel", XtCStatus, XtRString, sizeof(String),
	Offset(compound_label), XtRString, "Compound"},
    {"okLabel", XtCStatus, XtRString, sizeof(String),
	Offset(ok_label), XtRString, "Ok"},
    {"eofLabel", XtCStatus, XtRString, sizeof(String),
	Offset(eof_label), XtRString, "End Of File"},
    {"repeatLabel", XtCStatus, XtRString, sizeof(String),
	Offset(repeat_label), XtRString, "Repeat"},
    {"workingLabel", XtCStatus, XtRString, sizeof(String),
	Offset(working_label), XtRString, "..."},
    {"lookLabel", XtCStatus, XtRString, sizeof(String),
	Offset(look_label), XtRString, "Look"},
    {"lookCommand", "CommandLine", XtRString, sizeof(char*),
	Offset(look_cmd), XtRString, "/usr/bin/egrep -i"},
    {"wordsFile", "Words", XtRString, sizeof(char*),
	Offset(words_file), XtRString, "/usr/share/dict/words"},
    {"dictionary", "Dictionary", XtRString, sizeof(char*),
	Offset(dictionary), XtRString, "american"},
    {"dictionaries", "Dictionary", XtRString, sizeof(char*),
	Offset(dict_list), XtRString, "american americanmed+ english"},
    {"formatting", "TextFormat", XtRString, sizeof(char*),
	Offset(formatting), XtRString, "text"},
};
#undef Offset

#ifdef NO_LIBC_I18N
static int
ToLower(int ch)
{
    char buf[2];

    *buf = ch;
    XmuNCopyISOLatin1Lowered(buf, buf, sizeof(buf));

    return (*buf);
}

static int
ToUpper(int ch)
{
    char buf[2];

    *buf = ch;
    XmuNCopyISOLatin1Uppered(buf, buf, sizeof(buf));

    return (*buf);
}

static int
IsLower(int ch)
{
    char upbuf[2];
    char lobuf[2];

    *upbuf = *lobuf = ch;
    XmuNCopyISOLatin1Lowered(lobuf, lobuf, sizeof(lobuf));
    XmuNCopyISOLatin1Uppered(upbuf, upbuf, sizeof(upbuf));

    return (*lobuf != *upbuf && ch == *lobuf);
}

static int
IsUpper(int ch)
{
    char upbuf[2];
    char lobuf[2];

    *upbuf = *lobuf = ch;
    XmuNCopyISOLatin1Lowered(lobuf, lobuf, sizeof(lobuf));
    XmuNCopyISOLatin1Uppered(upbuf, upbuf, sizeof(upbuf));

    return (*lobuf != *upbuf && ch == *upbuf);
}
#else
#define	ToLower	tolower
#define ToUpper	toupper
#define IsLower islower
#define IsUpper isupper
#endif

/*
 * Implementation
 */
#ifdef STDERR_FILENO
# define WRITES(s) write(STDERR_FILENO, s, strlen(s))
#else
# define WRITES(s) write(fileno(stderr), s, strlen(s))
#endif

/*ARGSUSED*/
#ifndef SIGNALRETURNSINT
static void
timeout_signal(int unused)
{
    int olderrno = errno;

    WRITES("Warning: Timeout waiting ispell process to die.\n");
    kill(ispell.pid, SIGTERM);
    errno = olderrno;
}
#else
static int
timeout_signal(int unused)
{
    int olderrno = errno;

    WRITES("Warning: Timeout waiting ispell process to die.\n");
    kill(ispell.pid, SIGTERM);
    
    errno = olderrno;
    return (0);
}
#endif

static void
IspellSetSelection(XawTextPosition left, XawTextPosition right)
{
    /* Try to make sure the selected word is completely visible */
    XawTextSetInsertionPoint(ispell.ascii, right);
    XawTextSetInsertionPoint(ispell.ascii, left);
    XawTextSetSelection(ispell.ascii, left, right);
}

static void
IspellSetStatus(char *label)
{
    Arg args[1];

    XtSetArg(args[0], XtNlabel, label);
    XtSetValues(ispell.status, args, 1);
}

static void
IspellSetRepeated(Bool state)
{
    static char *mispelled, *repeated;
    Arg args[1];

    if (mispelled == NULL) {
	XtSetArg(args[0], XtNlabel, &mispelled);
	XtGetValues(ispell.mispelled, args, 1);
	mispelled = XtNewString(mispelled);
    }
    if (repeated == NULL) {
	XtSetArg(args[0], XtNlabel, &repeated);
	XtGetValues(ispell.repeated, args, 1);
	repeated = XtNewString(repeated);
    }
    XtSetSensitive(ispell.replaceAll, !state);
    XtSetSensitive(ispell.ignoreAll, !state);
    XtSetSensitive(ispell.add, !state);
    XtSetSensitive(ispell.addUncap, !state);
    if (!state) {
	XtSetArg(args[0], XtNlabel, mispelled);
	XtSetValues(ispell.mispelled, args, 1);
    }
    else {
	XtSetArg(args[0], XtNlabel, repeated);
	XtSetValues(ispell.mispelled, args, 1);
    }
}

static void
IspellSetSensitive(Bool state)
{
    XtSetSensitive(ispell.replace, state);
    XtSetSensitive(ispell.replaceAll, state);
    XtSetSensitive(ispell.ignore, state);
    XtSetSensitive(ispell.ignoreAll, state);
    XtSetSensitive(ispell.add, state);
    XtSetSensitive(ispell.addUncap, state);
}

static void
IspellSetTerseMode(Bool mode)
{
    Arg args[1];

    XtSetArg(args[0], XtNstate, ispell.terse_mode = mode);
    XtSetValues(ispell.terse, args, 1);
    write(ispell.ofd[1], mode ? "!\n" : "%\n", 2);
}

static void
IspellCheckUndo(void)
{
    ispell_undo *undo = XtNew(ispell_undo);

    if (ispell.undo_for && strcmp(ispell.undo_for, ispell.dictionary)) {
	XeditPrintf("Undo: Dictionary changed. Previous undo information lost.\n");
	IspellKillUndoBuffer();
	Feep();
    }

    undo->next = NULL;
    undo->repeat = False;
    undo->terse = ispell.undo_terse_mode;
    undo->format = ispell.format_mode;
    if ((undo->prev = ispell.undo_head) != NULL)
	undo->prev->next = undo;
    else
	undo->prev = NULL;
    ++ispell.undo_depth;
    if (!ispell.undo_base) {
	ispell.undo_base = undo;
	XtSetSensitive(ispell.undo, True);
    }
    else if (ispell.undo_depth > UNDO_DEPTH) {
	ispell_undo *tmp;

	if (ispell.undo_base->undo_str)
	    XtFree(ispell.undo_base->undo_str);
	tmp = ispell.undo_base->next;
	XtFree((char*)ispell.undo_base);
	tmp->prev = NULL;
	ispell.undo_base = tmp;
	ispell.undo_depth = UNDO_DEPTH;
    }
    ispell.undo_head = undo;
}

static char *
IspellReplacedWord(char *word, char *replace)
{
    ReplaceList *list;
    int ii = 0;
    char *pp = word;

    while (*pp)
	ii = (ii << 1) ^ *pp++;
    if (ii < 0)
	ii = -ii;
    ii %= RSTRTBLSZ;
    for (list = replace_list[ii]; list; list = list->next)
	if (strcmp(list->word, word) == 0) {
	    if (replace) {
		XtFree(list->replace);
		list->replace = XtNewString(replace);
	    }
	    return (list->replace);
	}

    if (!replace)
	return (NULL);

    list = XtNew(ReplaceList);
    list->word = XtNewString(word);
    list->replace = XtNewString(replace);
    list->next = replace_list[ii];
    replace_list[ii] = list;

    return (list->replace);
}

static Bool
IspellDoIgnoredWord(char *word, int cmd, int add)
{
    IgnoreList *list, *prev;
    int ii = 0;
    char *pp = word;

    while (*pp)
	ii = (ii << 1) ^ *pp++;
    if (ii < 0)
	ii = -ii;
    ii %= ISTRTBLSZ;
    for (prev = list = ignore_list[ii]; list; prev = list, list = list->next)
	if (strcmp(list->word, word) == 0) {
	    if (cmd == REMOVE) {
		XtFree(list->word);
		prev->next = list->next;
		XtFree((char*)list);
		if (prev == list)
		    ignore_list[ii] = NULL;
		return (True);
	    }
	    return (cmd == CHECK);
	}

    if (cmd != ADD)
	return (False);

    list = XtNew(IgnoreList);
    list->word = XtNewString(word);
    list->add = add;
    list->next = ignore_list[ii];
    ignore_list[ii] = list;

    return (True);
}

static Bool
IspellIgnoredWord(char *word, int cmd, int add)
{
    if (add != UNCAP && IspellDoIgnoredWord(word, cmd, add))
	return (True);

    /* add/remove uncapped word to/of list,
     * or cheks for correct capitalization */
    if (add == UNCAP || cmd == CHECK) {
	unsigned char *str = (unsigned char*)word;
	unsigned char string[1024];
	Bool upper, status;
	int i;

	status = True;
	upper = IsUpper(*str);
	*string = upper ? ToLower(*str) : *str;
	if (*str)
	    str++;
	if (IsLower(*str))
	    upper = False;
	for (i = 1; *str && i < sizeof(string) - 1; i++, str++) {
	    if (upper && IsLower(*str))
		status = False;
	    else if (!upper && IsUpper(*str))
		status = False;
	    string[i] = ToLower(*str);
	}
	string[i] = '\0';

	if ((cmd != CHECK || status) &&
	    IspellDoIgnoredWord((char*)string, cmd, add))
	    return (True);
    }

    return (False);
}

/*ARGSUSED*/
static Bool
IspellReceive(void)
{
    int i, len, old_len;
    Arg args[2];
    char *str, *end, **list, **old_list;
    char *tmp, word[1024];
    int j;

    if (ispell.lock || ispell.stat != RECEIVE)
	return (False);

    while (1) {		/* read the entire line */
	if (ispell.buflen >= ispell.bufsiz - 1)
	    ispell.buf = XtRealloc(ispell.buf, ispell.bufsiz += BUFSIZ);
	if ((len = read(ispell.ifd[0], &ispell.buf[ispell.buflen],
			ispell.bufsiz - ispell.buflen - 1)) <= 0)
	    break;
	ispell.buflen += len;
    }
    if (ispell.buflen <= 0)
	return (False);
    len = 0;
    i = ispell.buflen - 1;
    while (i >= 0 && ispell.buf[i] == '\n') {
	++len;
	--i;
    }
    if (len < 2 - ((ispell.terse_mode && i == -1) || ispell.buf[0] == '@'))
	return (False);
    ispell.buf[ispell.buflen - len] = '\0';
    ispell.buflen = 0;

    if ((tmp = strchr(ispell.sendbuf, '\n')) != NULL)
	*tmp = '\0';

    switch (ispell.buf[0]) {
	case '&':	/* MISS */
	case '?':	/* GUESS */
	    str = strchr(&ispell.buf[2], ' ');
	    if (!ispell.checkit) {
		*str = '\0';
		XtSetArg(args[0], XtNlabel, &ispell.buf[2]);
		XtSetValues(ispell.word, args, 1);
	    }
	    ++str;
	    list = NULL;
	    str = strchr(str, ':') + 1;
	    for (i = 0; ; i++) {
		end = strchr(str, ',');
		if (end)	*end = '\0';
		if ((i % 16) == 0)
		    list = (char**)XtRealloc((char*)list, (i + 16) * sizeof(char*));
		tmp = word;
		for (j = 1; j < sizeof(word) && str[j]; j++) {
		    if (str[j] == '+')
			continue;
		    else if (str[j] == '-' && str[j+1] != '-' && str[j-1] != '-') {
			char *p, string[256];
			int k, l;

			for (l = 0, k = j + 1; str[k] != '+' && str[k] != '-'
			     && str[k] && l < sizeof(string) - 1; k++, l++)
			    string[l] = str[k];
			string[l] = '\0';
			*tmp = '\0';
			if (l && (p = strstr(word, string)) != NULL) {
			    char *sav = p;

			    while ((p = strstr(p + l, string)) != NULL)
				sav = p;
			    p = sav;
			    if (strcmp(p, string) == 0) {
				tmp = p;
				j = k - 1;
			    }
			    else
				*tmp++ = '-';
			}
			else
			    *tmp++ = '-';
		    }
		    else
			*tmp++ = str[j];
		}
		*tmp = '\0';
		list[i] = XtNewString(word);

		if (end)	str = end + 1;
		else		break;
	    }
	    len = i + 1;

	    XtSetArg(args[0], XtNlist, &old_list);
	    XtSetArg(args[1], XtNnumberStrings, &old_len);
	    XtGetValues(ispell.list, args, 2);

	    ispell.item = NULL;
	    if ((str = IspellReplacedWord(&ispell.buf[2], NULL)) != NULL)
		for (i = 0; i < len; i++) {
		    if (strcmp(list[i], str) == 0) {
			ispell.item = list[i];
			break;
		    }
		}
	    else
		ispell.item = list[i = 0];
	    if (!ispell.item) {
		list = (char**)XtRealloc((char*)list, (len + 1) * sizeof(char*));
		ispell.item = list[i] = XtNewString(str);
		++len;
	    }

	    XtSetArg(args[0], XtNlist, list);
	    XtSetArg(args[1], XtNnumberStrings, len);
	    XtSetValues(ispell.list, args, 2);

	    XtSetSensitive(ispell.list, True);
	    if (!ispell.checkit)
		XawListHighlight(ispell.list, i);

	    if (old_len > 1 || (XtName(ispell.list) != old_list[0])) {
		while (--old_len > -1)
		    XtFree(old_list[old_len]);
		XtFree((char*)old_list);
	    }

	    if (!ispell.checkit) {
		XtSetArg(args[0], XtNstring, ispell.item);
		XtSetValues(ispell.text, args, 1);
		IspellSetSelection(ispell.left, ispell.right);
		if (ispell.repeat)
		    IspellSetRepeated(ispell.repeat = False);
	    }

	    IspellSetStatus(ispell.buf[0] == '?' ?
			    ispell.guess_label : ispell.miss_label);
	    ispell.undo_terse_mode = ispell.terse_mode;
	    ispell.format_mode = ispell.format_info->value;
	    ispell.lock = True;
	    break;
	case '#':	/* NONE */
	case '-':	/* COMPOUND */
	case '+':	/* ROOT */
	check_label:
	    str = &ispell.sendbuf[1];
	    if (!ispell.checkit) {
		XtSetArg(args[0], XtNlabel, str);
		XtSetValues(ispell.word, args, 1);
	    }

	    XtSetArg(args[0], XtNlist, &old_list);
	    XtSetArg(args[1], XtNnumberStrings, &old_len);
	    XtGetValues(ispell.list, args, 2);
	    ispell.item = NULL;

	    list = (char**)XtMalloc(sizeof(char**));
	    if ((tmp = IspellReplacedWord(str, NULL)) != NULL)
		str = tmp;
	    if (tmp == NULL && ispell.buf[0] == '#')
		list[0] = XtNewString("");
	    else
		list[0] = XtNewString(str);

	    XtSetArg(args[0], XtNlist, list);
	    XtSetArg(args[1], XtNnumberStrings, 1);
	    XtSetValues(ispell.list, args, 2);

	    if (tmp == NULL && ispell.buf[0] == '#') {
		XawListUnhighlight(ispell.list);
		XtSetSensitive(ispell.list, False);
	    }
	    else {
		XtSetSensitive(ispell.list, True);
		if (!ispell.checkit)
		    XawListHighlight(ispell.list, 0);
	    }
	    if (old_len > 1 || (XtName(ispell.list) != old_list[0])) {
		while (--old_len > -1)
		    XtFree(old_list[old_len]);
		XtFree((char*)old_list);
	    }

	    if (!ispell.checkit) {
		XtSetArg(args[0], XtNstring, str);
		XtSetValues(ispell.text, args, 1);
		IspellSetSelection(ispell.left, ispell.right);
		if (ispell.repeat)
		    IspellSetRepeated(ispell.repeat = False);
	    }

	    ispell.undo_terse_mode = ispell.terse_mode;
	    ispell.format_mode = ispell.format_info->value;
	    ispell.lock = True;
	    if (ispell.buf[0] == '+') {
		if ((tmp = strchr(&ispell.buf[2], '\n')) != NULL)
		    *tmp = '\0';
		XmuSnprintf(word, sizeof(word), "%s %s",
			    ispell.root_label, &ispell.buf[2]);
		IspellSetStatus(word);
	    }
	    else
		IspellSetStatus(ispell.buf[0] == '#' ? ispell.none_label :
				ispell.buf[0] == '-' ? ispell.compound_label :
				ispell.ok_label);
	    break;
	case '*':	/* OK */
	case '\0':	/* when running in terse mode */
	    if (!ispell.checkit)
		(void)IspellIgnoredWord(&ispell.sendbuf[1], ADD, 0);
	    else
		goto check_label;
	    ispell.lock = False;
	    break;
	case '@':	/* Ispell banner */
	    /* it only happens when the dictionary is changed */
	    if (!ispell.repeat) {
		XawTextPosition left, right;

		ispell.stat = SEND;
		while (IspellSend() == 0)
		    ;
		/* word chars may have changed */
		XawTextGetSelectionPos(ispell.ascii, &left, &right);
		if (left != ispell.left || right != ispell.right) {
		    XtSetArg(args[0], XtNstring, &ispell.sendbuf[1]);
		    XtSetValues(ispell.text, args, 1);
		    IspellSetSelection(ispell.left, ispell.right);
		}
		ispell.checkit = True;
	    }
	    else {
		IspellSetStatus(ispell.repeat_label);
		ispell.undo_terse_mode = ispell.terse_mode;
		ispell.format_mode = ispell.format_info->value;
		ispell.lock = True;
		return (True);
	    }
	    break;
	default:
	    fprintf(stderr, "Unknown ispell command '%c'\n", ispell.buf[0]);
	    return (False);
    }

    if (!ispell.lock && !ispell.checkit) {
	ispell.stat = SEND;
	while (IspellSend() == 0)
	    ;
    }

    return (True);
}

static int
IspellConvertHtmlAmp(char *buf)
{
    int len, ch = '?';

    /* this function is static, so I can do it */
    *strchr(++buf, ';') = '\0';

    len = strlen(buf);
    if (len == 0)
	return ('&');
    if (len > 1) {
	if (strcasecmp(&buf[1], "lt") == 0)
	    ch = '<';
	else if (strcasecmp(&buf[1], "gt") == 0)
	    ch = '>';
	else if (strcasecmp(&buf[1], "nbsp") == 0)
	    ch = ' ';
	else if (strcasecmp(&buf[1], "amp") == 0)
	    ch = '&';
	else if (strcasecmp(&buf[1], "quot") == 0)
	    ch = '"';
	else if (*buf == '#') {
	    char *tmp;

	    if (len == 1);
		return ('?');
	    ch = strtol(&buf[1], &tmp, 10);
	    if (*tmp)
		fprintf(stderr, "Warning: bad html interpreting '&#' mark.\n");
	}
	else if (strcmp(&buf[1], "acute") == 0) {
	    switch (*buf) {
		case 'a': ch = 'á'; break;
		case 'e': ch = 'é'; break;
		case 'i': ch = 'í'; break;
		case 'o': ch = 'ó'; break;
		case 'u': ch = 'ú'; break;
		case 'A': ch = 'Á'; break;
		case 'E': ch = 'É'; break;
		case 'I': ch = 'Í'; break;
		case 'O': ch = 'Ó'; break;
		case 'U': ch = 'Ú'; break;
	    }
	}
	else if (strcmp(&buf[1], "grave") == 0) {
	    switch (*buf) {
		case 'a': ch = 'à'; break;
		case 'e': ch = 'è'; break;
		case 'i': ch = 'ì'; break;
		case 'o': ch = 'ò'; break;
		case 'u': ch = 'ù'; break;
		case 'A': ch = 'À'; break;
		case 'E': ch = 'È'; break;
		case 'I': ch = 'Ì'; break;
		case 'O': ch = 'Ò'; break;
		case 'U': ch = 'Ù'; break;
	    }
	}
	else if (strcmp(&buf[1], "tilde") == 0) {
	    switch (*buf) {
		case 'a': ch = 'ã'; break;
		case 'o': ch = 'õ'; break;
		case 'n': ch = 'ñ'; break;
		case 'A': ch = 'ã'; break;
		case 'O': ch = 'Õ'; break;
		case 'N': ch = 'Ñ'; break;
	    }
	}
	else if (strcmp(&buf[1], "circ") == 0) {
	    switch (*buf) {
		case 'a': ch = 'â'; break;
		case 'e': ch = 'ê'; break;
		case 'i': ch = 'î'; break;
		case 'o': ch = 'ô'; break;
		case 'u': ch = 'û'; break;
		case 'A': ch = 'Â'; break;
		case 'E': ch = 'Ê'; break;
		case 'I': ch = 'Î'; break;
		case 'O': ch = 'Ô'; break;
		case 'U': ch = 'Û'; break;
	    }
	}
	else if (strcmp(&buf[1], "cedil") == 0) {
	    switch (*buf) {
		case 'c': ch = 'ç'; break;
		case 'C': ch = 'Ç'; break;
	    }
	}
	/* add more cases here */
    }

    return (ch);
}

/*ARGSUSED*/
static int
IspellSend(void)
{
    XawTextPosition position, old_left, pos;
    XawTextBlock block;
    int i, len, spaces, nls;
    Bool nl, html, inside_html;
    char ampbuf[32];
    int amplen;

    if (ispell.lock || ispell.stat != SEND)
	return (-1);

    len = 1;
    ispell.sendbuf[0] = '^';	/* don't evaluate following characters as commands */

    spaces = nls = 0;

    html = ispell.format_info->value == HTML;
    inside_html = False;
    amplen = 0;

    /* skip non word characters */
    pos = position = ispell.right;
    nl = False;
    while (1) {
	Bool done = False;
	char mb[sizeof(wchar_t)];

	retry_html_space:
	position = XawTextSourceRead(ispell.source, position,
				     &block, BUFSIZ);
	if (block.length == 0) {	/* end of file */
	    ispell.stat = 0;
	    ispell.lock = True;
	    XawTextSetInsertionPoint(ispell.ascii, ispell.right);
	    XawTextUnsetSelection(ispell.ascii);
	    IspellSetSensitive(False);
	    IspellSetStatus(ispell.eof_label);
	    return (-1);
	}
	for (i = 0; i < block.length; i++) {
	    if (international)
		wctomb(mb, ((wchar_t*)block.ptr)[i]);
	    else
		*mb = block.ptr[i];
	    if (amplen) {
		if (amplen + 2 >= sizeof(ampbuf)) {
		    if (!ispell.terse_mode)
			fprintf(stderr, "Warning: error interpreting '&' mark.\n");
		    amplen = 0;
		    position = pos + 1;
		    goto retry_html_space;
		}
		else if ((ampbuf[amplen++] = *mb) == ';') {
		    int ch;

		    ampbuf[amplen] = '\0';
		    ch = IspellConvertHtmlAmp(ampbuf);
		    amplen = 0;
		    if (isalpha(ch) ||
			(ch && strchr(ispell.wchars, ch))) {
			/* interpret it again */
			ispell.right = pos;
			i = 0;
			done = True;
			break;
		    }
		    else if ((ch == '\n' || isspace(ch)) && spaces >= 0)
			++spaces;
		    else
			spaces = -1;
		}
	    }
	    else if (html && *mb == '&') {
		ampbuf[amplen++] = *mb;
		pos = block.firstPos + i;
		continue;
	    }
	    else if ((!html || !inside_html) && (isalpha(*mb) ||
		(*mb && strchr(ispell.wchars, *mb)))) {
		done = True;
		break;
	    }
	    else if (!html && *mb == '\n') {
		nl = True;
		if (++nls > 1 && (!html || !inside_html))
		    spaces = -1;
		else if (spaces >= 0)
		    ++spaces;
	    }
	    else if (nl) {
		nl = False;
		if (*mb && strchr(ispell.skip, *mb)) {
		    position = ispell.right =
			XawTextSourceScan(ispell.source, ispell.right + i,
					  XawstEOL, XawsdRight, 1, False);
		    i = 0;
		    break;
		}
		else if (spaces >= 0 && isspace(*mb))
		    ++spaces;
		else
		    spaces = -1;
	    }
	    else if (html && inside_html) {
		if (*mb == '>')
		    inside_html = False;
	    }
	    else if (html && *mb == '<')
		inside_html = True;
	    else if (spaces >= 0 && (isspace(*mb) || (html && *mb == '\n')))
		++spaces;
	    else
		spaces = -1;
	}

	ispell.right += i;
	if (done)
	    break;
    }

    old_left = ispell.left;

    /* read a word */
    position = ispell.left = ispell.right;
    while (1) {
	Bool done = False;
	char mb[sizeof(wchar_t)];

	retry_html_word:
	position = XawTextSourceRead(ispell.source, position,
				     &block, BUFSIZ);
	if (block.length == 0 && len == 1) {	/* end of file */
	    ispell.stat = 0;
	    ispell.lock = True;
	    XawTextSetInsertionPoint(ispell.ascii, ispell.right);
	    XawTextUnsetSelection(ispell.ascii);
	    IspellSetSensitive(False);
	    IspellSetStatus(ispell.eof_label);
	    return (-1);
	}
	for (i = 0; i < block.length; i++) {
	    if (international)
		wctomb(mb, ((wchar_t*)block.ptr)[i]);
	    else
		*mb = block.ptr[i];
	    if (amplen) {
		if (amplen + 2 >= sizeof(ampbuf)) {
		    if (!ispell.terse_mode)
			fprintf(stderr, "Warning: error interpreting '&' mark.\n");
		    amplen = 0;
		    position = pos + 1;
		    if (strchr(ispell.wchars, '&')) {
			if (len + 1 >= sizeof(ispell.sendbuf) - 1) {
			    done = True;
			    fprintf(stderr, "Warning: word is too large!\n");
			    break;
			}
			ispell.sendbuf[len++] = '&';
			goto retry_html_word;
		    }
		    else {
			ispell.right = position;
			i = 0;
			done = True;
			break;
		    }
		}
		else if ((ampbuf[amplen++] = *mb) == ';') {
		    int ch;

		    ampbuf[amplen] = '\0';
		    ch = IspellConvertHtmlAmp(ampbuf);
		    amplen = 0;
		    if (!isalpha(ch) &&
			(!ch || !strchr(ispell.wchars, ch))) {
			ispell.right = pos;
			i = 0;
			done = True;
			break;
		    }
		    *mb = ch;
		}
		else
		    continue;
	    }
	    else if (html && *mb == '&') {
		ampbuf[amplen++] = *mb;
		pos = block.firstPos + i;
		continue;
	    }
	    else if (!isalpha(*mb) && (!*mb || !strchr(ispell.wchars, *mb))) {
		done = True;
		break;
	    }
	    ispell.sendbuf[len] = *mb;
	    if (++len >= sizeof(ispell.sendbuf) - 1) {
		done = True;
		fprintf(stderr, "Warning: word is too large!\n");
		break;
	    }
	}
	ispell.right += i;
	if (done || block.length == 0)
	    break;
    }

    ispell.sendbuf[len] = '\0';

    if (spaces > 0 && spaces <= 32 && strcmp(ispell.sendbuf, ispell.sentbuf) == 0) {
	Arg args[2];
	int old_len;	
	char **list, **old_list;
	char label[sizeof(ispell.sendbuf) + sizeof(ispell.sentbuf) + 32];

	strcpy(label, &ispell.sendbuf[1]);
	for (i = 0; i < spaces; i++)
	    label[len + i - 1] = ' ';
	strcpy(&label[len + i - 1], &ispell.sendbuf[1]);
	XtSetArg(args[0], XtNlabel, label);
	XtSetValues(ispell.word, args, 1);

	XtSetArg(args[0], XtNstring, &ispell.sendbuf[1]);
	XtSetValues(ispell.text, args, 1);

	XtSetArg(args[0], XtNlist, &old_list);
	XtSetArg(args[1], XtNnumberStrings, &old_len);
	XtGetValues(ispell.list, args, 2);
	list = (char**)XtMalloc(sizeof(char**));
	list[0] = XtNewString(&ispell.sendbuf[1]);
	XtSetArg(args[0], XtNlist, list);
	XtSetArg(args[1], XtNnumberStrings, 1);
	XtSetValues(ispell.list, args, 2);
	XtSetSensitive(ispell.list, True);
	XawListHighlight(ispell.list, 0);
	if (old_len > 1 || (XtName(ispell.list) != old_list[0])) {
	    while (--old_len > -1)
		XtFree(old_list[old_len]);
	    XtFree((char*)old_list);
	}

	IspellSetRepeated(True);
	IspellSetSelection(old_left, ispell.right);
	IspellSetStatus(ispell.repeat_label);
	ispell.repeat = ispell.lock = True;

	return (1);
    }
    strcpy(ispell.sentbuf, ispell.sendbuf);

    if (len <= 2 || IspellIgnoredWord(&ispell.sendbuf[1], CHECK, 0))
	return (0);

    ispell.sendbuf[len++] = '\n';

    write(ispell.ofd[1], ispell.sendbuf, len);

    ispell.stat = RECEIVE;

    return (1);
}

/*ARGSUSED*/
static void
IspellInputCallback(XtPointer closure, int *source, XtInputId *id)
{
    if (ispell.right < 0) {
	int len;
	char buf[1024];

	ispell.right = XawTextGetInsertionPoint(ispell.ascii);
	ispell.right = XawTextSourceScan(ispell.source, ispell.right,
					      XawstEOL, XawsdLeft, 1, True);
	len = read(ispell.ifd[0], buf, sizeof(buf));
	if (strncmp(buf, "@(#)", 4) == 0) {
	    Arg args[1];

	    buf[len - 1] = '\0';
	    XtSetArg(args[0], XtNtitle, &buf[5]);
	    XtSetValues(ispell.shell, args, 1);
	}
	else
	    fprintf(stderr, "Error: is ispell talking with me?\n");
	IspellSetTerseMode(ispell.terse_mode);
	while (IspellSend() == 0)
	    ;
    }
    else if (ispell.source)
	IspellReceive();
}

/*ARGSUSED*/
void
IspellCallback(Widget w, XtPointer client_data, XtPointer call_data)
{
    Cardinal zero = 0;

    IspellAction(textwindow, NULL, NULL, &zero);
}

/*ARGSUSED*/
void
IspellAction(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    Arg args[3];
    Cardinal num_args;
    char **strs, **list;
    int n_strs;
    Bool first_time = InitIspell();

    if (*num_params == 1 && (params[0][0] == 'e' || params[0][0] == 'E')) {
	PopdownIspell(w, (XtPointer)True, NULL);
	return;
    }

    if (!XtIsSubclass(w, textWidgetClass) || ispell.source) {
	Feep();
	return;
    }

    ispell.source = XawTextGetSource(ispell.ascii = w);

    if (first_time) {
	/* let the user choose the better position for the ispell window */
	Dimension width, height, b_width;
	Position x, y, max_x, max_y;

	x = y = -1;
	if (event) {
	    switch (event->type) {
		case ButtonPress:
		case ButtonRelease:
		    x = event->xbutton.x_root;
		    y = event->xbutton.y_root;
		    break;
		case KeyPress:
		case KeyRelease:
		    x = event->xkey.x_root;
		    y = event->xkey.y_root;
		    break;
	    }
	}
	if (x < 0 || y < 0) {
	    Window r, c;
	    int rx, ry, wx, wy;
	    unsigned mask;

	    XQueryPointer(XtDisplay(ispell.shell), XtWindow(ispell.shell),
			  &r, &c, &rx, &ry, &wx, &wy, &mask);
	    x = rx;
	    y = ry;
	}

	num_args = 0;
	XtSetArg(args[num_args], XtNwidth, &width);		num_args++;
	XtSetArg(args[num_args], XtNheight, &height);		num_args++;
	XtSetArg(args[num_args], XtNborderWidth, &b_width);	num_args++;
	XtGetValues(ispell.shell, args, num_args);

	width += b_width << 1;
	height += b_width << 1;

	x -= (Position)(width >> 1);
	if (x < 0)
	    x = 0;
	if (x > (max_x = (Position)(XtScreen(w)->width - width)))
	    x = max_x;

	y -= (Position)(height >> 1);
	if (y < 0)
	    y = 0;
	if (y > (max_y = (Position)(XtScreen(w)->height - height)))
	    y = max_y;

	num_args = 0;
	XtSetArg(args[num_args], XtNx, x);	num_args++;
	XtSetArg(args[num_args], XtNy, y);	num_args++;
	XtSetValues(ispell.shell, args, num_args);
    }

    if (ispell.repeat)
	IspellSetRepeated(False);
    ispell.lock = ispell.repeat = ispell.checkit = False;
    ispell.stat = SEND;

    IspellSetSensitive(True);
    XtSetSensitive(ispell.undo, False);

    XtSetArg(args[0], XtNlabel, "");
    XtSetValues(ispell.word, args, 1);

    XtSetArg(args[0], XtNstring, "");
    XtSetValues(ispell.text, args, 1);

    XtSetArg(args[0], XtNlist, &strs);
    XtSetArg(args[1], XtNnumberStrings, &n_strs);
    XtGetValues(ispell.list, args, 2);

    list = (char**)XtMalloc(sizeof(char**));
    list[0] = XtNewString("");
    XtSetArg(args[0], XtNlist, list);
    XtSetArg(args[1], XtNnumberStrings, 1);
    XtSetValues(ispell.list, args, 2);

    if (n_strs > 1 || (XtName(ispell.list) != strs[0])) {
	while (--n_strs > -1)
	    XtFree(strs[n_strs]);
	XtFree((char*)strs);
    }

    IspellSetStatus(ispell.working_label);

    if (!ispell.pid)
	(void)IspellStartProcess();
    else {
	ispell.right = XawTextGetInsertionPoint(ispell.ascii);
	ispell.right = XawTextSourceScan(ispell.source, ispell.right,
					      XawstEOL, XawsdLeft, 1, True);
	while (IspellSend() == 0)
	    ;
    }

    XtPopup(ispell.shell, XtGrabExclusive);
    XtSetKeyboardFocus(ispell.shell, ispell.text);
}

static Bool
IspellStartProcess(void)
{
    if (!ispell.pid) {
	int len;
	char *command;

	ispell.source = XawTextGetSource(ispell.ascii);

	len = strlen(ispell.cmd) + strlen(ispell.dictionary) +
	      strlen(ispell.wchars) + 16;
	command = XtMalloc(len);
	XmuSnprintf(command, len, "%s -a -d '%s' -w '%s'",
		    ispell.cmd, ispell.dictionary, ispell.wchars);

	pipe(ispell.ifd);
	pipe(ispell.ofd);
	if ((ispell.pid = fork()) == 0) {
	    close(0);
	    close(1);
	    dup2(ispell.ofd[0], 0);
	    dup2(ispell.ifd[1], 1);
	    close(ispell.ofd[0]);
	    close(ispell.ofd[1]);
	    close(ispell.ifd[0]);
	    close(ispell.ifd[1]);
	    execl("/bin/sh", "sh", "-c", command, NULL);
	    exit(-127);
	}
	else if (ispell.pid < 0) {
	    fprintf(stderr, "Cannot fork\n");
	    exit(1);
	}
	ispell.buf = XtMalloc(ispell.bufsiz = BUFSIZ);
	ispell.right = -1;
	ispell.id = XtAppAddInput(XtWidgetToApplicationContext(ispell.shell),
				  ispell.ifd[0], (XtPointer)XtInputReadMask,
				  IspellInputCallback, NULL);
	fcntl(ispell.ifd[0], F_SETFL, O_NONBLOCK);
    }
    else
	return (False);

    return (True);
}

/*ARGSUSED*/
static void
PopdownIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    (void)IspellEndProcess((Bool)(long)client_data, True);
    XtPopdown(ispell.shell);
    *ispell.sentbuf = '\0';
}

static Bool
IspellEndProcess(Bool killit, Bool killundo)
{
    ispell.source = NULL;

    if (ispell.pid) {
	IgnoreList *il, *pil, *nil;
	int i;

	/* insert added words in private dictionary */
	for (i = 0; i < ISTRTBLSZ; i++) {
	    pil = il = ignore_list[i];
	    while (il) {
		if (il->add) {
		    nil = il->next;
		    if (il == pil)
			ignore_list[i] = nil;
		    else
			pil->next = nil;
		    if (il->add == UNCAP)
			write(ispell.ofd[1], "&", 1);
		    else
			write(ispell.ofd[1], "*", 1);
		    write(ispell.ofd[1], il->word, strlen(il->word));
		    write(ispell.ofd[1], "\n", 1);
		    XtFree(il->word);
		    XtFree((char*)il);
		    il = nil;
		}
		else
		    il = il->next;
		pil = il;
	    }
	}
	write(ispell.ofd[1], "#\n", 2);		/* save dictionary */

	if (killit) {
	    ReplaceList *rl, *prl;

	    XtRemoveInput(ispell.id);

	    close(ispell.ofd[0]);
	    close(ispell.ofd[1]);
	    close(ispell.ifd[0]);
	    close(ispell.ifd[1]);

	    /* if something goes wrong, we don't want to block here forever */
	    old_timeout = signal(SIGALRM, timeout_signal);
	    alarm(10);
	    waitpid(ispell.pid, NULL, 0);
	    alarm(0);
	    signal(SIGALRM, old_timeout);

	    ispell.pid = 0;
	    if (ispell.buf)
		XtFree(ispell.buf);
	    ispell.buf = NULL;

	    for (i = 0; i < RSTRTBLSZ; i++) {
		prl = rl = replace_list[i];
		while (prl) {
		    rl = rl->next;
		    XtFree(prl->word);
		    XtFree(prl->replace);
		    XtFree((char*)prl);
		    prl = rl;
		}
		replace_list[i] = NULL;
	    }
	    for (i = 0; i < ISTRTBLSZ; i++) {
		pil = il = ignore_list[i];
		while (pil) {
		    il = il->next;
		    XtFree(pil->word);
		    XtFree((char*)pil);
		    pil = il;
		}
		ignore_list[i] = NULL;
	    }
	}

	if (killundo)
	    IspellKillUndoBuffer();
    }
    else
	return (False);

    return (True);
}

static void
IspellKillUndoBuffer(void)
{
    ispell_undo *undo, *pundo;

    undo = pundo = ispell.undo_base;
    while (undo) {
	undo = undo->next;
	if (pundo->undo_str)
	    XtFree(pundo->undo_str);
	XtFree((char*)pundo);
	pundo = undo;
    }
    ispell.undo_base = ispell.undo_head = NULL;
    ispell.undo_for = NULL;
    ispell.undo_depth = 0;
    XtSetSensitive(ispell.undo, False);
}

/*ARGSUSED*/
static void
RevertIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    Arg args[1];
    char *string, *repstr = NULL;

    XtSetArg(args[0], XtNlabel, &string);
    XtGetValues(ispell.word, args, 1);
    if ((repstr = strchr(string, ' ')) != NULL) {
	string = repstr = XtNewString(string);
	*strchr(repstr, ' ') = '\0';
    }
    XtSetArg(args[0], XtNstring, string);
    XtSetValues(ispell.text, args, 1);
    if (repstr)
	XtFree(repstr);
}

/*ARGSUSED*/
static void
SelectIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    XawListReturnStruct *info = (XawListReturnStruct *)call_data;
    Arg args[1];

    XtSetArg(args[0], XtNstring, ispell.item = info->string);
    XtSetValues(ispell.text, args, 1);
}

/*ARGSUSED*/
void
ReplaceIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    XawTextPosition pos = XawTextGetInsertionPoint(ispell.ascii);
    XawTextBlock check, search, replace;
    Arg args[1];
    char *text;

    if (!ispell.lock)
	return;

    XtSetArg(args[0], XtNlabel, &text);
    XtGetValues(ispell.word, args, 1);
    search.ptr = text;
    search.format = XawFmt8Bit;
    search.firstPos = 0;
    search.length = ispell.right - pos;

    XtSetArg(args[0], XtNstring, &text);
    XtGetValues(ispell.text, args, 1);
    replace.ptr = text;
    replace.format = XawFmt8Bit;
    replace.firstPos = 0;
    replace.length = strlen(text);

    if (strcmp(search.ptr, replace.ptr) != 0 &&
	XawTextReplace(ispell.ascii, pos, pos + search.length,
		       &replace) == XawEditDone) {
	ispell.right += replace.length - search.length;
	IspellCheckUndo();
	ispell.undo_head->undo_str = NULL;
	ispell.undo_head->undo_pos = pos;
	ispell.undo_head->undo_count = 1;

	if (ispell.repeat) {
	    ispell.undo_head->repeat = 2; /* To recognize later it was replaced */
	    ispell.undo_head->undo_count = ispell.right;
	    ispell.undo_head->undo_str = XtNewString(search.ptr);
	}
	if (client_data && !ispell.repeat) {
	    XawTextDisableRedisplay(ispell.ascii);
	    pos = ispell.right;
	    while ((pos = XawTextSourceSearch(ispell.source, pos, XawsdRight, &search))
		!= XawTextSearchError) {
		Bool do_replace = True;
		char mb[sizeof(wchar_t)];

		if (XawTextSourceRead(ispell.source, pos - 1, &check, 1) > 0) {
		    if (international)
			wctomb(mb, *(wchar_t*)check.ptr);
		    else
			*mb = *check.ptr;
		    do_replace = !isalpha(*mb) && *mb && !strchr(ispell.wchars, *mb);
		}
		if (do_replace &&
		    XawTextSourceRead(ispell.source, pos + search.length, &check, 1) > 0) {
		    if (international)
			wctomb(mb, *(wchar_t*)check.ptr);
		    else
			*mb = *check.ptr;
		    do_replace = !isalpha(*mb) && *mb && !strchr(ispell.wchars, *mb);
		}
		if (do_replace) {
		    XawTextReplace(ispell.ascii, pos, pos + search.length, &replace);
		    ++ispell.undo_head->undo_count;
		}
		pos += search.length;
	    }
	    XawTextEnableRedisplay(ispell.ascii);
	}
	(void)IspellReplacedWord(search.ptr, replace.ptr);

	strncpy(&ispell.sentbuf[1], replace.ptr, sizeof(ispell.sentbuf) - 2);
	ispell.sentbuf[sizeof(ispell.sentbuf) - 1] = '\0';
    }
    else
	Feep();

    if (ispell.repeat)
	ispell.right = ispell.left = XawTextGetInsertionPoint(ispell.ascii);
    else if (!ispell.terse_mode || !ispell.item ||
	     strcmp(ispell.item, replace.ptr))
	ispell.right = ispell.left;	/* check it again! */

    ispell.lock = ispell.checkit = False;

    ispell.stat = SEND;
    IspellSetStatus(ispell.working_label);
    while (IspellSend() == 0)
	;
}

/*ARGSUSED*/
void
IgnoreIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    Arg args[1];
    char *text;

    if (!ispell.lock)
	return;

    XtSetArg(args[0], XtNlabel, &text);
    XtGetValues(ispell.word, args, 1);

    IspellCheckUndo();

    if ((ispell.undo_head->repeat = ispell.repeat) != False) {
	ispell.undo_head->undo_count = ispell.right;
	ispell.undo_head->undo_str = XtNewString(text);
    }
    else
	ispell.undo_head->undo_count = 0;

    ispell.undo_head->undo_pos = XawTextGetInsertionPoint(ispell.ascii);

    if (!ispell.repeat) {
	if (client_data) {
	    IspellIgnoredWord(text, ADD, 0);
	    ispell.undo_head->undo_str = XtNewString(text);
	}
	else 
	    ispell.undo_head->undo_str = NULL;
    }

    ispell.lock = ispell.checkit = False;

    ispell.stat = SEND;
    IspellSetStatus(ispell.working_label);
    while (IspellSend() == 0)
	;
}

/*ARGSUSED*/
void
AddIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    Arg args[1];
    char *text;
    int cmd = (long)client_data;

    if (!ispell.lock || ispell.repeat)
	return;

    XtSetArg(args[0], XtNlabel, &text);
    XtGetValues(ispell.word, args, 1);

    IspellCheckUndo();
    ispell.undo_head->undo_str = XtNewString(text);
    ispell.undo_head->undo_pos = XawTextGetInsertionPoint(ispell.ascii);
    ispell.undo_head->undo_count = -cmd;

    (void)IspellIgnoredWord(text, ADD, cmd);

    ispell.lock = ispell.checkit = False;
    ispell.stat = SEND;
    IspellSetStatus(ispell.working_label);
    while (IspellSend() == 0)
	;
}

/*ARGSUSED*/
static void
UndoIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    Bool enable_redisplay = False;
    ispell_undo *undo = ispell.undo_head;

    if ((!ispell.lock && ispell.stat) || !undo)
	return;

    if (ispell.undo_for && strcmp(ispell.undo_for, ispell.dictionary)) {
	XeditPrintf("Undo: Dictionary changed. Undo information was lost.\n");
	IspellKillUndoBuffer();
	Feep();
	return;
    }

    if (undo->terse != ispell.terse_mode)
	IspellSetTerseMode(undo->terse);

    if (undo->format != ispell.format_info->value) {
	struct _ispell_format *fmt = &ispell_format[undo->format];
	ChangeFormatIspell(fmt->sme, (XtPointer)fmt, NULL);
    }

    if (undo->undo_count > 0 && !undo->repeat) {
	XawTextPosition tmp;

	enable_redisplay = undo->undo_count > 1;
	if (enable_redisplay)
	    XawTextDisableRedisplay(ispell.ascii);
	while (undo->undo_count--)
	    if (!_XawTextSrcUndo((TextSrcObject)ispell.source, &tmp)) {
		Feep();
		break;
	    }
    }
    else if (undo->undo_count < 0) {
	if (undo->undo_str)
	    (void)IspellIgnoredWord(undo->undo_str, REMOVE, -undo->undo_count);
    }
    else if (undo->undo_str) {
	if (!undo->repeat)
	    IspellIgnoredWord(undo->undo_str, REMOVE, 0);
    }

    XawTextSetInsertionPoint(ispell.ascii,
			     ispell.right = ispell.left = undo->undo_pos);
    if (enable_redisplay)
	XawTextEnableRedisplay(ispell.ascii);

    /* need to do it because may be two misspelled words together */
    if (undo->repeat) {
	char **list, **old_list;
	int old_len;
	Arg args[2];

	if (undo->repeat > 1) {
	    XawTextDisableRedisplay(ispell.ascii);
	    if (!_XawTextSrcUndo((TextSrcObject)ispell.source, &ispell.right))
		Feep();
	    XawTextEnableRedisplay(ispell.ascii);
	}
	else
	    ispell.right = (XawTextPosition)undo->undo_count;
	IspellSetRepeated(ispell.repeat = True);
	XtSetArg(args[0], XtNlabel, undo->undo_str);
	XtSetValues(ispell.word, args, 1);
	XmuSnprintf(ispell.sentbuf, sizeof(ispell.sentbuf), "^%s",
		    strrchr(undo->undo_str, ' ') + 1);
	strcpy(ispell.sendbuf, ispell.sentbuf);
	XtSetArg(args[0], XtNstring, &ispell.sentbuf[1]);
	XtSetValues(ispell.text, args, 1);

	XtSetArg(args[0], XtNlist, &old_list);
	XtSetArg(args[1], XtNnumberStrings, &old_len);
	XtGetValues(ispell.list, args, 2);

	list = (char **)XtMalloc(sizeof(char*));
	list[0] = XtNewString(&ispell.sentbuf[1]);
	XtSetArg(args[0], XtNlist, list);
	XtSetArg(args[1], XtNnumberStrings, 1);
	XtSetValues(ispell.list, args, 2);
	XtSetSensitive(ispell.list, True);
	XawListHighlight(ispell.list, 0);

	if (old_len > 1 || (XtName(ispell.list) != old_list[0])) {
	    while (--old_len > -1)
		XtFree(old_list[old_len]);
	    XtFree((char*)old_list);
	}

	IspellSetSelection(ispell.left, ispell.right);
	IspellSetStatus(ispell.repeat_label);
	ispell.lock = True;
	ispell.checkit = False;
    }
    else if (ispell.repeat) {
	*ispell.sentbuf = '\0';
	IspellSetRepeated(ispell.repeat = False);
    }

    if (undo->prev)
	undo->prev->next = NULL;
    ispell.undo_head = undo->prev;
    if (undo == ispell.undo_base) {
	ispell.undo_base = NULL;
	ispell.undo_for = NULL;
	XtSetSensitive(ispell.undo, False);
    }
    if (undo->undo_str)
	XtFree(undo->undo_str);
    XtFree((char*)undo);
    --ispell.undo_depth;

    if (!ispell.stat || ispell.checkit)
	IspellSetSensitive(True);

    if (!ispell.repeat) {
	ispell.lock = ispell.checkit = False;
	ispell.stat = SEND;
	IspellSetStatus(ispell.working_label);
	while (IspellSend() == 0)
	    ;
    }
}

/*ARGSUSED*/
static void
CheckIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    Arg args[1];
    char *text, *str, string[1024];
    int i, len;

    if (!ispell.lock)
	return;

    XtSetArg(args[0], XtNstring, &text);
    XtGetValues(ispell.text, args, 1);

    /* Check only a word at a time */
    len = 0;
    str = text;
    while (*str) {
	if (isalpha(*str) || strchr(ispell.wchars, *str))
	    break;
	++str;
	++len;
    }
    i = 0;
    while (*str) {
	if (isalpha(*str) || strchr(ispell.wchars, *str))
	    string[i++] = *str++;
	else
	    break;
    }
    string[i] = '\0';

    if (strcmp(text, string)) {
	XawTextPosition pos = XawTextGetInsertionPoint(ispell.text) - len;

	XtSetArg(args[0], XtNstring, string);
	XtSetValues(ispell.text, args, 1);
	XawTextSetInsertionPoint(ispell.text, pos);
	Feep();
    }

    if (i == 0) {
	Feep();
	return;
    }

    len = XmuSnprintf(ispell.sendbuf, sizeof(ispell.sendbuf), "^%s\n", string);

    ispell.sendbuf[sizeof(ispell.sendbuf) - 1] = '\n';

    write(ispell.ofd[1], ispell.sendbuf, len);

    ispell.lock = False;
    ispell.checkit = True;
    ispell.stat = RECEIVE;
}

/*ARGSUSED*/
static void
LookIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    int len, old_len;
    FILE *fd;
    Arg args[2];
    char *text, *str, **list, **old_list, command[1024], buffer[1024];
    Bool sensitive = True;

    if (!ispell.lock)
	return;

    XtSetArg(args[0], XtNstring, &text);
    XtGetValues(ispell.text, args, 1);

    if (!*text) {
	Feep();
	return;
    }

    if (strlen(ispell.look_cmd) + strlen(text) + strlen(ispell.words_file) + 8
	> sizeof(command) - 1) {
	fprintf(stderr, "Command line too large\n");
	return;
    }

    XmuSnprintf(command, sizeof(command), "%s '^%s.*$' %s",
		ispell.look_cmd, text, ispell.words_file);

    if ((fd = popen(command, "r")) == NULL) {
	fprintf(stderr, "Cannot popen '%s'\n", ispell.look_cmd);
	return;
    }

    list = NULL;
    len = 0;

#define	MAX_LOOK_RESULTS	256
    while (fgets(buffer, sizeof(buffer), fd) != NULL) {
	if ((str = strchr(buffer, '\n')) == NULL) {
	    fprintf(stderr, "String is too large\n");
	    break;
	}
	*str = '\0';
	if ((len % 16) == 0)
	    list = (char**)XtRealloc((char*)list, sizeof(char*) * (len + 16));
	list[len] = XtNewString(buffer);
	if (++len >= MAX_LOOK_RESULTS) {
	    Feep();
	    break;
	}
    }
#undef MAX_LOOK_RESULTS

    XtSetArg(args[0], XtNlist, &old_list);
    XtSetArg(args[1], XtNnumberStrings, &old_len);
    XtGetValues(ispell.list, args, 2);

    if (len == 0) {
	list = (char**)XtMalloc(sizeof(char*));
	list[0] = XtNewString("");
	len = 1;
	sensitive = False;
    }

    XtSetArg(args[0], XtNlist, list);
    XtSetArg(args[1], XtNnumberStrings, len);
    XtSetValues(ispell.list, args, 2);

    XtSetSensitive(ispell.list, sensitive);
    IspellSetStatus(sensitive ? ispell.look_label : ispell.none_label);

    if (old_len > 1 || (XtName(ispell.list) != old_list[0])) {
	while (--old_len > -1)
	    XtFree(old_list[old_len]);
	XtFree((char*)old_list);
    }

    pclose(fd);
}

/*ARGSUSED*/
static void
ToggleTerseIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    if (!ispell.lock)
	return;

    ispell.terse_mode = !ispell.terse_mode;
    write(ispell.ofd[1], ispell.terse_mode ? "!\n" : "%\n", 2);
}

/*ARGSUSED*/
static void
ChangeDictionaryIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    ispell_dict *tmp, *dic = (ispell_dict*)client_data;
    XawTextPosition pos = XawTextGetInsertionPoint(ispell.ascii);
    XawTextPosition right = ispell.right;
    Arg args[1];

    if (strcmp(XtName(dic->sme), ispell.dictionary) == 0)
	return;

    if (!ispell.lock) {
	Feep();
	return;
    }

    for (tmp = ispell.dict_info; tmp; tmp = tmp->next)
	if (strcmp(XtName(tmp->sme), ispell.dictionary) == 0) {
	    XtSetArg(args[0], XtNleftBitmap, None);
	    XtSetValues(tmp->sme, args, 1);
	}

    if (ispell.undo_base && !ispell.undo_for)
	ispell.undo_for = ispell.dictionary;

    XtSetArg(args[0], XtNleftBitmap, flist.pixmap);
    XtSetValues(dic->sme, args, 1);
    ispell.dictionary = XtName(dic->sme);
    ispell.wchars = dic->wchars;
    XtSetArg(args[0], XtNlabel, XtName(dic->sme));
    XtSetValues(ispell.dict, args, 1);

    IspellSetStatus(ispell.working_label);

    (void)IspellEndProcess(True, False);
    ispell.lock = ispell.checkit = False;
    (void)IspellStartProcess();

    ispell.stat = RECEIVE;

    /* restart at the same selected word */
    if (ispell.repeat == False)
	ispell.left = ispell.right = pos;
    else
	ispell.right = right;
}

/*ARGSUSED*/
static void
ChangeFormatIspell(Widget w, XtPointer client_data, XtPointer call_data)
{
    struct _ispell_format *fmt = (struct _ispell_format*)client_data;
    Arg args[1];

    if (strcmp(fmt->name, ispell.formatting) == 0)
	return;

    if (!ispell.lock) {
	Feep();
	return;
    }

    XtSetArg(args[0], XtNleftBitmap, None);
    XtSetValues(ispell.format_info->sme, args, 1);

    XtSetArg(args[0], XtNleftBitmap, flist.pixmap);
    XtSetValues(fmt->sme, args, 1);
    ispell.formatting = fmt->name;
    ispell.format_info = fmt;
    XtSetArg(args[0], XtNlabel, fmt->name);
    XtSetValues(ispell.format, args, 1);
}

static Bool
InitIspell(void)
{
    Atom delete_window;
    char *str, *list;
    XtResource dict_res;
    int i;
    static XtResource text_res[] = {
	{"skipLines", "Skip", XtRString, sizeof(char*),
	 XtOffsetOf(struct _ispell, skip), XtRString, "#"},
    };

    if (ispell.shell)
	return (False);

    ispell.shell	= XtCreatePopupShell("ispell", transientShellWidgetClass,
					     topwindow, NULL, 0);

    XtGetApplicationResources(ispell.shell, (XtPointer)&ispell, resources,
			      XtNumber(resources), NULL, 0);

    ispell.form		= XtCreateManagedWidget("form", formWidgetClass,
						ispell.shell, NULL, 0);
    ispell.mispelled	= XtCreateManagedWidget("mispelled", labelWidgetClass,
						ispell.form, NULL, 0);
    ispell.repeated	= XtCreateWidget("repeated", labelWidgetClass,
					 ispell.form, NULL, 0);
    ispell.word		= XtCreateManagedWidget("word", commandWidgetClass,
						ispell.form, NULL, 0);
    XtAddCallback(ispell.word, XtNcallback, RevertIspell, NULL);
    ispell.replacement	= XtCreateManagedWidget("replacement", labelWidgetClass,
						ispell.form, NULL, 0);
    ispell.text		= XtVaCreateManagedWidget("text", asciiTextWidgetClass,
						ispell.form,
						XtNeditType, XawtextEdit,
						NULL, 0);
    ispell.suggestions	= XtCreateManagedWidget("suggestions", labelWidgetClass,
						ispell.form, NULL, 0);
    ispell.viewport	= XtCreateManagedWidget("viewport", viewportWidgetClass,
						ispell.form, NULL, 0);
    ispell.list		= XtCreateManagedWidget("list", listWidgetClass,
						ispell.viewport, NULL, 0);
    XtAddCallback(ispell.list, XtNcallback, SelectIspell, NULL);
    ispell.commands	= XtCreateManagedWidget("commands", formWidgetClass,
						ispell.form, NULL, 0);
    ispell.check	= XtCreateManagedWidget("check", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.check, XtNcallback, CheckIspell, NULL);
    ispell.look		= XtCreateManagedWidget("look", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.look, XtNcallback, LookIspell, NULL);
    ispell.undo		= XtCreateManagedWidget("undo", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.undo, XtNcallback, UndoIspell, NULL);
    ispell.replace	= XtCreateManagedWidget("replace", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.replace, XtNcallback, ReplaceIspell, (XtPointer)False);
    ispell.replaceAll	= XtCreateManagedWidget("replaceAll", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.replaceAll, XtNcallback, ReplaceIspell, (XtPointer)True);
    ispell.ignore	= XtCreateManagedWidget("ignore", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.ignore, XtNcallback, IgnoreIspell, (XtPointer)False);
    ispell.ignoreAll	= XtCreateManagedWidget("ignoreAll", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.ignoreAll, XtNcallback, IgnoreIspell, (XtPointer)True);
    ispell.add		= XtCreateManagedWidget("add", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.add, XtNcallback, AddIspell, (XtPointer)ASIS);
    ispell.addUncap	= XtCreateManagedWidget("addUncap", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.addUncap, XtNcallback, AddIspell, (XtPointer)UNCAP);
    ispell.suspend	= XtCreateManagedWidget("suspend", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.suspend, XtNcallback, PopdownIspell, (XtPointer)False);
    ispell.cancel	= XtCreateManagedWidget("cancel", commandWidgetClass,
						ispell.commands, NULL, 0);
    XtAddCallback(ispell.cancel, XtNcallback, PopdownIspell, (XtPointer)True);
    ispell.terse	= XtVaCreateManagedWidget("terse", toggleWidgetClass,
						  ispell.commands,
						  XtNstate, ispell.terse_mode,
						  NULL, 0);
    XtAddCallback(ispell.terse, XtNcallback, ToggleTerseIspell, NULL);
    ispell.status	= XtCreateManagedWidget("status", labelWidgetClass,
						ispell.form, NULL, 0);
    ispell.options	= XtCreateManagedWidget("options", formWidgetClass,
						ispell.form, NULL, 0);
    ispell.dict		= XtVaCreateManagedWidget("dict", menuButtonWidgetClass,
						  ispell.options,
						  XtNmenuName, "dictionaries",
						  NULL, 0);
    ispell.dictMenu	= XtCreatePopupShell("dictionaries", simpleMenuWidgetClass,
					     ispell.options, NULL, 0);
    XtRealizeWidget(ispell.dictMenu);

    ispell.format	= XtVaCreateManagedWidget("format", menuButtonWidgetClass,
						  ispell.options,
						  XtNmenuName, "formats",
						  NULL, 0);
    ispell.formatMenu	= XtCreatePopupShell("formats", simpleMenuWidgetClass,
					     ispell.options, NULL, 0);
    XtRealizeWidget(ispell.formatMenu);

    XtRealizeWidget(ispell.shell);

    for (i = 0; i < sizeof(ispell_format) / sizeof(ispell_format[0]); i++) {
	struct _ispell_format *fmt = &ispell_format[i];

	fmt->sme = XtCreateManagedWidget(fmt->name, smeBSBObjectClass,
					 ispell.formatMenu, NULL, 0);
	XtAddCallback(fmt->sme, XtNcallback, ChangeFormatIspell, (XtPointer)fmt);

	if (strcmp(fmt->name, ispell.formatting) == 0) {
	    Arg args[1];

	    XtSetArg(args[0], XtNlabel, ispell.formatting);
	    XtSetValues(ispell.format, args, 1);
	    XtSetArg(args[0], XtNleftBitmap, flist.pixmap);
	    XtSetValues(fmt->sme, args, 1);
	    ispell.format_info = fmt;
	}
    }
    if (ispell.format_info == NULL) {
	Arg args[1];
	char msg[256];

	ispell.format_info = &ispell_format[TEXT];

	XmuSnprintf(msg, sizeof(msg),
		    "Unrecognized formatting type \"%s\", will use \"%s\"",
		    ispell.formatting, ispell.format_info->name);
	XtAppWarning(XtWidgetToApplicationContext(ispell.shell), msg);
	ispell.formatting = ispell.format_info->name;

	XtSetArg(args[0], XtNlabel, ispell.format_info->name);
	XtSetValues(ispell.format, args, 1);
	XtSetArg(args[0], XtNleftBitmap, flist.pixmap);
	XtSetValues(ispell.format_info->sme, args, 1);
    }
    XtGetApplicationResources(ispell_format[TEXT].sme, (XtPointer)&ispell,
			      text_res, XtNumber(text_res), NULL, 0);

    dict_res.resource_name = "wordChars";
    dict_res.resource_class = "Chars";
    dict_res.resource_type = XtRString;
    dict_res.resource_size = sizeof(char*);
    dict_res.resource_offset = XtOffsetOf(ispell_dict, wchars);
    dict_res.default_type = XtRString;
    dict_res.default_addr = "";

    list = XtNewString(ispell.dict_list);
    for (str = strtok(list, " \t,"); str; str = strtok(NULL, " \t,")) {
	ispell_dict *dic = XtNew(ispell_dict);

	dic->sme = XtCreateManagedWidget(str, smeBSBObjectClass,
					 ispell.dictMenu, NULL, 0);
	XtGetApplicationResources(dic->sme, (XtPointer)dic, &dict_res,
				  1, NULL, 0);
	XtAddCallback(dic->sme, XtNcallback, ChangeDictionaryIspell,
		      (XtPointer)dic);
	dic->next = NULL;
	if (!ispell.dict_info)
	    ispell.dict_info = dic;
	else {
	    ispell_dict *tmp = ispell.dict_info;

	    for (; tmp->next; tmp = tmp->next)
		;
	    tmp->next = dic;
	}
	if (strcmp(str, ispell.dictionary) == 0) {
	    Arg args[1];

	    XtSetArg(args[0], XtNleftBitmap, flist.pixmap);
	    XtSetValues(dic->sme, args, 1);
	    XtSetArg(args[0], XtNlabel, str);
	    XtSetValues(ispell.dict, args, 1);
	    ispell.wchars = dic->wchars;
	}
    }
    XtFree(list);

    delete_window = XInternAtom(XtDisplay(ispell.shell), "WM_DELETE_WINDOW", False);
    XSetWMProtocols(XtDisplay(ispell.shell), XtWindow(ispell.shell), &delete_window, 1);

    return (True);
}
