package CookieCutter.g1;

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

public class Cookie {
	public static final Cookie[] kZeroCookies = new Cookie[0];

	public static final double kEpsilon = 1e-5;
	public static final double kCollideEpsilon = 1e-3;
	
	protected Vertex mCentroid;
	protected double mRotation;
	protected double mArea;
	protected Box mBounds;

	protected double[] mX;
	protected double[] mY;
	private CookieCutter mCC;
	private Vertex[] mPoints; 
	
	public Cookie(Vertex[] iVertices, CookieCutter iCC) {
		try {
			mX = new double[iVertices.length];
			mY = new double[iVertices.length];
			for (int i = 0; i < iVertices.length; i++) {			
				mX[i] = iVertices[i].xpos();
				mY[i] = iVertices[i].ypos();				
			}
			
			mCC = iCC;
			mCentroid = mCC.centroid(iVertices);
			mArea = mCC.area(iVertices);
			mRotation = 0;
			mBounds = null;
		} catch (Exception ex) {
			System.err.println("Exception occurred: " + ex);
			System.exit(1);
		}
	}

	protected Cookie() {
		
	}

	private Cookie(double[] iX, double[] iY, Vertex iCentroid, 
		double iRot, double iArea, CookieCutter iCC) {
		mX = iX;
		mY = iY;
		mCentroid = iCentroid;
		mArea = iArea;
		mRotation = iRot;
		mBounds = null;
		mCC = iCC;
	}
	
	public Vertex[] getVertices() {
		if (mPoints == null) {
			mPoints = new Vertex[mX.length];
			for (int i = 0; i < mX.length; i++) {
				mPoints[i] = new Vertex(mX[i], mY[i]);
			}
		}
		
		return mPoints;
	}
	
	/**
	 * Returns the largest X value of any point in this polygon.  This value is
	 * used to determine the right boundary of a particular configuration of
	 * cookies.
	 */
	public double getLargestX() {
		return getBounds().getRight();
	}
	
	/**
	 * Translates and rotates this polygon.  This method makes a copy of the
	 * polygon and then translates and rotates the copy.  Therefore, the original
	 * polygon is unaffected.  The supplied rotation angle should be relative
	 * to the original orientation of the shape. 
	 */
	public Cookie transform(Vertex iCentroid, double iRotationAngle) {
		Cookie c = this.copy();
		
		double offsetX = iCentroid.xpos() - mCentroid.xpos();
		double offsetY = iCentroid.ypos() - mCentroid.ypos();
		
		double sinOfAngle = Math.sin(iRotationAngle);
		double cosOfAngle = Math.cos(iRotationAngle);
		
		for (int i = 0; i < c.mX.length; i++) {
			// translate...
			c.mX[i] += offsetX;
			c.mY[i] += offsetY;
			
			// ...and rotate
			if (iRotationAngle != 0) {
				rotateAboutPoint(c, i, iCentroid, sinOfAngle, cosOfAngle);
			}
		}		
		
		// update centroid and rotation
		c.mCentroid.setXpos(iCentroid.xpos());
		c.mCentroid.setYpos(iCentroid.ypos());
		c.mRotation = mRotation + iRotationAngle;
		
		return c;
	}
	
	protected final void rotateAboutPoint(Cookie iC, int iIndex, Vertex iPivot, 
			double iSine,	double iCosine) {
		
		double dx, dy, newDX, newDY;
		
		dx = iC.mX[iIndex] - iPivot.xpos();
		dy = iC.mY[iIndex] - iPivot.ypos();
		newDX = dx * iCosine - dy * iSine;
		newDY = dx * iSine + dy * iCosine;
		
		iC.mX[iIndex] = iPivot.xpos() + newDX;
		iC.mY[iIndex] = iPivot.ypos() + newDY;
	}
	
	public Cookie shift(double x, double y) {
		Vertex center = new Vertex(mCentroid.xpos()+x, mCentroid.ypos()+y);
		return this.transform(center, 0);
	}
	
