//
// JJB 2000

import java.awt.*;
import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.event.*;
import com.sun.j3d.utils.behaviors.picking.*;
import com.sun.j3d.utils.image.TextureLoader;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.universe.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import java.io.*;
import java.util.*;
import com.sun.j3d.utils.behaviors.mouse.*;


public class Blobs extends Applet {
	Canvas3D C;
	Dimension CD;
	Label L,Title;
	int nBlobs = 100;
	int nTypes = 3;
	double Safety = 0.30;
	double Close = 0.10;
	double Eat = 0.05;
    Thread T; 
	TransformGroup objTrans;

	Vector blobs;
	
	final double XBOX = 0.5;
	final double YBOX = 0.5;
	final double ZBOX = 0.5;
	final double VMAX = 0.01;

	String blobTypes[]			= {"Shark",						"Fish",							"FishLeader"};
	double blobProbabilities[]	= {0.01,						0.95,							0.05};
	float blobSizes[]			= {0.05f,						0.02f,							0.02f};
	Color3f blobColors[]		= {new Color3f(1.0f,0.0f,0.0f),	new Color3f(0.0f,1.0f,0.0f),	new Color3f(1.0f,1.0f,0.0f)};

	int blobCounts[];

    public Blobs() {
		setLayout(new BorderLayout());
		C = new Canvas3D(null);
		add("Center", C);
		BranchGroup scene = createSceneGraph();
		SimpleUniverse u = new SimpleUniverse(C);
        u.getViewingPlatform().setNominalViewingTransform();
		u.addBranchGraph(scene);
    }

    public static void main(String[] args) {
		new MainFrame(new Blobs(), 400, 400);
    }
    
	public BranchGroup createSceneGraph() {

		// Create the root of the branch graph
		BranchGroup objRoot = new BranchGroup();

		TransformGroup objScale = new TransformGroup();
		Transform3D sMat = new Transform3D();
		sMat.setScale(1.0);
		objScale.setTransform(sMat);

		objTrans = new TransformGroup();
		objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		objTrans.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
		objTrans.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
		objTrans.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);


		objRoot.addChild(objTrans);

		objTrans.addChild(objScale);

