/*
 * Copyright 1996, 1999, 2000 by Paul Mattes.
 *  Permission to use, copy, modify, and distribute this software and its
 *  documentation for any purpose and without fee is hereby granted,
 *  provided that the above copyright notice appear in all copies and that
 *  both that copyright notice and this permission notice appear in
 *  supporting documentation.
 */

/*
 *	keymap.c
 *		This module handles keymaps.
 */

#include "globals.h"
#include <X11/IntrinsicP.h>
#include <X11/Shell.h>
#include <X11/StringDefs.h>
#include <X11/Xaw/Command.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Text.h>
#include <X11/Xaw/AsciiText.h>
#include <X11/Xaw/AsciiSrc.h>
#include <errno.h>
#include "appres.h"
#include "objects.h"
#include "resources.h"

#include "hostc.h"
#include "keymapc.h"
#include "keypadc.h"
#include "kybdc.h"
#include "popupsc.h"
#include "screenc.h"
#include "statusc.h"
#include "utilc.h"

#define PA_ENDL		" " PA_END "()"

Boolean keymap_changed = False;
struct trans_list *trans_list = (struct trans_list *)NULL;
static struct trans_list **last_trans = &trans_list;
static struct trans_list *tkm_last;
struct trans_list *temp_keymaps;	/* temporary keymap list */
char *keymap_trace = CN;
static char *last_keymap = CN;
static Boolean last_nvt = False;

static void setup_keymaps(const char *km, Boolean do_popup);
static void add_keymap(const char *name, Boolean do_popup);
static void add_trans(const char *name, char *translations, char *pathname,
    Boolean is_from_server);
static char *get_file_keymap(const char *name, char **pathp);
static void keymap_3270_mode(Boolean);

/* Undocumented Xt function to convert translations to text. */
extern String _XtPrintXlations(Widget w, XtTranslations xlations,
    Widget accelWidget, Boolean includeRHS);

extern Widget *screen;

#if defined(X3270_MENUS) /*[*/
extern Pixmap diamond;
extern Pixmap no_diamond;

static enum { SORT_EVENT, SORT_KEYMAP, SORT_ACTION } sort = SORT_KEYMAP;

static Boolean km_isup = False;
static Widget km_shell, sort_event, sort_keymap, sort_action, text;
static char km_file[128];
static void create_text(void);
static void km_up(Widget w, XtPointer client_data, XtPointer call_data);
static void km_down(Widget w, XtPointer client_data, XtPointer call_data);
static void km_done(Widget w, XtPointer client_data, XtPointer call_data);
static void do_sort_action(Widget w, XtPointer client_data,
    XtPointer call_data);
static void do_sort_keymap(Widget w, XtPointer client_data,
    XtPointer call_data);
static void do_sort_event(Widget w, XtPointer client_data, XtPointer call_data);
static void format_xlations(String s, FILE *f);
static int action_cmp(char *s1, char *s2);
static int keymap_cmp(char *k1, int l1, char *k2, int l2);
static int event_cmp(char *e1, char *e2);
static Boolean is_temp(char *k);
static char *pathname(char *k);
static Boolean from_server(char *k);
static void km_regen(void);
#else /*][*/
#define km_regen()
#endif /*]*/

/* Keymap initialization. */
void
keymap_init(const char *km)
{
	static Boolean initted = False;

	if (km == CN)
		if ((km = (char *)getenv("KEYMAP")) == CN)
			if ((km = (char *)getenv("KEYBD")) == CN)
				km = "@server";
	setup_keymaps(km, initted);
	if (!initted) {
		initted = True;
		last_nvt = IN_ANSI;
		register_schange(ST_3270_MODE, keymap_3270_mode);
		register_schange(ST_CONNECT, keymap_3270_mode);
	} else {
		screen_set_keymap();
		keypad_set_keymap();
	}
	km_regen();

	/* Save the name(s) of the last keymap, so we can switch modes later. */
	if (km != last_keymap) {
		if (last_keymap != CN) {
			Free(last_keymap);
			last_keymap = CN;
		}
		if (km != CN)
			last_keymap = NewString(km);
	}
}

/*
 * 3270/NVT mode change.
 */
static void
keymap_3270_mode(Boolean ignored unused)
{
	if (last_nvt != IN_ANSI) {
		last_nvt = IN_ANSI;
		keymap_init(last_keymap);
	}
}

/*
 * Set up a user keymap.
 */
