/* tessellimage, Copyright (c) 2014-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"
#include "delaunay.h"

#ifndef HAVE_JWXYZ
# define XK_MISCELLANY
# include <X11/keysymdef.h>
#endif

struct state {
  Display *dpy;
  Window window;
  XWindowAttributes xgwa;
  GC wgc, pgc;
  int delay;
  Bool outline_p, cache_p, fill_p;
  double duration, duration2;
  int max_depth, max_resolution;
  double start_time, start_time2;

  XImage *img, *delta;
  Pixmap image, output, deltap;
  int nthreshes, threshes[256], vsizes[256];
  int thresh, dthresh;
  Pixmap cache[256];

  async_load_state *img_loader;
  XRectangle geom;
  Bool button_down_p;
  enum { DELAUNAY, VORONOI } mode;
};

typedef struct {
  int npoints;
  XPoint ctr;
  XPoint *p;
} voronoi_polygon;

typedef struct {
  XPoint p;
  double slope;
} voronoi_pa;


/* Returns the current time in seconds as a double.
 */
static double
double_time (void)
{
  struct timeval now;
# ifdef GETTIMEOFDAY_TWO_ARGS
  struct timezone tzp;
  gettimeofday(&now, &tzp);
# else
  gettimeofday(&now);
# endif

  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
}


static void *
tessellimage_init (Display *dpy, Window window)
{
  struct state *st = (struct state *) calloc (1, sizeof(*st));

  st->dpy = dpy;
  st->window = window;

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

  st->delay = get_integer_resource (st->dpy, "delay", "Integer");
  if (st->delay < 1) st->delay = 1;

  st->outline_p = get_boolean_resource (st->dpy, "outline", "Boolean");
  st->cache_p   = get_boolean_resource (st->dpy, "cache", "Boolean");
  st->fill_p    = get_boolean_resource (st->dpy, "fillScreen", "Boolean");

  st->max_depth = get_integer_resource (st->dpy, "maxDepth", "MaxDepth");
  if (st->max_depth < 100) st->max_depth = 100;

  st->max_resolution = get_integer_resource (st->dpy,
                                             "maxResolution", "MaxResolution");
  if (st->max_resolution < 0) st->max_resolution = 0;

  st->duration = get_float_resource (st->dpy, "duration", "Seconds");
  if (st->duration < 1) st->duration = 1;

  st->duration2 = get_float_resource (st->dpy, "duration2", "Seconds");
  if (st->duration2 < 0.001) st->duration = 0.001;

  XClearWindow(st->dpy, st->window);

  return st;
}


/* Given a bitmask, returns the position and width of the field.
 */
static void
decode_mask (unsigned int mask, unsigned int *pos_ret, unsigned int *size_ret)
{
  int i;
  for (i = 0; i < 32; i++)
    if (mask & (1L << i))
      {
        int j = 0;
        *pos_ret = i;
        for (; i < 32; i++, j++)
          if (! (mask & (1L << i)))
            break;
        *size_ret = j;
        return;
      }
}


