/*****************************************************************************
* Group7PlayerA2.java
* Adam M. Rosenzweig
* 9/30/03
*****************************************************************************/

package CookieCutter.g7;

import CookieCutter.*;
import java.io.Serializable;
import java.util.*;

/* Cookie Cutter player that uses a state-space search to find the best
   placement via a combination of different strategies. */
public class Group7PlayerA2 implements IFCPlayer {

    /* Cookie class used to store status of placed cookies */
    private class G7Cookie {
	public Vertex centroid;  // the translation of the centroid
	public double rotation;  // the angle of the cookie's rotation
	public Vertex [] verts;  // locations of the cookie's vertices
	
	/* Constructor, copies given vertices and sets no translation
	   or rotation. */
	public G7Cookie (Vertex [] vertices, CookieCutter cc) throws Exception {
	    int x;
	    verts = new Vertex [vertices.length];
	    // fill in verts with the given vertices
	    for (x = 0; x < vertices.length; x++) {
		verts [x] = new Vertex (vertices [x].xpos (), vertices [x].ypos ());
	    }
	    centroid = cc.centroid (verts);
	    rotation = 0.0;
	}

	// Copy constructor - produces a new cookie identical to parent
	public G7Cookie (G7Cookie parent) throws Exception {
	    int x;
	    centroid = new Vertex (parent.centroid);
	    rotation = parent.rotation;
	    verts = new Vertex [parent.verts.length];
	    // fill in verts with the parent's vertices
	    for (x = 0; x < verts.length; x++) {
		verts [x] = new Vertex (parent.verts [x]);
	    }
	}

	/* Given translation values and a rotation angle, this function
	   determines the new locations of all the vertices, and updates
	   the centroid and rotation member variables. 
	   This code lovingly ripped off from Group 1, who I believe
	   ripped their code off from the CookieCutter class. 
	   Retuns a new G7Cookie that has been rotated & translated. */
	public G7Cookie setLocation (double xpos, double ypos, double angle) 
	throws Exception {
	    G7Cookie result = new G7Cookie (this);
	    double dx, dy, newdx, newdy, deltax, deltay;
	    int x;
	    deltax = xpos - centroid.xpos ();
	    deltay = ypos - centroid.ypos ();
	    // for each vertex
	    for (x = 0; x < verts.length; x++) {
		// translate
		result.verts [x] = new Vertex (verts [x].xpos () + deltax,
					 verts [x].ypos () + deltay);
		
		// rotate if angle is not 0
		if (angle != 0) {
		    dx = result.verts [x].xpos () - xpos;
		    dy = result.verts [x].ypos () - ypos;
		    newdx = dx * Math.cos (angle) - dy * Math.sin (angle);
		    newdy = dx * Math.sin (angle) + dy * Math.cos (angle);
		    result.verts [x] = new Vertex (xpos + newdx, ypos + newdy);
		}
	    }
	    result.centroid = new Vertex (xpos, ypos);
	    if (angle != 0) {
		result.rotation = angle;
	    }
	    return result;
	}

	// Returns true if the cookie intersects with other, false otherwise
	public boolean intersects (G7Cookie other) throws Exception {
	    int x, y;
	    for (x = 0; x < verts.length; x++) {
		for (y = 0; y < verts.length; y++) {
		    if (CookieCutter.intersects (verts [x], verts [(x + 1) % verts.length], other.verts [y], other.verts [(y + 1) % verts.length])) {
			return true;
		    }
		}
	    }
	    return false;
	}
	
	// Returns true if the cookie is within the bounds of the board
	public boolean inBounds () throws Exception {
	    int x;
	    for (x = 0; x < verts.length; x++) {
		if ((verts [x].xpos () < 0.0) || 
		    (verts [x].ypos () < 0.0) || 
		    (verts [x].ypos () > 1.0)) {
		    return false;
		}
	    }
	    return true;
	}
	
	// Returns the largest X value of any vertex in the cookie.
	public double maximumX () throws Exception {
	    double maxX = Double.MIN_VALUE;
	    int y;
	    double x;
	    for (y = 0; y < verts.length; y++) {
		x = verts [y].xpos ();
		if (x > maxX) {
		    maxX = x;
		}
	    }
	    return maxX;
	}

