/* xscreensaver, Copyright (c) 1997, 2006 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.
 */

#include <math.h>
#include "screenhack.h"
#include "spline.h"
#include "alpha.h"


/* This is pretty compute-intensive, probably due to the large number of
   polygon fills.  I tried introducing a scaling factor to make the spline
   code emit fewer line segments, but that made the edges very rough.
   However, tuning *maxVelocity, *elasticity and *delay can result in much
   smoother looking animation.  I tuned these for a 1280x1024 Indy display,
   but I don't know whether these values will be reasonable for a slower
   machine...

   The more planes the better -- SGIs have a 12-bit pseudocolor display
   (4096 colormap cells) which is mostly useless, except for this program,
   where it means you can have 11 or 12 mutually-transparent objects instead
   of only 7 or 8.  But, if you are using the 12-bit visual, you should crank
   down the velocity and elasticity, or server slowness will cause the
   animation to look jerky (yes, it's sad but true, SGI's X server is
   perceptibly slower when using plane masks on a 12-bit visual than on an
   8-bit visual.)  Using -max-velocity 0.5 -elasticity 0.9 seems to work ok
   on my Indy R5k with visual 0x27 and the bottom-of-the-line 24-bit graphics
   board.

   It might look better if each blob had an outline, which was a *slightly*
   darker color than the center, to give them a bit more definition -- but
   that would mean using two planes per blob.  (Or maybe allocating the
   outline colors outside of the plane-space?  Then the outlines wouldn't be
   transparent, but maybe that wouldn't be so noticeable?)

   Oh, for an alpha channel... maybe I should rewrite this in GL.  Then the
   blobs could have thickness, and curved edges with specular reflections...
 */

#define SCALE       10000  /* fixed-point math, for sub-pixel motion */
#define DEF_COUNT   12	   /* When planes and count are 0, how many blobs. */


#define RAND(n) ((long) ((random() & 0x7fffffff) % ((long) (n))))
#define RANDSIGN() ((random() & 1) ? 1 : -1)

struct blob {
  long x, y;		/* position of midpoint */
  long dx, dy;		/* velocity and direction */
  double torque;	/* rotational speed */
  double th;		/* angle of rotation */
  long elasticity;	/* how fast they deform */
  long max_velocity;	/* speed limit */
  long min_r, max_r;	/* radius range */
  int npoints;		/* control points */
  long *r;		/* radii */
  spline *spline;
};

struct layer {
  int nblobs;
  struct blob **blobs;
  Pixmap pixmap;
  unsigned long pixel;
  GC gc;
};

enum goop_mode {
  transparent,
  opaque,
  xor,
  outline
};

struct goop {
  enum goop_mode mode;
  int width, height;
  int nlayers;
  struct layer **layers;
  unsigned long background;
  Pixmap pixmap;
  GC pixmap_gc;
  GC window_gc;
  Bool additive_p;
  Bool cmap_p;
  int delay;
};


static struct blob *
make_blob (Display *dpy, int maxx, int maxy, int size)
{
  struct blob *b = (struct blob *) calloc(1, sizeof(*b));
  int i;
  int mid;

  maxx *= SCALE;
  maxy *= SCALE;
  size *= SCALE;

  b->max_r = size/2;
  b->min_r = size/10;

  if (b->min_r < (5*SCALE)) b->min_r = (5*SCALE);
  mid = ((b->min_r + b->max_r) / 2);

  b->torque       = get_float_resource (dpy, "torque", "Torque");
  b->elasticity   = SCALE * get_float_resource (dpy, "elasticity", "Elasticity");
  b->max_velocity = SCALE * get_float_resource (dpy, "maxVelocity", "MaxVelocity");

  b->x = RAND(maxx);
  b->y = RAND(maxy);

  b->dx = RAND(b->max_velocity) * RANDSIGN();
  b->dy = RAND(b->max_velocity) * RANDSIGN();
  b->th = frand(M_PI+M_PI) * RANDSIGN();
  b->npoints = (random() % 5) + 5;

  b->spline = make_spline (b->npoints);
  b->r = (long *) malloc (sizeof(*b->r) * b->npoints);
  for (i = 0; i < b->npoints; i++)
    b->r[i] = ((random() % mid) + (mid/2)) * RANDSIGN();
  return b;
}

