/*************************************************************
  ashlar stonework function
  written by Jim Snow (jsnow@cs.georgefox.edu)
  for POV-RAY

  This pattern is an attempt to simulate the appearance of a stone wall made from rectangular blocks
  of unequal dimensions that interlock perfectly.

  The ashlar function accepts a point and returns a value between 0 and 1.
  If the point lies on a brick, it returns 0.  If it lies on a mortar line, it returns between 0
  and 1, depending on the distance from the mortar line.

  To illustrate how the function works, draw a large box on a piece of paper.  Pick some random points.
  Imagine that vertical lines pass through these points.  Pick some more random points.  Imagine that
  horizontal lines pass through these points.  Now, start extending the lines outward from the points.
  In case two lines intersect, the point that is closer to the intersection wins, and gets to continue
  on.  The other one dead ends.  When all lines have dead-ended somewhere, you have a nice drawing of
  interlocking rectangles.

  I don't know how to make patterns accept parameters, so I hard-coded in X_PTS, Y_PTS, BOXES, SEARCH, and 
  mortar.  X_PTS and Y_PTS are how many horizontals and verticals there are in a unit square.  BOXES is the 
  size of an array of random data, initialized the first time the function is run.  SEARCH is the number of 
  boxes over that the function looks for nearby points to consider: with SEARCH=1, the function examines a 
  grid of 9 unit squares every time its evaluated, and renders reasonably fast.  However, with SEARCH=1, there 
  is the occasional line dead-ending without colliding with another line because it ran off the edge of the 
  grid.  With SEARCH=2, this is much more rare (but could still potentially happen from time to time).
  Rendering is a lot slower with SEARCH=2 because it has to consider a grid of 25 unit squares each time
  its evaluated.  Mortar is the width of the mortar line.

  BUGS:
  
  This pattern ignores the Z axis.  Theoretically, it may be possible to generalize the algorithm to 3
  dimensions, but I'm not sure if I can visualize what's happening well enough to code it.

  With SEARCH set too low, lines may dead-end for no apparent reason.  There is a perfectly good explanation
  for why it does this, but I don't know if there exists an easy solution.

*************************************************************/

/* manhattan distance is the distance between 2 points if the only option is to travel straight north,
   east, south, or west (like you would in a city) */
static DBL manhattan_dist(VECTOR a, VECTOR b)
{
  return (fabs(a[X]-b[X]) + fabs(a[Y]-b[Y]));
}

/* solve circular dependency */
int cutoff_test_h(DBL dist, VECTOR ln, VECTOR* horizontals, VECTOR* verticals, int max_h, int max_v);
int cutoff_test_v(DBL dist, VECTOR ln, VECTOR* horizontals, VECTOR* verticals, int max_h, int max_v);

/* test a vertical to see if it's cut off by a horizontal */
int cutoff_test_v(DBL dist, VECTOR ln, VECTOR* horizontals, VECTOR* verticals, int max_h, int max_v)
{
  VECTOR point;
  VECTOR temp;
  VECTOR* h_cache;
  int i;
  int return_val=1;
  Assign_Vector(point, ln);
  point[Y]+=dist;
  dist=fabs(dist);

  /* separate inside from outside manhattan "circle" */
  for (i=0; i<max_h;)
    {
      Assign_Vector(temp, horizontals[i]);
      if (manhattan_dist(temp, ln) >= dist) /* outside */
	{
	  Assign_Vector(horizontals[i], horizontals[max_h-1]);
	  Assign_Vector(horizontals[max_h-1], temp);
	  max_h--;
	}
      else i++;
    }
  
  if (max_h)
    {
      h_cache = POV_MALLOC(sizeof(VECTOR)*max_h, "allocating unchanging cache of horizontals");
      for (i=0; i<max_h; i++)
	Assign_Vector(h_cache[i], horizontals[i]);
     
      /* cutoff test pass */
      for (i=0; i<max_h  && return_val; i++)
	{
	  if ( ((point[Y] - ln[Y]) * (h_cache[i][Y] - ln[Y])) > 0.0 ) /* same side of line */
	    {
	      temp[X] = point[X];
	      temp[Y] = h_cache[i][Y];
	      temp[Z] = 0.0;
	      
	      if (cutoff_test_h(h_cache[i][X]-temp[X], temp, horizontals, verticals, max_h, max_v)) /* not cutoff */
		return_val=0;
	    }
	}
      
      POV_FREE(h_cache);
    }
  return return_val;
}

