/* -*- Mode:C; tab-width: 8 -*-
 * remote.c --- remote control of Netscape Navigator for Unix.
 * version 1.1.3, for Netscape Navigator 1.1 and newer.
 *
 * Copyright © 1996 Netscape Communications Corporation, all rights reserved.
 * Created: Jamie Zawinski <jwz@netscape.com>, 24-Dec-94.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 *
 * To compile:
 *
 *    cc -o netscape-remote remote.c -DSTANDALONE -lXmu -lX11
 *
 * To use:
 *
 *    netscape-remote -help
 *
 * Documentation for the protocol which this code implements may be found at:
 *
 *    http://home.netscape.com/newsref/std/x-remote.html
 *
 * Bugs and commentary to x_cbug@netscape.com.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xmu/WinUtil.h>	/* for XmuClientWindow() */


/* vroot.h is a header file which lets a client get along with `virtual root'
   window managers like swm, tvtwm, olvwm, etc.  If you don't have this header
   file, you can find it at "http://home.netscape.com/newsref/std/vroot.h".
   If you don't care about supporting virtual root window managers, you can
   comment this line out.
 */
#include "vroot.h"


#ifdef STANDALONE
 static const char *progname = 0;
 static const char *expected_mozilla_version = "1.1";
#else  /* !STANDALONE */
 extern const char *progname;
 extern const char *expected_mozilla_version;
#endif /* !STANDALONE */

#define MOZILLA_VERSION_PROP   "_MOZILLA_VERSION"
#define MOZILLA_LOCK_PROP      "_MOZILLA_LOCK"
#define MOZILLA_COMMAND_PROP   "_MOZILLA_COMMAND"
#define MOZILLA_RESPONSE_PROP  "_MOZILLA_RESPONSE"
static Atom XA_MOZILLA_VERSION  = 0;
static Atom XA_MOZILLA_LOCK     = 0;
static Atom XA_MOZILLA_COMMAND  = 0;
static Atom XA_MOZILLA_RESPONSE = 0;

static void
mozilla_remote_init_atoms (Display *dpy)
{
  if (! XA_MOZILLA_VERSION)
    XA_MOZILLA_VERSION = XInternAtom (dpy, MOZILLA_VERSION_PROP, False);
  if (! XA_MOZILLA_LOCK)
    XA_MOZILLA_LOCK = XInternAtom (dpy, MOZILLA_LOCK_PROP, False);
  if (! XA_MOZILLA_COMMAND)
    XA_MOZILLA_COMMAND = XInternAtom (dpy, MOZILLA_COMMAND_PROP, False);
  if (! XA_MOZILLA_RESPONSE)
    XA_MOZILLA_RESPONSE = XInternAtom (dpy, MOZILLA_RESPONSE_PROP, False);
}

static Window
mozilla_remote_find_window (Display *dpy)
{
  int i;
  Window root = RootWindowOfScreen (DefaultScreenOfDisplay (dpy));
  Window root2, parent, *kids;
  unsigned int nkids;
  Window result = 0;
  Window tenative = 0;
  unsigned char *tenative_version = 0;

  if (! XQueryTree (dpy, root, &root2, &parent, &kids, &nkids))
    {
      fprintf (stderr, "%s: XQueryTree failed on display %s\n", progname,
	       DisplayString (dpy));
      exit (2);
    }

  /* root != root2 is possible with virtual root WMs. */

  if (! (kids && nkids))
    {
      fprintf (stderr, "%s: root window has no children on display %s\n",
	       progname, DisplayString (dpy));
      exit (2);
    }

  for (i = nkids-1; i >= 0; i--)
    {
      Atom type;
      int format, status;
      unsigned long nitems, bytesafter;
      unsigned char *version = 0;
      Window w;

      w = XmuClientWindow (dpy, kids[i]);
      status = XGetWindowProperty (dpy, w, XA_MOZILLA_VERSION,
				   0, (65536 / sizeof (long)),
				   False, XA_STRING,
				   &type, &format, &nitems, &bytesafter,
				   &version);
      if (! version)
	continue;
      if (strcmp ((char *) version, expected_mozilla_version) &&
	  !tenative)
	{
	  tenative = w;
	  tenative_version = version;
	  continue;
	}
      XFree (version);
      if (status == Success && type != None)
	{
	  result = w;
	  break;
	}
    }

  if (result && tenative)
    {
      fprintf (stderr,
	       "%s: warning: both version %s (0x%x) and version\n"
	       "\t%s (0x%x) are running.  Using version %s.\n",
	       progname, tenative_version, (unsigned int) tenative,
	       expected_mozilla_version, (unsigned int) result,
	       expected_mozilla_version);
      XFree (tenative_version);
      return result;
    }
  else if (tenative)
    {
#if 0
      fprintf (stderr,
	       "%s: warning: expected version %s but found version\n"
	       "\t%s (0x%x) instead.\n",
	       progname, expected_mozilla_version,
	       tenative_version, (unsigned int) tenative);
#endif
      XFree (tenative_version);
      return tenative;
    }
  else if (result)
    {
      return result;
    }
  else
    {
      fprintf (stderr, "%s: not running on display %s\n", progname,
	       DisplayString (dpy));
      exit (1);
    }
}

