import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.Math;

public class NodesApplet extends java.applet.Applet {
    NodeCanvas ncan;
    NodeControls nctl;
    boolean fileOK = false;

    public void init() {
        setLayout(new BorderLayout());
	ncan = new NodeCanvas();
	ncan.setFont(new Font("Helvetica", Font.BOLD, 12));
        add("Center", ncan);
	nctl = new NodeControls(ncan, fileOK);
	nctl.setFont(new Font("Helvetica", Font.PLAIN, 12));
        add("South", nctl);
    }
    public void start() {
	ncan.start();
    }
    public void stop() {
	ncan.stop();
    }

    public static void main(String args[]) {
	Frame f = new Frame("Nodes");
	NodesApplet nodes = new NodesApplet();

	nodes.fileOK = true;
	nodes.init();
	nodes.start();

	f.add("Center", nodes);
	f.setSize(800,600);
	f.show();
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {System.exit(0);}
        });	
    }
}
class NodeCanvas extends Canvas implements Runnable {
    static int framePause = 30;
    Thread thread;
    NodeModel model;
    Image offscreen;
    Dimension offscreensize;
    boolean dbg = false;
    FileWriter fout;

    NodeCanvas() {
	setBackground(Color.lightGray);
	model = new NodeModel(this);
        addMouseListener(new MouseAdapter() {
		public void mousePressed(MouseEvent e){model.mousePressed(e);}
		public void mouseReleased(MouseEvent e){model.mouseReleased(e);}
	    });
        addMouseMotionListener(new MouseMotionAdapter() {
		public void mouseDragged(MouseEvent e){model.mouseDragged(e);}
	    });
    }
    public void start() {
	thread = new Thread(this, "Canvas");
	thread.start();
    }
    public void stop() {
	thread = null;
    }
    public void run() {
        Thread me = Thread.currentThread();
	while (thread == me) {
	    model.recalc();
	    repaint();
	    try {Thread.sleep(framePause);} catch (Exception e) {}
	}
    }
    public void paint(Graphics g) {
	update(g);
    }
    public synchronized void update(Graphics gcan) {
	Dimension d = getSize();
	if ((offscreen == null) || (!d.equals(offscreensize)) ) {
	    offscreen = createImage(d.width, d.height);
	    offscreensize = d;
	    offscreen.getGraphics().setFont(getFont());
	}
	Graphics g = offscreen.getGraphics();
	g.setColor(getBackground());
       	g.fillRect(0, 0, d.width, d.height);
	g.setColor(getForeground());
       	g.drawRect(0, 0, d.width-1, d.height-1);
	model.repaint(g, d);
	gcan.drawImage(offscreen, 0, 0, null);
    }

    void debug (String s) {if (dbg) System.out.println(s);}
    void debugs (String s) {if (dbg) System.out.print(s);}
    void log (String s) {
	if (fout!=null) try {fout.write(s+"\n");} catch (Exception e) {}
    }
    void setlog(boolean dbg, FileWriter fout) {
	this.dbg = dbg;
	this.fout = fout;
    }
    void reset(String edge, int n, double g, double e, double t,
	       boolean rbd, boolean linear) {
	model.reset(edge, n, g, e, t, rbd, linear);
    }
}

