/* xscreensaver, Copyright (c) 1992-2018 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 "screenhack.h"

enum { DOWN = 0, LEFT, UP, RIGHT };
enum { VERTICAL, HORIZONTAL };

struct state {
  Display *dpy;
  Window window;

  int grid_size;
  int pix_inc;
  int hole_x, hole_y;
  int bitmap_w, bitmap_h;
  int xoff, yoff;
  int grid_w, grid_h;
  int delay, delay2;
  int duration;
  GC gc;
  unsigned long fg, bg;
  int max_width, max_height;
  int early_i;

  int draw_rnd, draw_i;
  int draw_x, draw_y, draw_ix, draw_iy, draw_dx, draw_dy;
  int draw_dir, draw_w, draw_h, draw_size, draw_inc;
  int draw_last;
  int draw_initted;

  time_t start_time;
  async_load_state *img_loader;
};


static void *
slidescreen_init (Display *dpy, Window window)
{
  struct state *st = (struct state *) calloc (1, sizeof(*st));
  XWindowAttributes xgwa;
  XGCValues gcv;
  long gcflags;

  st->dpy = dpy;
  st->window = window;
  XGetWindowAttributes (st->dpy, st->window, &xgwa);
  st->img_loader = load_image_async_simple (0, xgwa.screen, st->window,
                                            st->window, 0, 0);
  st->start_time = time ((time_t *) 0);

  st->max_width = xgwa.width;
  st->max_height = xgwa.height;

  st->delay = get_integer_resource (st->dpy, "delay", "Integer");
  st->delay2 = get_integer_resource (st->dpy, "delay2", "Integer");
  st->duration = get_integer_resource (st->dpy, "duration", "Seconds");
  st->grid_size = get_integer_resource (st->dpy, "gridSize", "Integer");
  st->pix_inc = get_integer_resource (st->dpy, "pixelIncrement", "Integer");

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


  /* Don't let the grid be smaller than 5x5 */
  while (st->grid_size > xgwa.width / 5)
    st->grid_size /= 2;
  while (st->grid_size > xgwa.height / 5)
    st->grid_size /= 2;

  if (st->delay < 0) st->delay = 0;
  if (st->delay2 < 0) st->delay2 = 0;
  if (st->duration < 1) st->duration = 1;
  if (st->pix_inc < 1) st->pix_inc = 1;
  if (st->grid_size < 1) st->grid_size = 1;


  {
    XColor fgc, bgc;
    char *fgs = get_string_resource(st->dpy, "background", "Background");
    char *bgs = get_string_resource(st->dpy, "foreground", "Foreground");
    Bool fg_ok, bg_ok;
    if (!XParseColor (st->dpy, xgwa.colormap, fgs, &fgc))
      XParseColor (st->dpy, xgwa.colormap, "black", &bgc);
    if (!XParseColor (st->dpy, xgwa.colormap, bgs, &bgc))
      XParseColor (st->dpy, xgwa.colormap, "gray", &fgc);
    if (fgs) free (fgs);
    if (bgs) free (bgs);

    fg_ok = XAllocColor (st->dpy, xgwa.colormap, &fgc);
    bg_ok = XAllocColor (st->dpy, xgwa.colormap, &bgc);

    /* If we weren't able to allocate the two colors we want from the
       colormap (which is likely if the screen has been grabbed on an
       8-bit SGI visual -- don't ask) then just go through the map
       and find the closest color to the ones we wanted, and use those
       pixels without actually allocating them.
     */
    if (fg_ok)
      st->fg = fgc.pixel;
    else
      st->fg = 0;

    if (bg_ok)
      st->bg = bgc.pixel;
    else
      st->bg = 1;

#ifndef HAVE_JWXYZ
    if (!fg_ok || bg_ok)
      {
        int i;
	unsigned long fgd = ~0;
	unsigned long bgd = ~0;
	int max = visual_cells (xgwa.screen, xgwa.visual);
	XColor *all = (XColor *) calloc(sizeof (*all), max);
	for (i = 0; i < max; i++)
	  {
	    all[i].flags = DoRed|DoGreen|DoBlue;
	    all[i].pixel = i;
	  }
	XQueryColors (st->dpy, xgwa.colormap, all, max);
	for(i = 0; i < max; i++)
	  {
	    long rd, gd, bd;
	    unsigned long dd;
	    if (!fg_ok)
	      {
		rd = (all[i].red   >> 8) - (fgc.red   >> 8);
		gd = (all[i].green >> 8) - (fgc.green >> 8);
		bd = (all[i].blue  >> 8) - (fgc.blue  >> 8);
		if (rd < 0) rd = -rd;
		if (gd < 0) gd = -gd;
		if (bd < 0) bd = -bd;
		dd = (rd << 1) + (gd << 2) + bd;
		if (dd < fgd)
		  {
		    fgd = dd;
		    st->fg = all[i].pixel;
		    if (dd == 0)
		      fg_ok = True;
		  }
	      }

	    if (!bg_ok)
	      {
		rd = (all[i].red   >> 8) - (bgc.red   >> 8);
		gd = (all[i].green >> 8) - (bgc.green >> 8);
		bd = (all[i].blue  >> 8) - (bgc.blue  >> 8);
		if (rd < 0) rd = -rd;
		if (gd < 0) gd = -gd;
		if (bd < 0) bd = -bd;
		dd = (rd << 1) + (gd << 2) + bd;
		if (dd < bgd)
		  {
		    bgd = dd;
		    st->bg = all[i].pixel;
		    if (dd == 0)
		      bg_ok = True;
		  }
	      }

	    if (fg_ok && bg_ok)
	      break;
	  }
	XFree(all);
      }
#endif /* !HAVE_JWXYZ */
  }

  gcv.foreground = st->fg;
  gcv.function = GXcopy;
  gcflags = GCForeground |GCFunction;
  st->gc = XCreateGC (st->dpy, st->window, gcflags, &gcv);

  return st;
}

