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

/* 18-Sep-97: Johannes Keukelaar <johannes@nada.kth.se>: Added some color.
 * Using -mono gives the old behaviour.  (Modified by jwz.)
 */
/*   Flying through an asteroid field.  Based on TI Explorer Lisp code by 
   John Nguyen <johnn@hx.lcs.mit.edu>
 */

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

#define MIN_ROCKS 1
#define MIN_DEPTH 2		/* rocks disappear when they get this close */
#define MAX_DEPTH 60		/* this is where rocks appear */
#define MIN_SIZE 3		/* how small where pixmaps are not used */
#define MAX_SIZE 400		/* how big (in pixels) rocks are at depth 1 */
#define DEPTH_SCALE 100		/* how many ticks there are between depths */
#define SIN_RESOLUTION 1000

#define MAX_DEP 0.3		/* how far the displacement can be (percent) */
#define DIRECTION_CHANGE_RATE 60
#define MAX_DEP_SPEED 5		/* Maximum speed for movement */
#define MOVE_STYLE 0		/* Only 0 and 1. Distinguishes the fact that
				   these are the rocks that are moving (1) 
				   or the rocks source (0). */

/* there's not much point in the above being user-customizable, but those
   numbers might want to be tweaked for displays with an order of magnitude
   higher resolution or compute power.
 */

struct state {
  Display *dpy;
  Window window;

  double sins [SIN_RESOLUTION];
  double coss [SIN_RESOLUTION];
  double depths [(MAX_DEPTH + 1) * DEPTH_SCALE];

  int width, height, midx, midy;
  int dep_x, dep_y;
  int ncolors;
  XColor *colors;
  float max_dep;
  GC erase_gc;
  GC *draw_gcs;
  Bool rotate_p;
  Bool move_p;
  int speed;
  Bool threed;
  GC threed_left_gc, threed_right_gc;
  double threed_delta;

  struct rock *rocks;
  int nrocks;
  Pixmap pixmaps [MAX_SIZE];
  int delay;

  int move_current_dep[2];
  int move_speed[2];
  short move_direction[2];
  int move_limit[2];

  int current_delta;	/* observer Z rotation */
  int  new_delta;
  int  dchange_tick;
};



#define GETZDIFF(z) \
        (st->threed_delta * 40.0 * \
	 (1.0 - ((MAX_DEPTH * DEPTH_SCALE / 2) / \
		 ((z) + 20.0 * DEPTH_SCALE))))

struct rock {
  int real_size;
  int r;
  int theta;
  int depth;
  int size, x, y;
  int diff;
  int color;
};

static void rock_compute (struct state *, struct rock *);
static void rock_draw (struct state *, struct rock *, Bool draw_p);

static void
rock_reset (struct state *st, struct rock *rock)
{
  rock->real_size = MAX_SIZE;
  rock->r = (SIN_RESOLUTION * 0.7) + (random () % (30 * SIN_RESOLUTION));
  rock->theta = random () % SIN_RESOLUTION;
  rock->depth = MAX_DEPTH * DEPTH_SCALE;
  rock->color = random() % st->ncolors;
  rock_compute (st, rock);
  rock_draw (st, rock, True);
}

static void
rock_tick (struct state *st, struct rock *rock, int d)
{
  if (rock->depth > 0)
    {
      rock_draw (st, rock, False);
      rock->depth -= st->speed;
      if (st->rotate_p)
        {
	  rock->theta = (rock->theta + d) % SIN_RESOLUTION;
        }
      while (rock->theta < 0)
	rock->theta += SIN_RESOLUTION;
      if (rock->depth < (MIN_DEPTH * DEPTH_SCALE))
	rock->depth = 0;
      else
	{
	  rock_compute (st, rock);
	  rock_draw (st, rock, True);
	}
    }
  else if ((random () % 40) == 0)
    rock_reset (st, rock);
}

