//package uk.ac.lancs.unix.cotton.tetris;
import java.applet.*;
import java.awt.*;
import java.net.*;
import java.io.*;
import java.awt.event.*;

/**
 * Another rip-off of the popular game Tetris.
 */
public class BlockDrop extends Applet implements AlarmUser, KeyListener
{
  final int WIDTH=10;
  final int HEIGHT=20;
  final Color[] BLOCK_COLOR = {new Color(0xffffff), new Color(0x0000cc),
                               new Color(0x00cc00), new Color(0xcc0000),
                               new Color(0x00aaaa), new Color(0xaa00aa),
                               new Color(0xaaaa00), new Color(0x000000),  
                            };
                            // BLOCK_COLOR[0] is used as the background
  final int NO_OF_COLORS = BLOCK_COLOR.length-1;
  final int RAISE_COLOR  = 7;

  final int SHAPE[][][]	= {	{{0,0},{ 0,1},{ 1,0},{ 1,1}},  //Square
                                {{0,0},{ 2,0},{ 1,0},{ 0,1}},  //L
				{{0,0},{-2,0},{-1,0},{ 0,1}},  //Inverted L
                                {{0,0},{-2,0},{-1,0},{ 1,0}},  //Line
				{{0,0},{ 0,1},{ 1,1},{ 1,2}},  //\
				{{0,0},{ 0,1},{-1,1},{-1,2}},  ///
				{{0,0},{-1,0},{ 1,0},{ 0,1}}   //T
                          };
  final int NO_OF_SHAPES = SHAPE.length;
  final int NO_OF_ROTATIONS=4;
  final int TETRA=SHAPE[0].length;

  /**
   * A lot of methods don't want another method
   * shifting the block on them.
   */
  Object blockMoveMutex = new Object();
  
  int blocks[][];		//blocks at the bottom of the pit
  int score;
  int hiscore;
  boolean pressToStart;

  /**
   * Details of the currently-dropping block.
   */
  int currentBlockX[];
  int currentBlockY[];
  int currentBlockColor;
  int currentShape;
  int currentRotation;

  /**
   * Networking options (multiplayer).
   */
  boolean netgame=false;
  String netgame_host = null;	//no, there isn't a server for
  String netgame_port = null;	//this at the moment
  Socket netsocket;
  DataInputStream netin;
  DataOutputStream netout;
  
  /**
   * More multiplayer-related things.
   */
  int raiseBy;			//number of penalty lines
  int blankColumn;		//which penalty column is blank
  
  /**
   * Timer for the block to drop a line.
   */
  Alarm dropAlarm;

  java.util.Random random=new java.util.Random();

/* *************************************************************
 * End of variables. start of methods.
 * ************************************************************/



  //ugly, gets used for the inner class of the next method
  BlockDrop blockDrop;
  /**
   * Initialize the applet.
   * Set up the background color.
   */
  public void init()
  {
    setBackground(BLOCK_COLOR[0]);
 
    addKeyListener(this);

    blockDrop = this;

    /**
     * Grab the keyboard focus when the mouse enters
     */
    addMouseListener(new MouseAdapter()
    {
      public void mouseEntered(MouseEvent e)
      {
	blockDrop.requestFocus();
      }
    });
  }

  /**
   * More initialization.  The network socket is opened here, so that unused
   * clients don't hog the server.
   * Also, the timer to drop the blocks asynchronously is started.
   *
   * Stopping the dropAlarm in this.stop(), then restarting the
   * old one causes problems, so we'll stop the old one, and
   * start a new one.
   */
  public void start()
  {
    pressToStart=true;
    repaint();

    if (dropAlarm != null )
    	dropAlarm.stop();
    dropAlarm=new Alarm(2000, this);
    dropAlarm.start();
    
    requestFocus();
    /**
     * Netgame stuff
     */
    if(netgame)
    {
      try
      {
        netsocket=new Socket(InetAddress.getByName(netgame_host),
		Integer.valueOf(netgame_port).intValue());
        netin =new DataInputStream(netsocket.getInputStream());
        netout=new DataOutputStream(netsocket.getOutputStream());
      }catch(Exception e)
      {
        e.printStackTrace();
        netgame = false;
      }
    }
  }

