/*
 * Wooble
 * (c) 2000  Lars Huttar
 * Summer Institute of Linguistics
 *
 * Written for JDK 1.3
 */

// As the name suggests, Wooble is a graphics component made for
// use on an About box.  It shows particles woobling over the surface
// of a sphere.  Sort of brownian motion, with repulsion between particles.
// I suppose the name was brought to mind by "wobble" and "bubble".
// But the word is from a Blackadder episode in which Capt. Blackadder
// pretends to be insane, hoping to be sent home from the front in WWI.
// 
// (Edmund wears underpants on his head and two pencils up his nose.)
// 
// Edmund: Right, Baldrick, this is an old trick I picked up in the
// Sudan. We tell HQ that I've gone insane, and I'll be invalided back
// to Blighty before you can say "Wooble" -- a poor gormless idiot.
// 
// Baldrick: But I'm a poor gormless idiot, sir, and I've never been
// invalided back to Blighty.
// 
// Edmund: Yes, Baldrick, but you never said "Wooble."  Now, ask me
// some simple questions.
// 
// Baldrick: Right. What is your name? 
// 
// Edmund: Wooble... 
// 
// Baldrick: What is two plus two? 
// 
// Edmund: Oh, wooble wooble. 
// 
// Baldrick: Where do you live? 
// 
// Edmund: London. 
// 
// Baldrick: Eh? 
// 
// Edmund: A small village on Mars, just outside the capital city, Wooble. 


// To do:
// x add parameters
// - add controls to set params (from a slider etc.); on an About box, let
//   this be on a normally-hidden part of the About box that can be opened
//   with a "More>>" or "Controls>>" button.
// x use color as another clue to distance (how many colors?)
// - sort particles by z-order?  It becomes more important as numDots >100
// x add friction
// - maybe add key controls to "nudge" all dots slightly left, right,
//   up, or down
// - it might be fun to give the dots different colors, so you could
//   track their individual movement better.
// - and/or draw their trails.

// Parameters:
// number of dots
// width & height of picture
// fps
// debugging?
// gravity constant (negative by default)
// dot size
// dampening (friction)
// initVel (initial velocity)
// maxVel (max velocity)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.Math;
import java.util.Random;
import java.util.Comparator;
import java.util.Arrays;

