import java.util.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;

class StringHolder {
    String text;
    public StringHolder() {}
}

/** This is the simulator for the battle.*/
public class GameSimulator extends Simulator {
    GamePlayer mainP;
    JDialog popup;
    Maze theMaze;
    //Random rg;
    //int numTurns;
    int numRB;
    int numJ;
    int numDough;
    int numCluesVisible;
    int currentTopClue;
    int firstVisibleClue;
    Doughnut droppedDoughnut;
    MazeObject[][] world;
    //boolean finished;
    int playersFinished;
    float wallDensity;

    SimulationState simState;
    public Vector objects;
    //public Clue[] clues;
    PlayerInitializer pInit;
    boolean addCops;

    //int NUM_PLAYERS;
    public static final int NUM_CLUES = 20;

    //public static final int QUIT_TIME = 40;
    public static final int BOTTOM_ENERGY = 40;
    public static final int BOTTOM_INSPIRATION = 40;
    public static final int SUNRISE = 1200;
    public static final int SUNSET = 720;

    public static final int TOTAL_DOUGHNUTS = 4;
    public static final int TOTAL_JAYS = 4;
    public static final int TOTAL_REDBULLS = 4;
    public static final int TOTAL_COPS = 5;

    public static final int LEAST_CLUE_TIME = 15;
    public static final float WALL_DENSITY = (float)0.08;

    /** constructor: initializes the maze, the vector, and the random
     number generator */
    public GameSimulator() {
	// default size
	this(40,40);
    }

    /** constructor - takes dimensions for the maze */
    public GameSimulator(int w, int h) {
	this(new Maze(w,h));
    }

    /** constructor - takes a maze */
    public GameSimulator(Maze aMaze) {
	theMaze = aMaze;
	world = new MazeObject[theMaze.width][theMaze.height];
	objects = new Vector();
	//rg = new Random();
	pInit = new PlayerInitializer();
	simState = new SimulationState(objects, pInit.getNumPlayers(),
				       NUM_CLUES);
    }

    public void setInitialState() {
	wallDensity = WALL_DENSITY;
	simState.timeElapsed = 0;
	numJ=0;
	numRB=0;
	numDough=0;
	playersFinished = 0;
	objects.clear();
	// clear world
	for (int i=0; i<theMaze.width; i++)
	    for (int j=0; j<theMaze.height; j++)
		world[i][j] = null;
	theMaze.RandomInitWalls(wallDensity);
	int h = theMaze.height;
	int w = theMaze.width;
	// create players
	for (int i=0; i<simState.numPlayers; i++) {
	    Player player = new Player(i, pInit.getImageFileName(i));
	    PlayerPercept pp = new PlayerPercept(player, theMaze, this);
	    PlayerInterface pi = new PlayerInterface(player, pp);

	    // Here is where we have to instantiate the teams' code.
	    player.pa = pInit.getActor(pi, i);
	    player.name = pInit.getName(i);
	    player.pp = pp;

	    simState.players[i] = player;
	}
	// now add them to the world and the percepts
	for (int i=0; i<simState.numPlayers; i++) {
	    int x,y, xOffset, yOffset;
	    xOffset = GamePlayer.nextInt(9) + 11;
	    x = (GamePlayer.nextFloat() < 0.5) ? 20 + xOffset : 20 - xOffset;
	    yOffset = 30 - xOffset;
	    y = (GamePlayer.nextFloat() < 0.5) ? 20 + yOffset : 20 - yOffset;
	    addObjectAt(simState.players[i], x,y);
	}
	// add all clues - only first one is visible
	for (int i=0; i<NUM_CLUES; i++) {
	    Clue c = new Clue(i+1);
	    // add first clue in center
	    if (i == 0)
		addObjectAt(c, 20,20);
	    else
		addObject(c);
	    c.plan(theMaze);
	    if (i>0)
		c.visible = false;
	    simState.clues[i] = c;
	}
	numCluesVisible = 1;
	firstVisibleClue = 0;
	currentTopClue = 0;
	// all players are looking for first clue
	for (int i=0; i<simState.numPlayers; i++)
	    simState.players[i].solveClue(simState.clues[0]);

	// create other objects
	for (int i=0; i<TOTAL_DOUGHNUTS; i++) {
	    addObject(new Doughnut());
	    numDough++;
	}
	for (int i=0; i<TOTAL_JAYS; i++) {
	    addObject(new Jay(theMaze));
	    numJ++;
	}
	for (int i=0; i<TOTAL_REDBULLS; i++) {
	    addObject(new RedBull(theMaze));
	    numRB++;
	}
	// add cops
	for (int i=0; i<TOTAL_COPS; i++)
	    addObject(new Cop(this, true));
    }

