/* xscreensaver, Copyright (c) 1997-2008 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 *
 * Written as an Xlib program some time in 1997.
 * Rewritten as an OpenGL program 24-Aug-2008.
 */

/*
  Currently, we do this:

    - start pieces off screen and move them in.
    - when they land, they fill the puzzle grid with a shuffled
      puzzle (pieces are rotated, too).
    - swap random pairs of pieces until the puzzle is solved.
    - scatter the pieces off screen (resulting in black).
    - load new image and repeat.

  Another idea would be to show the puzzle being solved the way
  a person would do it:

    - start off with black screen.
    - assume knowledge of size of grid (position of corners).
    - find a corner piece, and place it.
    - while puzzle unsolved:
      - pick a random piece;
      - if it is the correct piece for any open edge, place it;
      - if it fits physically in any rotation at any open edge,
        place it, then toss it back (show the fake-out).
      - if it doesn't fit at all, don't animate it at all.

   This would take a long time to solve, I think...

   An even harder idea would involve building up completed "clumps"
   and sliding them around (a coral growth / accretion approach)
 */

#define DEF_SPEED        "1.0"
#define DEF_COMPLEXITY   "1.0"
#define DEF_RESOLUTION   "100"
#define DEF_THICKNESS    "0.06"
#define DEF_WOBBLE       "True"
#define DEF_DEBUG        "False"

#define DEFAULTS  "*delay:		20000	\n" \
		  "*showFPS:		False	\n" \
		  "*wireframe:		False	\n" \
		  "*desktopGrabber:   xscreensaver-getimage -no-desktop %s\n" \
		  "*grabDesktopImages:	False	\n" \
		  "*chooseRandomImages:	True	\n"


# define refresh_jigsaw 0
# define release_jigsaw 0
#undef countof
#define countof(x) (sizeof((x))/sizeof((*x)))

#include "xlockmore.h"
#include "rotator.h"
#include "gltrackball.h"
#include "spline.h"
#include "normals.h"
#include "grab-ximage.h"

#undef BELLRAND
#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)

#ifdef USE_GL /* whole file */

#define TOP    0
#define RIGHT  1
#define BOTTOM 2
#define LEFT   3

#define IN    -1
#define FLAT   0
#define OUT    1

typedef struct jigsaw_configuration jigsaw_configuration;

typedef struct {
  double x,y,z,r;	/* position and Z rotation (in degrees) */
} XYZR;


typedef struct {
  jigsaw_configuration *jc;
  int edge[4];
  GLuint dlist;
  int polys;

  XYZR home;		/* correct position in puzzle */
  XYZR current;		/* where it is right now */
  XYZR from, to;	/* in transit from A to B */
  double tick;		/* 0-1.0, how far from A to B */
  double arc_height;	/* height of apex of curved path from A to B */
  double tilt, max_tilt;

} puzzle_piece;


struct jigsaw_configuration {
  GLXContext *glx_context;
  trackball_state *trackball;
  rotator *rot;
  Bool button_down_p;

  int puzzle_width;
  int puzzle_height;
  puzzle_piece *puzzle;

  enum { PUZZLE_LOADING,
         PUZZLE_UNSCATTER,
         PUZZLE_SOLVE, 
         PUZZLE_SCATTER } state;
  double pausing;
  double tick_speed;

  GLuint texid;
  GLfloat tex_x, tex_y, tex_width, tex_height, aspect;

  GLuint line_thickness;
};

static jigsaw_configuration *sps = NULL;

static GLfloat speed;
static GLfloat complexity_arg;
static int resolution_arg;
static GLfloat thickness_arg;
static Bool wobble_p;
static Bool debug_p;

static XrmOptionDescRec opts[] = {
  { "-speed",      ".speed",       XrmoptionSepArg, 0 },
  { "-complexity", ".complexity",  XrmoptionSepArg, 0 },
  { "-resolution", ".resolution",  XrmoptionSepArg, 0 },
  { "-thickness",  ".thickness",   XrmoptionSepArg, 0 },
  { "-wobble",     ".wobble",      XrmoptionNoArg, "True" },
  { "+wobble",     ".wobble",      XrmoptionNoArg, "False" },
  { "-debug",      ".debug",       XrmoptionNoArg, "True" },
};