	/**
	 * Returns a deep copy of this polygon.
	 */	
	public Cookie copy() {
		double[] x = new double[mX.length];
		double[] y = new double[mY.length];
		
		System.arraycopy(mX, 0, x, 0, mX.length);
		System.arraycopy(mY, 0, y, 0, mY.length);		

		return new Cookie(x, y, new Vertex(mCentroid), mRotation, mArea, mCC);
	}
	
	public int getNumVertices() {
		return mX.length;
	}
	
	/**
	 * @return
	 */
	public double getArea() {
		return mArea;
	}

	/**
	 * @return
	 */
	public Vertex getCentroid() {
		return mCentroid.copy();
	}
	
	/**
	 * @return
	 */
	public double getRotation() {
		return mRotation;
	}
	
	public boolean isInBound() {
		for (int i = 0; i < mX.length; i++) {
			if (mX[i] < 0 || mY[i] < 0 || mY[i] > 1) {
				return false;
			}
		}		
		return true;
	}
	
	public Box getBounds() {
		if (mBounds == null) {		
			double left = Double.MAX_VALUE;
			double right = Double.MIN_VALUE;
			double top = Double.MIN_VALUE;
			double bottom = Double.MAX_VALUE;
			
			for (int i = 0; i < mX.length; i++) {
				if (mX[i] < left) left = mX[i];
				if (mX[i] > right) right = mX[i];
				if (mY[i] > top) top = mY[i];
				if (mY[i] < bottom) bottom = mY[i];
			}	
			
			mBounds = new Box(left, top, right, bottom);			
		}
		
		return mBounds;
	}
	
	public Cookie pack(boolean iLeftFirst, List iPlacedCookies) {
		Vertex lastCentroid = mCentroid;
		Cookie c;
		if (iLeftFirst) {
			while (true) {				
				c = flushToLeftObstacle(iPlacedCookies)
					.flushToBottomObstacle(iPlacedCookies);
				if (c.mCentroid.equals(lastCentroid)) {
					break;
				}
				lastCentroid = c.mCentroid;
			}
		} else {
			while (true) {				
				c = flushToBottomObstacle(iPlacedCookies)
					.flushToLeftObstacle(iPlacedCookies);					
				if (c.mCentroid.equals(lastCentroid)) {
					break;
				}
				lastCentroid = c.mCentroid;
			}
		}
		
		return c;
	}
	
	public Cookie flushToLeftObstacle(List iPlacedCookies) {
		if (intersects(iPlacedCookies)) {
			return this;
		}
				
		double d = getBounds().getLeft() - kEpsilon;
		Cookie current = this;
				
		while (d > kCollideEpsilon &&
				current.getBounds().getLeft() > kCollideEpsilon) {
					
			Cookie moved = current.shift(-d, 0);			
			if (!moved.intersects(iPlacedCookies)) {			
				current = moved;
			}			
			
			d /= 2;
		}
		
		return current;
	}

	public Cookie flushToTopObstacle(List iPlacedCookies) {
		if (intersects(iPlacedCookies)) {
			return this;
		}
				
		double d = 1 - getBounds().getTop() - kEpsilon;
		Cookie current = this;
				
		while (d > kCollideEpsilon &&
				(1 - current.getBounds().getTop()) > kCollideEpsilon) {
					
			Cookie moved = current.shift(0, d);			
			if (!moved.intersects(iPlacedCookies)) {			
				current = moved;
			}
			
			d /= 2;
		}
		
		return current;
	}
	
	public Cookie flushToBottomObstacle(List iPlacedCookies) {
		if (intersects(iPlacedCookies)) {
			return this;
		}
				
		double d = getBounds().getBottom() - kEpsilon;
		Cookie current = this;
				
		while (d > kCollideEpsilon &&
				current.getBounds().getBottom() > kCollideEpsilon) {
					
			Cookie moved = current.shift(0, -d);			
			if (!moved.intersects(iPlacedCookies)) {			
				current = moved;
			}
			
			d /= 2;
		}
		
		return current;
	}
	
