//  NOTE: much of the code in this file, and many of the comments,
//  came directly from the Mac version of Kali by Jeff Weeks.  Thanks
//  to Jeff for his lucid programming style and copius comments!
//    mbp Mon Sep 16 17:06:26 1996

import java.awt.*;

/**
 * The Panorama object is where the math happens in Kali.  It keeps
 * track of the current group, and applies that group's action to the
 * line segments as they are drawn.  (The DrawPanel object, which
 * stores the list of line segments, called the Panorama's
 * drawSegment() method once for each line segment.)
 *
 * @see DrawPanel
 * @see KaliCanvas
 */
class Panorama implements Constants
{

/**
 * The current group.
 */
  SymmetryGroup	symmetryGroup;

/**
 * prepareToDraw() computes preparedTranslations, referenceVector,
 * and maxReference, based on the current group and current
 * coordinate system.
 */
  DVector[] 	preparedTranslations = new DVector[2];
  DVector 	referenceVector;
  double	maxReference;

/**
 * The KaliCanvas object that we draw on.
 */
  KaliCanvas	kaliCanvas;

/**
 * Handy rotation matrices.
 */
  DMatrix[] rotationMatrix = {
    new DMatrix(0.0, 0.0,
		0.0, 0.0),
    new DMatrix(1.0, 0.0,
		0.0, 1.0),
    new DMatrix(-1.0,  0.0,
		 0.0, -1.0),
    new DMatrix(-0.5,       -0.5*ROOT3,
		 0.5*ROOT3, -0.5       ),
    new DMatrix(0.0, -1.0,
		1.0,  0.0),
    new DMatrix(COS2PIOVER5, -SIN2PIOVER5,
		SIN2PIOVER5,  COS2PIOVER5),
    new DMatrix(0.5,       -0.5*ROOT3,
		0.5*ROOT3,  0.5       )
    };


/**
 * Create a new Panorama object
 * @param kaliCanvas	The KaliCanvas object that this Panorama
 *			should use for drawing.
 */
  public Panorama(KaliCanvas kaliCanvas) {
    this.kaliCanvas = kaliCanvas;
  }

/**
 * Set the current group.
 * @param index	The group index; should be one of the constants
 *		defined in Constants.java, e.g. GROUP_w2222 or
 *		GROUP_w3x3, etc.
 */
  public void setGroup(int index) {
    symmetryGroup = SymmetryGroups.group[index];
  }

/**
 * Return the current group
 */
  public SymmetryGroup getGroup() {
    return symmetryGroup;
  }


/**
 * Do the internal calculations necessary to prepare for drawing with
 * the current KaliCanvas coordinate systems.  This should be called
 * immediately before a sequence of drawSegment() calls to draw the
 * screen, but after the KaliCanvas object's coordinate systems have
 * been set.
 */
  public boolean prepareToDraw() {
    int		i;
    double	area,
    		width,
		altitude,
		det,
		dotProduct;

    //	Transfer symmetryGroup's translations to the screen
    //	coordinate system.
    for (i = 0; i < symmetryGroup.numTranslations; i++) {
      preparedTranslations[i] =
	kaliCanvas.internalToScreen(symmetryGroup.translations[i]);
//	kaliCanvas.position.times(symmetryGroup.translations[i]);
    }
    
    //	Make sure they're nondegenerate, and do further preparation as necessary.
    switch (symmetryGroup.numTranslations) {
    case 0:
      // No preparation is needed for a rosette group.
      return true;
      
    case 1:
      // Insist that the translation length be at least MIN_TRANSLATION pixels.
      if (preparedTranslations[0].length() < MIN_TRANSLATION) {
	return false;
      }
      
      // If the translation vector is "mainly horizontal", make it point to the right.
      // If the translation vector is "mainly vertical",   make it point upwards.
      // Vectors on the 45 degree lines are considered horizontal.
      if (Math.abs(preparedTranslations[0].c[0]) >= Math.abs(preparedTranslations[0].c[1])) {
	if (preparedTranslations[0].c[0] < 0.0) {
	  preparedTranslations[0].negate();
	}
      } else {
	if (preparedTranslations[0].c[1] < 0.0) {
	  preparedTranslations[0].negate();
	}
      }
      return true;
      
    case 2:
      // Insist that the area of a fundamental parallelogram
      // be at least MIN_AREA, so that we don't end up drawing
      // 100,000 copies of a one-pixel image.
      area = Math.abs(preparedTranslations[0].c[0] * preparedTranslations[1].c[1]
			 - preparedTranslations[0].c[1] * preparedTranslations[1].c[0]);
      if (area < MIN_AREA) {
	return false;
      }
      
      // In addition, insist that the parallelogram have altitude
      // at least MIN_ALTITUDE pixels in each direction, to avoid long
      // skinny parallelograms.
      for (i = 0; i < 2; i++) {
	width = preparedTranslations[i].length();
	altitude = area / width;
	if (altitude < MIN_ALTITUDE)
	  return false;
      }
      
      // Let preparedTranslations[0] be the more horizontal pointing Vector.
      if (Math.abs(preparedTranslations[0].c[0])
	  < Math.abs(preparedTranslations[1].c[0])) {
	DVector temp = preparedTranslations[0].copy();
	preparedTranslations[0] = preparedTranslations[1];
	preparedTranslations[1] = temp;
      }
      
      // Make sure preparedTranslations[0] points to the right.
      if (preparedTranslations[0].c[0] < 0.0) {
	preparedTranslations[0].negate();
      }
      
      // Make sure preparedTranslations form a right-handed basis.
      det = preparedTranslations[0].c[0] * preparedTranslations[1].c[1]
	- preparedTranslations[0].c[1] * preparedTranslations[1].c[0];
      if (det < 0.0) {
	preparedTranslations[1].negate();
      }
      
      // Imagine ruling the window with lines parallel
      // to preparedTranslations[0].  To help us decide which
      // of these lines a given point lies on, we define a
      // reference vector such that
      // (1)	referenceVector is parallel to preparedTranslations[0], and
      // (2)	<referenceVector, preparedTranslations[1]>.
      
      referenceVector = new DVector(- preparedTranslations[0].c[1],
				       preparedTranslations[0].c[0]);
      
      dotProduct = referenceVector.dot(preparedTranslations[1]);
      
      referenceVector.scale( 1.0 / dotProduct );
      
      // At this point we should have referenceVector[1] > 0.0
      
      // Find the maximum absolute value of <referenceVector, pt>
      // as pt ranges over the window frame.  Use the fact that the frame
      // is symmetrical about the origin.  The minimum will be the
      // negative of the maximum.
      maxReference =
	Math.abs(referenceVector.c[0]) * kaliCanvas.screenRight
	  + Math.abs(referenceVector.c[1]) * kaliCanvas.screenTop;
      
      return true;
      
    default:
      return false;	//	should never occur
    }
  }

/**
 * Draw a single segment in the given color under the
 * action of the current group.
 */
  public void drawSegment(Segment s, Color c) {
    drawSegmentRfGlRtTr(s, c);
  }

/** 
 * Draw the Segment under the action of symmetryGroup.
 * We take care of the possible reflection ("Rf") here,
 * and let other methods deal with the possible glide
 * reflection ("Gl"), rotation ("Rt") and translations ("Tr").
 */
  private void drawSegmentRfGlRtTr(Segment s, Color c) {

    //	Draw the Segment under the action of the possible
    //	glide reflection, rotation and translations.
    drawSegmentGlRtTr(s, c);

    //	If a reflection is called for, do it here, and then
    //	pass it to DrawSegmentGlRtTr() for the remaining operations.
    if (symmetryGroup.reflectionType != AXIS_NONE) {
      Segment reflectedSegment = s.copy();
      int i;
      switch (symmetryGroup.reflectionType) {
        case AXIS_X0:
	  for (i = 0; i < 2; i++) {
	    reflectedSegment.p[i].c[0] = - reflectedSegment.p[i].c[0];
	  }
	  break;
	case AXIS_X4:
	  for (i = 0; i < 2; i++) {
	    reflectedSegment.p[i].c[0] = 0.5 - reflectedSegment.p[i].c[0];
	  }
	  break;
	case AXIS_Y0:
	  for (i = 0; i < 2; i++) {
	    reflectedSegment.p[i].c[1] = - reflectedSegment.p[i].c[1];
	  }
	  break;
	case AXIS_Y4:
	  for (i = 0; i < 2; i++) {
	    reflectedSegment.p[i].c[1] = 0.5 - reflectedSegment.p[i].c[1];
	  }
	  break;
	}
      drawSegmentGlRtTr(reflectedSegment, c);
    }
  }

/**
 * Take care of the possible glide reflection ("Gl") here, and let
 * other methods deal with the possible rotation ("Rt") and
 * translations ("Tr").
 */
  private void drawSegmentGlRtTr(Segment s, Color c) {

    // Draw the Segment under the action of the possible
    // rotation and translations.
    drawSegmentRtTr(s, c);

    // If a glide reflection is called for, do it here, and then
    // pass it to DrawSegmentRtTr() for the remaining operations.
    if (symmetryGroup.glideReflectionType != AXIS_NONE) {
      Segment glideSegment = s.copy();
      int	i;
      switch (symmetryGroup.glideReflectionType) {
        case AXIS_X0:
	  for (i = 0; i < 2; i++) {
	    glideSegment.p[i].c[0] = - glideSegment.p[i].c[0];
	    glideSegment.p[i].c[1] += 0.5;
	  }
	  break;
	case AXIS_X4:
	  for (i = 0; i < 2; i++) {
	    glideSegment.p[i].c[0] = 0.5 - glideSegment.p[i].c[0];
	    glideSegment.p[i].c[1] += 0.5;
	  }
	  break;
	case AXIS_Y0:
	  for (i = 0; i < 2; i++) {
	    glideSegment.p[i].c[1] = - glideSegment.p[i].c[1];
	    glideSegment.p[i].c[0] += 0.5;
	  }
	  break;
	case AXIS_Y4:
	  for (i = 0; i < 2; i++) {
	    glideSegment.p[i].c[1] = 0.5 - glideSegment.p[i].c[1];
	    glideSegment.p[i].c[0] += 0.5;
	  }
	  break;
	}
      drawSegmentRtTr(glideSegment, c);
    }

  }

/**
 * Take care of the possible rotation ("Rt") here, and let
 * another method deal with the possible translations ("Tr").
 */
  private void drawSegmentRtTr(Segment s, Color c) {

    Segment rotatedSegment = s.copy();
    int	i, j;
    for (i = 0; i < symmetryGroup.rotationOrder; i++) {
      drawSegmentTr(rotatedSegment, c);
      for (j = 0; j < 2; j++) {
	rotatedSegment.p[j] =
	  rotationMatrix[symmetryGroup.rotationOrder].times(rotatedSegment.p[j]);
	}
    }
  }

/**  
 * Draw a Segment under the action of symmetryGroup's translations.
 */
  private void drawSegmentTr(Segment s, Color c) {
    
    // Here we must finally leave the pristine world of the internal
    // coordinate system and deal with the realities of the window
    // coordinate system.

    Segment screenSegment = new Segment(kaliCanvas.internalToScreen(s.p[0]),
					kaliCanvas.internalToScreen(s.p[1]));

    // Break into separate cases for rosette, frieze and wallpaper groups.
    switch (symmetryGroup.numTranslations) {
      case 0:
        drawSegmentRosette(screenSegment, c);
	break;
      case 1:
	drawSegmentFrieze(screenSegment, c);
	break;
      case 2:
	drawSegmentWallpaper(screenSegment, c);
	break;
      }
  }


