#include <stdio.h>
#include <ctype.h>

#ifdef SYSV
# define SIGCHLD SIGCLD
# ifndef att
#  define sigsetmask(x)
# endif /* !att */
#else /* SYSV */
# include <sys/wait.h>
#endif /* SYSV */

#include <sys/signal.h>
#include <sys/errno.h>

#include <X11/Xlib.h>
#include <X11/Xresource.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>

static char* programName;
#define programClass  "XIcon"

typedef struct {
    Window window;
    Bool running;
    char* name;
    char* title;
    char* class;
    char* icon_name;
    char* command;
} Icon;

static childDied = 0;
static wmFrame;
static XrmDatabase initialDatabase, serverDatabase = 0;

static XrmOptionDescRec switches[] = {
    { "-display",	".display",	XrmoptionSepArg,
	  (caddr_t) NULL },
    { "-wmframe",	".wmFrame",	XrmoptionNoArg,
	  (caddr_t) "true" },
    { "-xrm",		NULL,		XrmoptionResArg,
	  (caddr_t) NULL },
};

static void
usage()
{
    fprintf(stderr,
	    "usage: %s [ -display display ] \
[ -wmframe ] [ -xrm resource [ ... ] ]\n", programName);
    exit(1);
}

static char*
GetStringResource(name, class)
char* name;
char* class;
{
    char* type;
    XrmValue value;

    if (!XrmGetResource(initialDatabase, name, class, &type, &value))
	if (serverDatabase &&
	    !XrmGetResource(serverDatabase, name, class, &type, &value))
	    return 0;
    return (char*) value.addr;
}

static Bool
GetBooleanResource(name, class, dflt)
char* name;
char* class;
Bool dflt;
{
    char* resource;
    char* s;
    char** p;
    static char* affirmation[] = { "yes", "on", "true", 0 };
    static char* denial[] = { "no", "off", "false", 0 };

    resource = GetStringResource(name, class);

    if (!resource)
	return dflt;

    for (s = resource; *s; s++)
	if (isupper(*s))
	    *s = tolower(*s);

    p = affirmation;
    while (*p)
	if (strcmp(*p++, resource) == 0)
	    return 1;

    p = denial;
    while (*p)
	if (strcmp(*p++, resource) == 0)
	    return 0;

    return dflt;
}

/*
  We need a mapping from pids to windows.
 */

typedef struct pidmap {
    int pid;
    Window window;
    struct pidmap* next;
} pidmap;

static pidmap* processes = 0;

static void
HandleDeadChildren(display, icons)
Display* display;
XContext icons;
{
    while (childDied) {
	int pid = wait(0);
	pidmap** process = &processes;
	pidmap* old;
	Icon* icon;

	while (*process && (*process)->pid != pid)
	    process = &(*process)->next;

	if (!*process) {
	    fprintf(stderr, "Programmer error --- lost a process!\n");
	    abort();
	}

	old = *process;
	*process = old->next;
	XUnmapWindow(display, old->window);
	XMapWindow(display, old->window);

	/*
	  Thanks to jik@pit-manager.mit.edu for the correct cast in
	  XFindContext.  I just *couldn't* figure out what gcc was
	  complaining about....
	 */
	if (XFindContext(display, old->window, icons,
			 (caddr_t*) &icon) != XCSUCCESS) {
	    fprintf(stderr, "Programmer error --- lost an icon!\n");
	    abort();
	}
	icon->running = 0;
	free((char*) old);
	childDied--;
    }
}

static void
HandleSIGCHLD()
{
    childDied++;
}