	/**
	 * Returns a copy of this cookie, the left side of which is aligned with the
	 * given x-coordinate. 
	 */
	public Cookie flushLeftToX(double iX) {
		Box bounds = getBounds();
		double leftmost = bounds.getLeft();
		double dx = iX - leftmost + kEpsilon;
		
		return transform(new Vertex(mCentroid.xpos() + dx, mCentroid.ypos()), 0);
	}

	/**
	 * Returns a copy of this cookie, the bottom of which is aligned with the
	 * given y-coordinate. 
	 */
	public Cookie flushBottomToY(double iY) {
		Box bounds = getBounds();
		double bottommost = bounds.getBottom();
		double dy = iY - bottommost + kEpsilon;
		
		return transform(new Vertex(mCentroid.xpos(), mCentroid.ypos() + dy), 0);
	}
	
	public boolean intersects(Cookie iC) {
		if (this.equals(iC)) {
			return false;
		}
		
		if (iC instanceof CookieGroup) {
			return iC.intersects(this);
		}
		
		// if the cookies' bounding boxes don't intersect, the cookies themselves
		// can't possibly intersect
		if (!getBounds().intersects(iC.getBounds())) {
			return false;
		}
		
		for (int i = 0; i < mX.length; i++) {
			for (int j = 0; j < iC.mX.length; j++) {
				double ax1 = mX[i];
				double ay1 = mY[i];
				double ax2 = mX[(i + 1) % mX.length];
				double ay2 = mY[(i + 1) % mY.length];

				double bx1 = iC.mX[j];
				double by1 = iC.mY[j];
				double bx2 = iC.mX[(j + 1) % mX.length];
				double by2 = iC.mY[(j + 1) % mY.length];
				
				if (Line2D.linesIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)) {
					return true;
				}
			}			
		}
		
		return false;
	}
	
	public boolean intersects(List iCookies) {		
		for (Iterator i = iCookies.iterator(); i.hasNext(); ) {
			Cookie c = (Cookie) i.next();
			if (this.intersects(c)) {
				return true;
			}
		}
		
		return false;
	}	
	
	public boolean intersects(Cookie[] iCookies) {
		return intersects(iCookies, 0, iCookies.length - 1);
	}
	
	public boolean intersects(Cookie[] iC, int first, int last) {
		for (int k = first; k <= last; k++) {
			if (this.intersects(iC[k]))
				return true;
		}
		return false;
	}
	
	public static double computeRightBoundary(Cookie[] iCookies) {
		double rightmost = iCookies[0].getLargestX();
		for (int i = 1; i < iCookies.length; i++) {
			double x = iCookies[i].getLargestX(); 
			if (x > rightmost) {
				rightmost = x; 
			}
		}
				
		return rightmost + Cookie.kEpsilon;
	}
	
	public static double computeRightBoundary(List iCookies) {
		double rightmost = ((Cookie) iCookies.get(0)).getLargestX(); 
		for (Iterator i = iCookies.iterator(); i.hasNext(); ) {
			double x = ((Cookie) i.next()).getLargestX(); 
			if (x > rightmost) {
				rightmost = x; 
			}		
		}
				
		return rightmost + Cookie.kEpsilon;
	}
		
	public String toString() {
		String s = "[";
		
		for (int k = 0; k < mY.length; k++) {
			s += "(" + Util.nf().format(mX[k]) + "," + 
				Util.nf().format(mY[k]) + ") ";
		}
		
		return s + "]";
	}

	public Edge getLongestEdge() {
		Edge e = new Edge();
		double longest = Double.MIN_VALUE;
		double ax, ay, bx, by, d;
		
		for (int i = 0; i < mX.length; i++) {
			ax = mX[i];
			ay = mY[i];
			bx = mX[(i+1)%mX.length];
			by = mY[(i+1)%mY.length];
			
			d = Math.pow(ax - bx, 2) + Math.pow(ay - by, 2);
			
			if (d > longest) {
				longest = d;
				e.ax = ax;
				e.ay = ay;
				e.bx = bx;
				e.by = by;
			}
		}	
		
		return e;
	}
}