    /** If the simulation is not over, returns true */
    public boolean CanDoAnotherStep() {
	if (firstVisibleClue == NUM_CLUES)
	    return false;
	else
	    return true;
    }

    public int getTimeElapsed() {
	return(simState.timeElapsed);
    }

    private void tryToSolveClue(Player player) {
	if (player.thinking == player.thinktime) {
	    player.thinking = player.thinktime = 0;
	    player.solveClue(simState.clues[player.cluesSolved+1]);
	    if (player.cluesSolved > currentTopClue) {
		currentTopClue = player.cluesSolved;
		simState.clues[currentTopClue].visible = true;
		numCluesVisible++;
	    }
	}
	else
	    player.thinking++;
    }

    private void updatePlayerValues(Player player) {
	// burst of energy and inspiration at sunrise
	if (simState.timeElapsed == SUNRISE)
	    player.energy = (player.energy+15 > 100) ? 
		100 : player.energy+15;
	// burst of inspiration at 4:20
	if (simState.timeElapsed % 720 == 380)
	    player.inspiration = (player.inspiration+5 > 100) ? 
		100 : player.inspiration + 5;
	if (simState.timeElapsed % 15 == 0) {
	    if (player.energy > BOTTOM_ENERGY) {
		// lose more energy at night
		if (simState.timeElapsed > SUNSET && 
		    simState.timeElapsed < SUNRISE) {
		    if (player.energy == BOTTOM_ENERGY+1)
			player.energy = BOTTOM_ENERGY;
		    else
			player.energy-=2;
		}
		else
		    player.energy-=1;
	    }
	    if (player.inspiration > BOTTOM_INSPIRATION)
		player.inspiration-=1;
	}
    }

    private void checkForCops(Player player) {
	if (player.held() > 0) {
	    if (player.held() == 30) {
		player.setHeld(0);
		if (!player.holdingCop.superCop) {
		    player.holdingCop.delete = true;
		    player.holdingCop = null;
		} else {
		    player.holdingCop.gotPlayer = false;
		    //stop cop for two turns
		    player.holdingCop.setEating(28);
		    player.holdingCop = null;
		}
	    }
	    else
		player.setHeld(player.held() + 1);
	}
    }

    private void findClue(Player player, Clue c) {
	player.foundClue(c);
	// the last player to find it?
	if (c.found == simState.numPlayers) {
	    //System.out.println("last player found clue");
	    c.delete = true;
	    numCluesVisible--;
	    firstVisibleClue++;
	}
	// the last clue?
	if (player.cluesSolved == NUM_CLUES-1) {
	    // make sure it's ordered right
	    player.cluesSolved++;

	    // set delete flag and finish time
	    player.delete = true;
	    player.finishtime = simState.timeElapsed;
	    // remove from other player's percepts
	    removeObject(player);
	    playersFinished++;
	}
	else {
	    player.thinking = 1;
	    player.thinktime = LEAST_CLUE_TIME + 
		(100 - player.inspiration);
	}
    }