static void
rock_compute (struct state *st, struct rock *rock)
{
  double factor = st->depths [rock->depth];
  double rsize = rock->real_size * factor;

  rock->size = (int) (rsize + 0.5);
  rock->diff = (int) GETZDIFF(rock->depth);
  rock->x = st->midx + (st->coss [rock->theta] * rock->r * factor);
  rock->y = st->midy + (st->sins [rock->theta] * rock->r * factor);

  if (st->move_p)
    {
      double move_factor = (((double) MOVE_STYLE) -
			    (((double) rock->depth) /
			     (((double) (MAX_DEPTH + 1)) *
			      ((double) DEPTH_SCALE))));
      /* move_factor is 0 when the rock is close, 1 when far */
      rock->x += (((double) st->dep_x) * move_factor);
      rock->y += (((double) st->dep_y) * move_factor);
    }
}

static void
rock_draw (struct state *st, struct rock *rock, Bool draw_p)
{
  GC gc = (draw_p 
	   ? (st->threed ? st->erase_gc : st->draw_gcs[rock->color])
	   : st->erase_gc);

  if (rock->x <= 0 || rock->y <= 0 || rock->x >= st->width || rock->y >= st->height)
    {
      /* this means that if a rock were to go off the screen at 12:00, but
	 would have been visible at 3:00, it won't come back once the observer
	 rotates around so that the rock would have been visible again.
	 Oh well.
       */
      if (!st->move_p)
	rock->depth = 0;
      return;
    }
  if (rock->size <= 1)
    {
      if (st->threed)
	{
	  if (draw_p) gc = st->threed_left_gc;
	  XDrawPoint (st->dpy, st->window, gc, rock->x - rock->diff, rock->y);
	  if (draw_p) gc = st->threed_right_gc;
	  XDrawPoint (st->dpy, st->window, gc, rock->x + rock->diff, rock->y);
	}
      else
	{
	  XDrawPoint (st->dpy, st->window, gc, rock->x, rock->y);
	}
    }
  else if (rock->size <= MIN_SIZE || !draw_p)
    {
      if (st->threed)
	{
	  if (draw_p) gc = st->threed_left_gc;
	  XFillRectangle(st->dpy, st->window, gc,
			 rock->x - rock->size / 2 - rock->diff,
			 rock->y - rock->size / 2,
			 rock->size, rock->size);
	  if (draw_p) gc = st->threed_right_gc;
	  XFillRectangle(st->dpy, st->window, gc,
			 rock->x - rock->size / 2 + rock->diff,
			 rock->y - rock->size / 2,
			 rock->size, rock->size);
	}
      else
	{
	  XFillRectangle (st->dpy, st->window, gc,
			  rock->x - rock->size/2, rock->y - rock->size/2,
			  rock->size, rock->size);
	}
    }
  else if (rock->size < MAX_SIZE)
    {
      if (st->threed)
	{
	  gc = st->threed_left_gc;
	  XCopyPlane(st->dpy, st->pixmaps[rock->size], st->window, gc,
		     0, 0, rock->size, rock->size,
		     rock->x - rock->size / 2 - rock->diff,
		     rock->y - rock->size / 2, 1L);
	  gc = st->threed_right_gc;
	  XCopyPlane(st->dpy, st->pixmaps[rock->size], st->window, gc,
		     0, 0, rock->size, rock->size,
		     rock->x - rock->size / 2 + rock->diff,
		     rock->y - rock->size / 2, 1L);
	}
      else
	{
	  XCopyPlane (st->dpy, st->pixmaps [rock->size], st->window, gc,
		      0, 0, rock->size, rock->size,
		      rock->x - rock->size/2, rock->y - rock->size/2,
		      1L);
	}
    }
}


