import java.awt.*;
import java.awt.event.*;

/**
 * GolCanvas is a zoomable, drawable canvas for an implementation of Conway's
 * Game of Life. The rules for cell reproduction can be changed dynamically.
 * 
 * <p>
 * Made by Joakim "firetech" Andersson in June 2007, modified slightly in July
 * 2007, April 2008 and June 2008.<br>
 * <i>Free to use, abuse and/or modify for non-commercial purposes as long as
 * I'm credited.</i>
 * </p>
 */
public class GolCanvas extends Canvas {
	/** The back buffer for the double buffering. */
	private Image buffer;

	/** The currently used dimension for the cell matrix. */
	private Dimension dim;

	/** The cell matrix. */
	private boolean[][] cells;

	/** The pixed width and height of each cell in the cell matrix. */
	private short cellSize = 4;

	/** The generation counter */
	private int generations = 0;

	/** Label to write the generation count to, if any. */
	private Label genCounter;

	/** PlayingThread used for the play() method */
	private PlayThread t = null;

	/**
	 * The game rules. Should have 9 positions, corresponding to the possible
	 * number of neighbours (0-8) for a cell. Each position should have a value
	 * between 0 and 3:
	 * <ul>
	 * <li>0 - kills the cell</li>
	 * <li>1 - a living cell will survive</li>
	 * <li>2 - an empty cell will be brought to life</li>
	 * <li>3 - combination of 1 and 2</li>
	 * </ul>
	 */
	private short[] rules;

	/**
	 * Create a new canvas with specified canvas size and cell size 4x4.
	 * 
	 * @param width
	 *            the wanted canvas width.
	 * @param height
	 *            the wanted canvas height.
	 * @param rules
	 *            the game rules. See {@link #changeRules} for syntax.
	 * @param genCounter
	 *            the Label to update with the current generation counter. Can
	 *            be null if no Label is wanted.
	 * @throws IllegalArgumentException
	 *             if rules doesn't follow the correct syntax.
	 */
	public GolCanvas(int width, int height, String rules, Label genCounter) {
		super();
		changeRules(rules);
		this.genCounter = genCounter;

		GolPainter painter = new GolPainter();
		addMouseListener(painter);
		addMouseMotionListener(painter);

		setPreferredSize(new Dimension(width, height));
		setMinimumSize(getPreferredSize());
		setSize(getPreferredSize());
		setBackground(Color.white);

		/* Uncomment this block to start with a glider gun
		cells = new boolean[38][18];
		cells[0][5] = cells[1][5] = cells[0][6] = cells[1][6] = cells[9][5] =
			cells[10][5] = cells[8][6] = cells[10][6] = cells[8][7] =
			cells[9][7] = cells[16][7] = cells[17][7] = cells[16][8] =
			cells[18][8] = cells[16][9] = cells[23][3] = cells[24][3] =
			cells[22][4] = cells[24][4] = cells[22][5] = cells[23][5] =
			cells[34][3] = cells[35][3] = cells[34][4] = cells[35][4] =
			cells[24][15] = cells[25][15] = cells[26][15] = cells[24][16] =
			cells[25][17] = cells[35][10] = cells[36][10] = cells[35][11] =
			cells[37][11] = cells[35][12] = true;
		// */
	}

	/**
	 * Change the game rules.
	 * 
	 * @param newRules
	 *            the rules string. Syntax is "number of neighbours to stay
	 *            alive/number of neighbours to be born". (Regular expression
	 *            "0?1?2?3?4?5?6?7?8?/0?1?2?3?4?5?6?7?8?".) Standard Game of
	 *            Life rules are therefore "23/3".
	 * @throws IllegalArgumentException
	 *             if newRules doesn't follow the correct syntax.
	 */
	public void changeRules(String newRules) {
		if (!newRules.matches("0?1?2?3?4?5?6?7?8?/0?1?2?3?4?5?6?7?8?"))
			throw new IllegalArgumentException();

		rules = new short[9];
		short add = 1;
		for (int i = 0; i < newRules.length(); i++) {
			if (newRules.charAt(i) == '/') {
				add = 2;
			} else {
				rules[(int) (newRules.charAt(i) - '0')] += add;
			}
		}
	}

