/* petri, simulate mold in a petri dish. v2.7
 * by Dan Bornstein, danfuzz@milk.com
 * with help from Jamie Zawinski, jwz@jwz.org
 * Copyright (c) 1992-1999 Dan Bornstein.
 *
 * 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.
 *
 *
 * Brief description of options/resources:
 *
 * delay: the delay in microseconds between iterations
 * size: the size of a cell in pixels
 * count: the number of different kinds of mold (minimum: 2)
 * diaglim: the age limit for diagonal growth as a multiplier of orthogonal
 *   growth (minimum: 1, maximum 2). 1 means square growth, 1.414 
 *   (i.e., sqrt(2)) means approximately circular growth, 2 means diamond
 *   growth.
 * anychan: the chance (fraction, between 0 and 1) that at each iteration,
 *   any new cell will be born
 * minorchan: the chance (fraction, between 0 and 1) that, given that new
 *   cells will be added, that only two will be added (a minor cell birth
 *   event)
 * instantdeathchan: the chance (fraction, between 0 and 1) that, given
 *   that death and destruction will happen, that instead of using plague
 *   cells, death will be instantaneous
 * minlifespan: the minimum lifespan of a colony (before black death ensues)
 * maxlifespan: the maximum lifespan of a colony (before black death ensues)
 * minlifespeed: the minimum speed for living cells as a fraction of the
 *   maximum possible speed (fraction, between 0 and 1)
 * maxlifespeed: the maximum speed for living cells as a fraction of the
 *   maximum possible speed (fraction, between 0 and 1)
 * mindeathspeed: the minimum speed for black death cells as a fraction of the
 *   maximum possible speed (fraction, between 0 and 1)
 * maxdeathspeed: the maximum speed for black death cells as a fraction of the
 *   maximum possible speed (fraction, between 0 and 1)
 * originalcolors: if true, count must be 8 or less and the colors are a 
 *   fixed set of primary and secondary colors (the artist's original choices)
 *
 * Interesting settings:
 *
 *      petri -originalcolors -size 8
 *      petri -size 2
 *      petri -size 8 -diaglim 1.8
 *      petri -diaglim 1.1
 *
 *      petri -count 4 -anychan 0.01 -minorchan 1 \
 *              -minlifespan 2000 -maxlifespan 5000
 *
 *      petri -count 3 -anychan 1 -minlifespan 100000 \ 
 *              -instantdeathchan 0
 *
 *      petri -minlifespeed 0.02 -maxlifespeed 0.03 -minlifespan 1 \
 *              -maxlifespan 1 -instantdeathchan 0 -minorchan 0 \
 *              -anychan 0.3 -delay 4000
 */

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

#define FLOAT float
#define RAND_FLOAT (((FLOAT) (random() & 0xffff)) / ((FLOAT) 0x10000))

typedef struct cell_s 
{
    unsigned char col;              /*  0      */
    unsigned char isnext;           /*  1      */
    unsigned char nextcol;          /*  2      */
                                    /*  3      */
    struct cell_s *next;            /*  4      */
    struct cell_s *prev;            /*  8    - */
    FLOAT speed;                    /* 12      */
    FLOAT growth;                   /* 16 20 - */
    FLOAT nextspeed;                /* 20 28   */
                                    /* 24 36 - */
} cell;

struct state {
  Display *dpy;
  Window window;

  int arr_width;
  int arr_height;
  int count;

  cell *arr;
  cell *head;
  cell *tail;
  int blastcount;

  GC *coloredGCs;

  int windowWidth;
  int windowHeight;
  int xOffset;
  int yOffset;
  int xSize;
  int ySize;

  FLOAT orthlim;
  FLOAT diaglim;
  FLOAT anychan;
  FLOAT minorchan;
  FLOAT instantdeathchan;
  int minlifespan;
  int maxlifespan;
  FLOAT minlifespeed;
  FLOAT maxlifespeed;
  FLOAT mindeathspeed;
  FLOAT maxdeathspeed;
  Bool originalcolors;