class Node {
    static int size = 22;
    double x,y,vx,vy;
    String lbl;
    boolean fixed = false;
    Node (String lbl){
	x = y = vx = vy = 0.0;
	this.lbl = lbl;
    }	
    Node (String lbl, Dimension d, Point v, boolean random){
	x = d.width; // sub size/2??
	y = d.height;
	vx = v.x;
	vy = v.y;
	this.lbl = lbl;
	if (random) {
	    x *= Math.random();
	    y *= Math.random();
	    vx *= 2*(Math.random()-0.5);
	    vy *= 2*(Math.random()-0.5);
	}
    }	
    void paint(Graphics g) {
	FontMetrics fm = g.getFontMetrics();
	int sw = fm.stringWidth(lbl);
	int sh = fm.getAscent();
	g.setColor(fixed?Color.red:Color.yellow);
       	g.fillRect((int)x, (int)y, size, size);
	g.setColor(Color.black);
       	g.drawRect((int)x, (int)y, size-1, size-1);
	g.drawString(lbl, (int)x+(size-sw)/2, (int)y+(size+sh)/2);
    }
}
class Edge {
    Node from,to;
    double tension;
    Edge(Node from, Node to) {
	this.from = from;
	this.to = to;
    }
    Edge(Node[] node, int i, int j) {
	this(node[i],node[j]);
    }
    void paint(Graphics g) {
	int j = Node.size/2;
	g.setColor(tension<1?Color.black : (tension<2?Color.orange:Color.red));
	g.drawLine((int)from.x+j, (int)from.y+j,
		   (int)to.x+j, (int)to.y+j);
    }
}
class Nodes {
    static String styles[]={"Line","Star","Loop","Pendulum",
			    "Oscillator","None"};
    Node[] node;
    Edge[] edge;
    String style;
    Nodes(int num, String style, Dimension d, Point v){
	node = new Node[num];
	this.style = style;
	for (int i=0; i<num; i++)
	    node[i] = new Node (""+i, d, v, true);
	if (style.equals("Pendulum")||style.equals("Oscillator")) {
	    node[0].fixed = true;
	    node[0].x = (d.width-Node.size)/2;
	    node[0].vy = node[0].vy = node[0].y = 0;
	}
	if (style.equals("Oscillator")) {
	    int n = num-1;
	    node[n].fixed = true;
	    node[n].x = node[0].x;
	    node[n].y = d.height-Node.size;
	    node[n].vy = node[n].vy = 0;
	}

	if (style.equals("Loop")) {
	    edge = new Edge[num];
	    for (int i=0; i<edge.length; i++)
		edge[i] = new Edge(node, i, (i+1)%num);
	} else if (style.equals("Star")) {
	    edge = new Edge[num-1];
	    for (int i=0; i<edge.length; i++)
		edge[i] = new Edge(node, 0,i+1);
	} else if (style.equals("Line")||style.equals("Pendulum")) {
	    edge = new Edge[num-1];
	    for (int i=0; i<edge.length; i++)
		edge[i] = new Edge(node, i,i+1);
	} else if (style.equals("None")) {
	    edge = new Edge[0];
	} else { // Line, Oscillator, Pendulum
	    edge = new Edge[num-1];
	    for (int i=0; i<edge.length; i++)
		edge[i] = new Edge(node, i, i+1);
	}
    }
    void paint(Graphics g) {
	for (int i=0; i<edge.length; i++)
	    edge[i].paint(g);
	for (int i=0; i<node.length; i++)
	    node[i].paint(g);
    }
    Node closestNode(int x, int y) {
	Node n0 = null;
	double d0 = Double.MAX_VALUE;
	for (int i=0; i<node.length; i++) {
	    Node n = node[i];
	    double d = (x-n.x)*(x-n.x) + (y-n.y)*(y-n.y);
	    if (d < d0) {
		d0 = d;
		n0 = n;
	    }
	}
	return n0;
    }
}

class NodeModel {
    Nodes nodes;
    NodeCanvas can;
    boolean mouseDown = false;
    int sqSize = 22;

    double grav = 2.0;
    double elasticity = 0.80;
    double tension = 0.01;  //.01->.02
    boolean hardEdge = true;
    boolean rebound = true;
    boolean linear = true;  //linear or inverse square

    NodeModel(NodeCanvas can) {
	this.can = can;
    }
    void createNodes(String style, int num) {
	nodes = new Nodes(num, style, can.getSize(), new Point(4, 4));
    }
    public synchronized void recalc() {
	Dimension d = can.getSize();
       	double w = d.width - sqSize;
       	double h = d.height - sqSize;

	if (nodes == null)
	    createNodes(Nodes.styles[0], 5);
	if (!mouseDown) {
	    String log="";
	    for (int i=0; i<nodes.node.length; i++) {
		Node n=nodes.node[i];
		if (!n.fixed) {
		    n.x += n.vx;
		    n.y += n.vy;
		    if (hardEdge) {
			n.x = Math.max(0, Math.min(w, n.x));
			n.y = Math.max(0, Math.min(h, n.y));
		    }

		    can.debugs("Node("+i+"): x="+(int)n.x+" y="+(int)n.y+
			" w="+w+" h="+h+" vX="+(int)n.vx+" vY="+(int)n.vy);
		    if (log.length()!=0)
			log=log+"\t";
		    log=log+(int)n.x+" "+(int)n.y+" "+(int)n.vx+" "+(int)n.vy;

		    if (hardEdge) {
			if (n.x==0 || n.x==w)
			    n.vx = -n.vx*elasticity;

			if (n.y==0)
			    n.vy = 1-n.vy*elasticity;
			else if (n.y==h) {
			    if (n.vy<=(grav+2) && rebound) {
				//rebound just below top & adj for +grav below
				n.vy = -Math.sqrt(2*grav*h)-grav;
				if (Math.abs(n.vx) <= 1)
				    n.vx = Math.random()*10 - 5;
			    } else
				n.vy = -Math.min(n.vy*elasticity,n.vy-4);
			}
		    }

		    n.vy += grav; //keep independent of rebound calc

		    can.debug(" vX'=" +(int)n.vx+ " vY'=" +(int)n.vy);
		}
	    }
	    can.log(log);
	}
	for (int i=0; i<nodes.edge.length; i++) {
	    Edge e = nodes.edge[i];
	    double dx = e.to.x-e.from.x;
	    double dy = e.to.y-e.from.y;
	    double r2 = Math.max(sqSize*sqSize, dx*dx+dy*dy);
	    double r = Math.sqrt(r2);
	    double Fx, Fy;
	    if (linear) {
		Fx = dx*tension;
		Fy = dy*tension;
		e.tension = r*tension;
	    } else {
		e.tension = tension/r2;
		Fx = (tension*dx)/(r*r2);
		Fy = (tension*dy)/(r*r2);
	    }
	    if (!mouseDown) {
		e.from.vx += Fx;
		e.from.vy += Fy;
		e.to.vx -= Fx;
		e.to.vy -= Fy;
		can.debug( "Edge(" +i+ "): tension=" +(float)(e.tension)+
			   " dx=" +(int)dx+ " dy=" +(int)dy+
			   " Fx=" +(float)Fx+ " Fy=" +(float)Fy);
	    }
	}
    }
    public synchronized void repaint(Graphics g, Dimension d) {
	nodes.paint(g);
    }
    void reset(String edge, int n, double g, double e, double t,
	       boolean rbd, boolean linear) {
	this.grav = g;
	this.elasticity = e;
	this.tension = t*(linear?(.01):(10000));
	this.rebound = rbd;
	this.linear = linear;
	hardEdge = (rebound) || (elasticity != 0); //huristic.
	// this lets fixed nodes & other behaviors stay as they were
	if ( !(nodes.node.length == n && nodes.style.equals(edge)) )
	    createNodes(edge, n);
    }
    Node nDrag;
    void mouseDragged(MouseEvent e){
	Dimension d = can.getSize();
       	double w = d.width - sqSize;
       	double h = d.height - sqSize;
	nDrag.x = Math.max(0, Math.min(w, e.getX()-sqSize/2));
	nDrag.y = Math.max(0, Math.min(h, e.getY()-sqSize/2));
	can.repaint();
    }
    void mouseReleased(MouseEvent e){
	mouseDown = false;
	nDrag.vx = 0;
	nDrag.vy = 0;
    }
    void mousePressed(MouseEvent e){
	mouseDown=true;
	nDrag = nodes.closestNode(e.getX()-sqSize/2, e.getY()-sqSize/2);
	if (e.getClickCount()>1)
	    nDrag.fixed = !nDrag.fixed;
	mouseDragged(e);
    }
}



