/*
 * MarqueeLights.java
 *	A java program to attach blinking lights around an image
 *	Usage:	<applet code="MarqueeLights.class" width=100 height=100>
 *		<param name=image value="foo.gif">
 *		</applet>
 *
 *	The modes can be selected through
 *		<param name=mode value="frb">: Forward, reverse, blink
 *		The value string can contain
 *		f=forward chase, r=reverse chase, F,R use multicolors
 *		c,C=clear sequence, s,S=set sequence, C,S are random
 *		y=yes (all on), n=no (all off)
 *		b=blink, 1=one, +=additive
 *
 * Ken Shirriff		ken.shirriff@eng.sun.com
 *
 * Copyright (c) 1996 Sun Microsystems, Inc. All Rights Reserved.
 *
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
 * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 *
 * Please refer to the file "license.txt"
 * for further important copyright and licensing information.
 */
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.applet.Applet;

// A class to hold an image and an "ok" flag.  For the chase lights, stride
// images are used, with the appropriate lights lit in each one.  If the
// image is still being loaded, ok will be set to false.  imgs[stride]
// is a general image buffer.
class Img {
	Image image;
	Graphics graphics;
	boolean ok;
};

public class MarqueeLights extends java.applet.Applet implements Runnable, ImageObserver {

	Thread marquee = null;
	Image outbuf;
	Img imgs[];
	Image gif;

	final int ON=1;
	final int OFF=5;

	int width = 100;
	int height = 40;
	int border = 10;	// The total width of the image border 
	int stride = 3;		// Number of bulbs in the chase sequence
	int delay = 150;	// Delay between steps
	int newdelay = 150;	// Fudged delay
	int bulb = 3;		// Pixel size of a bulb
	boolean stale = true;	// Image has been updated
	String status = "";	// For mouse entry
	URL url = null;		// For mouse click
	String mode = "frbF";	// Default mode sequence
	int mg = 0; // Margin
	int gap = 1; // Gap between bulbs
	int wb, hb, nb; // width in bulbs, height in bulbs, number of bulbs
	boolean error = false;
	Point bulbs[];
	boolean statusflag = false;

	// Asynchronous update of image size.  Print error if size is wrong.
	// Set the stale flag so images are updated.
	public boolean imageUpdate(Image img, int infoflags, int x, int
	y, int inwidth, int inheight) {
		if ((infoflags & (WIDTH|HEIGHT)) != 0) {

			if (width != inwidth+2*border || height != inheight+2*border) {
				System.out.println("MarqueeLights: dimensions should be width="+(inwidth+2*border)+" height="+(inheight+2*border));
			}
		}
		stale = true;
		return true;
	}


	// Compute the size of everything
	// wb = width in bulbs, hb = height in bulbs, nb = number of bulbs
	public void sizeit() {
		wb = (width-2*mg+gap)/(bulb+gap);
		hb = (height-2*bulb-2*gap+gap)/(bulb+gap);
		nb = 2*wb+2*hb;
		bulbs = new Point[nb];
		int i;
		for (i=0;i<nb;i++) {
			bulbs[i] = new Point(0,0);
			pos(i,bulbs[i]);
		}
		resize(width, height);
	}

	public void init() {
		if (getParameter("url") != null) {
			status = getParameter("url");
			try {
				url = new URL(getDocumentBase(),status);
			} catch (MalformedURLException e) {
				status = "<bad url>";
				url = null;
			}
		}
		if (getParameter("width") != null) {
			width = Integer.parseInt(getParameter("width"));
		}
		if (getParameter("height") != null) {
			height = Integer.parseInt(getParameter("height"));
		}
		if (getParameter("delay") != null) {
			delay = Integer.parseInt(getParameter("delay"));
		}
		if (getParameter("bulb") != null) {
			bulb = Integer.parseInt(getParameter("bulb"));
			border = bulb+2;
		}
		if (getParameter("gap") != null) {
			gap = Integer.parseInt(getParameter("gap"));
			if (border<gap+2) {
				border = gap+2;
			}
		}
		if (getParameter("mode") != null) {
			mode = getParameter("mode");
		}
		if (getParameter("stride") != null) {
			stride = Integer.parseInt(getParameter("stride"));
		}
		if (getParameter("border") != null) {
			border = Integer.parseInt(getParameter("border"));
		}
		mg = (border-bulb)/2;
		if (mg<0) {
			mg = 0;
		}
		if (getParameter("image") != null) {
			gif = getImage(getDocumentBase(), getParameter("image"));
			if (gif==null) {
				System.out.println("Bad image "+getParameter("image"));
				error = true;
				marquee = null;
				return;
			}
			gif.getWidth(this);
		} else {
			System.out.println("No image specified");
			error = true;
			marquee = null;
			return;
		}
		sizeit();
		initImgs();
	}