static void
init_pixmaps (struct state *st)
{
  int i;
  XGCValues gcv;
  GC fg_gc = 0, bg_gc = 0;
  st->pixmaps [0] = st->pixmaps [1] = 0;
  for (i = MIN_DEPTH; i < MAX_SIZE; i++)
    {
      int w = (1+(i/32))<<5; /* server might be faster if word-aligned */
      int h = i;
      Pixmap p = XCreatePixmap (st->dpy, st->window, w, h, 1);
      XPoint points [7];
      st->pixmaps [i] = p;
      if (! p)
	{
	  fprintf (stderr, "%s: couldn't allocate pixmaps", progname);
	  exit (1);
	}
      if (! fg_gc)
	{	/* must use drawable of pixmap, not window (fmh) */
	  gcv.foreground = 1;
	  fg_gc = XCreateGC (st->dpy, p, GCForeground, &gcv);
	  gcv.foreground = 0;
	  bg_gc = XCreateGC (st->dpy, p, GCForeground, &gcv);
	}
      XFillRectangle (st->dpy, p, bg_gc, 0, 0, w, h);
      points [0].x = i * 0.15; points [0].y = i * 0.85;
      points [1].x = i * 0.00; points [1].y = i * 0.20;
      points [2].x = i * 0.30; points [2].y = i * 0.00;
      points [3].x = i * 0.40; points [3].y = i * 0.10;
      points [4].x = i * 0.90; points [4].y = i * 0.10;
      points [5].x = i * 1.00; points [5].y = i * 0.55;
      points [6].x = i * 0.45; points [6].y = i * 1.00;
      XFillPolygon (st->dpy, p, fg_gc, points, 7, Nonconvex, CoordModeOrigin);
    }
  XFreeGC (st->dpy, fg_gc);
  XFreeGC (st->dpy, bg_gc);
}


static int
compute_move(struct state *st, int axe)			/* 0 for x, 1 for y */
{
  int change = 0;

  st->move_limit[0] = st->midx;
  st->move_limit[1] = st->midy;

  st->move_current_dep[axe] += st->move_speed[axe];	/* We adjust the displacement */

  if (st->move_current_dep[axe] > (int) (st->move_limit[axe] * st->max_dep))
    {
      if (st->move_current_dep[axe] > st->move_limit[axe])
	st->move_current_dep[axe] = st->move_limit[axe];
      st->move_direction[axe] = -1;
    }			/* This is when we reach the upper screen limit */
  if (st->move_current_dep[axe] < (int) (-st->move_limit[axe] * st->max_dep))
    {
      if (st->move_current_dep[axe] < -st->move_limit[axe])
	st->move_current_dep[axe] = -st->move_limit[axe];
      st->move_direction[axe] = 1;
    }			/* This is when we reach the lower screen limit */
  if (st->move_direction[axe] == 1)	/* We adjust the speed */
    st->move_speed[axe] += 1;
  else if (st->move_direction[axe] == -1)
    st->move_speed[axe] -= 1;

  if (st->move_speed[axe] > MAX_DEP_SPEED)
    st->move_speed[axe] = MAX_DEP_SPEED;
  else if (st->move_speed[axe] < -MAX_DEP_SPEED)
    st->move_speed[axe] = -MAX_DEP_SPEED;

  if (st->move_p && !(random() % DIRECTION_CHANGE_RATE))
    {
      /* We change direction */
      change = random() & 1;
      if (change != 1)
	{
	  if (st->move_direction[axe] == 0)
	    st->move_direction[axe] = change - 1;	/* 0 becomes either 1 or -1 */
	  else
	    st->move_direction[axe] = 0;			/* -1 or 1 become 0 */
	}
    }
  return (st->move_current_dep[axe]);
}

static void
tick_rocks (struct state *st, int d)
{
  int i;

  if (st->move_p)
    {
      st->dep_x = compute_move(st, 0);
      st->dep_y = compute_move(st, 1);
    }

  for (i = 0; i < st->nrocks; i++)
    rock_tick (st, &st->rocks [i], d);
}


