
import java.awt.*;
import javax.swing.*;
import java.awt.image.BufferedImage;

/**
 *  A MosaicPanel object represents a grid containing rows
 *  and columns of colored rectangles.  There can be "grouting"
 *  between the rectangles.  (The grouting is just drawn as a 
 *  one-pixel outline around each rectangle.)  The rectangles
 *  are drawn as raised 3D-style rectangles.  Methods are 
 *  provided for getting and setting the colors of the rectangles.
 *  (Revision Spring 2006: added possibility of having a border.)
 */
public class MosaicPanel extends JPanel {
	
	
	//------------------ private instance variables --------------------
	
	
	private int rows;       // The number of rows of rectangles in the grid.
	private int columns;    // The number of columns of rectangles in the grid.
	private Color defaultColor;   // Color used for any rectangle whose color
								//    has not been set explicitly.  This
								//    can never be null.
	private Color groutingColor;  // The color for "grouting" between 
								//    rectangles.  If this is null, no
								//    grouting is drawn.
	private boolean alwaysDrawGrouting;  // Grouting is drawn around default-
										//    colored rects if this is true.
	private Color[][] grid; // An array that contains the rectangles' colors.
							//   If a null occurs in this array, the rectangle
							//   is drawn in the default color, and "grouting"
							//   will be drawn around that rectangle only if
							//   alwaysDrawGrouting is true.  Also, the 
							//   rectangle is drawn as a flat rectangle rather
							//   than as a 3D rectangle.
	private BufferedImage OSI;  // The mosaic is actually drawn here, then the image 
							   //is copied to the screen
	private boolean needsRedraw;   // This is set to true when a change has occurred that
								 // changes the appearance of the mosaic.

	
	//------------------------ constructors -----------------------------
	
	
	/**
	 *  Construct a MosaicPanel with 42 rows and 42 columns of rectangles,
	 *  and with preferred rectangle height and width both set to 16.
	 */
	public MosaicPanel() {
		this(42,42,16,16);
	}
	
	/**
	 *  Construct a MosaicPanel with specified numbers of rows and columns of rectangles,
	 *  and with preferred rectangle height and width both set to 16.
	 */
	public MosaicPanel(int rows, int columns) {
		this(rows,columns,16,16);
	}
	