  int warned;
  int delay;
};


#define cell_x(c) (st->arr_width ? ((c) - st->arr) % st->arr_width : 0)
#define cell_y(c) (st->arr_width ? ((c) - st->arr) / st->arr_width : 0)


static int random_life_value (struct state *st)
{
    return (int) ((RAND_FLOAT * (st->maxlifespan - st->minlifespan)) + st->minlifespan);
}

static void setup_random_colormap (struct state *st, XWindowAttributes *xgwa)
{
    XGCValues gcv;
    int lose = 0;
    int ncolors = st->count - 1;
    int n;
    XColor *colors = (XColor *) calloc (sizeof(*colors), st->count*2);
    
    colors[0].pixel = get_pixel_resource (st->dpy, xgwa->colormap,
                                          "background", "Background");
    
    make_random_colormap (xgwa->screen, xgwa->visual, xgwa->colormap,
			  colors+1, &ncolors, True, True, 0, True);
    if (ncolors < 1)
      {
        fprintf (stderr, "%s: couldn't allocate any colors\n", progname);
	exit (-1);
      }
    
    ncolors++;
    st->count = ncolors;
    
    memcpy (colors + st->count, colors, st->count * sizeof(*colors));
    colors[st->count].pixel = get_pixel_resource (st->dpy, xgwa->colormap,
                                              "foreground", "Foreground");
    
    for (n = 1; n < st->count; n++)
    {
	int m = n + st->count;
	colors[n].red = colors[m].red / 2;
	colors[n].green = colors[m].green / 2;
	colors[n].blue = colors[m].blue / 2;
	
	if (!XAllocColor (st->dpy, xgwa->colormap, &colors[n]))
	{
	    lose++;
	    colors[n] = colors[m];
	}
    }

    if (lose)
    {
	fprintf (stderr, 
		 "%s: unable to allocate %d half-intensity colors.\n",
		 progname, lose);
    }
    
    for (n = 0; n < st->count*2; n++) 
    {
	gcv.foreground = colors[n].pixel;
	st->coloredGCs[n] = XCreateGC (st->dpy, st->window, GCForeground, &gcv);
    }

    free (colors);
}

static void setup_original_colormap (struct state *st, XWindowAttributes *xgwa)
{
    XGCValues gcv;
    int lose = 0;
    int n;
    XColor *colors = (XColor *) calloc (sizeof(*colors), st->count*2);
    
    colors[0].pixel = get_pixel_resource (st->dpy, xgwa->colormap,
                                          "background", "Background");

    colors[st->count].pixel = get_pixel_resource (st->dpy, xgwa->colormap,
                                              "foreground", "Foreground");

    for (n = 1; n < st->count; n++)
    {
	int m = n + st->count;
	colors[n].red =   ((n & 0x01) != 0) * 0x8000;
	colors[n].green = ((n & 0x02) != 0) * 0x8000;
	colors[n].blue =  ((n & 0x04) != 0) * 0x8000;

	if (!XAllocColor (st->dpy, xgwa->colormap, &colors[n]))
	{
	    lose++;
	    colors[n] = colors[0];
	}

	colors[m].red   = colors[n].red + 0x4000;
	colors[m].green = colors[n].green + 0x4000;
	colors[m].blue  = colors[n].blue + 0x4000;

	if (!XAllocColor (st->dpy, xgwa->colormap, &colors[m]))
	{
	    lose++;
	    colors[m] = colors[st->count];
	}
    }

    if (lose)
    {
	fprintf (stderr, 
		 "%s: unable to allocate %d colors.\n",
		 progname, lose);
    }
    
    for (n = 0; n < st->count*2; n++) 
    {
	gcv.foreground = colors[n].pixel;
	st->coloredGCs[n] = XCreateGC (st->dpy, st->window, GCForeground, &gcv);
    }

    free (colors);
}

