// Implementation of an N-Body simulator 
// Ben Dugan, Copyright (c) 1995 and 1996

// This software is made available "as is", and BEN DUGAN
// DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, WITH REGARD
// TO THIS SOFTWARE, INCLUDING WITHOUT LIMITATION ALL IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND IN NO
// EVENT SHALL BEN DUGAN BE LIABLE FOR ANY SPECIAL,
// INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
// FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
// TORT (INCLUDING NEGLIGENCE) OR STRICT LIABILITY, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import java.awt.*;
import java.io.*;
import java.applet.*;


/////////////////////////////////////////////////////////////////////////////
// Ngrav is the applet.  It maintaints the collection of objects, graphics
// elements (tries to do double buffering), and the thread info.

public class NgravApplet extends Applet implements Runnable {

  static int     MaxBodies      = 10;
  static double  MetersPerPixel = 2000000.0;
  static double  SecPerLoop     = 12000.0;

  int    tailLength = 40;
  int    windowDim  = 600;
  double viewScale  = MetersPerPixel;
  Body   bodies[]   = new Body[MaxBodies];
  Color  colors[]   = new Color[MaxBodies];
  int    numBodies  = 0;
  Vector scaleV     = new Vector(SecPerLoop, 0.0);

  Thread       kicker          = null;
  boolean      threadSuspended = false;
  Image        image;
  Graphics     offScreen;
  MouseHandler mouseHandler;

  static int Wait      = 0; // Wait state
  static int Sizing    = 1; // Sizing means growing the planet with the mouse
  static int Pointing  = 2; // Pointing is specifying the vel vector
  static int MaxPoints = 10;

/////////////////////////////////////////////////////////////////////////////
// Vector implements the abstraction of a vector.  Vectors are used in
// this system to specify velocity, acceleration, as well as position... 

class Vector extends Object {
  private double magnitude;
  private double theta;
  
  public Vector () {
    magnitude = 0.0;
    theta     = 0.0; 
  }
  public Vector (double m, double th) {
    magnitude = m;
    theta     = th;
  }

  // setters

  public void setXY (double x, double y) {
    magnitude = Math.sqrt(x*x + y*y);
    theta     = Math.atan2(y, x);
  }
  public void setMagTheta (double m, double th) {
    magnitude = m;
    theta     = th;
  }

  // accessors
  
  public double magnitude() { return magnitude; }
  public double theta() { return theta; }
  public double x() { return Math.cos(theta) * magnitude; }
  public double y() { return Math.sin(theta) * magnitude; }

  // basic operations, add, subtract, and multiply two vectors

  public Vector add (Vector a) {
    Vector temp = new Vector();
    temp.setXY(this.x() + a.x(), this.y() + a.y());
    return temp;
  }
  public Vector mult (Vector a) {
    return new Vector(magnitude * a.magnitude, theta + a.theta);
  }
  public Vector sub (Vector a) {
    Vector temp = new Vector();
    temp.setXY(this.x() - a.x(), this.y() - a.y());
    return temp;
  }
}

/////////////////////////////////////////////////////////////////////////////
// XYPoint is used simply to hold information for drawing objects and
// their trajectories.

class XYPoint extends Object {
  double x;
  double y;
  boolean computed = false;
  
  public XYPoint (double x, double y) {
    x = x;
    y = y;
  }
}

/////////////////////////////////////////////////////////////////////////////
// Body is the abstraction for an object in our system.  It has color,
// acceleration, velocity, mass, and position.  It also has a "path",
// which is information about its trajectory.  Bodies know how to update
// their state wrt to the other bodies in the system (they are asked to
// do this each time slice).  They also know how to draw themselves.

class Body extends Object {
  private Color   color = Color.black;
  private Vector  accel = new Vector();
  private Vector  vel   = new Vector();
  private Vector  pos   = new Vector();
  private XYPoint path[];
  private double mass      = 0.0; // mass in kG
  private int    radius    = 0;   // draw radius, in pixels, cube root of mass
  private int    numPoints = 0;   // number of times drawn

  public Body () {
    init(MaxPoints, Color.black, 0.0, 0.0, 0.0, 0.0, 0.0);
  }

  public Body (int tailLength, Color c, double kiloGrams) {
    init(tailLength, c, kiloGrams, 0.0, 0.0, 0.0, 0.0);
  }