	public void initImgs() {
		int i;
		Graphics g;
		imgs = new Img[stride+1];
		for (i=0;i<stride+1;i++) {
			int j;
			imgs[i] = new Img();
			imgs[i].image = createImage(width,height);
			imgs[i].graphics = imgs[i].image.getGraphics();
			light(imgs[i].graphics,2,ON);
			resetImg(imgs[i].graphics);
			imgs[i].ok = false;
		}
	}

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

	public void paint(Graphics g) {
		g.drawImage(outbuf,0,0,this);
	}

	public void stop() {
		marquee.stop();
		marquee = null;
	}

	int cnt;

	// This loop does the work
	public void run() {
		int n=-1;
		int idx=0;
		if (error) {
			return;
		}
		char modec = ' ';
		while (marquee != null) {
			repaint();
			if (n<0) {
				// Time to advance to a new mode
				modec = mode.charAt(idx);
				idx++;
				if (idx>=mode.length()) {
					idx=0;
				}
				n=0;
				initMode(modec);
			}
			if (stale) {
				// Redraw the images
				int i;
				for (i=0;i<stride+1;i++) {
					imgs[i].ok = false;
				}
				stale = false;
			}
			try {
				// Do one step of the marquee
				n = updateMarquee(n,modec);
				marquee.sleep(newdelay);
			} catch (InterruptedException e) {
			}
		}
	}

	int randseq[];
	int tot=nb/2;

	// Initialize the buffers and counters for the mode
	public void initMode(char mode) {
		int i,j;
		if (mode == 'f' || mode == 'r' || mode=='F' || mode=='R') {
			cnt = 50;
			outbuf = imgs[0].image;
			for (i=0;i<stride;i++) {
				resetImg(imgs[i].graphics);
				for (j=0;j<nb;j++) {
					if (mode=='f'||mode=='r') {
						if ((j%stride)==i) {
							// Start in lower corner
							light(imgs[i].graphics,(j-hb+nb)%nb,ON);
						}
					} else {
						light(imgs[i].graphics,(j-hb+nb)%nb,(j-i+stride)%stride);
					}
				}
			}
		} else {
			outbuf = imgs[stride].image;
			resetImg(imgs[stride].graphics);
			tot=nb/2;
		}
		if (mode == 'c' || mode=='C' || mode=='y') {
			for (i=0;i<nb;i++) {
				light(imgs[stride].graphics,i,ON);
			}
		}
		if (mode=='S'||mode=='C') {
			randseq = new int[nb];
			for (i=0;i<nb;i++) {
				randseq[i] = i;
			}
			for (i=0;i<nb;i++) {
				int t;
				j = i+(int)(Math.random()*(nb-i));
				t = randseq[i];
				randseq[i] = randseq[j];
				randseq[j] = t;
			}
		}
		// Fudge the delays so it looks better
		newdelay = delay;
		if (mode=='b') {
			newdelay = delay*5;
		} else if (mode=='+') {
			newdelay = delay/4;
		} else if (mode=='y' || mode=='n') {
			newdelay = delay*10;
		} else if (mode=='f'||mode=='F'||mode=='r'||mode=='R') {
			newdelay = delay/2;
		}
	}

	// Do one step in the given mode.
	// This routine returns the new n value or -1 if the mode is done
	public int updateMarquee(int n, char mode) {
		int bnum=stride;
		int i;
		int rval= -1;
		if (mode == 'f' || mode == 'r' || mode=='F' || mode=='R') {
			bnum = n;
			outbuf = imgs[n].image;
			if (mode == 'f' || mode=='F') {
				rval = n+1;
				if (rval>=stride) {
					rval=0;
				}
			} else {
				rval = n-1;
				if (rval<0) {
					rval=stride-1;
				}
			}
			cnt--;
			if (cnt<=0 && (rval%2)==0) {
				rval = -1;
			}
		} else if (mode == '+') {
			n++;
			rval = n;
			if (n < tot) {
				light(imgs[stride].graphics,n-1,OFF);
				light(imgs[stride].graphics,n-1+nb/2,OFF);
			} else {
				tot--;
				n=0;
				rval = 0;
				if (tot==0) {
					rval = -1;
				}
			}
			light(imgs[stride].graphics,n,ON);
			light(imgs[stride].graphics,n+nb/2,ON);
		} else if (mode == '1') {
			light(imgs[stride].graphics,n,OFF);
			rval = n+1;
			if (rval>=nb) {
				rval = -1;
			} else {
				light(imgs[stride].graphics,rval,ON);
			}
		} else if (mode == 'S' || mode=='C') {
			light(imgs[stride].graphics,randseq[n],mode=='S'?ON:OFF);
			rval = n+1;
			if (rval>=nb) {
				rval = -1;
			}
		} else if (mode == 's' || mode=='c') {
			light(imgs[stride].graphics,n,mode=='s'?ON:OFF);
			rval = n+1;
			if (rval>=nb) {
				rval = -1;
			}
		} else if (mode=='b') {
			for (i=0;i<nb;i++) {
				light(imgs[stride].graphics,i,(n%2)==0?ON:OFF);
			}
			rval = n+1;
			if (rval>=6) {
				rval = -1;
			}
		} else if (mode=='y' || mode=='n') {
			rval = -1;
		}
		if (imgs[bnum].ok == false) {
			imgs[bnum].ok = true;
			imgs[bnum].image.getGraphics().drawImage(gif,border,border,width-2*border, height-2*border,this);
		}
		return rval;
	}

