/*----------------------------------------------------------------------+
|	Title:	Tumble.java						|
|		Java beta applet					|
|									|
|	Author:	David E. Joyce						|
|		Department of Mathematics and Computer Science   	|
|		Clark University					|
|		Worcester, MA 01610-1477				|
|		U.S.A.							|
|									|
|		http://aleph0.clarku.edu/~djoyce/home.html		|
|		djoyce@clarku.edu					|
|									|
|	Date:	December, 1995.  Modifications in January, 1996.	|
+----------------------------------------------------------------------*/


import java.awt.*;
import java.applet.*;
import java.util.StringTokenizer;

/*----------------------------------------------------------------------+
|	Collider class.  Nearly all the physics is in Collider.  It	|
|	determines when the collisions occur and what happens in a	|
|	collision.  The circular wall has radius 1 as far as the	|
|	collider is concerned.						|
+----------------------------------------------------------------------*/

class Collider extends Thread {
  Tumble tble;
  int nextColl_i, nextColl_j;
  double wallColl[];	// collision time tables
  double ballColl[][];
  double timeToCollide=Double.POSITIVE_INFINITY;
  double qg2;

  Collider (Tumble tble) {
    int i,j;

    this.tble = tble;
    qg2 = 0.25*tble.grav*tble.grav;
    // initialize the collision tables
    // determine collision times with walls
    wallColl = new double[tble.nBalls];
    for (i=0; i<tble.nBalls; ++i) 
        wallColl[i] = findWallColl(i);
    // determine collision times between balls
    ballColl = new double[tble.nBalls][];	// a triangular array
    for (i=1; i<tble.nBalls; ++i) {
      ballColl[i] = new double[i];
      for (j=0; j<i; ++j)
        ballColl[i][j] = findBallColl(i,j);
    }
    nextColl();
  }

  double solvequad (double a, double b, double c) {
    // find smallest positive root of ax^2+bx+c
    double disc = b*b - 4.0*a*c;
    if (disc < 1e-10d) return Double.POSITIVE_INFINITY;
    double discroot = Math.sqrt(disc);
    double x1 = (-b+discroot)/(2.0*a);
    if (x1<1e-10d) return Double.POSITIVE_INFINITY;
    double x2 = (-b-discroot)/(2.0*a);
    return (x2>1e-10d) ? x2 : x1;
  }

  double solvequart (double a, double b, double c, double d, double e) {
    // find smallest positive root of ax^4+bx^3+cx^2+dx+e
    double err = 1e-6d;
    double num, den;
    int i;

    // first find a root using Newton's method
    double x1 = 10.0;
    double diff = 1.0;
    for (i=0; (diff>err || diff<-err) && i<24; ++i) {
      num = ((((a*x1+b)*x1)+c)*x1+d)*x1+e;
      den = ((4.0*a*x1+3.0*b)*x1+2.0*c)*x1+d;
      diff = num/den;
      if (diff<-1e10d || diff>1e10d)
	x1 = 10.0*Math.random();
      else
        x1 -= diff;
    }
    b += a*x1;
    c += b*x1;
    d += c*x1;
    // now find a root of the cubic ax^3+bx^2+cx+d using Newton's method
    double x2 = -10.0;
    diff = 1.0;
    for (i=0; (diff>err || diff<-err) && i<20; ++i) {
      num = (((a*x2+b)*x2)+c)*x2+d;
      den = (3.0*a*x2+2.0*b)*x2+c;
      if (-0.00001<den && den<0.00001) diff = 0.1+Math.random();
      else diff = num/den;
      x2 -= diff;
    }
    b += a*x2;
    c += b*x2;
    double x3 = solvequad (a,b,c);
    if (x1>1e-10d && x1<x3) x3=x1;
    if (x2>1e-10d && x2<x3) x3=x2;
    return x3;
  }

  void nextColl() {  // which possible collision occurs next?
    int i,j;

    timeToCollide = Double.POSITIVE_INFINITY;
    for (i=0; i<tble.nBalls; ++i) 
      if (i!=tble.pick && wallColl[i]<timeToCollide) {
	timeToCollide = wallColl[i];
	nextColl_i = i;
	nextColl_j = -1;
      }
    for (i=1; i<tble.nBalls; ++i) if (i != tble.pick)
      for (j=0; j<i; ++j)
	if (j!=tble.pick && ballColl[i][j]<timeToCollide) {
	  timeToCollide = ballColl[i][j];
	  nextColl_i = i;
	  nextColl_j = j;
	}
    if (timeToCollide >= 2.1) {
      timeToCollide = 2.0;
      nextColl_i = -1;
      nextColl_j = -1;
  } }