  public Body (int tailLength, Color c, double kiloGrams, 
	       double velMag, double velTheta, double x, double y) {
    init(tailLength, c, kiloGrams, velMag, velTheta, x, y);
  }

  void init (int tailLength, Color c, double kG,
	     double velMag, double velTheta, double x, double y) {
    mass = kG;
    radius = Math.max(2, (int) Math.pow(kG / 1E22, .333333));
    path = new XYPoint[tailLength];
    for (int i=0; i<path.length; i++)
      path[i] = new XYPoint(0,0);
    color = c;
    vel.setMagTheta(velMag, velTheta);
    pos.setXY(x, y);
  }

  // updateAccel really does the computation: it takes a collection of
  // other bodies and computes the effect of those body on itself, and
  // changes its accel accordingly

  public void updateAccel (Body others[], int num) {
    Vector tempV;
    double radius, f;
    accel = new Vector();
    for (int i=0; i<num; i++) {
      if (this != others[i]) {
	tempV  = others[i].pos.sub(pos);
	radius = Math.abs(tempV.magnitude());
	f      = Ngrav.force(mass, others[i].mass, radius);
	tempV.setMagTheta(f/mass, tempV.theta());
	accel  = accel.add(tempV); 
      }
    }
  }

  // on each timeslice, each body is asked to update its velocity and 
  // position based on its new accel, as computed by updateAccel

  public void update (Vector scaleV) {
    vel = vel.add(accel.mult(scaleV));
    pos = pos.add(vel.mult(scaleV));
    if (numPoints >= path.length)
      numPoints = 0;
    path[numPoints].x = pos.x();
    path[numPoints].y = pos.y();
    path[numPoints].computed = true;
    numPoints++;
  }
 

  public void paint (Graphics g, int worldSize, double metersPerPix) {
    int pixX, pixY;
    for (int j=0; j<path.length; j++) {
      if (path[j].computed) {
        pixX = (int)(path[j].x / metersPerPix) + worldSize/2 - radius;
        pixY = -(int)(path[j].y / metersPerPix) + worldSize/2 - radius;
        if (pixX > 0 && pixX < worldSize && pixY > 0 && pixY < worldSize) {
          g.setColor(color);
	  g.fillOval(pixX, pixY, 2*radius, 2*radius);
        }
      }
    }
  }    
}

/////////////////////////////////////////////////////////////////////////////
// The MouseHandler class deals with mouse click events which are used
// by the user to enter new objects via "direct manip"

class MouseHandler extends Object {
  private int newX, newY;   // used to hold the initial position of a new obj
  private int mouseX, mouseY;  // keeps track of current mouse position.
  private int radius   = 0;
  private int addState = 0;

  public boolean mouseDown(java.awt.Event evt, int x, int y) {
    System.out.println("*** IN MOUSEDOWN\n");
    if (addState == Wait) {  // if we're waiting for a new object,
      addState = Sizing;     // go to the sizing state, and remember 
      newX = mouseX = x;     // these coordinates
      newY = mouseY = y;
    }
    return true;
  }

  public boolean mouseDrag(java.awt.Event evt, int x, int y) {
    if (addState == Sizing) { // if we're in the sizing state, just
      mouseX = x;             // go ahead and remember these coordinates
      mouseY = y;
    }
    return true;
  }

  public boolean mouseMove(java.awt.Event evt, int x, int y) {
    if (addState == Pointing) {  // if we're pointing the object 
      mouseX = x;                // then we want to remember these coords
      mouseY = y;
    }
    return true;
  }

  // On the mouseup event, if we are sizing the object, then we are ready
  // to calculate its mass and move onto the pointing state. If we're 
  // pointing, when we get a mouseUp, then it is time to add the new 
  // object, because we know its velocity vector

  public boolean mouseUp(java.awt.Event evt, int x, int y) {
    if (addState == Sizing) {
      addState = Pointing;
      radius = Math.max(Math.abs(newX-x), Math.abs(newY-y));
    }
    else if (addState == Pointing) {
      Vector v = new Vector();
      addState = Wait;
      v.setXY(10*(x-newX), 10*(newY-y));
      addObject(newX, newY, Math.max(1, radius * radius * radius), v);
      radius = 0;
    }
    return true;
  }

  // paint paints a circle and a vector as the user adds a new object...

