package CookieCutter.g5;

import java.util.*;
import java.awt.*;
import java.awt.geom.*;

/** A Cookie that Absorbs Other Cookie's.  This Cookie keeps references 
 * to the Cookie's it absorbs.  AmoebaCookie's facilitates the merging 
 * of each Cookie template into one massive piece that can be placed 
 * onto the Cookie dough.
 */
public class AmoebaCookie extends Cookie {
	private static final double STEPBACK_INC = 500.0;
	private static final double PRECISION_ERROR = .000000001;

	private Vector _cookies;

	public AmoebaCookie() throws Exception {
		super( -1 );
		_cookies = new Vector();
	}

	public AmoebaCookie( Cookie cookie ) throws Exception {
		super( -1 );
		_cookies = new Vector();
		_cookies.add( cookie );
	}

	public Vector getCookies() {
		return( _cookies );
	}

	public Corner[] getBestCornerComplement( Cookie cookie ) throws Exception {
		Vector otherCorners = cookie.getCorners();
		Corner[] pair = null;
		double minDiff = java.lang.Double.MAX_VALUE;

		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie myCookie = ( Cookie )_cookies.elementAt( i );
			Vector corners = myCookie.getCorners();

			for( int j = 0; j < corners.size(); j++ ) {
				Corner corner = ( Corner )corners.elementAt( j );

				if( !corner.isUsed() ) {
					Corner compCorner = corner.complement( otherCorners );

					if( compCorner != null ) {
						if( pair == null ) {
							pair = new Corner[2];
						}
						
						double diff = corner.idealMeasure( compCorner );

						if( Math.abs( diff ) < Math.abs( minDiff ) ) {
							minDiff = diff;
							pair[0] = corner;
							pair[1] = compCorner;
						}
						/*
						else if( Math.abs( diff ) == Math.abs( minDiff ) ) {
							if( pair[0] != null && pair[1] != null ) {
								if( Math.abs( corner.getY() ) < Math.abs( pair[0].getY() ) || 
									Math.abs( compCorner.getY() ) < Math.abs( pair[1].getY() ) ) {
									minDiff = diff;
									pair[0] = corner;
									pair[1] = compCorner;
								}
							}
						}
						*/
					}
				}
			}
		}