  double findWallColl (int i) { // when does ball i hit the wall?
    double when;
    double dr = 1.0 - tble.r[i];
    double dist2 = tble.x[i]*tble.x[i] + tble.y[i]*tble.y[i];
    double inprod = tble.x[i]*tble.xp[i] + tble.y[i]*tble.yp[i];
    if (dist2 > dr*dr  && inprod > 0.0) return 0.0;
    double speed2 = tble.xp[i]*tble.xp[i] + tble.yp[i]*tble.yp[i];
    if (qg2 == 0.0) {
      if (Math.abs(speed2) < 1e-8d) 	// motionless
	return Double.POSITIVE_INFINITY;
      when = solvequad (
	  speed2,
	  2.0*inprod,
	  dist2 - dr*dr
      );
    } else 
      when = solvequart (
	  qg2,
	  -tble.grav*tble.yp[i],
	  speed2 - tble.grav*tble.y[i],
	  2.0*inprod,
	  dist2 - dr*dr
      );
    if (when == Double.POSITIVE_INFINITY)
        tble.getAppletContext().showStatus("ball "+i+" never returning. x="+
	  tble.x[i]+", y="+tble.y[i]+", x'="+tble.xp[i]+", y'="+tble.yp[i]);
    return when;
  }

  double findBallColl (int i, int j) { // when might ball i hit ball j?
    double norm2 = tble.r[i]+tble.r[j];
    norm2 *= norm2;
    double dx = tble.x[i] - tble.x[j];
    double dy = tble.y[i] - tble.y[j];
    double dist2 = dx*dx + dy*dy;
    double d2x = tble.xp[i] - tble.xp[j];
    double d2y = tble.yp[i] - tble.yp[j];
    if (norm2 > dist2) return Double.POSITIVE_INFINITY;
    return solvequad (
	  d2x*d2x + d2y*d2y,
	  2.0*(dx*d2x + dy*d2y),
	  dist2 - norm2
    );
  }

  void updateWallColl() {
    for (int i=0; i<tble.nBalls; ++i)
      wallColl[i] = findWallColl(i);
  }

  void updateBallColl(int i) {		
    // change entries in the ball collision table involving ball i 
    int k;

    for (k=0; k<i; ++k)
      ballColl[i][k] = findBallColl(i,k);
    for (k=i+1; k<tble.nBalls; ++k)
      ballColl[k][i] = findBallColl(k,i);
  }

  void resetTimes() {
    int i,j;

    tble.lastCollisionTime = System.currentTimeMillis();
    // move the balls to their current positions
    double ghttc = 0.5 * tble.grav * timeToCollide;
    for (i=0; i<tble.nBalls; ++i) if (i != tble.pick) {
      tble.x[i] += tble.xp[i]*timeToCollide;
      tble.y[i] += (tble.yp[i]-ghttc)*timeToCollide;
      tble.yp[i] -= tble.grav*timeToCollide;
    }
    // reset the collision tables
    for (i=0; i<tble.nBalls; ++i) 
        wallColl[i] -= timeToCollide;
    for (i=1; i<tble.nBalls; ++i)
      for (j=0; j<i; ++j)
        ballColl[i][j] -= timeToCollide;
  }

  void bounceBall() {
    int i=nextColl_i, j=nextColl_j;
    if (j == -1) {			// ball hits wall
      if (i == -1) return;		// no collision
      double dist2 = tble.x[i]*tble.x[i]+tble.y[i]*tble.y[i];
      if (dist2 > 1.0) {	// somehow the ball got way outside the unit circle
	tble.x[i] = 0.0;
	tble.y[i] = 0.0;
	tble.xp[i] = 0.0;
	tble.yp[i] = 0.0;
      } else {
        double inprod = tble.x[i]*tble.xp[i] + tble.y[i]*tble.yp[i];
        if (inprod > 0) {		// ball headed outward
          double factor = 2.0*inprod / dist2;
          tble.xp[i] -= factor*tble.x[i]; 
          tble.yp[i] -= factor*tble.y[i];
	  if (tble.sound)
	    tble.play(tble.getDocumentBase(), "sounds/ip.au");
    } } }
    else {				// two balls collide
      double dx = tble.x[i] - tble.x[j];
      double dy = tble.y[i] - tble.y[j];
      double norm2 = dx*dx +dy*dy;
      double xgp = (tble.m[i]*tble.xp[i]+tble.m[j]*tble.xp[j])/(tble.m[i]+tble.m[j]);
      double ygp = (tble.m[i]*tble.yp[i]+tble.m[j]*tble.yp[j])/(tble.m[i]+tble.m[j]);
      double factor_i = 2.0*(dx*(tble.xp[i]-xgp)+dy*(tble.yp[i]-ygp))/norm2;
      double factor_j = 2.0*(dx*(tble.xp[j]-xgp)+dy*(tble.yp[j]-ygp))/norm2;
      tble.xp[i] -= factor_i*dx;
      tble.yp[i] -= factor_i*dy;
      tble.xp[j] -= factor_j*dx;
      tble.yp[j] -= factor_j*dy;
      if (tble.sound)
	 tble.play(tble.getDocumentBase(), "sounds/thin.bell.au");
  } }