static void
mozilla_remote_check_window (Display *dpy, Window window)
{
  Atom type;
  int format;
  unsigned long nitems, bytesafter;
  unsigned char *version = 0;
  int status = XGetWindowProperty (dpy, window, XA_MOZILLA_VERSION,
				   0, (65536 / sizeof (long)),
				   False, XA_STRING,
				   &type, &format, &nitems, &bytesafter,
				   &version);
  if (status != Success || !version)
    {
      fprintf (stderr, "%s: window 0x%x is not a Netscape window.\n",
	       progname, (unsigned int) window);
      exit (6);
    }
  else if (strcmp ((char *) version, expected_mozilla_version))
    {
      fprintf (stderr,
	       "%s: warning: window 0x%x is Netscape version %s;\n"
	       "\texpected version %s.\n",
	       progname, (unsigned int) window,
	       version, expected_mozilla_version);
    }
  XFree (version);
}


static char *lock_data = 0;

static void
mozilla_remote_obtain_lock (Display *dpy, Window window)
{
  Bool locked = False;
  Bool waited = False;

  if (! lock_data)
    {
      lock_data = (char *) malloc (255);
      sprintf (lock_data, "pid%d@", getpid ());
      if (gethostname (lock_data + strlen (lock_data), 100))
	{
	  perror ("gethostname");
	  exit (-1);
	}
    }

  do
    {
      int result;
      Atom actual_type;
      int actual_format;
      unsigned long nitems, bytes_after;
      unsigned char *data = 0;

      XGrabServer (dpy);   /* ################################# DANGER! */

      result = XGetWindowProperty (dpy, window, XA_MOZILLA_LOCK,
				   0, (65536 / sizeof (long)),
				   False, /* don't delete */
				   XA_STRING,
				   &actual_type, &actual_format,
				   &nitems, &bytes_after,
				   &data);
      if (result != Success || actual_type == None)
	{
	  /* It's not now locked - lock it. */
#ifdef DEBUG_PROPS
	  fprintf (stderr, "%s: (writing " MOZILLA_LOCK_PROP
		   " \"%s\" to 0x%x)\n",
		   progname, lock_data, (unsigned int) window);
#endif
	  XChangeProperty (dpy, window, XA_MOZILLA_LOCK, XA_STRING, 8,
			   PropModeReplace, (unsigned char *) lock_data,
			   strlen (lock_data));
	  locked = True;
	}

      XUngrabServer (dpy); /* ################################# danger over */
      XSync (dpy, False);

      if (! locked)
	{
	  /* We tried to grab the lock this time, and failed because someone
	     else is holding it already.  So, wait for a PropertyDelete event
	     to come in, and try again. */

	  fprintf (stderr, "%s: window 0x%x is locked by %s; waiting...\n",
		   progname, (unsigned int) window, data);
	  waited = True;

	  while (1)
	    {
	      XEvent event;
	      XNextEvent (dpy, &event);
	      if (event.xany.type == DestroyNotify &&
		  event.xdestroywindow.window == window)
		{
		  fprintf (stderr, "%s: window 0x%x unexpectedly destroyed.\n",
			   progname, (unsigned int) window);
		  exit (6);
		}
	      else if (event.xany.type == PropertyNotify &&
		       event.xproperty.state == PropertyDelete &&
		       event.xproperty.window == window &&
		       event.xproperty.atom == XA_MOZILLA_LOCK)
		{
		  /* Ok!  Someone deleted their lock, so now we can try
		     again. */
#ifdef DEBUG_PROPS
		  fprintf (stderr, "%s: (0x%x unlocked, trying again...)\n",
			   progname, (unsigned int) window);
#endif
		  break;
		}
	    }
	}
      if (data)
	XFree (data);
    }
  while (! locked);

  if (waited)
    fprintf (stderr, "%s: obtained lock.\n", progname);
}


