package Cluedo.GUI2;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.ImageIcon;
import javax.swing.JComponent;

import Cluedo.API.CluedoConfig;
import Cluedo.API.CluedoListener;
import Cluedo.API.LanguagePack;
import Cluedo.API.MapArea;
import Cluedo.API.Message;
import Cluedo.API.SoundEngine;
import Cluedo.Controller.GameController;
import Cluedo.Game.Player;
import Cluedo.Game.Position;

/**
 * Board GUI Component extends JComponent to abstract the notion of the board and image.<p>
 * - It validates clicks and then sends movePlayer alerts to the GameController<p>
 * - It generates a sound effect for paths which you may not reach.<p>
 * - It draws a path in an alpha transpareny level for the path of your last move<p>
 *
 */
public class Board extends JComponent
implements MouseListener  {

	static int paintsCalled = 0;
	
	boolean paintInProgress = false;
	
	/**
	 * Scale used to draw all of board.
	 */
	double scale = 1.0;
	
	/**
	 * Used in debugging
	 */
	final boolean paintPlayerDebug = false;
	
	/**
	 * Quick testing hack to players in set positions!!
	 */
	boolean positionsSet = true; 
	
	
	/**
	 * Board graphic as ImageIcon
	 */
	ImageIcon staticBoardImage = null;
	/**
	 * Board graphic as Image
	 */
	Image boardImage = null;
	/**
	 * Dimension of board graphic
	 */
	Dimension dimensions = null;
	/**
	 * List of defined areas
	 */
	ArrayList codes = new ArrayList();
	
	/**
	 * Last x
	 */
	int x = 0;
	/**
	 * Last y
	 */
	int y = 0;
	
	/**
	 * Width to draw counter objects
	 */
	int pieceWidth = 20;
	/**
	 * Height to draw counter objects
	 */
	int pieceHeight = 20;
	
	/**
	 * Current transparency level
	 */
	float alpha=(float)0.45;
	
	
	/**
	 * Pointer to game's config
	 */
	CluedoConfig gameConfig = null;
	
	/**
	 * Pointer to gameController!!
	 */
	GameController gameController = null;
	
	/**
	 * If we're drawing a path we don't want to accept clicks.
	 */
	boolean moveAnimationInProgress = false;
	
	/**
	 * The path to draw -- the animation of the last move.
	 */
	Vector movePath = new Vector();
	/**
	 * Add the nodes data (loaded from another class)
	 */
	
	/**
	 * Unused
	 */
	Player currentPlayer;
	
	/**
	 * Unused 
	 */
	MapArea lastPosition = null;
	
	/**
	 * When gameOver == true, it will be written across the screen.
	 */
	private boolean gameOver = false;
	
	Hashtable mapNodes = new Hashtable();
	
	/**
	 * Contains image map and players for better performance 
	 */
	BufferedImage buffer = null;
	
	/**
	 * List of all valid destinations from the player's current position
	 */
	Vector routes = new Vector();
	
	/**
	 * Colour to show the possible destination squares
	 */
	Color positionsOverlay = new Color(255,255,255,110);
	
	
	private CluedoListener cluedoListen = new CluedoListener() {
		public void notifyAlert(Message m) {
			
			if(m.type.equals("nextPlayer")) {
				currentPlayer = gameController.getBoard().getCurrentPlayer();
				movePath = new Vector();
				routes = new Vector();
				updateBuffer();
				repaint();
				
			}
			else if(m.type.equals("moveAlert")) {
				//	System.out.println("moveAlert " + m +" "+ m.parameters.size());
				if(m.parameters.size() == 2) {
					Vector newPath = (Vector)m.parameters.elementAt(1);
					if(newPath.size() >= 1) {
						movePath = newPath;
						moveAnimationInProgress=true;
						
					}
					else {
						movePath = new Vector();
					}
					routes = new Vector();
					updateBuffer();
					repaint();
				}
			}
			else if(m.type.equals("commenceGame")) {
				updateBuffer();
				repaint();
			}
			else if(m.type.equals("newRoll")) {
				routes = findAllRoutes();
				repaint();
			}
			else if(m.type.equals("winGame")) {
				gameOver = true;
				updateBuffer();
				repaint();
			}
		}
	};
	
	
	
	/**
	 * Listen for mouse-clicks and then look-up which position was clicked.
	 *
	 */
	public void mouseClicked (MouseEvent e) {
		
		boolean AIgo = gameController.getBoard().getCurrentPlayer().getPlayerType() >= 1;
		boolean moveOk = true;
		
		if(codes == null)
			return;
		
		System.out.println("Click\t\t[X:"+ e.getX() + "] [Y:" + e.getY()+"]\t\t(" +codes.size()+ ") shapes");
		
		double x = e.getX() / scale;
		double y = e.getY() / scale;
		
		
		// When not in animation-mode, draw the board as normal.
		// Also never allow clicks during the AI's play
		if(moveAnimationInProgress == false && AIgo == false) {
			
			// Check if the player is allowed to make a move.
			if(gameController.getBoard().getCurrentPlayer().hasMoved() == false && gameController.getBoard().getCurrentPlayer().hasRolled() == true) {
				
				// Find out where the player wants to move to.
				MapArea clickedArea = null;
				
				// Try to find out which area was clicked
				clickedArea = findMapArea((int)x, (int)y);
				
				// If the polygon was found
				if(clickedArea != null) {
					System.out.println("[Board:click] Hit\t\t[" + clickedArea.getID() + "]");
					
					// Relate the GUI model's node to the Game model's node.
					Cluedo.Game.Position destinationNode = gameController.getBoard().map.findByID( clickedArea.getID() );
					
					// Make sure the node was found
					if(destinationNode == null) {
						System.out.println("["+this+"mouseClicked] unable to locate corresponding node.");
					}
					else {
						
						// Some initial checks
						
						if(destinationNode == gameController.getBoard().getCurrentPlayer().getPosition())
							moveOk = false;
						if(destinationNode instanceof Cluedo.Game.Square) {
							if(destinationNode.isOccupied() == false)
								moveOk = true;
							else
								moveOk = false;
						}
						
						
						if(moveOk == true) {
							
							// Make a check to the map class -- can the player reach the destination with (x) moves ?
							Vector newPath = canReach(destinationNode, gameController.getBoard().getCurrentPlayer());
							
							if(newPath.size() >= 1) {
								movePlayer(gameController.getBoard().getCurrentPlayer(), destinationNode, newPath);
								moveAnimationInProgress = true;
								//updateBuffer();									
								
							}
							else {
								// When a player clicks in a bad location, sound a message
								try {
									SoundEngine.bufferSound("move.bad");
								}
								catch(Exception ee) {
									System.out.println("Can't load sound engine..");
								}
							}
						}
					}
				}
			}
		}
		
		//updateBuffer();	
	}
	
	
	
	/**
	 * Pass in an arraylist of codes
	 *  
	 */
	public void setLoadedMapAreas(ArrayList codes) {
		this.codes = codes;
		
		/**
		 * Convert data into a hashtable for O(1) access.
		 */
		mapNodes = new Hashtable();
		MapArea mapArea = null;
		
		for (int i = 0; i < codes.size(); i++) {
			mapArea = (MapArea) codes.get(i);
			
			mapNodes.put(mapArea.getID(), mapArea);
		}
	}
	
	/**
	 * Resets the movePath so we don't draw/animate it anymore
	 *  
	 */
	public void moveCompleted() {
		moveAnimationInProgress = false;
		movePath = null;
	}
	
	
	/**
	 * Validates whether a player can reach a position or not
	 * 
	 * @param destination
	 * @param who
	 * @return true if possible, otherwise false
	 */
	private Vector canReach(Cluedo.Game.Position destination, Player who) {
		Vector movePath = new Vector();
		
		/** Whether 6 was rolled and player wants to go into any room.  */
		boolean specialMoveNotApplicable = false;
		
		if(gameController.getBoard().getGameType() == 1) {
			System.out.println("Player got 12 and wants to go to room" );
			if(destination instanceof Cluedo.Game.Room && who.getMovesRemaining() == 12) {
				movePath.add(destination);
				specialMoveNotApplicable = true;
			}
			
		}
		if(specialMoveNotApplicable == false || gameController.getBoard().getGameType() == 0) {
			
			movePath = gameController.getBoard().map.routeTo(currentPlayer.getPosition(), destination);
			
			if(movePath.size() == 0 || movePath.size() > who.getMovesRemaining()+1) {
				movePath = new Vector();
			}
		}
		
		return movePath;
	}

	/**
	 * Wrapper funtion for directCommand to movePlayer
	 * 
	 * @param currentPlayer
	 * @param destinationNode
	 */
	private synchronized void movePlayer(Player currentPlayer, Cluedo.Game.Position destinationNode, Vector path) {
		Vector parameters = new Vector();
		
		parameters.add(destinationNode);
		parameters.add(currentPlayer);
		parameters.add(path);
		
		gameController.directCommand("movePlayer", parameters);
	}
	
	/**
	 * 
	 */
	synchronized void updateBuffer() {
		System.out.println("[GUI2.Board:updateBuffer] updating");
		long start = System.currentTimeMillis();
		
		BufferedImage newBuffer = null;

			paintInProgress = true;
			
			try {
				newBuffer = new BufferedImage(staticBoardImage.getIconWidth(), staticBoardImage.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
				
				Graphics g = newBuffer.getGraphics();
				Graphics2D g2 = (Graphics2D)g;
				
				g2.scale(scale, scale);
				
				// The background image
				staticBoardImage.paintIcon( this, g, 0, 0 );
				
				g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				
				g2.setComposite(makeComposite(alpha));
				//System.out.println("movePath: " + movePath);
				if(moveAnimationInProgress == true && movePath != null && movePath.size() > 0) {
					//System.out.println("paintPath(g2)");
					paintPath(g2);
				}
				
				paintPlayers(g2);
				
				
				if(gameOver == true) {
					g2.setColor(Color.red);
					g2.setComposite(makeComposite((float)0.2));
					g2.fillRect(0,0, staticBoardImage.getIconWidth(), staticBoardImage.getIconHeight());
					g2.setColor(Color.black);
					drawGameOverMessage(g2);
				}
				
				// Reset alpha level
				g2.setComposite(makeComposite(1));
				int wait = 0;

				while(paintsCalled > 0) {
					wait++;
				}
				System.out.println("[GUI2.Board:updateBuffer] synchronized(buffer) on " + wait);
				synchronized(buffer) {
					buffer = newBuffer;
				}
				System.out.println("[GUI2.Board:updateBuffer] synchronized(buffer) off");
			}
			catch(Exception e) {
				e.printStackTrace();
			}

			paintInProgress = false;
	//	}
		
		long end = System.currentTimeMillis();
		
		System.out.println("[GUI2.Board] Finished updating buffer " + (end - start) + " (ms)");
	}
	
	
	/**
	 * @return
	 */
	private Vector findAllRoutes() {
		Vector routes = new Vector();
		Cluedo.Game.Board b = gameController.getBoard();
		int moves = b.getCurrentPlayer().getMovesRemaining() + 1;
		Position src = b.getCurrentPlayer().getPosition();
		
		for(int i=0;i<codes.size();i++) {
			Position dest = b.map.findByID(((MapArea)codes.get(i)).getID());
			if(dest == null) {
				System.err.println("Map error in node = '" + ((MapArea)codes.get(i)).getID() + "' type = '" + codes.get(i).getClass().getName() + "', ref=" + codes.get(i).toString() );
			}
			else
				if( canReach(dest, b.getCurrentPlayer()).size() > 0) {
					routes.add(codes.get(i));                
				}
		}

		return routes;
	}
	
	
	
	/**
	 * Override the paint method in order to perform "custom" actions on updates
	 *
	 */
	public void paint( Graphics g ) {
		Graphics2D g2 = (Graphics2D)g;
		
		/*System.out.println("paint && Thread = "+Thread.currentThread().getName() +Thread.currentThread().getClass() );
		
		if(paintInProgress == true) {
			System.out.println("\t\t UPDATE IN PROGRESS, PAINT REQUESTED");
		}*/
		

		//System.out.println("\t\t Concurrent paint requests " + (++paintsCalled));
		/*
		int timeout = 100;
		while(paintInProgress == true && timeout >= 0) {
			try {
				Thread.sleep(100);
				
			}
			catch(Exception e) {
				e.printStackTrace();
			}
			timeout-=10;
			System.out.println("\ttimeout: "+timeout);
		}
		System.out.println("timeout: "+timeout);
*/
		
		synchronized(buffer) {
			/**
			 * Paint the buffered image containing the players, paths and the board
			 */
			g2.drawImage(buffer, 0, 0, null);
		}
		
		if(routes.size() > 0) {
			g2.setColor(positionsOverlay);
			for(int i =0;i<routes.size();i++) {
				g2.fill(((MapArea)routes.get(i)).getShape());
			}
			
		}

		//System.out.println("Stopped painting here");
		paintsCalled--;
	}
	
	/**
	 * Draws the "GameOver" message relatively in the centre of the screen
	 * 
	 * @param g 
	 * Takes a <b>Graphics2D</b> object as its argument.
	 * 
	 */
	private synchronized void drawGameOverMessage(Graphics2D g2) {
		g2.setComposite(makeComposite((float)0.7));
		g2.setColor(Color.black);
		g2.setFont( new Font( "Arial", Font.BOLD, 65 ));
		g2.drawString(LanguagePack.getString("game_over", "cluedo"), 200, 250);
	}
	
	/**
	 * Takes a <b>Graphics2D</b> object as its argument then paints out the path taken
	 * by the last moved player (who moved from choice, not from a disprove).
	 * The route is calculated from the <b>Cluedo.Game.Map</b> class, each node is looked up
	 * by its ID with <b>findByID</b> and then a transparency layer is drawn over it.
	 *
	 */
	private synchronized void paintPath(Graphics2D g2) {
		try {
			Position position = null;
			MapArea shape = null;
			g2.setColor(Color.white);
			
			for(int i = 0; i < movePath.size(); i++) {
				position = (Position)movePath.elementAt(i);
				shape = findByID(position.getID());
				if(shape == null)
				{
					System.out.println("[Board:paintPath] Error, polygon=" + shape + " [position=" + position + "ID=" + position.getID() + "]");
					SoundEngine.bufferSound("board.bug");
					
					break;
				}
				
				g2.fill(shape.getShape());
			}
			
			g2.setColor(Color.black);
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Paints all the players onto the board.
	 * @param g2
	 */
	private synchronized void paintPlayers(Graphics2D g2) {
		try {
			Vector players = gameController.getBoard().getPlayers();
			Player player = null;
			Cluedo.Game.Position gamePosition = null;
			MapArea guiPosition = null;
			
			Shape shap = null;
			Rectangle rect = null;
			
			boolean playersInRooms = false;
			
			for(int i = 0; i < players.size(); i++) {
				player = (Player)players.elementAt(i);
				gamePosition = player.getPosition();
				
				
				// We just paint players who aren't in rooms in the top corner of the shape
				if(gamePosition instanceof Cluedo.Game.Room == false) {
					
					guiPosition = lookupPosition(gamePosition);
					//System.out.println( "Paint: " + player.getRealName() + " at " + gamePosition.getID() + (guiPosition== null));
					if(guiPosition != null) {
						shap = guiPosition.getShape();
						rect = shap.getBounds();
						
						int height = rect.height/2;// + rect.height%2;
						int width = rect.width/2;// + rect.width%2;
						
						int x = height + rect.x;
						
						int y = width + rect.y;
						
						x = x - pieceHeight/2;// + pieceHeight%2 ;
						y = y - pieceWidth/2;// + pieceHeight%2;
						
						
						if(paintPlayerDebug == true) {
							System.out.println(player.getRealName());
							System.out.println("STD( " + (int)guiPosition.getPoints()[0][0] + "x"+ (int)guiPosition.getPoints()[0][1] + " )");
							
							System.out.println("FAIR( " + x + "x" +y + " )");
							System.out.println("height("+rect.height  +"x" + rect.width+")\n Dim ( " + rect.x  +"x" + rect.y+")\n");
						}
						
						// Fair                	
						paintPlayerAt(x, y, player, g2); // in top corner
						// Not fair
						//    				paintPlayerAt((int)guiPosition.getPoints()[0][0], (int)guiPosition.getPoints()[0][1], player, g2); // in top corner
					}
				}
				else
					playersInRooms = true;
			}
			
			if(playersInRooms == true) {
				paintObjectsInRooms(g2);
			}
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Paints any players who are in rooms, called from paintPlayers
	 * @param g2
	 */
	private synchronized void paintObjectsInRooms(Graphics2D g2) {
		try {
			
			Vector roomNodes = gameController.getBoard().map.getNodes("rooms");
			Cluedo.Game.Room room = null;
			ArrayList playerPoints = null;
			MapArea guiNode = null;
			
			/** If painting too early */  
			if(mapNodes.size() == 0)
				return;
			
			for(int j = 0; j < roomNodes.size(); j++) {
				room = (Cluedo.Game.Room)roomNodes.elementAt(j);
				
				// We have visitors, let's paint them !
				if(room.isOccupied() == true) {
					guiNode = findByID(room.getID());
					//System.out.println("GUI node to paint: " + room.getID() + " " + guiNode);
					
					if(guiNode.getPiecePoints() != null) {
						playerPoints = guiNode.getPiecePoints();
						
						//		System.out.println("GUI node to paint: " + guiNode.getID());
						
						Vector presentPlayers = room.getPresent();
						Player who = null;
						// Paint everyone present
						for(int k = 0; k < presentPlayers.size(); k++) {
							who = (Cluedo.Game.Player)presentPlayers.get(k);
							
							// (SAFETY) If there aren't enough defined points for everyone just break
							if(k >= playerPoints.size()) {
								System.out.println("[GUI2.Board] Can't fit all players here");
								break;
							}
							
							paintPlayerAt(
									((Point)playerPoints.get(k)).x,
									((Point)playerPoints.get(k)).y,
									who, g2);
						}
					}
					
				}
			}
		}
		catch(Exception e ){
			e.printStackTrace();
		}
	}
	
	/**
	 * Wrapper function to paint a player at a co-ordinate.
	 * 
	 * @param x
	 * @param y
	 * @param player
	 * @param g2
	 */
	private synchronized void paintPlayerAt(int x, int y, Player player, Graphics2D g2 ) {
		try {
			g2.setColor( Toolbar.getColourByName(player.getCharacter()) );
			g2.translate( x, y);
			g2.fillOval( 0, 0, pieceWidth, pieceHeight );
			g2.setColor( Color.white );
			g2.drawOval( 1, 1, pieceWidth - 2, pieceHeight - 2 );
			g2.setColor( Color.black );
			g2.drawOval( 2, 2, pieceWidth - 4, pieceHeight - 4 );
			
			// reset position on graphics map
			g2.translate( -x, -y);
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Lookup the corresponding MapArea object for a Position from a text ID.
	 *
	 * @param ID The text ID for the desired MapArea
	 *
	 * @return The corresponding MapArea or null.
	 *
	 */
	private MapArea findByID(String ID) {
		MapArea mapArea = (MapArea)mapNodes.get(ID);
		return mapArea; 	
	}
	
	/**
	 * Lookup the corresponding MapArea object for a Position from the game model.
	 * @param position The position object, ie where the player is.
	 * @return The corresponding MapArea or null.
	 *
	 */
	private MapArea lookupPosition(Cluedo.Game.Position position) {
		MapArea mapArea = (MapArea)mapNodes.get(position.getID());
		return mapArea;
	}
	
	
	/**
	 * Find a MapArea by x, y co-ordinates
	 *
	 * @param x x pos of mouse
	 * @param y y pos of mouse
	 * @return the map area requested or null
	 */    
	private MapArea findMapArea(int x, int y) {
		MapArea area = null;
		if(codes != null) {
			for(int i = 0; i < codes.size(); i++)
				if( (area = ((MapArea)codes.get(i))).hit(x, y) == true)
					break;
				
			if(area.hit(x, y) == false)
				area = null;
		}
		return area;
	}
	
	/**
	 * Will supply the room/square to the parent of which the mouse is currently in.
	 * @param x x pos of mouse
	 * @param y y pos of mouse
	 */
	public String getFloatingPosition(int x, int y) {
		MapArea mousePoint = findMapArea(x, y);
		
		if(mousePoint != null)
			return mousePoint.getID();
		else
			return "(None)";
		
	}
	
	/**
	 * For the G2 object, creates an alpha transparency level for drawing the path and pieces slightly transparent.
	 *
	 */
	private AlphaComposite makeComposite(float alpha) {
		int type = AlphaComposite.SRC_OVER;
		return(AlphaComposite.getInstance(type, alpha));
	}
	
	
	/**
	 * Pass the name of the image you want to load.
	 *
	 */
	public Board( String fileName,  CluedoConfig gameConfig, GameController gameController ) {
		super();
		
		gameController.bindListener(cluedoListen);
		
		
		this.gameConfig = gameConfig;
		this.gameController = gameController;
		
		
		// Load image as ImageIcon
		//		System.out.println(" " + fileName);
		staticBoardImage = new ImageIcon( (new java.io.File(fileName)).getPath() );
		
		// Cast to ImageIcon to Image to get some dimensions data from it
		boardImage = staticBoardImage.getImage();
		
		
		
		// Make the board doesn't grow / shrink
		dimensions = new Dimension( staticBoardImage.getIconWidth(), staticBoardImage.getIconHeight() );
		setMaximumSize( dimensions );
		setMaximumSize( dimensions ); setMinimumSize( dimensions ); setPreferredSize( dimensions );
		
		// Handle mouse events in this class
		addMouseListener( this );
		//addMouseMotionListener( moveAdapter );
		
		currentPlayer = gameController.getBoard().getCurrentPlayer();
		buffer = new BufferedImage(staticBoardImage.getIconWidth(), staticBoardImage.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
		updateBuffer();
		
	}

	public void mouseEntered(MouseEvent arg0) {			
	}
	
	public void mouseExited(MouseEvent arg0) {
		
	}
	
	public void mousePressed(MouseEvent arg0) {
	}
	
	
	public void mouseReleased(MouseEvent arg0) {
	}
	
}
