/***************************************
XWindow Support for QuickCam
by Paul Chinn <loomer@svpal.org>
Modified by Scott Laird <scott@laird.com>
 
I took a bunch of this code from the source for VGB
"Virtual GameBoy" emulator by Marat Fayzullin and
Elan Feingold
*****************/
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <unistd.h>

/** MIT Shared Memory Extension for X ************************/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <X11/extensions/XShm.h>
XShmSegmentInfo SHMInfo;


/** QuickCam include files */
#include "qcam.h"
#include "qcam-os.h"
#include "qcamip.h"
 
/** Various X-related variables ******************************/

Screen *screen;
Display *disp;
Window root,win;
XColor col;
Colormap cmap;
XImage *ximage;
XEvent ev;
GC gc;
int screen_num;
unsigned long white,black;
int xstarted=0; 
int quit=0;
int autocalibrate = 0;
int darkcurrent = 0;
int edge_detect = 0;

static int view_scale = 1;
static int view_interpolate = 0;

/* Set a flag to exit the loop at the end */

void quitprogram(int foo)
{
  quit=1;
}
 
/** Initialize xwindows, and prepare a shared memory buffer for
 the image.  Returns pointer to shared memory buffer. */
 
char *InitXWindows(struct qcam *q)
{
  XGCValues values;
  XSizeHints hints;
  XWMHints wmhints;
  int width, height;
  int xDepth = 8;
  char *sbuf;
  char *window_name="QuickCam";
  char *icon_name="QuickCam";
  XTextProperty windowName, iconName;

  width=q->width/q->transfer_scale;
  width *= view_scale;
  height=q->height/q->transfer_scale;
  height *= view_scale;

  disp=XOpenDisplay(NULL);
  if(!disp) {printf("open display failed\n"); return NULL;}
  
  screen=DefaultScreenOfDisplay(disp);
  screen_num=DefaultScreen(disp);
  white=XWhitePixel(disp,screen_num);
  black=XBlackPixel(disp,screen_num);
  
  root=DefaultRootWindow(disp);
  xDepth = DefaultDepth(disp, screen_num);

  win=XCreateSimpleWindow(disp,root,0,0,width,height,0,white,black);
  if(!win) {  
    printf("create window failed\n");
    return(NULL); 
  }
  
  /* tell window manager about our window */
  hints.flags=PSize|PMaxSize|PMinSize;
  hints.min_width=hints.max_width=hints.width=width;
  hints.min_height=hints.max_height=hints.height=height;
  wmhints.input=True;
  wmhints.flags=InputHint;

  XStringListToTextProperty(&window_name, 1 ,&windowName);
  XStringListToTextProperty(&icon_name, 1 ,&iconName);


  XSetWMProperties(disp, win, 
		   &windowName, &iconName,
		   NULL, 0,
		   &hints, &wmhints, NULL);
  
  /*  XStoreName(disp, win, "QuickCam"); */
  XSelectInput(disp, win, ExposureMask);
  XMapRaised(disp, win);
  XNextEvent(disp, &ev);
  
  gc = XCreateGC(disp, win, 0, &values);
  
  ximage = XShmCreateImage(disp, DefaultVisual(disp, screen_num), 
			   xDepth, ZPixmap, NULL, &SHMInfo, width, height);
  if(!ximage) {
    printf("CreateImage Failed\n");
    return(NULL);
  }
 
  SHMInfo.shmid=shmget(IPC_PRIVATE, 
		       ximage->bytes_per_line*ximage->height,
		       IPC_CREAT|0777);

  if(SHMInfo.shmid < 0) {
    perror("shmget failed:");
    return (NULL);
  }
 
  sbuf = ximage->data = SHMInfo.shmaddr = shmat(SHMInfo.shmid, 0, 0);
  XShmAttach(disp, &SHMInfo);
  signal(SIGHUP, quitprogram); 
  signal(SIGINT, quitprogram);
  signal(SIGQUIT, quitprogram); 
  signal(SIGTERM, quitprogram);
  xstarted=1;
  return(sbuf);
}