static void
mozilla_remote_free_lock (Display *dpy, Window window)
{
  int result;
  Atom actual_type;
  int actual_format;
  unsigned long nitems, bytes_after;
  unsigned char *data = 0;

#ifdef DEBUG_PROPS
	  fprintf (stderr, "%s: (deleting " MOZILLA_LOCK_PROP
		   " \"%s\" from 0x%x)\n",
		   progname, lock_data, (unsigned int) window);
#endif

  result = XGetWindowProperty (dpy, window, XA_MOZILLA_LOCK,
			       0, (65536 / sizeof (long)),
			       True, /* atomic delete after */
			       XA_STRING,
			       &actual_type, &actual_format,
			       &nitems, &bytes_after,
			       &data);
  if (result != Success)
    {
      fprintf (stderr, "%s: unable to read and delete " MOZILLA_LOCK_PROP
	       " property\n",
	       progname);
      return;
    }
  else if (!data || !*data)
    {
      fprintf (stderr, "%s: invalid data on " MOZILLA_LOCK_PROP
	       " of window 0x%x.\n",
	       progname, (unsigned int) window);
      return;
    }
  else if (strcmp ((char *) data, lock_data))
    {
      fprintf (stderr, "%s: " MOZILLA_LOCK_PROP
	       " was stolen!  Expected \"%s\", saw \"%s\"!\n",
	       progname, lock_data, data);
      return;
    }

  if (data)
    XFree (data);
}


