/* Spiro by Jimmc
 * Copyright 1996 Jim McBeath
 *
 * Permission to use, copy, and distribute Spiro, in source or binary form,
 * for personal non-commercial use is hereby granted without fee, provided
 * that this copyright notice appears in all copies, and that no charge is
 * associated with such copies.
 *
 * Permission to modify Spiro and make derivative works for personal
 * non-commercial use, and to distribute such modified versions, is hereby
 * granted without fee, provided that the users of such a modified version
 * are clearly notified that the work is a modified version of Spiro and
 * not the original Spiro.  As with unmodified distributions, this copyight
 * notice must appear in all modified copies, and no charge may be associated
 * with such copies.
 *
 * The author makes no representations about the suitability of this software
 * for any purpose.  It is provided "as-is", without any express or implied
 * warranty.  In no event will the author be held liable for any damages
 * arising from the use of this software.
 */
/* SpiroCanvas - the drawing and state portion of Spiro
 *
 * Jim McBeath, May 1996
 */

import java.util.Properties;
import java.awt.*;
import java.io.*;
import java.net.URL;

class SpiroState {
	/* Parameters controling the image.  If adding or chainging these, need
	 * to modify methods that convert between SpiroState and properties */
	WheelState wheels[];
	String color;	/* name(RGB) of color in which to draw this pattern */
	int dashPattern[];	/* on/off/etc steps */
	int steps;	/* numer of steps requested */
	String chain;		/* chain draw another file first */
	/* The following portions of the state are ignored when chained */
	boolean animate;	/* true means draw wheels and go slowly */
	int animationDelay;
	boolean animateCount;
	String drawMode;	/* Synchronous, Direct, or Backing */

	/* Internal state variables used during generation of the image,
	 * reset at the beginning of drawing the image. */
	int numwheels;
	int lcmsteps;	/* LCM of teeth counts in wheels[] */
	int numsteps;	/* either lcmsteps or user-requested number of steps */
	int stepNumber;	//The number of the current step being animated
	int lastx, lasty;
	int x, y;
	int cx, cy;
	int dashIndex;	/* index into dashPattern */
	int dashOffset;	/* offset into count at dashPattern[dashIndex] */
	boolean drawState;	/* true to draw, false to skip */

	public SpiroState(int nwheels) {
		numwheels = nwheels;
		wheels = new WheelState[numwheels];

		//Set up default values in case no Advanced mode
		drawMode = "Direct";
	}

	protected void clearWheels() {
		/* clear wheel state, and calculate first point */
		int i;
		x = cx;
		y = cy;
		int t[] = new int[numwheels];
		for (i=0; i<numwheels; i++) {
			wheels[i].reset();
			double a = wheels[i].pos * 2 * Math.PI;
			x += Math.sin(a) * wheels[i].radiusX;
			y += Math.cos(a) * wheels[i].radiusY;
			t[i] = Math.abs(wheels[i].teeth);
		}
		lcmsteps = FactorMath.leastCommonMultiple(t);
		if (steps!=0)	/* if <0, leave it <0 */
			numsteps = steps;
		else
			numsteps = lcmsteps;
		dashIndex = 0;
		dashOffset = 0;
		drawState = true;
	}

	public int getLcmSteps() {
		int t[] = new int[numwheels];
		for (int i=0; i<numwheels; i++) {
			t[i] = Math.abs(wheels[i].teeth);
		}
		int n = FactorMath.leastCommonMultiple(t);
		return n;
	}