static void
setup_display (struct state *st)
{
    XWindowAttributes xgwa;

    int cell_size = get_integer_resource (st->dpy, "size", "Integer");
    int osize, alloc_size;
#if 0
    int oalloc;
#endif
    int mem_throttle = 0;
    char *s;

    XGetWindowAttributes (st->dpy, st->window, &xgwa);

    if (cell_size < 1) cell_size = 1;

    osize = cell_size;

    if (xgwa.width > 2560 || xgwa.height > 2560)
      cell_size *= 2;  /* Retina displays */

    s = get_string_resource (st->dpy, "memThrottle", "MemThrottle");
    if (s)
      {
        int n;
        char c;
        if (1 == sscanf (s, " %d M %c", &n, &c) ||
            1 == sscanf (s, " %d m %c", &n, &c))
          mem_throttle = n * (1 << 20);
        else if (1 == sscanf (s, " %d K %c", &n, &c) ||
                 1 == sscanf (s, " %d k %c", &n, &c))
          mem_throttle = n * (1 << 10);
        else if (1 == sscanf (s, " %d %c", &n, &c))
          mem_throttle = n;
        else
          {
            fprintf (stderr, "%s: invalid memThrottle \"%s\" (try \"10M\")\n",
                     progname, s);
            exit (1);
          }
        
        free (s);
      }

    st->originalcolors = get_boolean_resource (st->dpy, "originalcolors", "Boolean");

    st->count = get_integer_resource (st->dpy, "count", "Integer");
    if (st->count < 2) st->count = 2;

    /* number of colors can't be greater than the half depth of the screen. */
    if (st->count > (unsigned int) (1L << (xgwa.depth-1)))
      st->count = (unsigned int) (1L << (xgwa.depth-1));

    /* Actually, since cell->col is of type char, this has to be small. */
    if (st->count >= (unsigned int) (1L << ((sizeof(st->arr[0].col) * 8) - 1)))
      st->count = (unsigned int) (1L << ((sizeof(st->arr[0].col) * 8) - 1));


    if (st->originalcolors && (st->count > 8))
    {
	st->count = 8;
    }

    st->coloredGCs = (GC *) calloc (sizeof(GC), st->count * 2);

    st->diaglim  = get_float_resource (st->dpy, "diaglim", "Float");
    if (st->diaglim < 1.0)
    {
	st->diaglim = 1.0;
    }
    else if (st->diaglim > 2.0)
    {
	st->diaglim = 2.0;
    }
    st->diaglim *= st->orthlim;

    st->anychan  = get_float_resource (st->dpy, "anychan", "Float");
    if (st->anychan < 0.0)
    {
	st->anychan = 0.0;
    }
    else if (st->anychan > 1.0)
    {
	st->anychan = 1.0;
    }
    
    st->minorchan = get_float_resource (st->dpy, "minorchan","Float");
    if (st->minorchan < 0.0)
    {
	st->minorchan = 0.0;
    }
    else if (st->minorchan > 1.0)
    {
	st->minorchan = 1.0;
    }
    
    st->instantdeathchan = get_float_resource (st->dpy, "instantdeathchan","Float");
    if (st->instantdeathchan < 0.0)
    {
	st->instantdeathchan = 0.0;
    }
    else if (st->instantdeathchan > 1.0)
    {
	st->instantdeathchan = 1.0;
    }

    st->minlifespan = get_integer_resource (st->dpy, "minlifespan", "Integer");
    if (st->minlifespan < 1)
    {
	st->minlifespan = 1;
    }

    st->maxlifespan = get_integer_resource (st->dpy, "maxlifespan", "Integer");
    if (st->maxlifespan < st->minlifespan)
    {
	st->maxlifespan = st->minlifespan;
    }

    st->minlifespeed = get_float_resource (st->dpy, "minlifespeed", "Float");
    if (st->minlifespeed < 0.0)
    {
	st->minlifespeed = 0.0;
    }
    else if (st->minlifespeed > 1.0)
    {
	st->minlifespeed = 1.0;
    }

    st->maxlifespeed = get_float_resource (st->dpy, "maxlifespeed", "Float");
    if (st->maxlifespeed < st->minlifespeed)
    {
	st->maxlifespeed = st->minlifespeed;
    }
    else if (st->maxlifespeed > 1.0)
    {
	st->maxlifespeed = 1.0;
    }

    st->mindeathspeed = get_float_resource (st->dpy, "mindeathspeed", "Float");
    if (st->mindeathspeed < 0.0)
    {
	st->mindeathspeed = 0.0;
    }
    else if (st->mindeathspeed > 1.0)
    {
	st->mindeathspeed = 1.0;
    }

    st->maxdeathspeed = get_float_resource (st->dpy, "maxdeathspeed", "Float");
    if (st->maxdeathspeed < st->mindeathspeed)
    {
	st->maxdeathspeed = st->mindeathspeed;
    }
    else if (st->maxdeathspeed > 1.0)
    {
	st->maxdeathspeed = 1.0;
    }

    st->minlifespeed *= st->diaglim;
    st->maxlifespeed *= st->diaglim;
    st->mindeathspeed *= st->diaglim;
    st->maxdeathspeed *= st->diaglim;

    st->windowWidth = xgwa.width;
    st->windowHeight = xgwa.height;
    
    st->arr_width = st->windowWidth / cell_size;
    st->arr_height = st->windowHeight / cell_size;

    alloc_size = sizeof(cell) * st->arr_width * st->arr_height;
# if 0
    oalloc = alloc_size;
# endif

    if (mem_throttle > 0)
      while (cell_size < st->windowWidth/10 &&
             cell_size < st->windowHeight/10 &&
             alloc_size > mem_throttle)
        {
          cell_size++;
          st->arr_width = st->windowWidth / cell_size;
          st->arr_height = st->windowHeight / cell_size;
          alloc_size = sizeof(cell) * st->arr_width * st->arr_height;
        }

    if (osize != cell_size)
      {
# if 0
        if (!st->warned)
          {
            fprintf (stderr,
             "%s: throttling cell size from %d to %d because of %dM limit.\n",
                     progname, osize, cell_size, mem_throttle / (1 << 20));
            fprintf (stderr, "%s: %dx%dx%d = %.1fM, %dx%dx%d = %.1fM.\n",
                     progname,
                     st->windowWidth, st->windowHeight, osize,
                     ((float) oalloc) / (1 << 20),
                     st->windowWidth, st->windowHeight, cell_size,
                     ((float) alloc_size) / (1 << 20));
            st->warned = 1;
          }
# endif
      }

    st->xSize = st->arr_width ? st->windowWidth / st->arr_width : 0;
    st->ySize = st->arr_height ? st->windowHeight / st->arr_height : 0;
    if (st->xSize > st->ySize)
    {
	st->xSize = st->ySize;
    }
    else
    {
	st->ySize = st->xSize;
    }
    
    st->xOffset = (st->windowWidth - (st->arr_width * st->xSize)) / 2;
    st->yOffset = (st->windowHeight - (st->arr_height * st->ySize)) / 2;

    if (st->originalcolors)
    {
	setup_original_colormap (st, &xgwa);
    }
    else
    {
	setup_random_colormap (st, &xgwa);
    }
}

