package jp.ac.nii.icpc2010.display;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.imageio.ImageIO;

import jp.ac.nii.icpc2010.Tron;
import jp.ac.nii.icpc2010.manager.OptionsManager;
import jp.ac.nii.icpc2010.manager.QuoteManager;
import jp.ac.nii.icpc2010.playfield.IPlayField;
import jp.ac.nii.icpc2010.playfield.PlayField;
import jp.ac.nii.icpc2010.playfield.PlayField.GameState;


public class AWTGameDisplay extends Canvas implements IGameDisplay {
	private static final long serialVersionUID = 1L;

	private PlayField playField;
	private PlayField oldPlayField;

	boolean displayScore = true;

	private Vector<BufferedImage> background;
	private BufferedImage wall;
	private Vector<BufferedImage> trons;
	private Vector<BufferedImage> tronsHeads;
	private Vector<BufferedImage> tronsTrailCorners;
	private BufferedImage coin;

	private Graphics g;

	private static final int CELLS_X_OFFSET = 256;
	private static final int CELLS_Y_OFFSET = 0;

	private static final int BOARD_X_OFFSET = 5;
	public static final int BOARD_Y_OFFSET = 0;
	public static final int BOARD_LINE_HEIGHT = 16;
	public static final int BOARD_LINE_MARGIN = 4;

	private int cellWidth;
	private int cellHeight;
	private boolean resetBackbufferFlag;

	private OptionsManager om;

	public AWTGameDisplay (PlayField playField, OptionsManager om){
		this.playField = playField;
		this.om = om;
		try
		{
			initImages();
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(1);
		}
		cellWidth = om.getCellWidth();
		cellHeight = om.getCellHeight();
		this.resetBackbufferFlag = true;
	}


	public void init() {
		try
		{
			initImages();
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(1);
		}
		setBackground(Color.black);
		this.resetBackbufferFlag = true;
	}


	private Image dbImage = null;
	private Graphics dbg;
	@Override
	public void update(Graphics g) {

		if (om.getBackbufferEnabled()) {
			if (resetBackbufferFlag == true || dbImage == null)
			{
				this.resetBackbufferFlag = false;
				System.out.println("New backbuffer screen created");
				dbImage = createImage (this.getWidth(), this.getHeight());
				dbg = dbImage.getGraphics ();
				oldPlayField = null;
			}
			//            dbg.setColor (getBackground ());
			//            dbg.fillRect (0, 0, this.getSize().width, this.getSize().height);
			//            dbg.setColor (getForeground());
			paint (dbg);
			g.drawImage (dbImage, 0, 0, this);
		}
		else
			super.update(g);
	}

	public void reset(){
		this.resetBackbufferFlag = true;
	}

	public void setCellSize(int cellWidth, int cellHeight){
		if(cellWidth >= 0){
			this.cellWidth = cellWidth;
		}
		if(cellHeight >= 0){
			this.cellHeight = cellHeight;
		}
		reset();
	}
	public int getCellWidth(){
		return this.cellWidth;
	}
	public int getCellHeight(){
		return this.cellHeight;
	}


	/**
	 * For the source of this code, see http://stackoverflow.com/questions/196890/java2d-performance-issues
	 * We had performance problems on Linux.
	 * @param image
	 * @return
	 */
	private BufferedImage toCompatibleImage(BufferedImage image)
	{
		// obtain the current system graphical settings
		GraphicsConfiguration gfx_config = GraphicsEnvironment.
		getLocalGraphicsEnvironment().getDefaultScreenDevice().
		getDefaultConfiguration();

		/*
		 * if image is already compatible and optimized for current system 
		 * settings, simply return it
		 */
		if (image.getColorModel().equals(gfx_config.getColorModel()))
			return image;

		// image is not optimized, so create a new image that is
		BufferedImage new_image = gfx_config.createCompatibleImage(
				image.getWidth(), image.getHeight(), image.getTransparency());


		// get the graphics context of the new image to draw the old image on
		Graphics2D g2d = (Graphics2D) new_image.getGraphics();

		// actually draw the image and dispose of context no longer needed
		g2d.drawImage(image, 0, 0, null);
		g2d.dispose();


		// return the new optimized image
		return new_image; 
	}


