/* NetHack 5.0	winX.c	$NHDT-Date: 1717967337 2024/06/09 21:08:57 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.136 $ */
/* Copyright (c) Dean Luick, 1992                                 */
/* NetHack may be freely redistributed.  See license for details. */

/*
 * "Main" file for the X window-port.  This contains most of the interface
 * routines.  Please see doc/window.txt for an description of the window
 * interface.
 */

#ifndef SYSV
#define PRESERVE_NO_SYSV /* X11 include files may define SYSV */
#endif

#ifdef MSDOS /* from compiler */
#define SHORT_FILENAMES
#endif

/* Can't use #ifdef LINUX because no header files have been processed yet.
 * This is needed to ensure the prototype for seteuid() is picked up when
 * the header files are processed.
 */
#ifdef __linux__
#define _POSIX_C_SOURCE 200809
#endif

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>
#include <X11/Xaw/AsciiText.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Scrollbar.h>
#include <X11/Xaw/Paned.h>
#include <X11/Xaw/Cardinals.h>
#include <X11/Xatom.h>
#include <X11/Xos.h>

/* for color support */
#ifdef SHORT_FILENAMES
#include <X11/IntrinsP.h>
#else
#include <X11/IntrinsicP.h>
#endif

#ifdef PRESERVE_NO_SYSV
#ifdef SYSV
#undef SYSV
#endif
#undef PRESERVE_NO_SYSV
#endif

#ifdef SHORT_FILENAMES
#undef SHORT_FILENAMES /* hack.h will reset via global.h if necessary */
#endif

#define X11_BUILD
#include "hack.h"
#undef X11_BUILD

#include "winX.h"
#include "dlb.h"
#include "xwindow.h"

#ifndef NO_SIGNAL
#include <signal.h>
#endif

/* Should be defined in <X11/Intrinsic.h> but you never know */
#ifndef XtSpecificationRelease
#define XtSpecificationRelease 0
#endif

/*
 * Icons.
 */
#include "../win/X11/nh72icon"
#include "../win/X11/nh56icon"
#include "../win/X11/nh32icon"

static struct icon_info {
    const char *name;
    unsigned char *bits;
    unsigned width, height;
} icon_data[] = {
    { "nh72", nh72icon_bits, nh72icon_width, nh72icon_height },
    { "nh56", nh56icon_bits, nh56icon_width, nh56icon_height },
    { "nh32", nh32icon_bits, nh32icon_width, nh32icon_height },
    { (const char *) 0, (unsigned char *) 0, 0, 0 }
};

/*
 * Private global variables (shared among the window port files).
 */
struct xwindow window_list[MAX_WINDOWS];
AppResources appResources;
void (*input_func)(Widget, XEvent *, String *, Cardinal *);
int click_x, click_y, click_button; /* Click position on a map window
                                     * (filled by set_button_values()). */
int updated_inventory; /* used to indicate perm_invent updating */
color_attr X11_menu_promptstyle = { NO_COLOR, ATR_NONE };

/* X11/Intrinsic.h prototype has an issue if [[NORETURN]] is used
 * rather than the old __attribute((noreturn)) under c23 */
#if defined(__GNUC__) || defined(__clang__)
#define USE_GNUC_PROTO
#endif

#ifdef USE_GNUC_PROTO
static void X11_error_handler(String) __attribute__((noreturn));
#else
ATTRNORETURN static XtErrorHandler X11_error_handler(String);
#endif

static int X11_io_error_handler(Display *);

static int (*old_error_handler)(Display *, XErrorEvent *);

#if defined(HANGUPHANDLING)
#if !defined(NO_SIGNAL) && defined(SAFERHANGUP)
#if XtSpecificationRelease >= 6
#define X11_HANGUP_SIGNAL
static XtSignalId X11_sig_id;
#endif
#endif
#endif

/* Interface definition, for windows.c */
struct window_procs X11_procs = {
    WPID(X11),
    ( WC_COLOR | WC_INVERSE | WC_HILITE_PET | WC_ASCII_MAP | WC_TILED_MAP
     | WC_PLAYER_SELECTION | WC_PERM_INVENT | WC_MOUSE_SUPPORT ),
    /* status requires VIA_WINDOWPORT(); WC2_FLUSH_STATUS ensures that */
    ( WC2_FLUSH_STATUS | WC2_SELECTSAVED
#ifdef STATUS_HILITES
      | WC2_RESET_STATUS | WC2_HILITE_STATUS
#endif
      | WC2_MENU_SHIFT ),
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, /* color availability */
    X11_init_nhwindows,
    X11_player_selection, X11_askname, X11_get_nh_event, X11_exit_nhwindows,
    X11_suspend_nhwindows, X11_resume_nhwindows, X11_create_nhwindow,
    X11_clear_nhwindow, X11_display_nhwindow, X11_destroy_nhwindow, X11_curs,
    X11_putstr, genl_putmixed, X11_display_file, X11_start_menu, X11_add_menu,
    X11_end_menu, X11_select_menu,
    genl_message_menu, /* no need for X-specific handling */
    X11_mark_synch, X11_wait_synch,
#ifdef CLIPPING
    X11_cliparound,
#endif
#ifdef POSITIONBAR
    donull,
#endif
    X11_print_glyph, X11_raw_print, X11_raw_print_bold, X11_nhgetch,
    X11_nh_poskey, X11_nhbell, X11_doprev_message, X11_yn_function,
    X11_getlin, X11_get_ext_cmd, X11_number_pad, X11_delay_output,
#ifdef CHANGE_COLOR /* only a Mac option currently */
    donull, donull,
#endif
#ifdef GRAPHIC_TOMBSTONE
    X11_outrip,
#else
    genl_outrip,
#endif
    X11_preference_update, X11_getmsghistory, X11_putmsghistory,
    X11_status_init, X11_status_finish, X11_status_enablefield,
    X11_status_update,
    genl_can_suspend_no, /* XXX may not always be correct */
    X11_update_inventory,
    X11_ctrl_nhwindow,
};

/*
 * Local functions.
 */
static winid find_free_window(void);
static void nhFreePixel(XtAppContext, XrmValuePtr, XtPointer, XrmValuePtr,
                        Cardinal *);
static boolean new_resource_macro(String, unsigned);
static void load_default_resources(void);
static void release_default_resources(void);
static int panic_on_error(Display *, XErrorEvent *);
#ifdef X11_HANGUP_SIGNAL
static void X11_sig(int);
static void X11_sig_cb(XtPointer, XtSignalId *);
#endif
static void d_timeout(XtPointer, XtIntervalId *);
static void X11_hangup(Widget, XEvent *, String *, Cardinal *);
ATTRNORETURN static void X11_bail(const char *) NORETURN;
static void askname_delete(Widget, XEvent *, String *, Cardinal *);
static void askname_done(Widget, XtPointer, XtPointer);
static void done_button(Widget, XtPointer, XtPointer);
static void getline_delete(Widget, XEvent *, String *, Cardinal *);
static void abort_button(Widget, XtPointer, XtPointer);
static void release_getline_widgets(void);
static void yn_delete(Widget, XEvent *, String *, Cardinal *);
static void yn_key(Widget, XEvent *, String *, Cardinal *);
static void release_yn_widgets(void);
static int input_event(int);
static void win_visible(Widget, XtPointer, XEvent *, Boolean *);
static void init_standard_windows(void);

/*
 * Local variables.
 */
static boolean x_inited = FALSE;    /* TRUE if window system is set up.     */
static winid message_win = WIN_ERR, /* These are the winids of the message, */
             map_win = WIN_ERR,     /*   map, and status windows, when they */
             status_win = WIN_ERR;  /*   are created in init_windows().     */
static Pixmap icon_pixmap = None;   /* Pixmap for icon.                     */

void
X11_putmsghistory(const char *msg, boolean is_restoring)
{
    if (WIN_MESSAGE != WIN_ERR) {
        struct xwindow *wp = &window_list[WIN_MESSAGE];
        debugpline2("X11_putmsghistory('%s',%i)", msg, is_restoring);
        if (msg)
            append_message(wp, msg);
    }
}

char *
X11_getmsghistory(boolean init)
{
    if (WIN_MESSAGE != WIN_ERR) {
        static struct line_element *curr = (struct line_element *) 0;
        static int numlines = 0;
        struct xwindow *wp = &window_list[WIN_MESSAGE];

        if (init)
            curr = (struct line_element *) 0;

        if (!curr) {
            curr = wp->mesg_information->head;
            numlines = 0;
        }

        if (numlines < wp->mesg_information->num_lines) {
            curr = curr->next;
            numlines++;
            debugpline2("X11_getmsghistory(%i)='%s'", init, curr->line);
            return curr->line;
        }
    }
    return (char *) 0;
}

/*
 * Find the window structure that corresponds to the given widget.  Note
 * that this is not the popup widget, nor the viewport, but the child.
 */
struct xwindow *
find_widget(Widget w)
{
    int windex;
    struct xwindow *wp;

    /*
     * Search to find the corresponding window.  Look at the main widget,
     * popup, the parent of the main widget, then parent of the widget.
     */
    for (windex = 0, wp = window_list; windex < MAX_WINDOWS; windex++, wp++)
        if (wp->type != NHW_NONE && (wp->w == w || wp->popup == w
                                     || (wp->w && (XtParent(wp->w)) == w)
                                     || (wp->popup == XtParent(w))))
            break;

    if (windex == MAX_WINDOWS)
        panic("find_widget:  can't match widget");
    return wp;
}

/*
 * Find a free window slot for use.
 */
static winid
find_free_window(void)
{
    int windex;
    struct xwindow *wp;

    for (windex = 0, wp = &window_list[0]; windex < MAX_WINDOWS;
         windex++, wp++)
        if (wp->type == NHW_NONE)
            break;

    if (windex == MAX_WINDOWS)
        panic("find_free_window: no free windows!");
    return (winid) windex;
}


XColor
get_nhcolor(struct xwindow *wp, int clr)
{
    init_menu_nhcolors(wp);
    /* FIXME: init_menu_nhcolors may fail */

    if (clr >= 0 && clr < CLR_MAX)
        return wp->nh_colors[clr];

    return wp->nh_colors[0];
}

void
init_menu_nhcolors(struct xwindow *wp)
{
    static const char *mapCLR_to_res[CLR_MAX] = {
        XtNblack,
        XtNred,
        XtNgreen,
        XtNbrown,
        XtNblue,
        XtNmagenta,
        XtNcyan,
        XtNgray,
        XtNforeground,
        XtNorange,
        XtNbright_green,
        XtNyellow,
        XtNbright_blue,
        XtNbright_magenta,
        XtNbright_cyan,
        XtNwhite,
    };
    static const char *wintypenames[NHW_TEXT] = {
        "message", "status", "map", "menu", "text"
    };
    Display *dpy;
    Colormap screen_colormap;
    XrmDatabase rDB;
    XrmValue value;
    Status rc;
    int color;
    char *ret_type[32];
    char wtn_up[20], clr_name[BUFSZ], clrclass[BUFSZ];
    const char *wtn;

    if (wp->nh_colors_inited || !wp->type)
        return;

    wtn = wintypenames[wp->type - 1];
    Strcpy(wtn_up, wtn);
    (void) upstart(wtn_up);

    dpy = XtDisplay(wp->w);
    screen_colormap = DefaultColormap(dpy, DefaultScreen(dpy));
    rDB = XrmGetDatabase(dpy);

    for (color = 0; color < CLR_MAX; color++) {
        Sprintf(clr_name, "nethack.%s.%s", wtn, mapCLR_to_res[color]);
        Sprintf(clrclass, "NetHack.%s.%s", wtn_up, mapCLR_to_res[color]);

        if (!XrmGetResource(rDB, clr_name, clrclass, ret_type, &value)) {
            Sprintf(clr_name, "nethack.map.%s", mapCLR_to_res[color]);
            Sprintf(clrclass, "NetHack.Map.%s", mapCLR_to_res[color]);
        }

        if (!XrmGetResource(rDB, clr_name, clrclass, ret_type, &value)) {
            impossible("XrmGetResource error (%s)", clr_name);
        } else if (!strcmp(ret_type[0], "String")) {
            char tmpbuf[256];

            if (value.size >= sizeof tmpbuf)
                value.size = sizeof tmpbuf - 1;
            (void) strncpy(tmpbuf, (char *) value.addr, (int) value.size);
            tmpbuf[value.size] = '\0';
            /* tmpbuf now contains the color name from the named resource */

            rc = XAllocNamedColor(dpy, screen_colormap, tmpbuf,
                                  &wp->nh_colors[color],
                                  &wp->nh_colors[color]);
            if (rc == 0) {
                impossible("XAllocNamedColor failed for color %i (%s)",
                           color, clr_name);
            }
        }
    }

    wp->nh_colors_inited = TRUE;
}