class NodeControls extends Panel implements ActionListener {
    TextField nodesField, gravityField, elasticityField, tensionField;
    Choice reboundChoice, edgeChoice, fileChoice, forceChoice;
    NodeCanvas ncan;
    boolean fileOK;
    FileWriter out;

    public void actionPerformed(ActionEvent ev) {
	if (out!=null) try {out.close();} catch (Exception e) {}
	out=null;

	String edge = edgeChoice.getSelectedItem();
	int nodes = Integer.valueOf(nodesField.getText()).intValue();
	double g = Double.valueOf(gravityField.getText()).doubleValue();
	double e = Double.valueOf(elasticityField.getText()).doubleValue();
	double t = Double.valueOf(tensionField.getText()).doubleValue();
	boolean rbd = reboundChoice.getSelectedItem().endsWith("On");
	boolean linear = forceChoice.getSelectedItem().equals("Linear");
	if (fileChoice!=null && fileChoice.getSelectedItem().endsWith("On")) {
	    String s = "N"+nodes+"-G"+g+"-E"+e+"-T"+t+(linear?"-FL":"-FI");
	    try {out=new FileWriter(s);} catch (Exception e1) {
		System.out.println("FileWriter!" + e1);
	    }
	}
	ncan.reset(edge, nodes, g, e, t, rbd, linear);
	ncan.setlog(false, out);
	ncan.debug("Style=" +edge+ " dbg=" +rbd+
		   " n=" +nodes+ " g=" +g+ " e=" +e+ " t=" +t);
    }
    protected Button makebutton(String name) {
        Button item = new Button(name);
        add(item);
	return item;
    }
    protected Label makelabel(String name) {
        Label item = new Label(name);
        add(item);
	return item;
    }
    protected TextField maketext(String data) {
        TextField item = new TextField(data);
        add(item);
	return item;
    }
    protected Choice makechoice(String [] items) {
        Choice item = new Choice();
	for (int i=0; i<items.length; i++)
	    item.add(items[i]);
        add(item);
	return item;
    }

    public NodeControls(NodeCanvas ncan, boolean fileOK) {
	this.ncan = ncan;
	this.fileOK = fileOK;
 
        setLayout(new GridLayout(2, 8));
   
	String force[] = {"Linear","InverseSq"};
        forceChoice = makechoice(force);
	String files[] = {"File Off", "File On"};
	if (fileOK)
	    fileChoice = makechoice(files);
	else
	    makelabel("");  //empty: to manage row
        makelabel("EdgeStyle:");
        edgeChoice = makechoice(Nodes.styles);
        makelabel("");  //empty: to manage row
	String rebounds[] = {"Rebound On","Rebound Off"};
        reboundChoice = makechoice(rebounds);
        makelabel("");  //empty: to manage row
        makebutton("Apply").addActionListener(this);
        makelabel("");  //empty: to manage row

        makelabel("Nodes:"); //another row
        nodesField = maketext("5");
        makelabel("Gravity:"); 
        gravityField = maketext("2.00");
        makelabel("Elasticity:"); 
        elasticityField = maketext("0.80");
        makelabel("Tension:");
        tensionField = maketext("1.0");
    }
}

