package jp.ac.nii.icpc2010.playfield;

import java.awt.Point;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
import java.util.Vector;

import jp.ac.nii.icpc2010.ITron;
import jp.ac.nii.icpc2010.Tron;
import jp.ac.nii.icpc2010.TronException;
import jp.ac.nii.icpc2010.display.DisplayDirection;
import jp.ac.nii.icpc2010.manager.IOptionSource;
import jp.ac.nii.icpc2010.manager.OptionsManager;
import jp.ac.nii.icpc2010.manager.QuoteManager;
import jp.ac.nii.icpc2010.score.AbstractScore;



public class PlayField extends AbstractPlayField
{
	private final int UNDERFIELD_COIN = 1;

	private int[][] _field;
	private DisplayDirection[][] _fieldDirection;
	private int[][] _underField;
	private Vector<PlayerSummary> _players;
	private Vector<Tron> _trons;
	private Vector<Point> _startPoints;
	private Vector<Point> _itemPoints;

	private long _ticks;

	private String _filename;
	private int _numOfCoins;

	private AbstractScore[] _scores;
	private AbstractScore[] _totalScores;
	private Method _getScoresMethod;

	private int _numOfComments;
	private Deque<String> _comments;

	private boolean _displayResized;

	public enum GameState {
		//INTRO,
		RUNNING,
		RESULT,
	}

	protected GameState _gameState;

	public GameState getGameState() {
		return _gameState;
	}


	private OptionsManager _options;


	public void reset() throws IOException, TronException{

		_round++;
		_turn = 0;
		_numOfCoins = 0;
		_gameState = GameState.RUNNING;
		//synchronized(_comments){
		//	_comments.clear();
		//}

		// set players

		synchronized(this){
			loadFromFile(_filename);
		}

		//set trons
		resetTrons();

		// set coins
		putItems(_options.getItemInitProb());

		// set scores
	}
	public void reset(String filename) throws IOException, TronException{
		_filename = filename;
		reset();
	}
	public void init() throws IOException, TronException{

		_gameround = _options.getGameRounds();
		_maxturn = _options.getMaxTurns();
		_round = 0;
		_turn = 0;
		_numOfCoins = 0;
		_gameState = GameState.RUNNING;
		_comments = new ArrayDeque<String>();

		// set players
		initPlayers();

		if (!_options.isTournamentMode())
			System.out.println("Load file '" + _filename + "'");

		synchronized(this){
			loadFromFile(_filename);
		}

		//set trons
		initTrons();

		// set coins
		putItems(_options.getItemInitProb());

		// set scores
		initScores();
	}
	public void init(String filename) throws IOException, TronException{
		_filename = filename;
		init();
	}

	public PlayField(OptionsManager om){
		_options = om;
		_numOfComments = _options.getCommentLines();
		_displayResized = false;
	}
	public PlayField(String filename, OptionsManager om) throws IOException, TronException
	{
		this(om);
		init(filename);
	}

	public PlayField(PlayField playField, OptionsManager om) {

		this._field = playField._field.clone();
		_options = om;
	}
	private void initPlayers() throws TronException{
		int numOfPlayers = _options.getNumOfPlayers();

		_players = new Vector<PlayerSummary>(); 

		Vector<String> playerNames = _options.getPlayerClasses();
		if(numOfPlayers > playerNames.size()){
			throw new TronException("Inconsistent setting -- lack of players");
		}

		for(int i = 0; i < numOfPlayers; i++){
			_players.add(new PlayerSummary(playerNames.get(i)));
		}		
	}