static unsigned long
pixel_distance (Screen *s, Visual *v, unsigned long p1, unsigned long p2)
{
  static int initted_p = 0;
  static unsigned long rmsk=0, gmsk=0, bmsk=0;
  static unsigned int rpos=0, gpos=0, bpos=0;
  static unsigned int rsiz=0, gsiz=0, bsiz=0;

  unsigned char r1, g1, b1;
  unsigned char r2, g2, b2;
  long distance;

  if (!p1 && !p2) return 0;

  if (! initted_p) {
    visual_rgb_masks (s, v, &rmsk, &gmsk, &bmsk);
    decode_mask (rmsk, &rpos, &rsiz);
    decode_mask (gmsk, &gpos, &gsiz);
    decode_mask (bmsk, &bpos, &bsiz);
    initted_p = 1;
  }

  r1 = (p1 & rmsk) >> rpos;
  g1 = (p1 & gmsk) >> gpos;
  b1 = (p1 & bmsk) >> bpos;

  r2 = (p2 & rmsk) >> rpos;
  g2 = (p2 & gmsk) >> gpos;
  b2 = (p2 & bmsk) >> bpos;

#if 0
  /* Compute the distance in linear RGB space.
   */
  distance = cbrt (((r2 - r1) * (r2 - r1)) +
                   ((g2 - g1) * (g2 - g1)) +
                   ((b2 - b1) * (b2 - b1)));

# elif 1
  /* Compute the distance in luminance-weighted RGB space.
   */
  {
    int rd = (r2 - r1) * 0.2989 * (1 / 0.5870);
    int gd = (g2 - g1) * 0.5870 * (1 / 0.5870);
    int bd = (b2 - b1) * 0.1140 * (1 / 0.5870);
    distance = cbrt ((rd * rd) + (gd * gd) + (bd * bd));
  }
# else
  /* Compute the distance in brightness-weighted HSV space.
     (Slower, and doesn't seem to look better than luminance RGB.)
   */
  {
    int h1, h2;
    double s1, s2;
    double v1, v2;
    double hd, sd, vd, dd;
    rgb_to_hsv (r1, g1, b1, &h1, &s1, &v1);
    rgb_to_hsv (r2, g2, b2, &h2, &s2, &v2);

    hd = abs (h2 - h1);
    if (hd >= 180) hd -= 180;
    hd /= 180.0;
    sd = fabs (s2 - s1);
    vd = fabs (v2 - v1);

    /* [hsv]d are now the distance as 0.0 - 1.0. */
    /* Compute the overall distance, giving more weight to V. */
    dd = (hd * 0.25 + sd * 0.25 + vd * 0.5);

    if (dd < 0 || dd > 1.0) abort();
    distance = dd * 255;
  }
# endif

  if (distance < 0) distance = -distance;
  return distance;
}


static void
flush_cache (struct state *st)
{
  int i;
  for (i = 0; i < countof(st->cache); i++)
    if (st->cache[i])
      {
        XFreePixmap (st->dpy, st->cache[i]);
        st->cache[i] = 0;
      }
  if (st->deltap)
    {
      XFreePixmap (st->dpy, st->deltap);
      st->deltap = 0;
    }
}


/* Scale up the bits in st->img so that it fills the screen, centered.
 */
static void
scale_image (struct state *st)
{
  double scale, s1, s2;
  XImage *img2;
  int x, y, cx, cy;

  if (st->geom.width <= 0 || st->geom.height <= 0)
    return;

  s1 = st->geom.width  / (double) st->img->width;
  s2 = st->geom.height / (double) st->img->height;
  scale = (s1 < s2 ? s1 : s2);

  img2 = XCreateImage (st->dpy, st->xgwa.visual, st->img->depth,
                       ZPixmap, 0, NULL,
                       st->img->width, st->img->height, 8, 0);
  if (! img2) abort();
  img2->data = (char *) calloc (img2->height, img2->bytes_per_line);
  if (! img2->data) abort();

  cx = st->img->width  / 2;
  cy = st->img->height / 2;

  if (st->geom.width < st->geom.height)  /* portrait: aim toward the top */
    cy = st->img->height / (2 / scale);

  for (y = 0; y < img2->height; y++)
    for (x = 0; x < img2->width; x++)
      {
        int x2 = cx + ((x - cx) * scale);
        int y2 = cy + ((y - cy) * scale);
        unsigned long p = 0;
        if (x2 >= 0 && y2 >= 0 &&
            x2 < st->img->width && y2 < st->img->height)
          p = XGetPixel (st->img, x2, y2);
        XPutPixel (img2, x, y, p);
      }
  free (st->img->data);
  st->img->data = 0;
  XDestroyImage (st->img);
  st->img = img2;

  st->geom.x = 0;
  st->geom.y = 0;
  st->geom.width = st->img->width;
  st->geom.height = st->img->height;
}