	/**
	 *  Construct a MosaicPanel with the specified number of rows and
	 *  columns of rectangles, and with a specified preferred size for the  
	 *  rectangle.  The default rectangle color is black, the
	 *  grouting color is gray, and alwaysDrawGrouting is set to false.
	 *  @param rows the mosaic will have this many rows of rectangles.  This must be a positive number.
	 *  @param columns the mosaic will have this many columns of rectangles.  This must be a positive number.
	 *  @param preferredBlockWidth the preferred width of the mosaic will be set this value times the number of
	 *  columns.  The actual width is set by the component that contains the mosaic, and so might not be
	 *  equal to the preferred width.  Size is measured in pixels.  The value should not be less than about 5.
	 *  @param preferredBlockHeight the preferred height of the mosaic will be set this value times the number of
	 *  rows.  The actual height is set by the component that contains the mosaic, and so might not be
	 *  equal to the preferred height.   Size is measured in pixels.  The value should not be less than about 5.
	 */
	public MosaicPanel(int rows, int columns, int preferredBlockWidth, int preferredBlockHeight) {
		this(rows, columns, preferredBlockWidth, preferredBlockHeight, null, 0);
	}

	
	/**
	 *  Construct a MosaicPanel with the specified number of rows and
	 *  columns of rectangles, and with a specified preferred size for the  
	 *  rectangle.  The default rectangle color is black, the
	 *  grouting color is gray, and alwaysDrawGrouting is set to false.
	 *  If a non-null border color is specified, then a border of that color is added
	 *  to the panel, and its width is taken into accoutn in the computation of the preferred
	 *  size of the panel.
	 *  @param rows the mosaic will have this many rows of rectangles.  This must be a positive number.
	 *  @param columns the mosaic will have this many columns of rectangles.  This must be a positive number.
	 *  @param preferredBlockWidth the preferred width of the mosaic will be set this value times the number of
	 *  columns.  The actual width is set by the component that contains the mosaic, and so might not be
	 *  equal to the preferred width.  Size is measured in pixels.  The value should not be less than about 5.
	 *  @param preferredBlockHeight the preferred height of the mosaic will be set this value times the number of
	 *  rows.  The actual height is set by the component that contains the mosaic, and so might not be
	 *  equal to the preferred height.   Size is measured in pixels.  The value should not be less than about 5.
	 *  @param borderColor if non-null, a border of this color is added to the panel.  The border is then
	 *  taken into account in the computation of the panel's preferred size.
	 *  @param borderWidth if borderColor is non-null, then this parameter gives the width of the border; any
	 *  value less than 1 is treated as 1.
	 */
	public MosaicPanel(int rows, int columns, int preferredBlockWidth, int preferredBlockHeight, Color borderColor, int borderWidth) {
		this.rows = rows;
		this.columns = columns;
		grid = new Color[rows][columns];
		defaultColor = Color.black;
		groutingColor = Color.gray;
		alwaysDrawGrouting = false;
		setBackground(defaultColor);
		setOpaque(true);
		setDoubleBuffered(false);
		if (borderColor != null) {
			if (borderWidth < 1)
				borderWidth = 1;
			setBorder(BorderFactory.createLineBorder(borderColor,borderWidth));
		}
		else
			borderWidth = 0;
		if (preferredBlockWidth > 0 && preferredBlockHeight > 0)
			setPreferredSize(new Dimension(preferredBlockWidth*columns + 2*borderWidth, preferredBlockHeight*rows + 2*borderWidth));
	}
	
	
	//--------- methods for getting and setting grid properties ----------
	
	
	/**
	 *  Set the defaultColor.  If c is null, the color will be set to black.
	 *  When a mosaic is first created, the defaultColor is black.
	 *  This is the color that is used for rectangles whose color
	 *  value is null.  Such rectangles are drawn as flat rather
	 *  than 3D rectangles.
	 */
	public void setDefaultColor(Color c) {
		if (c == null)
			c = Color.black;
		if (! c.equals(defaultColor)) {
			defaultColor = c;
			setBackground(c);
			redrawMosaic();
		}
	}
	
	
	/**
	 *  Return the defaultColor, which cannot be null.
	 */
	public Color getDefaultColor() {
		return defaultColor;
	}
	
	
	/**
	 *  Set the color of the "grouting" that is drawn between rectangles.
	 *  If the value is null, no grouting is drawn and the rectangles
	 *  fill the entire grid.   When a mosaic is first created, the
	 *  groutingColor is gray.
	 */
	public void setGroutingColor(Color c) {
		if (c == null || ! c.equals(groutingColor)) {
			groutingColor = c;
			redrawMosaic();
		}
	}
	
	
	/**
	 *  Get the current groutingColor, which can be null.
	 */
	public Color getGroutingColor(Color c) {
		return groutingColor;
	}
	
	
	/**
	 *  Set the value of alwaysDrawGrouting.  If this is false, then
	 *  no grouting is drawn around rectangles whose color value is null.
	 *  When a mosaic is first created, the value is false.
	 */
	public void setAlwaysDrawGrouting(boolean always) {
		if (alwaysDrawGrouting != always) {
			alwaysDrawGrouting = always;
			redrawMosaic();
		}
	}
	
	
	/**
	 *  Get the value of the alwaysDrawGrouting property.
	 */   
	public boolean getAlwaysDrawGrouting() {
		return alwaysDrawGrouting; 
	}
	
	
	/**
	 *  Set the number of rows and columns in the grid.  If the value of
	 *  the preserveData parameter is false, then the color values of all
	 *  the rectangles in the new grid are set to null.  If it is true,
	 *  then as much color data as will fit is copied from the old grid.
	 */
	public void setGridSize(int rows, 
			int columns, boolean preserveData) {
		if (rows > 0 && columns > 0) {
			Color[][] newGrid = new Color[rows][columns];
			if (preserveData) {
				int rowMax = Math.min(rows,this.rows);
				int colMax = Math.min(columns,this.columns);
				for (int r = 0; r < rowMax; r++)
					for (int c = 0; c < colMax; c++)
						newGrid[r][c] = grid[r][c];
			}
			grid = newGrid;
			this.rows = rows;
			this.columns = columns;
			redrawMosaic();
		}
	}
	
	
	/**
	 *  Return the number of rows of rectangles in the grid.
	 */
	public int getRowCount() {
		return rows;
	}
	
	
	/**
	 *  Return the number of columns of rectangles in the grid.
	 */
	public int getColumnCount() {
		return columns;
	}   
	
	
	//------------------ other useful public methods ---------------------
	
	
	