/*
 * Color conversion.  The default X11 color converters don't try very
 * hard to find matching colors in PseudoColor visuals.  If they can't
 * allocate the exact color, they puke and give you something stupid.
 * This is an attempt to find some close readonly cell and use it.
 */
XtConvertArgRec const nhcolorConvertArgs[] = {
    { XtWidgetBaseOffset,
      (XtPointer) (ptrdiff_t) XtOffset(Widget, core.screen),
      sizeof (Screen *) },
    { XtWidgetBaseOffset,
      (XtPointer) (ptrdiff_t) XtOffset(Widget, core.colormap),
      sizeof (Colormap) }
};

#define done(type, value)                             \
    {                                                 \
        if (toVal->addr != 0) {                       \
            if (toVal->size < sizeof(type)) {         \
                toVal->size = sizeof(type);           \
                return False;                         \
            }                                         \
            *(type *)(toVal->addr) = (value);         \
        } else {                                      \
            static type static_val;                   \
            static_val = (value);                     \
            toVal->addr = (genericptr_t) &static_val; \
        }                                             \
        toVal->size = sizeof(type);                   \
        return True;                                  \
    }

/*
 * Find a color that approximates the color named in "str".
 * The "str" color may be a color name ("red") or number ("#7f0000").
 * If str is Null, then "color" is assumed to contain the RGB color wanted.
 * The approximate color found is returned in color as well.
 * Return True if something close was found.
 */
Boolean
nhApproxColor(
    Screen *screen,    /* screen to use */
    Colormap colormap, /* the colormap to use */
    char *str,         /* color name */
    XColor *color)     /* the X color structure; changed only if successful */
{
    int ncells;
    long cdiff = 16777216; /* 2^24; hopefully our map is smaller */
    XColor tmp;
    static XColor *table = 0;
    int i, j;
    long tdiff;

    /* if the screen doesn't have a big colormap, don't waste our time
       or if it's huge, and _some_ match should have been possible */
    if ((ncells = CellsOfScreen(screen)) < 256 || ncells > 4096)
        return False;

    if (str != (char *) 0) {
        if (!XParseColor(DisplayOfScreen(screen), colormap, str, &tmp))
            return False;
    } else {
        tmp = *color;
        tmp.flags = 7; /* force to use all 3 of RGB */
    }

    if (!table) {
        table = (XColor *) XtCalloc(ncells, sizeof(XColor));
        for (i = 0; i < ncells; i++)
            table[i].pixel = i;
        XQueryColors(DisplayOfScreen(screen), colormap, table, ncells);
    }

    /* go thru cells and look for the one with smallest diff;
       diff is calculated abs(reddiff)+abs(greendiff)+abs(bluediff);
       a more knowledgeable color person might improve this -dlc */
 try_again:
    for (i = 0; i < ncells; i++) {
        if (table[i].flags == tmp.flags) {
            j = (int) table[i].red - (int) tmp.red;
            if (j < 0)
                j = -j;
            tdiff = j;
            j = (int) table[i].green - (int) tmp.green;
            if (j < 0)
                j = -j;
            tdiff += j;
            j = (int) table[i].blue - (int) tmp.blue;
            if (j < 0)
                j = -j;
            tdiff += j;
            if (tdiff < cdiff) {
                cdiff = tdiff;
                tmp.pixel = i; /* table[i].pixel == i */
            }
        }
    }

    if (cdiff == 16777216)
        return False; /* nothing found?! */

    /*
     * Found something.  Return it and mark this color as used to avoid
     * reuse.  Reuse causes major contrast problems :-)
     */
    *color = table[tmp.pixel];
    table[tmp.pixel].flags = 0;
    /* try to alloc the color, so no one else can change it */
    if (!XAllocColor(DisplayOfScreen(screen), colormap, color)) {
        cdiff = 16777216;
        goto try_again;
    }
    return True;
}

Boolean
nhCvtStringToPixel(
    Display *dpy,
    XrmValuePtr args, Cardinal *num_args,
    XrmValuePtr fromVal, XrmValuePtr toVal,
    XtPointer *closure_ret)
{
    String str = (String) fromVal->addr;
    XColor screenColor;
    XColor exactColor;
    Screen *screen;
    XtAppContext app = XtDisplayToApplicationContext(dpy);
    Colormap colormap;
    Status status;
    String params[1];
    Cardinal num_params = 1;

    if (*num_args != 2) {
        XtAppWarningMsg(app, "wrongParameters",
                        "cvtStringToPixel", "XtToolkitError",
             "String to pixel conversion needs screen and colormap arguments",
                        (String *) 0, (Cardinal *) 0);
        return False;
    }

    screen = *((Screen **) args[0].addr);
    colormap = *((Colormap *) args[1].addr);

/* If Xt colors, use the Xt routine and hope for the best */
#if (XtSpecificationRelease >= 5)
    if ((strcmpi(str, XtDefaultBackground) == 0)
        || (strcmpi(str, XtDefaultForeground) == 0)) {
        return XtCvtStringToPixel(dpy, args, num_args, fromVal, toVal,
                                  closure_ret);
    }
#else
    if (strcmpi(str, XtDefaultBackground) == 0) {
        *closure_ret = (char *) False;
        done(Pixel, WhitePixelOfScreen(screen));
    }
    if (strcmpi(str, XtDefaultForeground) == 0) {
        *closure_ret = (char *) False;
        done(Pixel, BlackPixelOfScreen(screen));
    }
#endif

    status = XAllocNamedColor(DisplayOfScreen(screen), colormap, (char *) str,
                              &screenColor, &exactColor);
    if (status == 0) {
        String msg, type;

        /* some versions of XAllocNamedColor don't allow #xxyyzz names */
        if (str[0] == '#' && XParseColor(DisplayOfScreen(screen), colormap,
                                         str, &exactColor)
            && XAllocColor(DisplayOfScreen(screen), colormap, &exactColor)) {
            *closure_ret = (char *) True;
            done(Pixel, exactColor.pixel);
        }

        params[0] = str;
        /* Server returns a specific error code but Xlib discards it.  Ugh */
        if (XLookupColor(DisplayOfScreen(screen), colormap, (char *) str,
                         &exactColor, &screenColor)) {
            /* try to find another color that will do */
            if (nhApproxColor(screen, colormap, (char *) str, &screenColor)) {
                *closure_ret = (char *) True;
                done(Pixel, screenColor.pixel);
            }
            type = nhStr("noColormap");
            msg = nhStr("Cannot allocate colormap entry for \"%s\"");
        } else {
            /* some versions of XLookupColor also don't allow #xxyyzz names */
            if (str[0] == '#'
                && (nhApproxColor(screen, colormap, (char *) str,
                                  &screenColor))) {
                *closure_ret = (char *) True;
                done(Pixel, screenColor.pixel);
            }
            type = nhStr("badValue");
            msg = nhStr("Color name \"%s\" is not defined");
        }

        XtAppWarningMsg(app, type, "cvtStringToPixel", "XtToolkitError", msg,
                        params, &num_params);
        *closure_ret = False;
        return False;
    } else {
        *closure_ret = (char *) True;
        done(Pixel, screenColor.pixel);
    }
}

/* Ask the WM for window frame size */
void
get_window_frame_extents(
    Widget w,
    long *top, long *bottom,
    long *left, long *right)
{
    XEvent event;
    Display *dpy = XtDisplay(w);
    Window win = XtWindow(w);
    Atom prop, retprop;
    int retfmt;
    unsigned long nitems;
    unsigned long nbytes;
    unsigned char *data = 0;
    long *extents;

    *top = *bottom = *left = *right = 0L;

    prop = XInternAtom(dpy, "_NET_FRAME_EXTENTS", True);
    if (prop == None) {
        /*
         * FIXME!
         */
#ifdef MACOS
        /*
         * Default window manager doesn't support _NET_FRAME_EXTENTS.
         * Without this position tweak, the persistent inventory window
         * creeps downward by approximately the height of its title bar
         * and also a smaller amount to the left every time it gets
         * updated.  Caveat:  amount determined by trial and error and
         * could change depending upon monitor resolution....
         *
         * This hack could be improved by implementing the anti-creep
         * value as an X resource so that player can adjust it.
         */
        *top = 22L;
#endif
        return;
    }

    while (XGetWindowProperty(dpy, win, prop,
                              0, 4, False, AnyPropertyType,
                              &retprop, &retfmt,
                              &nitems, &nbytes, &data) != Success
           || nitems != 4 || nbytes != 0) {
        XNextEvent(dpy, &event);
    }

    extents = (long *) data;
    if (extents) {
        *left = extents[0];
        *right = extents[1];
        *top = extents[2];
        *bottom = extents[3];
    }
}

void
get_widget_window_geometry(
    Widget w,
    int *x, int *y,
    int *width, int *height)
{
    long top, bottom, left, right;
    Arg args[5];

    XtSetArg(args[0], nhStr(XtNx), x);
    XtSetArg(args[1], nhStr(XtNy), y);
    XtSetArg(args[2], nhStr(XtNwidth), width);
    XtSetArg(args[3], nhStr(XtNheight), height);
    XtGetValues(w, args, 4);
    get_window_frame_extents(w, &top, &bottom, &left, &right);
    *x -= left;
    *y -= top;
}

/* Change the full font name string so the weight is "bold" */
char *
fontname_boldify(const char *fontname)
{
    static char buf[BUFSZ];
    char *bufp = buf;
    int idx = 0;

    while (*fontname) {
        if (*fontname == '-')
            idx++;
        *bufp = *fontname;
        if (idx == 3) {
            strcat(buf, "bold");
            bufp += 5;
            do {
                fontname++;
            } while (*fontname && *fontname != '-');
        } else {
            bufp++;
            fontname++;
        }
    }
    *bufp = '\0';
    return buf;
}

void
load_boldfont(struct xwindow *wp, Widget w)
{
    Arg args[1];
    XFontStruct *fs;
    unsigned long ret;
    char *fontname;
    Display *dpy;

    if (wp->boldfs)
        return;

    XtSetArg(args[0], nhStr(XtNfont), &fs);
    XtGetValues(w, args, 1);

    if (!XGetFontProperty(fs, XA_FONT, &ret))
        return;

    wp->boldfs_dpy = dpy = XtDisplay(w);
    fontname = fontname_boldify(XGetAtomName(dpy, (Atom)ret));
    wp->boldfs = XLoadQueryFont(dpy, fontname);
}

/* ARGSUSED */
static void
nhFreePixel(
    XtAppContext app,
    XrmValuePtr toVal,
    XtPointer closure,
    XrmValuePtr args,
    Cardinal *num_args)
{
    Screen *screen;
    Colormap colormap;

    if (*num_args != 2) {
        XtAppWarningMsg(app, "wrongParameters", "freePixel", "XtToolkitError",
                     "Freeing a pixel requires screen and colormap arguments",
                        (String *) 0, (Cardinal *) 0);
        return;
    }

    screen = *((Screen **) args[0].addr);
    colormap = *((Colormap *) args[1].addr);

    if (closure) {
        XFreeColors(DisplayOfScreen(screen), colormap,
                    (unsigned long *) toVal->addr, 1, (unsigned long) 0);
    }
}

/* [ALI] Utility function to ask Xaw for font height, since the previous
 * assumption of ascent + descent is not always valid.
 */
