//This code (c) Wen Jun Zhang & Zhong Li 1996-97
//This is also the homework for 20-625-715 
//It's free for anyone to download or make a copy of the file with this notice
//attached.
import java.util.*;
import java.awt.*;
import java.applet.Applet;
import java.lang.*;

class Ball {
   double x, y;
   double vx, vy;
   boolean Isfinished, Ismoved;
   int lbl;
   Color color;
}

class Hole { int  x, y, r; }

class Playpool extends Panel implements Runnable {
   Thread relaxer;                  // Thread for timing drawing intervals
   Ballplay play;                   // Connection to the applet widgets

   double time = 0.05;              // Speed of balls fudge factor
   boolean cuemovable = false;      // true if mouse cursor is over cue ball
   boolean hitenable = false;       // true if the cue ball can be hit
   boolean drawable = false;        // true if cue stick can be drawn
   int down_x,                      // horizontal mouse position when pressed
       down_y,                      // vertical mouse position when pressed
       tablewidth,                  // Pixel width of entire pool table
       tableheight,                 // Pixel height of entire pool table
       poolwidth,                   // Pixel width of playing area
       poolheight,                  // Pixel height of playing area
       sidelength,                  // Pixel width of buffer area
       ballR,                       // Pixel radius of Ball objects
       holeR,                       // Pixel radius of Hole objects
       tbstart_x,                   // Pixel start of table in applet (horiz)
       tbstart_y,                   // Pixel start of table in applet (vert)
       plstart_x,                   // Pixel start of playing area in applet
       plstart_y,                   // Pixel start of playing area in applet
       nballs = 15,                 // Number of Balls
       nholes = 6;                  // Number of Holes

   Ball balls[] = new Ball[20];         // Array of Balls
   Color ballColor[] = new Color[17];   // Array of ball colors
   Hole holes[] = new Hole[10];         // Array of Hole objects

   /***  Establish the table and playing areas  ***/
   /***  in the applet and initialize Balls  ***/
   Playpool (Ballplay play) {
      this.play = play;

      tablewidth = 600;
      tableheight = 350;
      ballR = 25; 
      holeR = 40; 
      sidelength = 30;
      poolwidth  = tablewidth - 2*sidelength;
      poolheight = tableheight - 2*sidelength;
      tbstart_x = 40;
      tbstart_y = 40;
      plstart_x = tbstart_x + sidelength;
      plstart_y = tbstart_y + sidelength;
      down_x = plstart_x + poolwidth/6 - 100;
      down_y = plstart_y + poolheight/2;
      setColor();
      initball();
   }

   void setColor(){
      ballColor[0] = Color.white;
      ballColor[1] = Color.yellow;
      ballColor[2] = Color.red;
      ballColor[3] = Color.orange;
      ballColor[4] = Color.blue;
      ballColor[5] = Color.black;
      ballColor[6] = Color.green;
      ballColor[7] = Color.cyan;
      ballColor[8] = Color.magenta;
      ballColor[9] = Color.yellow;
      ballColor[10] = Color.red;
      ballColor[11] = Color.green;
      ballColor[12] = Color.orange;
      ballColor[13] = Color.blue;
      ballColor[14] = Color.magenta;
      ballColor[15] = Color.cyan;
      ballColor[16] = Color.cyan;
   }

   /***  Put Balls into a Hole by setting "Isfinished" to true  ***/
   void isfinish (int n) {
      double dis1, x, y;
      for (int i=0 ; i < nholes ; i++) {
         x = balls[n].x - holes[i].x;
         y = balls[n].y - holes[i].y;
         dis1 = Math.sqrt(x*x + y*y);
         if ((int)dis1 < (Math.sqrt(holes[i].r*holes[i].r/4)))
	    balls[n].Isfinished = true;
      }
   }

   /***  Paint the table  ***/
   void painttable (Graphics g) {
      g.setColor(Color.blue);
      g.fill3DRect(tbstart_x, tbstart_y, tablewidth, tableheight, false);
      g.setColor(Color.green);
      g.fill3DRect(plstart_x, plstart_y, poolwidth, poolheight, false);
   }