static void 
throb_blob (struct blob *b)
{
  int i;
  double frac = ((M_PI+M_PI) / b->npoints);

  for (i = 0; i < b->npoints; i++)
    {
      long r = b->r[i];
      long ra = (r > 0 ? r : -r);
      double th = (b->th > 0 ? b->th : -b->th);
      long x, y;

      /* place control points evenly around perimiter, shifted by theta */
      x = b->x + ra * cos (i * frac + th);
      y = b->y + ra * sin (i * frac + th);

      b->spline->control_x[i] = x / SCALE;
      b->spline->control_y[i] = y / SCALE;

      /* alter the radius by a random amount, in the direction in which
	 it had been going (the sign of the radius indicates direction.) */
      ra += (RAND(b->elasticity) * (r > 0 ? 1 : -1));
      r = ra * (r >= 0 ? 1 : -1);

      /* If we've reached the end (too long or too short) reverse direction. */
      if ((ra > b->max_r && r >= 0) ||
	  (ra < b->min_r && r < 0))
	r = -r;
      /* And reverse direction in mid-course once every 50 times. */
      else if (! (random() % 50))
	r = -r;

      b->r[i] = r;
    }
}

static void
move_blob (struct blob *b, int maxx, int maxy)
{
  maxx *= SCALE;
  maxy *= SCALE;

  b->x += b->dx;
  b->y += b->dy;

  /* If we've reached the edge of the box, reverse direction. */
  if ((b->x > maxx && b->dx >= 0) ||
      (b->x < 0    && b->dx < 0))
    {
      b->dx = -b->dx;
    }
  if ((b->y > maxy && b->dy >= 0) ||
      (b->y < 0    && b->dy < 0))
    {
      b->dy = -b->dy;
    }

  /* Alter velocity randomly. */
  if (! (random() % 10))
    {
      b->dx += (RAND(b->max_velocity/2) * RANDSIGN());
      b->dy += (RAND(b->max_velocity/2) * RANDSIGN());

      /* Throttle velocity */
      if (b->dx > b->max_velocity || b->dx < -b->max_velocity)
	b->dx /= 2;
      if (b->dy > b->max_velocity || b->dy < -b->max_velocity)
	b->dy /= 2;
    }

  {
    double th = b->th;
    double d = (b->torque == 0 ? 0 : frand(b->torque));
    if (th < 0)
      th = -(th + d);
    else
      th += d;

    if (th > (M_PI+M_PI))
      th -= (M_PI+M_PI);
    else if (th < 0)
      th += (M_PI+M_PI);

    b->th = (b->th > 0 ? th : -th);
  }

  /* Alter direction of rotation randomly. */
  if (! (random() % 100))
    b->th *= -1;
}

static void
draw_blob (Display *dpy, Drawable drawable, GC gc, struct blob *b,
	   Bool fill_p)
{
  compute_closed_spline (b->spline);
#ifdef DEBUG
  {
    int i;
    for (i = 0; i < b->npoints; i++)
      XDrawLine (dpy, drawable, gc, b->x/SCALE, b->y/SCALE,
		 b->spline->control_x[i], b->spline->control_y[i]);
  }
#else
  if (fill_p)
    XFillPolygon (dpy, drawable, gc, b->spline->points, b->spline->n_points,
		  Nonconvex, CoordModeOrigin);
  else
#endif
    XDrawLines (dpy, drawable, gc, b->spline->points, b->spline->n_points,
		CoordModeOrigin);
}


