/*******************************************************************************
 FILE         :   TennisApplet.java 

 COPYRIGHT    :   DMALEX, 1999

 DESCRIPTION  :   Funny tennis 

 PROGRAMMED BY:   Alex Fedosov

 CREATION DATE:   05/11/1999

 LAST UPDATE  :   19/11/1999
*******************************************************************************/

import java.awt.*;
import java.applet.*;
import java.util.*;
import java.net.*;
import java.io.*;


// base class for objects which we can move
abstract class MoveableObject
{
   MoveableObject(Dimension _size, Dimension _CourtWidth, int _posX, int _posY)
   {
      CourtWidth = _CourtWidth;
      size       = _size;
      pos        = new Point(_posX, _posY);
      origPos    = new Point(_posX, _posY);
   }
   
   // here we rule over the object's movement
   public abstract void direct(int direction); 
   public abstract void show(Graphics g);
   
   // restore starting position
   public void reset(Graphics g) 
   {
      pos.setLocation(origPos);
      show(g);
   }

   // check for collisions
   public boolean isIntersect(MoveableObject o)
   {
      return pos.x + size.width >= o.pos.x && 
             pos.x <= o.pos.x + o.size.width;
   }
      
   Dimension CourtWidth; // game area
   Dimension size;       // object size  
   Point     pos;        // current object position
   Point     origPos;    // starting position
}


// you know what does 'Racket' means in the game, eh? :-)
// so it's just self-descriptive class => no comments
class Racket extends MoveableObject
{
   Racket(Dimension _size, Dimension _CourtWidth, int _posX, int _posY)
   {
      super(_size, _CourtWidth, _posX, _posY);
      deltaX = 0;
   }
   
   public void reset(Graphics g)
   {
      deltaX = 0;
  		g.clearRect(pos.x-1, pos.y, size.width+2, size.height);
      super.reset(g);
   }
   
   public void show(Graphics g)
   {
      if(deltaX == 0)
         {
  		   g.fillRect(pos.x, pos.y, size.width, size.height);
         return;
         }
      
      if(deltaX > 0)
         {
         // reduce time refresh but increase video-irregularities  ;-(
		   // g.fillRect(pos.x+size.width, pos.y, 1, size.height);
		   g.fillRect(pos.x+1, pos.y, size.width, size.height);
		   g.clearRect(pos.x, pos.y, 1, size.height);
         }
      else
         {
		   // g.fillRect(pos.x, pos.y, 1, size.height);
		   g.fillRect(pos.x, pos.y, size.width, size.height);
		   g.clearRect(pos.x + size.width, pos.y, 1, size.height);
         }
      
      pos.x += deltaX;
      if(pos.x + size.width >= CourtWidth.width || pos.x <= 0)
         {
         deltaX = -deltaX;
         pos.x += deltaX;
         }
   }
   
   public void direct(int direction)
   {
      deltaX = direction;
   }
   
   int deltaX;
}



// just self-descriptive class => no comments
class Ball extends MoveableObject
{
   Ball(Dimension _size, Dimension _CourtWidth, int _posX, int _posY, boolean _fSavePos)
   {
      super(_size, _CourtWidth, _posX, _posY);
      delta    = new Point(0, 0);
      randGen  = new Random(0);
      fSavePos = _fSavePos;
      savePos  = new Point(_posX, _posY);
   }

   int getNextRand()
   {
      // from range -2...2 except 0
      int r = (int)(randGen.nextFloat() * 3) + 1;
      if(r > 2)
         r = r - 5;
      
      return r;
   }
   
   public void direct(int direction)
   {
      if(direction == 0)
         delta = new Point(getNextRand(), getNextRand());
      else
         {
         delta.y = -delta.y;
         // adjust speed
         if(Math.abs(delta.x) == direction)
            delta.x = direction == 1 ? delta.x*2 : delta.x/2;
         }
   }
   
   public void reset(Graphics g)
   {
      if(fSavePos)
         return;
      
      delta = new Point(0, 0);
      g.clearRect(pos.x, pos.y, size.width, size.height);
      super.reset(g);
   }
   