   /***  Create a Hole object  ***/
   Hole addhole(int x, int y, int r) {
      Hole h = new Hole();
      h.x = x; 
      h.y = y; 
      h.r = r;
      return h;
   }

   /***  Create a Ball object  ***/
   Ball addball (double x, double y, int lbl, Color color) {
      Ball b = new Ball();
      b.x = x; 
      b.y = y;
      b.vx = 0.; 
      b.vy = 0.;
      b.Isfinished = false; 
      b.Ismoved = false;
      b.lbl = lbl; 
      b.color = color;
      return b;
   }

   /***  Move all moving balls at an epoch  ***/
   void moveballs() {
      double x, y, vx, vy;
      double f = 0.01;         // Frictional factor - slows balls down
      int h1;

      for (int i=0 ; i <= nballs ; i++) {
	 h1 = 99;   // For determining the closest hit ball to the ith ball
	 if ((balls[i].Ismoved == true) && (balls[i].Isfinished == false)){
            vx = balls[i].vx*(1-f);
            vy = balls[i].vy*(1-f);
            x = balls[i].x;
            x += vx*time;
            y = balls[i].y;
            y += vy*time;

            double dis1 = (double)ballR;
            for (int j=0 ; j <= nballs ; j++) {
	       if ((balls[j].Isfinished == false) && (i != j)) {
		  double x1 = x - balls[j].x;
		  double y1 = y - balls[j].y;
		  double dis2 = (Math.sqrt(x1*x1 + y1*y1));
		  if (dis2 < dis1) { 
		     h1 = j;        // mark this ball as closest
		     dis1 = dis2; 
		  }
               }
	    }

	    /*** If no balls hit by ith ball, just move ith ball straight  ***/
            if (h1 == 99) {
               balls[i].x = x; balls[i].y = y;
               balls[i].vx = vx; balls[i].vy = vy;
               isfinish(i);
               hitside(i);
            } else {
	       /***  Otherwise worry about hit ball  ***/
	       double d1, d2, d3, intime;
	       intime = 0.002;
	       d1 = balls[i].x - balls[h1].x;
	       d2 = balls[i].y - balls[h1].y;
	       d3 = Math.sqrt(d1*d1 + d2*d2);
	       while(d3 > ((double)ballR + 0.1)) {
		  balls[i].x += balls[i].vx*intime;
		  balls[i].y += balls[i].vy*intime;
		  d1 = balls[i].x - balls[h1].x;
		  d2 = balls[i].y - balls[h1].y;
		  d3 = Math.sqrt(d1*d1 + d2*d2);
	       }
	       twoballshit(i, h1);
	    }

	    /***  Stop a ball when it is below a "slow" threshold  ***/
            if (Math.abs(balls[i].vx) < 0.5 && Math.abs(balls[i].vy) < 0.5) {
               balls[i].Ismoved = false;
               balls[i].vx = 0.;
               balls[i].vy = 0.;
	    }
	 }
      }
   }


   void twoballshit(int n, int i) {
      double normalx, normaly, tangentx, tangenty, distx, disty, 
	 temp1, temp2, nhittervx, nhittervy, thittervx, thittervy,
         ntargetvx, ntargetvy, ttargetvx, ttargetvy, ntargetv, 
	 ttargetv, nhitterv, thitterv;

      distx = (balls[i].x - balls[n].x)*(balls[i].x - balls[n].x);
      disty = (balls[i].y - balls[n].y)*(balls[i].y - balls[n].y);
      normalx = (balls[i].x - balls[n].x)/Math.sqrt(distx + disty);
      normaly = (balls[i].y - balls[n].y)/Math.sqrt(distx + disty);
      tangentx = -normaly;
      tangenty = normalx;
      nhitterv = balls[n].vx*normalx + balls[n].vy*normaly;
      thitterv = balls[n].vx*tangentx + balls[n].vy*tangenty;
      ntargetv = balls[i].vx*normalx + balls[i].vy*normaly;
      ttargetv = balls[i].vx*tangentx + balls[i].vy*tangenty;
      temp1 = nhitterv;
      nhitterv = ntargetv;
      ntargetv = temp1;
      balls[n].vx = nhitterv*normalx + thitterv*tangentx;
      balls[n].vy = nhitterv*normaly + thitterv*tangenty;
      balls[i].vx = ntargetv*normalx + ttargetv*tangentx;
      balls[i].vy = ntargetv*normaly + ttargetv*tangenty;
      balls[i].Ismoved = true;
   }

