/** A Tetris brick.
 *
 * Each brick is tied to a PlayingField on which it is displayed.
 *
 * @see	PlayingField
 * @author Jim Grundy
 * @version $Revision: 1.20 $
 */

import java.util.Random;// Random number generator.
import java.awt.Color;	// Colors.

public class Brick extends FieldObject
{
    private boolean image[][];	// Bit mapped image of the Brick's shape
    private int xPosition;	// X coordinate of the bottom left of the brick
    private int yPosition;	// Y coordinate of the bottom left of the brick
    
    
    /* Class invariant: The brick must fit on the playing field.
     * 
     * (0 <= xPosition) && (xPosition + width(image) < field.getWidth()) &&
     * (0 <= yPosition) && (yPosition <= field.getHeight() */
    
    /** Make a random Tetris brick on the nominated playing field.
     *
     * The brick appears horizontally centred, with the bottom row of
     * the brick just above the playing field.
     * 
     * @param field The playing field the brick is displayed on.
     */
    public Brick(ObjectField field)
    {
	/* Use the next random shape/color/letter for the brick. */
	super(field, COLORS[nextRandom], LETTERS[nextRandom]);
	image = SHAPES[nextRandom];
	nextRandom = Math.abs(RANDOM.nextInt()) % SHAPES.length;
	
	/* Position the brick in the middle just above the top of the field. */
	xPosition = (getField().getWidth() - width(image))/2;
	yPosition = getField().getHeight();
	
	draw(true);
	getField().repaint();
    }
    
    /** Rotate brick left (counter clockwise). */
    public void rotateLeft()
    {
	/* Construct a left rotated image of the brick. */
	boolean[][] rotatedImage = new boolean[height(image)][width(image)];
	for(int x=0; x<width(rotatedImage); x=x+1){
	    for(int y=0; y<height(rotatedImage); y=y+1){
		rotatedImage[x][y] = image[y][height(image) - 1 - x];
	    }
	}
	
	/* The brick may need to move to maintain the centre position. */
	int deltaX = (width(image) - width(rotatedImage))/2;
	
	moveAndRotate(deltaX,0,rotatedImage);
    }
  
    /** Rotate brick right (clockwise). */
    public void rotateRight()
    {
	/* Construct a right rotated image of the brick. */
	boolean[][] rotatedImage = new boolean[height(image)][width(image)];
	for(int x=0; x<width(rotatedImage); x=x+1){
	    for(int y=0; y<height(rotatedImage); y=y+1){
		rotatedImage[x][y] = image[width(image) - 1 - y][x];
	    }
	}
	
	/* The brick may need to move to maintain the centre position. */
	int deltaX = (width(image) - width(rotatedImage))/2;
	
	moveAndRotate(deltaX,0,rotatedImage);
    }
    
    /** Move the brick down one level. */
    public void moveDown()
    {
	moveAndRotate(0,-1,image);
    }
    
    /** Move the brick one column to the left. */
    public void moveLeft()
    {
	moveAndRotate(-1,0,image);
    }
    
    /** Move the brick one column to the right. */
    public void moveRight()
    {
	moveAndRotate(1,0,image);
    }
    
    /** Drop a brick to the floor of the field in one step. */
    public void plummet()
    {
	draw(false);
	while (canFit(0,-1,image)) {
	    yPosition = yPosition - 1;
	}
	draw(true);
	getField().repaint();
    }
    
    /** Determine whether the brick has landed.
     *
     * @return	True if the brick has landed, and false otherwise. */
    public boolean hasLanded()
    {
	/* The brick has landed if it can't move down. */
	return !canFit(0,-1,image);
    }

    /* Try to move the brick by the specified deltaX and deltaY, and
     * give it the new (possibly) rotated image.  If the brick won't fit
     * as requested, then do nothing.  */
    private void moveAndRotate(int deltaX, int deltaY, boolean[][] newImage) 
    {
	draw(false);
	if (canFit(deltaX,deltaY,newImage)) {
	    xPosition = xPosition + deltaX;
	    yPosition = yPosition + deltaY;
	    image = newImage;
	}
	draw(true);
	getField().repaint();
    }
    