	// returns smallest X value of any vertex in the cookie
	public double minimumX () throws Exception {
	    double minX = Double.MAX_VALUE;
	    int y;
	    double x;
	    for (y = 0; y < verts.length; y++) {
		x = verts [y].xpos ();
		if (x < minX) {
		    minX = x;
		}
	    }
	    return minX;
	}

	// returns the largest Y value of any vertex in the cookie
	public double maximumY () throws Exception {
	    double maxY = Double.MIN_VALUE;
	    int x;
	    double y;
	    for (x = 0; x < verts.length; x++) {
		y = verts [x].ypos ();
		if (y > maxY) {
		    maxY = y;
		}
	    }
	    return maxY;
	}

	// returns the smallest Y value of any vertex in the cookie
	public double minimumY () throws Exception {
	    double minY = Double.MAX_VALUE;
	    int x;
	    double y;
	    for (x = 0; x < verts.length; x++) {
		y = verts [x].ypos ();
		if (y < minY) {
		    minY = y;
		}
	    }
	    return minY;
	}

    }

    /* Group7PlayerA2 member variables */
    private CookieCutter cookieCutter;  // holder for game engine
    private Vertex [] [] cookies;       // stores the list of provided cookies
    private int copies;                 // the number of each cookie wanted
    private static Random random;       // used by the dumb strategy
    // Strategy identity constants
    private static final int STRATEGY_DUMB = 0;
    private static final int STRATEGY_SHORTEST = 1;
    private static final int STRATEGY_VERTICAL = 2;
    private static final int STRATEGY_PAIR = 3;
    private static final int STRATEGY_VPAIR = 4;
    private static final double EPSILON = 1e-9;  // Offset to avoid contact
    private G7Cookie [] theCookies;     // stores transformed cookies
    private Move bestMove;              // best move found for this cookie
    private double shortestDistance;    // length of best move found so far
    private double bestRotation;        // stores shortest width rotation
    
    /* Important things to note:
       - theCookies, bestMove, and shortestDistance are reset for each new
       cookie, and so theCookies actually stores the copies of the current
       cookie, not each cookie.  It is used to describe the locations of the
       vertices of the copies of that cookie.  Thus, it contains the data
       needed for computing collisions and boundaries.
    */

    // register function called at beginning by game engine
    public void register (CookieCutter __cookiecutter) throws Exception {
	cookieCutter = __cookiecutter;
	cookies = cookieCutter.cookieshapes ();
	copies = cookieCutter.cookieCopies ();
    }
    
    // names this player
    public String name () throws Exception {
	return "Vorpal Cutter +4";
    }

    // required for IFCPlayer
    public boolean interactive () throws Exception {
	return false;
    }

    // IFCPlayer function, serves as wrapper for recursive move search
    public Move [] moves () throws Exception {
	int x, y;
	random = new Random ();
	Move [] theMove = new Move [cookies.length];
	// for each cookie shape
	for (x = 0; x < cookies.length; x++) {
	    theMove [x] = new Move (copies);
	    bestMove = new Move (copies);
	    for (y = 0; y < copies; y++) {
		bestMove.setCookiePosition (y, new Vertex (0.5 + y, 0.5), 0.0);
	    }
	    bestMove.setRightBoundary (copies);
	    shortestDistance = Double.MAX_VALUE;
	    theCookies = new G7Cookie [copies];
	    // initialize fresh copies of the cookies
	    for (y = 0; y < copies; y++) {
		theCookies [y] = new G7Cookie (cookies [x], cookieCutter);
	    }
	    bestRotation = -1.0;
	    // call the recursive function
	    recursiveMove (x, 0, theMove [x]);
	    // load up the move for this cookie
	    for (y = 0; y < copies; y++) {
		theMove [x].setCookiePosition (y, 
					       bestMove.GetCookieCenter (y),
					       bestMove.GetCookieAngle (y));
	    }
	    theMove [x].setRightBoundary (bestMove.GetRightBoundary ());
	}
	return theMove;
    }