void ExitXWindows(void)
{
  if(xstarted) {
    XShmDetach(disp, &SHMInfo);
    if(SHMInfo.shmaddr)
      shmdt(SHMInfo.shmaddr);
    if(SHMInfo.shmid > 0)
      shmctl(SHMInfo.shmid, IPC_RMID, 0);
  }
}


int *xqc_createpalette(Colormap cmap)
{
  int *pal;
  int i;

  pal=malloc(sizeof(int[64]));
  
  for(i=0; i<64; i++) {
    col.red =col.green = col.blue = i * 1024;
    if (!XAllocColor(disp, cmap, &col)) {
      fprintf(stderr,"XAllocColor failed on %d\n",i);
    }
    pal[i] = col.pixel;
  }
  return pal;
}

__inline__ static int
to_colortable(const int bpp, const int val, const int * const colortable)
{
  switch(bpp) {
    case 4: return colortable[(val<<2)+1];
    case 6: return colortable[val];
  }
  return colortable[0];
}



void xqc_sync(struct qcam *q, int *colortable, int autoexposure, int histogram)
{
  int i, j;
  int vx, vy;
  int pixel;
  int width, height;
  unsigned char map[MAX_HEIGHT][MAX_WIDTH];
  scanbuf *scan;

  /* true width & height after scaling */
  width = q->width / q->transfer_scale;
  height = q->height / q->transfer_scale;

  qc_set(q);
  if (autocalibrate) qc_calibrate(q);

  scan=qc_scan(q);

  if (darkcurrent)
    fixdark(q, scan);

  if (autoexposure)
    qcip_autoexposure(q, scan);

  if (edge_detect)
    qc_edge_detect(q, scan, -1);

  if (histogram)
    qcip_display_histogram(q, scan);

  if (!view_interpolate || view_scale == 1) {
    /* Easy way -- duplicate pixels rather than interpolate */
    for (i = width - 1; i >= 0; i--) {
      for (j = height - 1; j >= 0; j--) {
	pixel = to_colortable(q->bpp, scan[j*width + i], colortable);
	for (vx = 0; vx < view_scale; vx++) {
	  for (vy = 0; vy < view_scale; vy++) {
	    XPutPixel(ximage, i*view_scale+vx, j*view_scale+vy, pixel);
	  }
	}
      }
    }
  } else {
    /* Hard way -- interpolation */
    for (j = 0; j < height-1; j++) {
      for (i = 0; i < width-1; i++) {
	for (vy = 0; vy < view_scale; vy++) {
	  for (vx = 0; vx < view_scale; vx++) {
	    pixel =
		scan[j*width + i] * (view_scale - vx) * (view_scale - vy) +
		scan[j*width + i+1] * vx * (view_scale - vy) + 
		scan[(j+1)*width + i] * (view_scale - vx) * vy  +
		scan[(j+1)*width + i+1] * vx * vy;

	    if (q->bpp == 4) {
	      /* We've got a 64-shade color table, might as well use it */
	      pixel *= (64/16);
	    }
	    pixel /= (view_scale * view_scale);
	    pixel = to_colortable(6, pixel, colortable);

	    XPutPixel(ximage, i*view_scale+vx, j*view_scale+vy, pixel);
	  }
	}
      }
    }
    for (j = 0; j < height; j++)
      XPutPixel(ximage, (width-1)*view_scale, j*view_scale, map[j][width-1]);
    for (i = 0; i < width; i++)
      XPutPixel(ximage, i*view_scale, (height-1)*view_scale, map[height-1][i]);
  }

  free(scan);
  
  XShmPutImage(disp, win, gc, ximage, 0,0,0,0,
	(width-1)*view_scale, (height-1)*view_scale, False);
  XFlush(disp);
}


