/*
 * Group5K2.java
 *
 * Single-player strategy that puts all riders in a line to provide 
 * as much benefit from drafting as possible.
 *
 * The name "K" is a pictogram representing the convergence of the
 * four riders in one lane.
 *
 * Version 2: more robust line assembly, using decentralized logic:
 * each rider follows one other leader, and the line self-assembles
 * without top-down organization. Accounts for dead players.
 *
 * Version 3: drafts another team for 80% of the race, then
 * accelerates to optimal speed. Wins against Group1Player1.
 */

package Olympics.g5;

import Olympics.Move;
import Olympics.Olympics;
import Olympics.RiderInfo;
import java.util.*;

public class Group5K2 extends AbstractCoach {

    int turn, targetLane, numInPosition;
    RiderInfo[] info;
    RiderInfo[][] allInfo;
    boolean onetime, direction, moveOnceMore;
    Random rand;
    double optimalSpeed;
    Rider draftTarget;
    
    // stores sequence of riders; initialized by setOrder to minimize
    // getting-into-line time
    Vector rOrd;

    public void register(Olympics __olympics) throws Exception {
	super.register(__olympics);
	this.name = "Leapfrog";
	onetime = false;
	rand = new Random();
	rOrd = new Vector();
	direction = Math.random() < 0.5;
    }

    // runs once, at the start of the race, to order riders so that
    // getting into line takes as little time as possible.
    // also creates Rider wrapper objects.
    private void setupOrder(Vector rOrd) {
	int avgLane = 0;
	Vector tmp = new Vector();
	for (int i=0; i<info.length; i++) {
	    avgLane += info[i].lane();
	    tmp.add(new Rider(info[i], i));
	}
	avgLane /= info.length;
	while (!tmp.isEmpty()) {
	    int dist, mindist = numLanes;
	    Rider minr = (Rider)tmp.firstElement();
	    for (int i=0; i<tmp.size(); i++) {
		Rider r = (Rider)tmp.elementAt(i);
		dist = Math.abs(r.info.lane()-avgLane);
		if (dist < mindist) {
		    minr = r;
		    mindist = dist;
		}
	    }
	    rOrd.add(minr);
	    tmp.remove(minr);
	}
    }

    private Rider targetOf(Rider r) {
	int pos = rOrd.indexOf(r);
	if (pos == 0)
	    return r;

	return (Rider)rOrd.elementAt(pos-1);
    }

    // we use this as a rough upper bound on the maximum speed for a rider
    // we're drafting
    private double calcBestFullDraftSpeed() {
	Rider end = (Rider)rOrd.lastElement();
	return Math.pow(end.info.energy() / (0.7*(totalLength-end.info.position())), 
			2.0/3.0);
    }

    private double calcBestSpeed() {
	double top = 0;
	// TODO: document the math in the final report.
	for (int i=0; i<rOrd.size(); i++) {
	    Rider r = (Rider)rOrd.elementAt(rOrd.size()-1-i);
	    top += r.info.energy() * Math.pow(0.3, i);
	}
	Rider last = (Rider)rOrd.lastElement();
	double bottom = totalLength-last.info.position();
	double bestSpeed = Math.pow(top/bottom, 2.0/3.0);
	// trim to four decimal places
	return Math.floor(bestSpeed * 10000)/10000.0;
    }
	
