/**
 * Displays the game field consisting of a grid of dots and lines.  Forwards
 * mouse events to state machine.
 * 
 * Assignment: MP3
 * Class: CS 340, Fall 2005
 * TA: Nitin Jindal
 * System: jdk-1.5.0.4 and Eclipse 3.1 on Windows XP
 * @author Michael Leonhard (CS account mleonhar)
 * @version 12 Oct 2005
 */

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.Random;
import java.util.Vector;
import javax.swing.JComponent;
import javax.swing.event.MouseInputListener;

public class Field extends JComponent implements MouseInputListener {
	// version number of this class, used for serialization
	private static final long serialVersionUID = 1L;
	// game state object
	private State state;
	// number of columns of boxes
	int cols;
	// number of rows of boxes
	int rows;
	// origin of the box field
	private Point origin;
	// the length of each box side
	private int boxL;
	// size of a dots
	private int dotSize;
	// thickness of lines;
	private int lineThickness;
	// arrays of vertical and horizontal lines
	// format is line[VERTICAL][col][row]
	private Line[][][] line;
	// array of Box objects, box[col][row]
	private Box[][] box;
	// Pseudo-random number generator
	Random prng;

	// color of the field background
	private Color backgroundColor;
	// color of dots
	private Color dotColor;
	// color of X symbol (computer completed square)
	private Color xColor;
	// color of O symbol (user completed square)
	private Color oColor;

	/**
	 * Constructor, prepares object for rendering
	 * 
	 * @param cols number of columns in field
	 * @param rows number of rows in field
	 * @param computerCounter counter for computer's score
	 * @param userCounter counter for user's score
	 */
	public Field(int numCols, int numRows, CounterLabel userCount,
			CounterLabel computerCount) {
		// allow the super-class to initialize (JComponent)
		super();

		// save data
		this.cols = numCols;
		this.rows = numRows;

		// allocate origin data
		this.origin = new Point();

		// create colors
		this.backgroundColor = new Color(0xeeeeee);
		this.dotColor = Color.BLACK;
		this.xColor = new Color(0xa25f5f);
		this.oColor = new Color(0x72BD89);;

		// initialize the pseudo random number generator
		this.prng = new Random();

		// make state object
		this.state = new State(this, userCount, computerCount);

		// make array to hold lines
		this.line = new Line[2][this.cols + 1][this.rows + 1];

		// make Line objects
		for (int row = 0; row <= this.rows; row++) {
			for (int col = 0; col <= this.cols; col++) {
				// not lowest row, so make vertical line
				if (row != this.rows) this.line[Line.VERTICAL][col][row] = new Line(
						this, Line.VERTICAL, col, row);
				// not rightmost column, so make horizontal line
				if (col != this.cols) this.line[Line.HORIZONTAL][col][row] = new Line(
						this, Line.HORIZONTAL, col, row);
			}
		}

		// make array to hold Box objects
		this.box = new Box[this.cols][this.rows];

		// make Box objects
		for (int row = 0; row < this.rows; row++) {
			for (int col = 0; col < this.cols; col++) {
				// box sides
				Line northLine = this.line[Line.HORIZONTAL][col][row];
				Line southLine = this.line[Line.HORIZONTAL][col][row + 1];
				Line eastLine = this.line[Line.VERTICAL][col + 1][row];
				Line westLine = this.line[Line.VERTICAL][col][row];
				// make the box
				this.box[col][row] = new Box(this, col, row, northLine,
						southLine, eastLine, westLine);
			}
		}

		// listen to own mouse input
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
	}

	/**
	 * Returns true if this component is completely opaque.
	 * 
	 * @return true because this widget draws its own background
	 */
	public boolean isOpaque() {
		return true;
	}

	/**
	 * Calculates scaling and position of field, requests repaint.
	 * 
	 * @param newWidth the new width of the component
	 * @param newHeight the new height of the component
	 */
	private void newSize(int newWidth, int newHeight) {
		// box side lengths
		double sideX = (newWidth - 2.0) / (this.cols + 0.4);
		double sideY = (newHeight - 2.0) / (this.rows + 0.4);
		// choose the smaller of the two
		double side = sideX;
		if (sideY < sideX) side = sideY;
		// center of the widget
		int centerX = newWidth / 2;
		int centerY = newHeight / 2;
		// the length of a box side (must be >0)
		this.boxL = (int) side;
		if (this.boxL < 1) this.boxL = 1;
		// calculate the dimensions of the field area
		int fieldWidth = this.boxL * this.cols;
		int fieldHeight = this.boxL * this.rows;
		// the origin of the field
		this.origin.x = (int)centerX - (fieldWidth / 2);
		this.origin.y = (int)centerY - (fieldHeight / 2);

		// dot size
		this.dotSize = (int) (side / 10);
		if (this.dotSize == 0) this.dotSize = 1;
		// line thickness
		this.lineThickness = (int) (side / 20);
		if (this.lineThickness == 0) this.lineThickness = 1;

		// request a repaint
		this.repaint();
	}

