#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

/*********************************************************************
  Select or display blobs from a 24 bit color image, and some
  miscellaneous stuff. Requires a TrueColor display.
  Based on Davin Milun's image.c example.

  Joe Lammens 4/93
 *********************************************************************/

#define MAXWIDTH 	1252	/* max image width */
#define MAXHEIGHT 	1000	/* max image height */
#define MAXVALUE 	255	/* max pixel value */
#define BLOBWIDTH 	12	/* must be even */
#define BLOBHEIGHT 	16	/* must be even */
#define MAXNBOXES	10	/* max number of displayed boxes */
#define SUBSAMPLEDFILE 		".subsampled.ppm"
#define SUBSAMPLEDSIZEFILE 	".subsampled.xy"
#define SAMPLEFILE 		".sample.ppm"
#define SAMPLECOORDFILE 	".sample.xy"
#define RETURNEDPOINTSFILE	".returnedpoints.xy"

#define BLOBXDELTA 	(BLOBWIDTH/2)
#define BLOBYDELTA 	(BLOBHEIGHT/2)
#define BLOBAREA 	(BLOBWIDTH*BLOBHEIGHT)

typedef unsigned long pixel;
typedef struct { int x, y; } point;

static pixel imageData[MAXHEIGHT][MAXWIDTH], /* underlying image data */
             imageDisplay[MAXHEIGHT][MAXWIDTH];	/* displayed image */
static point boxCenters[(MAXNBOXES+1)];	/* currently displayed boxes */

/* read ppm image file into memory */

void ReadPpm(f, imageData, width, height, maxval, format)
     FILE *f;
     pixel imageData[MAXHEIGHT][MAXWIDTH];
     unsigned int *width, *height, *maxval;
     char format[];
{
  char line[128], line1[128], c;
  int col, row;

  /* read header */
  fscanf(f, "%s ", format);
  if(strcmp(format, "P6\0")) {
    fprintf(stderr, "Wrong image format: %s, should be P6 (ppm raw).\n",
	    format);
    exit(1);
  }
  while((c=getc(f))=='#') while(getc(f)!='\n');
  sprintf(line, "%c", c);
  fscanf(f, "%s", line1);
  strcat(line, line1);
  sscanf(line, "%u", width);
  fscanf(f, "%u %u ", height, maxval);
  fprintf(stderr, "%u x %u pixels, max value %u\n", *width, *height, *maxval);
  if(*maxval>MAXVALUE) {
    fprintf(stderr,
	    "Maximum pixel value %u too large; max allowed is %u\n",
	    *maxval, MAXVALUE);
    exit(1);
  }
  /* read image */
  for(row=0; row < *height; row++)
    for(col=0; col < *width; col++)
      imageData[row][col] = (((pixel) getc(f))<<16) | ((pixel) getc(f)) |
	(((pixel) getc(f))<<8) ;
}


/* copy image between buffers */

void CopyImage(in, out, width, height)
     pixel in[MAXHEIGHT][MAXWIDTH], out[MAXHEIGHT][MAXWIDTH];
     unsigned int width, height;
{
  register unsigned int row, col;

  for (row=0; row<height; row++)
    for (col=0; col<width; col++)
      out[row][col] = in[row][col];
}


/* average the rgb values of a blob around a given center point */

void BlobAverage(xCenter, yCenter, width, height, r, g, b)
     unsigned int xCenter, yCenter, width, height, *r, *g, *b;
{
  register int x0, x1, y0, y1, row, col;
  register unsigned long rsum, gsum, bsum, area;
  
  if(xCenter-BLOBXDELTA < 0) x0 = 0; else x0 = xCenter-BLOBXDELTA;
  if(yCenter-BLOBYDELTA < 0) y0 = 0; else y0 = yCenter-BLOBYDELTA;
  if(xCenter+BLOBXDELTA > width) x1 = width; else x1 = xCenter+BLOBXDELTA;
  if(yCenter+BLOBYDELTA > height) y1 = height; else y1 = yCenter+BLOBYDELTA;
  area = (x1-x0) * (y1-y0);

  rsum = gsum = bsum = 0;

  for(row=y0; row<y1; row++)
    for(col=x0; col<x1; col++) {
      rsum += imageData[row][col] & 0xff;
      gsum += imageData[row][col] >> 8 & 0xff;
      bsum += imageData[row][col] >> 16 & 0xff;
    }

  *r = rsum / area;
  *g = gsum / area;
  *b = bsum / area;
        
}