  /**
   * Stop.  Halt the asynchronous timer, and close any network connection.
   */
  public void stop()
  {
    dropAlarm.stopAlarming();
    if(netgame)
    {
      try
      {
        netin.close();
        netout.close();
        netsocket.close();
      }catch(Exception e)
      {
        e.printStackTrace();
      }
    }

  }

  /**
   * Start a new game.
   * If it is a multiplayer match, synchronise the psuedo-random numbers.
   * Create an array for the placed-block data.
   * Call newShape() to generate a new shape.
   */
  public void startGame()
  {
    if(netgame)
      try
      {  
        random.setSeed(Long.parseLong(netin.readLine()));
      }catch(Exception e){e.printStackTrace();}

    blocks=new int[WIDTH][];
    for(int x=0; x<WIDTH; x++)
    {
      blocks[x]=new int[HEIGHT];
    }

    blankColumn=(Math.abs(random.nextInt()))%WIDTH;
    currentBlockX=new int[TETRA];
    currentBlockY=new int[TETRA];
    newShape();
    score=0;
  }

  /**
   * Paint the applet.
   * Either put up a "Any key to start" message, or draw all the placed blocks
   * then call paintCurrentBlock() to draw the decending block.
   *
   * Really the game and scorescreen should be done with one or two
   * Panels, instead of the pressToStart trick that's spread over
   * too many methods of this class.
   */
  public void paint(Graphics g)
  {
    if(!pressToStart)
    {
      for(int x=0; x<WIDTH; x++)
      {
        for(int y=0; y<HEIGHT; y++)
        {
          if(blocks[x][y]!=0)
          {
            g.setColor(BLOCK_COLOR[blocks[x][y]]);
            g.fillRect(x*10, y*10, 10, 10);
          }
        }
      }
      paintCurrentBlock(g);
    }
    else	//Waiting for someone to hit <Any>
    {
      if(hiscore>0)
        g.drawString("Hiscore: "+hiscore+"00", 0, 50);
      g.drawString("Any key to start", 0, 100);
      if(score>0)
        g.drawString("Last score: "+score+"00", 0, 150);
    }

   //Now paint a border
   paintBorder(g);

 }

  /**
   * Paint the descending block.
   */
  public void paintCurrentBlock(Graphics g)
  {
    synchronized(blockMoveMutex)
    {
      g.setColor(BLOCK_COLOR[currentBlockColor]);
      for(int i=0; i<TETRA; i++)
      {
        g.fillRect(currentBlockX[i]*10, currentBlockY[i]*10, 10, 10);
      }
    }
  }

  /**
   * Paint a border.
   */
  public void paintBorder(Graphics g)
  {
    Dimension size = getSize();
    g.setColor(Color.black);
    g.drawLine(0, 0, size.width - 1, 0);
    g.drawLine(0, 0, 0, size.height - 1);
    g.drawLine(size.width - 1, 0, size.width - 1, size.height - 1);
    g.drawLine(0, size.height - 1, size.width - 1, size.height - 1);
  }

  /**
   * Handle all key presses.
   * If it's waiting for someone to hit the Any key, then call startGame(),
   * else move the current block as described in the keys documentation.
   *
   * The documentation is the tetris.html file.
   *
   * Updated for the new AWT model.
   */
  public void keyPressed(java.awt.event.KeyEvent e)
  {
    if(pressToStart)
    {
      pressToStart=false;
      startGame();
      return;
    }
    switch(Character.toLowerCase(e.getKeyChar()))
    {
      //The q key is being used as a debug switch for the multiplayer game
      case 'q':
      {
//          raiseBy++;
        break;
      }
      case 'a':  //down one
      {
        synchronized(blockMoveMutex)
        {
          if(canMove(0, +1))
            move(0, +1);
          else
            landed();
	}
        break;
      }
      case 'o':  //left
      case 'r':
      {
        synchronized(blockMoveMutex)
        {
          if(canMove(-1, 0))
            move(-1, 0);
	}
        break;
      }
      case 'p':  //right
      case 'l':
      {
        synchronized(blockMoveMutex)
        {
          if(canMove(+1, 0))
            move(+1, 0);
	}
        break;
      }
      case ' ':  //drop
      {
        synchronized(blockMoveMutex)
        {
          while(canMove(0, +1))
          {
            move(0, +1);
          }
          landed();
	}
        break;
      }
      case ']':  //clockwise
      case '=':
      {
        rotateCurrentShape(-1);
        repaint();
        break;
      }
      case '[':  //anticlockwise
      case '/':
      {
        rotateCurrentShape(1);
        repaint();
        break;
      }
    }
  }