Dimension
nhFontHeight(Widget w)
{
#ifdef _XawTextSink_h
    Widget sink;
    XawTextPosition pos = 0;
    int resWidth, resHeight;
    Arg args[1];

    XtSetArg(args[0], XtNtextSink, &sink);
    XtGetValues(w, args, 1);

    XawTextSinkFindPosition(sink, pos, 0, 0, 0, &pos, &resWidth, &resHeight);
    return resHeight;
#else
    XFontStruct *fs;
    Arg args[1];

    XtSetArg(args[0], XtNfont, &fs);
    XtGetValues(w, args, 1);

    /* Assume font height is ascent + descent. */
    return = fs->ascent + fs->descent;
#endif
}

static String *default_resource_data = 0, /* NULL-terminated arrays */
              *def_rsrc_macr = 0, /* macro names */
              *def_rsrc_valu = 0; /* macro values */

/* caller found "#define"; parse into macro name and its expansion value */
static boolean
new_resource_macro(
    String inbuf, /* points past '#define' rather than to start of buffer */
    unsigned numdefs) /* array slot to fill */
{
    String p, q;

    /* we expect inbuf to be terminated by newline; get rid of it */
    q = eos(inbuf);
    if (q > inbuf && q[-1] == '\n')
        q[-1] = '\0';

    /* figure out macro's name */
    for (p = inbuf; *p == ' ' || *p == '\t'; ++p)
        continue; /* skip whitespace */
    for (q = p; *q && *q != ' ' && *q != '\t'; ++q)
        continue; /* token consists of non-whitespace */
    Strcat(q, " "); /* guarantee something beyond '#define FOO' */
    *q++ = '\0'; /* p..(q-1) contains macro name */
    if (!*p) /* invalid definition: '#define' followed by nothing */
        return FALSE;
    def_rsrc_macr[numdefs] = dupstr(p);

    /* figure out macro's value; empty value is supported but not expected */
    while (*q == ' ' || *q == '\t')
        ++q; /* skip whitespace between name and value */
    for (p = eos(q); --p > q && (*p == ' ' || *p == '\t'); )
        continue; /* discard trailing whitespace */
    *++p = '\0'; /* q..p contains macro value */
    def_rsrc_valu[numdefs] = dupstr(q);
    return TRUE;
}

/* read the template NetHack.ad into default_resource_data[] to supply
   fallback resources to XtAppInitialize() */
static void
load_default_resources(void)
{
    FILE *fp;
    String inbuf;
    unsigned insiz, linelen, longlen, numlines, numdefs, midx;
    boolean comment, isdef;

    /*
     * Running nethack via the shell script adds $HACKDIR to the path used
     * by X to find resources, but running it directly doesn't.  So, if we
     * can find the template file for NetHack.ad in the current directory,
     * load its contents into memory so that the application startup call
     * in X11_init_nhwindows() can use them as fallback resources.
     *
     * No attempt to support the 'include' directive has been made, nor
     * backslash+newline continuation lines.  Macro expansion (at most
     * one substitution per line) is supported.  '#define' to introduce
     * a macro must be at start of line (no whitespace before or after
     * the '#' character).
     */
    fp = fopen("./NetHack.ad", "r");
    if (!fp)
        return;

    /* measure the file without retaining its contents */
    insiz = BUFSIZ; /* stdio BUFSIZ, not nethack BUFSZ */
    inbuf = (String) alloc(insiz);
    linelen = longlen = 0;
    numlines = numdefs = 0;
    comment = isdef = FALSE; /* lint suppression */
    while (fgets(inbuf, insiz, fp)) {
        if (!linelen) {
            /* !linelen: inbuf has start of record; treat empty as comment */
            comment = (*inbuf == '!' || *inbuf == '\n');
            isdef = !strncmp(inbuf, "#define", 7);
            ++numdefs;
        }
        linelen += strlen(inbuf);
        if (!strchr(inbuf, '\n'))
            continue;
        if (linelen > longlen)
            longlen = linelen;
        linelen = 0;
        if (!comment && !isdef)
            ++numlines;
    }
    insiz = longlen + 1;
    if (numdefs) { /* don't alloc if 0; no need for any terminator */
        def_rsrc_macr = (String *) alloc(numdefs * sizeof (String));
        def_rsrc_valu = (String *) alloc(numdefs * sizeof (String));
        insiz += BUFSIZ; /* include room for macro expansion within buffer */
    }
    if (insiz > BUFSIZ) {
        free((genericptr_t) inbuf);
        inbuf = (String) alloc(insiz);
    }
    ++numlines; /* room for terminator */
    default_resource_data = (String *) alloc(numlines * sizeof (String));

    /* now re-read the file, storing its contents into the allocated array
       after performing macro substitutions */
    (void) rewind(fp);
    numlines = numdefs = 0;
    while (fgets(inbuf, insiz, fp)) {
        if (!strncmp(inbuf, "#define", 7)) {
            if (new_resource_macro(&inbuf[7], numdefs))
                ++numdefs;
        } else if (*inbuf != '!' && *inbuf != '\n') {
            if (numdefs) {
                /*
                 * Macro expansion:  we assume at most one substitution
                 * per line.  That's all that our sample NetHack.ad uses.
                 *
                 * If we ever need more, this will have to become a lot
                 * more sophisticated.  It will need to find the first
                 * instance within inbuf[] rather than first macro which
                 * appears, and to avoid finding names within substituted
                 * expansion values.
                 *
                 * Any substitution which would exceed the buffer size is
                 * skipped.  A sophisticated implementation would need to
                 * be prepared to allocate a bigger buffer when needed.
                 */
                linelen = strlen(inbuf);
                for (midx = 0; midx < numdefs; ++midx) {
                    if ((linelen + strlen(def_rsrc_valu[midx])
                         < insiz - strlen(def_rsrc_macr[midx]))
                        && strNsubst(inbuf, def_rsrc_macr[midx],
                                     def_rsrc_valu[midx], 1))
                        break;
                }
            }
            default_resource_data[numlines++] = dupstr(inbuf);
        }
    }
    default_resource_data[numlines] = (String) 0;
    (void) fclose(fp);
    free((genericptr_t) inbuf);
    if (def_rsrc_macr) { /* implies def_rsrc_valu is non-Null too */
        for (midx = 0; midx < numdefs; ++midx) {
            free((genericptr_t) def_rsrc_macr[midx]);
            free((genericptr_t) def_rsrc_valu[midx]);
        }
        free((genericptr_t) def_rsrc_macr), def_rsrc_macr = 0;
        free((genericptr_t) def_rsrc_valu), def_rsrc_valu = 0;
    }
}

static void
release_default_resources(void)
{
    if (default_resource_data) {
        unsigned idx;

        for (idx = 0; default_resource_data[idx]; idx++)
            free((genericptr_t) default_resource_data[idx]);
        free((genericptr_t) default_resource_data), default_resource_data = 0;
    }
    /* def_rsrc_macr[] and def_rsrc_valu[] have already been released */
}

/* Global Functions ======================================================= */
void
X11_raw_print(const char *str)
{
    (void) puts(str);
}

void
X11_raw_print_bold(const char *str)
{
    (void) puts(str);
}

void
X11_curs(winid window, int x, int y)
{
    check_winid(window);

    if (x < 0 || x >= COLNO) {
        impossible("curs:  bad x value [%d]", x);
        x = 0;
    }
    if (y < 0 || y >= ROWNO) {
        impossible("curs:  bad y value [%d]", y);
        y = 0;
    }

    window_list[window].cursx = x;
    window_list[window].cursy = y;
}

void
X11_putstr(winid window, int attr, const char *str)
{
    winid new_win;
    struct xwindow *wp;

    check_winid(window);
    wp = &window_list[window];

    switch (wp->type) {
    case NHW_MESSAGE:
        (void) strncpy(gt.toplines, str, TBUFSZ); /* for Norep(). */
        gt.toplines[TBUFSZ - 1] = 0;
        append_message(wp, str);
        break;
#ifndef STATUS_HILITES
    case NHW_STATUS:
        adjust_status(wp, str);
        break;
#endif
    case NHW_MAP:
        impossible("putstr: called on map window \"%s\"", str);
        break;
    case NHW_MENU:
        if (wp->menu_information->is_menu) {
            impossible("putstr:  called on a menu window, \"%s\" discarded",
                       str);
            break;
        }
        /*
         * Change this menu window into a text window by creating a
         * new text window, then copying it to this winid.
         */
        new_win = X11_create_nhwindow(NHW_TEXT);
        X11_destroy_nhwindow(window);
        *wp = window_list[new_win];
        window_list[new_win].type = NHW_NONE; /* allow re-use */
        FALLTHROUGH;
        /*FALLTHRU*/
    case NHW_TEXT:
        add_to_text_window(wp, attr, str);
        break;
    default:
        impossible("putstr: unknown window type [%d] \"%s\"", wp->type, str);
    }
}

/* We do event processing as a callback, so this is a null routine. */
void
X11_get_nh_event(void)
{
    return;
}

int
X11_nhgetch(void)
{
    return input_event(EXIT_ON_KEY_PRESS);
}

int
X11_nh_poskey(coordxy *x, coordxy *y, int *mod)
{
    int val = input_event(EXIT_ON_KEY_OR_BUTTON_PRESS);

    if (val == 0) { /* user clicked on a map window */
        *x = click_x;
        *y = click_y;
        *mod = click_button;
    }
    return val;
}

winid
X11_create_nhwindow(int type)
{
    winid window;
    struct xwindow *wp;

    if (!x_inited)
        panic("create_nhwindow:  windows not initialized");

#ifdef X11_HANGUP_SIGNAL
    /* set up our own signal handlers on the first call.  Can't do this in
     * X11_init_nhwindows because unixmain sets its handler after calling
     * all the init routines. */
    if (X11_sig_id == 0) {
        X11_sig_id = XtAppAddSignal(app_context, X11_sig_cb, (XtPointer) 0);
#ifdef SA_RESTART
        {
            struct sigaction sact;

            (void) memset((char *) &sact, 0, sizeof(struct sigaction));
            sact.sa_handler = (SIG_RET_TYPE) X11_sig;
            (void) sigaction(SIGHUP, &sact, (struct sigaction *) 0);
#ifdef SIGXCPU
            (void) sigaction(SIGXCPU, &sact, (struct sigaction *) 0);
#endif
        }
#else
        (void) signal(SIGHUP, (SIG_RET_TYPE) X11_sig);
#ifdef SIGXCPU
        (void) signal(SIGXCPU, (SIG_RET_TYPE) X11_sig);
#endif
#endif
    }
#endif

    /*
     * We have already created the standard message, map, and status
     * windows in the window init routine.  The first window of that
     * type to be created becomes the standard.
     *
     * A better way to do this would be to say that init_nhwindows()
     * has already defined these three windows.
     */
    if (type == NHW_MAP && map_win != WIN_ERR) {
        window = map_win;
        map_win = WIN_ERR;
        return window;
    }
    if (type == NHW_MESSAGE && message_win != WIN_ERR) {
        window = message_win;
        message_win = WIN_ERR;
        return window;
    }
    if (type == NHW_STATUS && status_win != WIN_ERR) {
        window = status_win;
        status_win = WIN_ERR;
        return window;
    }

    window = find_free_window();
    wp = &window_list[window];

    /* The create routines will set type, popup, w, and Win_info. */
    wp->prevx = wp->prevy = wp->cursx = wp->cursy = wp->pixel_width =
        wp->pixel_height = 0;
    wp->keep_window = FALSE;
    wp->nh_colors_inited = FALSE;
    wp->boldfs = (XFontStruct *) 0;
    wp->boldfs_dpy = (Display *) 0;
    wp->title = (char *) 0;

    switch (type) {
    case NHW_MAP:
        create_map_window(wp, TRUE, (Widget) 0);
        break;
    case NHW_MESSAGE:
        create_message_window(wp, TRUE, (Widget) 0);
        break;
    case NHW_STATUS:
        create_status_window(wp, TRUE, (Widget) 0);
        break;
    case NHW_MENU:
        create_menu_window(wp);
        break;
    case NHW_TEXT:
        create_text_window(wp);
        break;
    default:
        panic("create_nhwindow: unknown type [%d]", type);
        break;
    }
    return window;
}