  public void paint(Graphics g) {    
    if (addState == Sizing) { 
      // dragging, sizing the planet
      g.setColor(Color.red);
      radius = Math.max(Math.abs(newX-mouseX), Math.abs(newY-mouseY));
      g.fillOval(newX-radius, newY-radius, radius*2, radius*2);
    }
    else if (addState == Pointing) { 
      // dragging, pointing object 
      g.setColor(Color.red);
      g.drawLine(newX, newY, mouseX, mouseY);
    }
  }
}

  public void setUpColors () {
    colors[0] = Color.red;      colors[1] = Color.blue;
    colors[2] = Color.black;    colors[3] = Color.green; 
    colors[4] = Color.magenta;  colors[5] = Color.yellow;
    colors[6] = Color.cyan;     colors[7] = Color.orange; 
    colors[8] = Color.darkGray; colors[9] = Color.pink; 
  }
     
  public void init () {
    resize(windowDim, windowDim);
    setUpColors();
    mouseHandler = new MouseHandler();     
    try {                                        // try double buffering
      image = createImage(windowDim, windowDim); 
      offScreen = image.getGraphics();
      System.out.println("Double Buffering!");
    }
    catch (Exception e) {
      offScreen = null;
    } 

    // add a few bodies to get things going...

    bodies[0] = new Body(tailLength, colors[0], 6E24);
    bodies[1] = new Body(tailLength, colors[1], 6E22, 1000.0, 1.57,
			 200000000.0, 0);
    bodies[2] = new Body(tailLength, colors[2], 6E22, 1600.0, -1.57,
			 -200000000.0, 0);
    numBodies = 3;
  }

  // paintWorld really just asks the mousehandler and the bodies to
  // paint themselves...

  public void paintWorld (Graphics g) {
    int pixX, pixY;
    g.clearRect(0, 0, windowDim, windowDim);
    g.setColor(Color.green);
    g.drawLine(windowDim/2, 0, windowDim/2, windowDim);
    g.drawLine(0, windowDim/2, windowDim, windowDim/2);
    for (int i = 0; i<numBodies; i++) 
      bodies[i].paint(g, windowDim, viewScale);
    mouseHandler.paint(g);
  }

  public void paint (Graphics g) {
    if (offScreen != null) {
      paintWorld(offScreen); 
      g.drawImage(image, 0, 0, this);
    }
    else 
      paintWorld(g);
  }
  
  public static double force (double m1, double m2, double radius) {
    return 6.7E-11 * ((m1*m2)/(radius*radius));
  }
  

  public void start () {
    if (kicker == null) {
      kicker = new Thread(this);
      kicker.start();
    }
  }

  public void stop () {
    if (kicker != null) kicker.stop();
    kicker = null;
  }

  // Mouse events are just passed along to the mousehandler

  public boolean mouseDown(java.awt.Event evt, int x, int y) {
    return mouseHandler.mouseDown(evt, x, y);
  }
  public boolean mouseUp(java.awt.Event evt, int x, int y) {
    return mouseHandler.mouseUp(evt, x, y);
  }
  public boolean mouseDrag(java.awt.Event evt, int x, int y) {
    return mouseHandler.mouseDrag(evt, x, y);
  }
  public boolean mouseMove(java.awt.Event evt, int x, int y) {
    return mouseHandler.mouseMove(evt, x, y);
  }


  public void addObject (int x, int y, double mass, Vector vel) {
    System.out.println(mass + " " + vel.magnitude() + " " + vel.theta());
    if (numBodies < MaxBodies) {
      bodies[numBodies] = 
	new Body(tailLength, colors[numBodies], mass * 1E22, vel.magnitude(), 
		 vel.theta(), (x-windowDim/2)*viewScale, 
		 -(y-windowDim/2)*viewScale);
      numBodies = numBodies + 1;
    }
  }

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


  public void run () {
    Vector tempV;
    double radius, f;
    System.out.println("*** IN RUN\n");
    while (kicker != null) {
      for (int i=0; i<numBodies; i++) 
	bodies[i].updateAccel(bodies, numBodies);
      for (int i=0; i<numBodies; i++)
        bodies[i].update(scaleV);

      repaint();
      try {Thread.sleep(100);} catch (java.lang.InterruptedException e) {}
    }
    kicker = null;
  }


}