static argtype vars[] = {
  {&speed,          "speed",       "Speed",       DEF_SPEED,      t_Float},
  {&complexity_arg, "complexity",  "Complexity",  DEF_COMPLEXITY, t_Float},
  {&resolution_arg, "resolution",  "Resolution",  DEF_RESOLUTION, t_Int},
  {&thickness_arg,  "thickness",   "Thickness",   DEF_THICKNESS,  t_Float},
  {&wobble_p,       "wobble",      "Wobble",      DEF_WOBBLE,     t_Bool},
  {&debug_p,        "debug",       "Debug",       DEF_DEBUG,      t_Bool},
};

ENTRYPOINT ModeSpecOpt jigsaw_opts = {countof(opts), opts, countof(vars), vars, NULL};


/* Returns a spline describing one edge of a puzzle piece of the given length.
 */
static spline *
make_puzzle_curve (int pixels)
{
  double x0 = 0.0000, y0 =  0.0000;
  double x1 = 0.3333, y1 =  0.1000;
  double x2 = 0.4333, y2 =  0.0333;
  double x3 = 0.4666, y3 = -0.0666;
  double x4 = 0.3333, y4 = -0.1666;
  double x5 = 0.3666, y5 = -0.2900;
  double x6 = 0.5000, y6 = -0.3333;

  spline *s = make_spline(20);
  s->n_controls = 0;

# define PT(x,y) \
    s->control_x[s->n_controls] = pixels * (x); \
    s->control_y[s->n_controls] = pixels * (y); \
    s->n_controls++
  PT (  x0, y0);
  PT (  x1, y1);
  PT (  x2, y2);
  PT (  x3, y3);
  PT (  x4, y4);
  PT (  x5, y5);
  PT (  x6, y6);
  PT (1-x5, y5);
  PT (1-x4, y4);
  PT (1-x3, y3);
  PT (1-x2, y2);
  PT (1-x1, y1);
  PT (1-x0, y0);
# undef PT

  compute_spline (s);
  return s;
}


static void
tess_error_cb (GLenum errorCode)
{
  fprintf (stderr, "%s: tesselation error: %s\n",
           progname, gluErrorString(errorCode));
  exit (0);
}


static void
tess_combine_cb (GLdouble coords[3], GLdouble *d[4], GLfloat w[4], 
                 GLdouble **dataOut)
{
  GLdouble *new = (GLdouble *) malloc (3 * sizeof(*new));
  new[0] = coords[0];
  new[1] = coords[1];
  new[2] = coords[2];
  *dataOut = new;
}


static void
tess_vertex_cb (void *vertex_data, void *closure)
{
  puzzle_piece *p = (puzzle_piece *) closure;
  GLdouble *v = (GLdouble *) vertex_data;
  GLdouble x = v[0];
  GLdouble y = v[1];
  GLdouble z = v[2];

  if (p)
    {
      GLfloat pw = p->jc->puzzle_width;
      GLfloat ph = p->jc->puzzle_height;

      GLfloat xx = x / (GLfloat) resolution_arg;  /* 0-1 from piece origin */
      GLfloat yy = y / (GLfloat) resolution_arg;
      GLdouble tx = (p->home.x + xx)      / pw;   /* 0-1 from puzzle origin */
      GLdouble ty = (ph - p->home.y - yy) / ph;

      tx = p->jc->tex_x + (tx * p->jc->tex_width);
      ty = p->jc->tex_y + (ty * p->jc->tex_height);

      glTexCoord2d (tx, ty);
    }

  glVertex3d (x, y, z);
}



/* Draws a puzzle piece.  The top/right/bottom/left_type args
   indicate the direction the tabs point: 1 for out, -1 for in, 0 for flat.
 */