/* (Un)draw a box around a blob, given its center point */

void ToggleBlobBox(display, window, gc, xCenter, yCenter)
     Display *display;
     Window window;
     GC gc;
     register int xCenter, yCenter;
{
  int function;
	
  function = gc->values.function;
  XSetFunction(display, gc, GXxor);

  XDrawRectangle(display, window, gc,
		 (xCenter-BLOBXDELTA), (yCenter-BLOBYDELTA),
		 BLOBWIDTH, BLOBHEIGHT);

  XSetFunction(display, gc, function);
}


/* (Un)draw all boxes with centers given in an array -- negative
   coordinates means end of list */

void ToggleAllBlobBoxes (display, window, gc, centers, flash)
     Display *display;
     Window window;
     GC gc;
     point centers[];
     int flash;
{
  register int i, j, k, n;
  unsigned int lw;
  int ls, cs, js;
  
  lw = gc->values.line_width;
  ls = gc->values.line_style;
  cs = gc->values.cap_style;
  js = gc->values.join_style;

  /* count number of boxes */
  for (i=0; i<MAXNBOXES && centers[i].x>=0; i++);
  n = i;

  if (flash) k=3; else k=1;
  for (j=0; j<k; j++) {
    /* draw them with decreasing line thickness */
    for (i=0; i<n; i++) {
      XSetLineAttributes (display, gc, n-i, LineSolid, cs, JoinRound);
      ToggleBlobBox (display, window, gc, centers[i].x, centers[i].y);
    }
    if (j<(k-1)) {
      XFlush(display);
      usleep(500000);
    }
  }

  XSetLineAttributes (display, gc, lw, ls, cs, js);
}


/* subsample image and save to a comment-less raw ppm file, and save
   the original image coordinates to a plain text file. */

void SubSample(width, height, maxval, ssw, ssh)
     unsigned int width, height, maxval, *ssw, *ssh;
{
  FILE *f;
  register unsigned int row, col;
  unsigned int r, g, b;

  /* save size file first, so external programs can synchronize on the
     ppm file */

  f = fopen(SUBSAMPLEDSIZEFILE, "w");
  if(f==NULL) {
    fprintf(stderr, "Unable to open file %s for output.\n",
	    SUBSAMPLEDSIZEFILE);
    return;
  }
  fprintf(f, "%d %d %d %d %d %d\n",
	  width, height, BLOBWIDTH, BLOBHEIGHT, BLOBXDELTA, BLOBYDELTA);
  fclose(f);

  /* save subsampled ppm image */

  f = fopen(SUBSAMPLEDFILE, "w");
  if(f==NULL) {
    fprintf(stderr, "Unable to open file %s for output.\n",
	    SUBSAMPLEDFILE);
    return;
  }

  *ssw = (width / BLOBWIDTH);
  *ssh = (height / BLOBHEIGHT);

  fprintf(f, "P6\n%u %u\n%u\n", *ssw, *ssh, maxval);

  for(row=BLOBYDELTA; row<height; row+=BLOBHEIGHT)
    for(col=BLOBXDELTA; col<width; col+=BLOBWIDTH) {
      BlobAverage(col, row, width, height, &r, &g, &b);
      fprintf(f, "%c%c%c", (char) r, (char) g, (char) b);
    }

  fclose(f);
}