		BoundingSphere bounds =
			new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);

		BranchGroup collideBG = new BranchGroup();
		collideBG.setCapability(BranchGroup.ALLOW_DETACH);

		blobs = new Vector(nBlobs);

		blobCounts = new int[nTypes];
		for(int i=0;i<nTypes;i++) blobCounts[i] = 0;

		for(int i=0;i<nBlobs;i++) {
			boolean selected = false;
			int j=0;
			
			while(!selected) {
				j = (int) (Math.random()*0.9999*(double)nTypes);
				//if(blobTypes[j].equals("FishLeader") && blobCounts[j] > 0) continue;
				//if(blobTypes[j].equals("Shark") && blobCounts[j] > 0) continue;
				if(Math.random() < blobProbabilities[j]) {
					selected = true;
					blobCounts[j]++;
				}
			}
			double x = -XBOX + 2.0*Math.random()*XBOX;
			double y = -YBOX + 2.0*Math.random()*YBOX;
			double z = -ZBOX + 2.0*Math.random()*ZBOX;
			double Vx = 0.0;
			double Vy = 0.0;
			double Vz = 0.0;
			if(!blobTypes[j].equals("Fish")) {
				Vx = -VMAX + 2.0*Math.random()*VMAX;
				Vy = -VMAX + 2.0*Math.random()*VMAX;
				Vz = -VMAX + 2.0*Math.random()*VMAX;
			} 

			Blob b = new Blob(j,x,y,z,Vx,Vy,Vz);

			blobs.addElement(b);

			objTrans.addChild(b.blobBG);


		}

		for(int i=0;i<nTypes;i++) {
			System.out.println("Generated "+blobCounts[i]+" "+blobTypes[i]);
		}

		Alpha collideAlpha = new Alpha(-1, Alpha.INCREASING_ENABLE |
				     Alpha.DECREASING_ENABLE,
				     0, 0,
				     2000, 1000, 200,
				     2000, 1000, 200);

		// Create the collide behavior
		CollideBehavior cBeh = new CollideBehavior(collideAlpha,objTrans);
		cBeh.setSchedulingBounds(bounds);
		objRoot.addChild(cBeh);

        // Set up the background
        Color3f bgColor = new Color3f(0.1f, 0.0f, 0.4f);
        Background bgNode = new Background(bgColor);
        bgNode.setApplicationBounds(bounds);
        objRoot.addChild(bgNode);

        // Set up the ambient light
        Color3f ambientColor = new Color3f(0.8f, 0.1f, 0.1f);
        AmbientLight ambientLightNode = new AmbientLight(ambientColor);
        ambientLightNode.setInfluencingBounds(bounds);
        objRoot.addChild(ambientLightNode);

        Color3f light1Color = new Color3f(1.0f, 1.0f, 0.9f);
        Vector3f light1Direction  = new Vector3f(4.0f, -7.0f, -12.0f);
        DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction);
        light1.setInfluencingBounds(bounds);
        objRoot.addChild(light1);

		// Create the rotate behavior node
	    MouseRotate behavior = new MouseRotate();
		behavior.setTransformGroup(objTrans);
		objRoot.addChild(behavior);
		behavior.setSchedulingBounds(bounds);

        // Have Java 3D perform optimizations on this scene graph.
        objRoot.compile();

		return objRoot;
    }


   public String getAppletInfo () {
      return "Blobs by J.J.B.";
   }


   class Blob {

		double Xpos;
		double Ypos;
		double Zpos;
		double Vx;
		double Vy;
		double Vz;
		float size;
		int type;
		String name;
		Blob nearest[];
		int inearest[];
		double dmin[];
		double dtime = 1.0; // unit time step on calculations

		BranchGroup    blobBG;
		TransformGroup blobTG;
		Transform3D    blobMat;

		Blob (int _type, double X, double Y, double Z, 
			double Vx0, double Vy0, double Vz0) {
			Xpos = X;
			Ypos = Y;
			Zpos = Z;
			Vx = Vx0;
			Vy = Vy0;
			Vz = Vz0;
			type = _type;
			name = new String(blobTypes[type]);
			size = blobSizes[type];
			nearest = new Blob[nTypes];
			inearest = new int[nTypes];
			dmin = new double[nTypes];

			Appearance apBlob= new Appearance();
			Material mm = new Material();
			mm.setLightingEnable(true);
			mm.setEmissiveColor(blobColors[type]);
			apBlob.setMaterial(mm);

			//       TextureLoader BlackBlob1Tex = new TextureLoader(new String("earth.jpg"), new String("RGB"));
			//       if (BlackBlob1Tex != null) apBlackBlob.setTexture(BlackBlob1Tex.getTexture());

			Sphere s = new Sphere(size,Sphere.GENERATE_NORMALS|Sphere.GENERATE_TEXTURE_COORDS,apBlob);
			blobMat = new Transform3D();
			blobTG = new TransformGroup(blobMat);
			blobTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
			blobTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ );

			blobMat.set(new Vector3d(Xpos,Ypos,Zpos));
			blobTG.setTransform(blobMat);
			blobTG.addChild(s);

			blobBG = new BranchGroup();
			blobBG.setCapability(BranchGroup.ALLOW_DETACH);

			blobBG.addChild(blobTG);

		}


		void update(double accelx, double accely, double accelz) {

			// find index of nearest blob of each type

			for(int i=0;i<nTypes;i++) {
				dmin[i] = 999.0;
				nearest[i] = null;
				inearest[i] = -1;
			}

			for(int i=0;i<blobs.size();i++) {

				Blob b = (Blob) blobs.elementAt(i);
				if(b != this) {

					double d = Math.sqrt( (Xpos-b.Xpos)*(Xpos-b.Xpos) + (Ypos-b.Ypos)*(Ypos-b.Ypos) + (Zpos-b.Zpos)*(Zpos-b.Zpos));
					if(d < dmin[b.type]) {
						dmin[b.type] = d;
						nearest[b.type] = b;
						inearest[b.type] = i;
					}

				}
			}

			
			int i=0;

			if(name.equals("Shark")) {
				// Adjust velocity towards that of nearest fish unless already very close

				double d = 999.0;
				Blob b = null;
				int in = -1;
				for(i=0;i<nTypes;i++) {
					if(i != type) {
						if(dmin[i] < d) {
							d = dmin[i];
							b = nearest[i];
							in = inearest[i];
						}
					}
				}

				if(b != null) {

					double dx = (Xpos-b.Xpos)/Math.abs(Xpos-b.Xpos);
					double dy = (Ypos-b.Ypos)/Math.abs(Ypos-b.Ypos);
					double dz = (Zpos-b.Zpos)/Math.abs(Zpos-b.Zpos);
	
					Vx = -1.0*dx*VMAX;
					Vy = -1.0*dy*VMAX;
					Vz = -1.0*dz*VMAX;
				}

				// Eat the fish if it's close enough

				if(d < Eat && b != null) {
					System.out.println("Eating object "+in);
					b.blobBG.detach();
					blobs.removeElementAt(in);
				}

			}

			if(name.equals("Fish")) {


				for(i=0;i<nTypes;i++) {

					Blob b = nearest[i];
					if(b == null) continue;

					if(blobTypes[i].equals("FishLeader")) {

						// Adjust position and velocity vector towards that of the FishLeader
						if(dmin[i] > 0.0) {
							double dx = (Xpos-b.Xpos)/Math.abs(Xpos-b.Xpos);
							double dy = (Ypos-b.Ypos)/Math.abs(Ypos-b.Ypos);
							double dz = (Zpos-b.Zpos)/Math.abs(Zpos-b.Zpos);

							Vx += -0.5*dx*VMAX; 
							Vy += -0.5*dy*VMAX;
							Vz += -0.5*dz*VMAX;

							Vx += 0.01*(b.Vx-Vx);
							Vy += 0.01*(b.Vy-Vy);
							Vz += 0.01*(b.Vz-Vz);

						}
					} 

					if(blobTypes[i].equals("Fish")) {
						// Adjust velocity towards that of nearest fish unless already very close
						if(dmin[i] > Close) {
	
							Vx += 0.01*(b.Vx-Vx);
							Vy += 0.01*(b.Vy-Vy);
							Vz += 0.01*(b.Vz-Vz);
						}
					}
				}
			} 

			if(name.equals("FishLeader")) {

				// Leader fish change direction randomly periodically

				if(Math.random() < 0.05) {
					Vx += 0.001*(Math.random()-0.5)*VMAX;
					Vy += 0.001*(Math.random()-0.5)*VMAX;
					Vz += 0.001*(Math.random()-0.5)*VMAX;
				}
			}

			// Swim away from nearest Shark

			if(!name.equals("Shark")) {

				for(i=0;i<nTypes;i++) {
					if(!blobTypes[i].equals("Shark")) continue;
					Blob b = nearest[i];

					if(b != null && dmin[i] < Safety) {
	
						double dx = (Xpos-b.Xpos)/Math.abs(Xpos-b.Xpos);
						double dy = (Ypos-b.Ypos)/Math.abs(Ypos-b.Ypos);
						double dz = (Zpos-b.Zpos)/Math.abs(Zpos-b.Zpos);

						Vx = 0.5*dx*VMAX;
						Vy = 0.5*dy*VMAX;
						Vz = 0.5*dz*VMAX;
					}
				}
			}

			// Always tend to return to the centre

			Vx -= 0.0001*Xpos/dtime;
			Vy -= 0.0001*Ypos/dtime;
			Vz -= 0.0001*Zpos/dtime;


			Xpos += (Vx*dtime) + (0.5*accelx*dtime*dtime);
			Ypos += (Vy*dtime) + (0.5*accely*dtime*dtime);
			Zpos += (Vz*dtime) + (0.5*accelz*dtime*dtime);
			Vx += accelx*dtime;
			Vy += accely*dtime;
			Vz += accelz*dtime;

			blobMat.set(new Vector3d(Xpos, Ypos, Zpos));
			blobTG.setTransform(blobMat);


		}
		
	}

		


	class CollideBehavior extends Behavior {

		Transform3D t1;
		Matrix3d mat1,rotx,roty,rotz;
		double direction_scale = -0.0001;
		double dphi = Math.PI/300;
		double lastTime = 0;
		double Vtangential=1.0;
		double Vradial=0.;
		double prob=0.05;
		int nsteps = 0;
		int ncounts = 0;

	    WakeupOnElapsedFrames w = new WakeupOnElapsedFrames(0);

		public void initialize() {
			//alpha.setStartTime(System.currentTimeMillis());
			wakeupOn(w);
		}

	    public void processStimulus(Enumeration criteria) {

			nsteps++;
			if(nsteps == 40) {
				double thisTime = System.currentTimeMillis();
				int fps = 0;
				if(thisTime > lastTime) fps = (int) ( nsteps*1000/(thisTime-lastTime) );
				lastTime = thisTime;
				System.out.println("Frame Rate = "+fps+" frames/sec");
				nsteps = 0;
				ncounts++;
				//if(ncounts > 40) direction_scale = -direction_scale;

			}

			//tg.getTransform(t1);
			double scale = t1.getScale();
		
			scale += direction_scale;
			//t1.setScale(scale);

			t1.getRotationScale(mat1);
			mat1.mul(rotx);
			mat1.mul(roty);
			mat1.mul(rotz);
			//t1.setRotationScale(mat1);

			//tg.setTransform(t1);

			for(int i=0;i<blobs.size();i++) {

				double accelx = 0.;
				double accely = 0.;
				double accelz = 0.;

				Blob b = (Blob) blobs.elementAt(i);

				b.update(accelx,accely,accelz);
			}

			// Set wakeup criteria for next time
			wakeupOn(w);
		}

	    public CollideBehavior(Alpha A, TransformGroup T) {
			t1 = new Transform3D();
			mat1 = new Matrix3d();
			rotx = new Matrix3d();
			rotx.rotX(dphi);
			roty = new Matrix3d();
			roty.rotY(-dphi);
			rotz = new Matrix3d();
			rotz.rotZ(-dphi);
			nsteps = 0;
		}
	}

}