  public void run () {
    tble.lastCollisionTime = System.currentTimeMillis();
    while (true) {
      if (timeToCollide > 0.0) {
        tble.nextCollisionTime = tble.lastCollisionTime 
			       + (long)(1000.0*timeToCollide);
        int sleepPeriod = 
	  (int) (tble.nextCollisionTime - System.currentTimeMillis());
        try {Thread.sleep(sleepPeriod);}
          catch (InterruptedException e) {}
      }
      if (tble.movedBall) {
	tble.movedBall = false;
	if (tble.pick != -2) {
	  updateBallColl(tble.pick);
	  updateWallColl();
	  tble.pick = -2;
      } }
      resetTimes();
      if (tble.pick != nextColl_i && tble.pick != nextColl_j) {
        bounceBall();
	updateWallColl();
        if (nextColl_i != -1) updateBallColl(nextColl_i);
        if (nextColl_j != -1) updateBallColl(nextColl_j);
      }
      nextColl();
} } }


/*----------------------------------------------------------------------+
|	Tumble class.  There are two threads that run.  One is the	|
|	painter, an ordinary thread which just paints the position of	|
|	balls at regular intervals.  The other is the collider.		|
+----------------------------------------------------------------------*/

public class Tumble extends Applet implements Runnable {
  Thread painter;
  int paintFrequency = 100;	// milliseconds
  Collider cldr;
  boolean suspended=false;

  int nBalls = 4;
  double grav = 0.0;
  boolean sound = true;
  int radius, gMidx, gMidy;
  Color circleColor;
  int grid;

  Color  c[] = new Color[nBalls];	// color of balls
  double r[] = new double[nBalls];	// radius of ball
  double m[] = new double[nBalls];	// mass of ball
  double x[] = new double[nBalls];
  double y[] = new double[nBalls];	// location of ball
  double xp[] = new double[nBalls];
  double yp[] = new double[nBalls];	// velocity of ball

  long lastCollisionTime,nextCollisionTime,stopTime,lastDragTime;

  int pick = -2;
  boolean movedBall = false;
  double lastDrag_x, lastDrag_y;

  Image offscreen;
  Dimension offscreensize;
  Graphics offgraphics;


  public String getAppletInfo() {
    return "Tumble. Copyright 1995, David Joyce, Clark University. Version 0.1f";
  }

  public String[][] getParameterInfo() {
    String[][] pinfo = {
	{"balls", 	"int", 		"number of balls"},
	{"gravity", 	"int", 		"gravitational constant * 1000"},
	{"sound",	"boolean",	"noisy applet?"},
	{"balldata",	"ball array",	"data for balls"},
	{"color",	"color",	"circle color"},
	{"grid",	"int",		"size of grid, if any"}
    };
    return pinfo;
  }


  Color randomColor() {
    int c;
    float hue = (float)(Math.random()*1.0);
    if (Math.random()<0.2) 
      c = Color.HSBtoRGB(hue,
	(float)(1.0-Math.random()*Math.random()),  // saturation
	(float)(1.0-Math.random()*Math.random())   // brightness
      );
    else if (Math.random()<0.4)
      c = Color.HSBtoRGB(hue,1.0f,(float)(0.3+0.7*Math.random()));
    else
      c = Color.HSBtoRGB(hue,(float)(Math.random()),1.0f);
    return new Color(c);
  }

  Color parseColor (String str) {
    StringTokenizer t = new StringTokenizer(str,":");
    float hue = (float)(Integer.parseInt(t.nextToken())/360.0);
    float sat = (float)(Integer.parseInt(t.nextToken())/100.0);
    float bri = (float)(Integer.parseInt(t.nextToken())/100.0);
    return new Color(Color.HSBtoRGB(hue,sat,bri));
  }