static void drawblock (struct state *st, int x, int y, unsigned char c)
{
  if (st->xSize == 1 && st->ySize == 1)
    XDrawPoint (st->dpy, st->window, st->coloredGCs[c], x + st->xOffset, y + st->yOffset);
  else
    XFillRectangle (st->dpy, st->window, st->coloredGCs[c],
		    x * st->xSize + st->xOffset, y * st->ySize + st->yOffset,
		    st->xSize, st->ySize);
}

static void setup_arr (struct state *st)
{
    int x, y;

    if (st->arr != NULL)
    {
	free (st->arr);
    }

    XFillRectangle (st->dpy, st->window, st->coloredGCs[0], 0, 0, 
		    st->windowWidth, st->windowHeight);
    
    if (!st->arr_width) st->arr_width = 1;
    if (!st->arr_height) st->arr_height = 1;

    st->arr = (cell *) calloc (sizeof(cell), st->arr_width * st->arr_height);  
    if (!st->arr)
      {
        fprintf (stderr, "%s: out of memory allocating %dx%d grid\n",
                 progname, st->arr_width, st->arr_height);
        exit (1);
      }

    for (y = 0; y < st->arr_height; y++)
    {
      int row = y * st->arr_width;
       for (x = 0; x < st->arr_width; x++) 
	{
	    st->arr[row+x].speed = 0.0;
	    st->arr[row+x].growth = 0.0;
	    st->arr[row+x].col = 0;
	    st->arr[row+x].isnext = 0;
	    st->arr[row+x].next = 0;
	    st->arr[row+x].prev = 0;
	}
    }

    if (st->head == NULL)
    {
	st->head = (cell *) malloc (sizeof (cell));
    }
    
    if (st->tail == NULL)
    {
	st->tail = (cell *) malloc (sizeof (cell));
    }

    st->head->next = st->tail;
    st->head->prev = st->head;
    st->tail->next = st->tail;
    st->tail->prev = st->head;

    st->blastcount = random_life_value (st);
}