  private void drawSegmentRosette(Segment s, Color c) {
    kaliCanvas.drawSegment(s, c);
  }

  private void drawSegmentFrieze(Segment segment, Color color) {
    int		i,
    		j;
    double	multiple;
    Segment	translatedSegment;

    //	At the moment I can't think of a more elegant way to do it,
    //	so let's break into cases according to whether the translation
    //	is mainly horizontal or mainly vertical.
    //	[prepareToDraw() has checked that it's not
    //	completely degenerate.]

    if (   Math.abs(preparedTranslations[0].c[0])
	>= Math.abs(preparedTranslations[0].c[1])) { // mainly horiz
      // mainly horizontal
      //
      // prepareToDraw() has already made sure that
      // preparedTranslations[0] points to the right.

      // Copy segment to translatedEndpoints in such a way that
      // translatedSegment.p[0] is not to the right of
      // translatedSegment.p[1].
      translatedSegment = segment.copy();
      if (segment.p[0].c[0] > segment.p[1].c[0]) {
	translatedSegment.reverse();
      }

      // Subtract off some multiple of preparedTranslations[0] to move
      // to the left most image which might be visible.
      multiple =
	Math.floor((translatedSegment.p[1].c[0] - kaliCanvas.screenLeft)
		   / preparedTranslations[0].c[0]);
      for (i = 0; i < 2; i++) {
	for (j = 0; j < 2; j++) {
	  translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j];
	}
      }

      // Draw images until we move off the right side of the frame.
      while (translatedSegment.p[0].c[0] < kaliCanvas.screenRight) {

	kaliCanvas.drawSegment(translatedSegment, color);

	for (i = 0; i < 2; i++) {
	  for (j = 0; j < 2; j++) {
	    translatedSegment.p[i].c[j] += preparedTranslations[0].c[j];
	  }
	}
      }
    } else {
      // mainly vertical
      //
      // prepareToDraw() has already made sure that
      // preparedTranslations[0] points upward.

      // Copy endpoints to translatedSegment.p in such a way that
      // translatedSegment.p[0] is not above translatedSegment.p[1].
      translatedSegment = segment.copy();
      if (segment.p[0].c[1] > segment.p[1].c[1]) {
	translatedSegment.reverse();
      }

      // Subtract off some multiple of preparedTranslations[0] to move
      // to the lowest image which might be visible.
      multiple =
	Math.floor((translatedSegment.p[1].c[1] - kaliCanvas.screenBottom)
		   / preparedTranslations[0].c[1]);
      for (i = 0; i < 2; i++) {
	for (j = 0; j < 2; j++) {
	  translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j];
	}
      }

