//========================================================================
//
// SplashScreen.cc
//
//========================================================================

#include <aconf.h>

#ifdef USE_GCC_PRAGMAS
#pragma implementation
#endif

#include <stdlib.h>
#include <string.h>
#include "gmem.h"
#include "SplashMath.h"
#include "SplashScreen.h"

//------------------------------------------------------------------------

static SplashScreenParams defaultParams = {
  splashScreenDispersed,	// type
  2,				// size
  2,				// dotRadius
  1.0,				// gamma
  0.0,				// blackThreshold
  1.0				// whiteThreshold
};

//------------------------------------------------------------------------

struct SplashScreenPoint {
  int x, y;
  int dist;
};

static int cmpDistances(const void *p0, const void *p1) {
  return ((SplashScreenPoint *)p0)->dist - ((SplashScreenPoint *)p1)->dist;
}

//------------------------------------------------------------------------
// SplashScreen
//------------------------------------------------------------------------

// If <clustered> is true, this generates a 45 degree screen using a
// circular dot spot function.  DPI = resolution / ((size / 2) *
// sqrt(2)).  If <clustered> is false, this generates an optimal
// threshold matrix using recursive tesselation.  Gamma correction
// (gamma = 1 / 1.33) is also computed here.
SplashScreen::SplashScreen(SplashScreenParams *params) {
  Guchar u, black, white;
  int i;

  if (!params) {
    params = &defaultParams;
  }

  switch (params->type) {

  case splashScreenDispersed:
    // size must be a power of 2
    for (size = 1; size < params->size; size <<= 1) ;
    mat = (Guchar *)gmallocn(size * size, sizeof(Guchar));
    buildDispersedMatrix(size/2, size/2, 1, size/2, 1);
    break;

  case splashScreenClustered:
    // size must be even
    size = (params->size >> 1) << 1;
    if (size < 2) {
      size = 2;
    }
    mat = (Guchar *)gmallocn(size * size, sizeof(Guchar));
    buildClusteredMatrix();
    break;

  case splashScreenStochasticClustered:
    // size must be at least 2*r
    if (params->size < 2 * params->dotRadius) {
      size = 2 * params->dotRadius;
    } else {
      size = params->size;
    }
    mat = (Guchar *)gmallocn(size * size, sizeof(Guchar));
    buildSCDMatrix(params->dotRadius);
    break;
  }

  // do gamma correction and compute minVal/maxVal
  minVal = 255;
  maxVal = 0;
  black = splashRound((SplashCoord)255.0 * params->blackThreshold);
  if (black < 1) {
    black = 1;
  }
  white = splashRound((SplashCoord)255.0 * params->whiteThreshold);
  if (white > 255) {
    white = 255;
  }
  for (i = 0; i < size * size; ++i) {
    u = splashRound((SplashCoord)255.0 *
		    splashPow((SplashCoord)mat[i] / 255.0, params->gamma));
    if (u < black) {
      u = black;
    } else if (u >= white) {
      u = white;
    }
    mat[i] = u;
    if (u < minVal) {
      minVal = u;
    } else if (u > maxVal) {
      maxVal = u;
    }
  }
}

void SplashScreen::buildDispersedMatrix(int i, int j, int val,
					int delta, int offset) {
  if (delta == 0) {
    // map values in [1, size^2] --> [1, 255]
    mat[i * size + j] = 1 + (254 * (val - 1)) / (size * size - 1);
  } else {
    buildDispersedMatrix(i, j,
			 val, delta / 2, 4*offset);
    buildDispersedMatrix((i + delta) % size, (j + delta) % size,
			 val + offset, delta / 2, 4*offset);
    buildDispersedMatrix((i + delta) % size, j,
			 val + 2*offset, delta / 2, 4*offset);
    buildDispersedMatrix((i + 2*delta) % size, (j + delta) % size,
			 val + 3*offset, delta / 2, 4*offset);
  }
}