void
X11_clear_nhwindow(winid window)
{
    struct xwindow *wp;

    check_winid(window);
    wp = &window_list[window];

    switch (wp->type) {
    case NHW_MAP:
        clear_map_window(wp);
        break;
    case NHW_TEXT:
        clear_text_window(wp);
        break;
    case NHW_STATUS:
    case NHW_MENU:
    case NHW_MESSAGE:
        /* do nothing for these window types */
        break;
    default:
        panic("clear_nhwindow: unknown window type [%d]", wp->type);
        break;
    }
}

void
X11_display_nhwindow(winid window, boolean blocking)
{
    struct xwindow *wp;

    check_winid(window);
    wp = &window_list[window];

    switch (wp->type) {
    case NHW_MAP:
        if (wp->popup)
            nh_XtPopup(wp->popup, (int) XtGrabNone, wp->w);
        display_map_window(wp); /* flush map */

        /*
         * We need to flush the message window here due to the way the tty
         * port is set up.  To flush a window, you need to call this
         * routine.  However, the tty port _pauses_ with a --more-- if we
         * do a display_nhwindow(WIN_MESSAGE, FALSE).  Thus, we can't call
         * display_nhwindow(WIN_MESSAGE,FALSE) in parse() because then we
         * get a --more-- after every line.
         *
         * Perhaps the window document should mention that when the map
         * is flushed, everything on the three main windows should be
         * flushed.  Note: we don't need to flush the status window
         * because we don't buffer changes.
         */
        if (WIN_MESSAGE != WIN_ERR)
            display_message_window(&window_list[WIN_MESSAGE]);
        if (blocking)
            (void) x_event(EXIT_ON_KEY_OR_BUTTON_PRESS);
        break;
    case NHW_MESSAGE:
        if (wp->popup)
            nh_XtPopup(wp->popup, (int) XtGrabNone, wp->w);
        display_message_window(wp); /* flush messages */
        break;
    case NHW_STATUS:
        if (wp->popup)
            nh_XtPopup(wp->popup, (int) XtGrabNone, wp->w);
        break; /* no flushing necessary */
    case NHW_MENU: {
        int n;
        menu_item *selected;

        /* pop up menu */
        n = X11_select_menu(window, PICK_NONE, &selected);
        if (n) {
            impossible("perminvent: %d selected??", n);
            free((genericptr_t) selected);
        }
        break;
    }
    case NHW_TEXT:
        display_text_window(wp, blocking); /* pop up text window */
        break;
    default:
        panic("display_nhwindow: unknown window type [%d]", wp->type);
        break;
    }
}

void
X11_destroy_nhwindow(winid window)
{
    struct xwindow *wp;

    check_winid(window);
    wp = &window_list[window];
    /*
     * "Zap" known windows, but don't destroy them.  We need to keep the
     * toplevel widget popped up so that later windows (e.g. tombstone)
     * are visible on DECWindow systems.  This is due to the virtual
     * roots that the DECWindow wm creates.
     */
    if (window == WIN_MESSAGE) {
        wp->keep_window = TRUE;
        WIN_MESSAGE = WIN_ERR;
        iflags.window_inited = 0;
    } else if (window == WIN_MAP) {
        wp->keep_window = TRUE;
        WIN_MAP = WIN_ERR;
    } else if (window == WIN_STATUS) {
        wp->keep_window = TRUE;
        WIN_STATUS = WIN_ERR;
    } else if (window == WIN_INVEN) {
        /* don't need to keep this one */
        WIN_INVEN = WIN_ERR;
    }

    if (wp->boldfs) {
        XFreeFont(wp->boldfs_dpy, wp->boldfs);
        wp->boldfs = (XFontStruct *) 0;
        wp->boldfs_dpy = (Display *) 0;
    }

    if (wp->title) {
        free(wp->title);
        wp->title = (char *) 0;
    }

    switch (wp->type) {
    case NHW_MAP:
        destroy_map_window(wp);
        break;
    case NHW_MENU:
        destroy_menu_window(wp);
        break;
    case NHW_TEXT:
        destroy_text_window(wp);
        break;
    case NHW_STATUS:
        destroy_status_window(wp);
        break;
    case NHW_MESSAGE:
        destroy_message_window(wp);
        break;
    default:
        panic("destroy_nhwindow: unknown window type [%d]", wp->type);
        break;
    }
}

/* display persistent inventory in its own window */
void
X11_update_inventory(int arg)
{
    struct xwindow *wp = 0;

    if (!x_inited)
        return;

    if (iflags.perm_invent) {
        /* skip any calls to update_inventory() before in_moveloop starts */
        if (program_state.in_moveloop || program_state.gameover) {
            updated_inventory = 1; /* hack to avoid mapping&raising window */
            if (!arg) {
                repopulate_perminvent();
            } else {
                x11_scroll_perminv(arg);
            }
            updated_inventory = 0;
        }
    } else if ((wp = &window_list[WIN_INVEN]) != 0
               && wp->type == NHW_MENU && wp->menu_information->is_up) {
        /* persistent inventory is up but perm_invent is off, take it down */
        x11_no_perminv(wp);
    }
    return;
}

win_request_info *
X11_ctrl_nhwindow(
    winid window UNUSED,
    int request UNUSED,
    win_request_info *wri UNUSED)
{
    if (!wri)
        return (win_request_info *) 0;

    switch(request) {
    case set_mode:
    case request_settings:
        break;
    case set_menu_promptstyle:
        X11_menu_promptstyle = wri->fromcore.menu_promptstyle;
        break;
    default:
        break;
    }
    return wri;
}

/* The current implementation has all of the saved lines on the screen. */
int
X11_doprev_message(void)
{
    return 0;
}

/* issue a beep to alert the user about something */
void
X11_nhbell(void)
{
    if (!flags.silent) {
        /* We can't use XBell until toplevel has been initialized. */
        if (x_inited) {
            XBell(XtDisplay(toplevel), 0);
#if 0
        } else {
            /* raw_print() uses puts() so appends a newline */
            X11_raw_print("\a"); /* '\a' == '\007' (^G), ascii BEL */
#endif
        }
    }
}

void
X11_mark_synch(void)
{
    if (x_inited) {
        /*
         * The window document is unclear about the status of text
         * that has been pline()d but not displayed w/display_nhwindow().
         * Both the main and tty code assume that a pline() followed
         * by mark_synch() results in the text being seen, even if
         * display_nhwindow() wasn't called.  Duplicate this behavior.
         */
        if (WIN_MESSAGE != WIN_ERR)
            display_message_window(&window_list[WIN_MESSAGE]);
        XSync(XtDisplay(toplevel), False);
    }
}

void
X11_wait_synch(void)
{
    if (x_inited) {
        X11_mark_synch();
        XFlush(XtDisplay(toplevel));
    }
}

/* Both resume_ and suspend_ are called from ioctl.c and unixunix.c. */
void
X11_resume_nhwindows(void)
{
    return;
}
/* ARGSUSED */
void
X11_suspend_nhwindows(const char *str)
{
    nhUse(str);

    return;
}

/* Under X, we don't need to initialize the number pad. */
/* ARGSUSED */
void
X11_number_pad(int state) /* called from options.c */
{
    nhUse(state);

    return;
}

#ifdef GRAPHIC_TOMBSTONE
void
X11_outrip(winid window, int how, time_t when)
{
    struct xwindow *wp;
    FILE *rip_fp = 0;

    check_winid(window);
    wp = &window_list[window];

    /* make sure the graphical tombstone is available; it's not easy to
       revert to the ASCII-art text tombstone once we're past this point */
    if (appResources.tombstone && *appResources.tombstone)
        rip_fp = fopen(appResources.tombstone, "r"); /* "rip.xpm" */
    if (!rip_fp) {
        genl_outrip(window, how, when);
        return;
    }
    (void) fclose(rip_fp);

    if (wp->type == NHW_TEXT) {
        wp->text_information->is_rip = TRUE;
    } else {
        panic("ripout on non-text window (window type [%d])", wp->type);
    }

    calculate_rip_text(how, when);
}
#endif

/* init and exit nhwindows ------------------------------------------------ */

XtAppContext app_context;     /* context of application */
Widget toplevel = (Widget) 0; /* toplevel widget */
Atom wm_delete_window;        /* pop down windows */

static XtActionsRec actions[] = {
    { nhStr("dismiss_text"), dismiss_text }, /* text widget button action */
    { nhStr("delete_text"), delete_text },   /* text widget delete action */
    { nhStr("key_dismiss_text"), key_dismiss_text }, /* text key action   */
#ifdef GRAPHIC_TOMBSTONE
    { nhStr("rip_dismiss_text"), rip_dismiss_text }, /* rip in text widget */
#endif
    { nhStr("menu_key"), menu_key },             /* menu accelerators */
    { nhStr("yn_key"), yn_key },                 /* yn accelerators */
    { nhStr("yn_delete"), yn_delete },           /* yn delete-window */
    { nhStr("askname_delete"), askname_delete }, /* askname delete-window */
    { nhStr("getline_delete"), getline_delete }, /* getline delete-window */
    { nhStr("menu_delete"), menu_delete },       /* menu delete-window */
    { nhStr("ec_key"), ec_key },                 /* extended commands */
    { nhStr("ec_delete"), ec_delete },           /* ext-com menu delete */
    { nhStr("ps_key"), ps_key },                 /* player selection */
    { nhStr("plsel_quit"), plsel_quit },   /* player selection dialog */
    { nhStr("plsel_play"), plsel_play },   /* player selection dialog */
    { nhStr("plsel_rnd"), plsel_randomize }, /* player selection dialog */
    { nhStr("race_key"), race_key },             /* race selection */
    { nhStr("gend_key"), gend_key },             /* gender selection */
    { nhStr("algn_key"), algn_key },             /* alignment selection */
    { nhStr("X11_hangup"), X11_hangup },         /* delete of top-level */
    { nhStr("input"), map_input },               /* key input */
    { nhStr("scroll"), nh_keyscroll },           /* scrolling by keys */
};

static XtResource resources[] = {
    { nhStr("slow"), nhStr("Slow"), XtRBoolean, sizeof(Boolean),
      XtOffset(AppResources *, slow), XtRString, nhStr("True") },
    { nhStr("fancy_status"), nhStr("Fancy_status"),
      XtRBoolean, sizeof(Boolean),
      XtOffset(AppResources *, fancy_status), XtRString, nhStr("True") },
    { nhStr("autofocus"), nhStr("AutoFocus"), XtRBoolean, sizeof(Boolean),
      XtOffset(AppResources *, autofocus), XtRString, nhStr("False") },
    { nhStr("message_line"), nhStr("Message_line"), XtRBoolean,
      sizeof(Boolean), XtOffset(AppResources *, message_line), XtRString,
      nhStr("False") },
    { nhStr("highlight_prompt"), nhStr("Highlight_prompt"), XtRBoolean,
      sizeof(Boolean), XtOffset(AppResources *, highlight_prompt), XtRString,
      nhStr("True") },
    { nhStr("double_tile_size"), nhStr("Double_tile_size"), XtRBoolean,
      sizeof(Boolean), XtOffset(AppResources *, double_tile_size), XtRString,
      nhStr("False") },
    { nhStr("tile_file"), nhStr("Tile_file"), XtRString, sizeof(String),
      XtOffset(AppResources *, tile_file), XtRString, nhStr("x11tiles") },
    { nhStr("icon"), nhStr("Icon"), XtRString, sizeof(String),
      XtOffset(AppResources *, icon), XtRString, nhStr("nh72") },
    { nhStr("message_lines"), nhStr("Message_lines"), XtRInt, sizeof(int),
      XtOffset(AppResources *, message_lines), XtRString, nhStr("12") },
    { nhStr("extcmd_height_delta"), nhStr("Extcmd_height_delta"),
      XtRInt, sizeof (int),
      XtOffset(AppResources *, extcmd_height_delta), XtRString, nhStr("0") },
    { nhStr("pet_mark_bitmap"), nhStr("Pet_mark_bitmap"), XtRString,
      sizeof(String), XtOffset(AppResources *, pet_mark_bitmap), XtRString,
      nhStr("pet_mark.xbm") },
    { nhStr("pet_mark_color"), nhStr("Pet_mark_color"), XtRPixel,
      sizeof(XtRPixel), XtOffset(AppResources *, pet_mark_color), XtRString,
      nhStr("Red") },
    { nhStr("pilemark_bitmap"), nhStr("Pilemark_bitmap"), XtRString,
      sizeof(String), XtOffset(AppResources *, pilemark_bitmap), XtRString,
      nhStr("pilemark.xbm") },
    { nhStr("pilemark_color"), nhStr("Pilemark_color"), XtRPixel,
      sizeof(XtRPixel), XtOffset(AppResources *, pilemark_color), XtRString,
      nhStr("Green") },
#ifdef GRAPHIC_TOMBSTONE
    { nhStr("tombstone"), nhStr("Tombstone"), XtRString, sizeof(String),
      XtOffset(AppResources *, tombstone), XtRString, nhStr("rip.xpm") },
    { nhStr("tombtext_x"), nhStr("Tombtext_x"), XtRInt, sizeof(int),
      XtOffset(AppResources *, tombtext_x), XtRString, nhStr("155") },
    { nhStr("tombtext_y"), nhStr("Tombtext_y"), XtRInt, sizeof(int),
      XtOffset(AppResources *, tombtext_y), XtRString, nhStr("78") },
    { nhStr("tombtext_dx"), nhStr("Tombtext_dx"), XtRInt, sizeof(int),
      XtOffset(AppResources *, tombtext_dx), XtRString, nhStr("0") },
    { nhStr("tombtext_dy"), nhStr("Tombtext_dy"), XtRInt, sizeof(int),
      XtOffset(AppResources *, tombtext_dy), XtRString, nhStr("13") },
#endif
};