static unsigned long
rocks_draw (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  if (st->current_delta != st->new_delta)
    {
      if (st->dchange_tick++ == 5)
	{
	  st->dchange_tick = 0;
	  if (st->current_delta < st->new_delta)
	    st->current_delta++;
	  else
	    st->current_delta--;
	}
    }
  else
    {
      if (! (random() % 50))
	{
	  st->new_delta = ((random() % 11) - 5);
	  if (! (random() % 10))
	    st->new_delta *= 5;
	}
    }
  tick_rocks (st, st->current_delta);

  return st->delay;
}

static void *
rocks_init (Display *d, Window w)
{
  struct state *st = (struct state *) calloc (1, sizeof(*st));
  int i;
  XGCValues gcv;
  Colormap cmap;
  XWindowAttributes xgwa;
  unsigned int bg;
  st->dpy = d;
  st->window = w;
  XGetWindowAttributes (st->dpy, st->window, &xgwa);

  st->width = xgwa.width;
  st->height = xgwa.height;
  st->midx = st->width/2;
  st->midy = st->height/2;

  cmap = xgwa.colormap;
  st->delay = get_integer_resource (st->dpy, "delay", "Integer");
  if (st->delay < 0) st->delay = 0;
  st->speed = get_integer_resource (st->dpy, "speed", "Integer");
  if (st->speed < 1) st->speed = 1;
  if (st->speed > 100) st->speed = 100;
  st->rotate_p = get_boolean_resource (st->dpy, "rotate", "Boolean");
  st->move_p = get_boolean_resource (st->dpy, "move", "Boolean");
  if (mono_p)
    st->ncolors = 2;
  else
    st->ncolors = get_integer_resource (st->dpy, "colors", "Colors");

  if (st->ncolors < 2)
    {
      st->ncolors = 2;
      mono_p = True;
  }

  st->colors = (XColor *) malloc(st->ncolors * sizeof(*st->colors));
  st->draw_gcs = (GC *) malloc(st->ncolors * sizeof(*st->draw_gcs));

  bg = get_pixel_resource (st->dpy, cmap, "background", "Background");
  st->colors[0].pixel = bg;
  st->colors[0].flags = DoRed|DoGreen|DoBlue;
  XQueryColor(st->dpy, cmap, &st->colors[0]);

  st->ncolors--;
  make_random_colormap(xgwa.screen, xgwa.visual, cmap,
                       st->colors+1, &st->ncolors, True,
		       True, 0, True);
  st->ncolors++;

  if (st->ncolors < 2)
    {
      st->ncolors = 2;
      mono_p = True;
  }

  if (mono_p)
    {
      unsigned int fg = get_pixel_resource(st->dpy, cmap, "foreground", "Foreground");
      st->colors[1].pixel = fg;
      st->colors[1].flags = DoRed|DoGreen|DoBlue;
      XQueryColor(st->dpy, cmap, &st->colors[1]);
      gcv.foreground = fg;
      gcv.background = bg;
      st->draw_gcs[0] = XCreateGC (st->dpy, st->window, GCForeground|GCBackground, &gcv);
      st->draw_gcs[1] = st->draw_gcs[0];
    }
  else
    for( i = 0; i < st->ncolors; i++ )
      {
	gcv.foreground = st->colors[i].pixel;
	gcv.background = bg;
	st->draw_gcs[i] = XCreateGC (st->dpy, st->window, GCForeground|GCBackground, &gcv);
      }

  gcv.foreground = bg;
  st->erase_gc = XCreateGC (st->dpy, st->window, GCForeground|GCBackground, &gcv);

  st->max_dep = (st->move_p ? MAX_DEP : 0);

  for (i = 0; i < SIN_RESOLUTION; i++)
    {
      st->sins [i] = sin ((((double) i) / (SIN_RESOLUTION / 2)) * M_PI);
      st->coss [i] = cos ((((double) i) / (SIN_RESOLUTION / 2)) * M_PI);
    }
  /* we actually only need i/speed of these, but wtf */
  for (i = 1; i < (sizeof (st->depths) / sizeof (st->depths[0])); i++)
    st->depths [i] = atan (((double) 0.5) / (((double) i) / DEPTH_SCALE));
  st->depths [0] = M_PI/2; /* avoid division by 0 */

  st->threed = get_boolean_resource(st->dpy, "use3d", "Boolean");
  if (st->threed)
    {
      gcv.background = bg;
      gcv.foreground = get_pixel_resource (st->dpy, cmap, "left3d", "Foreground");
      st->threed_left_gc = XCreateGC (st->dpy, st->window, GCForeground|GCBackground,&gcv);
      gcv.foreground = get_pixel_resource (st->dpy, cmap, "right3d", "Foreground");
      st->threed_right_gc = XCreateGC (st->dpy, st->window,GCForeground|GCBackground,&gcv);
      st->threed_delta = get_float_resource(st->dpy, "delta3d", "Integer");
    }

  /* don't want any exposure events from XCopyPlane */
  for( i = 0; i < st->ncolors; i++)
    XSetGraphicsExposures (st->dpy, st->draw_gcs[i], False);
  XSetGraphicsExposures (st->dpy, st->erase_gc, False);

  st->nrocks = get_integer_resource (st->dpy, "count", "Count");
  if (st->nrocks < 1) st->nrocks = 1;
  st->rocks = (struct rock *) calloc (st->nrocks, sizeof (struct rock));
  init_pixmaps (st);
  XClearWindow (st->dpy, st->window);
  return st;
}

