/*
 * Godmar Back	
 *
 * my favorite tetris in Java
 *
 * University of Utah, Computer Systems Lab, 1995
 * major rewrite for 1.1 event model etc.
 * Aug 1999 <gback@pa.dec.com>
 */
import java.awt.*;
import java.awt.event.*;
import java.io.*;

public class Tetris extends java.applet.Applet 
    implements Runnable
{
	int		sqlength;	// length of tetris square
	int		xoffset;	// distance from left
	int		yoffset;	// distance from top
	int		cols;		// columns
	int		rows;		// rows
	int		f[][]; 			// field
	int		of[][]; 		// old field
	int		what;			// type of current piece
	Square		curpiece[] = new Square[4];	// current piece
	boolean		lost;			// lost
	boolean		stopped = true;		// stopped
	boolean 	suspended = false; 	// the suggested off-switch
	boolean		neednewpiece;		// need new piece
	boolean		showScore = true;	// show the score
	Color		colors[];
	int		curval, score=0;
	int		level;			// current level of difficulty
	int		pieces;			// nr of pieces dropped
	String 		highscorehost;
	short  		highscoreport;
	boolean 	blackwhite;		// BW mode 
	boolean 	justupdating = false;


// applet is being loaded the first time
public void init() 
    {	// applet is loaded the first time

	// System.out.println("codebase= " + getCodeBase() +
	// 	"\ndocumentbase= " + getDocumentBase());

	String s = getParameter("squaresize"); 
	sqlength = s == null ? 20 : (new Integer(s)).intValue();

	s = getParameter("columns"); 
	cols = s == null ? 10 : (new Integer(s)).intValue();

	s = getParameter("rows"); 
	rows = s == null ? 20 : (new Integer(s)).intValue();

	s = getParameter("xoffset"); 
	xoffset = s == null ? 200 : (new Integer(s)).intValue();

	s = getParameter("yoffset"); 
	yoffset = s == null ? 0 : (new Integer(s)).intValue();

	s = getParameter("blackwhite"); 
	blackwhite = s == null ? false : (new Boolean(s)).booleanValue();

	s = getParameter("showscore"); 
	showScore = s == null ? true : (new Boolean(s)).booleanValue();

	highscorehost = getParameter("highscorehost"); 

	s = getParameter("highscoreport"); 
	highscoreport = (short)(s == null ? 4711 : (new Integer(s)).intValue());

	f = new int[cols][rows+4];	
	of = new int[cols][rows+4];	

	// resize(sqlength*cols+xoffset*2, sqlength*(rows+3));
	colors = new Color[8];
	// off color
	colors[0] = new Color(40,40,40);

	if (blackwhite) {
	    for(int i=1; i<8; i++)
		colors[i] = new Color(255, 255, 255);
	} else {
	    colors[1] = new Color(255,0,0);      // Default red	- ok
	    colors[2] = new Color(0,200,0);      // green ?
	    colors[3] = new Color(0,200,255);    // light blue 	- ok
	    colors[4] = new Color(255,255,0);    // yellow		- ok
	    colors[5] = new Color(255,150,0);    // orange		- ok
	    colors[6] = new Color(210,0,240);    // purple ?
	    colors[7] = new Color(40,0,240);     // dark blue ?
	}

	for(int i = 0; i < 8; i++) {
	    s = getParameter("color" + i); 
	    if (s != null) {
		// System.out.println("color" + i + "=`" + s + "'");
		colors[i] = Color.decode(s);
	    }
	    // System.out.println(colors[i]);
	}

	addKeyListener(keyListener);
	addMouseListener(mouseListener);
	addComponentListener(componentListener);
    }

// applet is started
public void start() 
    {
	for(int i=0; i<cols; i++)
	    for(int j=0; j<rows+4; j++) {
		f[i][j] = 0; 
		of[i][j] = -1;
	    }
	level = 5;
	score = 0;
	pieces= 0;
	curval = -1;
	neednewpiece = true;
	lost = false;
	stopped = false;

	// spawn gravity thread
        new Thread(this).start();
	requestFocus();
    }

// applet is stopped 
public synchronized void stop() 
    {
	stopped = true;
    }

public String getAppletInfo() 	// Credits
    {         
	return "Tetris!\n" +
		"Written by Godmar Back (gback@cs.utah.edu), 1995, 1999";
    }

// move a bunch of squares
private boolean movepiece(Square from[], Square to[]) 
    {
	int i;
      outerlabel:
	for (i=0; i<to.length; i++) {
	    if (!to[i].InBounds()) {
		return false;
	    }
	    if (f[to[i].x][to[i].y] != 0) {	// it's taken
		for (int j=0; j<from.length; j++)
		    if(to[i].IsEqual(from[j])) {
			continue outerlabel;		// but by a from
		    }
		return false;	// no can do
	    }
	}

	// blank old piece
	for (i=0; i<from.length; i++) 
	    if(from[i].InBounds()) {
		f[from[i].x][from[i].y] = 0;
	    }		

	// mark new piece
	for (i=0; i<to.length; i++) 
	    f[to[i].x][to[i].y] = to[i].c;
	return true;
    }

// throw new piece in the game
private void newpiece() 
    {
	Square  old[] = new Square[4];
	old[0] = old[1] = old[2] = old[3] = new Square(-1, -1, 0);

	int m = cols/2;
	what = (int) (Math.random()*7);

	switch(what) {
	    case 0:
		// XXXX		red
		curval = 100;
		curpiece[0] = new Square(m-1, rows-1, 1);
		curpiece[1] = new Square(m-2, rows-1, 1);
		curpiece[2] = new Square(m,   rows-1, 1);
		curpiece[3] = new Square(m+1, rows-1, 1);
		break;
	    case 1:
		//  X		orange
		// XXX
		curval = 120;
		curpiece[0] = new Square(m  , rows-2, 5);
		curpiece[1] = new Square(m  , rows-1, 5);
		curpiece[2] = new Square(m-1, rows-2, 5);
		curpiece[3] = new Square(m+1, rows-2, 5);
		break;
	    case 2:
		// XX		???
		//  XX
		curval = 180;
		curpiece[0] = new Square(m  , rows-2, 2);
		curpiece[1] = new Square(m-1, rows-1, 2);
		curpiece[2] = new Square(m  , rows-1, 2);
		curpiece[3] = new Square(m+1, rows-2, 2);
		break;
	    case 3:
		//  XX		???
		// XX 
		curval = 180;
		curpiece[0] = new Square(m  , rows-2, 7);
		curpiece[1] = new Square(m+1, rows-1, 7);
		curpiece[2] = new Square(m  , rows-1, 7);
		curpiece[3] = new Square(m-1, rows-2, 7);
		break;
	    case 4:
		//  XX		light blue
		//  XX 
		curval = 100;
		curpiece[0] = new Square(m-1, rows-1, 3);
		curpiece[1] = new Square(m  , rows-1, 3);
		curpiece[2] = new Square(m-1, rows-2, 3);
		curpiece[3] = new Square(m  , rows-2, 3);
		break;
	    case 5:
		//  XXX		
		//    X 
		curval = 120;
		curpiece[0] = new Square(m  , rows-1, 6);
		curpiece[1] = new Square(m-1, rows-1, 6);
		curpiece[2] = new Square(m+1, rows-1, 6);
		curpiece[3] = new Square(m+1, rows-2, 6);
		break;
	    case 6:
		//  XXX		yellow
		//  X  
		curval = 120;
		curpiece[0] = new Square(m  , rows-1, 4);
		curpiece[1] = new Square(m+1, rows-1, 4);
		curpiece[2] = new Square(m-1, rows-1, 4);
		curpiece[3] = new Square(m-1, rows-2, 4);
		break;
	}
	lost = !movepiece(old, curpiece);
    }

// move current piece
private synchronized boolean movecurpiece(int byx, int byy, boolean rotate) 
    {
	Square newpos[] = new Square[4];

	for(int i=0; i<4; i++) {
	    if(rotate) {
		int dx = curpiece[i].x - curpiece[0].x;
		int dy = curpiece[i].y - curpiece[0].y;
		// 90 degree rotation:  [ 0 -1]
		//			[ 1  0]
		newpos[i] = new Square(	curpiece[0].x - dy,
				   	curpiece[0].y + dx,
				   	curpiece[i].c);
	    } else
		newpos[i] = new Square(	curpiece[i].x + byx,
				   	curpiece[i].y + byy,
				   	curpiece[i].c);
	}
	if (!movepiece(curpiece, newpos)) 
	    return false;
	curpiece = newpos;
	return true;
    }

// remove full lines
private void removelines() 
    {
      outerlabel:
	for (int j=0; j<rows; j++) {
	    // check jth line
	    for (int i=0; i<cols; i++)
		if (f[i][j] == 0)
		    continue outerlabel;

	    // remove jth line
	    for (int k=j; k<rows-1; k++)
	        for (int i=0; i<cols; i++)
		    f[i][k] = f[i][k+1];
	    // recheck jth line
	    j--;
	}
    }
// main loop for launched thread
public void run() 
    {
	while (!lost) {
	    if (stopped) {
		return;
	    }

	    synchronized (this) {
		while (suspended) {
		    try {
			wait();
		    } catch (InterruptedException _) {
		    }
		}
	    }
	    try {
		int t = 
		    level == 3 ? 600 :
		    level == 4 ? 500 :
		    level == 5 ? 400 :
		    level == 6 ? 300 :
		    level == 7 ? 250 :
		    level == 8 ? 200 :
		    level == 9 ? 150 :
		    level == 10 ? 100 : 75;
		    ;
		Thread.sleep(t);
	    } catch (InterruptedException e){}
	    if (neednewpiece) {
		if(curval > 0) {
		    score += curval;
		    pieces++;
		    level = 5 + pieces/30;
		}
		removelines();
		newpiece();
		neednewpiece = false;
	    } else {
		neednewpiece = !movecurpiece(0, -1, false);	
		if(!neednewpiece)
		    curval -= 5;
	    }
	    repaint();
	}
	stopped = true;
	if (highscorehost == null)
	    return;

/*
	System.out.println("switching to highscore...");
	try {
	    HighScore hs = new HighScore(this, highscorehost, 
					highscoreport, score);
	} catch(Exception e) {
	    getAppletContext().showStatus("highscore failed: "+e.getMessage());
	} 
*/
	// this terminates second thread
    }

    private static final int RESTART = 1;
    private static final int LEFT = 2;
    private static final int RIGHT = 3;
    private static final int ROTATE = 4;
    private static final int DROP = 5;

// key is pressed
KeyListener keyListener = new KeyAdapter() {
    public void keyPressed(KeyEvent evt)
    {
	int action = -1;

	// System.out.println(evt);

	// write once, run anywhere at its finest.
	// we try to make this program compile under 1.1 and later,
	// so we must hardcode 1.2 constants not known to a 1.1 compiler.
	//
	// However, we want it to run under 1.0 and later on both Sun and Kaffe
	// which all deliver different codes and chars for the same keys
	// on my keyboard.
	//
	int key = evt.getKeyChar();

	// try keychar first
	switch (key) {
	    case 's':
		action = RESTART;
		break;

	    case 'h':		// left
	    case java.awt.Event.LEFT:			// jdk 1.0
		action = LEFT;
		break;

	    case 'k':		// right
	    case 'l':
	    case java.awt.Event.RIGHT:
		action = RIGHT;
		break;

	    case 'j':		// rotate
	    case java.awt.Event.UP:
		action = ROTATE;
		break;

	    case ' ':		// drop
	    case java.awt.Event.DOWN:
		action = DROP;
		break;

	    // try keycode second
	    default:

		switch (evt.getKeyCode()) {

		// on the itsy, that's the left blue button
		case java.awt.event.KeyEvent.VK_ENTER:
			action = RESTART;
			break;

		// should work with and without numlock key
		case java.awt.event.KeyEvent.VK_LEFT:		   // jdk 1.1
		case 226: /* java.awt.event.KeyEvent.VK_KP_LEFT */ // jdk 1.2
		case java.awt.event.KeyEvent.VK_NUMPAD4:
			action = LEFT;
			break;

		case java.awt.event.KeyEvent.VK_RIGHT:
		case 227: /* java.awt.event.KeyEvent.VK_KP_RIGHT */
		case java.awt.event.KeyEvent.VK_NUMPAD6:
			action = RIGHT;
			break;
		
		case java.awt.event.KeyEvent.VK_UP:
		case 224: /* java.awt.event.KeyEvent.VK_KP_UP */
		case java.awt.event.KeyEvent.VK_NUMPAD8:
			action = ROTATE;
			break;

		case java.awt.event.KeyEvent.VK_DOWN:
		case 225: /* java.awt.event.KeyEvent.VK_KP_DOWN */
		case java.awt.event.KeyEvent.VK_NUMPAD2:
			action = DROP;
			break;

		default:
			return;
		}
		break;
	}

	// allow only restart if game lost or suspended
	if (action != RESTART && (lost || suspended))
	    return;

	switch(action) {
	    case RESTART:
		stop();		// stop old game
		start();	// start new game
		break;

	    case LEFT:
		curval -= 5;
		movecurpiece(-1, 0, false);
		// we must set neednewpiece to false in case something
		// opened up so that a stuck piece got unstuck
		// otherwise, pieces may end up in the air
		//
		// this allows a lot of movement on the bottom line
		// even after a piece has reached the bottom line.  I like it.
		// 
		neednewpiece = false;
		repaint();
		break;

	    case RIGHT:
		curval -= 5;
		movecurpiece(1, 0, false);
		neednewpiece = false;
		repaint();
		break;

	    case ROTATE:
		if(what!=4) {
		    curval -= 5;
		    movecurpiece(0, 0, true);
		    repaint();
		}
		break;

	    case DROP:
		while(movecurpiece(0, -1, false))
			;
		// if you omit the next line, even dropped pieces
		// can be moved.
		neednewpiece = true;
		repaint();
		break;
	}
    }
};

public Dimension getPreferredSize() {
    return (new Dimension(xoffset + cols * sqlength, 
			  yoffset + rows * sqlength));
}

// we need to override this method, otherwise repaint()
// will erase the whole screen, causing tremendous flickering
public void update(Graphics g) 
    {
	justupdating = true;
	// tell paint to draw only stuff that has changed
	paint(g);
    }


// paint graphics on screen
public synchronized void paint(Graphics g) 
    {
	if (showScore) {
	    g.setFont(new java.awt.Font("Helvetica", 0, 18));
	    int  gx = sqlength;
	    int  gy = sqlength*rows/4;

	    g.clearRect(gx, gy-25, xoffset-gx, 25);
	    g.drawString("Score: " + score, gx, gy);

	    gy += 30;
	    g.clearRect(gx, gy-25, xoffset-gx, 25);
	    g.drawString("Level: " + level, gx, gy);
	}

	for (int i=0; i<cols; i++)
	    for (int j=0; j<rows; j++) {
		if (!justupdating ||
		    of[i][rows-1-j] == -1 ||
		    of[i][rows-1-j] != f[i][rows-1-j]) {
		    	g.setColor(colors[f[i][rows-1-j]]);
		    	g.fill3DRect(
				xoffset+sqlength*i, 
				yoffset+sqlength*j, 
			    	sqlength, sqlength, true);
		}
		of[i][rows-1-j] = f[i][rows-1-j];
	    }
	justupdating = false;
    }

// in kaffe's appletviewer, we must redefine this or else we won't 
// get the focus at all.   Despite of that, we have to rerequest the
// focus in mouseClick or else we don't regain it

public boolean isFocusTraversable() {
	return (true); 
}

// in Sun's appletviewer, it's a bit more complicated:
// we must request the focus explicitly.  However, we can't do that in
// start() or init() because it is ineffective while a component is
// not visible
//
// Interestingly, the spec says that componentShown events are only
// delivered to toplevel windows, not to a component when merely one
// of its anchestors becomes visible.  Fortunately, Sun's JDK doesn't
// follow its own spec so we have a chance to request the focus.
// 
// The more I learn about the inner workings of the awt, the more
// I want to puke.

    ComponentListener componentListener = new ComponentAdapter() {
	public void componentShown(ComponentEvent evt) {
	    ((Component)evt.getSource()).requestFocus();
	}
    };

    MouseAdapter mouseListener = new MouseAdapter() {
	public void mouseClicked(MouseEvent evt)
	{
	    if (stopped)
		return;

	    requestFocus();
	    synchronized (Tetris.this) {
		suspended = !suspended;
		Tetris.this.notify();
	    }
	}
    };


class Square {
	int	x, y, c;

Square(int x, int y, int c) {
	this.x = x;
	this.y = y;
	this.c = c;
    }

boolean InBounds() {
	return (x >= 0 && x < cols && 
		y >= 0 && y < rows+4);
    }

boolean IsEqual(Square s) {
	return x == s.x && y == s.y && c == s.c;
    }
}
} // end Tetris applet