static void
setup_keymaps(const char *km, Boolean do_popup)
{
	Boolean saw_apl_keymod = False;
	struct trans_list *t;
	struct trans_list *next;

	/* Make sure it starts with "base". */
	if (km == CN)
		km = "base";
	else {
		char *k = XtMalloc(5 + strlen(km) + 1);

		(void) sprintf(k, "base,%s", km);
		km = k;
	}

	if (do_popup)
		keymap_changed = True;

	/* Clear out any existing translations. */
	appres.key_map = (char *)NULL;
	for (t = trans_list; t != (struct trans_list *)NULL; t = next) {
		next = t->next;
		XtFree(t->name);
		if (t->pathname != CN)
			XtFree(t->pathname);
		XtFree((char *)t);
	}
	trans_list = (struct trans_list *)NULL;
	last_trans = &trans_list;

	/* Build up the new list. */
	if (km != CN) {
		char *ns = XtNewString(km);
		char *n0 = ns;
		char *comma;

		do {
			comma = strchr(ns, ',');
			if (comma)
				*comma = '\0';
			if (!strcmp(ns, Apl))
				saw_apl_keymod = True;
			add_keymap(ns, do_popup);
			if (comma)
				ns = comma + 1;
			else
				ns = NULL;
		} while (ns);

		XtFree(n0);
	}
	if (appres.apl_mode && !saw_apl_keymod)
		add_keymap(Apl, do_popup);
}

/*
 * Get a keymap from a file.
 */
static char *
get_file_keymap(const char *name, char **pathp)
{
#if 0
	char *home;
#endif
	char *path;
	XrmDatabase dd = (XrmDatabase)NULL;
	char *resname;
	XrmValue value;
	char *type;
	char *r = CN;

	*pathp = CN;

#if 0
	/* Look for a user's keymap file. */
	home = getenv("HOME");
	if (home != CN) {
		path = xs_buffer("%s/.x3270/keymap.%s", home, name);
		dd = XrmGetFileDatabase(path);
		if (dd != (XrmDatabase)NULL)
			*pathp = path;
		else
			XtFree(path);
	}
#endif

	/* Look for a global keymap file. */
	if (dd == (XrmDatabase)NULL) {
		path = xs_buffer("%s/keymap.%s", LIBX3270DIR, name);
		dd = XrmGetFileDatabase(path);
		if (dd != (XrmDatabase)NULL)
			*pathp = path;
		else {
			XtFree(path);
			return CN;
		}
	}

	/* Look up the resource in that file. */
	resname = xs_buffer("%s.%s.%s", XtName(toplevel), ResKeymap, name);
	if ((XrmGetResource(dd, resname, 0, &type, &value) == True) &&
		    *value.addr) {
		r = XtNewString(value.addr);
	} else {
		*pathp = CN;
	}
	XtFree(resname);
	XrmDestroyDatabase(dd);
	return r;
}

/*
 * Add to the list of user-specified keymap translations, finding both the
 * system and user versions of a keymap.
 */
