/* canvas.c: handles events on the canvas (drawing) window */

#include <Xm/Xm.h>
#include <X11/cursorfont.h>
#include <X11/keysym.h>

#include "canvas.h"
#include "ui.h"
#include "render.h"
#include "camera.h"
#include "select.h"
#include "error.h"

static CANVAS_TYPE canvastype;
static Window canvas_window;
static Display *canvas_display;
static int canvas_inited = FALSE;

static void CanvasResizeCallback(Widget canvas, XtPointer client_data, XtPointer call_data);
static void CanvasExposeCallback(Widget canvas, XtPointer client_data, XtPointer call_data);
static void CanvasMouseEvent(Widget canvas, XtPointer client_data, XEvent *event);
static void CanvasKeyEvent(Widget canvas, XtPointer client_data, XEvent *event);

int twobutton_motion;

/* creates an integrated canvas window. */
Widget CreateIntegratedCanvasWindow(Widget parent, Dimension hres, Dimension vres)
{
  canvastype = INTEGRATED_CANVAS;

  canvas = RenderCreateWindow(parent);

  if (hres > 0)		/* horizontal resolution specified as an option */
    XtVaSetValues(canvas, XmNwidth, hres, NULL);
  else
    XtVaGetValues(canvas, XmNwidth, &hres, NULL);
  
  if (vres > 0)		/* vertical resolution specified as an option */
    XtVaSetValues(canvas, XmNheight, vres, NULL);
  else
    XtVaGetValues(canvas, XmNheight, &vres, NULL);

  /* make sure the current camera has the correct horizontal and vertical 
   * resolution */
  CameraSet(&Camera, &Camera.eyep, &Camera.lookp, &Camera.updir,
	    Camera.fov, hres, vres, &Camera.background);

  /* window contents damaged  */
  XtAddCallback(canvas, XmNexposeCallback, CanvasExposeCallback, (XtPointer)NULL);

  /* window resizes */
  XtAddCallback(canvas, XmNresizeCallback, CanvasResizeCallback, (XtPointer)NULL);

  /* mouse events */
  XtAddEventHandler(canvas, 
		    ButtonPressMask | ButtonReleaseMask | ButtonMotionMask,
		    False,	/* no non-maskable event */
		    (XtEventHandler)CanvasMouseEvent,
		    (XtPointer)NULL);	/* client_data */

  /* canvas key event handler */
  XtAddEventHandler(canvas,
		    /*KeyPressMask |*/ KeyReleaseMask,
		    False,	/* no non-maskable event */
		    (XtEventHandler)CanvasKeyEvent,
		    (XtPointer)NULL);	/* client_data */

  return canvas;
}

/* connects to an external canvas window with given ID on the given display.
 * The display needs not be the same display as the one containing the rest of
 * the user interface. */
void ConnectExternalCanvasWindow(Display *display, Window window)
{
  XWindowAttributes wattr;
  XVisualInfo *vinfo, vinfo_template;
  long vinfo_mask;
  int nreturn;

  canvas_window = window;
  canvas_display = display;
  canvastype = EXTERNAL_CANVAS;

  /* get window attributes */
  if (!XGetWindowAttributes(display, window, &wattr))
    Fatal(1, NULL, "Bad external canvas window");

  /* look up the XVisualInfo for the window */
  vinfo_template.visualid = XVisualIDFromVisual(wattr.visual);
  vinfo_mask = VisualIDMask;
  vinfo = XGetVisualInfo(display, vinfo_mask, &vinfo_template, &nreturn);
  if (nreturn < 1)
    Fatal(1, NULL, "Couldn't find X visual info for external canvas window");

  /* see XSelectInput manpage. */
  if (wattr.all_event_masks & ButtonPressMask)
    Warning(NULL, "ButtonPress events are already being handled on the canvas window.\nNavigation via the mouse will not be possible");

  /* tell what events we want to receive for the window */
  XSelectInput(canvas_display, canvas_window,
	       ExposureMask | StructureNotifyMask |
	       ((wattr.all_event_masks & ButtonPressMask) ? 0 : ButtonPressMask) | ButtonReleaseMask | ButtonMotionMask |
	       /* KeyPressMask | */ KeyReleaseMask);

  /* initialize rendering into the window */
  RenderInitWindow(display, window, vinfo);

  /* set correct width and height for the camera */
  CameraSet(&Camera, &Camera.eyep, &Camera.lookp, &Camera.updir,
	    Camera.fov, wattr.width, wattr.height, &Camera.background);

  /* render the scene (no expose events on the external canvas window!) */
  RenderScene();
}

