/* xscreensaver, Copyright (c) 1992, 1993, 1994, 1997
 *  Jamie Zawinski <jwz@netscape.com>
 *
 * 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.
 */

/* This file contains code for grabbing an image of the screen to hack its
   bits.  This is a little tricky, since doing this involves the need to tell
   the difference between drawing on the actual root window, and on the fake
   root window used by the screensaver, since at this level the illusion 
   breaks down...
 */

#include "utils.h"

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

#ifdef HAVE_XMU
# ifndef VMS
#  include <X11/Xmu/WinUtil.h>
# else  /* VMS */
#  include <Xmu/WinUtil.h>
# endif /* VMS */
#endif

#include "usleep.h"
#include "colors.h"
#include "grabscreen.h"

#include "vroot.h"
#undef RootWindowOfScreen
#undef RootWindow
#undef DefaultRootWindow


#ifdef HAVE_READ_DISPLAY_EXTENSION
# include "visual.h"
# include <X11/extensions/readdisplay.h>
  static Bool read_display (Screen *, Window, Pixmap, Bool);
#endif /* HAVE_READ_DISPLAY_EXTENSION */


static void copy_default_colormap_contents (Screen *, Colormap, Visual *);


static Bool
MapNotify_event_p (Display *dpy, XEvent *event, XPointer window)
{
  return (event->xany.type == MapNotify &&
	  event->xvisibility.window == (Window) window);
}

#ifdef DEBUG
extern char *progname;
#endif /* DEBUG */


static void
raise_window(Display *dpy, Window window, Bool dont_wait)
{
#ifdef DEBUG
  fprintf(stderr, "%s: raising window 0x%08lX (%s)\n",
	  progname, (unsigned long) window,
	  (dont_wait ? "not waiting" : "waiting"));
#endif /* DEBUG */

  if (! dont_wait)
    {
      XWindowAttributes xgwa;
      XSizeHints hints;
      long supplied = 0;
      memset(&hints, 0, sizeof(hints));
      XGetWMNormalHints(dpy, window, &hints, &supplied);
      XGetWindowAttributes (dpy, window, &xgwa);
      hints.x = xgwa.x;
      hints.y = xgwa.y;
      hints.width = xgwa.width;
      hints.height = xgwa.height;
      hints.flags |= (PPosition|USPosition|PSize|USSize);
      XSetWMNormalHints(dpy, window, &hints);
    }

  XMapRaised(dpy, window);

  if (! dont_wait)
    {
      XEvent event;
      XIfEvent (dpy, &event, MapNotify_event_p, (XPointer) window);
      XSync (dpy, True);
    }
}


static Bool
xscreensaver_window_p (Display *dpy, Window window)
{
  Atom type;
  int format;
  unsigned long nitems, bytesafter;
  char *version;
  if (XGetWindowProperty (dpy, window,
			  XInternAtom (dpy, "_SCREENSAVER_VERSION", False),
			  0, 1, False, XA_STRING,
			  &type, &format, &nitems, &bytesafter,
			  (unsigned char **) &version)
      == Success
      && type != None)
    return True;
  return False;
}



static XErrorHandler old_ehandler = 0;
static int
BadWindow_ehandler (Display *dpy, XErrorEvent *error)
{
  if (error->error_code == BadWindow || error->error_code == BadDrawable)
    return 0;
  else if (!old_ehandler)
    abort();
  else
    return (*old_ehandler) (dpy, error);
}


/* Install the colormaps of all visible windows, deepest first.
   This should leave the colormaps of the topmost windows installed
   (if only N colormaps can be installed at a time, then only the
   topmost N windows will be shown in the right colors.)
 */