static int
draw_piece (jigsaw_configuration *jc, puzzle_piece *p,
            int resolution, GLfloat thickness,
            int top_type, int right_type,
            int bottom_type, int left_type,
            Bool wire)
{
  spline *s = make_puzzle_curve (resolution);
  GLdouble *pts = (GLdouble *) malloc (s->n_points * 4 * 3 * sizeof(*pts));
  int polys = 0;
  int i, o;
  GLdouble z = resolution * thickness;

  o = 0;
  if (top_type == 0) {
    pts[o++] = 0;
    pts[o++] = 0; 
    pts[o++] = z;

    pts[o++] = resolution;
    pts[o++] = 0;
    pts[o++] = z;
  } else {
    for (i = 0; i < s->n_points; i++) {
      pts[o++] = s->points[i].x;
      pts[o++] = s->points[i].y * top_type;
      pts[o++] = z;
    }
  }

  if (right_type == 0) {
    pts[o++] = resolution;
    pts[o++] = resolution;
    pts[o++] = z;
  } else {
    for (i = 1; i < s->n_points; i++) {
      pts[o++] = resolution + s->points[i].y * (-right_type);
      pts[o++] = s->points[i].x;
      pts[o++] = z;
    }
  }

  if (bottom_type == 0) {
    pts[o++] = 0;
    pts[o++] = resolution;
    pts[o++] = z;
  } else {
    for (i = 1; i < s->n_points; i++) {
      pts[o++] = s->points[s->n_points-i-1].x;
      pts[o++] = resolution + s->points[s->n_points-i-1].y * (-bottom_type);
      pts[o++] = z;
    }
  }

  if (left_type == 0) {
    pts[o++] = 0;
    pts[o++] = 0;
    pts[o++] = z;
  } else {
    for (i = 1; i < s->n_points; i++) {
      pts[o++] = s->points[s->n_points-i-1].y * left_type;
      pts[o++] = s->points[s->n_points-i-1].x;
      pts[o++] = z;
    }
  }

  free_spline (s);

  { GLfloat s = 1.0 / resolution; glScalef (s, s, s); }

  glPolygonMode (GL_FRONT_AND_BACK, wire ? GL_LINE : GL_FILL);

  if (wire)
    {
      glDisable (GL_TEXTURE_2D);
      glDisable (GL_BLEND);
      glDisable (GL_LIGHTING);
    }
  else
    {
# ifndef  _GLUfuncptr
#  define _GLUfuncptr void(*)(void)
# endif
      GLUtesselator *tess = gluNewTess();
      gluTessCallback(tess, GLU_TESS_BEGIN,      (_GLUfuncptr)glBegin);
      gluTessCallback(tess, GLU_TESS_VERTEX_DATA,(_GLUfuncptr)tess_vertex_cb);
      gluTessCallback(tess, GLU_TESS_END,        (_GLUfuncptr)glEnd);
      gluTessCallback(tess, GLU_TESS_COMBINE,    (_GLUfuncptr)tess_combine_cb);
      gluTessCallback(tess, GLU_TESS_ERROR,      (_GLUfuncptr)tess_error_cb);

      /* front face */
      glEnable (GL_TEXTURE_2D);
      glEnable (GL_BLEND);
      glEnable (GL_LIGHTING);
      glBindTexture(GL_TEXTURE_2D, jc->texid);
      glFrontFace (GL_CCW);
      glNormal3f (0, 0, 1);
      gluTessBeginPolygon (tess, p);
      gluTessBeginContour (tess);
      for (i = 0; i < o; i += 3)
        {
          GLdouble *p = pts + i;
          gluTessVertex (tess, p, p);
          polys++;  /* not quite right but close */
        }
      gluTessEndContour(tess);
      gluTessEndPolygon(tess);

      /* back face */
      glDisable (GL_TEXTURE_2D);
      glFrontFace (GL_CW);
      glNormal3f (0, 0, -1);
      gluTessBeginPolygon (tess, 0);
      gluTessBeginContour (tess);
      for (i = 0; i < o; i += 3)
        {
          GLdouble *p = pts + i;
          p[2] = -p[2];
          gluTessVertex (tess, p, p);
          polys++;  /* not quite right but close */
        }
      gluTessEndContour(tess);
      gluTessEndPolygon(tess);
      gluDeleteTess(tess);
    }

  /* side faces */
  glFrontFace (GL_CCW);
  glBegin (wire ? GL_LINES : GL_QUAD_STRIP);
  for (i = 0; i < o; i += 3)
    {
      int j = (i+o-3) % o;
      int k = (i+3)   % o;
      GLdouble *p  = pts + i;
      GLdouble *pj = pts + j;
      GLdouble *pk = pts + k;

      do_normal  (pj[0], pj[1], -pj[2],
                  pj[0], pj[1],  pj[2],
                  pk[0], pk[1],  pk[2]);

      glVertex3f (p[0], p[1], -p[2]);
      glVertex3f (p[0], p[1],  p[2]);
      polys++;
    }
  glEnd();

  if (! wire)
    glColor3f (0.3, 0.3, 0.3);

  glDisable (GL_TEXTURE_2D);
  glDisable (GL_LIGHTING);
  glLineWidth (jc->line_thickness);

  glBegin (GL_LINE_LOOP);
  for (i = 0; i < o; i += 3)
    glVertex3f (pts[i], pts[i+1], pts[i+2]);
  glEnd();
  polys += o/3;

  glBegin (GL_LINE_LOOP);
  for (i = 0; i < o; i += 3)
    glVertex3f (pts[i], pts[i+1], -pts[i+2]);
  glEnd();
  polys += o/3;

  free (pts);

  return polys;
}