static void
analyze (struct state *st)
{
  Window root;
  int x, y, i;
  unsigned int w, h, bw, d;
  unsigned long histo[256];

  {
    char *s = get_string_resource (st->dpy, "mode", "Mode");
    if (!s || !*s || !strcasecmp(s, "random"))
      st->mode = (random() & 1) ? DELAUNAY : VORONOI;
    else if (!strcasecmp(s, "delaunay"))
      st->mode = DELAUNAY;
    else if (!strcasecmp(s, "voronoi"))
      st->mode = VORONOI;
    else
      {
        fprintf (stderr, 
                 "%s: mode must be delaunay, voronoi or random, not \"%s\"\n",
                 progname, s);
        exit (1);
      }
    if (s) free (s);
  }

  flush_cache (st);

  /* Convert the loaded pixmap to an XImage.
   */
  XGetWindowAttributes (st->dpy, st->window, &st->xgwa);
  XGetGeometry (st->dpy, st->image, &root, &x, &y, &w, &h, &bw, &d);

  if (st->img)
    {
      free (st->img->data);
      st->img->data = 0;
      XDestroyImage (st->img);
    }
  st->img = XGetImage (st->dpy, st->image, 0, 0, w, h, ~0L, ZPixmap);

  if (st->fill_p) scale_image (st);

  /* Create the delta map: color space distance between each pixel.
     Maybe doing running a Sobel Filter matrix on this would be a
     better idea.  That might be a bit faster, but I think it would
     make no visual difference.
   */
  if (st->delta)
    {
      free (st->delta->data);
      st->delta->data = 0;
      XDestroyImage (st->delta);
    }
  st->delta = XCreateImage (st->dpy, st->xgwa.visual, d, ZPixmap, 0, NULL,
                            w, h, 32, 0);
  st->delta->data = (char *)
    calloc (st->delta->height, st->delta->bytes_per_line);

  for (y = 0; y < st->delta->height; y++)
    {
      for (x = 0; x < st->delta->width; x++)
        {
          unsigned long pixels[5];
          int i = 0;
          int distance = 0;
          pixels[i++] =                     XGetPixel (st->img, x,   y);
          pixels[i++] = (x > 0 && y > 0   ? XGetPixel (st->img, x-1, y-1) : 0);
          pixels[i++] = (         y > 0   ? XGetPixel (st->img, x,   y-1) : 0);
          pixels[i++] = (x > 0            ? XGetPixel (st->img, x-1, y)   : 0);
          pixels[i++] = (x > 0 && y < h-1 ? XGetPixel (st->img, x-1, y+1) : 0);

          for (i = 1; i < countof(pixels); i++)
            distance += pixel_distance (st->xgwa.screen, st->xgwa.visual,
                                        pixels[0], pixels[i]);
          distance /= countof(pixels)-1;
          XPutPixel (st->delta, x, y, distance);
        }
    }

  /* Collect a histogram of every distance value.
   */
  memset (histo, 0, sizeof(histo));
  for (y = 0; y < st->delta->height; y++)
    for (x = 0; x < st->delta->width; x++)
      {
        unsigned long p = XGetPixel (st->delta, x, y);
        if (p > countof(histo)) abort();
        histo[p]++;
      }

  /* Convert that from "occurrences of N" to ">= N".
   */
  for (i = countof(histo) - 1; i > 0; i--)
    histo[i-1] += histo[i];

# if 0
  fprintf (stderr, "%s: histo: ", progname);
  for (i = 0; i < countof(histo); i++)
    fprintf(stderr, "%d:%lu ", i, histo[i]);
  fprintf(stderr, "\n");
# endif

  /* Collect a useful set of threshold values, ignoring thresholds that
     result in a very similar number of control points (since those images
     probably won't look very different).
   */

  {
    int max_vsize = st->max_depth;
    int min_vsize = 20;
    int min_delta = 100;

    if (min_vsize > max_vsize/100)
      min_vsize = max_vsize/100;

    if (min_delta > max_vsize/1000)
      min_delta = max_vsize/1000;

    st->nthreshes = 0;
    for (i = countof(histo)-1; i >= 0; i--)
      {
        unsigned long vsize = histo[i];

        /* If this is a different vsize, push it. */
        if (vsize >= min_vsize &&
            vsize <= max_vsize &&
            (st->nthreshes == 0 ||
             vsize >= st->vsizes[st->nthreshes-1] + min_delta))
          {
            st->threshes[st->nthreshes] = i;
            st->vsizes[st->nthreshes] = vsize;
            st->nthreshes++;
          }
      }
  }
  
  st->thresh = 0;   /* startup */
  st->dthresh = 1;  /* forward */

  if (st->output)
    {
      XFreePixmap (st->dpy, st->output);
      st->output = 0;
    }


# if 0
  fprintf (stderr, "%s: threshes:", progname);
  for (i = 0; i < st->nthreshes; i++)
    fprintf (stderr, " %d=%d", st->threshes[i], st->vsizes[i]);
  fprintf (stderr, "\n");
# endif
}


