/* --------------------------------------------------------------------

PuzzleCanvas class

Copyright (c) 1996 by Christopher R. Waterson. All Rights Reserved.
Permission to use, copy, modify, and distribute this software
and its documentation for NON-COMMERCIAL purposes and without
fee is hereby granted provided that this copyright notice
appears in all copies.

File:		PuzzleCanvas.java
Synopsis:	An Eight Puzzle "Canvas" that provides UI
Author:		Christopher R. Waterson <waterson@eecs.umich.edu>
Created:	27 January 1996
Updated:	Christopher R. Waterson <waterson@eecs.umich.edu>
Last Modified:	22 Oct 1996

-------------------------------------------------------------------- */

package eightpuzzle;

import java.awt.*;
import java.util.*;


/**
 * Implements an eight-puzzle "canvas" that paints an eight-puzzle and
 * allows you to interact with it.
 */

class PuzzleCanvas extends Canvas {
	/**
	 * The underlying eight puzzle
	 */

	protected Puzzle m_puzzle = new Puzzle();


	/**
	 * The x-coordinate of each tile
	 */

	protected int[] m_x = new int[9];


	/**
	 * The y-coordinate of each tile
	 */

	protected int[] m_y = new int[9];


	/**
	 * The size of the canvans
	 */

	private Dimension m_oldSize = new Dimension();
		// Just initialize it to garbage


	/**
	 * An image for double buffering
	 */

	private Image m_imageBuffer;



	/**
	 * The image's graphics context
	 */

	private Graphics m_imageBufferGraphics;


	/**
	 * A separate thread to deal with moving tiles around
	 */

	TileMover m_tileMover = new TileMover(this);




	/**
	 * Create a new PuzzleCanvas
	 */

	public
	PuzzleCanvas() {
		// Start the tile mover thread
		m_tileMover.start();
	}


	/**
	 * Return the preferred size that the canvas would like
	 */

	public Dimension
	preferredSize() {
		FontMetrics m = getFontMetrics(getFont());

		int width = (m.getMaxAdvance() * 5) * 3;
		int height = (m.getHeight() * 5) * 3;

		return new Dimension(width, height);
	}



	/**
	 * Return the minimum size that the canvas can take
	 */

	public Dimension
	minimumSize() {
		FontMetrics m = getFontMetrics(getFont());

		int width = (m.getMaxAdvance() * 3) * 3;
		int height = (m.getHeight() * 3) * 3;

		return new Dimension(width, height);
	}


	/**
	 * Repaint the entire screen
	 */

	public void paint(Graphics g) {
		update(g);
	}


	/**
	 * Draw the tiles on the canvas using a double-buffering mechanism.
	 */

	public void update(Graphics g) {
		Dimension d = size();

		int dx = d.width / 3;
		int dy = d.height / 3;

		// Create a new image buffer if the size of the component changes
		if (m_oldSize.height != d.height
				|| m_oldSize.width != d.width
				|| m_imageBuffer == null) {

			// For debugging...
			//System.out.println("Resetting image buffer & recomputing dimensions.");

			// Dispose of the old graphics context, if one exists
			if (m_imageBufferGraphics != null) m_imageBufferGraphics.dispose();

			m_imageBuffer = createImage(d.width, d.height);
			m_imageBufferGraphics = m_imageBuffer.getGraphics();

			// Reset the locations of each tile
			setTileLocations();

			m_oldSize = d;
		}

		// Clear the image
		m_imageBufferGraphics.setColor(getBackground());
		m_imageBufferGraphics.fillRect(0, 0, d.width, d.height);
		FontMetrics fontMetrics = m_imageBufferGraphics.getFontMetrics();

		// Draw each tile
		for (int i = 1; i <= 8; ++i) {
			// ***ATTEN. For debugging
			// System.out.println("Drawing tile " + tile + " at (" + x + ", " + y + ")");

			// Draw the tile itself
			m_imageBufferGraphics.setColor(Color.gray);
			m_imageBufferGraphics.fill3DRect(m_x[i], m_y[i], dx, dy, true);

			// Draw the tile's number
			m_imageBufferGraphics.setColor(Color.black);

			m_imageBufferGraphics.drawString(String.valueOf(i),
				m_x[i] + (dx / 2) - (fontMetrics.stringWidth(String.valueOf(i)) / 2),
				m_y[i] + (dy / 2) + (fontMetrics.getHeight() / 2));
		}

		// Draw the double buffer
		g.drawImage(m_imageBuffer, 0, 0, getBackground(), this);
	}



	/**
	 * Reset the locaton of each tile according to its "true" position in the
	 * underlying puzzle.
	 */

	protected void
	setTileLocations() {
		Dimension d = size();

		for (int i = 0; i <= 8; ++i) {
			TileLocation loc = m_puzzle.getTileLocation(i);
			m_x[i] = loc.x * (d.width / 3);
			m_y[i] = loc.y * (d.height / 3);
		}
	}



	/**
	 * Translate an (x, y) coordinate to a spefic tile
	 */