static void
free_puzzle_grid (jigsaw_configuration *jc)
{
  int i;
  for (i = 0; i < jc->puzzle_width * jc->puzzle_height; i++)
    glDeleteLists (jc->puzzle[i].dlist,  1);
  free (jc->puzzle);
  jc->puzzle = 0;
  jc->puzzle_width = 0;
  jc->puzzle_height = 0;
}


static void
make_puzzle_grid (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int wire = MI_IS_WIREFRAME(mi);
  int x, y;
  GLfloat size = (8 + (random() % 8)) * complexity_arg;

  if (jc->puzzle)
    free_puzzle_grid (jc);

  if (wire)
    jc->aspect = MI_WIDTH(mi) / (float) MI_HEIGHT(mi);

  if (jc->aspect >= 1.0)
    {
      jc->puzzle_width  = size;
      jc->puzzle_height = (size + 0.5) / jc->aspect;
    }
  else
    {
      jc->puzzle_width  = (size + 0.5) * jc->aspect;
      jc->puzzle_height = size;
    }

  if (jc->puzzle_width  < 1) jc->puzzle_width  = 1;
  if (jc->puzzle_height < 1) jc->puzzle_height = 1;

  if (debug_p)
    fprintf (stderr, "%s: grid  %4d x %-4d               (%.2f)\n", progname,
             jc->puzzle_width, jc->puzzle_height,
             ((float) jc->puzzle_width / jc->puzzle_height));

  jc->puzzle = (puzzle_piece *) 
    calloc (jc->puzzle_width * (jc->puzzle_height+1), sizeof(*jc->puzzle));

  /* Randomize the right and bottom edge of each piece.
     Match the left edge of the piece to the right to our right edge.
     Match the top edge of the piece to the bottom to our bottom edge.
   */
  for (y = 0; y < jc->puzzle_height; y++)
    for (x = 0; x < jc->puzzle_width; x++)
      {
        puzzle_piece *p = &jc->puzzle [y     * jc->puzzle_width + x];
        puzzle_piece *r = &jc->puzzle [y     * jc->puzzle_width + x+1];
        puzzle_piece *b = &jc->puzzle [(y+1) * jc->puzzle_width + x];
        p->edge[RIGHT]  = (random() & 1) ? IN : OUT;
        p->edge[BOTTOM] = (random() & 1) ? IN : OUT;
        r->edge[LEFT]   = p->edge[RIGHT]  == IN ? OUT : IN;
        b->edge[TOP]    = p->edge[BOTTOM] == IN ? OUT : IN;
      }

  /* tell each piece where it belongs. */
  for (y = 0; y < jc->puzzle_height; y++)
    for (x = 0; x < jc->puzzle_width; x++)
      {
        puzzle_piece *p = &jc->puzzle [y * jc->puzzle_width + x];
        p->jc = jc;
        p->home.x = x;
        p->home.y = y;
        p->current = p->home;

        /* make sure the outer border is flat */
        if (p->home.x == 0)  p->edge[LEFT] = FLAT;
        if (p->home.y == 0)  p->edge[TOP]  = FLAT;
        if (p->home.x == jc->puzzle_width-1)  p->edge[RIGHT]  = FLAT;
        if (p->home.y == jc->puzzle_height-1) p->edge[BOTTOM] = FLAT;

        /* generate the polygons */
        p->dlist = glGenLists (1);
        check_gl_error ("generating lists");
        if (p->dlist <= 0) abort();

        glNewList (p->dlist, GL_COMPILE);
        p->polys += draw_piece (jc, p,
                                resolution_arg, thickness_arg,
                                p->edge[TOP], p->edge[RIGHT],
                                p->edge[BOTTOM], p->edge[LEFT], 
                                wire);
        glEndList();
      }
}


