/*
 * Copyright (C) 2003, 2004 Philip Blundell <philb@gnu.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 */

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <math.h>

#include <sys/time.h>
#include <sys/ioctl.h>

#include <X11/X.h>
#include <X11/Xlib.h>

#include <X11/extensions/Xrender.h>
#include <X11/Xft/Xft.h>
#include <X11/extensions/xcalibrate.h>
#include <X11/keysym.h>

#include "calibrate.h"

Display *dpy;
int screen;
Window w;
Window crosshair_w;
GC crosshair_gc;
Cursor cursor;
Picture pict;
XRectangle rectangles[2];
XRenderColor rect_color;
int crosshair_x, crosshair_y;
int event;
XftColor xftcol;
XftDraw *xftdraw;
XftFont *xftfont;
int screen_x, screen_y;
int samples;
Pixmap bg_pixmap;
int flag_debug;
int error_base, event_base;

int moving;

#define CROSSHAIR_SIZE	25
#define OFFSET 24
#define WIDTH 3
#define SPEED 8
#define ENOUGH 5
#define MAX_SAMPLES 40

#define FONTNAME "sans-10"

struct point 
{
  int x;
  int y;
};

struct point sample[MAX_SAMPLES];
calibration cal;

void handle_ts_event (int x, int y, int pressure);

void
hide_cursor (Display *dpy, Window w)
{
  Pixmap pix;
  XColor col;
  Cursor blank_curs;

  pix = XCreatePixmap (dpy, w, 1, 1, 1);
  memset (&col, 0, sizeof (col));
  blank_curs = XCreatePixmapCursor (dpy, pix, pix, &col, &col, 1, 1);
  XFreePixmap (dpy, pix);
  XDefineCursor (dpy, w, blank_curs);
}

void
draw_crosshair (void)
{
  XftDraw *draw;
  XRenderPictureAttributes att;

  crosshair_gc = XCreateGC (dpy, crosshair_w, 0, NULL);

  XSetForeground (dpy, crosshair_gc, WhitePixel (dpy, screen));

  draw = XftDrawCreate (dpy, crosshair_w,
			DefaultVisual (dpy, screen),
			DefaultColormap (dpy, screen));
  
  pict = XftDrawPicture (draw);
  
  att.poly_edge = PolyEdgeSmooth;
  XRenderChangePicture (dpy, pict, CPPolyEdge, &att);
  
  rect_color.red   = 0;
  rect_color.green = 0;
  rect_color.blue  = 0xffff;
  rect_color.alpha = 0xffff;

  rectangles[0].x = 0;
  rectangles[0].width = CROSSHAIR_SIZE;
  rectangles[0].y = (CROSSHAIR_SIZE / 2) - (WIDTH / 2);
  rectangles[0].height = WIDTH;
 
  rectangles[1].x = (CROSSHAIR_SIZE / 2) - (WIDTH / 2);
  rectangles[1].width = WIDTH;
  rectangles[1].y = 0;
  rectangles[1].height = CROSSHAIR_SIZE;
}

void
crosshair_expose (void)
{
  XRenderFillRectangles (dpy, PictOpSrc, pict, &rect_color, rectangles, 2);

  XFillRectangle (dpy, crosshair_w, crosshair_gc,
		  (CROSSHAIR_SIZE / 2) - (WIDTH / 2),
		  (CROSSHAIR_SIZE / 2) - (WIDTH / 2),
		  WIDTH, WIDTH);
}

void
draw_background (void)
{
  static const char *str[] = 
    {
      "Touch the crosshairs to",
      "calibrate the screen"
    };
  const char **sp;
  int y;

  y = screen_y / 3;

  for (sp = str; *sp; sp++)
    {
      XGlyphInfo extents;

      XftTextExtentsUtf8 (dpy, xftfont, *sp, strlen (*sp),
			  &extents);

      XftDrawStringUtf8 (xftdraw, &xftcol, xftfont, 
			 (screen_x / 2) - (extents.width / 2), y,
			 *sp, strlen (*sp));

      y += extents.height + 4;
    }
}

void
handle_events (void)
{
  while (XPending (dpy))
    {
      XEvent e;
      
      XNextEvent (dpy, &e);
      
      switch (e.type)
	{
	case KeyPress:
	  if (XKeycodeToKeysym (dpy, e.xkey.keycode, 0) == XK_Escape)
	    exit (0);
	  break;
	case Expose:
	  if (e.xexpose.window == crosshair_w)
	    crosshair_expose ();
	  break;
	default:
	  if (e.type == event_base)
	    {
	      XCalibrateRawTouchscreenEvent *te = (XCalibrateRawTouchscreenEvent *)&e;

	      handle_ts_event (te->x, te->y, te->pressure);
	    }
	  break;
	}
    }
}