static void
draw_grid (struct state *st)
{
  int i;
  Drawable d;
  int border;
  XWindowAttributes xgwa;

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

  border = get_integer_resource (st->dpy, "internalBorderWidth",
                                 "InternalBorderWidth");

  XGetWindowAttributes (st->dpy, st->window, &xgwa);
  st->bitmap_w = xgwa.width;
  st->bitmap_h = xgwa.height;

  if (xgwa.width < 50 || xgwa.height < 50) /* tiny window */
    {
      int s = (xgwa.width < xgwa.height ? xgwa.width : xgwa.height);
      border = 1;
      st->grid_size = s / 2;
      if (st->grid_size < 16) st->grid_size = 16;
      if (st->bitmap_w < st->grid_size*2) st->bitmap_w = st->grid_size*2;
      if (st->bitmap_h < st->grid_size*2) st->bitmap_h = st->grid_size*2;
    }

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

  st->grid_w = st->bitmap_w / st->grid_size;
  st->grid_h = st->bitmap_h / st->grid_size;
  st->hole_x = random () % st->grid_w;
  st->hole_y = random () % st->grid_h;
  st->xoff = (st->bitmap_w - (st->grid_w * st->grid_size)) / 2;
  st->yoff = (st->bitmap_h - (st->grid_h * st->grid_size)) / 2;

  d = st->window;

  st->early_i = -10;
  st->draw_last = -1;

  if (border)
    {
      int half = border/2;
      int half2 = (border & 1 ? half+1 : half);
      XSetForeground(st->dpy, st->gc, st->bg);
      for (i = 0; i < st->bitmap_w; i += st->grid_size)
	{
	  int j;
	  for (j = 0; j < st->bitmap_h; j += st->grid_size)
	    XDrawRectangle (st->dpy, d, st->gc,
			    st->xoff+i+half2, st->yoff+j+half2,
			    st->grid_size-border-1, st->grid_size-border-1);
	}

      XSetForeground(st->dpy, st->gc, st->fg);
      for (i = 0; i <= st->bitmap_w; i += st->grid_size)
	XFillRectangle (st->dpy, d, st->gc, st->xoff+i-half, st->yoff, border, st->bitmap_h);
      for (i = 0; i <= st->bitmap_h; i += st->grid_size)
	XFillRectangle (st->dpy, d, st->gc, st->xoff, st->yoff+i-half, st->bitmap_w, border);
    }

  if (st->xoff)
    {
      XFillRectangle (st->dpy, d, st->gc, 0, 0, st->xoff, st->bitmap_h);
      XFillRectangle (st->dpy, d, st->gc, st->bitmap_w - st->xoff, 0, st->xoff, st->bitmap_h);
    }
  if (st->yoff)
    {
      XFillRectangle (st->dpy, d, st->gc, 0, 0, st->bitmap_w, st->yoff);
      XFillRectangle (st->dpy, d, st->gc, 0, st->bitmap_h - st->yoff, st->bitmap_w, st->yoff);
    }
}