	/**
	 * Invoked by Swing to draw the component.
	 * 
	 * @param g the Graphics context in which to paint
	 */
	public void paint(Graphics g) {
		// TODO paint only inside clip rectangle

		// draw background
		Rectangle bounds = this.getBounds();
		g.setColor(this.backgroundColor);
		g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);

		// DRAW X and O in COMPLETED BOXES
		int off = this.lineThickness * 2;
		int off2 = off * 2;
		// iterate over rows and columns of boxes
		for (int row = 0; row < this.rows; row++) {
			for (int col = 0; col < this.cols; col++) {
				// box is completed
				if (this.box[col][row].isCompleted()) {
					int y = this.origin.y + row * boxL;
					int x = this.origin.x + col * boxL;
					// user made this box, so draw an O
					if (this.box[col][row].isUserCompleted()) {
						g.setColor(this.oColor);
						g.fillOval(x + off, y + off, this.boxL - off2,
								this.boxL - off2);
						g.setColor(this.backgroundColor);
						g.fillOval(x + off2, y + off2, this.boxL - off2 * 2,
								this.boxL - off2 * 2);
					}
					// computer made it, so drawn an X
					else {
						g.setColor(this.xColor);
						g.fillOval(x + this.lineThickness, y
								+ this.lineThickness, this.boxL
								- this.lineThickness * 2, this.boxL
								- this.lineThickness * 2);
						g.setColor(this.backgroundColor);
						// east
						g.fillArc(x + off2, y + off, this.boxL - off2,
								this.boxL - off2, -45, 90);
						// west
						g.fillArc(x, y + off, this.boxL - off2, this.boxL
								- off2, 135, 90);
						// north
						g.fillArc(x + off, y, this.boxL - off2, this.boxL
								- off2, 45, 90);
						// south
						g.fillArc(x + off, y + off2, this.boxL - off2,
								this.boxL - off2, -135, 90);
					}
				}
			}
		}

		// DRAW LINES
		// half the thickness of a line
		int halfThick = this.lineThickness / 2;
		// iterate over lines (two for every dot, except lower right dot)
		for (int row = 0; row <= this.rows; row++) {
			int y = this.origin.y + row * boxL;
			for (int col = 0; col <= this.cols; col++) {
				int x = this.origin.x + col * boxL;
				// not rightmost column, so draw horizontal line
				if (col != this.cols) {
					// line objects
					Line hor = this.line[Line.HORIZONTAL][col][row];
					// horizontal line is visible
					if (hor.isVisible()) {
						g.setColor(hor.getColor());
						g.fillRect(x, y - halfThick, boxL, this.lineThickness);
					}
				}
				// not lowest row, so draw vertical line
				if (row != this.rows) {
					Line ver = this.line[Line.VERTICAL][col][row];
					// vertical line is visible
					if (ver.isVisible()) {
						g.setColor(ver.getColor());
						g.fillRect(x - halfThick, y, this.lineThickness, boxL);
					}
				}
			}
		}