static int
panic_on_error(Display *display, XErrorEvent *error)
{
    char buf[BUFSZ];
    XGetErrorText(display, error->error_code, buf, BUFSZ);
    fprintf(stderr,
            "X Error: code %i (%s), request %i, minor %i, serial %lu\n",
            error->error_code, buf,
            error->request_code, error->minor_code, error->serial);
    panic("X Error");
    return 0;
}

static void
X11_error_handler(String str)
{
    nhUse(str);
#if defined(HANGUPHANDLING)
    hangup(1);
#ifdef SAFERHANGUP /* keeps going after hangup() for '#if SAFERHANGUP' */
    end_of_input();
#endif
#endif
    /*NOTREACHED*/
    nh_terminate(EXIT_FAILURE);
}

static int
X11_io_error_handler(Display *display)
{
    nhUse(display);
#if defined(HANGUPHANDLING)
    hangup(1);
#ifdef SAFERHANGUP /* keeps going after hangup() for '#if SAFERHANGUP' */
    end_of_input();
#endif
#endif
    /*NOREACHED*/ /* but not declared NORETURN */
    return 0;
}

void
X11_init_nhwindows(int *argcp, char **argv)
{
    int i;
    Cardinal num_args;
    Arg args[4];
    uid_t savuid;

    /* Init windows to nothing. */
    for (i = 0; i < MAX_WINDOWS; i++)
        window_list[i].type = NHW_NONE;

    /* force high scores display to be shown in a window, and don't allow
       that to be toggled off via 'O' (note: 'nethack -s' won't reach here;
       its output goes to stdout and could be redirected into a file) */
    iflags.toptenwin = TRUE;
    set_option_mod_status("toptenwin", set_in_config);

    /* add another option that can be set */
    set_wc_option_mod_status(WC_TILED_MAP, set_in_game);
    set_option_mod_status("mouse_support", set_in_game);

    load_default_resources(); /* create default_resource_data[] */

    /*
     * setuid hack: make sure that if nethack is setuid, to use real uid
     * when opening X11 connections, in case the user is using xauth, since
     * the "games" or whatever user probably doesn't have permission to open
     * a window on the user's display.  This code is harmless if the binary
     * is not installed setuid.
     */
    savuid = geteuid();
    (void) seteuid(getuid());

    XSetIOErrorHandler((XIOErrorHandler) X11_io_error_handler);

    num_args = 0;
    XtSetArg(args[num_args], XtNallowShellResize, True); num_args++;
    XtSetArg(args[num_args], XtNtitle, "NetHack"); num_args++;

    toplevel = XtAppInitialize(&app_context, "NetHack",     /* application  */
                               (XrmOptionDescList) 0, 0,    /* options list */
                               argcp, (String *) argv,      /* command line */
                               default_resource_data, /* fallback resources */
                               (ArgList) args, num_args);
    XtOverrideTranslations(toplevel,
              XtParseTranslationTable("<Message>WM_PROTOCOLS: X11_hangup()"));

    /* We don't need to realize the top level widget. */

    old_error_handler = XSetErrorHandler(panic_on_error);

    /* add new color converter to deal with overused colormaps */
    XtSetTypeConverter(XtRString, XtRPixel, nhCvtStringToPixel,
                       (XtConvertArgList) nhcolorConvertArgs,
                       XtNumber(nhcolorConvertArgs), XtCacheByDisplay,
                       nhFreePixel);

    /* Register the actions mentioned in "actions". */
    XtAppAddActions(app_context, actions, XtNumber(actions));

    /* Get application-wide resources */
    XtGetApplicationResources(toplevel, (XtPointer) &appResources, resources,
                              XtNumber(resources), (ArgList) 0, ZERO);

    /* Initialize other things. */
    init_standard_windows();

    /* Give the window manager an icon to use;  toplevel must be realized. */
    if (appResources.icon && *appResources.icon) {
        struct icon_info *ip;

        for (ip = icon_data; ip->name; ip++)
            if (!strcmp(appResources.icon, ip->name)) {
                icon_pixmap = XCreateBitmapFromData(
                    XtDisplay(toplevel), XtWindow(toplevel),
                    (genericptr_t) ip->bits, ip->width, ip->height);
                if (icon_pixmap != None) {
                    XWMHints hints;

                    (void) memset((genericptr_t) &hints, 0, sizeof hints);
                    hints.flags = IconPixmapHint;
                    hints.icon_pixmap = icon_pixmap;
                    XSetWMHints(XtDisplay(toplevel), XtWindow(toplevel),
                                &hints);
                }
                break;
            }
    }

    /* end of setuid hack: reset uid back to the "games" uid */
    (void) seteuid(savuid);

    x_inited = TRUE; /* X is now initialized */
    plsel_ask_name = FALSE;

    release_default_resources();

    /* Display the startup banner in the message window. */
    for (i = 1; i <= 4 + 2; ++i) /* (values beyond 4 yield blank lines) */
        X11_putstr(WIN_MESSAGE, 0, copyright_banner_line(i));
}

/*
 * All done.
 */
/* ARGSUSED */
void
X11_exit_nhwindows(const char *dummy)
{
    extern Pixmap tile_pixmap; /* from winmap.c */

    nhUse(dummy);

    /* explicitly free the icon and tile pixmaps */
    if (icon_pixmap != None) {
        XFreePixmap(XtDisplay(toplevel), icon_pixmap);
        icon_pixmap = None;
    }
    if (tile_pixmap != None) {
        XFreePixmap(XtDisplay(toplevel), tile_pixmap);
        tile_pixmap = None;
    }
    if (WIN_INVEN != WIN_ERR)
        X11_destroy_nhwindow(WIN_INVEN);
    if (WIN_STATUS != WIN_ERR)
        X11_destroy_nhwindow(WIN_STATUS);
    if (WIN_MAP != WIN_ERR)
        X11_destroy_nhwindow(WIN_MAP);
    if (WIN_MESSAGE != WIN_ERR)
        X11_destroy_nhwindow(WIN_MESSAGE);

    release_getline_widgets();
    release_yn_widgets();

    x_inited = FALSE;
}

#ifdef X11_HANGUP_SIGNAL
static void
X11_sig(int sig) /* Unix signal handler */
{
    XtNoticeSignal(X11_sig_id);
    hangup(sig);
}

static void
X11_sig_cb(XtPointer not_used, XtSignalId *id)
{
    XEvent event;
    XClientMessageEvent *mesg;

    nhUse(not_used);
    nhUse(id);

    /* Set up a fake message to the event handler. */
    mesg = (XClientMessageEvent *) &event;
    mesg->type = ClientMessage;
    mesg->message_type = XA_STRING;
    mesg->format = 8;

    XSendEvent(XtDisplay(window_list[WIN_MAP].w),
               XtWindow(window_list[WIN_MAP].w), False, NoEventMask,
               (XEvent *) mesg);
}
#endif

/* X11_delay_output ------------------------------------------------------- */

/*
 * Timeout callback for X11_delay_output().  Send a fake message to the map
 * window.
 */
/* ARGSUSED */
static void
d_timeout(XtPointer client_data, XtIntervalId *id)
{
    XEvent event;
    XClientMessageEvent *mesg;

    nhUse(client_data);
    nhUse(id);

    /* Set up a fake message to the event handler. */
    mesg = (XClientMessageEvent *) &event;
    mesg->type = ClientMessage;
    mesg->message_type = XA_STRING;
    mesg->format = 8;
    XSendEvent(XtDisplay(window_list[WIN_MAP].w),
               XtWindow(window_list[WIN_MAP].w), False, NoEventMask,
               (XEvent *) mesg);
}

/*
 * Delay for 50ms.  This is not implemented asynch.  Maybe later.
 * Start the timeout, then wait in the event loop.  The timeout
 * function will send an event to the map window which will be waiting
 * for a sent event.
 */
void
X11_delay_output(void)
{
    if (!x_inited)
        return;

    (void) XtAppAddTimeOut(app_context, 30L, d_timeout, (XtPointer) 0);

    /* The timeout function will enable the event loop exit. */
    (void) x_event(EXIT_ON_SENT_EVENT);
}

/* X11_hangup ------------------------------------------------------------- */
/* ARGSUSED */
static void
X11_hangup(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    nhUse(w);
    nhUse(event);
    nhUse(params);
    nhUse(num_params);

#if defined(HANGUPHANDLING)
    hangup(1); /* 1 is commonly SIGHUP, but ignored anyway */
#endif
    exit_x_event = TRUE;
}

/* X11_bail --------------------------------------------------------------- */
/* clean up and quit */
static void
X11_bail(const char *mesg)
{
    program_state.something_worth_saving = 0;
    clearlocks();
    X11_exit_nhwindows(mesg);
    nh_terminate(EXIT_SUCCESS);
    /*NOTREACHED*/
}

/* askname ---------------------------------------------------------------- */
/* ARGSUSED */
static void
askname_delete(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    nhUse(event);
    nhUse(params);
    nhUse(num_params);

    nh_XtPopdown(w);
    (void) strcpy(svp.plname, "Mumbles"); /* give them a name... ;-) */
    exit_x_event = TRUE;
}

/* Callback for askname dialog widget. */
/* ARGSUSED */
static void
askname_done(Widget w, XtPointer client_data, XtPointer call_data)
{
    unsigned len;
    char *s;
    Widget dialog = (Widget) client_data;

    nhUse(w);
    nhUse(call_data);

    s = (char *) GetDialogResponse(dialog);

    len = strlen(s);
    if (len == 0) {
        X11_nhbell();
        return;
    }

    /* Truncate name if necessary */
    if (len >= sizeof svp.plname - 1)
        len = sizeof svp.plname - 1;

    (void) strncpy(svp.plname, s, len);
    svp.plname[len] = '\0';
    XtFree(s);

    nh_XtPopdown(XtParent(dialog));
    exit_x_event = TRUE;
}

/* ask player for character's name to replace generic name "player" (or other
   values; see config.h) after 'nethack -u player' or OPTIONS=name:player */
