/* 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 "error.h"

/* 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, canvasmode = CANVASMODE_NORMAL;
static int modestack[CANVASMODESTACKSIZE], modestackidx;

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

/* initializes mouse event handling and cursor shape on the canvas widget */
void CanvasInit(Widget canvas)
{
  canvasmode = CANVASMODE_NORMAL;
  modestackidx = 0;
  modestack[modestackidx] = canvasmode;
  cursordefined = FALSE;

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

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

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

/* creates and initializes the canvas window. */
Widget CreateCanvasWindow(Widget parent)
{
  Widget canvas = RenderCreateWindow(parent);
  Dimension hres, vres;

  /* get window size */
  XtVaGetValues(canvas,
		XmNwidth, &hres,
		XmNheight, &vres,
		NULL);
  Camera.hres = hres;
  Camera.vres = vres;

  /* 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;
}

/* set a new canvas mode */
static void CanvasSetMode(int mode)
{
  switch (mode) {
  case CANVASMODE_NORMAL:
    if (cursordefined) {
      XUndefineCursor(XtDisplay(canvas), XtWindow(canvas));
      cursordefined = FALSE;
    }
    
    canvasmode = CANVASMODE_NORMAL;
    break;
  case CANVASMODE_WORKING:
    /* clock cursor */
    XDefineCursor(display, XtWindow(canvas), working_cursor);
    XSync(display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_WORKING;
    break;
  case CANVASMODE_SELECT_PATCH:
    /* crosshair cursor */
    XDefineCursor(display, XtWindow(canvas), select_cursor);
    XSync(display, False);
    cursordefined = TRUE;
    
    canvasmode = CANVASMODE_SELECT_PATCH;
    break;
  case CANVASMODE_RENDER:
    /* spraycan cursor cursor */
    XDefineCursor(display, XtWindow(canvas), render_cursor);
    XSync(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 */
int CanvasGetMode(void)
{
  return canvasmode;
}

/* pushes the current canvas mode on the canvas mode stack, so it can be
 * restored later */
void CanvasPushMode(int 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 */
void CanvasResizeCallback(Widget canvas, XtPointer client_data, XtPointer call_data)
{
  Dimension newwidth, newheight;

  XtVaGetValues(canvas,
		XmNheight, &newheight,
		XmNwidth, &newwidth,
		NULL);

  CameraSet(&Camera, &Camera.eyep, &Camera.lookp, &Camera.updir,
	    Camera.fov, newwidth, newheight, &Camera.background);

  RenderScene();
}

/* 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;
}

/* 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)
{
  if (RedrawWorkProcInstalled) {
    XtRemoveWorkProc(RedrawWorkProcId);
    RedrawWorkProcInstalled = FALSE;
  }
  RenderScene();
}

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

  /* 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. This very nice, graphics library independent, trick was posted on the 
   * Mesa mailing list (mesa@iqm.unicamp.br) by Jeroen van der Zijp 
   * (jvz@cyberia.cfdrc.com). */
  if (!RedrawWorkProcInstalled) {
    RedrawWorkProcId = XtAppAddWorkProc(app_context, RedrawWorkProc, (XtPointer)NULL);
    RedrawWorkProcInstalled = TRUE;
  }
}

static float move_factor = 1.0;

static void CameraMoveFaster(void)
{
  move_factor *= 1.41;
}

static void CameraMoveSlower(void)
{
  move_factor /= 1.41;
}

/* 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;

  XtVaGetValues(canvas,
		XmNheight, &maxy,
		XmNwidth, &maxx,
		NULL);

  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 */
  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, move_factor * (float)(x - lastx)/(float)maxx * w);
    if (y != lasty) 
      CameraMoveUp(&Camera, move_factor * (float)(y - lasty)/(float)maxx * w / aspect);

    break;

  case 3:	/* first and second button pressed simultaneously */
    if (x != lastx)
      CameraMoveRight(&Camera, move_factor * (float)(x - lastx)/(float)maxx * w);
    if (y != lasty)
      CameraMoveForward(&Camera, move_factor * (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();
}

/* handles mouse events on the canvas window */
void CanvasMouseEvent(Widget canvas, XtPointer client_data, XEvent *event)
{
  static int lastx, lasty, lastpressedx, lastpressedy;
  static int buttonspressed = 0;
  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) {
#ifdef TWOBUTTON_MOUSE
    case Button1: buttonspressed |= 1; break;	/* left button */
    case Button3: buttonspressed |= 2; break;	/* right button */
    case Button2: buttonspressed |= 3; break;	/* when the two buttons are pressed simultaneously */
#else
    case Button1: buttonspressed = 1; break;
    case Button2: buttonspressed = 2; break;
    case Button3: buttonspressed = 3; break;
#endif
    default: break;
    }

    lastpressedx = x;
    lastpressedy = y;

    break;

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

    if (canvasmode == CANVASMODE_SELECT_PATCH) {
      switch (buttonspressed) {
      case 1:
	/* ... */
	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) {
#ifdef TWOBUTTON_MOUSE
    case Button1: buttonspressed &= ~1; break;
    case Button2: buttonspressed &= ~3; break;
    case Button3: buttonspressed &= ~2; break;
#else
    case Button1: buttonspressed &= ~1; break;
    case Button2: buttonspressed &= ~2; break;
    case Button3: break;
#endif
    default: break;
    }

    break;

#ifndef SLOW_RENDERER
  case MotionNotify:    /* do the motion immediately */
    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;
  }
}

/* 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);
#ifdef DEBUG
  buf[charcount] = '\0';

  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 KeyPress and KeyRelease events on the canvas window */
void CanvasKeyEvent(Widget canvas, XtPointer client_data, XEvent *event)
{
  switch (GetKey((XKeyEvent *)event)) {
  case XK_n: GoToNextView();
    break;
  case XK_p: GoToPreviousView();
    break;
  case XK_0: GoToViewNr(0);
    break;
  case XK_1: GoToViewNr(1);
    break;
  case XK_2: GoToViewNr(2);
    break;
  case XK_3: GoToViewNr(3);
    break;
  case XK_4: GoToViewNr(4);
    break;
  case XK_5: GoToViewNr(5);
    break;
  case XK_6: GoToViewNr(6);
    break;
  case XK_7: GoToViewNr(7);
    break;
  case XK_8: GoToViewNr(8);
    break;
  case XK_9: GoToViewNr(9);
    break;
  case XK_less:
    CameraMoveSlower();
    break;
  case XK_greater:
    CameraMoveFaster();
    break;
  default:
    /* ignore */
    break;
  }
}