	/**
	 * Clear the canvas. (Kill all cells.) Will reset the generation counter and
	 * {@link #stop} animation, if it is running.
	 */
	public void clear() {
		stop();
		int width = (int) Math.floor(dim.width / cellSize);
		int height = (int) Math.floor(dim.height / cellSize);
		cells = new boolean[width][height];
		generations = 0;
		repaint();
	}

	/**
	 * Resize the cell matrix for a new zoom level or canvas size.
	 */
	private void resize() {
		int width = (int) Math.floor(dim.width / cellSize);
		int height = (int) Math.floor(dim.height / cellSize);
		boolean[][] newCells = new boolean[width][height];
		if (cells != null) {
			int xOffset = (width - cells.length) / 2;
			int yOffset = (height - cells[0].length) / 2;
			for (int y = -(int) Math.min(yOffset, 0); y < cells[0].length
					&& y + yOffset < height; y++) {
				for (int x = -(int) Math.min(xOffset, 0); x < cells.length
						&& x + xOffset < width; x++) {
					newCells[x + xOffset][y + yOffset] = cells[x][y];
				}
			}
		}
		cells = newCells;
	}

	/**
	 * Zoom the canvas out. Will make the cell width and height half of the old
	 * value. Will temporarily {@link #stop} animation, if it is running.
	 * 
	 * @return whether another step of zooming out should be allowed. (Minimum
	 *         cell size is 1x1.)
	 */
	public boolean zoomOut() {
		boolean restart = isPlaying();
		stop();
		cellSize /= 2;
		resize();
		repaint();
		if (restart) {
			play();
		}
		return cellSize > 1;
	}

	/**
	 * Zoom the canvas in. Will make the cell width and height twice as big.
	 * Will temporarily {@link #stop} animation, if it is running.
	 * 
	 * @return whether another step of zooming in should be allowed. (Maximum
	 *         cell size is 32x32.)
	 */
	public boolean zoomIn() {
		boolean restart = isPlaying();
		stop();
		cellSize *= 2;
		resize();
		repaint();
		if (restart) {
			play();
		}
		return cellSize < 32;
	}

	/**
	 * Draw the next generation according to the stored rules. Generates a new
	 * cell matrix corresponding to the next generation, then calls for a
	 * repaint().
	 */
	public void next() {
		int width = cells.length, height = cells[0].length;
		boolean[][] newCells = new boolean[width][height];
		for (int y = 0; y < cells[0].length; y++) {
			for (int x = 0; x < cells.length; x++) {
				// Count neighbours
				short neighbours = 0;
				for (int i = (int) Math.max(x - 1, 0); i <= (int) Math.min(
						x + 1, cells.length - 1); i++) {
					for (int j = (int) Math.max(y - 1, 0); j <= (int) Math.min(
							y + 1, cells[0].length - 1); j++) {
						if ((i != x || j != y) && cells[i][j]) {
							neighbours++;
						}
					}
				}
				// Check if cell should be alive according to the rules
				if ((cells[x][y] && (rules[neighbours] & 1) == 1)
						|| (!cells[x][y] && (rules[neighbours] & 2) == 2)) {
					newCells[x][y] = true;
				}
			}
		}
		cells = newCells;
		generations++;
		repaint();
	}

	/**
	 * Animate the evolution on the canvas. Will draw a new generation every 100
	 * milliseconds.
	 * 
	 * @see #stop
	 */
	public void play() {
		if (t == null) {
			t = new PlayThread();
			t.start();
		}
	}

	/**
	 * Check if animation is running.
	 * 
	 * @return whether animation is running
	 */
	public boolean isPlaying() {
		return (t != null);
	}

	/**
	 * Stop animating the canvas (if it is playing).
	 * <p>
	 * This should be called <u>before</u> doing any changes that affect the
	 * size of the cell matrix. Otherwise, strange things <b>will</b> happen.
	 * This is, however, done automatically by all built in operations that need
	 * it.
	 * </p>
	 * <p>
	 * Nothing will happen if this is called when animation already is stopped.
	 * </p>
	 */
	public void stop() {
		if (t != null) {
			t.stopPlaying();
			t = null;
		}
	}