/* sample blob around point, save values to a comment-less ppm raw
   file and coordinates to a plain text file.  */

void SamplePoint(x, y, width, height, maxval, r, g, b)
     int x, y;
     unsigned int width, height, maxval, *r, *g, *b;
{
  FILE *f;

  /* write point coordinates file first, so external programs can
     synchronize on the ppm file only */

  f = fopen(SAMPLECOORDFILE, "w");
  if(f==NULL) {
    fprintf(stderr, "Unable to open file %s for output.\n", SAMPLECOORDFILE);
    return;
  }
  fprintf(f, "%d %d\n", x, y);
  fclose(f);

  /* write ppm file */

  f = fopen(SAMPLEFILE, "w");
  if(f==NULL) {
    fprintf(stderr, "Unable to open file %s for output.\n", SAMPLEFILE);
    return;
  }

  /* negative coordinates means write an empty file as a flag for
     external programs */

  if (x<0) {
    fclose(f);
    return;
  }

  fprintf(f, "P6\n%u %u\n%u\n", 1, 1, maxval);
  BlobAverage(x, y, width, height, r, g, b);
  fprintf(f, "%c%c%c", (char) *r, (char) *g, (char) *b);
  fclose(f);

}


/* Get a list of points returned by an external function, reading the
   coordinates from a text file. */

void GetReturnedPoints(centers, n)
     point centers[];
     int *n;
{
  FILE *f;
  register int i;

  f = fopen(RETURNEDPOINTSFILE, "r");
  if(f==NULL) {
    fprintf(stderr, "Unable to open file %s for input.\n", RETURNEDPOINTSFILE);
    return;
  }

  i=0; fscanf(f, "%d %d", &centers[i].x, &centers[i].y);
  for (i=0; i<MAXNBOXES && centers[i].x >= 0; ) 
    fscanf(f, "%d %d", &centers[++i].x, &centers[i].y);

  *n = i;

  fclose(f);
}
	

/* Function to keep updating displayed boxes centered at points
   returned by an external program. Returns as soon as there is an
   unprocessed X event in the queue. Synchronizes with the external
   program by monitoring file updates. */

void displayBoxes (display, window, gc, centers)
     Display *display;
     Window window;
     GC gc;
     point centers[];
{
  int n;
  struct stat fstat;
  time_t mtime=0;
  
  while (XPending(display) == 0) {
    stat(RETURNEDPOINTSFILE, &fstat);
    if (fstat.st_mtime != mtime) {
      mtime = fstat.st_mtime;
      ToggleAllBlobBoxes(display, window, gc, centers, 0);      
      usleep(250000);		/* allow time to finish writing */
      GetReturnedPoints(centers, &n); 
      ToggleAllBlobBoxes(display, window, gc, centers, 1);
    }
    else
      usleep(100000);
  }    
}


/* adapt an image using the "stretching" technique, given the */
/* representatives for black and white */

AdaptImage(image, width, height, maxval, black, white)
     pixel image[MAXHEIGHT][MAXWIDTH], black, white;
     unsigned int width, height;
{
  register unsigned int row, col;
  register pixel whiteR, whiteG, whiteB, blackR, blackG, blackB,
  r, g, b, result;
  register long temp;

  whiteR = white & 0xff; blackR = black & 0xff;
  whiteG = white >> 8 & 0xff; blackG = black >> 8 & 0xff;
  whiteB = white >> 16 & 0xff; blackB = black >> 16 & 0xff; 

  for (row=0; row<height; row++)
    for (col=0; col<width; col++) {
      temp = image[row][col];
      r = temp & 0xff;
      g = temp >> 8 & 0xff;
      b = temp >> 16 & 0xff;
      temp = (long) (maxval * (r - blackR)) / (whiteR - blackR);
      if (temp<0) temp=0;
      result = (pixel) temp;
      temp = (long) (maxval * (g - blackG)) / (whiteG - blackG);
      if (temp<0) temp=0;
      result = result | (pixel) temp << 8;
      temp = (long) (maxval * (b - blackB)) / (whiteB - blackB);
      if (temp<0) temp=0;
      image[row][col] = result | (pixel) temp << 16;
    }
}