void
X11_askname(void)
{
    Widget popup, dialog;
    Arg args[1];

#ifdef SELECTSAVED
    if (iflags.wc2_selectsaved && !iflags.renameinprogress)
        switch (restore_menu(WIN_MAP)) {
        case -1: /* quit */
            X11_bail("Until next time then...");
            /*NOTREACHED*/
        case 0: /* no game chosen; start new game */
            break;
        case 1: /* save game selected, plname[] has been set */
            return;
        }
#else
    nhUse(X11_bail);
#endif /* SELECTSAVED */

    if (iflags.wc_player_selection == VIA_DIALOG) {
        /* X11_player_selection_dialog() handles name query */
        plsel_ask_name = TRUE;
        iflags.defer_plname = TRUE;
        return;
    } /* else iflags.wc_player_selection == VIA_PROMPTS */

    XtSetArg(args[0], XtNallowShellResize, True);

    popup = XtCreatePopupShell("askname", transientShellWidgetClass, toplevel,
                               args, ONE);
    XtOverrideTranslations(popup,
          XtParseTranslationTable("<Message>WM_PROTOCOLS: askname_delete()"));

    dialog = CreateDialog(popup, nhStr("dialog"), askname_done,
                          (XtCallbackProc) 0);

    SetDialogPrompt(dialog, nhStr("What is your name?")); /* set prompt */
    SetDialogResponse(dialog, svp.plname, PL_NSIZ); /* set default answer */

    XtRealizeWidget(popup);
    positionpopup(popup, TRUE); /* center,bottom */

    nh_XtPopup(popup, (int) XtGrabExclusive, dialog);

    /* The callback will enable the event loop exit. */
    (void) x_event(EXIT_ON_EXIT);

    /* tty's character selection uses this; we might someday;
       since we let user pick an arbitrary name now, he/she can
       pick another one during role selection */
    iflags.renameallowed = TRUE;

    XtDestroyWidget(dialog);
    XtDestroyWidget(popup);
}

/* getline ---------------------------------------------------------------- */
/* This uses Tim Theisen's dialog widget set (from GhostView). */

static Widget getline_popup, getline_dialog;

#define CANCEL_STR "\033"
static char *getline_input; /* buffer to hold user input; getline's output */

/* Callback for getline dialog widget. */
/* ARGSUSED */
static void
done_button(Widget w, XtPointer client_data, XtPointer call_data)
{
    int len;
    char *s;
    Widget dialog = (Widget) client_data;

    nhUse(w);
    nhUse(call_data);

    s = (char *) GetDialogResponse(dialog);
    len = strlen(s);

    /* Truncate input if necessary */
    if (len >= BUFSZ)
        len = BUFSZ - 1;

    (void) strncpy(getline_input, s, len);
    getline_input[len] = '\0';
    XtFree(s);

    nh_XtPopdown(XtParent(dialog));
    exit_x_event = TRUE;
}

/* ARGSUSED */
static void
getline_delete(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    nhUse(event);
    nhUse(params);
    nhUse(num_params);

    Strcpy(getline_input, CANCEL_STR);
    nh_XtPopdown(w);
    exit_x_event = TRUE;
}

/* Callback for getline dialog widget. */
/* ARGSUSED */
static void
abort_button(Widget w, XtPointer client_data, XtPointer call_data)
{
    Widget dialog = (Widget) client_data;

    nhUse(w);
    nhUse(call_data);

    Strcpy(getline_input, CANCEL_STR);
    nh_XtPopdown(XtParent(dialog));
    exit_x_event = TRUE;
}

static void
release_getline_widgets(void)
{
    if (getline_dialog)
        XtDestroyWidget(getline_dialog), getline_dialog = (Widget) 0;
    if (getline_popup)
        XtDestroyWidget(getline_popup), getline_popup = (Widget) 0;
}

/* ask user for a line of text */
void
X11_getlin(
    const char *question, /* prompt */
    char *input)          /* user's input, getlin's _output_ buffer */
{
    unsigned upromptlen;

    getline_input = input; /* used by popup actions */

    flush_screen(1); /* tell core to make sure that map is up to date */
    if (!getline_popup) {
        Arg args[1];

        XtSetArg(args[0], XtNallowShellResize, True);

        getline_popup = XtCreatePopupShell("getline",
                                           transientShellWidgetClass,
                                           toplevel, args, ONE);
        XtOverrideTranslations(getline_popup,
                               XtParseTranslationTable(
                                  "<Message>WM_PROTOCOLS: getline_delete()"));

        getline_dialog = CreateDialog(getline_popup, nhStr("dialog"),
                                      done_button, abort_button);

        XtRealizeWidget(getline_popup);
        XSetWMProtocols(XtDisplay(getline_popup), XtWindow(getline_popup),
                        &wm_delete_window, 1);
    }

    /* 64:  make the answer widget be wide enough to hold 64 characters,
       or the length of the prompt string, whichever is bigger.  User's
       response can be longer--when limit is reached, value-so-far will
       slide left hiding some chars at the beginning of the response but
       making room to type more.  [Prior to 3.6.1, width wasn't specifiable
       and answer box always got sized to match the width of the prompt.] */
    upromptlen = (unsigned) strlen(question);
    if (upromptlen < 64)
        upromptlen = 64;
#ifdef EDIT_GETLIN
    /* set default answer */
    SetDialogResponse(getline_dialog, input, upromptlen);
#else
    /* no default answer */
    SetDialogResponse(getline_dialog, nhStr(""), upromptlen);
#endif
    SetDialogPrompt(getline_dialog, (String) question); /* set prompt */
    positionpopup(getline_popup, TRUE);           /* center,bottom */

    nh_XtPopup(getline_popup, (int) XtGrabExclusive, getline_dialog);

    /* The callback will enable the event loop exit. */
    (void) x_event(EXIT_ON_EXIT);

    /* we get here after the popup has exited;
       put prompt and response into the message window (and into
       core's dumplog history) unless play hasn't started yet */
    if (program_state.in_moveloop || program_state.gameover) {
        /* single space has meaning (to remove a previously applied name) so
           show it clearly; don't care about legibility of multiple spaces */
        const char *visanswer = !input[0] ? "<empty>"
                                : (input[0] == ' ' && !input[1]) ? "<space>"
                                  : (input[0] == '\033') ? "<esc>"
                                    : input;
        int promptlen = (int) strlen(question),
            answerlen = (int) strlen(visanswer);

        /* prompt should be limited to QBUFSZ-1 by caller; enforce that
           here for the echoed value; answer could be up to BUFSZ-1 long;
           pline() will truncate the whole message to that amount so we
           truncate the answer for display; this only affects the echoed
           response, not the actual response being returned to the core */
        if (promptlen >= QBUFSZ)
            promptlen = QBUFSZ - 1;
        if (answerlen + 1 + promptlen >= BUFSZ) /* +1: separating space */
            answerlen = BUFSZ - (1 + promptlen) - 1;
        pline("%.*s %.*s", promptlen, question, answerlen, visanswer);
    }

    /* clear static pointer that's about to go stale */
    getline_input = 0;
}

/* Display file ----------------------------------------------------------- */

/* uses a menu (with no selectors specified) rather than a text window
   to allow previous_page and first_menu actions to move backwards */
void
X11_display_file(const char *str, boolean complain)
{
    dlb *fp;
    winid newwin;
    struct xwindow *wp;
    anything any;
    menu_item *menu_list;
#define LLEN 128
    char line[LLEN];
    int clr = 0;

    /* Use the port-independent file opener to see if the file exists. */
    fp = dlb_fopen(str, RDTMODE);
    if (!fp) {
        if (complain)
            pline("Cannot open %s.  Sorry.", str);
        return; /* it doesn't exist, ignore */
    }

    newwin = X11_create_nhwindow(NHW_MENU);
    wp = &window_list[newwin];
    X11_start_menu(newwin, MENU_BEHAVE_STANDARD);

    any = cg.zeroany;
    while (dlb_fgets(line, LLEN, fp)) {
        X11_add_menu(newwin, &nul_glyphinfo, &any, 0, 0, ATR_NONE,
                     clr, line, MENU_ITEMFLAGS_NONE);
    }
    (void) dlb_fclose(fp);

    /* show file name as the window title */
    if (str)
        wp->title = dupstr(str);

    wp->menu_information->permi = FALSE;
    wp->menu_information->disable_mcolors = TRUE;
    (void) X11_select_menu(newwin, PICK_NONE, &menu_list);
    X11_destroy_nhwindow(newwin);
}

/* yn_function ------------------------------------------------------------ */
/* (not threaded) */

static const char *yn_quitchars = " \n\r";
static const char *yn_choices; /* string of acceptable input */
static char yn_def;
static char yn_return;           /* return value */
static char yn_esc_map;          /* ESC maps to this char. */
static Widget yn_popup;          /* popup for the yn function (created once) */
static Widget yn_label;          /* label for yn function (created once) */
static boolean yn_getting_num;   /* TRUE if accepting digits */
static boolean yn_preserve_case; /* default is to force yn to lower case */
static boolean yn_no_default;    /* don't convert ESC or quitchars to def */
static int yn_ndigits;           /* digit count */
static long yn_val;              /* accumulated value */

static const char yn_translations[] = "#override\n\
     <Key>: yn_key()";

/*
 * Convert the given key event into a character.  If the key maps to
 * more than one character only the first is returned.  If there is
 * no conversion (i.e. just the CTRL key hit) a NUL is returned.
 */
char
key_event_to_char(XKeyEvent *key)
{
    char keystring[MAX_KEY_STRING];
    int nbytes;
    boolean meta = !!(key->state & Mod1Mask);

    nbytes = XLookupString(key, keystring, MAX_KEY_STRING, (KeySym *) 0,
                           (XComposeStatus *) 0);

    /* Modifier keys return a zero length string when pressed. */
    if (nbytes == 0)
        return '\0';

    return (char) (((int) keystring[0]) + (meta ? 0x80 : 0));
}

/*
 * Called when we get a WM_DELETE_WINDOW event on a yn window.
 */
/* ARGSUSED */
static void
yn_delete(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    nhUse(w);
    nhUse(event);
    nhUse(params);
    nhUse(num_params);

    yn_getting_num = FALSE;
    /* Only use yn_esc_map if we have choices.  Otherwise, return ESC. */
    yn_return = yn_choices ? yn_esc_map : '\033';
    exit_x_event = TRUE; /* exit our event handler */
}

/*
 * Called when we get a key press event on a yn window.
 */
/* ARGSUSED */
static void
yn_key(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
    char ch;

    if (appResources.slow && !input_func)
        map_input(w, event, params, num_params);

    ch = key_event_to_char((XKeyEvent *) event);

    if (ch == '\0') { /* don't accept nul char or modifier event */
        /* no bell */
        return;
    }

    if (!yn_choices /* accept any input */
        || (yn_no_default && (ch == '\033' || strchr(yn_quitchars, ch)))) {
        yn_return = ch;
    } else {
        if (!yn_preserve_case)
            ch = lowc(ch); /* move to lower case */

        if (ch == '\033') {
            yn_getting_num = FALSE;
            yn_return = yn_esc_map;
        } else if (strchr(yn_quitchars, ch)) {
            yn_return = yn_def;
        } else if (strchr(yn_choices, ch)) {
            if (ch == '#') {
                if (yn_getting_num) { /* don't select again */
                    X11_nhbell();
                    return;
                }
                yn_getting_num = TRUE;
                yn_ndigits = 0;
                yn_val = 0;
                return; /* wait for more input */
            }
            yn_return = ch;
            if (ch != 'y')
                yn_getting_num = FALSE;
        } else {
            if (yn_getting_num) {
                if (digit(ch)) {
                    long dgt = (long) (ch - '0');

                    yn_ndigits++;
                    /* yn_val = (10 * yn_val) + (ch - '0'); */
                    yn_val = AppendLongDigit(yn_val, dgt);
                    if (yn_val < 0L) {
                        yn_ndigits = 0;
                        yn_val = 0;
                    }
                    return; /* wait for more input */
                }
                if (yn_ndigits && (ch == '\b' || ch == 127 /*DEL*/)) {
                    yn_ndigits--;
                    yn_val = yn_val / 10;
                    return; /* wait for more input */
                }
            }
            X11_nhbell(); /* no match */
            return;
        }

        if (yn_getting_num) {
            yn_return = '#';
            if (yn_val < 0)
                yn_val = 0;
            yn_number = yn_val; /* assign global */
        }
    }
    exit_x_event = TRUE; /* exit our event handler */
}

/* called at exit time */
static void
release_yn_widgets(void)
{
    if (yn_label)
        XtDestroyWidget(yn_label), yn_label = (Widget) 0;
    if (yn_popup)
        XtDestroyWidget(yn_popup), yn_popup = (Widget) 0;
}