class MouseRotate extends MouseBehavior {
    double x_angle, y_angle;
    double x_factor = .03;
    double y_factor = .03;

  private MouseBehaviorCallback callback = null;


  /**
   * Creates a rotate behavior given the transform group.
   * @param transformGroup The transformGroup to operate on.
   */
  public MouseRotate(TransformGroup transformGroup) {
    super(transformGroup);
  }

  /**
   * Creates a default mouse rotate behavior.
   **/
  public MouseRotate() {
      super(0);
  }

  /**
   * Creates a rotate behavior.
   * Note that this behavior still needs a transform
   * group to work on (use setTransformGroup(tg)) and
   * the transform group must add this behavior.
   * @param flags interesting flags (wakeup conditions).
   */
  public MouseRotate(int flags) {
      super(flags);
   }

  public void initialize() {
    super.initialize();
    x_angle = 0;
    y_angle = 0;
    if ((flags & INVERT_INPUT) == INVERT_INPUT) {
       invert = true;
       x_factor *= -1;
       y_factor *= -1;
    }
  }

  /**
   * Return the x-axis movement multipler.
   **/

  public double getXFactor() {
    return x_factor;
  }
  
  /**
   * Return the y-axis movement multipler.
   **/

  public double getYFactor() {
    return y_factor;
  }
  