void SplashScreen::buildClusteredMatrix() {
  SplashCoord *dist;
  SplashCoord u, v, d;
  Guchar val;
  int size2, x, y, x1, y1, i;

  size2 = size >> 1;

  // initialize the threshold matrix
  for (y = 0; y < size; ++y) {
    for (x = 0; x < size; ++x) {
      mat[y * size + x] = 0;
    }
  }

  // build the distance matrix
  dist = (SplashCoord *)gmallocn(size * size2, sizeof(SplashCoord));
  for (y = 0; y < size2; ++y) {
    for (x = 0; x < size2; ++x) {
      if (x + y < size2 - 1) {
	u = (SplashCoord)x + 0.5 - 0;
	v = (SplashCoord)y + 0.5 - 0;
      } else {
	u = (SplashCoord)x + 0.5 - (SplashCoord)size2;
	v = (SplashCoord)y + 0.5 - (SplashCoord)size2;
      }
      dist[y * size2 + x] = u*u + v*v;
    }
  }
  for (y = 0; y < size2; ++y) {
    for (x = 0; x < size2; ++x) {
      if (x < y) {
	u = (SplashCoord)x + 0.5 - 0;
	v = (SplashCoord)y + 0.5 - (SplashCoord)size2;
      } else {
	u = (SplashCoord)x + 0.5 - (SplashCoord)size2;
	v = (SplashCoord)y + 0.5 - 0;
      }
      dist[(size2 + y) * size2 + x] = u*u + v*v;
    }
  }

  // build the threshold matrix
  minVal = 1;
  maxVal = 0;
  x1 = y1 = 0; // make gcc happy
  for (i = 0; i < size * size2; ++i) {
    d = -1;
    for (y = 0; y < size; ++y) {
      for (x = 0; x < size2; ++x) {
	if (mat[y * size + x] == 0 &&
	    dist[y * size2 + x] > d) {
	  x1 = x;
	  y1 = y;
	  d = dist[y1 * size2 + x1];
	}
      }
    }
    // map values in [0, 2*size*size2-1] --> [1, 255]
    val = 1 + (254 * (2*i)) / (2*size*size2 - 1);
    mat[y1 * size + x1] = val;
    val = 1 + (254 * (2*i+1)) / (2*size*size2 - 1);
    if (y1 < size2) {
      mat[(y1 + size2) * size + x1 + size2] = val;
    } else {
      mat[(y1 - size2) * size + x1 + size2] = val;
    }
  }

  gfree(dist);
}

// Compute the distance between two points on a toroid.
int SplashScreen::distance(int x0, int y0, int x1, int y1) {
  int dx0, dx1, dx, dy0, dy1, dy;

  dx0 = abs(x0 - x1);
  dx1 = size - dx0;
  dx = dx0 < dx1 ? dx0 : dx1;
  dy0 = abs(y0 - y1);
  dy1 = size - dy0;
  dy = dy0 < dy1 ? dy0 : dy1;
  return dx * dx + dy * dy;
}