    /* This is the recursive function that does most of the work.  It places
       cookies via each strategy that has been implemented, and compares each
       set of placements to the best it has found yet.  When the search space
       is exhausted, the best placement found is used as the solution. 
       cookie is the index of the current cookie shape, numPlaced is the 
       number of cookies already placed, and theMove is where the placement
       is stored. */
    private void recursiveMove (int cookie, int numPlaced, Move theMove) throws Exception {
	// Base case:  if all cookies have been placed
	if (numPlaced == copies) {
	    // Check if the placement is valid
	    if (checkValidity ()) {
		// compare to the best found yet and update best if needed
		compareToBest ();
	    }
	}
	// recursively call after adding one cookie via each strategy
	else {
	    recursiveMove (cookie, numPlaced + 1,
			   placeCookie (STRATEGY_VERTICAL, cookie, numPlaced,
					theMove));
	    recursiveMove (cookie, numPlaced + 1,
			   placeCookie (STRATEGY_SHORTEST, cookie, numPlaced,
					theMove));
	    /*if (numPlaced + 2 <= copies) {
		recursiveMove (cookie, numPlaced + 2,
			       placeCookie (STRATEGY_PAIR, cookie, numPlaced,
					    theMove));
					    recursiveMove (cookie, numPlaced + 2,
			       placeCookie (STRATEGY_VPAIR, cookie, numPlaced,
			       theMove));
			       }*/
	}
    }

    /* Adds one cookie of type cookie to the board, in index index of the
       Move theMove, using strategy strat.  Basically, given a strategy, a
       cookie, which copy to place, and the Move to store it in, adds the
       specified copy of the specified cookie to the given move via the
       specified strategy. */
    private Move placeCookie (int strat, int cookie, int index, Move theMove) throws Exception {
	switch (strat) {
	case STRATEGY_DUMB:  // dumb strategy, from DefaultPlayer.java
	    // Just places nth cookie at n.5, 0.5, random rotation
	    // No longer used
	    double angle = 0.0;
	    theCookies [index] = theCookies [index].setLocation (0.5 + index, 0.5, angle);
	    theMove.setCookiePosition (index, theCookies [index].centroid, 
				       theCookies [index].rotation);
	    break;
	case STRATEGY_SHORTEST: // simple shortest possible strategy
	    /* Finds the narrowest rotation that isn't too tall, places as
	       close to the previous cookie as it can, no height changes */
	    theMove = strategyShortest (cookie, index, theMove);
	    break;
	case STRATEGY_VERTICAL:  // simple vertical stacking
	    theMove = strategyVertical (cookie, index, theMove);
	    break;
	case STRATEGY_PAIR:  // attempts to place 2 cookies together
	    theMove = strategyPair (cookie, index, index + 1, theMove);
	    break;
	case STRATEGY_VPAIR:  // attempts to place 2 cookies together
	    // vertical stacking instead of horizontal
	    theMove = strategyVPair (cookie, index, index + 1, theMove);
	    break;
	default:
	    break;
	}
	return theMove;
    }

    /* Checks if the placement of the cookies is valid:  that is, if all
       cookies are entirely within the bounds of the dough strip, and that
       no cookies intersect.  Returns true if so, false otherwise */
    private boolean checkValidity () throws Exception {
	int x, y;
	for (x = 0; x < theCookies.length; x++) {
	    if (!theCookies [x].inBounds ()) {
		return false;
	    }
	    for (y = x + 1; y < theCookies.length; y++) {
		if (theCookies [x].intersects (theCookies [y])) {
		    return false;
		}
	    }
	}
	return true;
    }

    /* Compares the arrangement of the cookies (as given by the state of
       theCookies) to the best move found so far, by the simple criteria of
       total x-distance used. */
    private void compareToBest () throws Exception {
	int x;
	double maxX, nextX;
	maxX = theCookies [0].maximumX ();
	for (x = 1; x < theCookies.length; x++) {
	    nextX = theCookies [x].maximumX ();
	    if (nextX > maxX) {
		maxX = nextX;
	    }
	}
	if (maxX < shortestDistance) {
	    shortestDistance = maxX;
	    bestMove = new Move (copies);
	    for (x = 0; x < copies; x++) {
		bestMove.setCookiePosition (x, new Vertex (theCookies [x].centroid), theCookies [x].rotation);
	    }
	    bestMove.setRightBoundary (maxX + EPSILON);
	}
    }
    