		// DRAW DOTS
		g.setColor(this.dotColor);
		// half the dot size
		int halfDot = this.dotSize / 2;
		// iterate over rows of dots (not boxes!)
		for (int row = 0; row <= this.rows; row++) {
			int y = this.origin.y + row * boxL;
			// iterate over columns of dots (not boxes!)
			for (int col = 0; col <= this.cols; col++) {
				int x = this.origin.x + col * boxL;
				// draw dot
				g.fillOval(x - halfDot, y - halfDot, dotSize, dotSize);
			}
		}
	}

	/**
	 * Resize this component to the supplied dimensions
	 * 
	 * @param d the new size of this component
	 */
	public void setSize(Dimension d) {
		// System.out.println("Field.setSize: " + d);
		// let the super-class handle this
		super.setSize(d);
		// get ready to render as the new size
		this.newSize(d.width, d.height);
	}

	/**
	 * Resize this component to the specified width and height
	 * 
	 * @param width the new width of this component in pixels
	 * @param height the new height of this component in pixels
	 */
	public void setSize(int width, int height) {
		// System.out.println("Field.setSize: width=" + width + " height="
		// + height);
		// let the super-class handle this
		super.setSize(width, height);
		// get ready to render as the new size
		this.newSize(width, height);
	}

	/**
	 * Moves and resizes this component. The new location of the top-left corner
	 * is specified by x and y, and the new size is specified by width and
	 * height.
	 * 
	 * @param x the new x-coordinate of this component
	 * @param y the new y-coordinate of this component
	 * @param width the new width of this component
	 * @param height the new height of this component
	 */
	public void setBounds(int x, int y, int width, int height) {
		// System.out.println("Field.setBounds: x=" + x + " y=" + y + " width="
		// + width + " height=" + height);
		// let the super-class handle this
		super.setBounds(x, y, width, height);
		// get ready to render as the new size
		this.newSize(width, height);
	}

	/**
	 * Gets the line that is nearest the given point
	 * 
	 * @param x the x-coordinate of the point
	 * @param y the y-coordinate of the point
	 * @return the line that is nearest to the point
	 */
	private Line getNearestLine(int x, int y) {
		// find the mouse position relative to the field origin
		x -= origin.x;
		y -= origin.y;
		// mouse is over the box at this row and column
		int col = x / boxL;
		int row = y / boxL;

		// clamp point into field
		if (col < 0) col = 0;
		if (col >= this.cols) col = this.cols - 1;
		if (row < 0) row = 0;
		if (row >= this.rows) row = this.rows - 1;

		// the nearest box
		Box nearestBox = this.box[col][row];

		// find mouse position relative to the box's origin
		x -= boxL * col;
		y -= boxL * row;

		// get the Line from the Box objectt
		return nearestBox.getNearestLine(x, y, boxL);
	}

	/**
	 * Invoked when the mouse button has been clicked (pressed and released) on
	 * a component. Does nothing.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseClicked(MouseEvent e) {
		// this event is ignored
	}

	/**
	 * Invoked when a mouse button has been pressed on a component. Passes event
	 * to state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mousePressed(MouseEvent e) {
		// lookup line nearest to the mouse pointer
		Line nearestLine = getNearestLine(e.getX(), e.getY());
		// pass the event to state machine
		this.state.mousePressed(nearestLine);
	}

	/**
	 * Invoked when a mouse button has been released on a component. Passes
	 * event to state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseReleased(MouseEvent e) {
		// lookup line nearest to the mouse pointer
		Line nearestLine = getNearestLine(e.getX(), e.getY());
		// pass the event to state machine
		this.state.mouseReleased(nearestLine);
	}

	/**
	 * Invoked when the mouse enters a component. Passes event to state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseEntered(MouseEvent e) {
		// lookup line nearest to the mouse pointer
		Line nearestLine = getNearestLine(e.getX(), e.getY());
		// pass the event to state machine
		this.state.mouseMoved(nearestLine);
	}

	/**
	 * Invoked when the mouse exits a component. Informs state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseExited(MouseEvent e) {
		// inform state machine that mouse is not near any line
		this.state.mouseMoved(null);
	}

	/**
	 * Invoked when a mouse button is pressed on a component and then dragged.
	 * Forwards movement event to state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseDragged(MouseEvent e) {
		// lookup line nearest to the mouse pointer
		Line nearestLine = getNearestLine(e.getX(), e.getY());
		// pass the event to state machine
		this.state.mouseMoved(nearestLine);
	}

	/**
	 * Invoked when the mouse cursor has been moved onto a component but no
	 * buttons have been pushed. Forwards event to state machine.
	 * 
	 * @param e information about the mouse
	 */
	public void mouseMoved(MouseEvent e) {
		// lookup line nearest to the mouse pointer
		Line nearestLine = getNearestLine(e.getX(), e.getY());
		// pass the event to state machine
		this.state.mouseMoved(nearestLine);
	}

	/**
	 * Initiates a repaint for the area occupied by the specified line
	 * 
	 * @param orientation the line is Line.HORIZONTAL or Line.VERTICAL
	 * @param col the column of the line
	 * @param row the row of the line
	 */
	public void repaintLine(int orientation, int col, int row) {
		// half the thickness of a line
		int halfThick = this.lineThickness / 2;
		// coordinates of box origin
		int x = this.origin.x + col * boxL;
		int y = this.origin.y + row * boxL;
		// line is horizontal, request repainting of its rectangle
		if (orientation == Line.HORIZONTAL) this.repaint(x, y - halfThick,
				boxL, this.lineThickness);
		// line is vertical, request repainting of its rectangle
		else this.repaint(x - halfThick, y, this.lineThickness, boxL);
	}

	/**
	 * Initiates a repaint for the area occupied by the specified box
	 * 
	 * @param col the column of the box
	 * @param row the row of the box
	 */
	public void repaintBox(int col, int row) {
		// half the thickness of a line
		int halfThick = this.lineThickness / 2;
		// coordinates of box origin
		int x = this.origin.x + col * this.boxL;
		int y = this.origin.y + row * this.boxL;
		// request a repaint of the rectangular region
		this.repaint(x - halfThick, y - halfThick, this.boxL
				+ this.lineThickness, this.boxL + this.lineThickness);
	}

	/**
	 * Checks if any line is undrawn
	 * 
	 * @return true if there is an undrawn line, otherwise false
	 */
	public boolean hasUndrawnLine() {
		// iterate over lines
		for (int row = 0; row <= this.rows; row++) {
			for (int col = 0; col <= this.cols; col++) {
				// not rightmost column, so check horizontal line
				if (col != this.cols) {
					// horizontal line is not drawn
					if (!this.line[Line.HORIZONTAL][col][row].isDrawn()) {
						// System.out.println("Field.hasUndrawnLine() "
						// + this.line[Line.HORIZONTAL][col][row]
						// + " is undrawn");
						return true;
					}
				}
				// not lowest row, so check vertical line
				if (row != this.rows) {
					// vertical line is not drawn
					if (!this.line[Line.VERTICAL][col][row].isDrawn()) {
						// System.out.println("Field.hasUndrawnLine() "
						// + this.line[Line.VERTICAL][col][row]
						// + " is undrawn");
						return true;
					}
				}
			}
		}
		// all lines are drawn
		return false;
	}

	/**
	 * Searches for boxes whose number of drawn sides is n or m
	 * 
	 * @param n number of drawn sides
	 * @param m number of drawn sides
	 * @return the boxes that match the criterion
	 */
	private Box[] boxesWithNumDrawnSides(int n, int m) {
		// make vector to hold the boxes found
		Vector matchingBoxes = new Vector();
		// check every Box object
		for (int row = 0; row < this.rows; row++) {
			for (int col = 0; col < this.cols; col++) {
				Box candidateBox = this.box[col][row];
				int num = candidateBox.numDrawnSides();
				// box has the required number of sides, so add to the list
				if (num == n || num == m) matchingBoxes.add(candidateBox);
			}
		}
		// make array to hold result
		Box[] result = new Box[matchingBoxes.size()];
		// return result as array of Box objects
		return (Box[]) matchingBoxes.toArray(result);
	}

	/**
	 * Chooses a box at random and then one of its sides at random
	 * 
	 * @param boxes the boxes to choose from
	 * @return the randomly chosen side
	 */
	private Line randomUndrawnSide(Box[] boxes) {
		// choose a box at random
		int n = prng.nextInt(boxes.length);
		Box chosenBox = boxes[n];

		// loop until an undrawn side is chosen
		while (true) {
			// TODO move this into Box.java, pass prng
			// choose a side at random
			n = prng.nextInt(4);
			// get the line corresponding to that side
			Line chosenLine = chosenBox.getLine(n);
			// line is undrawn
			if (!chosenLine.isDrawn()) return chosenLine;
		}
	}

	/**
	 * Chooses an undrawn line according to following algorithm:
	 * 
	 * Search for all boxes with 3 drawn sides. Choose a side at random. If no
	 * boxes have 3 drawn sides then search for boxes with 0 or 1 drawn sides.
	 * Choose one at random. If no boxes have 0 or 1 drawn sides then search for
	 * boxes with 2 drawn sides. Choose a side at random. If no boxes are found
	 * then return null.
	 * 
	 * @return the chosen line, or null
	 */
	public Line computerChooseLine() {
		// get all boxes with 3 drawn sides
		Box[] matchingBoxes = boxesWithNumDrawnSides(3, 3);
		// some boxes were found, choose a random side
		if (matchingBoxes.length > 0) return randomUndrawnSide(matchingBoxes);
		// get all boxes that have 0 or 1 drawn sides
		matchingBoxes = boxesWithNumDrawnSides(0, 1);
		// some boxes were found, choose a random side
		if (matchingBoxes.length > 0) return randomUndrawnSide(matchingBoxes);
		// get all boxes that have 2 drawn sides (there must be some)
		matchingBoxes = boxesWithNumDrawnSides(2, 2);
		// some boxes were found, choose a random side
		if (matchingBoxes.length > 0) return randomUndrawnSide(matchingBoxes);
		// no boxes were found with 0, 1, 2, or 3 undrawn sides, so all lines
		// are drawn
		return null;
	}
}