   public void show(Graphics g)
   {
      if(fSavePos) 
         {
         // in the netgame we show the ball based on data from server side
         if(savePos.x != pos.x)
            g.clearRect(savePos.x, savePos.y, size.width, size.height);
         g.fillOval(pos.x, pos.y, size.width, size.height);
         savePos.x = pos.x;
         savePos.y = pos.y;
         return;
         }
      
      if(delta.x == 0 || delta.y == 0)
         {
         // slow down refresh  ;-(
         // g.setColor(Color.red);
         g.fillOval(pos.x, pos.y, size.width, size.height);
         return;
         }

      g.clearRect(pos.x, pos.y, size.width, size.height);
      
      pos.x += delta.x;
      pos.y += delta.y;
      
      if(pos.x + size.width >= CourtWidth.width || pos.x <= 0)
         delta.x = -delta.x;
      
      g.fillOval(pos.x, pos.y, size.width, size.height);
   }
   
   Point   delta;
   Random  randGen;
   boolean fSavePos; // should we save previous position of the ball?
   Point   savePos;  // previous position
}


// main class
class CourtView extends Canvas implements Runnable
{
   Dimension CourtSize  = new Dimension(230, 300);
   Dimension racketSize = new Dimension(40, 5);
   Dimension ballSize   = new Dimension(6, 6);
   
   Racket playerOne = new Racket(racketSize, CourtSize, CourtSize.width/2 - racketSize.width/2, CourtSize.height - racketSize.height);
   Racket playerTwo = new Racket(racketSize, CourtSize, CourtSize.width/2 - racketSize.width/2, 0);
   Ball   ball      = new Ball(ballSize, CourtSize, CourtSize.width/2, CourtSize.height/2, false);
   
   NetSettingsDialog nDlg = null;  // dialog of the net-tuning
   ServerSocket srv       = null;  // server (==null at the client side)
   Socket       socket    = null;  // current connection
   OutputStream os        = null;  // data stream
   InputStream  is        = null;  // data stream
   int          fState    = 0;     // 0 - no requests, 1 - request for single play, 2 - request for netgame
   boolean      isNetGame = false; // what type of the game do we play now?
      
   int    delay = 0;       // wait for nothing 'delay' msec
   int    playerOnePoints; // score for 'our' side
   int    playerTwoPoints; // score for 'opponent' side
   Label  scoreBoard;      // score-board

   
	CourtView(Label _scoreBoard)
	{
      scoreBoard = _scoreBoard;
	   resize(CourtSize);
		setBackground(Color.cyan);                  
	}
	
   public synchronized void Delay(int _delay)
   {
		try{
         switch(_delay)
            {
            case -1: 
               Thread.sleep(1500); 
               break;
               
            case 0:                              
               Thread.sleep(delay/10);
               // strange operation because of strange Thread.sleep(5)
               // behavior with magic parameter's value = 5
               for(int i = 0, j = 0; i < 10000 * delay; i++)
                  j++;
               break;
               
            default:
               delay = _delay;
               break;
            }
         }
		catch(Exception e)
			{
			}
   }
   
   
   void showMsg(String msg)
   {
      scoreBoard.setText(msg);
      Delay(-1); // generally user has very imperfect vision organs :-)
   }
   