   /***  Make adjustment if a ball hits the buffer area  ***/
   void hitside (int n) {
      double min_x, max_x, min_y, max_y;
      min_x = plstart_x + ballR/2;
      max_x = plstart_x + poolwidth - ballR/2;
      min_y = plstart_y + ballR/2;
      max_y = plstart_y + poolheight - ballR/2;

      if (balls[n].x <= min_x ) {
	 balls[n].x = min_x;
	 balls[n].vx = -balls[n].vx;
      }
      if (balls[n].x >= max_x) {
	 balls[n].x = max_x;
	 balls[n].vx = -balls[n].vx;
      }
      if (balls[n].y <= min_y) {
	 balls[n].y = min_y;
	 balls[n].vy = -balls[n].vy;
      }
      if (balls[n].y >= max_y) {
	 balls[n].y = max_y;
	 balls[n].vy = -balls[n].vy;
      }
   }

   /***  Returns the index of Ball object matching the input label  ***/
   int findball(int lbl) {
      int n1 = 99;
      for (int i=0 ; i < nballs ; i++) {
         if (balls[i].lbl == lbl) {
            return i;
	 }
      }
      return n1;
   }

   /***  Paint all the Hole objects  ***/
   void paintholes (Graphics g) {
      int x, y, r;
      for (int i=0 ; i < 3 ; i++) {
	 for (int j=0 ; j < 2 ; j++) {
	    x = (int)(plstart_x + i*poolwidth/2);
	    y = (int)(plstart_y + j*poolheight);
	    r = holeR;
	    holes[i*2+j] = addhole(x,y,r);
         }
      }
      for (int i=0 ; i < nholes ; i++) {
	 g.setColor(Color.black);
	 g.fillOval((holes[i].x - holes[i].r/2), 
		     (holes[i].y - holes[i].r/2),
		     holes[i].r,holes[i].r);
      }
   }

   /*** Put the balls on the table  ***/
   void initball() {
      double x, y;
      int lbl = 0;
      x = plstart_x + poolwidth/6;
      y = plstart_y + poolheight/2;
      balls[lbl] = addball(x, y, lbl, ballColor[lbl]);

      for (int i=1 ; i <= 5 ; i++) {
	 for (int j=1 ; j <= i ; j++) {
	    x = plstart_x + poolwidth*4/6 + (i-1)*ballR;
	    y = plstart_y + poolheight/2 - (i-1)*ballR/2 + (j-1)*ballR;
	    lbl++;
	    balls[lbl] = addball(x, y, lbl, ballColor[lbl]);
	 }
      }
   }

   /***  Paint a ball on the table  ***/
   void paintball (Graphics g, int n, FontMetrics fm) {
      String s = (new String()).valueOf(balls[n].lbl);
      int w = fm.stringWidth(s) + 10;
      int h = fm.getHeight() + 4;
      g.setColor(balls[n].color);
      g.fillOval((int)(balls[n].x - ballR/2),
		 (int)(balls[n].y - ballR/2), ballR, ballR);
      g.setColor(Color.white);
      g.drawString(s,(int)(balls[n].x - (w-10)/2),
		     (int)(balls[n].y - (h-4)/2 + fm.getAscent()));
   }