/* True if the distance between any two corners is too small for it to
   make sense to draw an outline around this triangle.
 */
static Bool
small_triangle_p (const XPoint *p)
{
  int min = 4;
  if (abs (p[0].x - p[1].x) < min) return True;
  if (abs (p[0].y - p[1].y) < min) return True;
  if (abs (p[1].x - p[2].x) < min) return True;
  if (abs (p[1].y - p[2].y) < min) return True;
  if (abs (p[2].x - p[0].x) < min) return True;
  if (abs (p[2].y - p[0].y) < min) return True;
  return False;
}

static Bool
small_cell_p (const voronoi_polygon *p)
{
  int min = 4;
  if (abs (p->p[0].x - p->ctr.x) < min) return True;
  if (abs (p->p[0].y - p->ctr.y) < min) return True;
  return False;
}


static int
cmp_ccw (const void *v1, const void *v2)
{
  const voronoi_pa *p1,*p2;
  p1 = v1;
  p2 = v2;
  if      (p1->slope < p2->slope) return -1;
  else if (p1->slope > p2->slope) return  1;
  return 0;
}

static void
sort_ccw (XPoint *ctr, XPoint *p, int npoints)
{
  voronoi_pa *pa = (void *) malloc (npoints * sizeof(*pa));
  int i;
  for (i = 0; i < npoints; i++)
    {
      pa[i].p = p[i];
      pa[i].slope = atan2 (p[i].x - ctr->x, p[i].y - ctr->y);
    }
  qsort (pa, npoints, sizeof(*pa), cmp_ccw);
  for (i = 0; i < npoints; i++)
    p[i] = pa[i].p;
  free (pa);
}


static voronoi_polygon *
delaunay_to_voronoi (int np, XYZ *p, int nv, ITRIANGLE *v, double scale)
{
  struct tri_list {
    int count, size;
    int *tri;
  };

  int i, j;
  struct tri_list *vert_to_tri = (struct tri_list *)
    calloc (np + 1, sizeof(*vert_to_tri));
  voronoi_polygon *out = (voronoi_polygon *) calloc (np + 1, sizeof(*out));

  /* Iterate the triangles to construct a map of vertices to the
     triangles that contain them.
   */
  for (i = 0; i < nv; i++)
    {
      for (j = 0; j < 3; j++)	/* iterate points in each triangle */
        {
          int p = *((&v[i].p1) + j);
          struct tri_list *t = &vert_to_tri[p];
          if (p < 0 || p >= np) abort();
          if (t->size <= t->count + 1)
            {
              t->size += 3;
              t->size *= 1.3;
              t->tri = realloc (t->tri, t->size * sizeof(*t->tri));
              if (! t->tri) abort();
            }
          t->tri[t->count++] = i;
        }
    }

  /* For every vertex, compose a polygon whose corners are the centers
     of each triangle using that vertex.  Skip any with less than 3 points.

     This is currently omitting the voronoi cells that should touch the edges
     of the outer rectangle. Not sure exactly how to include those.
   */
  for (i = 0; i < np; i++)
    {
      long ctr_x = 0, ctr_y = 0;
      struct tri_list *t = &vert_to_tri[i];
      int n = t->count;
      if (n < 3) n = 0;
      out[i].npoints = n;
      if (n == 0) continue;
      out[i].ctr.x = out[i].ctr.y = 0;
      out[i].p = (n > 0
                  ? (XPoint *) calloc (out[i].npoints + 1, sizeof (*out[i].p))
                  : 0);
      for (j = 0; j < out[i].npoints; j++)
        {
          ITRIANGLE *tt = &v[t->tri[j]];
          out[i].p[j].x = scale * (p[tt->p1].x + p[tt->p2].x + p[tt->p3].x) / 3;
          out[i].p[j].y = scale * (p[tt->p1].y + p[tt->p2].y + p[tt->p3].y) / 3;
          ctr_x += out[i].p[j].x;
          ctr_y += out[i].p[j].y;
        }
      out[i].ctr.x = ctr_x / out[i].npoints;  /* long -> short */
      out[i].ctr.y = ctr_y / out[i].npoints;
      if (out[i].ctr.x < 0) abort();
      if (out[i].ctr.y < 0) abort();
      sort_ccw (&out[i].ctr, out[i].p, out[i].npoints);
    }

  for (i = 0; i < np+1; i++)
    if (vert_to_tri[i].tri)
      free (vert_to_tri[i].tri);
  free (vert_to_tri);

  return out;
}