static int
slidescreen_draw_early (struct state *st)
{
  while (st->early_i < 0)
    {
      st->early_i++;
      return 1;
    }

  /* for (early_i = 0; early_i < grid_size; early_i += pix_inc) */
   {
     XPoint points [3];
     points[0].x = st->xoff + st->grid_size * st->hole_x;
     points[0].y = st->yoff + st->grid_size * st->hole_y;
     points[1].x = points[0].x + st->grid_size;
     points[1].y = points[0].y;
     points[2].x = points[0].x;
     points[2].y = points[0].y + st->early_i;
     XFillPolygon (st->dpy, st->window, st->gc, points, 3, Convex, CoordModeOrigin);

     points[1].x = points[0].x;
     points[1].y = points[0].y + st->grid_size;
     points[2].x = points[0].x + st->early_i;
     points[2].y = points[0].y + st->grid_size;
     XFillPolygon (st->dpy, st->window, st->gc, points, 3, Convex, CoordModeOrigin);

     points[0].x = points[1].x + st->grid_size;
     points[0].y = points[1].y;
     points[2].x = points[0].x;
     points[2].y = points[0].y - st->early_i;
     XFillPolygon (st->dpy, st->window, st->gc, points, 3, Convex, CoordModeOrigin);

     points[1].x = points[0].x;
     points[1].y = points[0].y - st->grid_size;
     points[2].x = points[1].x - st->early_i;
     points[2].y = points[1].y;
     XFillPolygon (st->dpy, st->window, st->gc, points, 3, Convex, CoordModeOrigin);
   }

   st->early_i += st->pix_inc;
   if (st->early_i < st->grid_size)
     return 1;

   XFillRectangle (st->dpy, st->window, st->gc,
                   st->xoff + st->grid_size * st->hole_x,
                   st->yoff + st->grid_size * st->hole_y,
                   st->grid_size, st->grid_size);
   return 0;
}


static unsigned long
slidescreen_draw (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  int this_delay = st->delay;

  /* this code is a total kludge, but who cares, it works... */

  if (st->img_loader)   /* still loading */
    {
      st->img_loader = load_image_async_simple (st->img_loader, 0, 0, 0, 0, 0);
      if (! st->img_loader) {  /* just finished */
        st->start_time = time ((time_t *) 0);
        draw_grid (st);
      }
      return st->delay;
    }

  if (!st->img_loader &&
      st->start_time + st->duration < time ((time_t *) 0)) {
    XWindowAttributes xgwa;
    XGetWindowAttributes(st->dpy, st->window, &xgwa);
    st->img_loader = load_image_async_simple (0, xgwa.screen, st->window,
                                              st->window, 0, 0);
    st->start_time = time ((time_t *) 0);
    st->draw_initted = 0;
    return st->delay;
  }

  if (! st->draw_initted)
    {
      if (!slidescreen_draw_early (st))
        {
          st->draw_initted = 1;
          return st->delay2;
        }
      else
        return st->delay;
    }

 if (st->draw_i == 0)
   {
     if (st->draw_last == -1) st->draw_last = random () % 2;

     /* alternate between horizontal and vertical slides */
     /* note that draw_dir specifies the direction the _hole_ moves, not the tiles */
     if (st->draw_last == VERTICAL) {
       if (((st->grid_w > 1) ? st->draw_rnd = random () % (st->grid_w - 1) : 0)
	   < st->hole_x) {
         st->draw_dx = -1; st->draw_dir = LEFT;  st->hole_x -= st->draw_rnd;
       } else {
         st->draw_dx =  1; st->draw_dir = RIGHT; st->draw_rnd -= st->hole_x;
       }
       st->draw_dy = 0; st->draw_w = st->draw_size = st->draw_rnd + 1; st->draw_h = 1;
       st->draw_last = HORIZONTAL;
     } else {
       if (((st->grid_h > 1) ? st->draw_rnd = random () % (st->grid_h - 1) : 0)
	   < st->hole_y) {
         st->draw_dy = -1; st->draw_dir = UP;    st->hole_y -= st->draw_rnd;
       } else {
         st->draw_dy =  1; st->draw_dir = DOWN;  st->draw_rnd -= st->hole_y;
       }
       st->draw_dx = 0; st->draw_h = st->draw_size = st->draw_rnd + 1; st->draw_w = 1;
       st->draw_last = VERTICAL;
     }
 
     st->draw_ix = st->draw_x = st->xoff + (st->hole_x + st->draw_dx) * st->grid_size;
     st->draw_iy = st->draw_y = st->yoff + (st->hole_y + st->draw_dy) * st->grid_size;
     st->draw_inc = st->pix_inc;

   }

 /* for (draw_i = 0; draw_i < grid_size; draw_i += draw_inc) */
   {
     int fx, fy, tox, toy;
     if (st->draw_inc + st->draw_i > st->grid_size)
       st->draw_inc = st->grid_size - st->draw_i;
     tox = st->draw_x - st->draw_dx * st->draw_inc;
     toy = st->draw_y - st->draw_dy * st->draw_inc;

     fx = (st->draw_x < 0 ? 0 : st->draw_x > st->max_width  ? st->max_width  : st->draw_x);
     fy = (st->draw_y < 0 ? 0 : st->draw_y > st->max_height ? st->max_height : st->draw_y);
     tox = (tox < 0 ? 0 : tox > st->max_width  ? st->max_width  : tox);
     toy = (toy < 0 ? 0 : toy > st->max_height ? st->max_height : toy);

     XCopyArea (st->dpy, st->window, st->window, st->gc,
		fx, fy,
		st->grid_size * st->draw_w, st->grid_size * st->draw_h,
		tox, toy);

     st->draw_x -= st->draw_dx * st->draw_inc;
     st->draw_y -= st->draw_dy * st->draw_inc;
     switch (st->draw_dir)
       {
       case DOWN: XFillRectangle (st->dpy, st->window, st->gc,
			       st->draw_ix, st->draw_y + st->grid_size * st->draw_h, st->grid_size * st->draw_w, st->draw_iy - st->draw_y);
	 break;
       case LEFT: XFillRectangle (st->dpy, st->window, st->gc, st->draw_ix, st->draw_iy, st->draw_x - st->draw_ix, st->grid_size * st->draw_h);
	 break;
       case UP: XFillRectangle (st->dpy, st->window, st->gc, st->draw_ix, st->draw_iy, st->grid_size * st->draw_w, st->draw_y - st->draw_iy);
	 break;
       case RIGHT: XFillRectangle (st->dpy, st->window, st->gc,
			       st->draw_x + st->grid_size * st->draw_w, st->draw_iy, st->draw_ix - st->draw_x, st->grid_size * st->draw_h);
	 break;
       }
   }

   st->draw_i += st->draw_inc;
   if (st->draw_i >= st->grid_size)
     {
       st->draw_i = 0;

       switch (st->draw_dir)
         {
         case DOWN: st->hole_y += st->draw_size; break;
         case LEFT: st->hole_x--; break;
         case UP: st->hole_y--; break;
         case RIGHT: st->hole_x += st->draw_size; break;
         }

       this_delay = st->delay2;
     }

   return this_delay;
}