/* creates an offscreen canvas "window" with given horizontal and vertical
 * resolution. */
void CreateOffscreenCanvasWindow(int hres, int vres)
{
  canvastype = OFFSCREEN_CANVAS;

  RenderCreateOffscreenWindow(hres, vres);

  /* set correct width and height for the camera */
  CameraSet(&Camera, &Camera.eyep, &Camera.lookp, &Camera.updir,
	    Camera.fov, hres, vres, &Camera.background);

  /* render the scene (no expose events on the external canvas window!) */
  RenderScene();
}

/* Size of the canvas mode stack. Canvas mode: determines how the program will
 * react to mouse events and what shape of cursor is displayed in the canvas
 * window, eg spraycan when renderin ... When the mode changes, the old mode
 * is pushed on a stack and restored afterwards */
#define CANVASMODESTACKSIZE 5
static int cursordefined = 0, modestackidx;
static CANVAS_MODE canvasmode = CANVASMODE_NORMAL;
static CANVAS_MODE modestack[CANVASMODESTACKSIZE];

static Cursor working_cursor, select_cursor, render_cursor;
#define WORKING_CURSOR	XC_watch
#define SELECT_CURSOR XC_crosshair
#define RENDER_CURSOR XC_spraycan

/* initializes event handling cursor shape on the canvas widget */
void CanvasInit(void)
{
  switch (canvastype) {
  case INTEGRATED_CANVAS:
    canvas_window = XtWindow(canvas);
    canvas_display = XtDisplay(canvas);
    break;

  case EXTERNAL_CANVAS:
    break;

  case OFFSCREEN_CANVAS:
    return;

  default:
    Fatal(-1, "CanvasInit", "Invalid canvas type 5d", canvastype);
  }

  canvasmode = CANVASMODE_NORMAL;
  modestackidx = 0;
  modestack[modestackidx] = canvasmode;
  cursordefined = FALSE;

  /* cursor to show that the program is busy computing */
  working_cursor = XCreateFontCursor(canvas_display, WORKING_CURSOR);	

  /* cursor to show that the user is expected to select a patch */
  select_cursor = XCreateFontCursor(canvas_display, SELECT_CURSOR); 

  /* cursor to show that the program is busy rendering */
  render_cursor = XCreateFontCursor(canvas_display, RENDER_CURSOR);

  canvas_inited = TRUE;
}