static struct layer *
make_layer (Display *dpy, Window window, int width, int height, int nblobs)
{
  int i;
  struct layer *layer = (struct layer *) calloc(1, sizeof(*layer));
  int blob_min, blob_max;
  XGCValues gcv;
  layer->nblobs = nblobs;

  layer->blobs = (struct blob **) malloc(sizeof(*layer->blobs)*layer->nblobs);

  blob_max = (width < height ? width : height) / 2;
  blob_min = (blob_max * 2) / 3;
  for (i = 0; i < layer->nblobs; i++)
    layer->blobs[i] = make_blob (dpy, width, height,
				 (random() % (blob_max-blob_min)) + blob_min);

  layer->pixmap = XCreatePixmap (dpy, window, width, height, 1);
  layer->gc = XCreateGC (dpy, layer->pixmap, 0, &gcv);

# ifdef HAVE_COCOA
  jwxyz_XSetAlphaAllowed (dpy, layer->gc, True);
# endif /* HAVE_COCOA */

  return layer;
}


#ifndef HAVE_COCOA
static void
draw_layer_plane (Display *dpy, struct layer *layer, int width, int height)
{
  int i;
  for (i = 0; i < layer->nblobs; i++)
    {
      throb_blob (layer->blobs[i]);
      move_blob (layer->blobs[i], width, height);
      draw_blob (dpy, layer->pixmap, layer->gc, layer->blobs[i], True);
    }
}
#endif /* !HAVE_COCOA */


static void
draw_layer_blobs (Display *dpy, Drawable drawable, GC gc,
		  struct layer *layer, int width, int height,
		  Bool fill_p)
{
  int i;
  for (i = 0; i < layer->nblobs; i++)
    {
      draw_blob (dpy, drawable, gc, layer->blobs[i], fill_p);
      throb_blob (layer->blobs[i]);
      move_blob (layer->blobs[i], width, height);
    }
}


static struct goop *
make_goop (Screen *screen, Visual *visual, Window window, Colormap cmap,
	   int width, int height, long depth)
{
  Display *dpy = DisplayOfScreen (screen);
  int i;
  struct goop *goop = (struct goop *) calloc(1, sizeof(*goop));
  XGCValues gcv;
  int nblobs = get_integer_resource (dpy, "count", "Count");

  unsigned long *plane_masks = 0;
# ifndef HAVE_COCOA
  unsigned long base_pixel = 0;
# endif
  char *s;

  s = get_string_resource (dpy, "mode", "Mode");
  goop->mode = transparent;
  if (!s || !*s || !strcasecmp (s, "transparent"))
    ;
  else if (!strcasecmp (s, "opaque"))
    goop->mode = opaque;
  else if (!strcasecmp (s, "xor"))
    goop->mode = xor;
  else
    fprintf (stderr, "%s: bogus mode: \"%s\"\n", progname, s);

  goop->delay = get_integer_resource (dpy, "delay", "Integer");

  goop->width = width;
  goop->height = height;

  goop->nlayers = get_integer_resource (dpy, "planes", "Planes");
  if (goop->nlayers <= 0)
    goop->nlayers = (random() % (depth-2)) + 2;
  goop->layers = (struct layer **) malloc(sizeof(*goop->layers)*goop->nlayers);

  goop->additive_p = get_boolean_resource (dpy, "additive", "Additive");
  goop->cmap_p = has_writable_cells (screen, visual);

  if (mono_p && goop->mode == transparent)
    goop->mode = opaque;

# ifndef HAVE_COCOA
  /* Try to allocate some color planes before committing to nlayers.
   */
  if (goop->mode == transparent)
    {
      int nplanes = goop->nlayers;
      allocate_alpha_colors (screen, visual, cmap,
                             &nplanes, goop->additive_p, &plane_masks,
			     &base_pixel);
      if (nplanes > 1)
	goop->nlayers = nplanes;
      else
	{
	  fprintf (stderr,
         "%s: couldn't allocate any color planes; turning transparency off.\n",
		   progname);
	  goop->mode = opaque;
	}
    }
# endif /* !HAVE_COCOA */

  {
    int lblobs[32];
    int total = DEF_COUNT;
    memset (lblobs, 0, sizeof(lblobs));
    if (nblobs <= 0)
      while (total)
	for (i = 0; total && i < goop->nlayers; i++)
	  lblobs[i]++, total--;
    for (i = 0; i < goop->nlayers; i++)
      goop->layers[i] = make_layer (dpy, window, width, height, 
				    (nblobs > 0 ? nblobs : lblobs[i]));
  }

# ifndef HAVE_COCOA
  if (goop->mode == transparent && plane_masks)
    {
      for (i = 0; i < goop->nlayers; i++)
	goop->layers[i]->pixel = base_pixel | plane_masks[i];
      goop->background = base_pixel;
    }
# endif /* !HAVE_COCOA */

  if (plane_masks)
    free (plane_masks);

# ifndef HAVE_COCOA
  if (goop->mode != transparent)
# endif /* !HAVE_COCOA */
    {
      XColor color;
      color.flags = DoRed|DoGreen|DoBlue;

      goop->background =
	get_pixel_resource (dpy,cmap, "background", "Background");

      for (i = 0; i < goop->nlayers; i++)
	{
	  int H = random() % 360;			   /* range 0-360    */
	  double S = ((double) (random()%70) + 30)/100.0;  /* range 30%-100% */
	  double V = ((double) (random()%34) + 66)/100.0;  /* range 66%-100% */
	  hsv_to_rgb (H, S, V, &color.red, &color.green, &color.blue);
	  if (XAllocColor (dpy, cmap, &color))
	    goop->layers[i]->pixel = color.pixel;
	  else
	    goop->layers[i]->pixel =
	      WhitePixelOfScreen(DefaultScreenOfDisplay(dpy));
# ifdef HAVE_COCOA
          if (goop->mode == transparent)
            {
              /* give a non-opaque alpha to the color */
              unsigned long pixel = goop->layers[i]->pixel;
              unsigned long amask = BlackPixelOfScreen (0);
              unsigned long a = (0xBBBBBBBB & amask);
              pixel = (pixel & (~amask)) | a;
              goop->layers[i]->pixel = pixel;
            }
# endif /* HAVE_COCOA */
	}
    }

  goop->pixmap = XCreatePixmap (dpy, window, width, height,
				(goop->mode == xor ? 1L : depth));

  gcv.background = goop->background;
  gcv.foreground = get_pixel_resource (dpy, cmap, "foreground", "Foreground");
  gcv.line_width = get_integer_resource (dpy, "thickness","Thickness");
  goop->pixmap_gc = XCreateGC (dpy, goop->pixmap, GCLineWidth, &gcv);
  goop->window_gc = XCreateGC (dpy, window, GCForeground|GCBackground, &gcv);

# ifdef HAVE_COCOA
  jwxyz_XSetAlphaAllowed (dpy, goop->pixmap_gc, True);
# endif /* HAVE_COCOA */
  
  return goop;
}