    /* Implements the simple shortest width strategy:  finds the rotation that
       has the shortest width and is still valid (in case it could be too tall
       for the dough in that width), and returns the cookie rotated that
       amount. Places the cookie next to the last one placed.  */
    private Move strategyShortest (int cookie, int index, Move theMove) throws Exception {

	theCookies [index] = new G7Cookie (cookies [cookie], cookieCutter);

	double rot;
	double bestRot = 0.0;
	double shortest = Double.MAX_VALUE;
	double left;
	double right;
	G7Cookie temp;
	if (bestRotation == -1.0) {
	    // for each angle in 0->pi/2, calculate the rotation and check
	    for (rot = 0.000; rot < Math.PI / 2; rot += 0.005) {
		temp = theCookies [index].setLocation (0.0, 0.0, rot);
		left = temp.minimumX ();
		right = temp.maximumX ();
		// is it the smallest found yet as well as not too tall?
		if ((right - left < shortest) && (temp.maximumY () - temp.minimumY () < 1.0)) {
		    shortest = right - left;
		    bestRot = rot;
		}
	    }
	    bestRotation = bestRot;
	}
	else {
	    bestRot = bestRotation;
	}
	temp = theCookies [index].setLocation (0.0, 0.0, bestRot);
	double transX = temp.centroid.xpos () - temp.minimumX () + EPSILON;
	double transY = temp.centroid.ypos () - temp.minimumY () + EPSILON;
	// if this isn't the first one, translated over enough to not hit
	if (index != 0) {
	    transX += theCookies [index - 1].maximumX ();
	    transX -= 0.01;
	    temp = theCookies [index].setLocation (transX, transY, bestRot);
	    boolean loop = true;
	    while ((loop) && (temp.inBounds ())) {
		for (int i = 0; i < index; i++) {
		    if (temp.intersects (theCookies [i])) {
			loop = false;
			break;
		    }
		}
		if (loop) {
		    transX -= 0.01;
		    temp = theCookies [index].setLocation (transX, transY, bestRot);
		}
	    }
	    transX += 0.01;
	}
	theCookies [index] = theCookies [index].setLocation (transX, transY,
							     bestRot);
	theMove.setCookiePosition (index, theCookies [index].centroid,
				   theCookies [index].rotation);
	return theMove;
    }

    /* This strategy is similar to the shortest strategy above.  It finds the
       minimal width rotation, but instead of stacking horizontally, it 
       attempts to stack vertically. */
    private Move strategyVertical (int cookie, int index, Move theMove) throws Exception {
	
	theCookies [index] = new G7Cookie (cookies [cookie], cookieCutter);

	double rot;
	double bestRot = 0.0;
	double shortest = Double.MAX_VALUE;
	double left;
	double right;
	G7Cookie temp;
	if (bestRotation == -1.0) {
	    // for each angle in 0->pi/2, calculate the rotation and check
	    for (rot = 0.0; rot < Math.PI / 2; rot += 0.005) {
		temp = theCookies [index].setLocation (0.0, 0.0, rot);
		left = temp.minimumX ();
		right = temp.maximumX ();
		// is it the smallest found yet as well as not too tall?
		if ((right - left < shortest) && (temp.maximumY () - temp.minimumY () < 1.0)) {
		    shortest = right - left;
		    bestRot = rot;
		}
	    }
	    bestRotation = bestRot;
	}
	else {
	    bestRot = bestRotation;
	}
	temp = theCookies [index].setLocation (0.0, 0.0, bestRot);
	double transX = temp.centroid.xpos () - temp.minimumX () + EPSILON;
	double transY = temp.centroid.ypos () - temp.minimumY () + EPSILON;
	if (index != 0) {
	    transX = theCookies [index - 1].centroid.xpos ();
	    transY += theCookies [index - 1].maximumY ();	    
	    transY -= 0.01;
	    temp = theCookies [index].setLocation (transX, transY, bestRot);
	    while ((temp.inBounds ()) && (!temp.intersects (theCookies [index - 1]))) {
		transY -= 0.01;
		temp = theCookies [index].setLocation (transX, transY, bestRot);
	    }
	    transY += 0.01;
	    /*transX -= 0.01;
	    boolean loop = true;
	    while (loop && temp.inBounds ()) {
		for (int i = 0; i < index; i++) {
		    if (temp.intersects (theCookies [i])) {
			loop = false;
			break;
		    }
		}
		if (loop) {
		    transX -= 0.01;
		    temp = theCookies [index].setLocation (transX, transY, bestRot);
		}
	    }
	    transX += 0.01;*/
	}
	theCookies [index] = theCookies [index].setLocation (transX, transY,
							     bestRot);
	theMove.setCookiePosition (index, theCookies [index].centroid,
				   theCookies [index].rotation);
	return theMove;

    }