static void
tessellate (struct state *st)
{
  Bool ticked_p = False;

  if (! st->image) return;

  if (! st->wgc)
    {
      XGCValues gcv;
      gcv.function = GXcopy;
      st->wgc = XCreateGC(st->dpy, st->window, GCFunction, &gcv);
      st->pgc = XCreateGC(st->dpy, st->image, GCFunction, &gcv);
    }

  if (! st->nthreshes) return;


  /* If duration2 has expired, switch to the next threshold. */

  if (! st->button_down_p)
    {
      double t2 = double_time();
      if (st->start_time2 + st->duration2 < t2)
        {
          st->start_time2 = t2;
          st->thresh += st->dthresh;
          ticked_p = True;
          if (st->thresh >= st->nthreshes)
            {
              st->thresh = st->nthreshes - 1;
              st->dthresh = -1;
            }
          else if (st->thresh < 0)
            {
              st->thresh = 0;
              st->dthresh = 1;
            }
        }
    }

  if (! st->output)
    ticked_p = True;

  /* If we've picked a new threshold, regenerate the output image. */

  if (ticked_p && st->cache[st->thresh])
    {
      if (st->output)
        XCopyArea (st->dpy, 
                   st->cache[st->thresh],
                   st->output, st->pgc,
                   0, 0, st->xgwa.width, st->xgwa.height, 
                   0, 0);
    }
  else if (ticked_p)
    {
      int threshold = st->threshes[st->thresh];
      int vsize = st->vsizes[st->thresh];
      ITRIANGLE *v;
      XYZ *p = 0;
      int nv = 0;
      int ntri = 0;
      int x, y, i;
      double wscale = st->xgwa.width / (double) st->delta->width;

#if 0
      fprintf(stderr, "%s: thresh %d/%d = %d=%d\n", 
              progname, st->thresh, st->nthreshes, threshold, vsize);
#endif

      /* Create a control point at every pixel where the delta is above
         the current threshold.  Triangulate from those. */

      vsize += 8;  /* corners of screen + corners of image */

      p = (XYZ *) calloc (vsize+4, sizeof(*p));
      v = (ITRIANGLE *) calloc (3*(vsize+4), sizeof(*v));
      if (!p || !v)
        {
          fprintf (stderr, "%s: out of memory (%d)\n", progname, vsize);
          abort();
        }

      /* Add control points for the corners of the screen, and for the
         corners of the image.
       */
      if (st->geom.width  <= 0) st->geom.width  = st->delta->width;
      if (st->geom.height <= 0) st->geom.height = st->delta->height;

      for (y = 0; y <= 1; y++)
        for (x = 0; x <= 1; x++)
          {
            p[nv].x = x ? st->delta->width-1  : 0;
            p[nv].y = y ? st->delta->height-1 : 0;
            p[nv].z = XGetPixel (st->delta, (int) p[nv].x, (int) p[nv].y);
            nv++;
            p[nv].x = st->geom.x + (x ? st->geom.width-1  : 0);
            p[nv].y = st->geom.y + (y ? st->geom.height-1 : 0);
            p[nv].z = XGetPixel (st->delta, (int) p[nv].x, (int) p[nv].y);
            nv++;
          }

      /* Add control points for every pixel that exceeds the threshold.
       */
      for (y = 0; y < st->delta->height; y++)
        for (x = 0; x < st->delta->width; x++)
          {
            unsigned long px = XGetPixel (st->delta, x, y);
            if (px >= threshold)
              {
                if (nv >= vsize) abort();
                p[nv].x = x;
                p[nv].y = y;
                p[nv].z = px;
                nv++;
              }
          }

      if (nv != vsize) abort();

      qsort (p, nv, sizeof(*p), delaunay_xyzcompare);
      if (delaunay (nv, p, v, &ntri))
        {
          fprintf (stderr, "%s: out of memory\n", progname);
          abort();
        }

      /* Create the output pixmap based on that triangulation. */

      if (st->output)
        XFreePixmap (st->dpy, st->output);
      st->output = XCreatePixmap (st->dpy, st->window,
                                  st->xgwa.width, st->xgwa.height,
                                  st->xgwa.depth);
      XFillRectangle (st->dpy, st->output, st->pgc, 
                      0, 0, st->xgwa.width, st->xgwa.height);

      switch (st->mode) {
      case VORONOI:
        {
          voronoi_polygon *polys =
            delaunay_to_voronoi (nv, p, ntri, v, wscale);
          for (i = 0; i < nv; i++)
            {
              if (polys[i].npoints >= 3)
                {
                  unsigned long color = XGetPixel (st->img,
                                                   polys[i].ctr.x / wscale,
                                                   polys[i].ctr.y / wscale);
                  XSetForeground (st->dpy, st->pgc, color);
                  XFillPolygon (st->dpy, st->output, st->pgc,
                                polys[i].p, polys[i].npoints,
                                Convex, CoordModeOrigin);

                  if (st->outline_p && !small_cell_p(&polys[i]))
                    {
                      XColor bd;
                      double scale = 0.8;
                      bd.pixel = color;
                      XQueryColor (st->dpy, st->xgwa.colormap, &bd);
                      bd.red   *= scale;
                      bd.green *= scale;
                      bd.blue  *= scale;

                      /* bd.red = 0xFFFF; bd.green = 0; bd.blue = 0; */

                      XAllocColor (st->dpy, st->xgwa.colormap, &bd);
                      XSetForeground (st->dpy, st->pgc, bd.pixel);
                      XDrawLines (st->dpy, st->output, st->pgc,
                                  polys[i].p, polys[i].npoints,
                                  CoordModeOrigin);
                      XFreeColors (st->dpy, st->xgwa.colormap, &bd.pixel,
                                   1, 0);
                    }
                }
              if (polys[i].p) free (polys[i].p);
              polys[i].p = 0;
            }
          free (polys);
        }
        break;

      case DELAUNAY:
        for (i = 0; i < ntri; i++)
          {
            XPoint xp[3];
            unsigned long color;
            xp[0].x = p[v[i].p1].x * wscale; xp[0].y = p[v[i].p1].y * wscale;
            xp[1].x = p[v[i].p2].x * wscale; xp[1].y = p[v[i].p2].y * wscale;
            xp[2].x = p[v[i].p3].x * wscale; xp[2].y = p[v[i].p3].y * wscale;

            /* Set the color of this triangle to the pixel at its midpoint. */
            color = XGetPixel (st->img,
                               (xp[0].x + xp[1].x + xp[2].x) / (3 * wscale),
                               (xp[0].y + xp[1].y + xp[2].y) / (3 * wscale));

            XSetForeground (st->dpy, st->pgc, color);
            XFillPolygon (st->dpy, st->output, st->pgc, xp, countof(xp),
                          Convex, CoordModeOrigin);

            if (st->outline_p && !small_triangle_p(xp))
              {	/* Border the triangle with a color that is darker */
                XColor bd;
                double scale = 0.8;
                bd.pixel = color;
                XQueryColor (st->dpy, st->xgwa.colormap, &bd);
                bd.red   *= scale;
                bd.green *= scale;
                bd.blue  *= scale;

                /* bd.red = 0xFFFF; bd.green = 0; bd.blue = 0; */

                XAllocColor (st->dpy, st->xgwa.colormap, &bd);
                XSetForeground (st->dpy, st->pgc, bd.pixel);
                XDrawLines (st->dpy, st->output, st->pgc,
                            xp, countof(xp), CoordModeOrigin);
                XFreeColors (st->dpy, st->xgwa.colormap, &bd.pixel, 1, 0);
              }
          }
        break;
      default:
        abort();
      }

      free (p);
      free (v);

      if (st->cache_p && !st->cache[st->thresh])
        {
          st->cache[st->thresh] =
            XCreatePixmap (st->dpy, st->window,
                           st->xgwa.width, st->xgwa.height,
                           st->xgwa.depth);
          if (! st->cache[st->thresh])
            {
              fprintf (stderr, "%s: out of memory\n", progname);
              abort();
            }
          if (st->output)
            XCopyArea (st->dpy, 
                       st->output,
                       st->cache[st->thresh],
                       st->pgc,
                       0, 0, st->xgwa.width, st->xgwa.height, 
                       0, 0);
        }
    }

  if (! st->output) abort();
}