static void
add_keymap(const char *name, Boolean do_popup)
{
	char *translations, *translations_nvt;
	char *buf, *buf_nvt;
	int any = 0;
	char *path, *path_nvt;
	Boolean is_from_server = False;

	if (appres.key_map == (char *)NULL)
		appres.key_map = XtNewString(name);
	else {
		char *t = xs_buffer("%s,%s", appres.key_map, name);

		XtFree(appres.key_map);
		appres.key_map = t;
	}

	/* Translate '@server' to a vendor-specific keymap. */
	if (!strcmp(name, "@server")) {
		struct sk {
			struct sk *next;
			char *vendor;
			char *keymap;
		};
		static struct sk *sk_list = (struct sk *)NULL;
		struct sk *sk;

		if (sk_list == (struct sk *)NULL) {
			char *s, *vendor, *keymap;

			s = get_resource("serverKeymapList");
			if (s == CN)
				return;
			s = XtNewString(s);
			while (split_dresource(&s, &vendor, &keymap) == 1) {
				sk = (struct sk *)XtMalloc(sizeof(struct sk));
				sk->vendor = vendor;
				sk->keymap = keymap;
				sk->next = sk_list;
				sk_list = sk;
			}
		}
		for (sk = sk_list; sk != (struct sk *)NULL; sk = sk->next) {
			if (!strcmp(sk->vendor, ServerVendor(display))) {
				name = sk->keymap;
				is_from_server = True;
				break;
			}
		}
		if (sk == (struct sk *)NULL)
			return;
	}

	/* Try for a file first, then resources. */
	translations = get_file_keymap(name, &path);
	buf_nvt = xs_buffer("%s.%s", name, ResNvt);
	translations_nvt = get_file_keymap(buf_nvt, &path_nvt);
	if (translations != CN || translations_nvt != CN) {
		any++;
		if (IN_ANSI && translations_nvt != CN)
			add_trans(buf_nvt, translations_nvt, path_nvt,
			    is_from_server);
		else if (translations != CN)
			add_trans(name, translations, path, is_from_server);
		if (translations != CN)
			XtFree(translations);
		if (translations_nvt != CN)
			XtFree(translations_nvt);
		XtFree(buf_nvt);
	} else {
		XtFree(buf_nvt);

		/* Shared keymap. */
		buf = xs_buffer("%s.%s", ResKeymap, name);
		translations = get_resource(buf);
		buf_nvt = xs_buffer("%s.%s.%s", ResKeymap, name, ResNvt);
		translations_nvt = get_resource(buf_nvt);
		if (translations != CN || translations_nvt != CN)
			any++;
		if (IN_ANSI && translations_nvt != CN)
			add_trans(buf_nvt + strlen(ResKeymap) + 1,
			    translations_nvt, CN, is_from_server);
		else if (translations != CN)
			add_trans(name, translations, CN, is_from_server);
		XtFree(buf);
		XtFree(buf_nvt);

		/* User keymap */
		buf = xs_buffer("%s.%s.%s", ResKeymap, name, ResUser);
		translations = get_resource(buf);
		buf_nvt = xs_buffer("%s.%s.%s.%s", ResKeymap, name, ResNvt,
		    ResUser);
		translations_nvt = get_resource(buf_nvt);
		if (translations != CN || translations_nvt != CN)
			any++;
		if (IN_ANSI && translations_nvt != CN)
			add_trans(buf_nvt + strlen(ResKeymap) + 1,
			    translations_nvt, CN, is_from_server);
		else if (translations != CN)
			add_trans(buf, translations, CN, is_from_server);
		XtFree(buf);
		XtFree(buf_nvt);
	}

	if (!any) {
		if (do_popup)
			popup_an_error("Cannot find %s \"%s\"", ResKeymap,
			    name);
		else
			xs_warning("Cannot find %s \"%s\"", ResKeymap, name);
	}
}

/*
 * Add a single keymap name and translation to the translation list.
 */
static void
add_trans(const char *name, char *translations, char *path_name,
    Boolean is_from_server)
{
	struct trans_list *t;

	t = (struct trans_list *)XtMalloc(sizeof(*t));
	t->name = XtNewString(name);
	t->pathname = path_name;
	t->is_temp = False;
	t->from_server = is_from_server;
	(void) lookup_tt(name, translations);
	t->next = NULL;
	*last_trans = t;
	last_trans = &t->next;
}

/*
 * Translation table expansion.
 */

/* Find the first unquoted newline an an action list. */
static char *
unquoted_newline(char *s)
{
	Boolean bs = False;
	enum { UQ_BASE, UQ_PLIST, UQ_Q } state = UQ_BASE;
	char c;

	for ( ; (c = *s); s++) {
		if (bs) {
			bs = False;
			continue;
		} else if (c == '\\') {
			bs = True;
			continue;
		}
		switch (state) {
		    case UQ_BASE:
			if (c == '(')
				state = UQ_PLIST;
			else if (c == '\n')
				return s;
			break;
		    case UQ_PLIST:
			if (c == ')')
				state = UQ_BASE;
			else if (c == '"')
				state = UQ_Q;
			break;
		    case UQ_Q:
			if (c == '"')
				state = UQ_PLIST;
			break;
		}
	}
	return CN;
}