		return( pair );
	}

	public Edge[] getBestEdgeComplement( Cookie cookie ) {
		Vector otherEdges = cookie.getEdges();
		Edge[] pair = null;
		double minDiff = java.lang.Double.MAX_VALUE;

		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie myCookie = ( Cookie )_cookies.elementAt( i );
			Vector edges = myCookie.getEdges();

			for( int j = 0; j < edges.size(); j++ ) {
				Edge edge = ( Edge )edges.elementAt( j );

				if( !edge.isUsed() ) {
					Edge compEdge = edge.complement( otherEdges );

					if( compEdge != null ) {
						if( pair == null ) {
							pair = new Edge[2];
						}
						
						double diff = edge.idealMeasure( compEdge );

						if( Math.abs( diff ) < Math.abs( minDiff ) ) {
							minDiff = diff;
							pair[0] = edge;
							pair[1] = compEdge;
						}
						else if( Math.abs( diff ) == Math.abs( minDiff ) ) {
							if( pair[0] != null && pair[1] != null ) {
								if( edge.length() > compEdge.length() ) {
									if( edge.length() > pair[0].length() || 
										edge.length() > pair[1].length() ) {
										minDiff = diff;
										pair[0] = edge;
										pair[1] = compEdge;
									}
								}
								else {
									if( compEdge.length() > pair[0].length() || 
										compEdge.length() > pair[1].length() ) {
										minDiff = diff;
										pair[0] = edge;
										pair[1] = compEdge;
									}
								}
							}
						}
					}
				}
			}
		}

		return( pair );
	}

	/** If a cookie and a pair of edges are given, fit the edge of the 
	  * cookie onto the edge of this
	  */
	public boolean transformCookie( Cookie cookie, Edge[] pair ) throws Exception {
		Vector edges = cookie.getEdges();
		double vec1[] = VectorUtils.normalize( pair[0].slope() );
		double vec2[] = VectorUtils.normalize( pair[1].slope() );

		AffineTransform transMatrix = new AffineTransform();
		double dp = VectorUtils.fixDotProduct( VectorUtils.dotProduct( vec1, vec2 ) );
		cookie.setRotation( Math.acos( dp ) );
		//System.out.println( "angle = " + ( cookie.getRotation() * 180 / Math.PI ) );
		transMatrix.rotate( cookie.getRotation() );
		cookie.transform( transMatrix );
		cookie.update();

		Cookie matchingCookie = ( Cookie )_cookies.elementAt( pair[0].Owner() );
		Point2D.Double cent = matchingCookie.centroid();
		cookie.translate( cent.getX(), cent.getY() );
		cookie.setTranslation( cent.getX(), cent.getY() );
		cookie.update();

		pair[1] = ( Edge )edges.elementAt( pair[1].ID() );
		double updatedVec2[] = VectorUtils.normalize( pair[1].slope() );

		if( Math.abs( updatedVec2[0] - vec1[0] ) < PRECISION_ERROR && 
			Math.abs( updatedVec2[1] - vec1[1] ) < PRECISION_ERROR ) {
			/*
			System.out.println( "updatedVec2 = " + updatedVec2[0] + " " + updatedVec2[1] + 
				" vec1 = " + vec1[0] + " " + vec1[1] 
			);
			*/

			// temp undo translation
			cookie.translate( -cent.getX(), -cent.getY() );
			
			cookie.setRotation( cookie.getRotation() - Math.PI );
			transMatrix = new AffineTransform();
			transMatrix.rotate( Math.PI );
			cookie.transform( transMatrix );

			// do translation again
			cookie.translate( cent.getX(), cent.getY() );
		}

		cookie.update();
		pair[1] = ( Edge )edges.elementAt( pair[1].ID() );

		// translate 'till the two edges meet
		double trans[] = new double[2];
		
		Point2D.Double midpt = pair[0].midPoint();
		Point2D.Double cookieMidpt = pair[1].midPoint();

		trans[0] = midpt.getX() - cookieMidpt.getX();
		trans[1] = midpt.getY() - cookieMidpt.getY();

		if( trans[0] < 0 ) {
			trans[0] -= BOUNDARY_OFFSET;
		}
		else {
			trans[0] += BOUNDARY_OFFSET;
		}

		if( trans[1] < 0 ) {
			trans[1] -= BOUNDARY_OFFSET;
		}
		else {
			trans[1] += BOUNDARY_OFFSET;
		}
		
		cookie.translate( trans[0], trans[1] );
		Point2D.Double trans1 = cookie.getTranslation();
		cookie.setTranslation( trans1.getX() + trans[0], trans1.getY() + trans[1] );
		cookie.update();

		// translate away from the shape until no more overlap occurs;
		// translate away vector should be perpendicular to the slope of 
		// the edge we're attaching cookie to
		double normVector[] = new double[2];
		normVector[0] = ( midpt.getX() - cent.getX() ) / STEPBACK_INC;
		normVector[1] = ( midpt.getY() - cent.getY() ) / STEPBACK_INC;
		int i = 0;

		//while( this.overlapsWith( cookie, i ) && i < MAX_BACKOUT + 1 ) {
		while( this.overlapsWith( cookie, i ) ) {
			Point2D.Double cookieTrans = cookie.getTranslation();
			cookie.translate( normVector[0], normVector[1] );
			cookie.setTranslation( cookieTrans.getX() + normVector[0], 
				cookieTrans.getY() + normVector[1] 
			);
			cookie.update();
			i++;
		}
		System.out.println( "backed out " + i + " times" );

		// exceeds y dimension; undo and mark corner as used
		if( violatesVerticalBoundary( cookie ) ) {
			// undo translation
			Point2D.Double transl = cookie.getTranslation();
			cookie.translate( -transl.getX(), -transl.getY() );
			cookie.setTranslation( 0.0, 0.0 );

			// undo rotation
			transMatrix.rotate( -cookie.getRotation() );
			cookie.transform( transMatrix );
			cookie.setRotation( 0.0 );
			cookie.update();

			// mark corner as used
			pair[1] = ( Edge )edges.elementAt( pair[1].ID() );
			pair[0].use();
			pair[1].use();

			cookie.update();
			return( false );
		}

		cookie.update();
		return( true );
	}

	public boolean overlapsWith( Cookie cookie, int timesCalled ) {
		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie myCookie = ( Cookie )_cookies.elementAt( i );
			
			if( myCookie.overlapsWith( cookie, timesCalled  ) ) {
				return( true );
			}
		}

		return( false );
	}

	/** If a cookie and a pair of corners are given, fit the second 
	  * corner of the cookie into the first corner of this
	  */
	public boolean transformCookie( Cookie cookie, Corner[] pair ) throws Exception {
		// angle bisector for corners
		double normOutVec1[] = pair[0].angleBisector();
		double normOutVec2[] = pair[1].angleBisector();

		// flip normOutVec1
		double flippedNormOutVec1[] = new double[2];
		flippedNormOutVec1[0] = -normOutVec1[0];
		flippedNormOutVec1[1] = -normOutVec1[1];

		// save for moves()
		double dp = VectorUtils.fixDotProduct( VectorUtils.dotProduct( flippedNormOutVec1, normOutVec2 ) );
		double angle = Math.acos( dp );
		normOutVec2 = VectorUtils.rotate( normOutVec2, angle );
		dp = VectorUtils.fixDotProduct( VectorUtils.dotProduct( normOutVec1, normOutVec2 ) );

		double newAngle = Math.acos( dp );
		//System.out.println( "new angle = " + newAngle * 180 / Math.PI );

		if( newAngle != Math.PI ) {
			/*
			normOutVec2 = VectorUtils.rotate( normOutVec2, -2 * angle );
			dp = VectorUtils.fixDotProduct( VectorUtils.dotProduct( normOutVec1, normOutVec2 ) );
			newAngle = Math.acos( dp );
			System.out.println( "new angle = " + newAngle * 180 / Math.PI );
			*/
			cookie.setRotation( -angle );
		}
		else {
			cookie.setRotation( angle );
		}

		AffineTransform mergeMatrix = new AffineTransform();
		mergeMatrix.rotate( cookie.getRotation() );

		// complete transformation
		cookie.transform( mergeMatrix );

		// get amount to translate by
		double x = pair[0].getX() - cookie.getX( pair[1].ID() );
		double y = pair[0].getY() - cookie.getY( pair[1].ID() );

		cookie.setTranslation( x, y );
		cookie.translate( x, y );

		// move out until no more overlap
		double incStepOut[] = new double[2];

		incStepOut[0] = normOutVec1[0] / STEPBACK_INC;
		incStepOut[1] = normOutVec1[1] / STEPBACK_INC;
		int i = 0;
		
		while( this.overlapsWith( cookie, i ) ) {
			Point2D.Double transl = cookie.getTranslation();
			cookie.translate( incStepOut[0], incStepOut[1] );
			cookie.setTranslation( transl.getX() + incStepOut[0], transl.getY() + incStepOut[1] );
			cookie.update();
			i++;
		}

		// once more for good measure
		Point2D.Double transl = cookie.getTranslation();
		cookie.translate( incStepOut[0], incStepOut[1] );
		cookie.setTranslation( transl.getX() + incStepOut[0], transl.getY() + incStepOut[1] );

		// exceeds y dimension; undo and mark corner as used
		if( violatesVerticalBoundary( cookie ) ) {
			// undo translation
			transl = cookie.getTranslation();
			cookie.translate( -transl.getX(), -transl.getY() );
			cookie.setTranslation( 0.0, 0.0 );

			// undo rotation
			mergeMatrix.rotate( -cookie.getRotation() );
			cookie.transform( mergeMatrix );
			cookie.setRotation( 0.0 );

			// mark corner as used
			pair[0].use();
			pair[1].use();

			cookie.update();
			return( false );
		}

		cookie.update();
		return( true );
	}

	public Rectangle2D getBounds2D() {
		double min_x = java.lang.Double.MAX_VALUE, 
			min_y = java.lang.Double.MAX_VALUE,
			max_x = java.lang.Double.MIN_VALUE, 
			max_y = java.lang.Double.MIN_VALUE;

		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie cookie = ( Cookie )_cookies.elementAt( i );
			Rectangle2D cookieBounds = cookie.getBounds2D();

			if( cookieBounds.getX() < min_x ) {
				min_x = cookieBounds.getX();
			}

			if( cookieBounds.getY() < min_y ) {
				min_y = cookieBounds.getY();
			}

			if( cookieBounds.getX() + cookieBounds.getWidth() > max_x ) {
				max_x = cookieBounds.getX() + cookieBounds.getWidth();
			}

			if( cookieBounds.getY() + cookieBounds.getHeight() > max_y ) {
				max_y = cookieBounds.getY() + cookieBounds.getHeight();
			}
		}

		return( new Rectangle2D.Double( min_x - BOUNDARY_OFFSET, 
			min_y - BOUNDARY_OFFSET, max_x - min_x + BOUNDARY_OFFSET, 
			max_y - min_y + BOUNDARY_OFFSET
		) );
	}		

	/*
	public void assimilate( Cookie cookie, Edge[] commonEdges ) 
		throws Exception {
		_cookies.add( cookie );

		if( commonEdges != null ) {
			Vector newCoords = new Vector();
			int k = 0;

			// amoeba's start to prev point of edges
			do {
				newCoords.add( new Point2D.Double( getX( k ), getY( k ) ) );
			}
			while( k++ < commonEdges[0].ID() );

			if( commonEdges[0].correspondingVertices( commonEdges[1] ) ) {
				// cookie's prev to cookie's start
				for( int i = commonEdges[1].ID() - 1; i > -1; i-- ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}

				for( int i = cookie.getVertexCount() - 1; i > commonEdges[1].ID() + 1; i-- ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}
			}
			else {
				// cookies's next to cookie's start
				for( int i = commonEdges[1].ID() + 2; i < cookie.getVertexCount(); i++ ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}

				// cookie's start to cookie's prev
				for( int i = 0; i < commonEdges[1].ID(); i++ ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}
			}

			// amoeba's next to amoeba's start
			for( int i = commonEdges[0].ID() + 1; i < getVertexCount(); i++ ) {
				newCoords.add( new Point2D.Double( getX( i ), getY( i ) ) );
			}

			// done
			replaceCoordinates( newCoords );
			return;
		}

		absorbFirstCookie( cookie );
	}
	*/

	/*
	// TODO: this code still crashes under certain conditions
	public void assimilate( Cookie cookie, Corner[] bridgePair ) 
		throws Exception {
		_cookies.add( cookie );

		// bridge amoeba cookie w/ newly added cookie; skip corresponding corners
		if( bridgePair != null ) {
			Vector newCoords = new Vector();

			// amoeba's start coordinate to local corner prev
			for( int i = 0; i < getVertexCount(); i++ ) {
				newCoords.add( new Point2D.Double( getX( i ), getY( i ) ) );

				if( getX( i ) == bridgePair[0].prev().getX() && 
					getY( i ) == bridgePair[0].prev().getY() ) {
					break;
				}
			}

			// TODO: optimally pick bridge segments that minimize conceded area
			if( bridgePair[0].correspondingVertices( bridgePair[1] ) ) {
				// new cookie's corner's prev to new cookie's start coordinate
				for( int i = bridgePair[1].ID() - 1; i > -1; i-- ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}

				// new cookie's start to new cookie's corner's next
				if( bridgePair[1].ID() != 0 ) {
					for( int i = cookie.getVertexCount() - 1; i > -1; i-- ) {
						newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );

						if( cookie.getX( i ) == bridgePair[1].next().getX() && 
							cookie.getY( i ) == bridgePair[1].next().getY() ) {
							break;
						}
					}
				}
			}
			else {
				// new cookie's corner's next to new cookie's start coordinate
				for( int i = bridgePair[1].ID() + 1; i < cookie.getVertexCount(); i++ ) {
					newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );
				}

				if( bridgePair[1].ID() != 0 ) {
					// new cookie's start to new cookie's corner's prev 
					for( int i = 0; i < cookie.getVertexCount(); i++ ) {
						newCoords.add( new Point2D.Double( cookie.getX( i ), cookie.getY( i ) ) );

						if( cookie.getX( i ) == bridgePair[1].prev().getX() && 
							cookie.getY( i ) == bridgePair[1].prev().getY() ) {
							break;
						}
					}
				}
			}

			// local corner next back to amoeba's start coordinate
			for( int i = bridgePair[0].ID() + 1; i < getVertexCount(); i++ ) {
				newCoords.add( new Point2D.Double( getX( i ), getY( i ) ) );
			}
			
			// done
			replaceCoordinates( newCoords );
			return;
		}
	}
	*/

	public void assimilate( Cookie cookie, Corner[] pair ) 
		throws Exception {
		if( pair != null ) {
			_cookies.add( cookie );
			recenter();

			// invalidate corners and surrounding corners
			pair[0].prev().use();
			pair[0].use();
			pair[0].next().use();
			pair[1].prev().use();
			pair[1].use();
			pair[1].next().use();
		}
	}

	public void assimilate( Cookie cookie, Edge[] pair ) 
		throws Exception {
		if( pair != null ) {
			_cookies.add( cookie );
			recenter();

			// invalidate used edges
			pair[0].use();
			pair[1].use();
		}
	}

	/** Replace old coordinates w/ new ones
	 */
	/*
	protected void replaceCoordinates( Vector newCoords ) throws Exception {
		_coords = newCoords;
		Point2D.Double center = centroid();

		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie cookie = ( Cookie )_cookies.elementAt( i );
			cookie.recenter( center );
		}

		super.recenter();
		super.updateCorners();
	}
	*/

	public void recenter() throws Exception {
		recenter( centroid() );
	}

	public void recenter( Point2D.Double center ) throws Exception {
		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie cookie = ( Cookie )_cookies.elementAt( i );
			cookie.recenter( center );
			cookie.update();
		}
	}

	public Point2D.Double centroid() {
		double totalX = 0.0, totalY = 0.0;
		int totalPoints = 0;

		for( int i = 0; i < _cookies.size(); i++ ) {
			Cookie cookie = ( Cookie )_cookies.elementAt( i );

			for( int j = 0; j < cookie.getVertexCount(); j++ ) {
				Point2D.Double vertex = cookie.getVertex( j );
				totalX += vertex.getX();
				totalY += vertex.getY();
				totalPoints++;
			}
		}

		return( new Point2D.Double( totalX / totalPoints, totalY / totalPoints ) );
	}

	public void globallyOptimize() {
		Rectangle2D bounds = getBounds2D();

		if( bounds.getHeight() < 1.0 ) {
			double heightDiff = 1.0 - bounds.getHeight();
			double weight = bounds.getWidth();
			

		}
	}

	protected boolean violatesVerticalBoundary( Cookie cookie ) {
		Rectangle2D currBounds = getBounds2D();
		Rectangle2D cookieBounds = cookie.getBounds2D();
		double min_y = currBounds.getY(), 
			max_y = currBounds.getY() + currBounds.getHeight();

		if( cookieBounds.getY() < min_y ) {
			min_y = cookieBounds.getY();
		}

		if( cookieBounds.getY() + cookieBounds.getHeight() > max_y ) {
			max_y = cookieBounds.getY() + cookieBounds.getHeight();
		}

		return( max_y - min_y >= 1.0 );
	}
}