static void *
goop_init (Display *dpy, Window window)
{
  XWindowAttributes xgwa;
  XGetWindowAttributes (dpy, window, &xgwa);
  return make_goop (xgwa.screen, xgwa.visual, window, xgwa.colormap,
		    xgwa.width, xgwa.height, xgwa.depth);
}

static unsigned long
goop_draw (Display *dpy, Window window, void *closure)
{
  struct goop *goop = (struct goop *) closure;
  int i;

  switch (goop->mode)
    {
# ifndef HAVE_COCOA
    case transparent:

      for (i = 0; i < goop->nlayers; i++)
	draw_layer_plane (dpy, goop->layers[i], goop->width, goop->height);

      XSetForeground (dpy, goop->pixmap_gc, goop->background);
      XSetFunction (dpy, goop->pixmap_gc, GXcopy);
      XSetPlaneMask (dpy, goop->pixmap_gc, AllPlanes);
      XFillRectangle (dpy, goop->pixmap, goop->pixmap_gc, 0, 0,
		      goop->width, goop->height);

      XSetForeground (dpy, goop->pixmap_gc, ~0L);

      if (!goop->cmap_p && !goop->additive_p)
        {
          int j;
          for (i = 0; i < goop->nlayers; i++)
            for (j = 0; j < goop->layers[i]->nblobs; j++)
              draw_blob (dpy, goop->pixmap, goop->pixmap_gc,
                         goop->layers[i]->blobs[j], True);
          XSetFunction (dpy, goop->pixmap_gc, GXclear);
        }

      for (i = 0; i < goop->nlayers; i++)
	{
	  XSetPlaneMask (dpy, goop->pixmap_gc, goop->layers[i]->pixel);
	  draw_layer_blobs (dpy, goop->pixmap, goop->pixmap_gc,
			    goop->layers[i], goop->width, goop->height,
			    True);
	}
      XCopyArea (dpy, goop->pixmap, window, goop->window_gc, 0, 0,
		 goop->width, goop->height, 0, 0);
      break;
#endif /* !HAVE_COCOA */

    case xor:
      XSetFunction (dpy, goop->pixmap_gc, GXcopy);
      XSetForeground (dpy, goop->pixmap_gc, 0);
      XFillRectangle (dpy, goop->pixmap, goop->pixmap_gc, 0, 0,
		      goop->width, goop->height);
      XSetFunction (dpy, goop->pixmap_gc, GXxor);
      XSetForeground (dpy, goop->pixmap_gc, 1);
      for (i = 0; i < goop->nlayers; i++)
	draw_layer_blobs (dpy, goop->pixmap, goop->pixmap_gc,
			  goop->layers[i], goop->width, goop->height,
			  (goop->mode != outline));
      XCopyPlane (dpy, goop->pixmap, window, goop->window_gc, 0, 0,
		  goop->width, goop->height, 0, 0, 1L);
      break;

# ifdef HAVE_COCOA
    case transparent:
# endif
    case opaque:
    case outline:
      XSetForeground (dpy, goop->pixmap_gc, goop->background);
      XFillRectangle (dpy, goop->pixmap, goop->pixmap_gc, 0, 0,
		      goop->width, goop->height);
      for (i = 0; i < goop->nlayers; i++)
	{
	  XSetForeground (dpy, goop->pixmap_gc, goop->layers[i]->pixel);
	  draw_layer_blobs (dpy, goop->pixmap, goop->pixmap_gc,
			    goop->layers[i], goop->width, goop->height,
			    (goop->mode != outline));
	}
      XCopyArea (dpy, goop->pixmap, window, goop->window_gc, 0, 0,
		 goop->width, goop->height, 0, 0);
      break;

    default:
      abort ();
      break;
    }
  return goop->delay;
}