   // here is the dedicated bug place! 
   // I should use special synchronized class instead fState variable
   // but I have no time and motivation to do this...
   // ...all the more it still works and main programmers rule is:
   // "if it works don't touch it!" :-)
   public synchronized void start(boolean _isNetGame)
   {
      // what this capricious user wanna now? :-)
      fState = _isNetGame ? 2 : 1;
   }

   
   void closeConnection()
   {
      try{         
         if(os != null)
            {
            os.close();
            os = null;
            }
                  
         if(is != null)
            {
            is.close();
            is = null;
            }
               
         if(socket != null)
            {
            socket.close();            
            socket = null;
            }
               
         if(srv != null)
            {
            srv.close();
            srv = null;
            }
         }
      catch(Exception e)
         {
         os = null;
         is = null;
         socket = null;
         srv = null;
         }
   }
   
   
   boolean selectNetGame()
   {     
      if(nDlg == null)
         nDlg = new NetSettingsDialog();
      
      nDlg.show();
      if(nDlg.fCancel)
         return false;
      
      try{         
         closeConnection();
         showMsg("Looking for server...  ");
         socket = new Socket(InetAddress.getByName(nDlg.txtIPAddr.getText()), Integer.valueOf(nDlg.txtIPPort.getText()).intValue());
         os = socket.getOutputStream();
         is = socket.getInputStream();
         ball = new Ball(ballSize, CourtSize, CourtSize.width/2, CourtSize.height/2, true);         
         }
      catch(Exception e)
         {
         // so I'm server!
         try{            
            showMsg("Server not found. Create new server.");
            srv = new ServerSocket(Integer.valueOf(nDlg.txtIPPort.getText()).intValue(), 5);
            socket = srv.accept();
            os = socket.getOutputStream();
            is = socket.getInputStream();
            ball = new Ball(ballSize, CourtSize, CourtSize.width/2, CourtSize.height/2, false);
            }
         catch(Exception x)
            {
            closeConnection();
            showMsg("Network problems. Tune security in your browser");
            // "Tune security, tune security....." 
            // .... or just force me to do 'signed applet'! :-)
            return false;
            }
         }            
      return true;
    }
                                  
   public void reset(Graphics g)
   {     
      g.clearRect(0, 0, CourtSize.width, CourtSize.height);
      ball.reset(g);
      playerOne.reset(g);
      playerTwo.reset(g);
      showMsg("You: " + String.valueOf(playerOnePoints) + 
              " Opponent: " + String.valueOf(playerTwoPoints));
   }
   
	public synchronized void paint(Graphics g)
	{
      switch(fState)
         {
         case 0: break;
         case 1:
            {
            fState = 0;
            isNetGame = false;
            closeConnection(); // to release other side
            ball = new Ball(ballSize, CourtSize, CourtSize.width/2, CourtSize.height/2, false);
            showMsg("Single play against computer");
            playerTwoPoints = playerOnePoints = 0;
            reset(g);
            break;
            }
            
         case 2:
            {
            fState = 0;
            if(!selectNetGame()) 
               break;
            
            isNetGame = true;
            playerTwoPoints = playerOnePoints = 0;
            reset(g);
            break;
            }
         }
      
     
      if(isNetGame)
         dataExchange();
      else
         // computer's AI very simple and human-loved.... in the meanwhile :-)
         if(ball.pos.x > playerTwo.pos.x + playerTwo.size.width/2)
            playerTwo.direct(1);
         else
            playerTwo.direct(-1);
      
      // lady... sorry... player (server especially) first! :-)
      if(ball.delta.x == 0 && playerOne.deltaX != 0 && ((isNetGame && srv != null) || !isNetGame))
         ball.direct(0);

      // time to win? ...or lose? 
      boolean fBallAtPlayerTwoSide = ball.pos.y <= playerTwo.size.height + 1;
      if(fBallAtPlayerTwoSide || ball.pos.y + ball.size.height >= ball.CourtWidth.height - playerOne.size.height - 1)
         if(ball.isIntersect(fBallAtPlayerTwoSide ? playerTwo : playerOne))
            if(fBallAtPlayerTwoSide  && ball.delta.x *  playerTwo.deltaX > 0 ||
               !fBallAtPlayerTwoSide && ball.delta.x *  playerOne.deltaX > 0)
               ball.direct(1); // speed up
            else
               ball.direct(2); // speed down
         else
            {
            checkScore(fBallAtPlayerTwoSide);
            reset(g);
            }

      // just refresh
      ball.show(g); 
      playerOne.show(g);
      playerTwo.show(g);
	}

   
   void checkScore(boolean playerPoint)
   {
      if(playerPoint)
         playerOnePoints++;
      else
         playerTwoPoints++;
            
      if(playerOnePoints >= 20)
         {
         showMsg("You are winner!");
         playerTwoPoints = playerOnePoints = 0;
         return;
         }
      else               
         if(playerTwoPoints >= 20)
            {
            showMsg("You are loser!");
            playerTwoPoints = playerOnePoints = 0;
            return;
            }      
   }
   