	private void stepWheelsCalc(Graphics g,boolean drawAnimation) {
		int i;
		double a;
		x = cx;
		y = cy;
		Color paintColor = g.getColor();
		if (drawAnimation) {
			g.setColor(Color.white);
			g.setXORMode(Color.black);
			if (numsteps>0 && animateCount) {
			    String stepInfo = ""+stepNumber+"/"+numsteps;
			    g.drawString(stepInfo,4,12);
			}
		}
		for (i=0; i<numwheels; i++) {
		    if (wheels[i].increment!=0) {
			a = wheels[i].pos * 2 * Math.PI;
			int rx = wheels[i].radiusX;
			int ry = wheels[i].radiusY;
			int dx = (int)(Math.sin(a) * rx);
			int dy = (int)(Math.cos(a) * ry);
			if (drawAnimation) {
			    g.drawOval(x-rx,fix_y(y-ry,ry*2),rx*2,ry*2);
				//Draw the circle for the wheel
			    g.drawLine(x,fix_y(y),
					x+dx,fix_y(y+dy)); //radial line
			}
			x += dx;
			y += dy;
		    }
		}
		if (drawAnimation) {
			g.setPaintMode();
		        g.setColor(paintColor);
		}
	}

	private int sleepErrorCount=0;
	protected void stepWheels(Graphics g, Panel canvas, boolean doDraw) {
		int i;
		double a;

		//if drDraw==false, then we're setting up the first point
		Toolkit tk = Toolkit.getDefaultToolkit();

		if (doDraw && animate)
			//undraw previous animation
			stepWheelsCalc(g,true);

		if (doDraw) {
			//Advance all wheels
			for (i=0; i<numwheels; i++) {
			    if (wheels[i].increment!=0) {
				wheels[i].pos += wheels[i].increment;
				if (wheels[i].pos>=1.0)
					wheels[i].pos -= 1.0;
			    }
			}
		}
		
		//Calculate new point
		stepWheelsCalc(g,false);
		if (doDraw)
		    stepNumber++;

		if (dashPattern!=null && dashPattern.length>0) {
			if (dashOffset++>=dashPattern[dashIndex]) {
				drawState = !drawState;
				dashOffset = 1;
				if (++dashIndex>=dashPattern.length)
					dashIndex = 0;
			}
		}

		//draw line
		if (doDraw && drawState)
			g.drawLine(lastx,fix_y(lasty),x,fix_y(y));

		if (animate) {
			//if (animationDelay==0)
				//animationDelay = 1;
			//draw new animation
			stepWheelsCalc(g,true);
			try {
//If we don't put in any delay, the process uses 100% of the cpu time,
//and will draw too fast to see on fast processors.  On a 350Mhz box,
//it draws 1000 steps in about 4 seconds.
//But if we put in even 1ns of delay, then the sleep takes about 50ms,
//so it takes about 20 seconds to draw the 1000 steps.
				Thread.sleep(animationDelay,1);
					//slow enough to see
			} catch (Exception e) {
			    if (sleepErrorCount++<10)
		   		System.out.println("Sleep exception: "+e);
			}
/* repaint is not needed when in direct mode, and makes the drawing take
   literally about 10 times as long.
			if (canvas!=null)
				canvas.repaint();
 */
			tk.sync();
		}

		lastx = x;	/* save previous point */
		lasty = y;
	}

	private int fix_y(int y) {
		return (2*cy - y);
	}

	private int fix_y(int y, int size) {
		return (2*cy - y - size);
	}

	private void drawSpiroChain(Graphics g, Panel canvas,
			SpiroCanvas rcanvas,
			String chainList[], String chain) {
		if (chainList==null)
			chainList = new String[0];
		String newChainList[] = new String[chainList.length+1];
		int i;
		for (i=0; i<chainList.length; i++) {
			if (chain.equals(chainList[i])) {
				System.out.println("Chain loop detected");
				return;
			}
			newChainList[i] = chainList[i];
		}
		newChainList[i] = chain;
		SpiroState chainState = SpiroState.loadFile(chain);
		if (chainState!=null) {
			chainState.cx = cx;
			chainState.cy = cy;
			chainState.animate = animate;
			chainState.drawMode = drawMode;
			chainState.drawSpiro(g,canvas,rcanvas,newChainList);
		}
	}

	public void drawSpiro(Graphics g, Panel canvas, SpiroCanvas rcanvas,
				String chainList[]) {
		if (chain!=null && !chain.equals(""))
			drawSpiroChain(g,canvas,rcanvas,chainList,chain);
		int i;
		Color usercolor = convertColor(color);
		g.setColor(usercolor);
		clearWheels();	/* reset and set up last x/y */
		stepNumber = 0;
		stepWheels(g,canvas,false);	/* calculate first x/y */
		for (i=0; rcanvas.runFlag && (i<numsteps); i++)
			stepWheels(g,canvas,true); //calculate next and draw
	}