/* Expand a translation table with keymap tracing calls. */
static char *
expand_table(const char *name, char *table)
{
	char *cm, *t0, *t, *s;
	int nlines = 1;

	if (table == CN)
		return CN;

	/* Roughly count the number of lines in the table. */
	cm = table;
	while ((cm = strchr(cm, '\n')) != CN) {
		nlines++;
		cm++;
	}

	/* Allocate a new buffer. */
	t = t0 = (char *)XtMalloc(2 + strlen(table) +
	    nlines * (strlen(" " PA_KEYMAP_TRACE "(,nnnn) ") + strlen(name) +
		      strlen(PA_ENDL)));
	*t = '\0';

	/* Expand the table into it. */
	s = table;
	nlines = 0;
	while (*s) {
		/* Skip empty lines. */
		while (*s == ' ' || *s == '\t')
			s++;
		if (*s == '\n') {
			*t++ = '\n';
			*t = '\0';
			s++;
			continue;
		}

		/* Find the '>' from the event name, and copy up through it. */
		cm = strchr(s, '>');
		if (cm == CN) {
			while ((*t++ = *s++))
				;
			break;
		}
		while (s <= cm)
			*t++ = *s++;

		/* Find the ':' following, and copy up throught that. */
		cm = strchr(s, ':');
		if (cm == CN) {
			while ((*t++ = *s++))
				;
			break;
		}
		nlines++;
		while (s <= cm)
			*t++ = *s++;

#if defined(X3270_TRACE) /*[*/
		/* Insert a PA-KeymapTrace call. */
		(void) sprintf(t, " " PA_KEYMAP_TRACE "(%s,%d) ", name, nlines);
		t = strchr(t, '\0');
#endif /*]*/

		/*
		 * Copy to the next unquoted newline and append a PA-End call.
		 */
		cm = unquoted_newline(s);
		if (cm == CN) {
			while ((*t = *s)) {
				t++;
				s++;
			}
		} else {
			while (s < cm)
				*t++ = *s++;
		}
#if defined(X3270_TRACE) /*[*/
		(void) strcpy(t, PA_ENDL);
		t += strlen(PA_ENDL);
#endif /*]*/
		if (cm == CN)
			break;
		else
			*t++ = *s++;
	}
	*t = '\0';

	return t0;
}

#if defined(X3270_TRACE) /*[*/
/*
 * Trace a keymap.
 *
 * This function leaves a value in the global "keymap_trace", which is used
 * by the action_debug function when subsequent actions are called.
 */
void
PA_KeymapTrace_action(Widget w unused, XEvent *event unused, String *params,
    Cardinal *num_params)
{
	if (!toggled(EVENT_TRACE) || *num_params != 2)
		return;
	if (keymap_trace != CN)
		XtFree((XtPointer)keymap_trace);
	keymap_trace = XtMalloc(strlen(params[0]) + 1 + strlen(params[1]) + 1);
	(void) sprintf(keymap_trace, "%s:%s", params[0], params[1]);
}
#endif /*]*/

/*
 * End a keymap trace.
 *
 * This function clears the value in the global "keymap_trace".
 */
void
PA_End_action(Widget w unused, XEvent *event unused, String *params unused,
    Cardinal *num_params unused)
{
	if (keymap_trace != CN)
		XtFree((XtPointer)keymap_trace);
	keymap_trace = CN;
}

/*
 * Translation table cache.
 */
XtTranslations
lookup_tt(const char *name, char *table)
{
	struct tt_cache {
		char *name;
		XtTranslations trans;
		struct tt_cache *next;
	};
#	define TTN (struct tt_cache *)NULL
	static struct tt_cache *tt_cache = TTN;
	struct tt_cache *t;
	char *xtable;

	/* Look for an old one. */
	for (t = tt_cache; t != TTN; t = t->next)
		if (!strcmp(name, t->name))
			return t->trans;

	/* Allocate and translate a new one. */
	t = (struct tt_cache *)XtMalloc(sizeof(*t));
	t->name = XtNewString(name);
	xtable = expand_table(name, table);
	t->trans = XtParseTranslationTable(xtable);
	XtFree((XtPointer)xtable);
	t->next = tt_cache;
	tt_cache = t;

	return t->trans;
}
#undef TTN

/*
 * Set or clear a temporary keymap.
 *
 * If the parameter is CN, removes all keymaps.
 * Otherwise, toggles the keymap by that name.
 *
 * Returns 0 if the action was successful, -1 otherwise.
 *
 */