	private int
	coordinateToTile(int x, int y) {
		Dimension d = size();

		int dx = d.width / 3;
		int dy = d.height / 3;

		return m_puzzle.tileAt(x / dx, y / dy);
	}


	/**
	 * Shuffle the eight puzzle
	 */

	public void
	shuffle() {
		// Make the underlying puzzle shuffle itself
		m_puzzle.shuffle(100);

		// To force update of the (x, y) coordinates...
		setTileLocations();

		// To redraw the puzzle...
		repaint();
	}


	/**
	 * Move the specified tile (pretty easy 'cause it can only go one place!
	 */

	public void
	move(int tile) {
		// Queue a move to the tile mover
		m_tileMover.move(tile);
	}



	/**
	 * Return the underlying eight puzzle object
	 */

	public Puzzle
	getPuzzle() { return m_puzzle; }


	/**
	 * Deal with a click by moving a tile (if appropriate)
	 */

	public boolean
	mouseUp(Event event, int x, int y) {
		int tile = coordinateToTile(x, y);
		move(tile);

		return true;
	}
}



/**
 * A process that moves a tile
 *
 * @author		Christopher R. Waterson
 * @version		1.0.0
 */

class TileMover extends Thread {

	/**
	 * The animation redraw interval, in ms.
	 */

	private static final long REDRAW_INTERVAL = 60;


	/**
	 * How many steps will the animation take
	 */

	private static final int ANIMATION_STEPS = 10;


	/**
	 * The canvas to which this TileMover is tied
	 */

	PuzzleCanvas m_canvas;


	/**
	 * A queue of tiles to move
	 */

	private Vector m_moves = new Vector();



	/**
	 * Create a new TileMover, tied to the specified canvas
	 */

	public
	TileMover(PuzzleCanvas canvas) {
		// For future reference...
		m_canvas = canvas;
	}



	/**
	 * Queue a new move to be made
	 *
	 * @param tile	The tile to move.
	 */

	public synchronized void
	move(int tile) {
		m_moves.addElement(new Integer(tile));
		notify();
	}


	/**
	 * Retrieve the next move from the queue
	 */

	public synchronized int
	getNextMove()
	throws InterruptedException {
		while (m_moves.size() == 0) {
			wait();
		}

		Integer move = (Integer) m_moves.elementAt(0);
		m_moves.removeElementAt(0);

		return move.intValue();
	}


	/**
	 * Loops forever, waiting for new moves to come in
	 */

	public void
	run() {
		try {
			while (true) {
				int tile = getNextMove();
				if (m_canvas.m_puzzle.canMove(tile))
					animateMove(tile);
			}
		} catch (java.lang.InterruptedException e) {
			System.out.println("TileMover.run: I recieved an InterruptedException, so I'm terminating...bye!");
			e.printStackTrace();
		}
	}


	/**
	 * Does the dirty work of moving the tile
	 */

	private void
	animateMove(int tile) {
		// Determine the location of the blank and the tile that we want to move.
		TileLocation blankLoc = m_canvas.m_puzzle.getTileLocation(Puzzle.BLANK);
		TileLocation tileLoc  = m_canvas.m_puzzle.getTileLocation(tile);

		Dimension d = m_canvas.size();

		// Compute the "out-of-placeness" in the x and y directions.
		int dx = (blankLoc.x - tileLoc.x) * (d.width  / (3 * ANIMATION_STEPS));
		int dy = (blankLoc.y - tileLoc.y) * (d.height / (3 * ANIMATION_STEPS));

		// For debugging...
		//System.out.println("Step size = (" + dx + ", " + dy + ")");

		// Compute the clipping region
		int left, right, top, bottom;

		if (blankLoc.x < tileLoc.x) {
			left   = blankLoc.x * (d.width / 3);
			right  = (tileLoc.x + 1) * (d.width / 3);
		} else {
			right  = (blankLoc.x + 1) * (d.width / 3);
			left   = tileLoc.x * (d.width / 3);
		}

		if (blankLoc.y < tileLoc.y) {
			top    = blankLoc.y * (d.height / 3);
			bottom = (tileLoc.y + 1) * (d.height / 3);
		} else {
			bottom = (blankLoc.y + 1) * (d.height / 3);
			top    = tileLoc.y * (d.height / 3);
		}

		// Animate the move of the tile
		try {
			for (int i = 0; i < ANIMATION_STEPS; ++i) {
				sleep(REDRAW_INTERVAL);

				// Move the tile on the canvas
				m_canvas.m_x[tile] += dx;
				m_canvas.m_y[tile] += dy;

				// Re-paint the screen
				m_canvas.repaint(REDRAW_INTERVAL / 2, left, top, right, bottom);
			}
		} catch (InterruptedException e) {
			// Just leave quietly. This is thrown by the call to sleep().
		}

		// Actually move the tile in the underlying Puzzle object
		m_canvas.m_puzzle.move(tile);

		// To make sure everything lines up nicely, do one more repaint
		m_canvas.setTileLocations();
		m_canvas.repaint(REDRAW_INTERVAL);
	}
}