  void parseBall (String str, int i) {
    StringTokenizer u = new StringTokenizer(str,",");
    r[i] = Integer.parseInt(u.nextToken())*0.001;
    m[i] = r[i]*r[i]*r[i];
    x[i] = Integer.parseInt(u.nextToken())*0.001;
    y[i] = Integer.parseInt(u.nextToken())*0.001;
    xp[i] = Integer.parseInt(u.nextToken())*0.001;
    yp[i] = Integer.parseInt(u.nextToken())*0.001;
    c[i] = randomColor();
    c[i] = (u.hasMoreTokens()) ? parseColor(u.nextToken()) : randomColor();
  }

  void randomBall(int i) {
    double a,b;
    boolean bad;
    c[i] = randomColor();
    do {
      r[i] = 0.050+Math.random()/(2.0+i);
      a = (Math.random()-0.5)*2.0;
      b = (Math.random()-0.5)*2.0;
      bad = (a*a+b*b >= (1.0-r[i])*(1.0-r[i]));
      for (int j=0; !bad && (j<nBalls); ++j) if (i!=j)
	bad = ((a-x[j])*(a-x[j])+(b-y[j])*(b-y[j]) <= 
			(r[i]+r[j])*(r[i]+r[j]));
    } while (bad);
    m[i] = r[i]*r[i]*r[i];
    x[i] = a;
    y[i] = b;
    xp[i] = 0.5*(Math.random()-0.5);
    yp[i] = 0.5*(Math.random()-0.5);
  }

  public void init() {
    getAppletContext().showStatus("initializing Tumble applet");
    String param = getParameter("sound");
    sound = (param == null) ?
      true :
      (param.equalsIgnoreCase("yes") || param.equalsIgnoreCase("true"));
    if (sound)
      getAudioClip(getDocumentBase(), "sounds/thin.bell.au");
    param = getParameter("color");
    circleColor = (param == null) ? randomColor() : parseColor(param);
    param = getParameter("balls");
    nBalls = (param == null) ? 4 : Integer.parseInt(param);
    param = getParameter("gravity");
    grav = (param == null) ? 0.0 : Integer.parseInt(param)/1000.0;
    param = getParameter("grid");
    grid = (param == null) ? 0 : Integer.parseInt(param);
    gMidx = size().width/2;
    gMidy = size().height/2;
    radius = Math.min(gMidx,gMidy);
    // read ball info if any
    int i=0;
    param = getParameter("balldata");
    if (param != null) {
      StringTokenizer t = new StringTokenizer(param,";");
      while (t.hasMoreTokens()) {
	parseBall(t.nextToken(),i);
	++i;
    } }
    // generate random ball info for remaining balls
    for (; i<nBalls; ++i)
      randomBall(i);
    lastCollisionTime = System.currentTimeMillis();
    stopTime = nextCollisionTime = lastCollisionTime;
    if (sound)
      getAudioClip(getDocumentBase(), "sounds/ip.au");
  }

  public void start() {
    movedBall = false;
    long diffTime = System.currentTimeMillis() - stopTime;
    lastCollisionTime += diffTime;
    nextCollisionTime += diffTime;
    if (painter == null) {
      painter = new Thread(this);
      painter.start();
    }
    if (cldr == null) {
      cldr = new Collider(this);
      cldr.start();
    }
    suspended = false;
  }

  public void stop() {
    stopTime = System.currentTimeMillis();
    if (painter != null) {
      painter.stop();
      painter = null;
    }
    if (cldr != null) {
      cldr.stop();
      cldr = null;
    }
  }

  public boolean mouseEnter(Event evt, int x, int y) {
    if (suspended) {
      long diffTime = System.currentTimeMillis() - stopTime;
      lastCollisionTime += diffTime;
      nextCollisionTime += diffTime;
      if (painter!=null) painter.resume();
      if (cldr!=null)    cldr.resume();
      suspended = false;
    }
    return true;
  }

  public boolean mouseExit(Event evt, int x, int y) {
    if (!suspended) {
      stopTime = System.currentTimeMillis();
      if (painter!=null) painter.suspend();
      if (cldr!=null)    cldr.suspend();
      suspended = true;
    }
    return true;
  }

  public void run() {
    while(true) {
      try {Thread.currentThread().sleep(paintFrequency);}
        catch (InterruptedException e) {}
      tumble();
  } }

  synchronized void tumble() {
    if (System.currentTimeMillis() <= nextCollisionTime)
      repaint();
  }

