/* $Id: remote-x.c,v 1.2 2003/10/17 11:48:16 olle Exp $ */
/***************************************************************************
 *
 *  remote-x.c - control X11 remotely - version 0.0.1
 *
 *  compile: cc -o remote-x remote-x.c -L/usr/X11R6/lib -lX11 -lXtst
 *
 *  authors: olle@nxs.se, fredrikwidlund@yahoo.com
 *
 *  usage: remote-x hostname:dpy# [window id]
 *
 *  tip: when in root window mode (no window id specified on cmdline),
 *        hold left ctrl and alt to drag viewport around screen.
 *
 *  bugs: when twm and fvwm resize/move/place windows, they grab the
 *         whole XServer while waiting for mouse input. this means we
 *         cannot reach it to send the needed mouse input. catch-22.
 *         the XServer will not respond to any client calls until
 *         *local* mouse input has ended the resize/move/place action.
 *
 ****************************************************************************/
/*

Copyright (c) 2001, Olle Segerdahl <olle@nxs.se>, Fredrik Widlund
All rights reserved.

Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

- Neither the names of the authors nor the names of any contributors may be used
  to endorse or promote products derived from this software without specific
  prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <unistd.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h>

#ifndef WIN32
#include <err.h>
#else
#define err(e,s) do{fprintf(stderr,"ERR: %s\n",s);exit(e);}while(0)
#endif

#define CONSTRAINT(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))

// colormap cache
unsigned long cache[65536][3];

int ConvertXImageDepth(Display *display, Colormap colormap, XImage *image, int dpy2depth)
{
  int i, j;

  // return zero if conversion not needed
  if (dpy2depth == image->depth)
    return 0;


  // expand 8 bpp ZPixmap to 24 bpp, using colormap
  if (dpy2depth >= 16 && image->depth == 8)
    {
      char *iptr, *optr, *ilptr, *olptr, *tmp;
      int   obperlin, padbits;

      XColor color;

      // calculate bytes per output line 
      obperlin = (image->width * 24 / 8);
      
      // calculate pixel padding bits
      padbits = (image->bytes_per_line * 8  / image->width) - image->depth;

      // setup data pointers
      ilptr = (char *) image->data;
      olptr = tmp = malloc(image->height * obperlin);
      if (!olptr)
	{ 
	  fprintf(stderr,"Couldn't allocate memory for image conversion!\n");
	  return(1);
	}

      // for each line
      for (i=0; i<image->height; i++)
	{
	  // setup temp data pointers
	  iptr = ilptr;  optr = olptr;	  
	  
	  // for each pixel
	  for (j=0; j<image->width; j++)
	    {

              // convert 8 bit pixel value to monochrome to avoid using colormap
              // optr[0] = optr[1] = (*iptr & 0x01) ? 0xff : 0x00;

	      color.pixel = (unsigned char) *iptr;

	      // lookup rbg value if not cached
	      if (cache[color.pixel][0] == -1)
		{
		  XQueryColor(display, colormap, &color);
		  cache[color.pixel][2] = color.red;
		  cache[color.pixel][1] = color.green;
		  cache[color.pixel][0] = color.blue;
		}

	      optr[2] = (unsigned char)cache[color.pixel][2];
	      optr[1] = (unsigned char)cache[color.pixel][1];
	      optr[0] = (unsigned char)cache[color.pixel][0];
	      	      
	      iptr++;
	      optr+=3;
	      
	      // skip padding - nonexistant in 8 bit?
	      //iptr +=  padbits / 8;
	      
	    }
	  
	  ilptr += image->bytes_per_line;  olptr += obperlin;
	}
      
      free(image->data);
      image->data = (char *) tmp;
      image->red_mask = 0xff0000;
      image->green_mask = 0x00ff00;
      image->blue_mask = 0x0000ff;
      image->bytes_per_line = obperlin;
      image->depth = image->bits_per_pixel = 24;
      
}


  // expand 16 bpp ZPixmap to 32/24 bpp
  if (dpy2depth >= 24 && image->depth == 16)
    {
      char *iptr, *optr, *ilptr, *olptr, *tmp;
      int   obperlin, padbits;
     
      // calculate bytes per output line 
      //obperlin = ((image->width * dpy2depth + image->bitmap_pad - 1) / image->bitmap_pad) * (image->bitmap_pad / 8);
      obperlin = (image->width * dpy2depth / 8);
      
      // calculate pixel padding bits
      padbits = (image->bytes_per_line * 8  / image->width) - image->depth;

      // setup data pointers
      ilptr = (char *) image->data;
      olptr = tmp = malloc(image->height * obperlin);
      if (!olptr)
	{ 
	  fprintf(stderr,"Couldn't allocate memory for image conversion!\n");
	  return(1);
	}
      
      // for each line
      for (i=0; i<image->height; i++)
	{
	  // setup temp data pointers
	  iptr = ilptr;  optr = olptr;
	  
	  // for each pixel
	  for (j=0; j<image->width; j++) 
	    {
	      u_int16_t *w;
	      
	      w = (u_int16_t *) iptr;
	      optr[2] = (*w & image->red_mask) >> 8;
	      optr[1] = (*w & image->green_mask) >> 3;
	      optr[0] = (*w & image->blue_mask) << 3;
	      optr+=3;
	      iptr+=2;
	      
	      // skip padding
	      iptr += (padbits / 8);
	    }
	  
	  ilptr += image->bytes_per_line;  olptr += obperlin;
	}

      free(image->data);
      image->data = (char *) tmp;
      image->red_mask = 0xff0000;
      image->green_mask = 0x00ff00;
      image->blue_mask = 0x0000ff;
      image->bytes_per_line = obperlin;
      image->depth = image->bits_per_pixel = dpy2depth;

    }


  // compress 32/24 bpp ZPixmap to 16 bpp
  if (dpy2depth == 16 && image->depth >= 24) 
    {
      char *iptr, *optr, *ilptr, *olptr, *tmp;
      int obperlin, padbits;

      // calculate bytes per output line
      obperlin = (image->width * dpy2depth) / 8;

      // calculate pixel padding bits
      padbits = (image->bytes_per_line * 8  / image->width) - image->depth;

      // set new color mask values
      image->red_mask = 0xf800;
      image->green_mask = 0x07c0;
      image->blue_mask = 0x001f;
      
      // setup data pointers
      ilptr = (char *) image->data;
      olptr = tmp = malloc(image->height * obperlin);
      if (!olptr)
	{ 
	  fprintf(stderr,"Couldn't allocate memory for image conversion!\n");
	  return(1);
	}
      
      // for each line
      for (i=0; i<image->height; i++)
	{
	  // setup temp data pointers
	  iptr = ilptr;  optr = olptr;
	  
	  // for each pixel
	  for (j=0; j<image->width; j++) 
	    {
	      u_int16_t *w;
	      
	      w = (u_int16_t *) optr;
	      *w = 0;
	      *w |= (iptr[2] << 8) & image->red_mask;
	      *w |= (iptr[1] << 3) & image->green_mask;
	      *w |= (iptr[0] >> 3) & image->blue_mask;
	      iptr+=3;
	      optr+=2;
	      
	      // skip padding
	      iptr +=  padbits / 8;
	    }
	
	  // advance data pointers to next line
	  ilptr += image->bytes_per_line;  olptr += obperlin;
	}
      
      free(image->data);
      image->data = (char *) tmp;
      image->bytes_per_line = obperlin;
      image->depth = image->bits_per_pixel = dpy2depth;

    }
  
  
  // return non-zero if conversion failed
  if (image->depth != dpy2depth)
    return 1;
  else
    return 0;

}


int main(int argc, char **argv)
{
  char *display_name;
  Display *display, *display_remote;
  Window window, window_remote;
  XImage *image;
  XSetWindowAttributes attr;
  XWindowAttributes attributes, attributes_remote;
  XEvent event;
  int x, y, bx, by;
  int remote_root, ctrl_down, alt_down;
  int event_mask, check_event, f;
  char title[128], tmp_title[128];
  
  // clear colormap cache
  for (x=0;x<65536;x++)
    cache[x][0]=-1;
  
  display_name = argv[1];

  if (argc < 2)
    {
      fprintf(stdout, "usage: %s hostname:dpy# [window id]\n\n", argv[0]);
      return(0);
    }

  // open remote display
  display_remote = XOpenDisplay(display_name);
  if (!display_remote)
    err(1, "XOpenDisplay");

  if (argc >= 3)
    {
      // in windowed mode, get window id from cmdline
      remote_root = 0;
      window_remote = strtoul(argv[2], NULL, 0);
      if (!window_remote)
	err(1,"Bad Window ID specified!");
      else
	fprintf(stderr, "Starting remote-x in Window mode, Window ID = 0x%x\n\n", (unsigned int)window_remote);
    }
  else
    {
      // in root mode, get remote root window id
      remote_root = 1;
      window_remote = DefaultRootWindow(display_remote);
      if (!window_remote)
	err(1,"Couldn't get Root Window!");
      else
	fprintf(stderr, "Starting remote-x in RootWindow mode, RootWindow ID = 0x%x\n\n", (unsigned int)window_remote);
    }

  // get remote window attributes
  XGetWindowAttributes(display_remote, window_remote, &attributes_remote);

  // check that the remote window is viewable 
  if (attributes_remote.map_state != IsViewable)
    {
      fprintf(stderr,"Window 0x%x not in Viewable state!\n", (unsigned int)window_remote);
      return(1);
    }

  // open local display 
  display = XOpenDisplay(NULL);
  if (!display)
    err(1, "XOpenDisplay");

  // create local window
  window = XCreateWindow(display, DefaultRootWindow(display), 0, 0,
		         (remote_root ? 320 : attributes_remote.width), 
		         (remote_root ? 240 : attributes_remote.height),
			 0, CopyFromParent, InputOutput, CopyFromParent, 0, &attr);
  if (!window)
    err(1, "XCreateWindow");

  // set local window to recieve keyboard and mouse events
  event_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask; // | PointerMotionMask ;
  XSelectInput(display, window, event_mask);

  // set local window WM properties
  snprintf(title, 100, "remote-x: %s in %s mode", display_name, (remote_root ? "RootWindow" : "Window"));
  title[127]=0;
  XSetStandardProperties(display, window, title, "remote-x", None, argv, argc, NULL);

  // make local window visible on screen
  XMapWindow(display, window);
 
  // get local window attributes 
  XGetWindowAttributes(display, window, &attributes);

  // warn about bad 8 bpp conversion
  if ((attributes_remote.depth == 8) && (attributes.depth != 8))
    fprintf(stderr, "WARNING! Remote display uses 8 bits per pixel. Your display uses %d bpp.\n"
	    " This program will try to convert 8 bpp images (badly) using a colormap.\n"
	    " It would probably be a good idea to change your display depth to 8 bpp.\n\n"
	    , attributes.depth);

  // start main program loop
  f = 0;
  x = 0;
  y = 0;
  bx = 0;
  by = 0;
  alt_down = 0;
  ctrl_down = 0;
  image = NULL;
  while(1)
    {
      // enter event handler
      while(1)
	{
	  // check if there are events to process
	  check_event = XCheckWindowEvent(display, window, event_mask, &event);

          // if no new events to process, continue with main program loop
	  if (!check_event)
	    break;
	 
          // handle event 
	  switch(event.type)
	    {
	    case ConfigureNotify:
	      break;
	    case ButtonPress:
	    case ButtonRelease:

	      if (remote_root)
		{
		  if (ctrl_down && alt_down)
		    {
		      // drag viewport around remote desktop
		      if (event.type == ButtonPress)
			{
			  bx = event.xbutton.x;
			  by = event.xbutton.y;
			}
		      else if (event.type == ButtonRelease)
			{
			  x += bx - event.xbutton.x;
			  y += by - event.xbutton.y;
			  x = CONSTRAINT(x, attributes_remote.x, attributes_remote.x + attributes_remote.width - attributes.width);
			  y = CONSTRAINT(y, attributes_remote.y, attributes_remote.y + attributes_remote.height - attributes.height);
			}

		    } else {
			
		      // position remote mouse pointer
		      XTestFakeMotionEvent(display_remote, -1, x + event.xbutton.x, y + event.xbutton.y, CurrentTime);

		      // send mouse button event
		      XTestFakeButtonEvent(display_remote, event.xbutton.button, (event.type == ButtonPress ? 1 : 0), CurrentTime);

		    }

		} else {
		  // forward mouse event to remote window 
		  event.xbutton.x_root = attributes_remote.x + event.xbutton.x;
		  event.xbutton.y_root = attributes_remote.y + event.xbutton.y;
		  event.xany.display = display_remote;
		  event.xany.window = window_remote;
		  XSendEvent(display_remote, window_remote, 1, event_mask, &event);
		}

	      break;
	    case KeyPress:
	    case KeyRelease:
	      // save state of ctrl&alt modifiers
	      if (event.xkey.keycode == 0x25) // this should be a keycode symbol!
		ctrl_down = (event.type == KeyPress) ? 1 : 0;
	      if (event.xkey.keycode == 0x40) // this should be a keycode symbol! 
		alt_down  = (event.type == KeyPress) ? 1 : 0;

	      // get keybord focus
	      XSetInputFocus(display_remote, window_remote, RevertToParent, CurrentTime);

	      // send keyevent to remote window
	      XTestFakeKeyEvent(display_remote, event.xkey.keycode, (event.type == KeyPress ? 1 : 0), CurrentTime);

	      break;
	    case MotionNotify:
	      if (remote_root)
		{
		  if (!(ctrl_down && alt_down))
		    // move remote mouse pointer
		    XTestFakeMotionEvent(display_remote, -1, x + event.xmotion.x, y + event.xmotion.y, CurrentTime);
		  //fprintf(stderr, "\rMotion: x=%d,y=%d", x + event.xmotion.x, y + event.xmotion.y);

		} else {
		  // forward mouse event to remote window 
		  event.xany.display = display_remote;
		  event.xany.window = window_remote;
		  XSendEvent(display_remote, window_remote, 1, event_mask, &event);

		}

	      break;
	    default:
	      break;
	    }
      
	  // flush X11 event buffers
	  XSync(display_remote, False);
	}

      if (remote_root)
        {
	  // resize local window if too large for image
	  if (attributes.width > attributes_remote.width) 
	    XResizeWindow(display, window, attributes_remote.width, attributes.height);
	  if (attributes.height > attributes_remote.height)
	    XResizeWindow(display, window, attributes.width, attributes_remote.height);

        } else { 
	  // resize local window if remote window size has changed
	  if ((attributes.width != attributes_remote.width) || (attributes.height != attributes_remote.height))
	    XResizeWindow(display, window, attributes_remote.width, attributes_remote.height);
        }

      // get local and remote window attributes 
      XGetWindowAttributes(display, window, &attributes);
      XGetWindowAttributes(display_remote, window_remote, &attributes_remote);

      // get updated image of remote window
      image = XGetImage(display_remote, window_remote, x, y,
			CONSTRAINT(attributes.width, 0, attributes_remote.width),
                        CONSTRAINT(attributes.height, 0, attributes_remote.height),
			AllPlanes, ZPixmap);

      // convert image bitdepth
      if (ConvertXImageDepth(display_remote, attributes_remote.colormap, image, attributes.depth))
	{
	  fprintf(stderr, "ERROR! The remote display uses %d bits per pixel. Your display uses %d bpp.\n"
		  " This program does not currently have the means to convert %d bpp to %d bpp.\n"
		  " It would be a VERY good idea to change your display depth to %d bpp.\n\n",
		  image->depth, attributes.depth,
		  image->depth, attributes.depth,
		  image->depth);
	  return 1;
	}

      // update local window with new image
      XPutImage(display, window, DefaultGC(display, DefaultScreen(display)), image, 0, 0, 0, 0, attributes.width, attributes.height);

      // delete image of remote window
      if (image)
	XDestroyImage(image);

      // update frame counter in window title
      sprintf(tmp_title, "%s (frames: %u)", title, f++);
      XStoreName(display, window, tmp_title);

    }
  
  return(0);
}