    private void draftOtherTeam() {
	Rider leader = (Rider)rOrd.firstElement();

	if (draftTarget == leader) {
	    runLine();
	    return;
	}

	if (turn%100==0 || draftTarget == null || !draftTarget.alive()) {
	    // get a list of the last rider in each team
	    double minSpeed = calcBestSpeed();
	    double maxSpeed = calcBestFullDraftSpeed();
	    Vector lastRiderFromEachTeam = new Vector();
	    for (int t=0; t<allInfo.length; t++) {
		if (t == myIndex) continue; // don't scan our own riders
		double lastPos = Double.MAX_VALUE;
		int lastIndex = 0;
		for (int i=0; i<allInfo[t].length; i++)
		    if (allInfo[t][i].position() < lastPos && 
			allInfo[t][i].energy() > 0) {
			lastPos = allInfo[t][i].position();
			lastIndex = i;
		    }
		lastRiderFromEachTeam.add(new Rider(allInfo[t][lastIndex], lastIndex));
	    }
	    // then pick out the furthest-ahead rider from that list
	    // going at an acceptable speed
	    // who won't take too long to get to
	    double firstPos = Double.MIN_VALUE;
	    boolean picked = false;
	    for (int i=0; i<lastRiderFromEachTeam.size(); i++) {
		Rider r = (Rider)lastRiderFromEachTeam.elementAt(i);
		if (r.info.position() > firstPos && 
		    r.info.position() > leader.info.position() &&
		    r.info.speed() >= minSpeed &&
		    r.info.speed() <= maxSpeed &&
		    r.info.position() < leader.info.position() + 50*leader.info.speed()) {
		    firstPos = r.info.position();
		    draftTarget = r;
		    picked = true;
		}
	    }
	    if (!picked) {
		draftTarget = leader;
		runLine();
		return;
	    }
	}
	leader.follow(draftTarget);
	// check that we're not being blocked
	if (draftTarget.info.lane() == leader.info.lane()) {
	    for (int t=0; t<allInfo.length; t++) {
		if (t == myIndex) continue; // don't scan our own riders
		double lastPos = Double.MAX_VALUE;
		for (int i=0; i<allInfo[t].length; i++) {
		    // 3*numriders: 1.5 * a full team length
		    if (allInfo[t][i].lane() == leader.info.lane() &&
			allInfo[t][i].position() > leader.info.position() &&
			allInfo[t][i].position() < draftTarget.info.position() - 3*numRiders) {
			leader.goToLane(leader.info.lane() + (Math.random()<0.5?-1:1));
			break;
		    }
		}
	    }
	}
	else if (!clearPassage(leader.info.lane() + leader.lanechange)) {
	    leader.lanechange = 0;
	}
	for (int i=1; i<rOrd.size(); i++)
	    ((Rider)rOrd.elementAt(i)).imitate((Rider)rOrd.elementAt(i-1));
    }

    private void goToClearLane() {
	Rider leader = (Rider)rOrd.firstElement();
	boolean[] clearLanes = new boolean[numLanes];
	for (int i=0; i<clearLanes.length; i++)
	    if (!playersAhead(i))
		clearLanes[i] = true;
	int goodLane = leader.info.lane();
	int min = numLanes;
	int middleness = 0;
	if (!clearLanes[goodLane])
	    for (int i=0; i<clearLanes.length; i++) {
		if (clearLanes[i] && clearPassage(i) && 
		    Math.abs(leader.info.lane()-i) < min &&
		    Math.min(leader.info.lane(),numLanes-leader.info.lane()) > middleness) {
		    middleness = Math.min(leader.info.lane(),numLanes-leader.info.lane());
		    min = leader.info.lane()-i;
		    goodLane = i;
		}
	    }

	leader.goToLane(goodLane);
	for (int i=1; i<rOrd.size(); i++)
	    ((Rider)rOrd.elementAt(i)).imitate((Rider)rOrd.elementAt(i-1));
    }

    private boolean clearPassage(int lane) {
	Rider leader = (Rider)rOrd.firstElement();
	Rider last = (Rider)rOrd.lastElement();
	for (int t=0; t<allInfo.length; t++) {
	    if (t == myIndex) continue; // don't scan our own riders
	    for (int i=0; i<allInfo[t].length; i++)
		if (allInfo[t][i].lane() >= Math.min(leader.info.lane(), lane) &&
		    allInfo[t][i].lane() <= Math.max(leader.info.lane(), lane) &&
		    allInfo[t][i].position() < leader.info.position() + 2 &&
		    allInfo[t][i].position() > last.info.position() - 2)
		    return false;
	}
	return true;
    }
	