    private void movePlayer(Player player) {
	// deal with cops
	checkForCops(player);
	boolean canMove = true;
	int command = player.nextMove();
	// deal with non-moving player
	if ((player.held() == 0) && (command < 0)) {
	    player.noMove++;
	    // if he's standing still too long, move to a random clear spot if possible
	    if (player.noMove > 8) {
		int tries = 0;
		boolean done = false;
		while (tries < 10 && !done) {
		    command = GamePlayer.nextInt(4);
		    if ((command == 0 && player.pp.clearUp) ||
			(command == 1 && player.pp.clearRight) ||
			(command == 2 && player.pp.clearDown) ||
			(command == 3 && player.pp.clearLeft)) {
			done = true;
		    }
		    tries++;
		}
	    }
	}
	Position newpos;
	if (player.energy/100.0 > GamePlayer.nextFloat() && command >=0) {
	    newpos = getNewPos(player.x(), player.y(), command);
	    if (theMaze.CoordsAreInMaze(newpos.x,newpos.y) && 
		theMaze.wallIsBetween(player.x(), player.y(), 
				      newpos.x, newpos.y) == false) {
		MazeObject mo = world[newpos.x][newpos.y];
		if (mo != null) {
		    // pick up doughnuts
		    if (mo instanceof Doughnut) {
			player.pickUpDoughnut();
			// 5 energy for scooping neighbor's donut
			int nd = ((Doughnut)mo).neighborsDoughnut();
			if (nd > -1 && nd != player.id)
			    player.energy = (player.energy+5 > 100) ?
				100 : player.energy+5;
			mo.delete = true;
			// remove from players' percepts
			removeObject(mo);
			numDough--;
		    }
		    // drink red bulls for energy
		    else if (mo instanceof RedBull) {
			player.energy = (player.energy+10 > 100) ?
			    100 : player.energy+10;
			mo.delete = true;
			// remove from players' percepts
			removeObject(mo);
			numRB--;
		    }
		    // smoke jays for inspiration
		    else if (mo instanceof Jay) {
			player.inspiration = (player.inspiration+10  > 100) ?
			    100 : player.inspiration+10;
			mo.delete = true;
			// remove from players' percepts
			removeObject(mo);
			numJ--;
		    }
		    // begin working on clue, unless end
		    else if (mo instanceof Clue) {
			//System.out.println("on clue " + ((Clue)mo).id);
			mo.visible = false;
			((Clue)mo).trampled = true;
			// make sure it's the clue the player's looking for
			if (mo.getPosition().equals(player.cluePos))
			    findClue(player, (Clue)mo);
		    }
		    // if other player is on clue, you find clue also
		    else if (mo instanceof Player) { 
			canMove = false;
			if (mo.getPosition().equals(player.cluePos)) {
			    findClue(player, player.clue);
			}
		    }
		    // can't move through cops or other players
		    else if (mo instanceof Cop)
			canMove = false;
		}
		if (canMove == true) {
		    world[player.x()][player.y()] = null;
		    world[newpos.x][newpos.y] = player;
		    player.Move(newpos.x, newpos.y);
		    player.noMove = 0;
		}
	    }
	}
    }

    private void moveCop(Cop cop, Position newpos) {
	boolean canMove = true;
	if (theMaze.CoordsAreInMaze(newpos.x, newpos.y)) {
	    MazeObject mo = world[newpos.x][newpos.y];
	    if (mo != null) {
		if (mo instanceof Doughnut) {
		    mo.delete = true;
		    // remove from players' percepts
		    removeObject(mo);
		    numDough--;
		    cop.setEating(cop.getEating()+1);
		}
	    else
		canMove = false;
	    }
	    if (canMove == true) {
		world[cop.x()][cop.y()] = null;
		world[newpos.x][newpos.y] = cop;
		cop.Move(newpos.x, newpos.y);
	    }
	}
	// apprehend if chased player is adjacent, unless wall between
	if (((Cop)cop).chasingPlayer) {
	    Player player = ((Cop)cop).chasedPlayer;
	    if (adjacent(cop.x(), cop.y(), player.x(), player.y()) &&
		!theMaze.wallIsBetween(cop.x(), cop.y(), 
				       player.x(), player.y())) {
		((Cop)cop).gotPlayer = true;
		player.setHeld(player.held() +1);
		player.holdingCop = (Cop)cop;
	    }
	}
    }

    private boolean adjacent(int x1, int y1, int x2, int y2) {
	if (x1 == x2)
	    return (Math.abs(y2-y1) == 1);
	else if (y1 == y2)
	    return (Math.abs(x2-x1) == 1);
	else
	    return false;
    }