    /* Places two cookies simultaneously by matching them up on the longest
       edge where they can be against each other, and finding the minimum
       width rotation of that shape.  It is then placed horizontally in 
       similar fashion to the strategyShortest method above.  It is very 
       naive:  it does not check if the paired shapes intersect or not. */
    private Move strategyPair (int cookie, int index, int index2, Move theMove)  throws Exception {

	int x;
	G7Cookie temp;
	G7Cookie temp2;
	double length;
	double longestLength;
	int firstVertex;
	firstVertex = 0;
	double dist;
	double dx;
	double dy;
	double rot;
	double bestRot = 0.0;
	double shortest = Double.MAX_VALUE;

	longestLength = Math.sqrt (Math.pow (theCookies [index].verts [0].xpos () - theCookies [index].verts [1].xpos (), 2) + Math.pow (theCookies [index].verts [0].ypos () - theCookies [index].verts [1].ypos (), 2));
	for (x = 1; x < theCookies [index].verts.length - 1; x++) {
	    length = Math.sqrt (Math.pow (theCookies [index].verts [x].xpos () - theCookies [index].verts [x + 1].xpos (), 2) + Math.pow (theCookies [index].verts [x].ypos () - theCookies [index].verts [x + 1].ypos (), 2));
	    if (length > longestLength) {
		longestLength = length;
		firstVertex = x;
	    }
	}
	for (rot = 0.0; rot < Math.PI / 2; rot += 0.005) {
	    temp = theCookies [index].setLocation (0.0, 0.0, rot);
	    length = temp.maximumX () - temp.minimumX ();
	    if ((length < shortest) && (temp.maximumY () - temp.minimumY () < 1.0)) {
		bestRot = rot;
		shortest = length;
	    }
	}

	temp = theCookies [index].setLocation (0.0, 0.0, bestRot);
	temp2 = theCookies [index2].setLocation (0.0, 0.0, bestRot + Math.PI);
	dist = CookieCutter.distance (temp.centroid.xpos (), temp.centroid.ypos (), temp.verts [firstVertex].xpos (), temp.verts [firstVertex].ypos (), temp.verts [firstVertex + 1].xpos (), temp.verts [firstVertex + 1].ypos ()) * 2 + EPSILON;
	dx = dist * Math.cos (bestRot);
	dy = dist * Math.sin (bestRot);
	if (temp.centroid.xpos () > temp.verts [firstVertex].xpos ()) {
	    dx *= -1;
	}
	if (temp.centroid.ypos () > temp.verts [firstVertex].ypos ()) {
	    dy *= -1;
	}
	double transX = temp.centroid.xpos () - temp.minimumX ();
	double transY = temp.centroid.ypos () - temp.minimumY ();
	if (index != 0) {
	    transX += theCookies [index - 1].maximumX ();
	}
	theCookies [index] = theCookies [index].setLocation (transX, transY, bestRot);
	theCookies [index2] = theCookies [index2].setLocation (transX + dx, transY + dy, bestRot + Math.PI);
	theMove.setCookiePosition (index, theCookies [index].centroid, theCookies [index].rotation);
	theMove.setCookiePosition (index2, theCookies [index2].centroid, theCookies [index2].rotation);
	return theMove;

    }