/* Convert the delta map into a displayable pixmap.
 */
static Pixmap
get_deltap (struct state *st)
{
  int x, y;
  int w = st->xgwa.width;
  int h = st->xgwa.height;
  double wscale = st->xgwa.width / (double) st->delta->width;
  XImage *dimg;

  Visual *v = st->xgwa.visual;
  unsigned long rmsk=0, gmsk=0, bmsk=0;
  unsigned int rpos=0, gpos=0, bpos=0;
  unsigned int rsiz=0, gsiz=0, bsiz=0;

  if (st->deltap) return st->deltap;

  visual_rgb_masks (st->xgwa.screen, v, &rmsk, &gmsk, &bmsk);
  decode_mask (rmsk, &rpos, &rsiz);
  decode_mask (gmsk, &gpos, &gsiz);
  decode_mask (bmsk, &bpos, &bsiz);

  dimg = XCreateImage (st->dpy, st->xgwa.visual, st->xgwa.depth,
                       ZPixmap, 0, NULL, w, h, 8, 0);
  if (! dimg) abort();
  dimg->data = (char *) calloc (dimg->height, dimg->bytes_per_line);
  if (! dimg->data) abort();

  for (y = 0; y < h; y++)
    for (x = 0; x < w; x++)
      {
        unsigned long v = XGetPixel (st->delta, x / wscale, y / wscale) << 5;
        unsigned long p = (((v << rpos) & rmsk) |
                           ((v << gpos) & gmsk) |
                           ((v << bpos) & bmsk));
        XPutPixel (dimg, x, y, p);
      }

  st->deltap = XCreatePixmap (st->dpy, st->window, w, h, st->xgwa.depth);
  XPutImage (st->dpy, st->deltap, st->pgc, dimg, 0, 0, 0, 0, w, h);
  XDestroyImage (dimg);
  return st->deltap;
}