static void shuffle_grid (ModeInfo *mi);


static void
image_loaded_cb (const char *filename, XRectangle *geometry,
                 int image_width, int image_height, 
                 int texture_width, int texture_height,
                 void *closure)
{
  ModeInfo *mi = (ModeInfo *) closure;
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];

  jc->tex_x      = geometry->x      / (float) texture_width;
  jc->tex_y      = geometry->y      / (float) texture_height;
  jc->tex_width  = geometry->width  / (float) texture_width; 
  jc->tex_height = geometry->height / (float) texture_height;
  jc->aspect     = geometry->width  / (float) geometry->height;

  if (debug_p)
    {
      fprintf (stderr, "%s: image %s\n", progname, 
               (filename ? filename : "(null)"));
      fprintf (stderr, "%s: image %4d x %-4d + %4d + %-4d (%.2f)\n", progname,
               geometry->width, geometry->height, geometry->x, geometry->y,
               (float) geometry->width / geometry->height);
      fprintf (stderr, "%s: tex   %4d x %-4d\n", progname,
               texture_width, texture_height);
      fprintf (stderr, "%s: tex   %4.2f x %4.2f + %4.2f + %4.2f (%.2f)\n",
               progname,
               jc->tex_width, jc->tex_height, jc->tex_x, jc->tex_y,
               (jc->tex_width / jc->tex_height) *
               (texture_width / texture_height));
    }

  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

  make_puzzle_grid (mi);
}


static void
load_image (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  load_texture_async (mi->xgwa.screen, mi->window,
                      *jc->glx_context, 0, 0, 
                      False, jc->texid,
                      image_loaded_cb, mi);
}


/* Whether the two pieces are the same shape, when the second piece
   is rotated by the given degrees.
 */
static Bool
same_shape (puzzle_piece *p0, puzzle_piece *p1, int rotated_by)
{
  switch (rotated_by) 
    {
    case 0:
      return (p0->edge[0] == p1->edge[0] &&
              p0->edge[1] == p1->edge[1] &&
              p0->edge[2] == p1->edge[2] &&
              p0->edge[3] == p1->edge[3]);
    case 90:
      return (p0->edge[0] == p1->edge[1] &&
              p0->edge[1] == p1->edge[2] &&
              p0->edge[2] == p1->edge[3] &&
              p0->edge[3] == p1->edge[0]);
    case 180:
      return (p0->edge[0] == p1->edge[2] &&
              p0->edge[1] == p1->edge[3] &&
              p0->edge[2] == p1->edge[0] &&
              p0->edge[3] == p1->edge[1]);
    case 270:
      return (p0->edge[0] == p1->edge[3] &&
              p0->edge[1] == p1->edge[0] &&
              p0->edge[2] == p1->edge[1] &&
              p0->edge[3] == p1->edge[2]);
    default:
      abort();
    }
}


/* Returns the proper rotation for the piece at the given position. 
 */
static int
proper_rotation (jigsaw_configuration *jc, puzzle_piece *p, 
                 double x, double y)
{
  puzzle_piece *p1;
  int cx = x;
  int cy = y;
  if (cx != x) abort();  /* must be in integral position! */
  if (cy != y) abort();
  p1 = &jc->puzzle [cy * jc->puzzle_width + cx];
  if (same_shape (p, p1, 0))   return 0;
  if (same_shape (p, p1, 90))  return 90;
  if (same_shape (p, p1, 180)) return 180;
  if (same_shape (p, p1, 270)) return 270;
  abort();		/* these two pieces don't match in any rotation! */
}


/* Returns the piece currently at the given position. 
 */
static puzzle_piece *
piece_at (jigsaw_configuration *jc, double x, double y)
{
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;
  int cx = x;
  int cy = y;
  if (cx != x) abort();  /* must be in integral position! */
  if (cy != y) abort();

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p = &jc->puzzle [i];
      if (p->current.x == cx &&
          p->current.y == cy)
        return p;
    }
  abort();   /* no piece at that position? */
}