  /**
   * Set the x-axis amd y-axis movement multipler with factor.
   **/

  public void setFactor( double factor) {
    x_factor = y_factor = factor;
    
  }
  
  /**
   * Set the x-axis amd y-axis movement multipler with xFactor and yFactor
   * respectively.
   **/

  public void setFactor( double xFactor, double yFactor) {
    x_factor = xFactor;
    y_factor = yFactor;    
  }

  public void processStimulus (Enumeration criteria) {
      WakeupCriterion wakeup;
      AWTEvent[] event;
      int id;
      int dx, dy;

      while (criteria.hasMoreElements()) {
         wakeup = (WakeupCriterion) criteria.nextElement();
         if (wakeup instanceof WakeupOnAWTEvent) {
            event = ((WakeupOnAWTEvent)wakeup).getAWTEvent();
            for (int i=0; i<event.length; i++) { 
	      processMouseEvent((MouseEvent) event[i]);

	      if (((buttonPress)&&((flags & MANUAL_WAKEUP) == 0)) ||
		  ((wakeUp)&&((flags & MANUAL_WAKEUP) != 0))){
		
		id = event[i].getID();
		if ((id == MouseEvent.MOUSE_DRAGGED) && 
		    !((MouseEvent)event[i]).isMetaDown() && 
		    !((MouseEvent)event[i]).isAltDown()){
		  
                  x = ((MouseEvent)event[i]).getX();
                  y = ((MouseEvent)event[i]).getY();

                  dx = x - x_last;
                  dy = y - y_last;

		  if (!reset){	    
		    x_angle = dy * y_factor;
		    y_angle = dx * x_factor;
		    
		    transformX.rotX(x_angle);
		    transformY.rotY(y_angle);
		    
		    transformGroup.getTransform(currXform);
		    
		    //Vector3d translation = new Vector3d();
		    //Matrix3f rotation = new Matrix3f();
		    Matrix4d mat = new Matrix4d();
		    
		    // Remember old matrix
		    currXform.get(mat);
		    
		    // Translate to origin
		    currXform.setTranslation(new Vector3d(0.0,0.0,0.0));
		    if (invert) {
			currXform.mul(currXform, transformX);
			currXform.mul(currXform, transformY);
		    } else {
			currXform.mul(transformX, currXform);
			currXform.mul(transformY, currXform);
		    }
		    
		    // Set old translation back
		    Vector3d translation = new 
		      Vector3d(mat.m03, mat.m13, mat.m23);
		    currXform.setTranslation(translation);
		    
		    // Update xform
		    transformGroup.setTransform(currXform);

		    transformChanged( currXform );

                    if (callback!=null)
                        callback.transformChanged( MouseBehaviorCallback.TRANSLATE,
                                               currXform );


		  }
		  else {
		    reset = false;
		  }

                  x_last = x;
                  y_last = y;
               }
               else if (id == MouseEvent.MOUSE_PRESSED) {
                  x_last = ((MouseEvent)event[i]).getX();
                  y_last = ((MouseEvent)event[i]).getY();
               }
	      }
	    }
         }
      }

      wakeupOn (mouseCriterion);
      
   }

  /**
    * Users can overload this method  which is called every time
    * the Behavior updates the transform
    *
    * Default implementation does nothing
    */
  public void transformChanged( Transform3D transform ) {
  }

  /**
    * The transformChanged method in the callback class will
    * be called every time the transform is updated
    */
  public void setupCallback( MouseBehaviorCallback callback ) {
      this.callback = callback;
  }
}





           