static void
AddProcess(display, window, icons)
Display* display;
Window window;
XContext icons;
{
    Icon* icon;
    int pid;
    XUnmapEvent sendEvent;

    if (XFindContext(display, window, icons, (caddr_t*) &icon) != XCSUCCESS) {
	fprintf(stderr, "Programmer error --- lost an icon!\n");
	abort();
    }

    XUnmapWindow(display, icon->window);

    /*
      ICCCM says we must do this, as the window may have since
      become unmapped....
      */
    sendEvent.event = DefaultRootWindow(display);
    sendEvent.window = icon->window;
    sendEvent.from_configure = 0;
    XSendEvent(display, sendEvent.event, 0,
	       SubstructureRedirectMask | SubstructureNotifyMask,
	       (XEvent*) &sendEvent);

    XFlush(display);		/* do it NOW! */
    
    if (icon->running)
	/* Prevent a race-sort of condition if the child dies too quickly? */
	HandleDeadChildren(display, icons);
    else {
	pidmap* process;
	char* command = icon->command;
	char* s;

	icon->running = 1;

	/* ??? We need to conditionally redefine vfork as fork. */

	/*
	  Never you mind the above; there is a bug with Ultrix 4.0+
	  RISC; if the child process from a vfork fails to do an exec,
	  the parent never receives a SIGCHLD.
	     -|trost Fri Feb  8 12:00:06 1991
	 */

	switch (pid = fork()) {
	case -1:
	    perror("fork");
	    exit(2);
	    /* NOT REACHED */
	case 0:
	    setpgrp(0, getpid());
	    for (s = command; *s; s++)
		if (index(" \t\n|<>&'\"*?$()[]{}=\\;~`", *s)) {
		    char* getenv();
		    char* shell = getenv("SHELL");
		    
		    execl(shell, shell, "-c", command, 0);
		    break;
		}
	    if (!*s)
		execlp(command, command, 0);
	    fprintf(stderr, "Couldn't run \"%s\".\n", command);
	    perror("exec");
	    exit(0);
	    /* NOT REACHED */
	}

	process = (pidmap*) malloc(sizeof(*process));
	process->window = icon->window;
	process->pid = pid;
	process->next = processes;
	processes = process;
    }
}

static Window
CreateIconWindow(display, icon)
Display* display;
Icon* icon;
{
    Window window = XCreateSimpleWindow(display, DefaultRootWindow(display),
					0, 0,  2, 2,  0, 0,  None);
    XClassHint	classHint;
    XSizeHints	sizeHints;
    XWMHints	wmHints;
    char	hostname[255];
    XSetWindowAttributes	attributes;

    XSelectInput(display, window, ExposureMask | StructureNotifyMask);

    classHint.res_name = icon->name;
    classHint.res_class = icon->class;
    XSetClassHint(display, window, &classHint);

    /*
      Obsolete, but we should set for old wm's.  And maybe new wm's
      will be nice and do it for us anyhow (since I can't figure out
      the new way of doing it).
     */
    sizeHints.flags = PPosition;
    sizeHints.x = 200;
    sizeHints.y = 200;
    XSetNormalHints(display, window, &sizeHints);

    /* This was hacked in so that tvtwm will put window on-screen. */
    /* Transient for self?  Sure, why not? */
    XSetTransientForHint(display, window, window);

    XStoreName(display, window, icon->title);
    XSetIconName(display, window, icon->icon_name);

    wmHints.flags = StateHint;
    wmHints.initial_state = IconicState;
    XSetWMHints(display, window, &wmHints);

    attributes.backing_store = NotUseful;
    XChangeWindowAttributes(display, window, CWBackingStore, &attributes);

    gethostname(hostname, sizeof(hostname));
    XChangeProperty(display, window, XA_WM_CLIENT_MACHINE, XA_STRING, 8,
		    PropModeReplace, (unsigned char*) hostname,
		    strlen(hostname));

    return window;
}

static void
CreateIcon(display, icons, name)
Display* display;
XContext icons;
char* name;
{
    char	resource[255], class[255];
    char	baseResource[255];
    char	baseClass[255];
    Icon*	icon = (Icon*) Xpermalloc(sizeof(Icon));

    icon->name = name;

    sprintf(baseResource, "%s.%s.%%s", programName, name);
    sprintf(baseClass, "%s.%s.%%s", programClass, name);

    sprintf(resource, baseResource, "class");
    sprintf(class, baseClass, "Class");
    if (icon->class = GetStringResource(resource, class))
	sprintf(baseClass, "%s.%s.%%s", programClass, icon->class);
    else
	icon->class = name;

    sprintf(resource, baseResource, "title");
    sprintf(class, baseClass, "Title");
    icon->title = GetStringResource(resource, class);
    if (!icon->title)
	icon->title = name;

    sprintf(resource, baseResource, "iconName");
    sprintf(class, baseClass, "IconName");
    icon->icon_name = GetStringResource(resource, class);
    if (!icon->icon_name)
	icon->icon_name = icon->title ? icon->title : name;

    sprintf(resource, baseResource, "command");
    sprintf(class, baseClass, "Command");
    icon->command = GetStringResource(resource, class);
    if (!icon->command)
	icon->command = name;

    icon->running = 0;
    icon->window = CreateIconWindow(display, icon);

    XSaveContext(display, icon->window, icons, icon);

    sprintf(resource, baseResource, "startIconic");
    sprintf(class, baseClass, "Command");
    if (GetBooleanResource(resource, class, 1))
	XMapWindow(display, icon->window);
    else
	AddProcess(display, icon->window, icons);
}
    