int
temporary_keymap(char *k)
{
	char *kmname, *km;
	XtTranslations trans;
	struct trans_list *t, *prev;
	char *path = CN;
#	define TN (struct trans_list *)NULL

	if (k == CN) {
		struct trans_list *next;

		/* Delete all temporary keymaps. */
		for (t = temp_keymaps; t != TN; t = next) {
			XtFree((XtPointer)t->name);
			if (t->pathname != CN)
				XtFree((XtPointer)t->pathname);
			next = t->next;
			XtFree((XtPointer)t);
		}
		tkm_last = temp_keymaps = TN;
		screen_set_temp_keymap((XtTranslations)NULL);
		keypad_set_temp_keymap((XtTranslations)NULL);
		status_kmap(False);
		km_regen();
		return 0;
	}

	/* Check for deleting one keymap. */
	for (prev = TN, t = temp_keymaps; t != TN; prev = t, t = t->next)
		if (!strcmp(k, t->name))
			break;
	if (t != TN) {

		/* Delete the keymap from the list. */
		if (prev != TN)
			prev->next = t->next;
		else
			temp_keymaps = t->next;
		if (tkm_last == t)
			tkm_last = prev;
		XtFree((XtPointer)t->name);
		XtFree((XtPointer)t);

		/* Rebuild the translation tables from the remaining ones. */
		screen_set_temp_keymap((XtTranslations)NULL);
		keypad_set_temp_keymap((XtTranslations)NULL);
		for (t = temp_keymaps; t != TN; t = t->next) {
			trans = lookup_tt(t->name, CN);
			screen_set_temp_keymap(trans);
			keypad_set_temp_keymap(trans);
		}

		/* Update the status line. */
		if (temp_keymaps == TN)
			status_kmap(False);
		km_regen();
		return 0;
	}

	/* Add a keymap. */

	/* Try a file first. */
	km = get_file_keymap(k, &path);
	if (km == CN) {
		/* Then try a resource. */
		kmname = xs_buffer("%s.%s", ResKeymap, k);
		km = get_resource(kmname);
		XtFree(kmname);
		if (km == CN)
			return -1;
	}

	/* Update the translation tables. */
	trans = lookup_tt(k, km);
	screen_set_temp_keymap(trans);
	keypad_set_temp_keymap(trans);

	/* Add it to the list. */
	t = (struct trans_list *)XtMalloc(sizeof(*t));
	t->name = XtNewString(k);
	t->pathname = path;
	t->is_temp = True;
	t->from_server = False;
	t->next = TN;
	if (tkm_last != TN)
		tkm_last->next = t;
	else
		temp_keymaps = t;
	tkm_last = t;

	/* Update the status line. */
	status_kmap(True);
	km_regen();

	/* Success. */
	return 0;
}
#undef TN

#if defined(X3270_MENUS) /*[*/
/* Create and pop up the current keymap pop-up. */
void
do_keymap_display(Widget w unused, XtPointer userdata unused,
    XtPointer calldata unused)
{
	Widget form, label, done;

	/* Create the popup. */
	km_shell = XtVaCreatePopupShell(
	    "kmPopup", transientShellWidgetClass, toplevel,
	    NULL);
	XtAddCallback(km_shell, XtNpopupCallback, place_popup,
	    (XtPointer) CenterP);
	XtAddCallback(km_shell, XtNpopupCallback, km_up, (XtPointer)NULL);
	XtAddCallback(km_shell, XtNpopdownCallback, km_down, (XtPointer)NULL);

	/* Create a form in the popup. */
	form = XtVaCreateManagedWidget(
	    ObjDialog, formWidgetClass, km_shell,
	    NULL);

	/* Create the title. */
	label = XtVaCreateManagedWidget("label", labelWidgetClass, form,
	    XtNborderWidth, 0,
	    NULL);

	/* Create the options. */
	sort_event = XtVaCreateManagedWidget("sortEventOption",
	    commandWidgetClass, form,
	    XtNborderWidth, 0,
	    XtNfromVert, label,
	    XtNleftBitmap, sort == SORT_EVENT ? diamond : no_diamond,
	    NULL);
	XtAddCallback(sort_event, XtNcallback, do_sort_event,
	    (XtPointer)NULL);
	sort_keymap = XtVaCreateManagedWidget("sortKeymapOption",
	    commandWidgetClass, form,
	    XtNborderWidth, 0,
	    XtNfromVert, sort_event,
	    XtNleftBitmap, sort == SORT_KEYMAP ? diamond : no_diamond,
	    NULL);
	XtAddCallback(sort_keymap, XtNcallback, do_sort_keymap,
	    (XtPointer)NULL);
	sort_action = XtVaCreateManagedWidget("sortActionOption",
	    commandWidgetClass, form,
	    XtNborderWidth, 0,
	    XtNfromVert, sort_keymap,
	    XtNleftBitmap, sort == SORT_ACTION ? diamond : no_diamond,
	    NULL);
	XtAddCallback(sort_action, XtNcallback, do_sort_action,
	    (XtPointer)NULL);

	/* Create a text widget attached to the file. */
	text = XtVaCreateManagedWidget(
	    "text", asciiTextWidgetClass, form,
	    XtNfromVert, sort_action,
	    XtNscrollHorizontal, XawtextScrollAlways,
	    XtNscrollVertical, XawtextScrollAlways,
	    XtNdisplayCaret, False,
	    NULL);
	create_text();

	/* Create the Done button. */
	done = XtVaCreateManagedWidget(ObjConfirmButton,
	    commandWidgetClass, form,
	    XtNfromVert, text,
	    NULL);
	XtAddCallback(done, XtNcallback, km_done, (XtPointer)NULL);

	/* Pop it up. */
	popup_popup(km_shell, XtGrabNone);
}