static void newcell (struct state *st, cell *c, unsigned char col, FLOAT sp)
{
    if (! c) return;
    
    if (c->col == col) return;
    
    c->nextcol = col;
    c->nextspeed = sp;
    c->isnext = 1;
    
    if (c->prev == 0) {
	c->next = st->head->next;
	c->prev = st->head;
	st->head->next = c;
	c->next->prev = c;
    }
}

static void killcell (struct state *st, cell *c)
{
    c->prev->next = c->next;
    c->next->prev = c->prev;
    c->prev = 0;
    c->speed = 0.0;
    drawblock (st, cell_x(c), cell_y(c), c->col);
}


static void randblip (struct state *st, int doit)
{
    int n;
    int b = 0;
    if (!doit 
	&& (st->blastcount-- >= 0) 
	&& (RAND_FLOAT > st->anychan))
    {
	return;
    }
    
    if (st->blastcount < 0) 
    {
	b = 1;
	n = 2;
	st->blastcount = random_life_value (st);
	if (RAND_FLOAT < st->instantdeathchan)
	{
	    /* clear everything every so often to keep from getting into a
	     * rut */
	    setup_arr (st);
	    b = 0;
	}
    }
    else if (RAND_FLOAT <= st->minorchan) 
    {
	n = 2;
    }
    else 
    {
	n = random () % 3 + 3;
    }
    
    while (n--) 
    {
      int x = st->arr_width ? random () % st->arr_width : 0;
      int y = st->arr_height ? random () % st->arr_height : 0;
	int c;
	FLOAT s;
	if (b)
	{
	    c = 0;
	    s = RAND_FLOAT * (st->maxdeathspeed - st->mindeathspeed) + st->mindeathspeed;
	}
	else
	{
	    c = ((st->count - 1) ? random () % (st->count-1) : 0) + 1;
	    s = RAND_FLOAT * (st->maxlifespeed - st->minlifespeed) + st->minlifespeed;
	}
	newcell (st, &st->arr[y * st->arr_width + x], c, s);
    }
}

static void update (struct state *st)
{
    cell *a;
    
    for (a = st->head->next; a != st->tail; a = a->next) 
    {
	static const XPoint all_coords[] = {{-1, -1}, {-1, 1}, {1, -1}, {1, 1},
                                            {-1,  0}, { 1, 0}, {0, -1}, {0, 1},
                                            {99, 99}};

        const XPoint *coords = 0;

        if (a->speed == 0) continue;
        a->growth += a->speed;

	if (a->growth >= st->diaglim) 
	{
	    coords = all_coords;
	}
        else if (a->growth >= st->orthlim)
	{
	    coords = &all_coords[4];
	}
	else
	{
	    continue;
	}

	while (coords->x != 99)
	{
	    int x = cell_x(a) + coords->x;
	    int y = cell_y(a) + coords->y;
	    coords++;

	    if (x < 0) x = st->arr_width - 1;
	    else if (x >= st->arr_width) x = 0;
	    
	    if (y < 0) y = st->arr_height - 1;
	    else if (y >= st->arr_height) y = 0;
	    
	    newcell (st, &st->arr[y * st->arr_width + x], a->col, a->speed);
	}

	if (a->growth >= st->diaglim) 
	    killcell (st, a);
    }

    randblip (st, (st->head->next) == st->tail);

    for (a = st->head->next; a != st->tail; a = a->next)
    {
	if (a->isnext) 
	{
	    a->isnext = 0;
	    a->speed = a->nextspeed;
	    a->growth = 0.0;
	    a->col = a->nextcol;
	    drawblock (st, cell_x(a), cell_y(a), a->col + st->count);
	}
    }
}