  /**
   * We're not interested in keyTyped...
   */
  public void keyTyped(KeyEvent e)
  {
  }

  /**
   * ...and we're not interested in keyReleased.
   */
  public void keyReleased(KeyEvent e)
  {
  }

  
  /**
   * Rotate the descending shape.
   * @param rotateBy The amount to rotate by. 1 == 90degrees counterclockwise
   */
  public void rotateCurrentShape(int rotateBy)
  {
    synchronized(blockMoveMutex)
    {
      int temp=currentRotation;
      currentRotation+=rotateBy;
      if(currentRotation<0)
        currentRotation=NO_OF_ROTATIONS-1;
      if(currentRotation>=NO_OF_ROTATIONS)
        currentRotation=0;
      setCurrentShape();
      if(!canMove(0, 0))
      {
        currentRotation=temp;
        setCurrentShape();
      }
    }
  }

  /**
   * Timer-controlled routine to drop the block.
   */
  public void alarmCall()
  {
    if(!pressToStart)
    {
      synchronized(blockMoveMutex)
      {
        if(canMove(0, +1))
          move(0, +1);
        else
          landed();
      }
    }
  }


  /**
   * Time to place the descending block on the landscape,
   * check for completed lines and put another block at the top.
   *
   * This doesn't have to be synchronized with the
   * blockMoveMutex - it is only called when the block has
   * landed, so nothing else will move it.
   */
  public void landed()
  {
    for(int i=0; i<TETRA; i++)
      blocks[currentBlockX[i]][currentBlockY[i]]=currentBlockColor;

    /*
     * Check for full lines and remove them.
     * Beginning at the bottom, run each row through this:
     * <ul>
     * <li>If there are no blanks in the line, increment
     * <code>lowerBy</code> and set <code>rmLine</code>.</li>
     * <li>If the line is being rechecked, and <code>rmLine</code>
     * is unset, skip to the next line.</li>
     * <li>Make this line a copy of the line <code>lowerBy</code> above.</li>
     * <li>Clear the line <code>lowerBy</code> above.</li>
     * <li>If the line has been removed, set <code>repeatedLine</code>
     * and decrement <code>i</code> so the new line is checked.</li>
     * <ul>
     */
    int lowerBy=0;
    boolean repeatedLine=false;
    for(int i=HEIGHT-1; i>0; i--)
    {
      boolean rmLine=true;
      for(int j=0; j<WIDTH; j++)
        if(blocks[j][i]==0)
        {
           rmLine=false;
           break;
        }
      if(rmLine)
        lowerBy++;
      if(rmLine || !repeatedLine)
      {
        if(lowerBy!=0 && i-lowerBy>=0)
          for(int j=0; j<WIDTH; j++)
          {
            blocks[j][i]=blocks[j][i-lowerBy];
            blocks[j][i-lowerBy]=0;
          }
        else if(i-lowerBy<0)	// implies lowerBy != 0
          for(int j=0; j<WIDTH; j++)
          {
            blocks[j][i]=0;
          }
      }
      if(rmLine)
      {
        i++;
        repeatedLine=true;
      }
      else
        repeatedLine=false;
    }
    score+=lowerBy*lowerBy;

    /*
     * Tell the server how many lines were removed.
     */
    if(netgame)
    {
      try
      {
        netout.writeChars(""+lowerBy+"\n");
      }catch(Exception e){e.printStackTrace();}
    }

    /*
     * Add lines at the bottom.
     * This is for a multiplayer game, after the other
     * player has given you some lines.
     */
    if(raiseBy!=0)
    {
      for(int i=0; i<HEIGHT; i++)
      {
        if(i+raiseBy<HEIGHT)
        {
          for(int j=0; j<WIDTH; j++)
            blocks[j][i]=blocks[j][i+raiseBy];
        }
        else
        {
          for(int j=0; j<WIDTH; j++)
            blocks[j][i]=RAISE_COLOR;
          blocks[blankColumn][i]=0;
        }
      }
      raiseBy=0;
    }

    /*
     * And finally
     */
    newShape();
  }