/* Format the keymap into a text source. */
static void
create_text(void)
{
	String s;
	FILE *f;
	Widget source, old;

	/* Ready a file. */
	(void) sprintf(km_file, "/tmp/km.%d", getpid());
	f = fopen(km_file, "w");
	if (f == (FILE *)NULL) {
		popup_an_errno(errno, "temporary file open");
		return;
	}

	s = _XtPrintXlations(*screen, (*screen)->core.tm.translations, NULL,
	    True);
	format_xlations(s, f);
	XtFree(s);
	fclose(f);
	source = XtVaCreateWidget(
	    "source", asciiSrcObjectClass, text,
	    XtNtype, XawAsciiFile,
	    XtNstring, km_file,
	    XtNeditType, XawtextRead,
	    NULL);
	old = XawTextGetSource(text);
	XawTextSetSource(text, source, (XawTextPosition)0);
	XtDestroyWidget(old);
}

/* Refresh the keymap display, if it's up. */
static void
km_regen(void)
{
	if (km_isup)
		create_text();
}

/* Popup callback. */
static void
km_up(Widget w unused, XtPointer client_data unused, XtPointer call_data unused)
{
	km_isup = True;
}

/* Popdown callback.  Destroy the widget. */
static void
km_down(Widget w unused, XtPointer client_data unused,
    XtPointer call_data unused)
{
	km_isup = False;
	(void) unlink(km_file);
	XtDestroyWidget(km_shell);
}

/* Done button callback.  Pop down the widget. */
static void
km_done(Widget w unused, XtPointer client_data unused,
    XtPointer call_data unused)
{
	XtPopdown(km_shell);
}

/* "Sort-by-event" button callback. */
static void
do_sort_event(Widget w unused, XtPointer client_data unused,
    XtPointer call_data unused)
{
	if (sort != SORT_EVENT) {
		sort = SORT_EVENT;
		XtVaSetValues(sort_action, XtNleftBitmap, no_diamond, NULL);
		XtVaSetValues(sort_keymap, XtNleftBitmap, no_diamond, NULL);
		XtVaSetValues(sort_event, XtNleftBitmap, diamond, NULL);
		create_text();
	}
}

/* "Sort-by-keymap" button callback. */
static void
do_sort_keymap(Widget w unused, XtPointer client_data unused,
    XtPointer call_data unused)
{
	if (sort != SORT_KEYMAP) {
		sort = SORT_KEYMAP;
		XtVaSetValues(sort_action, XtNleftBitmap, no_diamond, NULL);
		XtVaSetValues(sort_keymap, XtNleftBitmap, diamond, NULL);
		XtVaSetValues(sort_event, XtNleftBitmap, no_diamond, NULL);
		create_text();
	}
}

/* "Sort-by-action" button callback. */
static void
do_sort_action(Widget w unused, XtPointer client_data unused,
    XtPointer call_data unused)
{
	if (sort != SORT_ACTION) {
		sort = SORT_ACTION;
		XtVaSetValues(sort_action, XtNleftBitmap, diamond, NULL);
		XtVaSetValues(sort_keymap, XtNleftBitmap, no_diamond, NULL);
		XtVaSetValues(sort_event, XtNleftBitmap, no_diamond, NULL);
		create_text();
	}
}

#define DASHES \
    "-------------------------- ---------------- ------------------------------------"

/*
 * Format translations for display.
 *
 * The data from _XtPrintXlations looks like this:
 *  [<space>]event:<space>[PA-KeymapTrace("keymap","line")<space>][action...]
 * with the delightful complication that embedded quotes are not quoted.
 *
 * What we want to do is to:
 *  remove all lines without PA-KeymapTrace
 *  remove the leading space
 *  sort by actions list
 *  reformat as:
 *    action... event (keymap:line)
 */