static void
rocks_reshape (Display *dpy, Window window, void *closure, 
                 unsigned int w, unsigned int h)
{
  struct state *st = (struct state *) closure;
  st->width = w;
  st->height = h;
  st->midx = st->width/2;
  st->midy = st->height/2;
}

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

static void
rocks_free (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  int i;
  for (i = 0; i < MAX_SIZE; i++)
    if (st->pixmaps[i]) XFreePixmap (dpy, st->pixmaps[i]);
  for (i = 0; i < st->ncolors; i++)
    if (st->draw_gcs[i]) XFreeGC (dpy, st->draw_gcs[i]);
  XFreeGC (dpy, st->erase_gc);
  free (st->colors);
  free (st->rocks);
  free (st->draw_gcs);
  free (st);
}



static const char *rocks_defaults [] = {
/*  ".lowrez:     true", */
  ".background:	Black",
  ".foreground:	#E9967A",
  "*fpsSolid:	true",
  "*colors:	5",
  "*count:	100",
  "*delay:	50000",
  "*speed:	100",
  "*rotate:     true",
  "*move:       true",
  "*use3d:      False",
  "*left3d:	Blue",
  "*right3d:	Red",
  "*delta3d:	1.5",
#ifdef HAVE_MOBILE
  "*ignoreRotation: True",
#endif
  0
};

static XrmOptionDescRec rocks_options [] = {
  { "-count",		".count",	XrmoptionSepArg, 0 },
  { "-rotate",          ".rotate",      XrmoptionNoArg,  "true" },
  { "-no-rotate",        ".rotate",      XrmoptionNoArg,  "false" },
  { "-move",            ".move",        XrmoptionNoArg,  "true" },
  { "-no-move",          ".move",        XrmoptionNoArg,  "false" },
  { "-delay",		".delay",	XrmoptionSepArg, 0 },
  { "-speed",		".speed",	XrmoptionSepArg, 0 },
  {"-3d",		".use3d",	XrmoptionNoArg, "True"},
  {"-no-3d",		".use3d",	XrmoptionNoArg, "False"},
  {"-left3d",		".left3d",	XrmoptionSepArg, 0 },
  {"-right3d",		".right3d",	XrmoptionSepArg, 0 },
  {"-delta3d",		".delta3d",	XrmoptionSepArg, 0 },
  { "-colors",		".colors",	XrmoptionSepArg, 0 },
  { 0, 0, 0, 0 }
};

XSCREENSAVER_MODULE ("Rocks", rocks)