static void *
petri_init (Display *dpy, Window win)
{
    struct state *st = (struct state *) calloc (1, sizeof(*st));
    st->dpy = dpy;
    st->window = win;

    st->delay = get_integer_resource (st->dpy, "delay", "Delay");
    st->orthlim = 1;

    setup_display (st);
    setup_arr (st);
    randblip (st, 1);
    
    return st;
}

static unsigned long
petri_draw (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  update (st);
  return st->delay;
}

static void
petri_reshape (Display *dpy, Window window, void *closure, 
                 unsigned int w, unsigned int h)
{
}

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

static void
petri_free (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  if (st->arr) free (st->arr);
  if (st->head) free (st->head);
  if (st->tail) free (st->tail);
  if (st->coloredGCs) {
    int i;
    for (i = 0; i < st->count*2; i++)
      XFreeGC (st->dpy, st->coloredGCs[i]);
    free (st->coloredGCs);
  }
  free (st);
}


static const char *petri_defaults [] = {
  ".background:		black",
  ".foreground:		white",
  "*fpsSolid:		true",
  "*delay:		10000",
  "*count:		20",
  "*size:		2",
  "*diaglim:		1.414",
  "*anychan:		0.0015",
  "*minorchan:		0.5",
  "*instantdeathchan:	0.2",
  "*minlifespan:	500",
  "*maxlifespan:	1500",
  "*minlifespeed:	0.04",
  "*maxlifespeed:	0.13",
  "*mindeathspeed:	0.42",
  "*maxdeathspeed:	0.46",
  "*originalcolors:	false",
  "*memThrottle:        22M",	/* don't malloc more than this much.
                                   Scale the pixels up if necessary. */
#ifdef HAVE_MOBILE
  "*ignoreRotation:     True",
#endif
    0
};

static XrmOptionDescRec petri_options [] = {
  { "-delay",		 ".delay",		XrmoptionSepArg, 0 },
  { "-size",		 ".size",		XrmoptionSepArg, 0 },
  { "-count",		 ".count",		XrmoptionSepArg, 0 },
  { "-diaglim",		 ".diaglim",		XrmoptionSepArg, 0 },
  { "-anychan",		 ".anychan",		XrmoptionSepArg, 0 },
  { "-minorchan",	 ".minorchan",		XrmoptionSepArg, 0 },
  { "-instantdeathchan", ".instantdeathchan",	XrmoptionSepArg, 0 },
  { "-minlifespan",	 ".minlifespan",	XrmoptionSepArg, 0 },
  { "-maxlifespan",	 ".maxlifespan",	XrmoptionSepArg, 0 },
  { "-minlifespeed",	 ".minlifespeed",	XrmoptionSepArg, 0 },
  { "-maxlifespeed",	 ".maxlifespeed",	XrmoptionSepArg, 0 },
  { "-mindeathspeed",	 ".mindeathspeed",	XrmoptionSepArg, 0 },
  { "-maxdeathspeed",	 ".maxdeathspeed",	XrmoptionSepArg, 0 },
  { "-originalcolors",	 ".originalcolors",	XrmoptionNoArg,  "true" },
  { "-mem-throttle",	 ".memThrottle",	XrmoptionSepArg,  0 },
  { 0, 0, 0, 0 }
};


XSCREENSAVER_MODULE ("Petri", petri)