    private boolean playersAhead(int lane) {
	Rider leader = (Rider)rOrd.firstElement();
	for (int t=0; t<allInfo.length; t++) {
	    if (t == myIndex) continue; // don't scan our own riders
	    for (int i=0; i<allInfo[t].length; i++)
		if (allInfo[t][i].position() > leader.info.position() &&
		    allInfo[t][i].position() < leader.info.position() + 5*leader.info.speed() &&
		    allInfo[t][i].lane() == lane)
		    return true;
	}
	return false;
    }

    private boolean allInSameLane() {
	int lane = ((Rider)rOrd.firstElement()).info.lane();
	for (int i=1; i<rOrd.size(); i++)
	    if (((Rider)rOrd.elementAt(i)).info.lane() != lane)
		return false;

	return true;
    }

    private boolean othersInBetween() {
	double frontpos = ((Rider)rOrd.firstElement()).info.position();
	double rearpos = ((Rider)rOrd.lastElement()).info.position()-2;
	int lane = ((Rider)rOrd.firstElement()).info.lane();
	for (int t=0; t<allInfo.length; t++) {
	    if (t == myIndex) continue; // don't scan our own riders
	    for (int i=0; i<allInfo[t].length; i++)
		if (allInfo[t][i].lane() == lane &&
		    allInfo[t][i].position() < frontpos &&
		    allInfo[t][i].position() > rearpos)
		    return true;
	}
	return false;
    }

    private void runLine() {
	// break away from team you're drafting
	goToClearLane();
	// calc speed periodically after you get into position
	if (turn%100 == 0)
	    optimalSpeed = calcBestSpeed();
	for (int i=0; i<rOrd.size(); i++)
	    ((Rider)rOrd.elementAt(i)).goToSpeed(optimalSpeed);
    }

    public Move move() throws Exception {
	turn = olympics.currTime();
	info = olympics.TeamStatus(myIndex);
	allInfo = olympics.TeamStatus();
	double[] accelerations = new double[numRiders];
	int[] lanechanges = new int[numRiders];

	if (!onetime) {
	    onetime = true;
	    setupOrder(rOrd);
	    optimalSpeed = Math.pow(initialEnergy/(double)totalLength, 2.0/3.0)
		-rand.nextDouble();
	}

	// remove dead riders from the list
	for (int i=0; i<rOrd.size(); i++) {
	    Rider r = (Rider)rOrd.elementAt(i);
	    if (!r.alive()) {
		rOrd.remove(r);
		i--;
	    }
	}	
	if (rOrd.size() == 0)
	    return new Move(accelerations, lanechanges);
	
	// leader, lead
	Rider leader = (Rider)rOrd.firstElement();
	leader.lead(optimalSpeed, leader.info.lane()); 
	numInPosition = 1;

 	// everyone else, follow the rider ahead of you in sequence
	for (int i=1; i<rOrd.size(); i++) {
	    Rider r = (Rider)rOrd.elementAt(i);
	    if (r.follow(targetOf(r)))
		numInPosition++;
	}

	// end (& beginning?) of race: run alone
	// middle: follow other team
	if (numInPosition == rOrd.size()) {
	    if (leader.info.position()/totalLength > 0.05 &&
		leader.info.position()/totalLength < 0.95 && allInfo.length > 1)
		draftOtherTeam();
	    else 
		runLine();
	}
	else if (othersInBetween()) {
	    if (leader.info.lane() < 5)
		direction = false;
	    else if (numLanes - leader.info.lane() < 5)
		direction = true;
	    leader.goToLane(leader.info.lane() + (direction?-1:1));
	    for (int i=1; i<rOrd.size(); i++)
		((Rider)rOrd.elementAt(i)).goToLane(leader.info.lane() + (direction?-1:1));
	}
	// write updates from Rider objects to movement arrays
	for (int i=0; i<rOrd.size(); i++) {
	    Rider r = (Rider)rOrd.elementAt(i);
	    accelerations[r.id] = r.acceleration;
	    lanechanges[r.id] = r.lanechange;
	}
	return new Move(accelerations, lanechanges);
    }
}