static int
mozilla_remote_command (Display *dpy, Window window, const char *command,
			Bool raise_p)
{
  int result = 0;
  Bool done = False;
  char *new_command = 0;

  /* The -noraise option is implemented by passing a "noraise" argument
     to each command to which it should apply.
   */
  if (! raise_p)
    {
      char *close;
      new_command = (char *) malloc (strlen (command) + 20);
      strcpy (new_command, command);
      close = strrchr (new_command, ')');
      if (close)
	strcpy (close, ", noraise)");
      else
	strcat (new_command, "(noraise)");
      command = new_command;
    }

#ifdef DEBUG_PROPS
  fprintf (stderr, "%s: (writing " MOZILLA_COMMAND_PROP " \"%s\" to 0x%x)\n",
	   progname, command, (unsigned int) window);
#endif

  XChangeProperty (dpy, window, XA_MOZILLA_COMMAND, XA_STRING, 8,
		   PropModeReplace, (unsigned char *) command,
		   strlen (command));

  while (!done)
    {
      XEvent event;
      XNextEvent (dpy, &event);
      if (event.xany.type == DestroyNotify &&
	  event.xdestroywindow.window == window)
	{
	  /* Print to warn user...*/
	  fprintf (stderr, "%s: window 0x%x was destroyed.\n",
		   progname, (unsigned int) window);
	  result = 6;
	  goto DONE;
	}
      else if (event.xany.type == PropertyNotify &&
	       event.xproperty.state == PropertyNewValue &&
	       event.xproperty.window == window &&
	       event.xproperty.atom == XA_MOZILLA_RESPONSE)
	{
	  Atom actual_type;
	  int actual_format;
	  unsigned long nitems, bytes_after;
	  unsigned char *data = 0;

	  result = XGetWindowProperty (dpy, window, XA_MOZILLA_RESPONSE,
				       0, (65536 / sizeof (long)),
				       True, /* atomic delete after */
				       XA_STRING,
				       &actual_type, &actual_format,
				       &nitems, &bytes_after,
				       &data);
#ifdef DEBUG_PROPS
	  if (result == Success && data && *data)
	    {
	      fprintf (stderr, "%s: (server sent " MOZILLA_RESPONSE_PROP
		       " \"%s\" to 0x%x.)\n",
		       progname, data, (unsigned int) window);
	    }
#endif

	  if (result != Success)
	    {
	      fprintf (stderr, "%s: failed reading " MOZILLA_RESPONSE_PROP
		       " from window 0x%0x.\n",
		       progname, (unsigned int) window);
	      result = 6;
	      done = True;
	    }
	  else if (!data || strlen((char *) data) < 5)
	    {
	      fprintf (stderr, "%s: invalid data on " MOZILLA_RESPONSE_PROP
		       " property of window 0x%0x.\n",
		       progname, (unsigned int) window);
	      result = 6;
	      done = True;
	    }
	  else if (*data == '1')	/* positive preliminary reply */
	    {
	      fprintf (stderr, "%s: %s\n", progname, data + 4);
	      /* keep going */
	      done = False;
	    }
#if 1
	  else if (!strncmp ((char *)data, "200", 3)) /* positive completion */
	    {
	      result = 0;
	      done = True;
	    }
#endif
	  else if (*data == '2')		/* positive completion */
	    {
	      fprintf (stderr, "%s: %s\n", progname, data + 4);
	      result = 0;
	      done = True;
	    }
	  else if (*data == '3')	/* positive intermediate reply */
	    {
	      fprintf (stderr, "%s: internal error: "
		       "server wants more information?  (%s)\n",
		       progname, data);
	      result = 3;
	      done = True;
	    }
	  else if (*data == '4' ||	/* transient negative completion */
		   *data == '5')	/* permanent negative completion */
	    {
	      fprintf (stderr, "%s: %s\n", progname, data + 4);
	      result = (*data - '0');
	      done = True;
	    }
	  else
	    {
	      fprintf (stderr,
		       "%s: unrecognised " MOZILLA_RESPONSE_PROP
		       " from window 0x%x: %s\n",
		       progname, (unsigned int) window, data);
	      result = 6;
	      done = True;
	    }

	  if (data)
	    XFree (data);
	}
#ifdef DEBUG_PROPS
      else if (event.xany.type == PropertyNotify &&
	       event.xproperty.window == window &&
	       event.xproperty.state == PropertyDelete &&
	       event.xproperty.atom == XA_MOZILLA_COMMAND)
	{
	  fprintf (stderr, "%s: (server 0x%x has accepted "
		   MOZILLA_COMMAND_PROP ".)\n",
		   progname, (unsigned int) window);
	}
#endif /* DEBUG_PROPS */
    }

 DONE:

  if (new_command)
    free (new_command);

  return result;
}

int
mozilla_remote_commands (Display *dpy, Window window, char **commands)
{
  Bool raise_p = True;
  int status = 0;
  mozilla_remote_init_atoms (dpy);

  if (window == 0)
    window = mozilla_remote_find_window (dpy);
  else
    mozilla_remote_check_window (dpy, window);

  XSelectInput (dpy, window, (PropertyChangeMask|StructureNotifyMask));

  mozilla_remote_obtain_lock (dpy, window);

  while (*commands)
    {
      if (!strcmp (*commands, "-raise"))
	raise_p = True;
      else if (!strcmp (*commands, "-noraise"))
	raise_p = False;
      else
	status = mozilla_remote_command (dpy, window, *commands, raise_p);

      if (status != 0)
	break;
      commands++;
    }

  /* When status = 6, it means the window has been destroyed */
  /* It is invalid to free the lock when window is destroyed. */

  if ( status != 6 )
  mozilla_remote_free_lock (dpy, window);

  return status;
}


#ifdef STANDALONE