void
crosshair_to (int x, int y)
{
  int dx, dy;
  double r;

  dx = x - crosshair_x;
  dy = y - crosshair_y;

  if (dx == 0 && dy == 0)
    return;

  moving = True;

  r = (double)dy / (double)dx;

  if (fabs (r) < 1.0)
    {
      int i, xs;
      double fy = crosshair_y;

      xs = (crosshair_x > x) ? -SPEED : SPEED;

      for (i = crosshair_x; (i - x) / SPEED; )
        {
          int midpoint = (x + crosshair_x) / 2;
          int distance_from_midpoint = abs(i - midpoint); 
	  int distance_from_endpoint = abs(x - crosshair_x) / 2 - distance_from_midpoint;
          int xsadjust = xs + (xs >> 3) * distance_from_endpoint / 10;
          i += xsadjust;
          fy += r * xsadjust;

	  XMoveWindow (dpy, crosshair_w, i, fy);
	  XFlush (dpy);
	  handle_events ();
	  usleep (5000);
	}
    }
  else
    {
      int i, ys;
      double fx = crosshair_x;

      r = (double)dx / (double)dy;

      ys = (crosshair_y > y) ? -SPEED : SPEED;

      for (i = crosshair_y; (i - y) / SPEED; )
        {
          int midpoint = (y + crosshair_y) / 2;
          int distance_from_midpoint = abs(i - midpoint); 
	  int distance_from_endpoint = abs(y - crosshair_y) / 2 - distance_from_midpoint;
          int ysadjust = ys + (ys >> 3) * distance_from_endpoint / 10;
          i += ysadjust;
          fx += r * ysadjust;

	  XMoveWindow (dpy, crosshair_w, fx, i);
	  XFlush (dpy);
	  handle_events ();
	  usleep (5000);
	}
    }

  XMoveWindow (dpy, crosshair_w, x, y);
  crosshair_x = x;
  crosshair_y = y;

  moving = False;
}

void
next_event (void)
{
  crosshair_to (cal.xscr[event] - CROSSHAIR_SIZE / 2, cal.yscr[event] - CROSSHAIR_SIZE / 2);
}

int 
sort_by_x (const void* a, const void *b)
{
  return (((struct point *)a)->x - ((struct point *)b)->x);
}

int 
sort_by_y (const void* a, const void *b)
{
  return (((struct point *)a)->y - ((struct point *)b)->y);
}

void
set_calibration (calibration *cal)
{
  calibration ocal = *cal;

      FILE *fp;
      if (flag_debug)
	printf ("constants are: %d %d %d %d %d %d %d\n", cal->a[1], cal->a[2], cal->a[0], cal->a[4], cal->a[5], cal->a[3], cal->a[6]);
      fp = fopen ("/etc/pointercal", "w");
      if (!fp)
	{
	  perror ("/etc/pointercal");
	  exit (1);
	}
      fprintf (fp, "%d %d %d %d %d %d %d\n", cal->a[1], cal->a[2], cal->a[0], cal->a[4], cal->a[5], cal->a[3], cal->a[6]);
      fclose (fp); 
}

void
handle_ts_event (int x, int y, int pressure)
{
  if (moving)
    {
      /* Ignore any accidental clicks during animation */
      return;
    }

  if (pressure)
    {
      if (samples < MAX_SAMPLES)
	{
	  sample[samples].x = x;
	  sample[samples].y = y;
	  samples++;
	}
    }
  else
    {
      if (samples > ENOUGH)
	{
	  int middle, sx, sy;
	  
	  middle = samples / 2;
	  qsort (sample, samples, sizeof(struct point), sort_by_x);
	  if (samples & 1)
	    sx = sample[middle].x;
	  else
	    sx = (sample[middle-1].x + sample[middle].x) / 2;
	  qsort (sample, samples, sizeof(struct point), sort_by_y);
	  if (samples & 1)
	    sy = sample[middle].y;
	  else
	    sy = (sample[middle-1].y + sample[middle].y) / 2;
	  
	  cal.x[event] = sx;
	  cal.y[event] = sy;
	  
	  if (flag_debug)
	    fprintf (stderr, "point %d: [%d %d]\n", event, cal.x[event], cal.y[event]);
	  
	  event++;
	  
	  if (event < NR_POINTS)
	    {
	      samples = 0;
	      next_event ();
	    }
	  else
	    {
	      if (perform_calibration (&cal))
		{
		  set_calibration (&cal);
		  XCalibrateSetRawMode (dpy, False);
		  exit (0);
		}
	      else
		{
		  samples = 0;
		  event = 0;
		  next_event ();
		}
	    }
	}
    }
}

void
usage (const char *name)
{
  fprintf (stderr, "usage: %s [-debug]\n", name);
  exit (1);
}