/* test a horizontal to see if its cut off by a vertical */
int cutoff_test_h(DBL dist, VECTOR ln, VECTOR* horizontals, VECTOR* verticals, int max_h, int max_v)
{
  VECTOR point;
  VECTOR temp;
  VECTOR* v_cache;
  int i;
  int return_val=1;
  Assign_Vector(point, ln);
  point[X]+=dist;
  dist = fabs(dist);

  /* separate inside from outside manhattan "circle" */
  for (i=0; i<max_v;)
    {
      Assign_Vector(temp, verticals[i]);
      if (manhattan_dist(temp, ln) >= dist ) /* outside */
	{
	  Assign_Vector(verticals[i], verticals[max_v-1]);
	  Assign_Vector(verticals[max_v-1], temp);
	  max_v--;
	}
      else i++;
    }

  if (max_v)
    {
      v_cache = POV_MALLOC(sizeof(VECTOR)*max_v, "allocating unchanging cache of verticals");
      for (i=0; i<max_v; i++)
	Assign_Vector(v_cache[i], verticals [i]);
     
      /* cutoff test pass */
      for (i=0; i<max_v  && return_val; i++)
	{
	  if ( ((point[X] - ln[X]) * (v_cache[i][X] - ln[X])) > 0.0 ) /* same side of line */
	    {
	      temp[Y] = point[Y];
	      temp[X] = v_cache[i][X];
	      temp[Z] = 0.0;
	      
	      if (cutoff_test_v(v_cache[i][Y]-temp[Y], temp, horizontals, verticals, max_h, max_v)) /* not cutoff */ 
		return_val=0;
	    }
	}
      
      POV_FREE(v_cache);
    }
  return return_val;
}