static unsigned long
tessellimage_draw (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;

  if (st->img_loader)   /* still loading */
    {
      st->img_loader = load_image_async_simple (st->img_loader, 0, 0, 0, 0,
                                                &st->geom);
      if (! st->img_loader) {  /* just finished */
        analyze (st);
        st->start_time = double_time();
        st->start_time2 = st->start_time;
      }
      goto DONE;
    }

  if (!st->img_loader &&
      st->start_time + st->duration < double_time()) {
    int w = st->xgwa.width;
    int h = st->xgwa.height;

    /* Analysing a full-resolution image on a Retina display is too slow,
       so scale down the source at image-load time. */
    if (st->max_resolution > 10)
      {
        if (w > h && w > st->max_resolution)
          h = st->max_resolution * h / w, w = st->max_resolution;
        else if (h > st->max_resolution)
          w = st->max_resolution * w / h, h = st->max_resolution;
      }
    /* fprintf(stderr,"%s: loading %d x %d\n", progname, w, h); */

    XClearWindow (st->dpy, st->window);
    if (st->image) XFreePixmap (dpy, st->image);
    st->image = XCreatePixmap (st->dpy, st->window, w, h, st->xgwa.depth);
    st->img_loader = load_image_async_simple (0, st->xgwa.screen, st->window,
                                              st->image, 0, &st->geom);
    goto DONE;
  }

  tessellate (st);

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

  if (st->output)
    XCopyArea (st->dpy, 
               (st->button_down_p ? get_deltap (st) : st->output),
               st->window, st->wgc,
               0, 0, st->xgwa.width, st->xgwa.height, 0, 0);
  else if (!st->nthreshes)
    XCopyArea (st->dpy,
               st->image,
               st->window, st->wgc,
               0, 0, st->xgwa.width, st->xgwa.height, 0, 0);


 DONE:
  return st->delay;
}
  