	private Color convertColor(String cname) {
		if (cname.equalsIgnoreCase("black")) return Color.black;
		if (cname.equalsIgnoreCase("blue")) return Color.blue;
		if (cname.equalsIgnoreCase("cyan")) return Color.cyan;
		if (cname.equalsIgnoreCase("darkGray")) return Color.darkGray;
		if (cname.equalsIgnoreCase("gray")) return Color.gray;
		if (cname.equalsIgnoreCase("green")) return Color.green;
		if (cname.equalsIgnoreCase("lightGray")) return Color.lightGray;
		if (cname.equalsIgnoreCase("magenta")) return Color.magenta;
		if (cname.equalsIgnoreCase("orange")) return Color.orange;
		if (cname.equalsIgnoreCase("pink")) return Color.pink;
		if (cname.equalsIgnoreCase("red")) return Color.red;
		if (cname.equalsIgnoreCase("white")) return Color.white;
		if (cname.equalsIgnoreCase("yellow")) return Color.yellow;
		//We don't recognize it, assume it's a R,G,B triplet
		IntVector v = new IntVector(cname);
		return new Color(v.a[0],v.a[1],v.a[2]);
	}

	public Properties getProperties() {
		/* return the state info as a property list */
		Properties p = new Properties();
		for (int i=0; i<numwheels; i++) {
			String s = wheels[i].radiusX + "," + wheels[i].radiusY;
			p.put("wheel"+(i+1)+"Radius",s);
			p.put("wheel"+(i+1)+"Teeth",
				Integer.toString(wheels[i].teeth));
			p.put("wheel"+(i+1)+"Offset",
				Integer.toString(wheels[i].offset));
		}
		p.put("color",color);
		if (dashPattern!=null && dashPattern.length>0)
			p.put("dashPattern",
				IntVector.toCommaString(dashPattern));
		p.put("steps",Integer.toString(steps));
		if (chain!=null && !chain.equals(""))
			p.put("chain",chain);
		p.put("animate",(animate?"true":"false"));
		if (drawMode!=null && !drawMode.equals(""))
			p.put("drawMode",drawMode);
		return p;
	}

	public SpiroState(Properties p) {
		/* create a SpiroState from a property list */
		String s;
		int i = 0;
		numwheels = 0;
		while (true) {
			s = p.getProperty("wheel"+(i+1)+"Radius");
			if (s==null)
				break;
			numwheels++;
			WheelState newwheels[] = new WheelState[numwheels];
			for (int j=0; j<numwheels-1; j++)
				newwheels[j] = wheels[j];
			wheels = newwheels;
			wheels[i] = new WheelState(i);
			wheels[i].teeth = Integer.parseInt(
				p.getProperty("wheel"+(i+1)+"Teeth","0"));
			wheels[i].offset = Integer.parseInt(
				p.getProperty("wheel"+(i+1)+"Offset","0"));
			WheelControl.parseRadius(s,wheels[i]);
			i++;
		}
		color = p.getProperty("color","red");
		s = p.getProperty("dashPattern");
		if (s!=null)
			dashPattern = (new IntVector(s)).getArray();
		steps = Integer.parseInt(p.getProperty("steps","0"));
		chain = p.getProperty("chain","");
		animate = p.getProperty("animate","false")=="true";
		drawMode = p.getProperty("drawMode","Direct");
	}

	public static SpiroState loadFile(String inFileName) {
		FileInputStream inFile;
		try {
			inFile = new FileInputStream(inFileName);
		} catch (IOException e) {
			return null;
		}
		//load parameters from file into p
		Properties p = new Properties();
		try {
			p.load(inFile);
			inFile.close();
		} catch (IOException e) {
			return null;
		}
		//convert from parameter list back into state object
		SpiroState state = new SpiroState(p);
		return state;
	}