static void
shuffle_grid (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int max_tries = jc->puzzle_width * jc->puzzle_height;
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p0 = &jc->puzzle [i];
      puzzle_piece *p1 = 0;
      int k;

      for (k = 0; k < max_tries; k++)
        {
          p1 = &jc->puzzle [random() % npieces];
          if (same_shape (p0, p1, 0))   break;
          if (same_shape (p0, p1, 90))  break;
          if (same_shape (p0, p1, 180)) break;
          if (same_shape (p0, p1, 270)) break;
          p1 = 0;  /* mismatch */
        }
      if (p1 && p0 != p1)
        {
          XYZR s;
          s = p0->current; p0->current = p1->current; p1->current = s;
          p0->current.r = 
            proper_rotation (jc, p0, p0->current.x, p0->current.y);
          p1->current.r = 
            proper_rotation (jc, p1, p1->current.x, p1->current.y);
        }
    }
}


/* We tend to accumulate floating point errors, e.g., z being 0.000001
   after a move.  This makes sure float values that should be integral are.
 */
static void
smooth_grid (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p = &jc->puzzle [i];
# define SMOOTH(P) \
        P.x = (int) (P.x + 0.5);                  \
        P.y = (int) (P.y + 0.5);                  \
        P.z = (int) (P.z + 0.5);                  \
        P.r = (int) (P.r + 0.5)
      SMOOTH(p->home);
      SMOOTH(p->current);
      SMOOTH(p->from);
      SMOOTH(p->to);
      if (p->tick <= 0.0001) p->tick = 0.0;
      if (p->tick >= 0.9999) p->tick = 1.0;
    }
}


static void
begin_scatter (ModeInfo *mi, Bool unscatter_p)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;
  XYZR ctr = { 0, };
  ctr.x = jc->puzzle_width  / 2;
  ctr.y = jc->puzzle_height / 2;

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p = &jc->puzzle [i];
      XYZ a;
      double d, r, th;
      p->tick = -frand(1.0);
      p->from = p->current;

      a.x = p->from.x - ctr.x;	/* position relative to center */
      a.y = p->from.y - ctr.y;

      r = sqrt (a.x*a.x + a.y*a.y);
      th = atan2 (a.x, a.y);

      d = MAX (jc->puzzle_width, jc->puzzle_height) * 2;
      r = r*r + d;

      p->to.x = ctr.x + (r * sin (th));
      p->to.y = ctr.y + (r * cos (th));
      p->to.z = p->from.z;
      p->to.r = ((int) p->from.r + (random() % 180)) % 360;
      p->arc_height = frand(10.0);

      if (unscatter_p)
        {
          XYZR s = p->to; p->to = p->from; p->from = s;
          p->current = p->from;
        }
    }
}


static Bool
solved_p (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p = &jc->puzzle [i];
      if (p->current.x != p->home.x ||
          p->current.y != p->home.y ||
          p->current.z != p->home.z)
        return False;
    }
  return True;
}


static void
move_one_piece (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;

  for (i = 0; i < npieces * 100; i++)  /* shouldn't take that long */
    {
      int i = random() % npieces;
      puzzle_piece *p0 = &jc->puzzle [i];
      puzzle_piece *p1;

      if (p0->current.x == p0->home.x &&
          p0->current.y == p0->home.y &&
          p0->current.z == p0->home.z)
        continue;		/* piece already solved - try again */

      /* swap with the piece occupying p0's home cell. */
      p1 = piece_at (jc, p0->home.x, p0->home.y);

      if (p0 == p1) abort();    /* should have caught this above */

      p0->tick = 0;
      p0->from = p0->current;
      p0->to   = p1->current;
      p0->to.r = proper_rotation (jc, p0, p0->to.x, p0->to.y);

      p1->tick = 0;
      p1->from = p1->current;
      p1->to   = p0->current;
      p1->to.r = proper_rotation (jc, p1, p1->to.x, p1->to.y);

      p0->arc_height = 0.5 + frand(3.0);
      p1->arc_height = 1.0 + frand(3.0);

# define RTILT(V) \
         V = 90 - BELLRAND(180);       \
         if (! (random() % 5)) V *= 2; \
         if (! (random() % 5)) V *= 2; \
         if (! (random() % 5)) V *= 2
      RTILT (p0->max_tilt);
      RTILT (p1->max_tilt);
# undef RTILT

      if (debug_p)
        fprintf (stderr, "%s: swapping %2d,%-2d with %2d,%d\n", progname,
                 (int) p0->from.x, (int) p0->from.y,
                 (int) p1->from.x, (int) p1->from.y);
      return;
    }

  abort();  /* infinite loop! */
}


