package CookieCutter.g1;

import java.util.*;
import CookieCutter.*;

/**
 * @author Mark Ayzenshtat
 * @author Vlad Shchogolev
 */
public class StateSpacePlacer implements CookiePlacer {	
	private static final boolean kUseExactArea = false;
	private static final double kCostFactor = 0;
	
	private static double sAlpha = 3.5;	// overestimation factor
	private static double sBeta = 100;	// branching factor
	
	private double mCookieArea;
	private int mNumTotalCookies;
	private Cookie[] mCookies;

	public void placeCookies(Cookie[] iCookies) {
		placeCookies(iCookies, true);
	}
	
	public void placeCookies(Cookie[] iCookies, boolean optimize) {
		initPosition(iCookies);
		mCookies = iCookies;
		
		Node bestNode = null;
		
		if (kUseExactArea) { 		
			mCookieArea = iCookies[0].getArea();
		} else {		
			mCookieArea = iCookies[0].getBounds().getArea();
		}
				
		mNumTotalCookies = iCookies.length;
		
		double degreeIncrement = 2 * Math.PI / sBeta;

		BinaryHeap heap = new BinaryHeap(1000);			
		Node start = new Node(this);
		heap.insert(start);
		int count = 0;
		while (true) {
			if (heap.isEmpty()) {
				// failure
				return;
			}
			
			Node n = (Node) heap.deleteMin();
			
			if (n.mPlacedCookies.size() == mNumTotalCookies) {
				// we're done
				bestNode = n;
				break;
			}
			
			// expand the node			
			for (int i = 0; i < sBeta; i++) {
				Node next = getBestPlacement(n, degreeIncrement * i);
				if (next == null) {
					continue;
				}
				
				heap.insert(next);
				count++;				
			}
		}		
		
		List best = bestNode.mPlacedCookies;
		double[] bestScores = new double[best.size()];
		double n = bestScores.length;
		int bestOption = -1;
		double bestScore = Cookie.computeRightBoundary(best);
		
		for (int i=0; i < n; i++) {
			bestScores[i] = ((Cookie)best.get(i)).getBounds().getRight();
			if (i > 0 && bestScores[i-1] > bestScores[i])
				bestScores[i] = bestScores[i-1];	
		}
		
		for (int i=0; i < n; i++) {
			bestScores[i] *= Math.ceil((double)n/(i+1)); 
			if (bestScores[i] < bestScore) {
				bestOption = i;
				bestScore = bestScores[i];
			}
		}		
		
		if (bestOption >= 0 && optimize) {
			CookieGroup cg = new CookieGroup(best.subList(0, bestOption+1));
			List consolidated = new ArrayList();
		
			for (int i = 0; i < iCookies.length; i+=(bestOption+1)) {
				if (i >= (bestOption+1) * Math.floor((double)n/bestOption) ) {
					consolidated.add(iCookies[i]);
				} else {
					// join two cookies together
					consolidated.add(cg.copy());
				}
			}
				
			Cookie[] temp = (Cookie[])consolidated.toArray(Cookie.kZeroCookies);
			new StateSpacePlacer().placeCookies(temp, false);
			
			if (Cookie.computeRightBoundary(temp) < Cookie.computeRightBoundary(best)) {
				Util.fillCookiesArray(temp, iCookies);
				return;
			} 
		} 
			
		System.arraycopy(bestNode.mPlacedCookies.toArray(Cookie.kZeroCookies),
			0, iCookies, 0, iCookies.length);
	}
	
	private Node getBestPlacement(Node iStart, double iRotation) {
		int index = iStart.mPlacedCookies.size();		
		Cookie c = mCookies[index];
		
		c = c.transform(c.getCentroid(), iRotation).flushToTopObstacle(iStart.mPlacedCookies);
				
		Cookie c1 = c.pack(true, iStart.mPlacedCookies);
		Cookie c2 = c.pack(false, iStart.mPlacedCookies);
		double score1 = c1.getLargestX();
		double score2 = c2.getLargestX();
		
		if (!c1.isInBound()) {
			if (!c2.isInBound()) {
				return null;
			} else {
				c = c2;
			}
		} else {
			if (!c2.isInBound()) {
				c = c1;
			} else {
				c = (score1 < score2) ? c1 : c2;				
			}
		}
		
		return iStart.placeNextCookie(c);		 
	}
	
	private void initPosition(Cookie[] iCookies) {
		new SafeCookiePlacer().placeCookies(iCookies);
		
		for (int i = 0; i < iCookies.length; i++) {
			iCookies[i] = iCookies[i].shift((iCookies[i].getBounds().getWidth() + i) * 3, 0);	
		}
		
	}
	
	private static class Node implements Comparable {		
		private StateSpacePlacer mPlacer;
		
		public List mPlacedCookies;
		public double mCost;
		
		public Node(StateSpacePlacer iPlacer) {
			this(iPlacer, new ArrayList(10));
		}
		
		private Node(StateSpacePlacer iPlacer, List iPlacedCookies) {
			mPlacer = iPlacer;
			mPlacedCookies = iPlacedCookies;
			updateCost();
		}
		
		public void updateCost() {
			double costSoFar;
			if (mPlacedCookies.isEmpty()) {
				costSoFar = 0;
			} else {
				costSoFar = Cookie.computeRightBoundary(mPlacedCookies) * 
					(1.0 + kCostFactor/mPlacedCookies.size());
			}
			
			double remaining = estimateRemainingCost();
			mCost = costSoFar + remaining;
		}
		
		public Node placeNextCookie(Cookie iC) {
			List l = new ArrayList(mPlacedCookies);
			l.add(iC);
			return new Node(mPlacer, l);			
		}
		
		public int compareTo(Object iO) {
			Node other = (Node) iO;
			return Double.compare(mCost, other.mCost);
		}
		
		private double estimateRemainingCost() {
			int numCookiesLeft = mPlacer.mNumTotalCookies - mPlacedCookies.size();
			return mPlacer.mCookieArea * numCookiesLeft * sAlpha;
		}
	}	
}