static void
slidescreen_reshape (Display *dpy, Window window, void *closure, 
                 unsigned int w, unsigned int h)
{
  struct state *st = (struct state *) closure;
  st->max_width = w;
  st->max_height = h;
  if (! st->img_loader) {
    XWindowAttributes xgwa;
    XGetWindowAttributes (st->dpy, st->window, &xgwa);
    st->img_loader = load_image_async_simple (0, xgwa.screen, st->window,
                                              st->window, 0, 0);
    st->start_time = time ((time_t *) 0);
  }
}

static Bool
slidescreen_event (Display *dpy, Window window, void *closure, XEvent *event)
{
  struct state *st = (struct state *) closure;
  if (screenhack_event_helper (dpy, window, event))
    {
      st->start_time = 0;
      return True;
    }
  return False;
}

static void
slidescreen_free (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  XFreeGC (dpy, st->gc);
  free (st);
}



static const char *slidescreen_defaults [] = {
  "*dontClearRoot:		True",
  "*fpsSolid:			true",
  ".background:			Black",
  ".foreground:			#BEBEBE",
  "*gridSize:			70",
  "*pixelIncrement:		10",
  "*internalBorderWidth:	4",
  "*delay:			50000",
  "*delay2:			1000000",
  "*duration:			120",
#ifdef HAVE_MOBILE
  "*ignoreRotation:             True",
  "*rotateImages:               True",
#endif
  0
};

static XrmOptionDescRec slidescreen_options [] = {
  { "-grid-size",	".gridSize",		XrmoptionSepArg, 0 },
  { "-ibw",		".internalBorderWidth",	XrmoptionSepArg, 0 },
  { "-increment",	".pixelIncrement",	XrmoptionSepArg, 0 },
  { "-delay",		".delay",		XrmoptionSepArg, 0 },
  { "-delay2",		".delay2",		XrmoptionSepArg, 0 },
  {"-duration",		".duration",		XrmoptionSepArg, 0 },
  { 0, 0, 0, 0 }
};

XSCREENSAVER_MODULE ("SlideScreen", slidescreen)