	private BufferedImage readImage(String file) throws IOException {
		return toCompatibleImage(ImageIO.read(new File(file)));
	}

	private void initImages() throws IOException{

		//Toolkit kit = Toolkit.getDefaultToolkit();

		background = new Vector<BufferedImage>();

		for (String s : om.getBackgroundImageName())
			background.add(readImage("./res/" + s));

		wall = readImage("./res/" + om.getWallImageName());

		int nPlayer = om.getNumOfPlayers();
		trons = new Vector<BufferedImage>();
		tronsHeads = new Vector<BufferedImage>();
		tronsTrailCorners = new Vector<BufferedImage>();
		for(int i = 0; i < nPlayer; i++){
			trons.add(readImage("./res/" + om.getTronImageName(i)));
			if(om.isTronHeadImage())
			{
				tronsHeads.add(readImage("./res/" + om.getTronHeadImageName(i)));
			}
			tronsTrailCorners.add(readImage("./res/" + om.getTronTrailCornerImageName(i)));
		}
		coin = readImage("./res/" + om.getCoinImageName());
	}


	private long aho = 0;
	@Override
	public void paint(Graphics g) {
		aho++;
//		lastTronHeadPositions.clear();

		switch (playField.getGameState()) {

		case RUNNING:
			paintRunning(g);
			break;
		case RESULT:
			if(om.isDisplayQuote())
				paintResult(g);
			else
				paintRunning(g);
		}
	}


	private void paintRunning(Graphics g) {

		this.g = g;

		synchronized(playField) {

			for(int i = 0; i < playField.getWidth(); i++)
				for(int j = 0; j < playField.getHeight(); j++)
					drawCell(i, j);

			for(Tron tron : playField.getTronsNocopy())
				drawTron(tron);

			if(displayScore)
				drawScore();
		}

		oldPlayField = new PlayField(playField, om);
	}

	private void paintResult(Graphics g) {

		oldPlayField = null;
		paintRunning(g);

		double x1 = (double)getWidth() / 5f;
		double y1 = (double)getWidth() / 5f;

		g.setColor(Color.black);

		for (int y = (int)y1; y < getHeight() - (int)y1; y++) {
			for (int x = (int) x1; x < getWidth() - (int)x1; x++) {
				if ((x % 2 == 0 ? y + 1 : y) % 2 == 0)
					g.drawRect(x, y, 0, 0);
			}
		}

		g.setColor(Color.white);
		Font f = new Font(g.getFont().getName(), Font.PLAIN, 20);

		Graphics2D g2 = (Graphics2D)g;
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g.setFont(f);

		g.drawString(QuoteManager.Singleton().getWinner() + " says to " + QuoteManager.Singleton().getLoser() + ":", (int)x1 + 20, (int) (y1) + 20);
		String text = QuoteManager.Singleton().getQuote();
		StringTokenizer st = new StringTokenizer(text);

		int index = 0;
		int offset = 50;
		FontMetrics met = g2.getFontMetrics();
		String currentLine = "";
		String currentWord = "";

		while (st.hasMoreTokens() || !currentWord.equals("")) {

			while (met.getStringBounds(currentLine + " " + currentWord, g2).getWidth() < (getWidth() - x1 - x1 - 40)) {

				currentLine += currentWord + " ";

				if (!st.hasMoreTokens())
					break;

				currentWord = st.nextToken();
			}

			if (currentLine.startsWith(" "))
				currentLine = currentLine.substring(1);

			g2.drawString(currentLine, (int)x1 + 20, (int)y1 + offset);
			offset += 20;
			index++;
			if (currentLine.contains(currentWord))
				currentWord = "";

			currentLine = "";
		}

		g.drawString(QuoteManager.Singleton().getWinner() + " wins!!!", (int)x1 + 20, (int) (y1) + offset + 20);
	}