/* guts of the X11_yn_function(), not to be confused with core code;
   requires an extra argument: ynflags */
char
X11_yn_function_core(
    const char *ques,     /* prompt text */
    const char *choices,  /* allowed response chars; any char if Null */
    char def,             /* default if user hits <space> or <return> */
    unsigned ynflags)     /* flags; currently just log-it or not */
{
    static XFontStruct *yn_font = 0;
    static Dimension yn_minwidth = 0;
    char buf[BUFSZ], buf2[BUFSZ];
    Arg args[4];
    Cardinal num_args;
    boolean suppress_logging = (ynflags & YN_NO_LOGMESG) != 0U,
            no_default_cnvrt = (ynflags & YN_NO_DEFAULT) != 0U;

    yn_choices = choices; /* set up globals for callback to use */
    yn_def = def;
    yn_no_default = no_default_cnvrt;
    yn_preserve_case = !choices; /* preserve case when an arbitrary
                                  * response is allowed */

    /*
     * This is sort of a kludge.  There are quite a few places in the main
     * nethack code where a pline containing information is followed by a
     * call to yn_function().  There is no flush of the message window
     * (it is implicit in the tty window port), so the line never shows
     * up for us!  Solution: do our own flush.
     */
    if (WIN_MESSAGE != WIN_ERR)
        display_message_window(&window_list[WIN_MESSAGE]);

    if (choices) {
        char *cb, choicebuf[QBUFSZ];

        Strcpy(choicebuf, choices); /* anything beyond <esc> is hidden */
        /* default when choices are present is to force yn answer to
           lowercase unless one or more choices are explicitly uppercase;
           check this before stripping the hidden choices */
        for (cb = choicebuf; *cb; ++cb)
            if ('A' <= *cb && *cb <= 'Z') {
                yn_preserve_case = TRUE;
                break;
            }
        if ((cb = strchr(choicebuf, '\033')) != 0)
            *cb = '\0';
        /* ques [choices] (def) */
        int ln = ((int) strlen(ques)        /* prompt text */
                  + 3                       /* " []" */
                  + (int) strlen(choicebuf) /* choices within "[]" */
                  + 4                       /* " (c)" */
                  + 1                       /* a trailing space */
                  + 1);                     /* \0 terminator */
        if (ln >= BUFSZ)
            panic("X11_yn_function:  question too long (%d)", ln);
        Snprintf(buf, sizeof buf, "%.*s [%s]", QBUFSZ - 1, ques, choicebuf);
        if (def)
            Sprintf(eos(buf), " (%c)", def);
        Strcat(buf, " ");

        /* escape maps to 'q' or 'n' or default, in that order */
        yn_esc_map = (strchr(choices, 'q') ? 'q'
                      : strchr(choices, 'n') ? 'n'
                        : def);
    } else {
        int ln = ((int) strlen(ques)        /* prompt text */
                  + 1                       /* a trailing space */
                  + 1);                     /* \0 terminator */
        if (ln >= BUFSZ)
            panic("X11_yn_function:  question too long (%d)", ln);
        Strcpy(buf, ques);
        Strcat(buf, " ");
    }
    /* for popup-style, add some extra elbow room to the prompt to
       enhance its visibility; there's no cursor shown, just the text */
    if (!appResources.slow) {
        /* insert one leading space and two extra trailing spaces */
        Strcpy(buf2, buf);
        Snprintf(buf, sizeof buf, " %s  ", buf2);
    }

    /*
     * The 'slow' resource is misleadingly named.  When it is True, we
     * re-use the same one-line widget above the map for all yn prompt
     * responses instead of re-using a popup widget for each one.  It's
     * crucial for client/server configs where the server is slow putting
     * up a popup or requires click-to-focus to respond to the popup, but
     * is also useful for players who just want to be able to look at the
     * same location to read questions they're being asked to answer.
     */

    if (appResources.slow) {
        /*
         * 'slow' is True:  the yn_label widget was created when the map
         * and status widgets were, and is positioned between them.  It
         * will persist until end of game.  All we need to do for
         * yn_function is direct keystroke input to the yn response
         * handler and reset its label to be the prompt text (below).
         */
        input_func = yn_key;
        highlight_yn(FALSE); /* expose yn_label as separate from map */
    } else {
        /*
         * 'slow' is False; create a persistent widget that will be popped
         * up as needed, then down again, and last until end of game.  The
         * associated yn_label widget is used to track whether it exists.
         */
        if (!yn_label) {
            XtSetArg(args[0], XtNallowShellResize, True);
            yn_popup = XtCreatePopupShell("query", transientShellWidgetClass,
                                          toplevel, args, ONE);
            XtOverrideTranslations(yn_popup,
               XtParseTranslationTable("<Message>WM_PROTOCOLS: yn_delete()"));

            num_args = 0;
            XtSetArg(args[num_args], nhStr(XtNjustify), XtJustifyLeft);
                                                                   num_args++;
            XtSetArg(args[num_args], XtNtranslations,
                     XtParseTranslationTable(yn_translations)); num_args++;
            yn_label = XtCreateManagedWidget("yn_label", labelWidgetClass,
                                             yn_popup, args, num_args);

            XtRealizeWidget(yn_popup);
            XSetWMProtocols(XtDisplay(yn_popup), XtWindow(yn_popup),
                            &wm_delete_window, 1);

            /* get font that will be used; we'll need it to measure text */
            (void) memset((genericptr_t) args, 0, sizeof args);
            XtSetArg(args[0], nhStr(XtNfont), &yn_font);
            XtGetValues(yn_label, args, ONE);

            /* set up minimum yn_label width; we don't actually set
               the XtNminWidth attribute */
            (void) memset(buf2, 'X', 25), buf2[25] = '\0'; /* 25 'X's */
            yn_minwidth = (Dimension) XTextWidth(yn_font, buf2,
                                                 (int) strlen(buf2));
        }
    }

    /* set the label of the yn widget to be the prompt text */
    (void) memset((genericptr_t) args, 0, sizeof args);
    num_args = 0;
    XtSetArg(args[num_args], XtNlabel, buf); num_args++;
    XtSetValues(yn_label, args, num_args);

    /* for !slow, pop up the prompt+response widget */
    if (!appResources.slow) {
        /*
         * Setting the text doesn't always perform a resize when it
         * should.  We used to just set the text a second time, but that
         * wasn't a reliable workaround, at least on OSX with XQuartz.
         * This seems to work reliably.
         *
         * It also enforces a minimum prompt width, which wasn't being
         * done before, so that really short prompts are more noticeable
         * if they pop up where the pointer is parked and it happens to
         * be sitting somewhere the player isn't looking.
         */
        Dimension promptwidth, labelwidth = 0;

        (void) memset((genericptr_t) args, 0, sizeof args);
        num_args = 0;
        XtSetArg(args[num_args], XtNwidth, &labelwidth); num_args++;
        XtGetValues(yn_label, args, num_args);

        promptwidth = (Dimension) XTextWidth(yn_font, buf, (int) strlen(buf));
        if (labelwidth != promptwidth || labelwidth < yn_minwidth) {
            labelwidth = max(promptwidth, yn_minwidth);
            (void) memset((genericptr_t) args, 0, sizeof args);
            num_args = 0;
            XtSetArg(args[num_args], XtNwidth, labelwidth); num_args++;
            XtSetValues(yn_label, args, num_args);
        }

        positionpopup(yn_popup, TRUE);
        nh_XtPopup(yn_popup, (int) XtGrabExclusive, yn_label);
    }

    yn_getting_num = FALSE;
    (void) x_event(EXIT_ON_EXIT); /* get keystroke(s) */

    /* erase or remove the prompt */
    if (appResources.slow) {
        (void) memset((genericptr_t) args, 0, sizeof args);
        num_args = 0;
        XtSetArg(args[num_args], XtNlabel, " "); num_args++;
        XtSetValues(yn_label, args, num_args);

        input_func = 0; /* keystrokes now belong to the map */
        highlight_yn(FALSE); /* disguise yn_label as part of map */
    } else {
        nh_XtPopdown(yn_popup); /* this removes the event grab */
    }

    if (!suppress_logging) {
        char *p = trimspaces(buf); /* remove !slow's extra whitespace */

        pline("%s %s", p, (yn_return == '\033') ? "ESC" : visctrl(yn_return));
    }

    return yn_return;
}

/* X11-specific edition of yn_function(), the routine called by the core
   to show a prompt and get a single key answer, often 'y' vs 'n' */
char
X11_yn_function(
    const char *ques,     /* prompt text */
    const char *choices,  /* allowed response chars; any char if Null */
    char def)             /* default if user hits <space> or <return> */
{
    char res = X11_yn_function_core(ques, choices, def, YN_NORMAL);

    return res;
}

/* used when processing window-capability-specific run-time options;
   we support toggling tiles on and off via iflags.wc_tiled_map */
void
X11_preference_update(const char *pref)
{
    if (!strcmp(pref, "tiled_map")) {
        if (WIN_MAP != WIN_ERR)
            display_map_window(&window_list[WIN_MAP]);
    } else if (!strcmp(pref, "perm_invent")) {
        ; /* TODO... */
    }
}

/* End global functions =================================================== */

/*
 * Before we wait for input via nhgetch() and nh_poskey(), we need to
 * do some pre-processing.
 */
static int
input_event(int exit_condition)
{
    if (appResources.fancy_status && WIN_STATUS != WIN_ERR)
        check_turn_events(); /* hilighting on the fancy status window */
    if (WIN_MAP != WIN_ERR) /* make sure cursor is not clipped */
        check_cursor_visibility(&window_list[WIN_MAP]);
    if (WIN_MESSAGE != WIN_ERR) /* reset pause line */
        set_last_pause(&window_list[WIN_MESSAGE]);

    return x_event(exit_condition);
}

/*ARGSUSED*/
void
msgkey(Widget w, XtPointer data, XEvent *event, Boolean *continue_to_dispatch)
{
    Cardinal num = 0;

    nhUse(w);
    nhUse(data);
    nhUse(continue_to_dispatch);

    map_input(window_list[WIN_MAP].w, event, (String *) 0, &num);
}

/* only called for autofocus */
/*ARGSUSED*/
static void
win_visible(Widget w, XtPointer data, /* client_data not used */
            XEvent *event,
            Boolean *flag) /* continue_to_dispatch flag not used */
{
    XVisibilityEvent *vis_event = (XVisibilityEvent *) event;

    nhUse(data);
    nhUse(flag);

    if (vis_event->state != VisibilityFullyObscured) {
        /* one-time operation; cancel ourself */
        XtRemoveEventHandler(toplevel, VisibilityChangeMask, False,
                             win_visible, (XtPointer) 0);
        /* grab initial input focus */
        XSetInputFocus(XtDisplay(w), XtWindow(w), RevertToNone, CurrentTime);
    }
}

/* if 'slow' and 'highlight_prompt', set the yn_label widget to look like
   part of the map when idle or to invert background and foreground when
   a prompt is active */
void
highlight_yn(boolean init)
{
    struct xwindow *xmap;

    if (!appResources.slow || !appResources.highlight_prompt)
        return;

    /* first time through, WIN_MAP isn't fully initialized yet */
    xmap = ((map_win != WIN_ERR) ? &window_list[map_win]
               : (WIN_MAP != WIN_ERR) ? &window_list[WIN_MAP] : 0);

    if (init && xmap) {
        Arg args[2];
        XGCValues vals;
        unsigned long fg_bg = (GCForeground | GCBackground);
        GC ggc = (xmap->map_information->is_tile
                    ? xmap->map_information->tile_map.white_gc
                    : xmap->map_information->text_map.copy_gc);

        (void) memset((genericptr_t) &vals, 0, sizeof vals);
        if (XGetGCValues(XtDisplay(xmap->w), ggc, fg_bg, &vals)) {
            XtSetArg(args[0], XtNforeground, vals.foreground);
            XtSetArg(args[1], XtNbackground, vals.background);
            XtSetValues(yn_label, args, TWO);
        }
    } else
        swap_fg_bg(yn_label);
}