	/**
	 * Paints the current cell matrix to the canvas. Should <b>only</b> be
	 * called from AWT. Can be indirectly called with repaint(), though.
	 * 
	 * @param g
	 *            the Graphics window to paint with.
	 */
	public void paint(Graphics g) {
		if (dim == null || dim.width != getWidth() || dim.height != getHeight()) {
			dim = getSize();
			buffer = createImage(dim.width, dim.height);
			boolean restart = isPlaying();
			stop();
			resize();
			if (restart) {
				play();
			}
		}
		Graphics plane = buffer.getGraphics();
		plane.clearRect(0, 0, dim.width, dim.height);
		plane.setColor(Color.black);
		for (int y = 0; y < cells[0].length; y++) {
			for (int x = 0; x < cells.length; x++) {
				if (cells[x][y]) {
					plane.fillRect(x * cellSize, y * cellSize, cellSize,
							cellSize);
				}
			}
		}
		if (genCounter != null) {
			String genText = String.valueOf(generations);
			// Prevent the Label from going too small after window resize.
			while (genText.length() < 4)
				genText = "0" + genText;
			genCounter.setText(genText);
		}
		g.drawImage(buffer, 0, 0, this);
	}

	/**
	 * Update the canvas. Needed for the double buffering to work properly. Just
	 * calls paint(g). Should <b>only</b> be called from AWT.
	 * 
	 * @param g
	 *            the Graphics window to paint with.
	 */
	public void update(Graphics g) {
		paint(g);
	}

	private class PlayThread extends Thread {
		/** Whether animation is running. Used to stop the thread. */
		private boolean playing;

		/**
		 * Thread operation for the play() method.
		 */
		public void run() {
			playing = true;
			try {
				synchronized (this) {
					while (playing) {
						next();
						wait(100);
					}
				}
			} catch (InterruptedException e) {
			}
		}

		/**
		 * Stop the thread in a controlled manner.
		 */
		public synchronized void stopPlaying() {
			playing = false;
			notify();
		}
	}

	/**
	 * GolPainter handles the mouse events. I.E. drawing on the canvas.
	 */
	private class GolPainter extends MouseAdapter implements
			MouseMotionListener {
		/**
		 * Whether drawAt() should give birth to or kill cells, set by
		 * mousePressed().
		 */
		private boolean draw = true;

		/**
		 * Draw at the specified cell. Whether to make it alive or dead is
		 * decided by what mouse button was clicked last. (Left = live.)
		 * 
		 * @param x
		 *            the x position (in the canvas) to draw at.
		 * @param y
		 *            the y position (in the canvas) to draw at.
		 */
		private void drawAt(int x, int y) {
			x = (int) Math.max(Math.min(Math.floor(x / cellSize),
					cells.length - 1), 0);
			y = (int) Math.max(Math.min(Math.floor(y / cellSize),
					cells[0].length - 1), 0);
			cells[x][y] = draw;
			repaint();
		}

		/**
		 * Called when a mouse click is initiated (the button is held down).
		 * Will determine whether drawAt() will give birth to or kill cells.
		 */
		public void mousePressed(MouseEvent e) {
			draw = e.getButton() == MouseEvent.BUTTON1;
		}

		/**
		 * Called when a mouse click is done (the button is released). Will call
		 * drawAt() for the clicked coordinates.
		 */
		public void mouseClicked(MouseEvent e) {
			drawAt(e.getX(), e.getY());
		}

		/**
		 * Called when the mouse is dragged over the field. Will call drawAt()
		 * for coordinates which the mouse is dragged over.
		 */
		public void mouseDragged(MouseEvent e) {
			drawAt(e.getX(), e.getY());
		}

		/**
		 * Not used, required by MouseMotionListener
		 */
		public void mouseMoved(MouseEvent e) {
		}
	}
}