void usage(void)
{
  fprintf(stderr,"Usage:\n");
  fprintf(stderr,"  xqcam [options]\n");
  fprintf(stderr,"    Options:\n");
  fprintf(stderr,"      -x width   Set width\n");
  fprintf(stderr,"      -y height  Set height\n");
  fprintf(stderr,"      -p port    Set port\n");
  fprintf(stderr,"      -B bpp     Set bits per pixel\n");

  fprintf(stderr,"      -c val     Set contrast\n");
  fprintf(stderr,"      -w val     Set white balance\n");
  fprintf(stderr,"      -W         Auto-set white balance\n");
  fprintf(stderr,"      -b val     Set brightness\n");
  fprintf(stderr,"      -E \"vals\"  Autoexposure mode, parameters required\n");
  fprintf(stderr,"      -D         Remove dark speckling\n");
  fprintf(stderr,"      -e         Edge detection\n");

  fprintf(stderr,"      -H         Display Histogram\n");
  fprintf(stderr,"      -s val     Set scaling factor (1, 2, or 4)\n");
  fprintf(stderr,"      -t val     Set top line of scan\n");
  fprintf(stderr,"      -l val     Set left column of scan\n");
  fprintf(stderr,"      -d val     seconds delay between scans\n");
  fprintf(stderr,"      -V         Show version information\n");
  fprintf(stderr,"      -v         Verbose output\n");
  fprintf(stderr,"      -C         Use private colormap\n");
  fprintf(stderr,"      -r         Release the lock after each scan\n");
  fprintf(stderr,"      -u         Force unidirectional mode\n");

  fprintf(stderr,"      -S val     Set viewer scale\n");
  fprintf(stderr,"      -i         Interpolate viewer\n");

}