	/**
	 *  Get the color has been set for the rectangle in the specified
	 *  row and column of the grid.  This value can be null if no
	 *  color has been set for that rectangle.  (Such rectangles are
	 *  actually displayed using the defaultColor.)  If the specified
	 *  rectangle is outside the grid, then null is returned.
	 */
	public Color getColor(int row, int col) {
		if (row >=0 && row < rows && col >= 0 && col < columns)
			return grid[row][col];
		else
			return null;
	}
	
	
	/**
	 *  Return the red component of color of the rectangle in the
	 *  specified row and column.  If that rectangle lies outside 
	 *  the grid or if no color has been specified for the rectangle,
	 *  then the red component of the defaultColor is returned.
	 */
	public int getRed(int row, int col) {
		if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null)
			return grid[row][col].getRed();
		else
			return defaultColor.getRed();
	}
	
	
	/**
	 *  Return the green component of color of the rectangle in the
	 *  specified row and column.  If that rectangle lies outside 
	 *  the grid or if no color has been specified for the rectangle,
	 *  then the green component of the defaultColor is returned.
	 */
	public int getGreen(int row, int col) {
		if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null)
			return grid[row][col].getGreen();
		else
			return defaultColor.getGreen();
	}
	
	
	/**
	 *  Return the blue component of color of the rectangle in the
	 *  specified row and column.  If that rectangle lies outside 
	 *  the grid or if no color has been specified for the rectangle,
	 *  then the blue component of the defaultColor is returned.
	 */
	public int getBlue(int row, int col) {
		if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null)
			return grid[row][col].getBlue();
		else
			return defaultColor.getBlue();
	}
	
	
	/**
	 *  Set the color of the rectangle in the specified row and column.
	 *  If the rectangle lies outside the grid, this is simply ignored.
	 *  The color can be null.  Rectangles for which the color is null
	 *  will be displayed in the defaultColor, and they will be shown
	 *  as flat rather than 3D rects.
	 */
	public void setColor(int row, int col, Color c) {
		if (row >=0 && row < rows && col >= 0 && col < columns) {
			grid[row][col] = c;
			drawSquare(row,col);
		}
	}
	
	
	/**
	 *  Set the color of the rectangle in the specified row and column.
	 *  The color is specified by giving red, green, and blue components
	 *  of the color.  These values should be in the range from 0 to
	 *  255, inclusive, and they are clamped to lie in that range.
	 *  If the rectangle lies outside the grid, this is simply ignored.
	 */
	public void setColor(int row, int col, int red, int green, int blue) {
		if (row >=0 && row < rows && col >= 0 && col < columns) {
			red = (red < 0)? 0 : ( (red > 255)? 255 : red);
			green = (green < 0)? 0 : ( (green > 255)? 255 : green);
			blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue);
			grid[row][col] = new Color(red,green,blue);
			drawSquare(row,col);
		}
	}
	
	
	/**
	 *  Set the color of the rectangle in the specified row and column.
	 *  The color is specified by giving hue, saturation, and brightness
	 *  components of the color.  These values should be in the range from 
	 *  0.0 to 1.0, inclusive, and they are clamped to lie in that range.
	 *  If the rectangle lies outside the grid, this is simply ignored.
	 */
	public void setHSBColor(int row, int col, 
			double hue, double saturation, double brightness) {
		if (row >=0 && row < rows && col >= 0 && col < columns) {
			grid[row][col] = makeHSBColor(hue,saturation,brightness);
			drawSquare(row,col);
		}
	}
	
	
	/**
	 *  A little utility routine that is provided for making a color
	 *  from hue, saturation, and brightness values.  These values should
	 *  be in the range from 0.0 to 1.0, inclusive, and they are clamped
	 *  to lie in that range.  (This method is more convenient than
	 *  Color.getHSBColor() since it use double values rather than float.)
	 */
	public static Color makeHSBColor(
			double hue, double saturation, double brightness) {
		float h = (float)hue;
		float s = (float)saturation;
		float b = (float)brightness;
		h = (h < 0)? 0 : ( (h > 1)? 1 : h );
		s = (s < 0)? 0 : ( (s > 1)? 1 : s );
		b = (b < 0)? 0 : ( (b > 1)? 1 : b );
		return Color.getHSBColor(h,s,b);
	}
	
	
	/**
	 *  Set all rectangles of the grid to have the specified color.
	 *  The color can be null.  In that case, the rectangles are
	 *  drawn as flat rather than 3D rects in the defaultColor.
	 */
	public void fill(Color c) {
		for (int i = 0; i < rows; i++)
			for (int j = 0; j < columns; j++)
				grid[i][j] = c;
		redrawMosaic();      
	}
	
	
	/**
	 *  Set all rectangles of the grid to have the color specified by
	 *  the given red, green, and blue components.  These components 
	 *  should be integers in the range 0 to 255 and are clamped to lie
	 *  in that range.
	 */
	public void fill(int red, int green, int blue) {
		red = (red < 0)? 0 : ( (red > 255)? 255 : red);
		green = (green < 0)? 0 : ( (green > 255)? 255 : green);
		blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue);
		fill(new Color(red,green,blue));
	}
	
	
	/**
	 *  Fill all the rectangles with randomly selected colors.
	 */
	public void fillRandomly() {
		for (int i = 0; i < rows; i++)
			for (int j = 0; j < columns; j++) {
				int r = (int)(256*Math.random());
				int g = (int)(256*Math.random());
				int b = (int)(256*Math.random());
				grid[i][j] = new Color(r,g,b);
			}
		redrawMosaic();
	}
	
	
	/**
	 *   Clear the mosaic by setting all the colors to null.
	 */
	public void clear() {
		fill(null);
	}
	
	
	/**
	 *   Return an object that contains the color data that
	 *   is needed to redraw the mosaic.  This includes the 
	 *   defaultColor, the groutingColor, the number of rows and
	 *   columns, the color of each rectangle, and the
	 *   value of alwaysDrawGrouting.
	 */
	public Object copyColorData() {
		// Note:  This is a fudge.  The data about defaultColor,
		// groutingColor, and alwaysDrawGrouting is added to the
		// last row of the grid.  If alwaysDrawGrouting is true,
		// this is recorded by adding an extra empty space to
		// that row.
		Color[][] copy = new Color[rows][columns];
		// Replace the last row with a longer row.
		if (alwaysDrawGrouting)
			copy[rows-1] = new Color[columns+3];
		else
			copy[rows-1] = new Color[columns+2];
		for (int r = 0; r < rows; r++)
			for (int c = 0; c < columns; c++)
				copy[r][c] = grid[r][c];
		copy[rows-1][columns] = defaultColor;
		copy[rows-1][columns+1] = groutingColor;
		return copy;
	}
	
	
	/**
	 *  The parameter to this method should be an Object that
	 *  was created by the copyColorData() method.  This method
	 *  will restore the data in the object to the grid.  This
	 *  can change the size of the grid, the colors in the grid,
	 *  the defaultColor, the groutingColor, and the value of
	 *  alwaysDrawGrouting.  If the object is of the proper
	 *  form, then the return value is true.  If not, the return
	 *  value is false and no changes are made to the current data.
	 */
	public boolean restoreColorData(Object data) {
		if (data == null || !(data instanceof Color[][]))
			return false;
		Color[][] newGrid = (Color[][])data;
		int newRows = newGrid.length;
		if (newRows == 0 || newGrid[0].length == 0)
			return false;
		int newColumns = newGrid[0].length;
		for (int r = 1; r < newRows-1; r++)
			if (newGrid[r].length != newColumns)
				return false;
		if (newGrid[newRows-1].length != newColumns+2
				&& newGrid[newRows-1].length != newColumns+3)
			return false;
		if (newGrid[newRows-1][newColumns] == null)
			return false;
		rows = newRows;
		columns = newColumns;
		grid = new Color[rows][columns];
		for (int i = 0; i < rows; i++)
			for (int j = 0; j < columns; j++)
				grid[i][j] = newGrid[i][j];
		defaultColor = newGrid[newRows-1][newColumns];
		setBackground(defaultColor);
		groutingColor = newGrid[newRows-1][newColumns+1];
		alwaysDrawGrouting = newGrid[newRows-1].length == 3;
		redrawMosaic();
		return true;
	}
	
	/**
	 * Given an x-coordinate of a pixel in the MosaicPanel, this method returns
	 * the row number of the mosaic rectangle that contains that pixel.  If
	 * the x-coordinate does lie within the bounds of the mosaic, the return
	 * value is -1 or is equal to the number of columns, depending on whether
	 * x is to the left or to the right of the mosaic.
	 */
	public int xCoordToColumnNumber(int x) {
		Insets insets = getInsets();
		if (x < insets.left)
			return -1;
		double colWidth = (double)(getWidth()-insets.left-insets.right) / columns;
		int col = (int)( (x-insets.left) / colWidth);
		if (col >= columns)
			return -1;
		else
			return col;
	}
	
	/**
	 * Given a y-coordinate of a pixel in the MosaicPanel, this method returns
	 * the column number of the mosaic rectangle that contains that pixel.  If
	 * the y-coordinate does lie within the bounds of the mosaic, the return
	 * value is -1  or is equal to the number of rows, depending on whether
	 * y is above or below the mosaic.
	 */
	public int yCoordToRowNumber(int y) {
		Insets insets = getInsets();
		if (y < insets.top)
			return -1;
		double rowHeight = (double)(getHeight()-insets.top-insets.bottom) / rows;
		int row = (int)( (y-insets.top) / rowHeight);
		if (row >= rows)
			return -1;
		else
			return row;
	}
	
	/**
	 *  Returns the BufferedImage that contains the actual image of the mosaic.
	 *  If this is called before the mosaic has been drawn on screen, the return value will be null.
	 */
	public BufferedImage getImage() {
		return OSI;
	}
	
	//--------------- implementation details ------------------------
	//---------- (routines called internally or by the system) ------
	
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		if ( (OSI == null) || OSI.getWidth() != getWidth() || OSI.getHeight() != getHeight() ) {
			OSI = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
			needsRedraw = true;
		}
		if (needsRedraw) {
			Graphics OSG = OSI.getGraphics();
			for (int r = 0; r < rows; r++)
				for (int c = 0; c < columns; c++)
					drawSquare(OSG,r,c,false);
			OSG.dispose();
			needsRedraw = false;
		}
		g.drawImage(OSI,0,0,null);
	}
	
	private void redrawMosaic() {
		needsRedraw = true;
		repaint();
	}
		
	private void drawSquare(Graphics g, int row, int col, boolean callRepaint) {
		// Draw one of the rectangles in a specified graphics 
		// context.  g must be non-null and (row,col) must be
		// in the grid.
		Insets insets = getInsets();
		double rowHeight = (double)(getHeight()-insets.left-insets.right) / rows;
		double colWidth = (double)(getWidth()-insets.top-insets.bottom) / columns;
		int xOffset = insets.left;
		int yOffset = insets.top; 
		int y = yOffset + (int)Math.round(rowHeight*row);
		int h = Math.max(1, (int)Math.round(rowHeight*(row+1))+yOffset - y);
		int x = xOffset + (int)Math.round(colWidth*col);
		int w = Math.max(1, (int)Math.round(colWidth*(col+1))+xOffset - x);
		Color c = grid[row][col];
		g.setColor( (c == null)? defaultColor : c );
		if (groutingColor == null || (c == null && !alwaysDrawGrouting)) {
			if (c == null)
				g.fillRect(x,y,w,h);
			else
				g.fill3DRect(x,y,w,h,true);
		}
		else {
			if (c == null)
				g.fillRect(x+1,y+1,w-2,h-2);
			else
				g.fill3DRect(x+1,y+1,w-2,h-2,true);
			g.setColor(groutingColor);
			g.drawRect(x,y,w-1,h-1);
		}
		if (callRepaint)
			repaint(x,y,w,h);
	}
	
	private void drawSquare(int row, int col) {
		// Draw a specified rectangle directly on the applet in
		// the off-screen image, and call repaint to copy that
		// square to the screen.  (row,col) must be
		// within the grid.
		if (OSI == null)
			repaint();
		else {
			Graphics g = OSI.getGraphics();
			drawSquare(g,row,col,true);
			g.dispose();
		}
	}
	
	
} // end class MosaicPanel