public class Wooble extends JLabel
	                implements ActionListener {
	// Params
	int fps = 20;
	int numDots = 35;
	double dotSize = 0.1;
	boolean debugging = false;
	boolean frame = true;
	double gravity = -0.1; // negative means repulsion
	double friction = 0.01;
	double initVel = 0.1;
	double maxVel = 2; // ???

	final double maxAngle = Math.PI/8;

    Dimension preferredSize = new Dimension(400, 400);
	Timer timer;
	boolean frozen = false;
	Matrix m;
	Vec Vecs[];
	int margin = 10;

	public Wooble(JApplet app) {
		String s;

		// get params
		if ((s = app.getParameter("numDots")) != null)
			numDots = Integer.parseInt(s);
		if ((s = app.getParameter("margin")) != null)
			margin = Integer.parseInt(s);
		if ((s = app.getParameter("fps")) != null)
			fps = Integer.parseInt(s);
		if ((s = app.getParameter("dotSize")) != null)
			dotSize = Double.parseDouble(s);
		if ((s = app.getParameter("debugging")) != null)
			debugging = s.equalsIgnoreCase("true");
		if ((s = app.getParameter("frame")) != null)
			frame = s.equalsIgnoreCase("true");
		if ((s = app.getParameter("gravity")) != null)
			gravity = Double.parseDouble(s);
		if ((s = app.getParameter("friction")) != null)
			friction = Double.parseDouble(s);
		if ((s = app.getParameter("initVel")) != null)
			initVel = Double.parseDouble(s);
		if ((s = app.getParameter("maxVel")) != null)
			maxVel = Double.parseDouble(s);
		
		int delay = (fps > 0) ? (1000 / fps) : 100;

		timer = new Timer(delay, this);
        timer.setInitialDelay(0);
        timer.setCoalesce(true);

		m = new Matrix();
		double[] tmp = new double[3];
		Random rnd = new Random();

		Vecs = new Vec[numDots];
		for (int i = 0; i < numDots; i++) {
			Vecs[i] = new Vec(1, 0, 0);
			Vec sp = Vecs[i];

			sp.vel[2] = 1;		// acceleration = <0, 0, 1>

			for (int j = 0; j < 3; j++) {
				// Set tmp = vel x p
				Vec.cross(sp.vel, sp.p, tmp);
				// then rotate vel and p around tmp by a random amount.
				// This is probably not efficient but it's simple.
				m.rotateAboutV(tmp, rnd.nextDouble() * 2* Math.PI);
				sp.transform(m, true);
				// Now set vel off at a right angle.
				Vec.cross(sp.vel, sp.p, tmp);
				Vec.copyVec(tmp, sp.vel);
				Vec.normalize(sp.vel);
				// Make sure these don't spiral down to zero through
				// numerical error.
				Vec.normalize(sp.p);
			}
			sp.velMag = initVel;
		}
	}

    public void start() {
        startAnimation();
    }

    // Invoked by the browser only.  invokeLater not needed
    // because stopAnimation can be called from any thread.
    public void stop() {
        stopAnimation();
    }

    // Can be invoked from any thread.
    public synchronized void startAnimation() {
        if (frozen) { 
            // Do nothing.  The user has requested that we 
            // stop changing the image.
        } else {
            // Start animating!
            if (!timer.isRunning()) {
                timer.start();
            }
        }
    }

    //Can be invoked from any thread.
    public synchronized void stopAnimation() {
        //Stop the animating thread.
        if (timer.isRunning()) {
            timer.stop();
        }
    }

    public Dimension getPreferredSize() {
        return preferredSize;
    }

    public void paintComponent(Graphics g) {
        Insets insets = getInsets();
        int cWidth = getWidth() - insets.left - insets.right - margin*2;
        int cHeight = getHeight() - insets.top - insets.bottom - margin*2;
		int cx = insets.left + margin + (int)(cWidth*0.5);
		int cy = insets.top + margin + (int)(cHeight*0.5);

		// Put up a reference frame.
		if (frame)
			g.drawOval(insets.left + margin, insets.top + margin,
					   cWidth, cHeight);

		int i, sgn, x, y, r, axis;
		float z, gray, baser;
		axis = (cWidth < cHeight ? cWidth : cHeight);
		baser = (float)(dotSize * axis * 0.5);
		g.setColor(Color.black);

		// We use sgn instead of a full z-order sort.  All that matters
		// is that the rear ones are drawn before the front ones.

		for (sgn = -1; sgn <= 1; sgn += 2) {
			for (i = 0; i < Vecs.length; i++) {
				z = (float)Vecs[i].p[2];
				if ((sgn < 0) == (z < 0)) {
					gray = (float)(0.35 - z*0.35);
					g.setColor(new Color(gray, gray, gray));
					r = (int)(baser/(4 - z));
					if (r < 1) r = 1;
					x = cx + (int)(Vecs[i].p[0] * cWidth * 0.5 - r);
					y = cy + (int)(Vecs[i].p[1] * cHeight * 0.5 - r);
					g.drawOval(x, y, r*2, r*2);
					// if (Vecs[i].p[2] >= 0)
					g.fillOval(x, y, r*2, r*2);
				}
			}
		}

        super.paintComponent(g);  // paint label
    }

    public void actionPerformed(ActionEvent e) {
		int i;
		double angle;
		
		// Now change the velocity of each point, based on its
		// proximity to the others.
		accelerate();

		// Move the dots based on their velocity.
		for (i = 0; i < Vecs.length; i++) {
			Vec sp = Vecs[i];

			// Set tmp = vel x p
			Vec.cross(sp.vel, sp.p, tmp);
			// then rotate vel and p around tmp.
			// This is probably not efficient but it's simple.
			angle = sp.velMag * 0.1 / Math.PI; // adjust coeff?
			// if (angle > maxAngle)
			//    angle = maxAngle;
			m.rotateAboutV(tmp, angle);
			sp.transform(m, true);
			Vec.normalize(sp.p);
		}

		if (debugging)
			System.out.println(Vecs[0].toString());
        // Request that the frame be painted.
		repaint();
    }

	double[] tmp = new double[3];

	// accelerate all points with repulsion/attraction
	void accelerate() {
		double d, accmag;
		double[] a;
		
		int i, j;

		// initialize all accelerations to zero
		for (i=0; i < Vecs.length; i++) {
			Vecs[i].acc[0] = 0;
			Vecs[i].acc[1] = 0;
			Vecs[i].acc[2] = 0;
		}

		// Tally up all accelerations due to gravity
		for (i = 0; i < Vecs.length; i++) {
			for (j = i + 1; j < Vecs.length; j++) {
				Vec.subtract3(Vecs[i].p, Vecs[j].p, tmp);
				d = Vec.normalize(tmp);
				if (d == 0) continue;
				accmag = -gravity/(d*d); // why negative?
				Vec.scale(tmp, accmag);
				Vec.subtract(Vecs[i].acc, tmp);
				Vec.add(Vecs[j].acc, tmp);
			}
		}
	   
		// Now put those accelerations into effect, changing velocity.
		for (i = 0; i < Vecs.length; i++) {
			Vec sp = Vecs[i];
			Vec.scale(sp.vel, sp.velMag);
			Vec.add(sp.vel, sp.acc);
			sp.velMag = Vec.mag(sp.vel); // save magnitude of velocity
			sp.velMag *= (1.0 - friction);
			if (sp.velMag > maxVel)	// maximum velocity
			    sp.velMag = maxVel;
			// Now make sure vel is perp to p.
			// This can be done with cross- or dot-products: 
			// v := p x (v x p) = (p . p)v - (p . v)p
			Vec.cross(sp.vel, sp.p, tmp);
			Vec.cross(sp.p, tmp, sp.vel);
			Vec.normalize(sp.vel); // get rid of the change in magnitude
		}
	}
}