int main(int argc, char **argv)
{
  int arg;
  extern char *optarg;
  struct qcam *q;
  int *colortable;
  char *sbuf;
  int verbose=0;
  int privatecmap=0;
  int release_lock=0;
  int autoexposure = 0;
  int ae_arg_cnt;
  int ae_mode, ae_lum_target, ae_lum_tolerance;
  int ae_lum_std_target, ae_lum_std_tolerance;
  int histogram = 0;
  int delay = 0;
  Colormap cmap;
  struct timeval tv1, tv2;
  double framerate=0,fr;

  colortable=malloc(sizeof(int[64]));

  if(geteuid()) {
    fprintf(stderr,"%s: Must be installed SUID or run as root.  Exiting.\n",
	    argv[0]);
    exit(1);
  }

  q=qc_init();
  qc_initfile(q, 0);

  /* Read command line */

  while((arg=getopt(argc,argv,"WhCvx:y:p:b:B:c:t:l:s:w:d:E:HVruDeS:i"))>0) { 
    switch (arg) {
    case 'x':
      q->width=atoi(optarg);
      break;
    case 'y':
      q->height=atoi(optarg);
      break;
    case 'p':
      if (!getuid())
	q->port=strtol(optarg,NULL,0);
      break;
    case 'B':
      q->bpp=atoi(optarg);
      break;
    case 'b':
      q->brightness=atoi(optarg);
      break;
    case 'c':
      q->contrast=atoi(optarg);
      break;
    case 'w':
      q->whitebal=atoi(optarg);
      break;
    case 'd':
      delay=atoi(optarg);
      break;
    case 'V':
      fprintf(stderr,"%s: Version %s\n",argv[0], VERSION);
      exit(0);
      break;
    case 'h':
      usage();
      exit(0);
      break;
    case 'C':
      privatecmap=1;
      break;
    case 'v':
      verbose=1;
      break;
    case 's':
      if (qc_settransfer_scale(q, atoi(optarg)))
	fprintf(stderr, "Bad scaling factor, valid values are 1, 2, or 4\n");
      break;
    case 't':
      if (qc_settop(q, atoi(optarg)))
	fprintf(stderr, "Bad top line, valid values are 1 - 243\n");
      break;
    case 'l':
      if (qc_setleft(q, atoi(optarg)))
	fprintf(stderr, "Bad left column, valid values are 2 - 336, and must be even\n");
      break;
    case 'W':
      autocalibrate = 1;
      break;
    case 'E':
      autoexposure = 1;
      ae_arg_cnt = sscanf(optarg, "%d %d %d %d %d", &ae_mode,
			  &ae_lum_target, &ae_lum_tolerance,
			  &ae_lum_std_target, &ae_lum_std_tolerance);
      switch (ae_arg_cnt) {
      case 5:
	if (qcip_set_luminance_std_tolerance(q, ae_lum_std_tolerance))
	  fprintf(stderr, "Invalid luminance std tolerance.\n");
      case 4:
	if (qcip_set_luminance_std_target(q, ae_lum_std_target))
	  fprintf(stderr, "Invalid luminance std target.\n");
      case 3:
	if (qcip_set_luminance_tolerance(q, ae_lum_tolerance))
	  fprintf(stderr, "Invalid luminance tolerance.\n");
      case 2:
	if (qcip_set_luminance_target(q, ae_lum_target))
	  fprintf(stderr, "Invalid luminance target.\n");
      case 1:
	if (qcip_set_autoexposure_mode(ae_mode))
	  fprintf(stderr, "Invalid autoexposure mode.\n");
	break;
      default:
	  fprintf(stderr, "Invalid arguments for auto exposure.\n");
	  exit(1);
	break;
      }
      break;
    case 'H':
      histogram = 1;
      break;
    case 'r':
      release_lock=1;
      break;
    case 'u':
      qc_forceunidir(q);
      break;
    case 'D':
      darkcurrent = 1;
      break;
    case 'e':
      edge_detect = 1;
      break;
    case 'S':
      view_scale = atoi(optarg);
      if (view_scale < 1 || view_scale > 15) {
	view_scale = 1;
	fprintf(stderr, "Bad view_scale, using default.\n");
      }
      break;
    case 'i':
      view_interpolate = 1;
      break;
    default:
      fprintf(stderr,"%s: Unknown option or error in option\n",argv[0]);
      usage();
      exit(1);
      break;
    }
  }

  switch (q->bpp) {
  case 4: 
  case 6: break;
  default:
    fprintf(stderr,"%s: Error: Unsupported bit depth\n",argv[0]);
    exit(1);
  }

  if (q->width % (20 * q->transfer_scale) != 0) {
    fprintf(stderr, "Warning: width must be multiple of 20*transfer_scale.\n");
  }

  /* Attempt to get permission to access IO ports.  Must be root */

  if (qc_open(q)) {
    fprintf(stderr,"Cannot open QuickCam; exiting.\n");
    exit(1);
  }

  if (!release_lock) {
     /* we no longer need root permission cuz we've done our last qc_open... */
     setuid(getuid());
  }

  fprintf(stderr,"Scanning from QuickCam at 0x%x at %dx%d (%d:1) @ %dbpp\n",
	  q->port,q->width,q->height,q->transfer_scale,q->bpp);

  /* Start X Display */

  if ((sbuf=InitXWindows(q))==NULL) {
    fprintf(stderr,"InitXWindows failed, exiting\n");
    exit(1);
  }

  if (privatecmap) {
    cmap = XCreateColormap(disp, win, DefaultVisual(disp, screen_num), 
			   AllocNone);
    XSetWindowColormap(disp, win, cmap);
  } else {
    cmap=DefaultColormap(disp, screen_num);
  }
  colortable=xqc_createpalette(cmap);


  /* Scan one image */
  if(verbose) {
    gettimeofday(&tv1,NULL);
  }
  
  if (release_lock) {
     /* Release IO privileges */
     qc_close(q);
  }

  while(!quit) {
     if (release_lock) {
        if (qc_open(q)) {
          fprintf(stderr,"Cannot open QuickCam; exiting.\n");
          exit(1);
        }
     }

    xqc_sync(q,colortable,autoexposure,histogram);

     if (release_lock) {
        /* Release IO privileges */
        qc_close(q);
     }

    /* Calculate frame rate */
    if(verbose) {
      gettimeofday(&tv2,NULL);

      /* The frame rate is calculated using the TCP RTT algorithm */

      fr=(1.0/(tv2.tv_sec-tv1.tv_sec+(tv2.tv_usec-tv1.tv_usec)/1000000.0));
      if(framerate!=0)
	framerate=0.9*framerate+0.1*fr;
      else
	framerate=fr;

      fprintf(stderr,"Frame rate: %f fps       \r",framerate);
      tv1.tv_sec=tv2.tv_sec;
      tv1.tv_usec=tv2.tv_usec;
    }

    if (delay != 0)
      sleep(delay);

  }

  ExitXWindows();

  if (!release_lock) {
     /* Release IO privileges */
     qc_close(q);
  }

  return 0;
}