      // Draw images until we move off the top of the frame.
      while (translatedSegment.p[0].c[1] < kaliCanvas.screenTop) {
	kaliCanvas.drawSegment(translatedSegment, color);
	for (i = 0; i < 2; i++) {
	  for (j = 0; j < 2; j++) {
	    translatedSegment.p[i].c[j] += preparedTranslations[0].c[j];
	  }
	}

      }
    }

  }

  private void drawSegmentWallpaper(Segment segment, Color color) {
    
    int		i,
    j,
    numRows,
    row;
    double[] dotProduct = new double[2];
    double	higherDotProduct,
    dotProductDifference,
    multiple;
    Segment	translatedSegment;
    
    //	prepareToDraw() has already set up preparedTranslations so that
    //	(1)	preparedTranslations[0] points to the right,
    //	(2)	preparedTranslations[] for a right-handed basis, and
    //	(3)	the corresponding parallelogram has MIN_AREA and MIN_ALTITUDE.
    
    //	We want to draw all translates whose initial endpoint lies within the frame.
    
    //	Copy segment to translatedSegment in such a way that
    //	translatedSegment.p[0] is not to the right of translatedSegment.p[1].
    translatedSegment = segment.copy();
    if (segment.p[0].c[0] > segment.p[1].c[0]) {
      translatedSegment.reverse();
    }
    
    //	Imagine the coordinate grid defined by preparedTranslations[].
    //	Compute <translatedSegment.p[i], referenceVector> to see which horizontal
    //	grid line we're near (cf. the definition of referenceVector in
    //	prepareToDraw()).  E.g. if dotProduct is 2.25, we're
    //	a quarter of the way from grid line 2 to grid line 3.
    for (i = 0; i < 2; i++) {
      dotProduct[i] = referenceVector.dot(translatedSegment.p[i]);
    }
    
    higherDotProduct = Math.max(dotProduct[0], dotProduct[1]);
    dotProductDifference = Math.abs(dotProduct[0] - dotProduct[1]);
    
    //	Subtract off some multiple of preparedTranslations[1] so that
    //	we move to the lowest parallel line which might still intersect the window.
    multiple = Math.floor(higherDotProduct - (-maxReference));
    for (i = 0; i < 2; i++) {
      for (j = 0; j < 2; j++) {
	translatedSegment.p[i].c[j] -= multiple * preparedTranslations[1].c[j];
      }
    }
    
    //	The number of rows we need to consider is given by
    //	[maxReference - (-maxReference)] + dotProductDifference
    //	= 2*maxReference + dotProductDifference.
    numRows = (int)Math.round(Math.ceil(2.0*maxReference + dotProductDifference));
    
    for (row = 0; row < numRows; row++) {
      //	Draw one row.
      
      //	Subtract some multiple of preparedTranslations[0] to move
      //	to the left most position within the row.
      multiple = Math.floor(
			       (translatedSegment.p[1].c[0] - kaliCanvas.screenLeft)
			       / preparedTranslations[0].c[0]);
      for (i = 0; i < 2; i++) {
	for (j = 0; j < 2; j++) {
	  translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j];
	}
      }
      
      //	Draw images until we move off the right side of the frame.
      while (translatedSegment.p[0].c[0] < kaliCanvas.screenRight) {

	kaliCanvas.drawSegment(translatedSegment, color);
	
	for (i = 0; i < 2; i++) {
	  for (j = 0; j < 2; j++) {
	    translatedSegment.p[i].c[j] += preparedTranslations[0].c[j];
	  }
	}
      }
      
      //	Add preparedTranslations[1] to each of translatedSegment
      //	to move up to the next row.
      for (i = 0; i < 2; i++) {
	for (j = 0; j < 2; j++) {
	  translatedSegment.p[i].c[j] += preparedTranslations[1].c[j];
	}
      }
    }
  }
  
  
}