static void
format_xlations(String s, FILE *f)
{
	char *t;
	char *t_next;
	struct xl {
		struct xl *next;
		char *actions;
		char *event;
		char *keymap;
		int km_line;
		char *full_keymap;
	} *xl_list = (struct xl *)NULL, *x, *xs, *xlp, *xn;
	char *km_last;
	int line_last = 0;

	/* Construct the list. */
	for (t = s; t != CN; t = t_next) {
		char *k, *a, *kk;
		int nq;
		static char cmps[] = ": " PA_KEYMAP_TRACE "(";

		/* Find the end of this rule and terminate this line. */
		t_next = strstr(t, PA_ENDL "\n");
		if (t_next != CN) {
			t_next += strlen(PA_ENDL);
			*t_next++ = '\0';
		}

		/* Remove the leading space. */
		while (*t == ' ')
			t++;

		/* Use only traced events. */
		k = strstr(t, cmps);
		if (k == CN)
			continue;
		*k = '\0';
		k += strlen(cmps);

		/* Find the rest of the actions. */
		a = strchr(k, ')');
		if (a == CN)
			continue;
		while (*(++a) == ' ')
			;
		if (!*a)
			continue;

		/* Remove the trailing PA-End call. */
		if (strlen(a) >= strlen(PA_ENDL) &&
		    !strcmp(a + strlen(a) - strlen(PA_ENDL), PA_ENDL))
			a[strlen(a) - strlen(PA_ENDL)] = '\0';

		/* Allocate the new element. */
		x = (struct xl *)XtCalloc(sizeof(struct xl), 1);
		x->actions = XtNewString(a);
		x->event = XtNewString(t);
		x->keymap = kk = (char *)XtMalloc(a - k + 1);
		x->km_line = 0;
		x->full_keymap = (char *)XtMalloc(a - k + 1);
		nq = 0;
		while (*k != ')') {
			if (*k == '"') {
				nq++;
			} else if (nq == 1) {
				*kk++ = *k;
			} else if (nq == 3) {
				x->km_line = atoi(k);
				break;
			}
			k++;
		}
		*kk = '\0';
		(void) sprintf(x->full_keymap, "%s:%d", x->keymap, x->km_line);

		/* Find where it should be inserted. */
		for (xs = xl_list, xlp = (struct xl *)NULL;
		     xs != (struct xl *)NULL;
		     xlp = xs, xs = xs->next) {
			int halt = 0;

			switch (sort) {
			    case SORT_EVENT:
				halt = (event_cmp(xs->event, x->event) > 0);
				break;
			    case SORT_KEYMAP:
				halt = (keymap_cmp(xs->keymap, xs->km_line,
						   x->keymap, x->km_line) > 0);
				break;
			    case SORT_ACTION:
				halt = (action_cmp(xs->actions, a) > 0);
				break;
			}
			if (halt)
				break;
		}

		/* Insert it. */
		if (xlp != (struct xl *)NULL) {
			x->next = xlp->next;
			xlp->next = x;
		} else {
			x->next = xl_list;
			xl_list = x;
		}
	}

	/* Walk it. */
	if (sort != SORT_KEYMAP)
		(void) fprintf(f, "%-26s %-16s %s\n%s\n",
		    get_message("kmEvent"),
		    get_message("kmKeymapLine"),
		    get_message("kmActions"),
		    DASHES);
	km_last = CN;
	for (xs = xl_list; xs != (struct xl *)NULL; xs = xs->next) {
		switch (sort) {
		    case SORT_EVENT:
			if (km_last != CN) {
				char *l;

				l = strchr(xs->event, '<');
				if (l != CN) {
					if (strcmp(km_last, l))
						(void) fprintf(f, "\n");
					km_last = l;
				}
			} else
				km_last = strchr(xs->event, '<');
			break;
		    case SORT_KEYMAP:
			if (km_last == CN || strcmp(xs->keymap, km_last)) {
				char *p;

				(void) fprintf(f, "%s%s '%s'%s",
				    km_last == CN ? "" : "\n",
				    get_message(is_temp(xs->keymap) ?
						    "kmTemporaryKeymap" :
						    "kmKeymap"),
				    xs->keymap,
				    from_server(xs->keymap) ?
					get_message("kmFromServer") : "");
				if ((p = pathname(xs->keymap)) != CN)
					(void) fprintf(f, ", %s %s",
					    get_message("kmFile"), p);
				else
					(void) fprintf(f,
					    ", %s %s.%s.%s",
					    get_message("kmResource"),
					    programname, ResKeymap, xs->keymap);
				(void) fprintf(f,
				    "\n%-26s %-16s %s\n%s\n",
				    get_message("kmEvent"),
				    get_message("kmKeymapLine"),
				    get_message("kmActions"),
				    DASHES);
				km_last = xs->keymap;
				line_last = 0;
			}
			while (xs->km_line != ++line_last) {
				(void) fprintf(f, "%-26s %s:%d\n",
				    get_message("kmOverridden"),
				    xs->keymap, line_last);
			}
			break;
		    case SORT_ACTION:
			break;
		}
		(void) fprintf(f, "%-26s %-16s ",
		    xs->event, xs->full_keymap);
		(void) fcatv(f, xs->actions);
		(void) fputc('\n', f);
	}

	/* Free it. */
	for (xs = xl_list; xs != (struct xl *)NULL; xs = xn) {
		xn = xs->next;
		XtFree((XtPointer)xs->actions);
		XtFree((XtPointer)xs->event);
		XtFree((XtPointer)xs->keymap);
		XtFree((XtPointer)xs->full_keymap);
		XtFree((XtPointer)xs);
	}
}