/* set a new canvas mode */
static void CanvasSetMode(CANVAS_MODE mode)
{
  if (!canvas_inited || canvastype == OFFSCREEN_CANVAS)
    return;

  switch (mode) {
  case CANVASMODE_NORMAL:
    if (cursordefined) {
      XUndefineCursor(canvas_display, canvas_window);
      cursordefined = FALSE;
    }

    canvasmode = CANVASMODE_NORMAL;
    break;
  case CANVASMODE_WORKING:
    /* clock cursor */
    XDefineCursor(canvas_display, canvas_window, working_cursor);
    XSync(canvas_display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_WORKING;
    break;
  case CANVASMODE_SELECT_PATCH:
    /* crosshair cursor */
    XDefineCursor(canvas_display, canvas_window, select_cursor);
    XSync(canvas_display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_SELECT_PATCH;
    break;
  case CANVASMODE_SELECT_PIXEL:
    /* crosshair cursor */
    XDefineCursor(canvas_display, canvas_window, select_cursor);
    XSync(canvas_display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_SELECT_PIXEL;
    break;
  case CANVASMODE_RENDER:
    /* spraycan cursor cursor */
    XDefineCursor(canvas_display, canvas_window, render_cursor);
    XSync(canvas_display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_RENDER;
    break;
  default:
    Fatal(4, "CanvasSetMode", "Invalid mode %d - internal error.", mode);
    break;
  }

  modestack[modestackidx] = canvasmode;
}

/* returns the current canvas mode */
CANVAS_MODE CanvasGetMode(void)
{
  return canvasmode;
}

/* pushes the current canvas mode on the canvas mode stack, so it can be
 * restored later */
void CanvasPushMode(CANVAS_MODE mode)
{
  modestackidx++;
  if (modestackidx >= CANVASMODESTACKSIZE) 
    Fatal(4, "CanvasPushMode", "Mode stack size (%d) exceeded.", CANVASMODESTACKSIZE);

  CanvasSetMode(mode);
}

/* restores the last saved canvas mode */
void CanvasPullMode(void)
{
  modestackidx--;
  if (modestackidx < 0)
    Fatal(4, "CanvasPullMode", "Canvas mode stack underflow.\n");
  
  CanvasSetMode(modestack[modestackidx]);
}

/* handles canvas window resize events */
static void CanvasResize(int width, int height)
{
  CameraSet(&Camera, &Camera.eyep, &Camera.lookp, &Camera.updir,
	    Camera.fov, width, height, &Camera.background);

  RenderScene();
}

static void CanvasResizeCallback(Widget canvas, XtPointer client_data, XtPointer call_data)
{
  Dimension width, height;

  XtVaGetValues(canvas,
		XmNwidth, &width,
		XmNheight, &height,
		NULL);

  CanvasResize(width, height);
}

/* Expose event handling: redraw the scene just once, nomatter how many expose
 * events are coming at the same time. */
static int RedrawWorkProcInstalled = FALSE;
static XtWorkProcId RedrawWorkProcId;

static Boolean RedrawWorkProc(XtPointer client_data)
{
  RenderScene();
  RedrawWorkProcInstalled = FALSE;
  return TRUE;
}

/* Install a work procedure which will redraw the scene. Install it just once,
 * when the global variable RedrawWorkProcInstalled is FALSE, so the scene will
 * be redrawn just once, no matter how many expose events are coming at the same
 * time. */
void CanvasPostRedraw(void)
{
  if (canvastype == INTEGRATED_CANVAS && !RedrawWorkProcInstalled) {
    RedrawWorkProcId = XtAppAddWorkProc(app_context, RedrawWorkProc, (XtPointer)NULL);
    RedrawWorkProcInstalled = TRUE;
  }
}

/* cancels previous redrawing requests due to e.g. expose events on the 
 * canvas window. */
void CanvasCancelRedraw(void)
{
  if (canvastype == INTEGRATED_CANVAS && RedrawWorkProcInstalled) {
    XtRemoveWorkProc(RedrawWorkProcId);
    RedrawWorkProcInstalled = FALSE;
  }
}

/* Takes care of possible expose events immediately: if the scene needs to be
 * redrawn, this routine removes the installed redraw work procedure and immediately
 * rerenders the scene. A redraw work procedure is used for expose event compression:
 * Normally, the scene is redrawn in the background, after all pending events have been
 * processed. */
void CanvasRedraw(void)
{
  RenderScene();
  CanvasCancelRedraw();
}

static void ExternalCanvasExpose(void)
{
  if (canvasmode == CANVASMODE_RENDER)
    return;	/* ignore expose events while rendering */

  RenderScene();
}

/* handles canvas expose events on the integrated canvas widget */
static void CanvasExposeCallback(Widget canvas, XtPointer client_data, XtPointer call_data)
{
  if (canvasmode == CANVASMODE_RENDER)
    return;	/* ignore expose events while rendering */

  CanvasPostRedraw();
}

/* this routine moves the camera corresponding to which mouse buttons were
 * pressed and the distance the mouse moved, and than rerenders the scene. 
 * Conventions are the same as for a WALK viewer in CosmoWorlds. */
static void DoMotion(int x, int y, int lastx, int lasty, int buttonspressed)
{
  Dimension maxx, maxy;
  float fov, aspect, a, w, view_dist;
  VECTOR d;

  maxx = Camera.hres;
  maxy = Camera.vres;
  fov = 2. * Camera.fov * M_PI / 180.;
  aspect = (float)maxx/(float)maxy;
  if (aspect > 1) fov *= aspect;
  VECTORSUBTRACT(Camera.lookp, Camera.eyep, d);
  view_dist = VECTORNORM(d);
  w = view_dist * fov;

  /* w = sin(angle between up direction and viewing direction). Used
   * to have slower rotations when viewing direction and updirection
   * almost coincide. It "feels" better this way. */
  a = VECTORDOTPRODUCT(Camera.Z, Camera.updir) / VECTORNORM(Camera.updir);
  a = sin(acos(a < -1. ? -1. : (a > 1. ? 1. : a)));

  /* what motion to perform depends on which mouse buttons were pressed 
   * while the pointer was moved */
  if (twobutton_motion) {
    switch (buttonspressed) {
    case 1:  	/* first mouse button pressed and moved */
      if (x != lastx) 
	CameraTurnRight(&Camera, (float)(x - lastx)/(float)maxx * fov * a);
      if (y != lasty) 
	CameraMoveHorizontal(&Camera, (float)(lasty - y)/(float)maxy * 2. * view_dist);
      break;

    case 2:   /* second mouse button (middle button on a three button mouse) */
      if (x != lastx) 
	CameraMoveRight(&Camera, (float)(x - lastx)/(float)maxx * w);
      if (y != lasty) 
	CameraMoveUp(&Camera, (float)(y - lasty)/(float)maxx * w / aspect);
      break;

    case 3:	/* first and second button pressed simultaneously */
      if (x != lastx) 
	CameraTurnRight(&Camera, (float)(x - lastx)/(float)maxx * fov * a);
      if (y != lasty) 
	CameraTurnUp(&Camera, (float)(lasty - y)/(float)maxy * fov / aspect);
      break;

    default:
      /* do nothing */
      return;
    }
  } else { /*three button motion */
    switch (buttonspressed) {
    case 1:  	/* first mouse button pressed and moved */
      if (x != lastx) 
	CameraTurnRight(&Camera, (float)(x - lastx)/(float)maxx * fov * a);
      if (y != lasty) 
	CameraTurnUp(&Camera, (float)(lasty - y)/(float)maxy * fov / aspect);
      break;
      
    case 2:   /* second mouse button (middle button on a three button mouse) */
      if (x != lastx) 
	CameraMoveRight(&Camera, (float)(x - lastx)/(float)maxx * w);
      if (y != lasty) 
	CameraMoveUp(&Camera, (float)(y - lasty)/(float)maxx * w / aspect);
      break;

    case 3:	/* first and second button pressed simultaneously */
    case 4:	/* right button on three-button mouse */
      if (x != lastx) 
	CameraMoveRight(&Camera, (float)(x - lastx)/(float)maxx * w);
      if (y != lasty) 
	CameraMoveForward(&Camera, (float)(lasty - y)/(float)maxy * 2. * view_dist);
      break;

    default:
      /* do nothing */
      return;
    }
  }

  /* rerender the scene as seen from the new camera position etc... */
  RenderScene();
}

static PATCH *the_patch;
static POINT the_point;
static void GetThePatch(PATCH *patch, POINT *hitp)
{
  the_patch = patch;
  the_point = *hitp;
}

static void CanvasClick(XEvent *event)
{
  the_patch = (PATCH *)NULL;
  SelectPatchSetCallback(GetThePatch);
  CanvasPushMode(CANVASMODE_SELECT_PATCH);
  SelectPatch(event->xbutton.x, event->xbutton.y);

  if (the_patch)
    PopUpCanvasMenu(event, the_patch, &the_point);
}

/* handles mouse events on the canvas window */
static int buttonspressed = 0;	/* global because we want to be able to simulate
				 * buttonpresses/releases with the keyboard */

static void CanvasMouse(XEvent *event)
{
  static int lastx, lasty;
  int x, y;

  if (canvasmode == CANVASMODE_RENDER)
    return;	/* ignore mouse events */

  switch (event->type) {
  case ButtonPress: 
    x = lastx = event->xbutton.x;
    y = lasty = event->xbutton.y;
    
    switch (event->xbutton.button) {
    case Button1: buttonspressed |= 1; break;
    case Button2: buttonspressed |= 2; break;
    case Button3: 
      if (twobutton_motion) 
	CanvasClick(event);
      else
	buttonspressed |= 4;
      break;
    default: break;
    }

    break;
	
  case ButtonRelease: 
    x = event->xbutton.x;
    y = event->xbutton.y;

    if (canvasmode == CANVASMODE_SELECT_PATCH || 
		canvasmode == CANVASMODE_SELECT_PIXEL) {
      switch (buttonspressed) {
      case 1:	/* select callbacks know what to do with the selected patch ... */
	if(canvasmode == CANVASMODE_SELECT_PATCH)
	  SelectPatch(event->xbutton.x, event->xbutton.y);
	else
	  SelectPixel(event->xbutton.x, event->xbutton.y);
	break;
      default: /* cancel the selection */
	CanvasPullMode();
	break;
      }
    } else {
#ifdef SLOW_RENDERER
      /* do all motion at once when the user releases the mouse
       * button */
      if (lastx != x || lasty != y)
	DoMotion(x, y, lastx, lasty, buttonspressed);

      lastx = x;
      lasty = y;
#endif /*SLOW_RENDERER*/
    }

    switch (event->xbutton.button) {
    case Button1: buttonspressed &= ~1; break;
    case Button2: buttonspressed &= ~2; break;
    case Button3:
      if (!twobutton_motion)
	buttonspressed &= ~4;
      break;
    default: break;
    }

    break;

#ifndef SLOW_RENDERER	/* do the motion immediately */
  case MotionNotify:
    x = event->xmotion.x;
    y = event->xmotion.y;
 
    DoMotion(x, y, lastx, lasty, buttonspressed);

    lastx = x;
    lasty = y;
    break;
#endif /*SLOW_RENDERER*/

  default:
    /* ignore the event */
    break;
  }
}

static void CanvasMouseEvent(Widget canvas, XtPointer client_data, XEvent *event)
{
  CanvasMouse(event);
}

/* determines the key that was pressed/released from a XKeyEvent structure. */
static KeySym GetKey(XKeyEvent *event)
{
  char buf[20];
  int bufsize = 20;
  KeySym key;
  XComposeStatus compose;
  int charcount;

  charcount = XLookupString(event, buf, bufsize, &key, &compose);
  buf[charcount] = '\0';

#ifdef DEBUG
  switch (key) {
  case XK_Shift_L: fprintf(stderr, "Shift_L "); break;
  case XK_Shift_R: fprintf(stderr, "Shift_R "); break;
  case XK_Shift_Lock: fprintf(stderr, "Shift_Lock "); break;
  case XK_Caps_Lock: fprintf(stderr, "Caps_Lock "); break;
  default: fprintf(stderr, "Key %x '%s' ", (unsigned)key, buf);
  }
#endif
  return key;
}

/* handles KeyRelease events on the canvas window */
static void CanvasKey(XKeyEvent *event)
{
  /* extern void TestGlobalLine(void); */
  switch (GetKey(event)) {
  case XK_Q: exit(0);  break;
    /* case XK_t: TestGlobalLine(); break; */
  default:
    /* ignore */
    break;
  }
}

static void CanvasKeyEvent(Widget canvas, XtPointer client_data, XEvent *event)
{
  CanvasKey((XKeyEvent *)event);
}

/* handles XEvents on external canvas window (which may even be on another 
 * display than the rest of the interface ...*/
void ExternalCanvasEvent(XEvent *event)
{
  switch (event->type) {
  case Expose:
    /* compress expose events: only handle the last one in the event queue. We
     * don't bother compute=ing the union of the exposed window regions as
     * these are not used. */
    while (XCheckWindowEvent(canvas_display, canvas_window, ExposureMask, event)) {}
    ExternalCanvasExpose();
    break;

  case ConfigureNotify:
    CanvasResize(event->xconfigure.width, event->xconfigure.height);
    break;

  case KeyPress: case KeyRelease:
    CanvasKey((XKeyEvent *)event);
    break;

  case ButtonPress: case ButtonRelease:
    CanvasMouse(event);
    break;

  case MotionNotify:
    /* compress motion events: only handle the last one in the event queue */
    while (XCheckWindowEvent(canvas_display, canvas_window, ButtonMotionMask, event)) {}
    CanvasMouse(event);
    break;

  case DestroyNotify:
    fprintf(stderr, "External canvas window destroyed!\n");
    exit(0);
    break;

  default:
    /* fprintf(stderr, "I don't handle it.\n"); */
    break;
  }
}