    private Move strategyVPair (int cookie, int index, int index2, Move theMove) throws Exception {
	
	int x;
	G7Cookie temp;
	G7Cookie temp2;
	double length;
	double longestLength;
	int firstVertex;
	firstVertex = 0;
	double dist;
	double dx;
	double dy;
	double rot;
	double bestRot = 0.0;
	double shortest = Double.MAX_VALUE;

	longestLength = Math.pow (theCookies [index].verts [0].xpos () -
				  theCookies [index].verts [1].xpos (), 2) +
	    Math.pow (theCookies [index].verts [0].ypos () - 
		      theCookies [index].verts [1].ypos (), 2);
	for (x = 1; x < theCookies [index].verts.length - 1; x++) {
	    length = Math.pow (theCookies [index].verts [x].xpos () -
			       theCookies [index].verts [x + 1].xpos (), 2) +
		Math.pow (theCookies [index].verts [x].ypos () - 
			  theCookies [index].verts [x + 1].ypos (), 2);
	    if (length > longestLength) {
		longestLength = length;
		firstVertex = x;
	    }
	}
	/*temp = new G7Cookie (theCookies [index]);
	  dist = CookieCutter.distance (temp.centroid.xpos (), temp.centroid.ypos (), temp.verts [firstVertex].xpos (), temp.verts [firstVertex].ypos (), temp.verts [firstVertex + 1].xpos (), temp.verts [firstVertex + 1].ypos ()) * 2.0;*/

	for (rot = 0.0; rot < Math.PI / 2.0; rot += 0.005) {
	    temp = theCookies [index].setLocation (0.0, 0.0, rot);
	    //temp2 = theCookies [index2].setLocation (dist * Math.cos (rot) + EPSILON, dist * Math.sin (rot) + EPSILON, rot + Math.PI);
	    temp2 = theCookies [index2].setLocation (0.0, 0.0, rot + Math.PI);
	    dx = temp.verts [firstVertex + 1].xpos () - temp2.verts [firstVertex].xpos () + EPSILON;
	    dy = temp.verts [firstVertex + 1].ypos () - temp2.verts [firstVertex].ypos () + EPSILON;
	    temp2 = theCookies [index2].setLocation (dx, dy, 0.0);
	    length = Math.max (temp.maximumX (), temp2.maximumX ()) - 
		Math.min (temp.minimumX (), temp2.minimumX ());
	    if ((length < shortest) && (Math.max (temp.maximumY (), temp2.maximumY ()) - Math.min (temp.minimumY (), temp2.minimumY ()) < 1.0)) {
		shortest = length;
		bestRot = rot;
	    }
	}

	temp = theCookies [index].setLocation (0.0, 0.0, bestRot);
	temp2 = theCookies [index2].setLocation (0.0, 0.0, bestRot + Math.PI);
	dx = temp.verts [firstVertex + 1].xpos () - temp2.verts [firstVertex].xpos () + EPSILON;
	dy = temp.verts [firstVertex + 1].ypos () - temp2.verts [firstVertex].ypos () + EPSILON;
	temp2 = theCookies [index2].setLocation (dx, dy, 0.0);
	//temp2 = theCookies [index2].setLocation (dist * Math.cos (bestRot) + EPSILON, dist * Math.sin (bestRot) + EPSILON, bestRot + Math.PI);
	double transX = temp.centroid.xpos () - Math.min (temp.minimumX (), temp2.minimumX ()) + EPSILON;
	double transY = temp.centroid.ypos () - Math.min (temp.minimumY (), temp2.minimumY ()) + EPSILON;
	if (index != 0) {
	    transX = theCookies [index - 1].centroid.xpos ();
	    transY += theCookies [index - 1].maximumY ();
	}
	theCookies [index] = theCookies [index].setLocation (transX, transY,
							     bestRot);
	theMove.setCookiePosition (index, theCookies [index].centroid,
				   theCookies [index].rotation);
	/*theCookies [index2] = theCookies [index2].setLocation (transX + dist * Math.cos (bestRot) + EPSILON, transY + dist * Math.sin (bestRot) + EPSILON, bestRot + Math.PI);*/
	theCookies [index2] = theCookies [index2].setLocation (transX + dx, transY + dy, bestRot + Math.PI);
	theMove.setCookiePosition (index2, theCookies [index2].centroid,
				   theCookies [index2].rotation);
	return theMove;

    }
    
}