	public static SpiroState loadURL(String inFileName) {
System.out.println("Load URL: "+inFileName);
		URL inURL;
		InputStream inFile;
		try {
			inURL = new URL(inFileName);
		} catch (java.net.MalformedURLException e) {
			return null;
		}
		try {
			inFile = inURL.openStream();
		} catch (IOException e) {
			return null;
		}
System.out.println("got stream");
		//load parameters from file into p
		Properties p = new Properties();
		try {
			p.load(inFile);
			inFile.close();
		} catch (IOException e) {
			return null;
		}
System.out.println("got properties");
		//convert from parameter list back into state object
		SpiroState state = new SpiroState(p);
System.out.println("state="+state);
		return state;
	}
}

class SpiroCanvas extends Panel implements Runnable {
	Spiro spiro;
	SpiroState defaultState;
	Thread drawThread;
	Image backingImage;
	Graphics backingGraphics;
	public boolean drawing;	//true when we are drawing
	boolean runFlag = true;	/* set to false to stop the drawing */

	public SpiroCanvas(Spiro s) {
		if (s==null) {
			throw new IllegalArgumentException(
				"Program Error: null Spiro");
		}
		spiro = s;
		setBackground(Color.white);
		setForeground(Color.black);
	}

	public void draw() {
		if (defaultState!=null &&
				defaultState.drawMode.equals("Synchronous")){
			repaint();
			return;
		}
		if (drawThread!=null && drawThread.isAlive())
			drawThread.stop();
		drawThread = new Thread(this,"SpiroPainter");
		drawThread.start();
	}

	public void run() {	/* run the draw thread */
		boolean drawDirect;
		drawing = true;
		Dimension d = size();
		drawDirect = (defaultState!=null &&
				defaultState.drawMode.equals("Direct"));
		if (drawDirect) {
			backingGraphics = getGraphics();
			drawBacking(backingGraphics,d,null,this);
		} else {
			backingImage = createImage(d.width,d.height);
			backingGraphics = backingImage.getGraphics();
			drawBacking(backingGraphics,d,this,this);
		}
		drawing = false;
		if (!drawDirect)
			repaint();	//copy backing image to screen
		//end of the thread
	}

	private void drawBacking(Graphics g, Dimension d, SpiroCanvas sc,
			SpiroCanvas rc) {
		showStatus("Drawing");
		g.setColor(getBackground());
		g.fillRect(0,0,d.width,d.height); //clear background
		g.setColor(Color.blue);
		g.drawRect(0,0,d.width-1,d.height-1); //paint outline box
		g.setPaintMode();
		if (defaultState==null)
			return;	/* nothing to draw yet */
		defaultState.cx = d.width/2;
		defaultState.cy = d.height/2;
		defaultState.drawSpiro(g,sc,rc,null);
		if (rc.runFlag)
			showStatus("Done");
		else
			showStatus("Stopped");
	}

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

	public void update(Graphics g) {
		if (defaultState!=null && defaultState.drawMode=="Synchronous"){
			Dimension d = size();
			drawing = true;
			drawBacking(g,d,null,this);
			drawing = false;
			return;
		}
		if (defaultState!=null && defaultState.drawMode=="Direct")
			return;
		if (backingImage!=null)
			g.drawImage(backingImage,0,0,this);
	}

	private void showStatus(String s) {
		spiro.controls.showStatus(s);
	}
}


class WheelState {
	int num;	//wheel number
	double pos;	//current wheel position, in the range [0,1)
	double increment;	//amount by which to increment pos
	int radiusX;		//value input from radiusW
	int radiusY;		//second value in radiusW, e.g. "100,50"
	int teeth;		//value input from teethW
	int offset;		//value input from offsetW

	public WheelState(int n) {
		num = n;
	}

	void reset() {
		if (teeth==0) {
			increment = 0;	//disabled
			offset = 0;
			pos = 0;
		} else {
			increment = 1.0/teeth;
			offset = offset%teeth;
			pos= ((double)offset)/((double)teeth);
		}
	}
}

/* end */