static void
goop_reshape (Display *dpy, Window window, void *closure, 
                 unsigned int w, unsigned int h)
{
  /* #### write me */
}

static Bool
goop_event (Display *dpy, Window window, void *closure, XEvent *event)
{
  return False;
}

static void
goop_free (Display *dpy, Window window, void *closure)
{
}




static const char *goop_defaults [] = {
  ".background:		black",
  ".foreground:		yellow",
  "*delay:		12000",
  "*additive:		true",
  "*mode:		transparent",
  "*count:		0",
  "*planes:		0",
  "*thickness:		5",
  "*torque:		0.0075",
  "*elasticity:		0.9",
  "*maxVelocity:	0.5",
  0
};

static XrmOptionDescRec goop_options [] = {
  { "-delay",		".delay",	XrmoptionSepArg, 0 },
  { "-count",		".count",	XrmoptionSepArg, 0 },
  { "-planes",		".planes",	XrmoptionSepArg, 0 },
  { "-mode",		".mode",	XrmoptionSepArg, 0 },
  { "-xor",		".mode",	XrmoptionNoArg, "xor" },
  { "-transparent",	".mode",	XrmoptionNoArg, "transparent" },
  { "-opaque",		".mode",	XrmoptionNoArg, "opaque" },
  { "-additive",	".additive",	XrmoptionNoArg, "True" },
  { "-subtractive",	".additive",	XrmoptionNoArg, "false" },
  { "-thickness",	".thickness",	XrmoptionSepArg, 0 },
  { "-torque",		".torque",	XrmoptionSepArg, 0 },
  { "-elasticity",	".elasticity",	XrmoptionSepArg, 0 },
  { "-max-velocity",	".maxVelocity",	XrmoptionSepArg, 0 },
  { 0, 0, 0, 0 }
};

XSCREENSAVER_MODULE ("Goop", goop)