static DBL ashlar(VECTOR EPoint)
{

  /* these should be set up according to parameters passed to the texture
     I don't know how to do that, so they're hard coded for now */

  int X_PTS = 3; /* horizontal starting points per unit box, reasonable range is 1-5 */
  int Y_PTS = 3; /* vertical starting points per unit box, reasonable range is 1-5 */
  int PTS_PER_BOX =  X_PTS+Y_PTS;
  int SEARCH = 1; /* 1 is fast but a little sloppy, 2 is slow but makes fewer mistakes */
  int SEARCH_GRID = ((SEARCH*2)+1)*((SEARCH*2)+1);
  int BOXES = 23; /* bigger is better but uses more memory */

  int seed, old_seed, cur_box;
  
  long x = (EPoint[X] > 0) ? EPoint[X] : EPoint[X]-1;
  long y = (EPoint[Y] > 0) ? EPoint[Y] : EPoint[Y]-1;
  long z = 0;

  static VECTOR* vector_pool;
  static VECTOR* horizontals;
  static VECTOR* verticals;
  static int first_time = 1;
  static int last_box = -1;

  static VECTOR* h_cache;
  static VECTOR* v_cache;

  VECTOR temp, temp2;

  DBL mortar = .03; /* mortar thickness, this should also be user-defined*/
  DBL y_offset, x_offset;
  int index, x_pos, y_pos, ctr, ctr2;
  int h_index=0;
  int v_index=0;
  DBL return_val = 0.0;

  /* initialize an array of random vectors */
  if (first_time)
    {
      vector_pool = POV_MALLOC(sizeof(VECTOR)*BOXES*PTS_PER_BOX, "initializing brick data");

      seed = 3586; /* arbitrary number */
      old_seed = POV_GET_OLD_RAND(); /* save current seed */
      
      POV_SRAND(seed);
      
      /* fill the vector pool with quasi-random vectors, keeping parallel lines somewhat separated */
      for (ctr=0; ctr<BOXES; ctr++)
	{
	  for (ctr2=0; ctr2 < X_PTS; ctr2++)
	    {
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][X] = FRAND();
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][Y] = (FRAND()+ctr2) / X_PTS;
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][Z] = 0;
	    }
	  for (ctr2=X_PTS; ctr2 < PTS_PER_BOX; ctr2++)
	    {
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][X] = (FRAND()+(ctr2-X_PTS)) / Y_PTS;
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][Y] = FRAND();
	      vector_pool[(ctr*PTS_PER_BOX)+ctr2][Z] = 0;
	    }
	}

      POV_SRAND(old_seed);  /* restore old seed */

      verticals   = POV_MALLOC(sizeof(VECTOR)*SEARCH_GRID*Y_PTS, "allocating vertical line data");
      horizontals = POV_MALLOC(sizeof(VECTOR)*SEARCH_GRID*X_PTS, "allocating horizontal line data");
      v_cache = POV_MALLOC(sizeof(VECTOR)*SEARCH_GRID*Y_PTS, "allocating unchanging cache of vertical lines");
      h_cache =POV_MALLOC(sizeof(VECTOR)*SEARCH_GRID*X_PTS, "allocating unchanging cache of horizontal lines");
	

      first_time=0;
    }

  cur_box = Hash2d(x, y);
  if (cur_box != last_box) /* don't re-fill arrays if the old data is still good */
    {

      /* LOAD UP HORIZONTAL AND VERTICAL ARRAYS */
      for (x_pos = -SEARCH; x_pos <= SEARCH; x_pos++)
	for (y_pos = -SEARCH; y_pos <= SEARCH; y_pos++)
	  {
	    cur_box = Hash2d(x+x_pos, y+y_pos);
	    
	    for (ctr = 0; ctr < X_PTS; ctr++)
	      {
		index=((cur_box*PTS_PER_BOX)+ctr)%(BOXES*PTS_PER_BOX);
		Assign_Vector(temp, vector_pool[index]);
		
		temp[Y] += (y_pos + y );
		temp[X] += (x_pos + x );
		
		Assign_Vector(horizontals[h_index], temp);
		Assign_Vector(h_cache[h_index], temp);
		h_index++;
	      }
	    
	    for (ctr = X_PTS; ctr < PTS_PER_BOX; ctr++)
	      {
		index=((cur_box*PTS_PER_BOX)+ctr)%(BOXES*PTS_PER_BOX);
		Assign_Vector(temp, vector_pool[index]);
		
		temp[Y] += ( y_pos + y );
		temp[X] += ( x_pos + x );
		
		Assign_Vector(verticals[v_index], temp);
		Assign_Vector(v_cache[v_index], temp);
		v_index++;
	      }
	  }
    }
  last_box = Hash2d(x, y);

  /* search through horizontals for points that line up */
  for (ctr = 0; ctr < SEARCH_GRID*X_PTS; ctr++)
    {
      Assign_Vector(temp, h_cache[ctr]);
      y_offset = temp[Y] - EPoint[Y];
      x_offset = temp[X] - EPoint[X];

      if ( fabs(y_offset) < mortar)
	{
	  Assign_Vector(temp2, EPoint);
	  temp2[Y] += y_offset;
	  if (cutoff_test_h(x_offset, temp2, horizontals, verticals, SEARCH_GRID*X_PTS, SEARCH_GRID*Y_PTS))
	    return_val = max( return_val, 1-fabs(y_offset/mortar));
	}
    }

  /* search through verticals for points that line up */
  for (ctr = 0; ctr < SEARCH_GRID*Y_PTS; ctr++)
    {
      Assign_Vector(temp, v_cache[ctr]);
      y_offset = temp[Y] - EPoint[Y];
      x_offset = temp[X] - EPoint[X];

      if ( fabs(x_offset) < mortar )
	{
	  Assign_Vector(temp2, EPoint);
	  temp2[X] += x_offset;
	  if (cutoff_test_v(y_offset, temp2, horizontals, verticals, SEARCH_GRID*X_PTS, SEARCH_GRID*Y_PTS))
	    return_val = max( return_val, 1-fabs(x_offset/mortar));
	} 
    }

  return return_val;
}