	private void drawAtCellXYCellSize(int xpos, int ypos, Graphics g, BufferedImage im, AffineTransform aff) {
		if(aff == null)
		{
			g.drawImage(im, cellStartX(xpos), cellStartY(ypos), cellWidth, cellHeight, this);
		}
		else
		{
			AffineTransformOp ato = new AffineTransformOp(aff, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
			BufferedImage dst = new BufferedImage(im.getWidth(), im.getHeight(), BufferedImage.TYPE_INT_ARGB);
			ato.filter(im, dst);
			g.drawImage(dst, cellStartX(xpos), cellStartY(ypos), cellWidth, cellHeight, this);
		}
	}

	public void drawBackGround(int xpos, int ypos)
	{
		assert(g != null);

		BufferedImage cellImage = background.get(0);
		drawAtCellXYCellSize(xpos, ypos, g, cellImage, null);
	}

//	private Vector<Point> lastTronHeadPositions = new Vector<Point>();

	public void drawCell(int xpos, int ypos) {

		if (om.getDrawOnlyChanges()) {

			if (oldPlayField != null) {

				try {
					if (oldPlayField.getObjectAt(xpos, ypos) == playField.getObjectAt(xpos, ypos)) {
						boolean occupied = false;
						for(Tron tron : playField.getTronsNocopy()) {

//							if (tron.getLastX() == xpos && tron.getLastY() == ypos)
//								occupied = true;
//							if (tron.getNextX() == xpos && tron.getNextY() == ypos)
//								occupied = true;
							
							if (Math.abs(tron.getLastX() - xpos) < 3 && Math.abs(tron.getLastY() - ypos) < 3)
								occupied = true;
							if (Math.abs(tron.getTailX() - xpos) < 3 && Math.abs(tron.getTailY() - ypos) < 3)
								occupied = true;
						}

						// redraw the field where a head was drawn in the last iteration
//						for (Point p : lastTronHeadPositions) 
//							if (p.x == xpos && p.y == ypos)
//								occupied = true;

						if (!occupied)
							return;
					}
			}
			catch (ArrayIndexOutOfBoundsException e) {

				dbImage = null;
				oldPlayField = null;
			}
			}
		}

		BufferedImage cellImage = null;
		assert(g != null);
		int object = playField.getObjectAt(xpos, ypos);

		boolean drawBG = false;
		AffineTransform aff = null;
		switch(object)
		{
		case IPlayField.FIELD_FREE:

			drawBG = true;
			break;
		case IPlayField.FIELD_WALL:

			cellImage = wall;
			break;
		case IPlayField.FIELD_COIN:
			cellImage = coin;
			drawBG = true;
			break;
		default:

			drawBG = true;
			if(object >= PlayField.playerTrail(0) && object < PlayField.playerTrail(this.playField.getNumOfPlayers())){
				int playerId = PlayField.playerId(object);
				if (xpos == playField.getTron(playerId).getX() && ypos == playField.getTron(playerId).getY()){
					cellImage = null;
				}else{
					DisplayDirection dd = playField.getDirectionAt(xpos, ypos);
					aff = new AffineTransform();
					if (dd == null) {
						cellImage = null;
						break;
					}
					switch(dd)
					{
					case LeftRight:
						cellImage = trons.get(playerId);
						break;
					case UpDown:
						cellImage = trons.get(playerId);
						aff.quadrantRotate(1, cellImage.getWidth()/2, cellImage.getHeight()/2);
						break;
					case LeftUp:
						cellImage = tronsTrailCorners.get(playerId);
						aff.quadrantRotate(2, cellImage.getWidth()/2, cellImage.getHeight()/2);
						break;
					case RightUp:
						cellImage = tronsTrailCorners.get(playerId);
						aff.quadrantRotate(3, cellImage.getWidth()/2, cellImage.getHeight()/2);
						break;
					case LeftDown:
						cellImage = tronsTrailCorners.get(playerId);
						aff.quadrantRotate(1, cellImage.getWidth()/2, cellImage.getHeight()/2);
						break;
					case RightDown:
						cellImage = tronsTrailCorners.get(playerId);
						break;
					}
				}
			}
			break;
		}

		if (drawBG)
			drawBackGround(xpos, ypos);

		if (cellImage != null){
			drawAtCellXYCellSize(xpos, ypos, g, cellImage, aff);

		}
	}

	private int cellStartX(int cellX) {
		return cellX * cellWidth + CELLS_X_OFFSET;
	}

	private int cellStartY(int cellY) {
		return cellY * cellHeight + CELLS_Y_OFFSET;
	}

	public void drawTron(Tron t) {

//		lastTronHeadPositions.add(new Point(t.getLastX(), t.getLastY()));
		int x1, x2;
		int y1, y2;

		x1 = t.getLastX();
		x2 = x1 + 1;
		y1 = t.getLastY();
		y2 = y1 + 1;

		x1 = cellStartX(x1);
		x2 = cellStartX(x2);
		y1 = cellStartY(y1);
		y2 = cellStartY(y2);


		int angleStart = 0;

		if(playField.getGameState() == GameState.RUNNING && t.isAlive() && playField.getTurn() > 0){
			double animeRate = Math.min(1.0, (System.currentTimeMillis() - playField.getTicks()) / (double)om.getTurnTimeslot());

			switch(t.getDir()){
			case Up:
				y1 -= (int)(animeRate * cellHeight);
				y2 -= (int)(animeRate * cellHeight);
				break;
			case Down:
				y1 += (int)(animeRate * cellHeight);
				y2 += (int)(animeRate * cellHeight);
				break;
			case Left:
				x1 -= (int)(animeRate * cellWidth);
				x2 -= (int)(animeRate * cellWidth);
				break;
			case Right:
				x1 += (int)(animeRate * cellWidth);
				x2 += (int)(animeRate * cellWidth);
				break;
			}
		}

		switch(t.getDir()){
		case Up:
			angleStart = 90;
			break;
		case Down:
			angleStart = 270;
			break;
		case Left:
			angleStart = 180;
			break;
		case Right:
			angleStart = 0;
			break;
		}

		int step = (int) (aho % 10);
		if (step >= 5)
			step = 10 - step;

		int angleMove =  20 + (step * 10);


		int xBegin = cellStartX(0);
		int xEnd = cellStartX(playField.getWidth());
		int yBegin = cellStartY(0);
		int yEnd = cellStartY(playField.getHeight());
		if(x1 < xBegin || y1 < yBegin || x2 > xEnd || y2 > yEnd){
			int xWidth = xEnd - xBegin;
			int yWidth = yEnd - yBegin;

			if(!om.isTronHeadImage())
			{
				g.setColor(Color.black);
				g.drawArc(xBegin + (x1 - xBegin + xWidth) % xWidth, yBegin + (y1 - yBegin + yWidth) % yWidth, cellWidth,  cellHeight, angleStart + angleMove, 360 - (angleMove<<1));
				g.drawArc(xBegin + (x2 - xBegin) % xWidth - cellWidth, yBegin + (y2 - yBegin) % yWidth - cellWidth, cellWidth, cellHeight, angleStart + angleMove, 360 - (angleMove<<1));
				g.setColor(om.getTronColor(PlayField.playerTrail(t.getPlayerId())));
				g.fillArc(xBegin + (x1 - xBegin + xWidth) % xWidth, yBegin + (y1 - yBegin + yWidth) % yWidth, cellWidth,  cellHeight, angleStart + angleMove, 360 - (angleMove<<1));
				g.fillArc(xBegin + (x2 - xBegin) % xWidth - cellWidth, yBegin + (y2 - yBegin) % yWidth - cellWidth, cellWidth, cellHeight, angleStart + angleMove, 360 - (angleMove<<1));
				//g.fillRect(xBegin + (x1 - xBegin + xWidth) % xWidth, yBegin + (y1 - yBegin + yWidth) % yWidth, CELL_WIDTH,  CELL_HEIGHT);
				//g.fillRect(xBegin + (x2 - xBegin) % xWidth - CELL_WIDTH, yBegin + (y2 - yBegin) % yWidth - CELL_WIDTH, CELL_WIDTH, CELL_HEIGHT);
			}
		}else{
			if(om.isTronHeadImage())
			{
				AffineTransform aff = new AffineTransform();
				BufferedImage tronhead = tronsHeads.get(t.getPlayerId());
				BufferedImage dst = new BufferedImage(tronhead.getWidth(), tronhead.getHeight(), 
						BufferedImage.TYPE_INT_ARGB);
				switch(t.getDir())
				{
				case Up:
					aff.quadrantRotate(3, tronhead.getWidth() / 2, tronhead.getHeight() / 2);
					break;
				case Down:
					aff.quadrantRotate(1, tronhead.getWidth() / 2, tronhead.getHeight() / 2);
					break;
				case Right:
					break;
				case Left:
					aff.quadrantRotate(2, tronhead.getWidth() / 2, tronhead.getHeight() / 2);
					break;
				}
				AffineTransformOp ato = new AffineTransformOp(aff, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
				ato.filter(tronhead, dst);
				g.drawImage(dst, x1, y1, cellWidth, cellHeight, this);
			}
			else
			{
				g.setColor(Color.black);
				g.drawArc(x1, y1, cellWidth,    cellHeight,
						angleStart + angleMove, 360 - (angleMove<<1));
				g.setColor(om.getTronColor(PlayField.playerTrail(t.getPlayerId())));
				g.fillArc(x1, y1, cellWidth,    cellHeight,
						angleStart + angleMove, 360 - (angleMove<<1));
				//g.fillRect(x1, y1, CELL_WIDTH, CELL_HEIGHT);
			}
		}

	}


	public void drawScore() {
		g.setColor (om.getBoardBgColor());
		g.fillRect (0, 0, this.getSize().width, CELLS_Y_OFFSET);
		g.fillRect (0, 0, CELLS_X_OFFSET, this.getSize().height);

		g.setFont(new Font("Monospaced", Font.BOLD, BOARD_LINE_HEIGHT - BOARD_LINE_MARGIN));
		Graphics2D g2 = (Graphics2D)g;
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		Color counterColor = om.getCounterColor();
		g.setColor(counterColor);
		g.drawString(String.format("Round:%2d Turn:%4d (%4d)", playField.getRound(), playField.getTurn(), om.getMaxTurns()), BOARD_X_OFFSET, BOARD_Y_OFFSET + BOARD_LINE_HEIGHT - (BOARD_LINE_MARGIN>>1));

		Color scoreColor = om.getScoreColor();
		g.setColor(scoreColor);
		for(int i = 0; i < playField.getNumOfPlayers(); i++){
			int l = i + 2;
			g.drawString("Player " + i + ": " + playField.getScore(i).toString() + " (" + playField.getTotalScore(i).toString() + ")", BOARD_X_OFFSET, BOARD_Y_OFFSET + l * BOARD_LINE_HEIGHT - (BOARD_LINE_MARGIN>>1));
		}

		Color commentColor = om.getCommentColor();
		g.setColor(commentColor);
		synchronized(playField.getComments()){
			int i = playField.getNumOfPlayers() + 2;
			for(String comment: playField.getComments()){
				g.drawString(comment, BOARD_X_OFFSET, BOARD_Y_OFFSET + i * BOARD_LINE_HEIGHT - (BOARD_LINE_MARGIN>>1));
				i++;
			}
		}
	}

	public int getMaxNumOfComments(){
		return (getDimension().height - 2 * BOARD_Y_OFFSET) / BOARD_LINE_HEIGHT - 1 - this.playField.getNumOfPlayers();
	}

	public Dimension getDimension() {
		return new Dimension(this.cellWidth * playField.getWidth() + CELLS_X_OFFSET,
				this.cellHeight * playField.getHeight() + CELLS_Y_OFFSET);
	}
}