  public void update(Graphics g) {
    if (System.currentTimeMillis() <= nextCollisionTime) {
      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();
	gMidx = d.width/2;
	gMidy = d.height/2;
	radius = Math.min(gMidx,gMidy);
      }
      offgraphics.setColor(getBackground());
      offgraphics.fillRect(0, 0, d.width, d.height);
      offgraphics.setColor(circleColor);
      offgraphics.fillOval(gMidx-radius,gMidy-radius, radius*2,radius*2);
      if (grid>0) {
	int rdg = radius/grid;
	for (int i=-rdg; i<=rdg; ++i) {
	  int a = i*grid;
	  int b = (int)Math.sqrt(radius*radius-a*a);
	  offgraphics.setColor((i==0)?Color.black:Color.darkGray);
	  offgraphics.drawLine(-a+gMidx,-b+gMidy, -a+gMidx,b+gMidy);
	  offgraphics.drawLine(a+gMidx,-b+gMidy,  a+gMidx,b+gMidy);
	  offgraphics.drawLine(-b+gMidx,a+gMidy,  b+gMidx,a+gMidy);
	  offgraphics.drawLine(-b+gMidx,-a+gMidy, b+gMidx,-a+gMidy);
	}
      }
      for (int i=0; i<nBalls; ++i) {
	int x_coord, y_coord;
        offgraphics.setColor(c[i]);
	if (i == pick) {
	  x_coord = gMidx + (int)(radius*(x[i]-r[i]));
	  y_coord = gMidy + (int)(radius*(-y[i]-r[i]));
	} else {
          double t = (System.currentTimeMillis()-lastCollisionTime)/1000.0;
          x_coord = gMidx + (int)(radius*(x[i] + xp[i]*t - r[i]));
          y_coord = gMidy + 
		(int)(radius*(-(y[i]+(yp[i]-0.5*grav*t)*t) - r[i]));
	}
        offgraphics.fillOval(x_coord,y_coord,
		2*(int)(radius*r[i]),2*(int)(radius*r[i]));
      }
      g.drawImage(offscreen, 0, 0, null);
  } }

  public void paint(Graphics g) {repaint();}

  void moveBall(int i, double a, double b, int motion) {
    double norm = Math.sqrt(a*a+b*b);
    if (norm > 1.0-r[i]) {
      a = a*(1.0-r[i])/norm;
      b = b*(1.0-r[i])/norm;
    }
    x[i] = a;
    y[i] = b;
    if (motion==1) {		// stop the ball
      xp[i] = 0.0;
      yp[i] = 0.0;
    }
    else  {			// toss or release the ball
      double dragtime = (System.currentTimeMillis()-lastDragTime) * 0.001;
      xp[i] = (xp[i] + (a-lastDrag_x)/dragtime) * 0.5;
      yp[i] = (yp[i] + (b-lastDrag_y)/dragtime) * 0.5;
    }
    lastDragTime = System.currentTimeMillis();
    lastDrag_x = a;
    lastDrag_y = b;
  }

  void makePick (double c, double d) {
    lastDrag_x = c;
    lastDrag_y = d;    double bestdist2 = Double.POSITIVE_INFINITY;
    double t = (System.currentTimeMillis()-lastCollisionTime)/1000.0;
    for (int i=0; i<nBalls; ++i) {
      double a = x[i] + xp[i]*t;
      double b = y[i] + (yp[i]-0.5*grav*t)*t;
      double dist2 = (a-c)*(a-c) + (b-d)*(b-d);
      if (dist2 < bestdist2) {
	pick = i;
	bestdist2 = dist2;
  } } }

  public boolean mouseDown(Event evt, int cg, int dg) {
    // determine which ball is closest to location (c,d).
    double c = (double)(cg - gMidx)/radius;
    double d = (double)(gMidy - dg)/radius;
    makePick(c,d);
    moveBall(pick, c, d, 1);
    return true;
  } 

  public boolean mouseDrag(Event evt, int cg, int dg) {
    double c = (double)(cg - gMidx)/radius;
    double d = (double)(gMidy - dg)/radius;
    if (pick == -2)
      makePick(c,d);
    moveBall(pick, c, d, 2);
    return true;
  }

  public boolean mouseUp(Event evt, int cg, int dg) {
    double c = (double)(cg - gMidx)/radius;
    double d = (double)(gMidy - dg)/radius;
    if (pick == -2) 
      makePick(c,d);
    moveBall(pick, c, d, 3);
    movedBall = true;
    return true;
  }
}