   /***  Run this every 25 milliseconds  ***/
   public synchronized void play() {
      hitenable = true;

      for (int i=0 ; i <= nballs ; i++) {
	 if (balls[i].Ismoved == true && balls[i].Isfinished == false) {
	    hitenable = false;
	 }
      }

      moveballs();

      if (hitenable) {
	 if (balls[0].Isfinished) {
	    balls[0].Ismoved = false;
	    balls[0].vx = 0;
	    balls[0].vy = 0;
	    balls[0].x = plstart_x + poolwidth/6;
	    balls[0].y = plstart_y + poolheight/2;
	    balls[0].Isfinished = false;
	 }
      }
      repaint();
   }

   public synchronized boolean mouseDown(Event evt, int x, int y) {
      drawable = true;
      down_x = x;
      down_y = y;
      balls[0].vy = 0.;
      balls[0].vx = 0.;
      balls[0].Ismoved = false;
      if (cuemovable) { 
	 balls[0].x = x;
	 balls[0].y = y;
	 play.msg.setText("Drag cueball to desired position");
      }
      repaint();
      return true;
   }

   public synchronized boolean mouseDrag(Event evt, int x, int y) {
      if (cuemovable) { 
	 balls[0].x = x; 
	 balls[0].y = y; 
      }
      if (drawable && !cuemovable) {
	 down_x = x;
	 down_y = y;
	 play.msg.setText("Release mouse to take a shot!");
      }
      repaint();
      return true;
   }

   public synchronized boolean mouseUp(Event evt, int x, int y) {
      drawable = true;
      if (drawable && !cuemovable) {
	 if (((int)balls[0].x - x) > 500) { balls[0].vx = 500; }
	 else {
	    if (((int)balls[0].x - x) < -500) { balls[0].vx = -500; }
	    else { balls[0].vx = balls[0].x - x; }
	 }
	 if (((int)balls[0].y - y) > 500) { balls[0].vy = 500; }
	 else{
	    if (((int)balls[0].y - y) < -500) { balls[0].vy = -500; }
	    else { balls[0].vy = balls[0].y - y; }
	 }
	 play.msg.setText("To stop cueball, press mouse.");
	 balls[0].Ismoved = true;
	 hitenable = false;
	 drawable = false;
      } else { 
	 play.msg.setText("Ready for next shooting !"); 
      }
      if (cuemovable) { 
	 balls[0].x = x; 
	 balls[0].y = y; 
	 cuemovable = false;
      }
      repaint();
      return true;
   }

   /***  Update the pool table every 25 milliseconds  ***/
   public void run() {
      while (true) {
	 play();
	 try{ Thread.sleep(25); } catch(InterruptedException e) { break; }
      }
   }

   /***  Double Buffering for smooth appearance  ***/

   Graphics offgraphics;
   Image offscreen;
   Dimension offscreensize;

   public synchronized void update (Graphics g) {
      Dimension d = size();
      if ((offscreen == null) || 
	  (d.width != offscreensize.width) || 
	  (d.height != offscreensize.height)) {
	 offscreen = createImage(d.width, d.height);
	 offscreensize = d;
	 offgraphics = offscreen.getGraphics();
	 offgraphics.setFont(getFont());
      }

      FontMetrics fm = offgraphics.getFontMetrics();
      offgraphics.setColor(getBackground());
      offgraphics.fillRect(0,0,d.width,d.height);

      painttable(offgraphics);
      paintholes(offgraphics);
      for (int i=0 ; i <= nballs ; i++) {
	 if (balls[i].Isfinished == false) paintball(offgraphics,i,fm);
      }

      if (drawable && !cuemovable) {
	 g.setColor(Color.yellow);
	 g.drawLine((int)balls[0]. x, (int)balls[0]. y, down_x, down_y);
      }

      g.drawImage(offscreen, 0, 0, null);
    }

   public void start() {
      relaxer = new Thread(this);
      relaxer.start();
   }

   public void stop() {
      relaxer.stop();
   }
}