    private void updateObject(MazeObject obj, ListIterator i) {
	Position newpos;
	int command = obj.nextMove();
	// remove deleted cops
	if (obj instanceof Cop && obj.delete == true) {
	    world[obj.x()][obj.y()] = null;
	    removeObject(obj);
	    i.remove();
	}
	else if (command >=0) {
	    newpos = getNewPos(obj.x(), obj.y(), command);
	    // deal with doughnut eating cops
	    if (obj instanceof Cop)
		moveCop((Cop)obj, newpos);
	    // move other objects if new cell is in maze and empty
	    else if (theMaze.CoordsAreInMaze(newpos.x, newpos.y) &&
		     world[newpos.x][newpos.y] == null) {
		world[obj.x()][obj.y()] = null;
		world[newpos.x][newpos.y] = obj;
		obj.Move(newpos.x, newpos.y);
	    }
	}
    }

    private void createObjects() {
	// if player dropped doughnut, add it to the objects vector
	if (droppedDoughnut != null) {
	    objects.add(droppedDoughnut);
	    droppedDoughnut = null;
	}
	// fill the world with objects
	while(numDough < TOTAL_DOUGHNUTS) {
	    addObject(new Doughnut());
	    numDough++;
	}
	while (numRB < TOTAL_REDBULLS) {
	    addObject(new RedBull(theMaze));
	    numRB++;
	}
	while (numJ < TOTAL_JAYS) {
	    addObject(new Jay(theMaze));
	    numJ++;
	}
    }

    private void performCommands(Player player) {
	// drop doughnuts
	if (player.dropDoughnut == true) {
	    if (player.doughnutCount > 0) {
		player.doughnutCount--;
		Position pos = getClosePos(player.getPosition(), 1);
		droppedDoughnut = new Doughnut(player.id);
		addObjectAt(droppedDoughnut, pos.x, pos.y);
		numDough++;
	    }
	    player.dropDoughnut = false;
	}
	// eat doughnuts
	if (player.eatDoughnut == true) {
	    if (player.doughnutCount > 0) {
		player.doughnutCount--;
		player.energy = (player.energy + 5) > 100 ? 
		    100 : player.energy + 5;
	    }
	    player.eatDoughnut = false;
	}
    }

    private void updatePlayer(Player player) {
	// The player must determine its commands
	player.pp.updatePercept(player,this);
	player.act();
	if (player.thinking > 0)
	    tryToSolveClue(player);
	if (simState.timeElapsed % 5 == 0)
	    updatePlayerValues(player);
	// react to any non-movement commands
	performCommands(player);
	movePlayer(player);
    }

    private void randomizePlayers() {
	for (int i=0; i<simState.numPlayers-playersFinished; i++) {
	    int j = GamePlayer.nextInt(simState.numPlayers-playersFinished);
	    // swap player with another, unless other is same
	    if (i != j) {
		Player p = (Player)(objects.elementAt(j));
		objects.setElementAt(objects.elementAt(i), j);
		objects.setElementAt(p, i);
	    }
	}
    }

    /** Moves the simulation one step */
    public void Step() {
	// update time elapsed
	simState.timeElapsed++;
	//randomize player order
	randomizePlayers();
	// update all maze objects
	for (ListIterator i = objects.listIterator(); i.hasNext();) {
	    MazeObject obj = (MazeObject)(i.next());
	    if (obj.delete) {
		if ((obj instanceof Cop) || (obj instanceof Player))
		    world[obj.x()][obj.y()] = null;
		i.remove();
		continue;
	    }
	    if (obj instanceof Player) {
		//System.out.println(((Player)obj).id);
		updatePlayer((Player)obj);
	    }
	    // replace any "trampled" clues
	    else if (obj instanceof Clue) {
		if (((Clue)obj).trampled && world[obj.x()][obj.y()] == null) {
		    world[obj.x()][obj.y()] = obj;
		    //System.out.println("id " + ((Clue)obj).id);
		    if (((Clue)obj).id <= currentTopClue+1)
			obj.visible = true;
		    ((Clue)obj).trampled = false;
		}
	    }
	    else {
		updateObject(obj, i);
	    }
	}
	//System.out.println("------");
	createObjects();
	// sort players by rank
	Arrays.sort(simState.players, simState.players[0]);
    }