/*
 * Comparison function for actions.  Basically, a strcmp() that handles "PF"
 * and "PA" specially.
 */
#define PA	"PA("
#define PF	"PF("
static int
action_cmp(char *s1, char *s2)
{
	if ((!strncmp(s1, PA, 3) && !strncmp(s2, PA, 3)) ||
	    (!strncmp(s1, PF, 3) && !strncmp(s2, PF, 3)))
		return atoi(s1 + 4) - atoi(s2 + 4);
	else
		return strcmp(s1, s2);
	    
}

/* Return a keymap's index in the lists. */
static int
km_index(char *n)
{
	struct trans_list *t;
	int ix = 0;

	for (t = trans_list; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, n))
			return ix;
		ix++;
	}
	for (t = temp_keymaps; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, n))
			return ix;
		ix++;
	}
	return ix;
}

/* Return whether or not a keymap is temporary. */
static Boolean
is_temp(char *k)
{
	struct trans_list *t;

	for (t = trans_list; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->is_temp;
	}
	for (t = temp_keymaps; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->is_temp;
	}
	return False;
}

/* Return the pathname associated with a keymap. */
static char *
pathname(char *k)
{
	struct trans_list *t;

	for (t = trans_list; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->pathname;
	}
	for (t = temp_keymaps; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->pathname;
	}
	return CN;
}

/* Return whether or not a keymap was translated from "@server". */
static Boolean
from_server(char *k)
{
	struct trans_list *t;

	for (t = trans_list; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->from_server;
	}
	for (t = temp_keymaps; t != (struct trans_list *)NULL; t = t->next) {
		if (!strcmp(t->name, k))
			return t->from_server;
	}
	return False;
}

/*
 * Comparison function for keymaps.
 */
static int
keymap_cmp(char *k1, int l1, char *k2, int l2)
{
	/* If the keymaps are the same, do a numerical comparison. */
	if (!strcmp(k1, k2))
		return l1 - l2;


	/* The keymaps are different.  Order them according to trans_list. */
	return km_index(k1) - km_index(k2);
}

/*
 * strcmp() that handles <KeyPress>Fnn numercally.
 */
static int
Fnn_strcmp(char *s1, char *s2)
{
	static char kp[] = "<KeyPress>F";
#	define KPL (sizeof(kp) - 1)

	if (strncmp(s1, kp, KPL) || !isdigit(s1[KPL]) ||
	    strncmp(s2, kp, KPL) || !isdigit(s2[KPL]))
		return strcmp(s1, s2);
	return atoi(s1 + KPL) - atoi(s2 + KPL);
}

/*
 * Comparison function for events.
 */
static int
event_cmp(char *e1, char *e2)
{
	char *l1, *l2;
	int r;

	/* If either has a syntax problem, do a straight string compare. */
	if ((l1 = strchr(e1, '<')) == CN ||
	    (l2 = strchr(e2, '<')) == CN)
		return strcmp(e1, e2);

	/*
	 * If the events are different, sort on the event only.  Otherwise,
	 * sort on the modifier(s).
	 */
	r = Fnn_strcmp(l1, l2);
	if (r)
		return r;
	else
		return strcmp(e1, e2);
}
#endif /*]*/