int
main (int argc, char *argv[])
{
  XSetWindowAttributes attributes;
  int xfd;
  XRenderColor colortmp;
  int max_fd;
  GC bg_gc;
  int i;

  for (i = 1; i < argc; i++)
    {
      if (!strcmp (argv[i], "-debug"))
	flag_debug = 1;
      else
	usage (argv[0]);
    }
  
  dpy = XOpenDisplay (NULL);
  if (dpy == NULL)
    {
      fprintf (stderr, "Couldn't open display\n");
      exit (1);
    }

  screen = DefaultScreen (dpy);

  if (XCalibrateQueryExtension (dpy, &event_base, &error_base))
    {
      int r;

      if (flag_debug)
	fprintf (stderr, "Using XCALIBRATE\n");

      r = XCalibrateSetRawMode (dpy, True);
      if (r)
	{
	  fprintf (stderr, "failed to set raw mode: error %d\n", r);
	  exit (1);
	}
    }
  else
    {
      perror ("XCALIBRATE extension missing");
      exit (1);
    }

  attributes.override_redirect = flag_debug ? False : True;
  attributes.background_pixel = WhitePixel (dpy, screen);
    
  w = XCreateWindow (dpy, DefaultRootWindow(dpy), 0 , 0,
		     DisplayWidth(dpy, 0), DisplayHeight(dpy, 0),
		     0,
		     DefaultDepth(dpy, 0),
		     InputOutput,
		     DefaultVisual(dpy, 0),
		     CWOverrideRedirect | CWBackPixel,
		     &attributes );

  screen_x = DisplayWidth (dpy, screen);
  screen_y = DisplayHeight (dpy, screen);

  hide_cursor (dpy, w);

  crosshair_x = OFFSET;
  crosshair_y = OFFSET;

  crosshair_w = XCreateSimpleWindow (dpy, w, crosshair_x, crosshair_y,
				     CROSSHAIR_SIZE, CROSSHAIR_SIZE,
				     0, None, WhitePixel (dpy, screen));

  draw_crosshair ();

  xftfont = XftFontOpenName (dpy, screen, FONTNAME);
  
  if (xftfont == NULL)
    { 
      fprintf (stderr, "Can't open XFT font\n"); 
      exit(1); 
    }

  colortmp.red   = 0;
  colortmp.green = 0;
  colortmp.blue  = 0;
  colortmp.alpha = 0xffff;
  XftColorAllocValue (dpy,
		     DefaultVisual (dpy, screen),
		     DefaultColormap (dpy, screen),
		     &colortmp,
		     &xftcol);

  bg_pixmap = XCreatePixmap (dpy, w, screen_x, screen_y, DefaultDepth (dpy, screen));
  bg_gc = XCreateGC (dpy, bg_pixmap, 0, NULL);
  XSetForeground (dpy, bg_gc, WhitePixel (dpy, screen));

  XFillRectangle (dpy, bg_pixmap, bg_gc, 0, 0, screen_x, screen_y);

  xftdraw = XftDrawCreate (dpy, bg_pixmap,
			   DefaultVisual (dpy, screen),
			   DefaultColormap (dpy, screen));

  draw_background ();

  XSetWindowBackgroundPixmap (dpy, w, bg_pixmap);

  XSelectInput (dpy, w, StructureNotifyMask | ButtonPressMask | KeyPressMask);
  XSelectInput (dpy, crosshair_w, ExposureMask);
  XMapWindow (dpy, w);
  XMapRaised (dpy, crosshair_w);
  XGrabPointer (dpy, w, True, Button1Mask, GrabModeAsync, GrabModeAsync,
		None, None, CurrentTime);
  XGrabKeyboard (dpy, w, True, GrabModeAsync, GrabModeAsync, CurrentTime);

  xfd = ConnectionNumber (dpy);

#if NR_POINTS == 5
  cal.xscr[0] = OFFSET;
  cal.yscr[0] = OFFSET;

  cal.xscr[1] = screen_x - OFFSET;
  cal.yscr[1] = OFFSET;

  cal.xscr[2] = screen_x - OFFSET;
  cal.yscr[2] = screen_y - OFFSET;

  cal.xscr[3] = OFFSET;
  cal.yscr[3] = screen_y - OFFSET;

  cal.xscr[4] = (screen_x / 2);
  cal.yscr[4] = (screen_y / 2);
#else

#error Unsupported number of calibration points

#endif

  for (i = 0; i < NR_POINTS; i++)
    {
      cal.xfb[i] = cal.xscr[i];
      cal.yfb[i] = cal.yscr[i];
      XCalibrateScreenToCoord (dpy, &cal.xfb[i], &cal.yfb[i]);

      if (flag_debug)
	printf ("rotation conversion: (%d,%d) -> (%d,%d)\n",
		cal.xscr[i], cal.yscr[i], cal.xfb[i], cal.yfb[i]);
    }

  next_event ();

  for (;;)
    {
      fd_set fds;

      handle_events ();

      FD_ZERO (&fds);
      FD_SET (xfd, &fds);

      select (xfd + 1, &fds, NULL, NULL, NULL);
    }
}