    public void PostSimulationStep(GamePlayer gp) {
	mainP = gp;
	popup = new JDialog();
	popup.setLocationRelativeTo(gp);
	popup.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
	Container contentPane = popup.getContentPane();
	JPanel buttonPane = new JPanel();
	buttonPane.setLayout(new FlowLayout(FlowLayout.CENTER));
	JButton button;
	button = new JButton("Play again");
	button.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    popup.dispose();
		    mainP.ResetAnimation();
		    mainP.StartAnimation();
		}
	    });
	button.setBorder(BorderFactory.createRaisedBevelBorder());
	buttonPane.add(button);
	button = new JButton("Quit");
	button.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    popup.dispose();
		    mainP.Quit();
		}
	    });
	button.setBorder(BorderFactory.createRaisedBevelBorder());
	buttonPane.add(button);
	contentPane.add(buttonPane, BorderLayout.CENTER);
	popup.pack();
	popup.show();
    }

    private void removeObject(MazeObject obj) {
	for (int i=0; i<simState.numPlayers; i++)
	    if (!simState.players[i].delete)
		simState.players[i].pp.removeObject(obj.po);
    }

    private void addObject(MazeObject obj) {
	int x, y;
	// find an empty world slot
	do {
	    x = GamePlayer.nextInt(theMaze.width);
	    y = GamePlayer.nextInt(theMaze.height);
	}
	while (world[x][y] != null);

	// create an object there
	addObjectAt(obj, x, y);
    }

    private void addObjectAt(MazeObject obj, int x, int y) {
	// move object to new location
	obj.Move(x,y);

	// add to percepts - don't add clues
	if (!(obj instanceof Clue)) {
	    for (int i=0; i<simState.numPlayers; i++)
		// don't add self and don't add to deleted players
		if ((!(obj instanceof Player) || ((Player)obj).id != i)
		    && !simState.players[i].delete)
		    simState.players[i].pp.addObject(obj.po);
	}
	// don't add dropped doughnut to vector yet
	if (!(obj instanceof Doughnut) || 
	    ((Doughnut)obj).neighborsDoughnut() == -1)
	    objects.add(obj);
	// add object to world
	world[x][y] = obj;
    }

    public static int distanceTo(Position pos1, Position pos2) {
	return distanceTo(pos1.x, pos1.y, pos2.x, pos2.y);
    }

    public static int distanceTo(int x1, int y1, int x2, int y2) {
	int xdist = Math.abs(x1 - x2);
	int ydist = Math.abs(y1 - y2);
	return Math.max(xdist, ydist);
    }

    public static int distanceTo(Position pos, int x, int y) {
	return distanceTo(pos.x, pos.y, x, y);
    }

    private Position getClosePos(Position pos, int distance) {
	int x, y;
	do {
	    x = pos.x;
	    y = pos.y;
	    int xoffset = GamePlayer.nextInt(distance) + 1;
	    int yoffset = GamePlayer.nextInt(distance) + 1;
	    if (GamePlayer.nextFloat() < 0.5)
		x += xoffset;
	    else
		x -= xoffset;
	    if (GamePlayer.nextFloat() < 0.5)
		y += yoffset;
	    else
		y -= yoffset;
	}
	while (theMaze.CoordsAreInMaze(x, y) == false || world[x][y] != null);
	return new Position(x,y);
    }
	

    public static Position getNewPos(int x, int y, int command) {
	int newx = x;
	int newy = y;
	Position newpos = new Position();
	switch(command) {
	case 0:
	    newy = y-1; break;
	case 1: 		
	    newx = x+1; break;
	case 2: 		
	    newy = y+1; break;
	case 3: 		
	    newx = x-1; break;
	default:
	}
	newpos.x = newx;
	newpos.y = newy;
	return newpos;
    }
}