	public String getAppletInfo() {
		return "MarqueeLights by Ken Shirriff   shirriff@eng.sun.com";
	}

	public String[][] getParameterInfo() {
		String [][] info = {
			{"image      ","URL        ", "image (Required)"},
			{"width      ","int        ", "image width"},
			{"height     ","int        ", "image height"},
			{"delay      ","int        ", "delay, default=50"},
			{"bulb       ","int        ", "bulb size, default=10"},
			{"gap        ","int        ", "bulb gap, default=1"},
			{"border     ","int        ", "border width, default=10"},
			{"stride     ","int        ", "chase stride"},
			{"url        ","URL        ", "click url"},
			{"mode       ","string     ", "flash mode"}
		};
		return info;
	}

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

	public boolean mouseDown(java.awt.Event evt, int x, int y) {
		if (url !=null) {
			getAppletContext().showDocument(url);
		}
		return true;
	}

	public boolean mouseEnter(java.awt.Event evt, int x, int y) {
		statusflag = !statusflag;
		if (statusflag) {
			showStatus(status);
		} else {
			showStatus("MarqueeLights by Ken Shirriff");
		}
		return true;
	}

	public boolean mouseExit(java.awt.Event evt, int x, int y) {
		showStatus(" ");
		return true;
	}

	// Compute the position of bulb n
	public void pos(int n, Point p) {
		int x=0,y=0;
		int excess;
		if (n<wb) {
			excess=width-((bulb+gap)*wb-gap);
			x = (bulb+gap)*n+excess/2;
			y = mg;
		} else if (n-wb<hb) {
			excess=height-((bulb+gap)*hb-gap);
			x = width-border+mg;
			if (x+bulb>width) {
				x=width-bulb;
			}
			y = (bulb+gap)*(n-wb)+excess/2;
		} else if (n-wb-hb<wb) {
			excess=width-((bulb+gap)*wb-gap);
			x = width-(bulb+gap)*(n-wb-hb)-bulb-excess/2;
			y = height-border+mg;
			if (y+bulb>height) {
				y=height-bulb;
			}
		} else if (n-2*wb-hb<hb) {
			excess=height-((bulb+gap)*hb-gap);
			x = mg;
			y = (bulb+gap)*(hb-(n-2*wb-hb)-1)+excess/2;
		} else {
			showStatus("Bad bulb pos");
		}
		p.x = x;
		p.y = y;
	}

	// Light up bulb in in color c
	public void light(Graphics g, int n, int c) {
		if (c==0) {
			g.setColor(Color.red);
		} else if (c==1) {
			g.setColor(Color.yellow);
		} else if (c==2) {
			g.setColor(Color.green);
		} else if (c==3) {
			g.setColor(Color.blue);
		} else if (c==4) {
			g.setColor(Color.white);
		} else if (c==5) {
			g.setColor(Color.black);
		}
		if (bulb==4) {
			g.fillRect(bulbs[n].x+1, bulbs[n].y,2,4);
			g.fillRect(bulbs[n].x, bulbs[n].y+1,4,2);
		} else if (bulb>=5) {
			g.fillOval(bulbs[n].x, bulbs[n].y,bulb,bulb);
		} else {
			g.fillRect(bulbs[n].x, bulbs[n].y,bulb,bulb);
		}
	}

	// Clear the image to have no bulbs on
	public void resetImg(Graphics g) {
		g.setColor(Color.black);
		g.fillRect(0,0,width,height);
		g.drawImage(gif,border,border,width-2*border, height-2*border,this);
	}

} // MarqueeLights