static void
tessellimage_reshape (Display *dpy, Window window, void *closure, 
                 unsigned int w, unsigned int h)
{
  struct state *st = (struct state *) closure;
  XGetWindowAttributes (st->dpy, st->window, &st->xgwa);
}

static Bool
tessellimage_event (Display *dpy, Window window, void *closure, XEvent *event)
{
  struct state *st = (struct state *) closure;
  if (event->xany.type == ButtonPress)
    {
      st->button_down_p = True;
      return True;
    }
  else if (event->xany.type == ButtonRelease)
    {
      st->button_down_p = False;
      return True;
    }
  else if (screenhack_event_helper (dpy, window, event))
    {
      st->start_time = 0;   /* load next image */
      return True;
    }

  return False;
}


static void
tessellimage_free (Display *dpy, Window window, void *closure)
{
  struct state *st = (struct state *) closure;
  flush_cache (st);
  if (st->wgc) XFreeGC (dpy, st->wgc);
  if (st->pgc) XFreeGC (dpy, st->pgc);
  if (st->image)  XFreePixmap (dpy, st->image);
  if (st->output) XFreePixmap (dpy, st->output);
  if (st->delta)  XDestroyImage (st->delta);
  if (st->img)    XDestroyImage (st->img);
  free (st);
}




static const char *tessellimage_defaults [] = {
  ".background:			black",
  ".foreground:			white",
/*  ".lowrez:                     True", */
  "*dontClearRoot:		True",
  "*fpsSolid:			true",
  "*mode:			random",
  "*delay:			30000",
  "*duration:			120",
  "*duration2:			0.4",
  "*maxDepth:			30000",
  "*maxResolution:		1024",
  "*outline:			True",
  "*fillScreen:			True",
  "*cache:			True",
#ifdef HAVE_MOBILE
  "*ignoreRotation:             True",
  "*rotateImages:               True",
#endif
  0
};

static XrmOptionDescRec tessellimage_options [] = {
  { "-delay",		".delay",		XrmoptionSepArg, 0 },
  { "-duration",	".duration",		XrmoptionSepArg, 0 },
  { "-duration2",	".duration2",		XrmoptionSepArg, 0 },
  { "-max-depth",	".maxDepth",		XrmoptionSepArg, 0 },
  { "-max-resolution",	".maxResolution",	XrmoptionSepArg, 0 },
  { "-mode",		".mode",		XrmoptionSepArg, 0 },
  { "-outline",		".outline",		XrmoptionNoArg, "True"  },
  { "-no-outline",	".outline",		XrmoptionNoArg, "False" },
  { "-fill-screen",	".fillScreen",		XrmoptionNoArg, "True"  },
  { "-no-fill-screen",	".fillScreen",		XrmoptionNoArg, "False" },
  { "-cache",		".cache",		XrmoptionNoArg, "True"  },
  { "-no-cache",	".cache",		XrmoptionNoArg, "False" },
  { 0, 0, 0, 0 }
};

XSCREENSAVER_MODULE ("Tessellimage", tessellimage)