// Algorithm taken from:
// Victor Ostromoukhov and Roger D. Hersch, "Stochastic Clustered-Dot
// Dithering" in Color Imaging: Device-Independent Color, Color
// Hardcopy, and Graphic Arts IV, SPIE Vol. 3648, pp. 496-505, 1999.
void SplashScreen::buildSCDMatrix(int r) {
  SplashScreenPoint *dots, *pts;
  int dotsLen, dotsSize;
  char *tmpl;
  char *grid;
  int *region, *dist;
  int x, y, xx, yy, x0, x1, y0, y1, i, j, d, iMin, dMin, n;

  //~ this should probably happen somewhere else
  srand(123);

  // generate the random space-filling curve
  pts = (SplashScreenPoint *)gmallocn(size * size, sizeof(SplashScreenPoint));
  i = 0;
  for (y = 0; y < size; ++y) {
    for (x = 0; x < size; ++x) {
      pts[i].x = x;
      pts[i].y = y;
      ++i;
    }
  }
  for (i = 0; i < size * size; ++i) {
    j = i + (int)((double)(size * size - i) *
		  (double)rand() / ((double)RAND_MAX + 1.0));
    x = pts[i].x;
    y = pts[i].y;
    pts[i].x = pts[j].x;
    pts[i].y = pts[j].y;
    pts[j].x = x;
    pts[j].y = y;
  }

  // construct the circle template
  tmpl = (char *)gmallocn((r+1)*(r+1), sizeof(char));
  for (y = 0; y <= r; ++y) {
    for (x = 0; x <= r; ++x) {
      tmpl[y*(r+1) + x] = (x * y <= r * r) ? 1 : 0;
    }
  }

  // mark all grid cells as free
  grid = (char *)gmallocn(size * size, sizeof(char));
  for (y = 0; y < size; ++y) {
    for (x = 0; x < size; ++x) {
      grid[y*size + x] = 0;
    }
  }

  // walk the space-filling curve, adding dots
  dotsLen = 0;
  dotsSize = 32;
  dots = (SplashScreenPoint *)gmallocn(dotsSize, sizeof(SplashScreenPoint));
  for (i = 0; i < size * size; ++i) {
    x = pts[i].x;
    y = pts[i].y;
    if (!grid[y*size + x]) {
      if (dotsLen == dotsSize) {
	dotsSize *= 2;
	dots = (SplashScreenPoint *)greallocn(dots, dotsSize,
					      sizeof(SplashScreenPoint));
      }
      dots[dotsLen++] = pts[i];
      for (yy = 0; yy <= r; ++yy) {
	y0 = (y + yy) % size;
	y1 = (y - yy + size) % size;
	for (xx = 0; xx <= r; ++xx) {
	  if (tmpl[yy*(r+1) + xx]) {
	    x0 = (x + xx) % size;
	    x1 = (x - xx + size) % size;
	    grid[y0*size + x0] = 1;
	    grid[y0*size + x1] = 1;
	    grid[y1*size + x0] = 1;
	    grid[y1*size + x1] = 1;
	  }
	}
      }
    }
  }

  gfree(tmpl);
  gfree(grid);

  // assign each cell to a dot, compute distance to center of dot
  region = (int *)gmallocn(size * size, sizeof(int));
  dist = (int *)gmallocn(size * size, sizeof(int));
  for (y = 0; y < size; ++y) {
    for (x = 0; x < size; ++x) {
      iMin = 0;
      dMin = distance(dots[0].x, dots[0].y, x, y);
      for (i = 1; i < dotsLen; ++i) {
	d = distance(dots[i].x, dots[i].y, x, y);
	if (d < dMin) {
	  iMin = i;
	  dMin = d;
	}
      }
      region[y*size + x] = iMin;
      dist[y*size + x] = dMin;
    }
  }

  // compute threshold values
  for (i = 0; i < dotsLen; ++i) {
    n = 0;
    for (y = 0; y < size; ++y) {
      for (x = 0; x < size; ++x) {
	if (region[y*size + x] == i) {
	  pts[n].x = x;
	  pts[n].y = y;
	  pts[n].dist = distance(dots[i].x, dots[i].y, x, y);
	  ++n;
	}
      }
    }
    qsort(pts, n, sizeof(SplashScreenPoint), &cmpDistances);
    for (j = 0; j < n; ++j) {
      // map values in [0 .. n-1] --> [255 .. 1]
      mat[pts[j].y * size + pts[j].x] = 255 - (254 * j) / (n - 1);
    }
  }

  gfree(pts);
  gfree(region);
  gfree(dist);

  gfree(dots);
}

SplashScreen::SplashScreen(SplashScreen *screen) {
  size = screen->size;
  mat = (Guchar *)gmallocn(size * size, sizeof(Guchar));
  memcpy(mat, screen->mat, size * size * sizeof(Guchar));
  minVal = screen->minVal;
  maxVal = screen->maxVal;
}

SplashScreen::~SplashScreen() {
  gfree(mat);
}

int SplashScreen::test(int x, int y, Guchar value) {
  int xx, yy;

  if (value < minVal) {
    return 0;
  }
  if (value >= maxVal) {
    return 1;
  }
  if ((xx = x % size) < 0) {
    xx = -xx;
  }
  if ((yy = y % size) < 0) {
    yy = -yy;
  }
  return value < mat[yy * size + xx] ? 0 : 1;
}

GBool SplashScreen::isStatic(Guchar value) {
  return value < minVal || value >= maxVal;
}