static Bool
anim_tick (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  int npieces = jc->puzzle_width * jc->puzzle_height;
  int i;
  Bool finished_p = True;

  if (jc->pausing > 0)
    {
      jc->pausing -= jc->tick_speed * speed;
      if (debug_p && jc->pausing <= 0)
        fprintf (stderr, "%s:   (done pausing)\n", progname);
      return False;
    }

  for (i = 0; i < npieces; i++)
    {
      puzzle_piece *p = &jc->puzzle [i];
      double tt;

      if (p->tick >= 1.0) continue;	/* this piece is done */
      finished_p = False;		/* not done */

      p->tick += jc->tick_speed * speed;
      if (p->tick > 1.0) p->tick = 1.0;

      if (p->tick < 0.0) continue;	/* not yet started */

      tt = 1 - sin (M_PI/2 - p->tick * M_PI/2);

      p->current.x = p->from.x + ((p->to.x - p->from.x) * tt);
      p->current.y = p->from.y + ((p->to.y - p->from.y) * tt);
      p->current.z = p->from.z + ((p->to.z - p->from.z) * tt);
      p->current.r = p->from.r + ((p->to.r - p->from.r) * tt);

      p->current.z += p->arc_height * sin (p->tick * M_PI);

      p->tilt = p->max_tilt * sin (p->tick * M_PI);

    }

  return finished_p;
}


static void
animate (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  double slow = 0.01;
  double fast = 0.04;

  if (jc->button_down_p) return;

  switch (jc->state)
    {
    case PUZZLE_LOADING:
      if (!jc->puzzle) break;	/* still loading */
      jc->tick_speed = slow;
      shuffle_grid (mi);
      smooth_grid (mi);
      begin_scatter (mi, True);
      jc->pausing = 0;
      jc->state = PUZZLE_UNSCATTER;
      if (debug_p) fprintf (stderr, "%s: unscattering\n", progname);
      break;

    case PUZZLE_UNSCATTER:
      jc->tick_speed = slow;
      if (anim_tick (mi))
        {
          smooth_grid (mi);
          jc->pausing = 1.0;
          jc->state = PUZZLE_SOLVE;
          if (debug_p) fprintf (stderr, "%s: solving\n", progname);
        }
      break;

    case PUZZLE_SOLVE:
      jc->tick_speed = fast;
      if (anim_tick (mi))
        {
          smooth_grid (mi);
          if (solved_p (mi))
            {
              if (debug_p) fprintf (stderr, "%s: solved!\n", progname);
              begin_scatter (mi, False);
              jc->state = PUZZLE_SCATTER;
              jc->pausing = 3.0;
              if (debug_p) fprintf (stderr, "%s: scattering\n", progname);
            }
          else
            {
              move_one_piece (mi);
              jc->pausing = 0.3;
            }
        }
      break;

    case PUZZLE_SCATTER:
      jc->tick_speed = slow;
      if (anim_tick (mi))
        {
          free_puzzle_grid (jc);
          load_image (mi);
          jc->state = PUZZLE_LOADING;
          jc->pausing = 1.0;
          if (debug_p) fprintf (stderr, "%s: loading\n", progname);
        }
      break;

    default:
      abort();
    }
}


/* Window management, etc
 */
ENTRYPOINT void
reshape_jigsaw (ModeInfo *mi, int width, int height)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  GLfloat h = (GLfloat) height / (GLfloat) width;

  glViewport (0, 0, (GLint) width, (GLint) height);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective (30.0, 1/h, 1.0, 100.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt( 0.0, 0.0, 30.0,
             0.0, 0.0, 0.0,
             0.0, 1.0, 0.0);

  glClear(GL_COLOR_BUFFER_BIT);

  jc->line_thickness = (MI_IS_WIREFRAME (mi) ? 1 : MAX (1, height / 300.0));
}