main(argc, argv)
  int argc;
  char **argv;
{
  Display *mydisplay;
  Window mywindow;
  GC mygc;
  Cursor mycursor;
  XEvent myevent;
  KeySym mykey;
  XSizeHints myhint;
  int myscreen;
  unsigned long myforeground, mybackground;
  int i;
  char text[10];
  int done, getsample;

  unsigned int width, height, maxval;
  int status, bytes_per_line;
  char format[128];
  XImage *myimage;
  XVisualInfo myvisualinfo;
  Visual *myvisual;
  XSetWindowAttributes *myattributes;
  XGCValues myxgcvalues;
  pixel p, samples[3];

  /* open the display and select the default screen */

  mydisplay = XOpenDisplay("");
  myscreen = DefaultScreen (mydisplay);

  mybackground = BlackPixel (mydisplay, myscreen);
  myforeground = WhitePixel (mydisplay, myscreen);

/*  
  XSynchronize(mydisplay, 1);
/*

  /* make sure we can use DirectColor visuals */

  status = XMatchVisualInfo(mydisplay, myscreen, 24, TrueColor,
			    &myvisualinfo);
  if (status) myvisual = myvisualinfo.visual;
  else {
    fprintf(stderr, "This program requires 24 bit TrueColor visuals.\n");
    exit(1);
  }

  /* read the image data into memory, copy to the display buffer, and */
  /* determine the size of the window to be used */

  ReadPpm(stdin, imageData, &width, &height, &maxval, format);
  CopyImage(imageData, imageDisplay, width, height);

  myhint.x = (1152-width)/2;
  myhint.y = (900-height)/2;
  myhint.width = width;
  myhint.height = height;
  myhint.flags = PSize;

  /* create a window */

  mywindow = XCreateWindow (mydisplay, 
			    DefaultRootWindow (mydisplay),
			    myhint.x,
			    myhint.y, 
			    myhint.width, 
			    myhint.height,
			    0,
			    24,
			    InputOutput,
			    myvisual,
			    0,
			    0);

  XSetStandardProperties (mydisplay, mywindow, "Color Naming", 
			  "ClrName", None, argv, argc, &myhint);

  mygc = XCreateGC (mydisplay, mywindow, 0, &myxgcvalues);
  XSetBackground (mydisplay, mygc, mybackground);
  XSetForeground (mydisplay, mygc, myforeground);

  /* create an X image structure for the image */

  bytes_per_line = (MAXWIDTH * sizeof(pixel));
  myimage = XCreateImage (mydisplay,
			  myvisual,
			  24,
			  ZPixmap,
			  0,
			  imageDisplay,
			  width,
			  height,
			  32,
			  bytes_per_line);

  XSelectInput (mydisplay, mywindow,
                   ButtonPressMask | KeyPressMask | ExposureMask);

  XMapRaised (mydisplay, mywindow);

  mycursor = XCreateFontCursor(mydisplay,XC_crosshair);
  XDefineCursor(mydisplay,mywindow,mycursor);
  
  ShowMenu();

  done = 0;
  getsample = 0;
  boxCenters[0].x = boxCenters[0].y = -1000;
  while (!done) {
    int input = 0, menu;
    unsigned int r, g, b, w, h, n;
    static int x = -1000, y = -1000, boxesVisible = 0;
    while (!input) {
      XNextEvent (mydisplay, &myevent);
      switch (myevent.type) {
      case Expose:
	XPutImage (mydisplay, mywindow, mygc, myimage, 0, 0, 0, 0,
		   width, height);
	ToggleBlobBox(mydisplay, mywindow, mygc, x, y);
	if (boxesVisible)
	  ToggleAllBlobBoxes(mydisplay, mywindow, mygc, boxCenters, 0);
	break;
      case MappingNotify:	/* Just because it's needed */
	XRefreshKeyboardMapping ((XMappingEvent*)&myevent);
	break;
      case ButtonPress:
	switch (myevent.xbutton.button) {
	case 1:
	  /* button 1 = sample blob under point */
	  ToggleBlobBox(mydisplay, mywindow, mygc, x, y);
	  x = myevent.xbutton.x;
	  y = myevent.xbutton.y;
	  ToggleBlobBox(mydisplay, mywindow, mygc, x, y);
	  XFlush(mydisplay);
	  SamplePoint(x, y, width, height, maxval, &r, &g, &b);
	  fprintf(stderr, "Blob at (%d,%d): %u %u %u (file %s)\n",
		  x, y, r, g, b, SAMPLEFILE);
	  if (getsample) {
	    samples[getsample] =
	      ((pixel) r) | ((pixel) g<<8) | ((pixel) b<<16);
	    if (getsample<2) {
	      getsample++;
	      fprintf(stderr, "\7Please point out white\n");
	    }
	    else {
	      getsample=0;
	      AdaptImage(imageDisplay, width, height, maxval,
			 samples[1], samples[2]);
	      XPutImage (mydisplay, mywindow, mygc, myimage, 0, 0, 0, 0,
			 width, height);
	      ToggleBlobBox(mydisplay, mywindow, mygc, x, y);
	      if (boxesVisible)
		ToggleAllBlobBoxes(mydisplay, mywindow, mygc, boxCenters, 0);
	    }
	  }
	  break;
	case 2:
	  /* button 2 = write empty sample file as a flag for external
	     programs to stop naming colors */
	  ToggleBlobBox(mydisplay, mywindow, mygc, x, y);
	  x = y = -1000;
	  SamplePoint(x, y, width, height, maxval, &r, &g, &b);
	  break;
	case 3:
	  /* button 3 = toggle display mode for examples returned by
	     external programs */
	  boxesVisible = !boxesVisible;
	  ToggleAllBlobBoxes(mydisplay, mywindow, mygc, boxCenters, 0);      
	  break;
	}
	break;
      case KeyPress:
	i = XLookupString ((XKeyEvent*)&myevent, text, 10, &mykey, 0);
	menu = text[0];
	if (i==1) input = 1;	/* Only exit on simple key presses */
	break;
      } /* switch myevent.type */
      if (boxesVisible && !input)
	displayBoxes (mydisplay, mywindow, mygc, boxCenters);
    } /* while !input */

    switch (menu) {
    case 'a' :
    case 'A' :
      getsample = 1;
      fprintf(stderr, "\7Please point out black\n");
      break;
    case 'q' :
    case 'Q' :
      done = 1;
      break;
    case 's' :
    case 'S' :
      SubSample(width, height, maxval, &w, &h);
      fprintf(stderr, "Wrote %u x %u subsampled file %s\n",
	      w, h, SUBSAMPLEDFILE);
      break;
    } /* switch menu */
  } /* while !done */

  XFreeGC (mydisplay, mygc);
  XDestroyWindow (mydisplay, mywindow);
  XDestroyImage(myimage);
  XCloseDisplay (mydisplay);
}


ShowMenu()
/* For now, this is simply text in the regular text window */
{
  fprintf (stderr, "Color Naming\n\n");
  fprintf (stderr, "Mouse commands:\n");
  fprintf (stderr, "\tleft\t - sample blob around point\n");
  fprintf (stderr, "\tmiddle\t - stop sampling\n");
  fprintf (stderr, "\tright\t - toggle examples display\n");
  fprintf (stderr, "Keyboard commands (in the Color Naming window):\n");
  fprintf (stderr, "\tS - Subsample and save\n");
  fprintf (stderr, "\tQ - Quit program\n");
  fprintf (stderr, "\n");
}