/*
 * Set up the playing console.  This has three major parts:  the
 * message window, the map, and the status window.
 *
 * For configs specifying the 'slow' resource, the yn_label widget
 * is placed above the map and below the message window.  Prompts
 * requiring a single character response are displayed there rather
 * than using a popup.
 */
static void
init_standard_windows(void)
{
    Widget form, message_viewport, map_viewport, status;
    Arg args[8];
    Cardinal num_args;
    Dimension message_vp_width, map_vp_width, status_width, max_width;
    int map_vp_hd, status_hd;
    struct xwindow *wp;

    num_args = 0;
    XtSetArg(args[num_args], XtNallowShellResize, True); num_args++;
    form = XtCreateManagedWidget("nethack", panedWidgetClass, toplevel, args,
                                 num_args);

    XtAddEventHandler(form, KeyPressMask, False, (XtEventHandler) msgkey,
                      (XtPointer) 0);

    if (appResources.autofocus)
        XtAddEventHandler(toplevel, VisibilityChangeMask, False, win_visible,
                          (XtPointer) 0);

    /*
     * Create message window.
     */
    WIN_MESSAGE = message_win = find_free_window();
    wp = &window_list[message_win];
    wp->cursx = wp->cursy = wp->pixel_width = wp->pixel_height = 0;
    wp->popup = (Widget) 0;
    create_message_window(wp, FALSE, form);
    message_viewport = XtParent(wp->w);

    /* Tell the form that contains it that resizes are OK. */
    num_args = 0;
    XtSetArg(args[num_args], nhStr(XtNresizable), True); num_args++;
    XtSetArg(args[num_args], nhStr(XtNleft), XtChainLeft); num_args++;
    XtSetArg(args[num_args], nhStr(XtNtop), XtChainTop); num_args++;
    XtSetValues(message_viewport, args, num_args);

    if (appResources.slow) {
        num_args = 0;
        XtSetArg(args[num_args], XtNtranslations,
                 XtParseTranslationTable(yn_translations)); num_args++;
        yn_label = XtCreateManagedWidget("yn_label", labelWidgetClass, form,
                                         args, num_args);
        num_args = 0;
        XtSetArg(args[num_args], nhStr(XtNfromVert), message_viewport);
                                                                   num_args++;
        XtSetArg(args[num_args], nhStr(XtNjustify), XtJustifyLeft);
                                                                  num_args++;
        XtSetArg(args[num_args], nhStr(XtNresizable), True); num_args++;
        XtSetArg(args[num_args], nhStr(XtNlabel), " "); num_args++;
        XtSetValues(yn_label, args, num_args);
    }

    /*
     * Create the map window & viewport and chain the viewport beneath the
     * message_viewport.
     */
    map_win = find_free_window();
    wp = &window_list[map_win];
    wp->cursx = wp->cursy = wp->pixel_width = wp->pixel_height = 0;
    wp->popup = (Widget) 0;
    create_map_window(wp, FALSE, form);
    map_viewport = XtParent(wp->w);

    /* Chain beneath message_viewport or yn window. */
    num_args = 0;
    if (appResources.slow) {
        XtSetArg(args[num_args], nhStr(XtNfromVert), yn_label); num_args++;
    } else {
        XtSetArg(args[num_args], nhStr(XtNfromVert), message_viewport);
                                                                   num_args++;
    }
    XtSetArg(args[num_args], nhStr(XtNbottom), XtChainBottom); num_args++;
    XtSetValues(map_viewport, args, num_args);

    /* Create the status window, with the form as its parent. */
    status_win = find_free_window();
    wp = &window_list[status_win];
    wp->cursx = wp->cursy = wp->pixel_width = wp->pixel_height = 0;
    wp->popup = (Widget) 0;
    create_status_window(wp, FALSE, form);
    status = wp->w;

    /*
     * Chain the status window beneath the viewport.  Mark the left and right
     * edges so that they stay a fixed distance from the left edge of the
     * parent, as well as the top and bottom edges so that they stay a fixed
     * distance from the bottom of the parent.  We do this so that the status
     * will never expand or contract.
     */
    num_args = 0;
    XtSetArg(args[num_args], nhStr(XtNfromVert), map_viewport); num_args++;
    XtSetArg(args[num_args], nhStr(XtNleft), XtChainLeft); num_args++;
    XtSetArg(args[num_args], nhStr(XtNright), XtChainLeft); num_args++;
    XtSetArg(args[num_args], nhStr(XtNtop), XtChainBottom); num_args++;
    XtSetArg(args[num_args], nhStr(XtNbottom), XtChainBottom); num_args++;
    XtSetValues(status, args, num_args);

    /*
     * Realize the popup so that the status widget knows its size.
     *
     * If we unset MappedWhenManaged then the DECwindow driver doesn't
     * attach the nethack toplevel to the highest virtual root window.
     * So don't do it.
     */
    /* XtSetMappedWhenManaged(toplevel, False); */
    XtRealizeWidget(toplevel);
    wm_delete_window = XInternAtom(XtDisplay(toplevel),
                                   "WM_DELETE_WINDOW", False);
    XSetWMProtocols(XtDisplay(toplevel), XtWindow(toplevel),
                    &wm_delete_window, 1);

    /*
     * Resize to at most full-screen.
     */
    {
#define TITLEBAR_SPACE 18 /* Leave SOME screen for window decorations */

        int screen_width = WidthOfScreen(XtScreen(wp->w));
        int screen_height = HeightOfScreen(XtScreen(wp->w)) - TITLEBAR_SPACE;
        Dimension form_width, form_height;

        XtSetArg(args[0], XtNwidth, &form_width);
        XtSetArg(args[1], XtNheight, &form_height);
        XtGetValues(toplevel, args, TWO);

        if (form_width > screen_width || form_height > screen_height) {
            XtSetArg(args[0], XtNwidth, min(form_width, screen_width));
            XtSetArg(args[1], XtNheight, min(form_height, screen_height));
            XtSetValues(toplevel, args, TWO);
            XMoveWindow(XtDisplay(toplevel), XtWindow(toplevel), 0,
                        TITLEBAR_SPACE);
        }
#undef TITLEBAR_SPACE
    }

    post_process_tiles(); /* after toplevel is realized */

    /*
     * Now get the default widths of the windows.
     */
    XtSetArg(args[0], nhStr(XtNwidth), &message_vp_width);
    XtGetValues(message_viewport, args, ONE);
    XtSetArg(args[0], nhStr(XtNwidth), &map_vp_width);
    XtSetArg(args[1], nhStr(XtNhorizDistance), &map_vp_hd);
    XtGetValues(map_viewport, args, TWO);
    XtSetArg(args[0], nhStr(XtNwidth), &status_width);
    XtSetArg(args[1], nhStr(XtNhorizDistance), &status_hd);
    XtGetValues(status, args, TWO);

    /*
     * Adjust positions and sizes.  The message viewport widens out to the
     * widest width.  Both the map and status are centered by adjusting
     * their horizDistance.
     */
    if (map_vp_width < status_width || map_vp_width < message_vp_width) {
        if (status_width > message_vp_width) {
            XtSetArg(args[0], nhStr(XtNwidth), status_width);
            XtSetValues(message_viewport, args, ONE);
            max_width = status_width;
        } else {
            max_width = message_vp_width;
        }
        XtSetArg(args[0], nhStr(XtNhorizDistance),
                 map_vp_hd + ((int) (max_width - map_vp_width) / 2));
        XtSetValues(map_viewport, args, ONE);

    } else { /* map is widest */
        XtSetArg(args[0], nhStr(XtNwidth), map_vp_width);
        XtSetValues(message_viewport, args, ONE);
    }
    /*
     * Clear all data values on the fancy status widget so that the values
     * used for spacing don't appear.  This needs to be called some time
     * after the fancy status widget is realized (above, with the game popup),
     * but before it is popped up.
     */
    if (appResources.fancy_status)
        null_out_status();
    /*
     * Set the map size to its standard size.  As with the message window
     * above, the map window needs to be set to its constrained size until
     * its parent (the viewport widget) was realized.
     *
     * Move the message window's slider to the bottom.
     */
    set_map_size(&window_list[map_win], COLNO, ROWNO);
    set_message_slider(&window_list[message_win]);

    /* attempt to catch fatal X11 errors before the program quits */
    (void) XtAppSetErrorHandler(app_context, X11_error_handler);

    highlight_yn(TRUE); /* switch foreground and background */

    /* We can now print to the message window. */
    iflags.window_inited = 1;
}

void
nh_XtPopup(Widget w,        /* widget */
           int grb,         /* type of grab */
           Widget childwid) /* child to receive focus (can be None) */
{
    XtPopup(w, (XtGrabKind) grb);
    XSetWMProtocols(XtDisplay(w), XtWindow(w), &wm_delete_window, 1);
    if (appResources.autofocus)
        XtSetKeyboardFocus(toplevel, childwid);
}

void
nh_XtPopdown(Widget w)
{
    XtPopdown(w);
    if (appResources.autofocus)
        XtSetKeyboardFocus(toplevel, None);
}

void
win_X11_init(int dir)
{
    if (dir != WININIT)
        return;

    return;
}

void
find_scrollbars(
    Widget w,      /* widget of interest; scroll bars are probably attached
                      to its parent or grandparent */
    Widget last_w, /* if non-zero, don't search ancestry beyond this point */
    Widget *horiz, /* output: horizontal scrollbar */
    Widget *vert)  /* output: vertical scrollbar */
{
    *horiz = *vert = (Widget) 0;
    /* for 3.6 this looked for an ancestor with both scrollbars but
       menus might have only vertical */
    if (w) {
        do {
            *horiz = XtNameToWidget(w, "*horizontal");
            *vert = XtNameToWidget(w, "*vertical");
            if (*horiz || *vert)
                break;
            w = XtParent(w);
        } while (w && (!last_w || w != last_w));
    }
}

/* Callback
 * Scroll a viewport, using standard NH 1,2,3,4,6,7,8,9 directions.
 */
/*ARGSUSED*/
void
nh_keyscroll(Widget viewport, XEvent *event, String *params,
             Cardinal *num_params)
{
    Arg arg[2];
    Widget horiz_sb = (Widget) 0, vert_sb = (Widget) 0;
    float top, shown;
    Boolean do_call;
    int direction;
    Cardinal in_nparams = (num_params ? *num_params : 0);

    nhUse(event);

    if (in_nparams != 1)
        return; /* bad translation */

    direction = atoi(params[0]);

    find_scrollbars(viewport, (Widget) 0, &horiz_sb, &vert_sb);

#define H_DELTA 0.25 /* distance of horiz shift */
    /* vert shift is half of curr distance */
    /* The V_DELTA is 1/2 the value of shown. */

    if (horiz_sb) {
        XtSetArg(arg[0], nhStr(XtNshown), &shown);
        XtSetArg(arg[1], nhStr(XtNtopOfThumb), &top);
        XtGetValues(horiz_sb, arg, TWO);

        do_call = True;

        switch (direction) {
        case 1:
        case 4:
        case 7:
            top -= H_DELTA;
            if (top < 0.0)
                top = 0.0;
            break;
        case 3:
        case 6:
        case 9:
            top += H_DELTA;
            if (top + shown > 1.0)
                top = 1.0 - shown;
            break;
        default:
            do_call = False;
        }

        if (do_call) {
            XtCallCallbacks(horiz_sb, XtNjumpProc, &top);
        }
    }

    if (vert_sb) {
        XtSetArg(arg[0], nhStr(XtNshown), &shown);
        XtSetArg(arg[1], nhStr(XtNtopOfThumb), &top);
        XtGetValues(vert_sb, arg, TWO);

        do_call = True;

        switch (direction) {
        case 7:
        case 8:
        case 9:
            top -= shown / 2.0;
            if (top < 0.0)
                top = 0;
            break;
        case 1:
        case 2:
        case 3:
            top += shown / 2.0;
            if (top + shown > 1.0)
                top = 1.0 - shown;
            break;
        default:
            do_call = False;
        }

        if (do_call) {
            XtCallCallbacks(vert_sb, XtNjumpProc, &top);
        }
    }
}

/*winX.c*/