    /* The width of an image. */
    private int width(boolean[][] image)
    {
	return image.length;
    }
    
    /* The height of an image. */
    private int height(boolean[][] image)
    {
	return image[0].length;
    }
    
    /* Determine whether the brick would fit if moved by deltaX and
     * deltaY, and given a new image.
     *
     * PRECONDITION: Brick must be erased when this check is made. */
    private boolean canFit(int deltaX, int deltaY, boolean[][] newImage) 
    {
	/* Calculate the new position of the brick. */
	int newX = xPosition + deltaX;
	int newY = yPosition + deltaY;
	
	/* If the new image extends to the left, right or below the
	 * field, then it could not possibly fit. */
	if ((newX < 0) || (newX + width(newImage) > getField().getWidth())
	    || (newY < 0))
	    {
		return false;
	    }
	
	/* Determine if there would be a collision between the new image
	 * and any other objects on the field. */
	boolean collision = false;
	for (int x=0; x<width(newImage); x=x+1) {
	    for (int y = 0;
		 (y < height(newImage)) && 
		     (y+yPosition < getField().getHeight());
		 y = y+1)
		{
		    /* INVARIANT: 
		     *   (0 <= x <= width(newImage)) &&
		     *   (0 <= y <= height(newImage) &&
		     *   (collision = "some tile in the new image before
		     *   (x,y) that would collide with another object on the 
		     *   field if the brick were to be placed at (newX,newY).")
		     */
		    Object occupant = getField().getObject(newX+x,newY+y);
		    collision = collision
			|| (newImage[x][y] 
			    && (occupant != null) && (occupant != this));
		}
	}
	
	return !collision;
    }
    
    /* Draw or erase the brick on the field.
     * If visisble is true then draw the brick, otherwise erase it.
     *
     * PRECONDITION: 
     *   (0 <= xPosition) &&
     *   (xPosition + width(image) < getField().getWidth()) &&
     *   (0 <= yPosition) */
    private void draw(boolean visible)
    {
	for (int x=0; x<width(image); x=x+1) {
	    for (int y=0;
		 (y < height(image)) && (y+yPosition < getField().getHeight());
		 y=y+1)
		{
		    if (image[x][y]){
			if (visible) {
			    getField().setObject(xPosition+x,yPosition+y,this);
			} else {
			    getField().setObject(xPosition+x,yPosition+y,null);
			}
		    }
		}
	}
    }
    
    /* The following arrays describe the shape and color of the various
     * falling bricks in a Tetris game.  Each shape has a corresponding
     * color, so the two arrays must be the same size. Note that the
     * descriptions of the shapes appear rotated from the way they will
     * appear on the playing field.  */
    private static final boolean SHAPES[][][] =
    {
	// XX
	// XX
	{/* square */
	    {true,true},
	    {true,true}
	},
	//  XX
	// XX
	{/* chair */
	    {true,false},
	    {true,true},
	    {false,true}
	},
	// XX
	//  XX
	{/* chair reversed */
	    {false,true},
	    {true,true},
	    {true,false}
	},
	//   X
	// XXX
	{/* L */
	    {true,false},
	    {true,false},
	    {true,true}
	},
	// XXX
	//   X
	{/* L reversed */
	    {false,true},
	    {false,true},
	    {true,true}
	},
	// X
	// X
	// X
	// X
	{/* line */
	    {true,true,true,true}
	},
	//  X
	// XX
	//  X
	{/* pyramid */
	    {false,true,false},
	    {true,true,true}
	}
    };
    private static final Color[] COLORS = {
	Color.red, Color.blue, Color.green, Color.gray, 
	Color.cyan, Color.magenta, Color.yellow
    };
    private static final char[] LETTERS = {'R','B','G','A','C','M','Y'};

    /* A Random number generator to randomly determine what kind of
     * brick to create next.  */
    private static final Random RANDOM = new Random(); 
    private static int nextRandom = 
	Math.abs(RANDOM.nextInt()) % SHAPES.length;
}