   // reload the function to prevent undesirable view refresh
	public synchronized void update(Graphics g)
	{
      paint(g);
	}
	
	public synchronized void proceedKey(int key)
	{
		switch(key)
		   {
		   case Event.LEFT:  playerOne.direct(-1); break;
		   case Event.RIGHT: playerOne.direct(1);  break;
		   }
	}	
   
   // we can transfer the data byte for byte over the net but our view 
   // area size could exceed 256 size limit! so we need data converter.
   int bytes2coord(byte one, byte two)
   {
      return (one+127) * 255 + two + 127;
   }
   
   Point coord2bytes(int coord)
   {
      Point ret = new Point();
      ret.x = coord / 255;
      ret.y = coord - ret.x * 255;
      ret.x -= 127;
      ret.y -= 127;
      return ret;
   }
   
   synchronized void dataExchange()
   {
		try{
         if(srv == null)
            {
            // I'm client
            byte buf[] = new byte[1];
            buf[0] = (byte)playerOne.deltaX;
            os.write(buf);
                          
            buf = new byte[5];
            is.read(buf); 
            playerTwo.direct(-buf[0]);
            // I don't like 'getter/setter' here :-)
            ball.pos.x = ball.CourtWidth.width  - ball.size.width  - bytes2coord(buf[1], buf[2]);
            ball.pos.y = ball.CourtWidth.height - ball.size.height - bytes2coord(buf[3], buf[4]);
            }
         else
            {
            // I'm server
            byte buf[] = new byte[1];
            is.read(buf); 
            playerTwo.direct(-buf[0]);
                     
            buf = new byte[5];
            buf[0] = (byte)(playerOne.deltaX);
            buf[1] = (byte)coord2bytes(ball.pos.x).x;
            buf[2] = (byte)coord2bytes(ball.pos.x).y;
            buf[3] = (byte)coord2bytes(ball.pos.y).x;
            buf[4] = (byte)coord2bytes(ball.pos.y).y;
            os.write(buf);
            }
         }
		catch(Exception e)
			{
         closeConnection();
         showMsg("Some problem during data transfer.");
         fState = 1;
			}
}

	public void run()
	{
		try{
         // main loop
			while(true)
			   {
				this.repaint(); 
            Delay(0);            
            }
         }
		catch(Exception e)
			{
         showMsg("Sorry, there's some software problem. Bye.");
			}
	}
}


// to bring speed of the computer into accord with human's abilities
class speedRegulator extends Panel
{
   final int maxDelay   = 100;
   Scrollbar scrlSpeed  = new Scrollbar(Scrollbar.HORIZONTAL, 1, 10, 0, maxDelay + 10);
   Label     valueSpeed = new Label();
   CourtView cView;
   int       width;
   int       height;

   speedRegulator(CourtView _cView, int _width, int _height) 
   {
      setLayout(new GridLayout(1, 2));
      add(scrlSpeed);
      add(valueSpeed);
      cView  = _cView;
      width  = _width;
      height = _height;
      
      // speed calibration (roughly)
      long msecBefore = System.currentTimeMillis();
      for(int i = 0; i < 10000000; i++) height = _height;
      long msecAfter = System.currentTimeMillis();
      
      if(msecAfter <= msecBefore)
         scrlSpeed.setValue(maxDelay); // max value
      else      
         scrlSpeed.setValue(Math.max((80 - ((int)(msecAfter - msecBefore))/20), 0));
      
      set();
   }
   
   void set() 
   {
      valueSpeed.setText("Speed delay: " + String.valueOf(scrlSpeed.getValue()));
      cView.Delay(scrlSpeed.getValue());
   }
   