static void
usage (void)
{
  fprintf (stderr, "usage: %s [ options ... ]\n\
       where options include:\n\
\n\
       -help                     to show this message.\n\
       -display <dpy>            to specify the X server to use.\n\
       -remote <remote-command>  to execute a command in an already-running\n\
                                 Netscape process.  See the manual for a\n\
                                 list of valid commands.\n\
       -id <window-id>           the id of an X window to which the -remote\n\
                                 commands should be sent; if unspecified,\n\
                                 the first window found will be used.\n\
       -raise                    whether following -remote commands should\n\
                                 cause the window to raise itself to the top\n\
                                 (this is the default.)\n\
       -noraise                  the opposite of -raise: following -remote\n\
                                 commands will not auto-raise the window.\n\
",
	   progname);
}


void
main (int argc, char **argv)
{
  Display *dpy;
  char *dpy_string = 0;
  char **remote_commands = 0;
  int remote_command_count = 0;
  int remote_command_size = 0;
  unsigned long remote_window = 0;
  Bool sync_p = False;
  int i;

  progname = strrchr (argv[0], '/');
  if (progname)
    progname++;
  else
    progname = argv[0];

  /* Hack the -help and -version arguments before opening the display. */
  for (i = 1; i < argc; i++)
    {
      if (!strcasecmp (argv [i], "-h") ||
	  !strcasecmp (argv [i], "-help"))
	{
	  usage ();
	  exit (0);
	}
      else if (!strcmp (argv [i], "-d") ||
	       !strcmp (argv [i], "-dpy") ||
	       !strcmp (argv [i], "-disp") ||
	       !strcmp (argv [i], "-display"))
	{
	  i++;
	  dpy_string = argv [i];
	}
      else if (!strcmp (argv [i], "-sync") ||
	       !strcmp (argv [i], "-synchronize"))
	{
	  sync_p = True;
	}
      else if (!strcmp (argv [i], "-remote"))
	{
	  if (remote_command_count == remote_command_size)
	    {
	      remote_command_size += 20;
	      remote_commands =
		(remote_commands
		 ? realloc (remote_commands,
			    remote_command_size * sizeof (char *))
		 : calloc (remote_command_size, sizeof (char *)));
	    }
	  i++;
	  if (!argv[i] || *argv[i] == '-' || *argv[i] == 0)
	    {
	      fprintf (stderr, "%s: invalid `-remote' option \"%s\"\n",
		       progname, argv[i] ? argv[i] : "");
	      usage ();
	      exit (-1);
	    }
	  remote_commands [remote_command_count++] = argv[i];
	}
      else if (!strcmp (argv [i], "-raise") ||
	       !strcmp (argv [i], "-noraise"))
	{
	  char *r = argv [i];
	  if (remote_command_count == remote_command_size)
	    {
	      remote_command_size += 20;
	      remote_commands =
		(remote_commands
		 ? realloc (remote_commands,
			    remote_command_size * sizeof (char *))
		 : calloc (remote_command_size, sizeof (char *)));
	    }
	  remote_commands [remote_command_count++] = r;
	}
      else if (!strcmp (argv [i], "-id"))
	{
	  char c;
	  if (remote_command_count > 0)
	    {
	      fprintf (stderr,
		"%s: the `-id' option must precede all `-remote' options.\n",
		       progname);
	      usage ();
	      exit (-1);
	    }
	  else if (remote_window != 0)
	    {
	      fprintf (stderr, "%s: only one `-id' option may be used.\n",
		       progname);
	      usage ();
	      exit (-1);
	    }
	  i++;
	  if (argv[i] &&
	      1 == sscanf (argv[i], " %ld %c", &remote_window, &c))
	    ;
	  else if (argv[i] &&
		   1 == sscanf (argv[i], " 0x%lx %c", &remote_window, &c))
	    ;
	  else
	    {
	      fprintf (stderr, "%s: invalid `-id' option \"%s\"\n",
		       progname, argv[i] ? argv[i] : "");
	      usage ();
	      exit (-1);
	    }
	}
    }

  dpy = XOpenDisplay (dpy_string);
  if (! dpy)
    exit (-1);

  if (sync_p)
    XSynchronize (dpy, True);

  exit (mozilla_remote_commands (dpy, (Window) remote_window,
				 remote_commands));
}

#endif /* STANDALONE */