	@SuppressWarnings("unchecked")
	private void initScores(){
		String scoreClassName = "jp.ac.nii.icpc2010.score." + _options.getScoreClass();

		Class<? extends AbstractScore> scoreClass = null;
		try {
			scoreClass = (Class<? extends AbstractScore>) Class.forName(scoreClassName);
			_getScoresMethod = scoreClass.getMethod("getScores", new Class[]{PlayField.class});
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}

		try {
			// this is valid because _score is newly generated soon.
			_scores = (AbstractScore[]) _getScoresMethod.invoke(null, new Object[]{this});
			_totalScores = new AbstractScore[getNumOfPlayers()];
			for(int i = 0; i < getNumOfPlayers(); i++){
				_totalScores[i] = scoreClass.newInstance();
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

	private void initTrons(){
		int numOfPlayers = _players.size();

		_trons = new Vector<Tron>();
		for(int i = 0; i < numOfPlayers; i++){
			int x = _startPoints.get((i + _round) % numOfPlayers).x;
			int y = _startPoints.get((i + _round) % numOfPlayers).y;
			_trons.add(new Tron(i, x, y, FieldDirection.Up, _players.get(i).getName(), _options));
			setObjectAt(x, y, playerTrail(i));
		}		
	}
	private void resetTrons(){
		// TODO
		initTrons();
	}

	/**
	 * Load a level from a file, and do some basic checking of the level's integrity.
	 * Sets up most of the instance variables in this class based on the contents of the file.
	 * 
	 * @param filename
	 * @throws TronException If the level is inconsistent.
	 * @throws IOException If there is a problem reading the file.
	 */

	private void loadFromFile(String filename) throws TronException, IOException {

		Vector<String> buffer = new Vector<String>();
		try {
			BufferedReader input =  new BufferedReader(new FileReader("./res/" + filename));
			try {
				String line = null; 
				while (( line = input.readLine()) != null){
					buffer.add(line);
				}
			}
			finally {
				input.close();
			}
		}
		catch (IOException ex){
			ex.printStackTrace();
			throw ex;
		}
		//buffer.remove(buffer.size() - 1);


		int width = buffer.get(0).length();
		int height =  buffer.size();
		if(width != _width || height != _height){
			_displayResized = true;
		}
		_width = width;
		_height = height;

		_field = new int[_width][_height];
		_fieldDirection = new DisplayDirection[_width][_height];
		_underField = new int[_width][_height];
		_startPoints = new Vector<Point>();
		_itemPoints = new Vector<Point>();

		int lineLength = -1;

		int i = 0;
		for (String s : buffer) {

			//Check that all lines have the same length
			if (lineLength == -1) {
				lineLength = s.length();
			} else {
				if (s.length() != lineLength) {
					throw new TronException("Incorrect file format -- lines have different length");
				}
			}

			if (lineLength != _width) {
				throw new TronException("Incorrect file format -- expected width " + _width + ", got " + lineLength);
			}

			int j = 0;

			for (char c : s.toCharArray()) {
				if (_field[j] == null) {
					_field[j] = new int[_height];
				}

				switch(c) {
				case '0':
				case ' ':
					setObjectAt(j, i, FIELD_FREE);
					break;
				case '1':
					setObjectAt(j, i, FIELD_WALL);
					break;
				case '2':
				case '3':
					setObjectAt(j, i, FIELD_FREE);					
					_startPoints.add(new Point(j, i));
					break;
				case '4':
					setUnderObjectAt(j, i, UNDERFIELD_COIN);
					_itemPoints.add(new Point(j, i));
					break;
				}
				j++;
			}
			i++;
		}

		if (_startPoints.size() < _players.size()) {
			throw new TronException("Inconsistent setting -- lack of start points (got " + _startPoints.size() + ", needed " + _players.size() + ")");
		}

		if (!_options.isTournamentMode())
			System.out.println("Field size " + _width + "x" + _height);
	}

	public void setObjectAt(int x, int y, int object)
	{
		_field[x][y] = object;
	}
	
	public void setDirectionAt(int x, int y, DisplayDirection dir)
	{
		_fieldDirection[x][y] = dir;
	}


	public int getObjectAt(int x, int y)
	{
		return _field[x][y];
	}
	
	public DisplayDirection getDirectionAt(int x, int y)
	{
		return _fieldDirection[x][y];
	}

	public void setUnderObjectAt(int x, int y, int object){
		_underField[x][y] = object;
	}
	public int getUnderObjectAt(int x, int y){
		return _underField[x][y];
	}

	public int getNumOfPlayers(){
		return _players.size();
	}
	public Vector<String> getPlayerNames(){
		Vector<String> playerNames = new Vector<String>();
		for(PlayerSummary player: _players){
			playerNames.add(player.getName());
		}
		return playerNames;
	}

	//	public int getTronId(int playerId){
	//		return _players.get(playerId).getTronId();
	//	}
	public Tron getTron(int playerId)
	{
		return (Tron) _trons.get(playerId);
	}

	/**
	 * Gets the trons without copying the vector. Internal API.
	 * getTrons is now a public API that actually copies the vector.
	 * @return
	 */
	public Vector<Tron> getTronsNocopy()
	{
		return _trons;
	}

	public AbstractScore getScore(int playerId)
	{
		return _scores[playerId];
	}

	public AbstractScore[] getScores()
	{
		return _scores;
	}

	public AbstractScore getTotalScore(int playerId)
	{
		return _totalScores[playerId];
	}

	public AbstractScore[] getTotalScores()
	{
		return _totalScores;
	}

	public void update(FieldDirection[] inputs){
		_turn++;

		for(int i = 0; i < inputs.length; i++)
		{
			if(getTron(i).isAlive()){
				getTron(i).setDirection(inputs[i]);
			}
		}

		if(! checkField())
		{
			for(int i = 0; i < getNumOfPlayers(); i++){
				_totalScores[i].add(_scores[i]);
			}
			QuoteManager.Singleton().loadNewQuote();
			_gameState = GameState.RESULT;
		}

		setTicks(System.currentTimeMillis());

		return;
	}

	private boolean checkField()
	{
		//Trons pos are updated.
		//check whether they collide with each other/wall

		Vector<Tron> alives = new Vector<Tron>();
		for(Tron tron : _trons)
		{
			if(tron.isAlive()){
				alives.add(tron);

				tron.nextStep(this);

				int x = tron.getNextX();
				int y = tron.getNextY();
				int object = getObjectAt(x, y);
				switch(object)
				{
				case FIELD_FREE:
				case FIELD_COIN:
					break;
				case FIELD_WALL:
				{
					String s =  "[" + _turn + "] Player " + tron.getPlayerId() + " dies: wall (" + x + "," + y + ")";
					//System.out.println(s);
					putComment(s);
					tron.die();
					break;
				}
				default:
				{
					if(object >= playerTrail(0) && object < playerTrail(_players.size())){
						String s = "[" + _turn + "] Player " + tron.getPlayerId() +	" dies: trail " + playerId(object) + " (" + x + "," + y + ")";
						//System.out.println(s);
						putComment(s);
						tron.die();
					}
					break;
				}
				}
			}
		}

		//collide with each other
		for(int i = 0; i < _trons.size() - 1; i++) {
			Tron tron1 = _trons.get(i);
			for(int j = i + 1; j < _trons.size(); j++) {
				Tron tron2 = _trons.get(j);

				if(tron1.getNextX() == tron2.getNextX() && tron1.getNextY() == tron2.getNextY()) {
					String s = "[" + _turn + "] Players " + tron1.getPlayerId() + " and " + tron2.getPlayerId() + " die: collision (" + tron1.getNextX() + "," + tron1.getNextY() + ")";
					//System.out.println(s);
					putComment(s);
					tron1.die();
					tron2.die();
				}
			}
		}

		boolean gameEnd = true;
		for(Tron tron : alives){
			if(tron.isAlive()){
				gameEnd = false;
			}
		}

		if(_options.getMaxTurns() > 0 
				&& _turn > _options.getMaxTurns())
		{
			gameEnd = true;
		}

		if(gameEnd){
			return false;
		}


		//update field
		int aliveCount = 0;
		for(Tron tron : alives)
		{
			if(tron.isAlive()){
				aliveCount++;
				tron.step();

				int x = tron.getX();
				int y = tron.getY();
				int object = getObjectAt(x, y);
				switch(object){
				case FIELD_COIN:
					putComment("[" + _turn + "] Player " + tron.getPlayerId() + " gets: coin (" + x + "," + y + ")");
					tron.setCoins(tron.getCoins() + 1);
					break;
				}
				setObjectAt(x, y, playerTrail(tron.getPlayerId()));
				
				//Direction
				int lastX = tron.getLastX();
				int lastY = tron.getLastY();
				FieldDirection lastDir = tron.getLastDir();
				FieldDirection dir = tron.getDir();
				DisplayDirection dd = null;
				if(lastDir == dir && (lastDir == FieldDirection.Up || lastDir == FieldDirection.Down))
				{
					dd = DisplayDirection.UpDown;
				}
				else if(lastDir == dir)
				{
					dd = DisplayDirection.LeftRight;
				}
				else if((lastDir == FieldDirection.Down && dir == FieldDirection.Left) ||
						(lastDir == FieldDirection.Right && dir == FieldDirection.Up))
				{
					dd = DisplayDirection.LeftUp;					
				}
				else if((lastDir == FieldDirection.Up && dir == FieldDirection.Right) ||
						(lastDir == FieldDirection.Left && dir == FieldDirection.Down))
				{
					dd = DisplayDirection.RightDown;					
				}
				else if((lastDir == FieldDirection.Right && dir == FieldDirection.Down) ||
						(lastDir == FieldDirection.Up && dir == FieldDirection.Left))
				{
					dd = DisplayDirection.LeftDown;					
				}
				else if((lastDir == FieldDirection.Left && dir == FieldDirection.Up) ||
						(lastDir == FieldDirection.Down && dir == FieldDirection.Right))
				{
					dd = DisplayDirection.RightUp;					
				}
				else
				{
					if(dir == FieldDirection.Up || dir == FieldDirection.Down)
					{
						dd = DisplayDirection.UpDown;
					}
					else
					{
						dd = DisplayDirection.LeftRight;
					}
				}
				
				setDirectionAt(lastX, lastY, dd);
				
				if (_options.hasTrailLimit()){
					if(tron.getTrailLength() > this._width * this._height * _options.getTrailLimitAmount() / 1000) {
						try {
							clearTrail(playerTrail(tron.getPlayerId()), tron.removeTailX(), tron.removeTailY());
							//tron.setTraveled(tron.getTraveled() - 1);
						} catch (NoSuchElementException e) {}
					}
				}
			}
		}

		// regenerate coins
		putItems(_options.getItemRegenProb());


		// update scores
		try {
			_scores = (AbstractScore[]) _getScoresMethod.invoke(null, new Object[]{this});
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}

		//after score update
		if(aliveCount == 1 && _options.isLastmanStanding())
		{
			return false;
		}

		if (_options.isLoggingOn()) {
			//System.out.println("all trons are alive.");
			System.out.println("some trons are alive.");
		}
		return true;
	}

	private void setCoin(int x, int y){
		_field[x][y] = PlayField.FIELD_COIN;
		_numOfCoins++;
	}
	public void scatterCoins()
	{
		double prob = 1.0;

		for(int i = 0; i < this.getWidth(); i++)
		{
			for(int j = 0; j < this.getHeight(); j++)
			{
				if(_field[i][j] == PlayField.FIELD_FREE)
				{
					if(Math.random() > (1 - prob))
					{
						setCoin(i, j);
					}
				}
			}
		}
	}

	public void putItems(double prob)
	{
		for(Point p : _itemPoints){
			int x = p.x;
			int y = p.y;
			if(getObjectAt(x, y) == FIELD_FREE){
				if(Math.random() < prob){
					switch(getUnderObjectAt(x, y)){
					case UNDERFIELD_COIN:
						setCoin(x, y);
						break;
					}
				}
			}
		}		
	}

	public void clearTrail(int id, int x, int y) {
		setObjectAt(x, y, FIELD_FREE);
	}

	private void setTicks(long ticks) {
		_ticks = ticks;
	}
	public long getTicks() {
		return _ticks;
	}

	public void setKeyPressed(KeyEvent e){
		this.keyPressed = e;
	}

	public boolean isRunning(){
		return _gameState == GameState.RUNNING;
	}

	public String getPlayerName(int playerId){
		return _players.get(playerId).getName();
	}

	public int getNumOfCoins(){
		return _numOfCoins;
	}

	@Override
	public Vector<ITron> getTrons() {
		Vector<ITron> trons = new Vector<ITron>();
		for(Tron tron: _trons){
			trons.add(tron);
		}
		return trons;
	}

	// using returned object requires synchronized(ret)
	public Deque<String> getComments(){
		return _comments;
	}

	public void putComment(String comment){
		synchronized(_comments){
			//if(_comments.size() >= _options.getCommentLines()){
			if(_comments.size() >= _numOfComments){
				_comments.removeFirst();
			}
			_comments.addLast(comment);
		}
	}

	public void setNumOfComments(int n){
		_numOfComments = n;
	}

	public boolean isDisplayResized(){
		return _displayResized;
	}
	public void clearDisplayResized(){
		_displayResized = false;
	}
	
	@Override
	public int getRemainingRounds() {
		return this._options.getGameRounds() - this.getRound() - 1;
	}
	@Override
	public int getRemainingTurns() {
		return this._options.getMaxTurns() - this.getTurn() - 1;
	}
}