	public boolean handleEvent(Event e) 
	{		
      if(e.target.equals(scrlSpeed))
         {
         set();
   		return true;
         }
      
      return super.handleEvent(e);
   }
   
	public Dimension preferredSize()
   {
      return new Dimension(width, height);
   }
}



// wanna play with the REAL opponent? Select it!
class NetSettingsDialog extends Dialog
{
   Button btnOK        = new Button("  OK  ");
   Button btnCancel    = new Button("Cancel");
   TextField txtIPAddr = new TextField("1.1.1.1");
   TextField txtIPPort = new TextField("8000");
   boolean fCancel     = true;
   
   NetSettingsDialog()
   {
      super(new Frame(), "Network game setting", true);
		setLayout(new GridLayout(4,2));
      
      try{
         add(new Label("Your IP address:"));
         add(new Label(InetAddress.getLocalHost().toString()));
         }
      catch(Exception x)
         {
         // apparently you have very strange IP 
         // and how are you going to play network game in this case? 
         // really I don't care! :-)
         }
      
      Label lblOppon = new Label("IP address of your opponent:");
      add(lblOppon);
		add(txtIPAddr);
      add(new Label("IP port of your opponent:"));
		add(txtIPPort);      
      
      Panel btns = new Panel();
      btns.setLayout(new FlowLayout());
		btns.add(btnOK);
		btns.add(btnCancel);
      add(btns);
      // It's pity but it doesn't work ;-( So use predefined settings instead.
      // int h, w = lblOppon.getFontMetrics(lblOppon.getFont()).stringWidth("IP address of your opponent:  111.111.111.111");
      // h = lblOppon.getFontMetrics(lblOppon.getFont()).getHeight();
      // resize(w + 10, h * 4 + 10);
      resize(350, 150);
   }
   
	public boolean handleEvent(Event e)
   {
      if(e.id == Event.WINDOW_DESTROY)
         {
         hide();
         return true;
         }
      
      return super.handleEvent(e);
   }
      
	public boolean keyDown(Event e, int key)
	{		
      if(key == Event.ENTER || key == Event.ESCAPE)
         {
         fCancel = key == Event.ESCAPE;
         hide();
   		return true;
         }
		return false;
   }
   
	public boolean action(Event e, Object o)
   {
      if(e.target.equals(btnOK) || e.target.equals(btnCancel))
         {
         fCancel = e.target.equals(btnCancel);
         hide();
         return true;
         }
      
      return false;
   }
   
}



public class TennisApplet extends Applet
{
	/**
	 * The entry point for the applet. 
	 */
	public void init()
	{		
		initForm();
	}

   // some useful controls
	Label     scoreBoard = new Label("Press 'Restart' button to begin......");
	Button    btnStart   = new Button("Restart");
	Button    btnNetGame = new Button("NetGame");
	CourtView cView      = new CourtView(scoreBoard); 
   speedRegulator speedReg = new speedRegulator(cView, 200, 20);
	Thread	 threadPic  = null; // game's thread
		
	public boolean keyDown(Event e, int key)
	{		
		cView.proceedKey(key);
		return false;
	}
	
	
	public void start()
	{
		if(threadPic == null)
		{
			threadPic = new Thread(cView);
			threadPic.start();
		}
	}
	
	
	public void stop()
	{
		if(threadPic != null)
		{
			threadPic.stop();
			threadPic = null;
		}
	}  
	
	public boolean action(Event e, Object o)
	{		
		if(e.target.equals(btnStart))
			{
			cView.start(false);
			return true;
			}
      else
		   if(e.target.equals(btnNetGame))
		   	{
		   	cView.start(true);
		   	return true;
		   	}
	
		return false;
	}

	/**
	 * Intializes values for the applet and its components
	 */
	void initForm()
	{
		setBackground(Color.lightGray);
		setForeground(Color.black);
		setLayout(new FlowLayout());
		add(btnStart);
      add(btnNetGame);
		add(scoreBoard);
		add(cView);
		add(speedReg);
	}
}