static char**
ParseIconList(list)
char* list;
{
    int i;
    static char* rv[255];
    char buf[1024];

    if (!list) {
	rv[0] = 0;
	return rv;
    }

    /*
      The upper bound for the loop has been decreased by one so that
      we can add the terminating null pointer to the array if our
      array fills up.
     */
    for (i = 0; i < sizeof(rv)/sizeof(rv[0]) - 1; i++) {
	char* s = buf;
	/* eat leading whitespace */
	while (*list == ' ' || *list == '\t')
	    list++;
	while (*list && *list != ',')
	    *s++ = *list++;
	*s++ = '\0';
	rv[i] = Xpermalloc(s - buf);
	bcopy(buf, rv[i], s - buf);
	if (!*list) {
	    rv[i + 1] = 0;
	    return rv;
	}
	list++;
    }
    fprintf(stderr, "Sorry, \"only\" 255 icons supported; continuing....\n");
    rv[i] = 0;
    return rv;
}

static void
Dispatch(event, icons)
XEvent* event;
XContext icons;
{
    XEvent junkEvent;
    switch (event->type) {
    case UnmapNotify:
    case ReparentNotify:
    case ConfigureNotify:
	/* ignore */
	break;
    case MapNotify:
	XRaiseWindow(event->xany.display, event->xany.window);
	XMoveWindow(event->xany.display, event->xany.window, 5, 5);
	break;
    case Expose:
	/*
	  Let's try sucking off all the exposes that might be pending
	  for this window.
	 */
	if (wmFrame) {
	    Window root, parent;
	    Window* children;
	    unsigned nchildren;	/* should always be zero */

	    XQueryTree(event->xany.display, event->xany.window,
		       &root, &parent, &children, &nchildren);
	    XFree((char *) children);
	    if (root == parent)
		break;
	}
	AddProcess(event->xany.display, event->xany.window, icons);
	break;
    default:
	fprintf(stderr, "Strange event %d.\n", event->type);
	break;
    }
}

main(argc, argv)
int argc;
char** argv;
{
    Display* display;
    char* displayName;
    char resource[255], class[255];
    char* waitString;
    int i;
    char** iconNames;
    char** name;
    XContext icons;
    char* rindex();
    int fd;

    programName = rindex(argv[0], '/');
    if (programName)
	programName++;
    else
	programName = argv[0];

    XrmParseCommand(&initialDatabase,
		    switches, sizeof(switches)/sizeof(switches[0]),
		    programName, &argc, argv);

    if (argc > 1)
	usage();
		    
    sprintf(resource, "%s.display", programName);
    display = XOpenDisplay(displayName = GetStringResource(resource, ""));

    if (!display) {
	fprintf(stderr, "Couldn't open display \"%s\"\n",
		XDisplayName(displayName));
	usage();
    }

    serverDatabase = XrmGetStringDatabase(display->xdefaults);

    sprintf(resource, "%s.wmFrame", programName);
    sprintf(class, "%s.WmFrame", programClass);
    wmFrame = GetBooleanResource(resource, class, 0);

    sprintf(resource, "%s.icons", programName);
    sprintf(class, "%s.Icons", programClass);
    iconNames = ParseIconList(GetStringResource(resource, class));

    for (name = iconNames; *name; name++)
	CreateIcon(display, icons, *name);

    fd = ConnectionNumber(display);

    (void) signal(SIGCHLD, HandleSIGCHLD);

    /*
      Some machines (e.g., Tek 4317) start processes with some bogus
      sigmask.  If I were trying to port this program and sigsetmask
      failed to link, I'd remove the following lines, or, if you feel
      up to it, inspect the #ifdef junk at the top of the file.
     */

    sigsetmask(0);

    for (;;) {
	static unsigned long readfds[1], exceptfds[1];
	XEvent event;
	extern int errno;

	if (*readfds) {
	    XNextEvent(display, &event);
	    Dispatch(&event, icons);
	}

	while (i = XPending(display))
	    while (i--) {
		XNextEvent(display, &event);
		Dispatch(&event, icons);
	    }
	XFlush(display);
	
	readfds[fd/32] = 1 << fd % 32;
	exceptfds[fd/32] = 1 << fd % 32;

	HandleDeadChildren(display, icons);
	/*
	  race condition here:  If a child dies here before the select starts,
	  no one will ever know about it....
	 */
	while (select(fd + 1, readfds, 0, exceptfds, 0) < 1) {
	    if (errno != EINTR) {
		perror("select");
		exit(2);
	    }
	    HandleDeadChildren(display, icons);
	    XFlush(display);
	}
    }
}