ENTRYPOINT Bool
jigsaw_handle_event (ModeInfo *mi, XEvent *event)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];

  if (event->xany.type == ButtonPress &&
      event->xbutton.button == Button1)
    {
      jc->button_down_p = True;
      gltrackball_start (jc->trackball,
                         event->xbutton.x, event->xbutton.y,
                         MI_WIDTH (mi), MI_HEIGHT (mi));
      return True;
    }
  else if (event->xany.type == ButtonRelease &&
           event->xbutton.button == Button1)
    {
      jc->button_down_p = False;
      return True;
    }
  else if (event->xany.type == ButtonPress &&
           (event->xbutton.button == Button4 ||
            event->xbutton.button == Button5 ||
            event->xbutton.button == Button6 ||
            event->xbutton.button == Button7))
    {
      gltrackball_mousewheel (jc->trackball, event->xbutton.button, 10,
                              !!event->xbutton.state);
      return True;
    }
  else if (event->xany.type == MotionNotify &&
           jc->button_down_p)
    {
      gltrackball_track (jc->trackball,
                         event->xmotion.x, event->xmotion.y,
                         MI_WIDTH (mi), MI_HEIGHT (mi));
      return True;
    }
  else if (event->xany.type == KeyPress)
    {
      KeySym keysym;
      char c = 0;
      XLookupString (&event->xkey, &c, 1, &keysym, 0);

      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
        {
          begin_scatter (mi, False);
          jc->state = PUZZLE_SCATTER;
          return True;
        }
    }


  return False;
}


ENTRYPOINT void 
init_jigsaw (ModeInfo *mi)
{
  jigsaw_configuration *jc;
  int wire = MI_IS_WIREFRAME(mi);

  if (!sps) {
    sps = (jigsaw_configuration *)
      calloc (MI_NUM_SCREENS(mi), sizeof (jigsaw_configuration));
    if (!sps) {
      fprintf(stderr, "%s: out of memory\n", progname);
      exit(1);
    }
  }
  jc = &sps[MI_SCREEN(mi)];
  jc->glx_context = init_GL(mi);

  reshape_jigsaw (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  if (!wire)
    {
      GLfloat pos[4] = {0.05, 0.07, 1.00, 0.0};
      GLfloat amb[4] = {0.2, 0.2, 0.2, 1.0};
      GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
      GLfloat spc[4] = {0.0, 1.0, 1.0, 1.0};

      glLightfv(GL_LIGHT0, GL_POSITION, pos);
      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
    }

  jc->trackball = gltrackball_init ();
  jc->rot = make_rotator (0, 0, 0, 0, speed * 0.002, True);

  jc->state = PUZZLE_LOADING;

  resolution_arg /= complexity_arg;

  if (wire)
    make_puzzle_grid (mi);
  else
    load_image (mi);
}


ENTRYPOINT void
draw_jigsaw (ModeInfo *mi)
{
  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  int wire = MI_IS_WIREFRAME(mi);

  if (!jc->glx_context)
    return;

  animate (mi);

  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(jc->glx_context));

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();

  gltrackball_rotate (jc->trackball);

  if (wobble_p && jc->puzzle)
    {
      double x, y, z;
      double max = 60;
      get_position (jc->rot, &x, &y, &z, !jc->button_down_p);
      x = 1; /* always lean back */
      glRotatef (max/2 - x*max, 1, 0, 0);
      glRotatef (max/2 - z*max, 0, 1, 0);
    }

  mi->polygon_count = 0;

  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  glEnable(GL_LINE_SMOOTH);

  if (jc->puzzle)
    {
      GLfloat s = 14.0 / jc->puzzle_height;
      int x, y;

      glScalef (s, s, s);
      glTranslatef (-jc->puzzle_width / 2.0, -jc->puzzle_height / 2.0, 0);

      if (! wire)
        {
          glEnable (GL_TEXTURE_2D);
          glEnable (GL_BLEND);
          glEnable (GL_LIGHTING);
          glEnable (GL_LIGHT0);
          glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        }

      for (y = 0; y < jc->puzzle_height; y++)
        for (x = 0; x < jc->puzzle_width; x++)
          {
            puzzle_piece *p = &jc->puzzle [y * jc->puzzle_width + x];
            glPushMatrix();
            glTranslatef (p->current.x, p->current.y, p->current.z);
            glTranslatef (0.5, 0.5, 0);
            glRotatef (p->current.r, 0, 0, 1);
            glRotatef (p->tilt, 0, 1, 0);
            glTranslatef (-0.5, -0.5, 0);
            glCallList(p->dlist);
            mi->polygon_count += p->polys;
            glPopMatrix();
          }
    }

  glPopMatrix ();

  if (mi->fps_p) do_fps (mi);
  glFinish();

  glXSwapBuffers(dpy, window);
}

XSCREENSAVER_MODULE ("Jigsaw", jigsaw)

#endif /* USE_GL */