  /**
   * Place a randomly-chosen shape in a random rotation with
   * a random color at the top of the screen.
   */
  public void newShape()
  {
    currentBlockX[0]=WIDTH/2;
    currentBlockY[0]=2;
    currentShape=((Math.abs(random.nextInt()))%NO_OF_SHAPES);
    currentRotation=((Math.abs(random.nextInt()))%NO_OF_ROTATIONS);
    currentBlockColor=((Math.abs(random.nextInt()))%NO_OF_COLORS)+1;
    setCurrentShape();
    repaint();

    /*
     * Check that the space that we're putting the new block in is empty,
     * game over if it isn't.
     *
     * Probably should wait a while before starting a new game, as
     * it's too easy to start a new one at the moment.
     */
    if(!canMove(0, 0))
    {
      pressToStart=true;
      if(score>hiscore)
        hiscore=score;
      repaint();
    }
  }

  /**
   * Given that the instance variables currentShape and currentRotation
   * are defined, create the data for the 4 little blocks that make up
   * one big block.
   */
  private void setCurrentShape()
  {
    synchronized(blockMoveMutex)  //possibly unnecessary
    {
      for(int i=1; i < TETRA; i++)
      {
        switch(currentRotation)
        {
          case 0:
          {
            currentBlockX[i]=currentBlockX[0]+SHAPE[currentShape][i][0];
            currentBlockY[i]=currentBlockY[0]+SHAPE[currentShape][i][1];
            break;
          }
          case 1:
          {
            currentBlockX[i]=currentBlockX[0]+SHAPE[currentShape][i][1];
            currentBlockY[i]=currentBlockY[0]-SHAPE[currentShape][i][0];
            break;
          }
          case 2:
          {
            currentBlockX[i]=currentBlockX[0]-SHAPE[currentShape][i][0];
            currentBlockY[i]=currentBlockY[0]-SHAPE[currentShape][i][1];
            break;
          }
          case 3:
          {
            currentBlockX[i]=currentBlockX[0]-SHAPE[currentShape][i][1];
            currentBlockY[i]=currentBlockY[0]+SHAPE[currentShape][i][0];
            break;
          }
	}
      }
    }
  }

  /**
   * Collision detection.
   * True if the current shape can move to the given relative position.
   *@param xinc number of spaces right
   *@param yinc number of spaces up
   */
  public boolean canMove(int xinc, int yinc)
  {
    synchronized(blockMoveMutex)
    {
      for(int i=0; i<TETRA; i++)
      {
        if(currentBlockY[i] + yinc > HEIGHT-1
         ||currentBlockX[i] + xinc < 0
         ||currentBlockX[i] + xinc > WIDTH-1
         ||
           (currentBlockX[i]+xinc  >=0                   //this catchs an 'oversize' block going off the top of the screen
          && currentBlockY[i]+yinc >=0
          && blocks[currentBlockX[i]+xinc][currentBlockY[i]+yinc]!=0)
           )
          return false;
      }
    }
    return true;
  }

  /**
   * Moves the block to the given relative position, and redraws it.
   * To minimise the repaint overhead and flicker, it uses this technique:
   * <ul>
   * <li>Set the current block color to the background colour</li>
   * <li>Paint block</li>
   * <li>Set the current block color to the block's colour</li>
   * <li>Move block</li>
   * <li>Paint block</li>
   * </ul>
   *@param xinc number of spaces right
   *@param yinc number of spaces up   
   */
  public void move(int xinc, int yinc)
  {
    Graphics g=getGraphics();
    synchronized(blockMoveMutex)
    {
      int tempColor=currentBlockColor;
      currentBlockColor=0;
      paintCurrentBlock(g);
      currentBlockColor=tempColor;

      for(int i=0; i<TETRA; i++)
      {
        currentBlockX[i]+=xinc;
        currentBlockY[i]+=yinc;
      }
      paintCurrentBlock(g);
      paintBorder(g);
    }
    g.dispose();
  }
}