static void
install_screen_colormaps (Screen *screen)
{
  int i;
  Display *dpy = DisplayOfScreen (screen);
  Window vroot, real_root;
  Window parent, *kids = 0;
  unsigned int nkids = 0;

  XSync (dpy, False);
  old_ehandler = XSetErrorHandler (BadWindow_ehandler);

  vroot = VirtualRootWindowOfScreen (screen);
  if (XQueryTree (dpy, vroot, &real_root, &parent, &kids, &nkids))
    for (i = 0; i < nkids; i++)
      {
	XWindowAttributes xgwa;
	Window client;
#ifdef HAVE_XMU
	/* #### need to put XmuClientWindow() in xmu.c, sigh... */
	if (! (client = XmuClientWindow (dpy, kids[i])))
#endif
	  client = kids[i];
	xgwa.colormap = 0;
	XGetWindowAttributes (dpy, client, &xgwa);
	if (xgwa.colormap && xgwa.map_state == IsViewable)
	  XInstallColormap (dpy, xgwa.colormap);
      }
  XInstallColormap (dpy, DefaultColormapOfScreen (screen));
  XSync (dpy, False);
  XSetErrorHandler (old_ehandler);
  XSync (dpy, False);

  if (kids)
    XFree ((char *) kids);
}


void
grab_screen_image (Screen *screen, Window window)
{
  Display *dpy = DisplayOfScreen (screen);
  XWindowAttributes xgwa;
  Window real_root = XRootWindowOfScreen (screen);  /* not vroot */
  Bool root_p = (window == real_root);
  Bool saver_p = xscreensaver_window_p (dpy, window);
  Bool grab_mouse_p = False;
  int unmap_time = 0;

  if (saver_p)
    /* I think this is redundant, but just to be safe... */
    root_p = False;

  if (saver_p)
    /* The only time grabbing the mouse is important is if this program
       is being run while the saver is locking the screen. */
    grab_mouse_p = True;

  if (!root_p)
    {
      if (saver_p)
	unmap_time = 2500000;  /* 2 1/2 seconds */
      else
	unmap_time =  660000;  /* 2/3rd second */
    }

#ifdef DEBUG
  fprintf(stderr,
	  "\n%s: window 0x%08lX root: %d saver: %d grab: %d wait: %.1f\n",
	  progname, (unsigned long) window,
	  root_p, saver_p, grab_mouse_p, ((double)unmap_time)/1000000.0);
  {
    XWindowAttributes xgwa2;
    XGetWindowAttributes (dpy, window, &xgwa2);
    fprintf(stderr, "%s: ", progname);
    describe_visual(stderr, screen, xgwa2.visual);
  }
#endif /* DEBUG */

  if (!root_p)
    XSetWindowBackgroundPixmap (dpy, window, None);

  if (grab_mouse_p)
    {
      /* prevent random viewer of the screen saver (locker) from messing
	 with windows.   We don't check whether it succeeded, because what
	 are our options, really... */
      XGrabPointer (dpy, real_root, True, ButtonPressMask|ButtonReleaseMask,
		    GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
      XGrabKeyboard (dpy, real_root, True, GrabModeSync, GrabModeAsync,
		     CurrentTime);
    }

  if (unmap_time > 0)
    {
      XUnmapWindow (dpy, window);
      install_screen_colormaps (screen);
      XSync (dpy, True);
      usleep(unmap_time); /* wait for everyone to swap in and handle exposes */
    }

  XGetWindowAttributes (dpy, window, &xgwa);

  if (!root_p)
    {
#ifdef HAVE_READ_DISPLAY_EXTENSION
      if (! read_display(screen, window, 0, saver_p))
#endif /* HAVE_READ_DISPLAY_EXTENSION */
	{
#if defined(HAVE_READ_DISPLAY_EXTENSION) && defined(DEBUG)
	  fprintf(stderr, "%s: read_display() failed\n", progname);
#endif /* DEBUG */
	  copy_default_colormap_contents (screen, xgwa.colormap, xgwa.visual);
	  raise_window(dpy, window, saver_p);
	}
    }
  else  /* root_p */
    {
      Pixmap pixmap;
      XWindowAttributes xgwa;
      XGetWindowAttributes(dpy, window, &xgwa);
      pixmap = XCreatePixmap(dpy, window, xgwa.width, xgwa.height, xgwa.depth);

#ifdef HAVE_READ_DISPLAY_EXTENSION
      if (! read_display(screen, window, pixmap, True))
#endif
	{
	  Window real_root = XRootWindowOfScreen (xgwa.screen); /* not vroot */
	  XGCValues gcv;
	  GC gc;

#if defined(HAVE_READ_DISPLAY_EXTENSION) && defined(DEBUG)
	  fprintf(stderr, "%s: read_display() failed\n", progname);
#endif /* DEBUG */

	  copy_default_colormap_contents (screen, xgwa.colormap, xgwa.visual);

	  gcv.function = GXcopy;
	  gcv.subwindow_mode = IncludeInferiors;
	  gc = XCreateGC (dpy, window, GCFunction | GCSubwindowMode, &gcv);
	  XCopyArea (dpy, real_root, pixmap, gc,
		     xgwa.x, xgwa.y, xgwa.width, xgwa.height, 0, 0);
	  XFreeGC (dpy, gc);
	}
      XSetWindowBackgroundPixmap (dpy, window, pixmap);
      XFreePixmap (dpy, pixmap);
    }

  if (grab_mouse_p)
    {
      XUngrabPointer (dpy, CurrentTime);
      XUngrabKeyboard (dpy, CurrentTime);
    }

  XSync (dpy, True);
}


/* When we are grabbing and manipulating a screen image, it's important that
   we use the same colormap it originally had.  So, if the screensaver was
   started with -install, we need to copy the contents of the default colormap
   into the screensaver's colormap.
 */
static void
copy_default_colormap_contents (Screen *screen,
				Colormap to_cmap,
				Visual *to_visual)
{
  Display *dpy = DisplayOfScreen (screen);
  Visual *from_visual = DefaultVisualOfScreen (screen);
  Colormap from_cmap = XDefaultColormapOfScreen (screen);

  XColor *old_colors, *new_colors;
  unsigned long *pixels;
  XVisualInfo vi_in, *vi_out;
  int out_count;
  int from_cells, to_cells, max_cells, got_cells;
  int i;

  if (from_cmap == to_cmap)
    return;

  vi_in.screen = XScreenNumberOfScreen (screen);
  vi_in.visualid = XVisualIDFromVisual (from_visual);
  vi_out = XGetVisualInfo (dpy, VisualScreenMask|VisualIDMask,
			   &vi_in, &out_count);
  if (! vi_out) abort ();
  from_cells = vi_out [0].colormap_size;
  XFree ((char *) vi_out);

  vi_in.screen = XScreenNumberOfScreen (screen);
  vi_in.visualid = XVisualIDFromVisual (to_visual);
  vi_out = XGetVisualInfo (dpy, VisualScreenMask|VisualIDMask,
			   &vi_in, &out_count);
  if (! vi_out) abort ();
  to_cells = vi_out [0].colormap_size;
  XFree ((char *) vi_out);

  max_cells = (from_cells > to_cells ? to_cells : from_cells);

  old_colors = (XColor *) calloc (sizeof (XColor), max_cells);
  new_colors = (XColor *) calloc (sizeof (XColor), max_cells);
  pixels = (unsigned long *) calloc (sizeof (unsigned long), max_cells);
  for (i = 0; i < max_cells; i++)
    old_colors[i].pixel = i;
  XQueryColors (dpy, from_cmap, old_colors, max_cells);

  got_cells = max_cells;
  allocate_writable_colors (dpy, to_cmap, pixels, &got_cells);
  XStoreColors (dpy, to_cmap, old_colors, got_cells);

#ifdef DEBUG
  fprintf(stderr, "%s: installing copy of default colormap\n", progname);
#endif /* DEBUG */

  free (old_colors);
  free (new_colors);
  free (pixels);
}



/* The SGI ReadDisplay extension.
   This extension lets you get back a 24-bit image of the screen, taking into
   account the colors with which all windows are *currently* displayed, even
   if those windows have different visuals.  Without this extension, presence
   of windows with different visuals or colormaps will result in technicolor
   when one tries to grab the screen image.
 */

#ifdef HAVE_READ_DISPLAY_EXTENSION

static void make_cubic_colormap (Screen *, Window, Visual *);

static Bool
read_display (Screen *screen, Window window, Pixmap into_pixmap,
	      Bool dont_wait)
{
  Display *dpy = DisplayOfScreen (screen);
  XWindowAttributes xgwa;
  int rd_event_base = 0;
  int rd_error_base = 0;
  unsigned long hints = 0;
  XImage *image = 0;
  XGCValues gcv;
  int class;
  GC gc;
  Bool install_p = False;

  /* Check to see if the server supports the extension, and bug out if not.
   */
  if (! XReadDisplayQueryExtension (dpy, &rd_event_base, &rd_error_base))
    return False;

  /* If this isn't a visual we know how to handle, bug out.  We handle:
      = TrueColor in depths 8, 12, 16, and 32;
      = PseudoColor and DirectColor in depths 8 and 12.
   */
  XGetWindowAttributes(dpy, window, &xgwa);
  class = visual_class (screen, xgwa.visual);
  if (class == TrueColor)
    {
      if (xgwa.depth != 8  && xgwa.depth != 12 && xgwa.depth != 16 &&
	  xgwa.depth != 24 && xgwa.depth != 32)
	return False;
    }
  else if (class == PseudoColor || class == DirectColor)
    {
      if (xgwa.depth != 8 && xgwa.depth != 12)
	return False;
      else
	/* Install a colormap that makes this visual behave like
	   a TrueColor visual of the same depth. */
	install_p = True;
    }


  /* Try and read the screen.
   */
  hints = (XRD_TRANSPARENT | XRD_READ_POINTER);
  image = XReadDisplay (dpy, window, xgwa.x, xgwa.y, xgwa.width, xgwa.height,
			hints, &hints);
  if (!image)
    return False;
  if (!image->data)
    {
      XDestroyImage(image);
      return False;
    }

  /* Uh, this can't be right, can it?  But it's necessary.  X sucks.
     If the visual is of depth 24, but the image came back as depth 32,
     hack it to be 24 lest we get a BadMatch from XPutImage.  (I presume
     I'm expected to look at the server's pixmap formats or some such
     nonsense... but fuck it.
   */
  if (xgwa.depth == 24 && image->depth == 32)
    image->depth = 24;

  /* If the visual of the window/pixmap into which we're going to draw is
     less deep than the screen itself, then we need to convert the grabbed bits
     to match the depth by clipping off the less significant bit-planes of each
     color component.
   */
  if (image->depth > xgwa.depth)
    {
      int x, y;
      /* We use the same image->data in both images -- that's ok, because
	 since we're reading from B and writing to A, and B uses more bytes
	 per pixel than A, the write pointer won't overrun the read pointer.
       */
      XImage *image2 = XCreateImage (dpy, xgwa.visual, xgwa.depth,
				     ZPixmap, 0, image->data,
				     xgwa.width, xgwa.height,
				     8, 0);
      if (!image2)
	return False;

#ifdef DEBUG
      fprintf(stderr, "%s: converting from depth %d to depth %d\n",
	      progname, image->depth, xgwa.depth);
#endif /* DEBUG */

      for (y = 0; y < image->height; y++)
	for (x = 0; x < image->width; x++)
	  {
	    /* #### really these shift values should be determined from the
	       mask values -- but that's a pain in the ass, and anyway,
	       this is an SGI-specific extension so hardcoding assumptions
	       about the SGI server's behavior isn't *too* heinous... */
	    unsigned long pixel = XGetPixel(image, x, y);
	    unsigned int r = (pixel & image->red_mask);
	    unsigned int g = (pixel & image->green_mask) >> 8;
	    unsigned int b = (pixel & image->blue_mask) >> 16;

	    if (xgwa.depth == 8)
	      pixel = ((r >> 5) | ((g >> 5) << 3) | ((b >> 6) << 6));
	    else if (xgwa.depth == 12)
	      pixel = ((r >> 4) | ((g >> 4) << 4) | ((b >> 4) << 8));
	    else if (xgwa.depth == 16)
	      pixel = ((r >> 3) | ((g >> 3) << 5) | ((b >> 3) << 10));
	    else
	      abort();

	    XPutPixel(image2, x, y, pixel);
	  }
      image->data = 0;
      XDestroyImage(image);
      image = image2;
    }


  /* Now actually put the bits into the window or pixmap -- note the design
     bogosity of this extension, where we've been forced to take 24 bit data
     from the server to the client, and then push it back from the client to
     the server, *without alteration*.  We should have just been able to tell
     the server, "put a screen image in this drawable", instead of having to
     go through the intermediate step of converting it to an Image.  Geez.
     (Assuming that the window is of screen depth; we happen to handle less
     deep windows, but that's beside the point.)
   */
  gcv.function = GXcopy;
  gc = XCreateGC (dpy, window, GCFunction, &gcv);

  if (into_pixmap)
    {
      gcv.function = GXcopy;
      gc = XCreateGC (dpy, into_pixmap, GCFunction, &gcv);
      XPutImage (dpy, into_pixmap, gc, image, 0, 0, 0, 0,
		 xgwa.width, xgwa.height);
    }
  else
    {
      gcv.function = GXcopy;
      gc = XCreateGC (dpy, window, GCFunction, &gcv);

      /* Ok, now we'll be needing that window on the screen... */
      raise_window(dpy, window, dont_wait);

      /* Plop down the bits... */
      XPutImage (dpy, window, gc, image, 0, 0, 0, 0, xgwa.width, xgwa.height);
    }
  XFreeGC (dpy, gc);

  if (image->data)
    {
      free(image->data);
      image->data = 0;
    }
  XDestroyImage(image);

  if (install_p)
    make_cubic_colormap (screen, window, xgwa.visual);

  return True;
}

static void
make_cubic_colormap (Screen *screen, Window window, Visual *visual)
{
  Display *dpy = DisplayOfScreen (screen);
  Colormap cmap = XCreateColormap(dpy, window, visual, AllocAll);
  int nr, ng, nb, cells;
  int r, g, b;
  int depth;
  XColor colors[4097];
  int i;

  depth = visual_depth(screen, visual);
  switch (depth)
    {
    case 8:  nr = 3; ng = 3; nb = 2; cells = 256;  break;
    case 12: nr = 4; ng = 4; nb = 4; cells = 4096; break;
    default: abort(); break;
    }

  memset(colors, 0, sizeof(colors));
  for (i = 0; i < cells; i++)
    {
      colors[i].flags = DoRed|DoGreen|DoBlue;
      colors[i].red = colors[i].green = colors[i].blue = 0;
    }

  for (r = 0; r < (1 << nr); r++)
    for (g = 0; g < (1 << ng); g++)
      for (b = 0; b < (1 << nb); b++)
	{
	  i = (r | (g << nr) | (b << (nr + ng)));
	  colors[i].pixel = i;
	  if (depth == 8)
	    {
	      colors[i].red   = ((r << 13) | (r << 10) | (r << 7) |
				 (r <<  4) | (r <<  1));
	      colors[i].green = ((g << 13) | (g << 10) | (g << 7) |
				 (g <<  4) | (g <<  1));
	      colors[i].blue  = ((b << 14) | (b << 12) | (b << 10) |
				 (b <<  8) | (b <<  6) | (b <<  4) |
				 (b <<  2) | b);
	    }
	  else
	    {
	      colors[i].red   = (r << 12) | (r << 8) | (r << 4) | r;
	      colors[i].green = (g << 12) | (g << 8) | (g << 4) | g;
	      colors[i].blue  = (b << 12) | (b << 8) | (b << 4) | b;
	    }
	}

#ifdef DEBUG
  fprintf(stderr, "%s: installing cubic colormap\n", progname);
#endif /* DEBUG */

  XStoreColors (dpy, cmap, colors, cells);
  XSetWindowColormap (dpy, window, cmap);

  /* Gag, install the colormap.
     This is definitely right in the `if xscreensaver_window_p' case, since
     it will never get installed otherwise.  But, if we don't do it
     unconditionally, then the new colormap won't get installed until the
     window (re-)gains focus.  It's generally very antisocial to install
     the colormap of a non-OverrideRedirect window (that task belongs to
     the WM) and if we were being kosher, we would only install this cmap
     if the old cmap was already installed (or perhaps, if the window had
     focus.)  But, since this extension only exists on SGIs, and since SGIs
     can handle four colormaps at once, let's go ahead and install it all
     the time, so that even if the window pops up and has never had focus,
     it will still display in the proper colors.
   */
  XInstallColormap (dpy, cmap);
}


#endif /* HAVE_READ_DISPLAY_EXTENSION */
