/*
 * Copyright Jerry Huxtable 1999
 *
 * Feel free to do anything you like with this code.
 */
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import java.io.*;
import com.jhlabs.image.*;

/**
 * An applet which animates a simple particle system.
 */
public class ParticleApplet extends Applet implements Runnable, MouseListener {
	protected MemoryImageSource source;
	protected Image image;
	protected boolean newImage = true;
	protected ColorModel colorModel;
	protected Thread thread;
	protected byte[] pixels1, pixels2;
	protected int width, height;
	protected Image offscreen;
	protected Graphics offscreenG;
	public Action[] actions;
	protected Filter[] filters;
	protected Object[] gradients;
	private boolean running = false;
	private boolean startAnimation = false;

	public ParticleApplet() {
	}
	
	public void init() {
		Dimension size = getSize();
		width = size.width;
		height = size.height;
		try {
			ObjectInputStream ois = new ObjectInputStream(getClass().getResourceAsStream("gradients.ser"));
			gradients = (Object[])ois.readObject();
			ois.close();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		addMouseListener(this);
		setGradient(3);
		int numFilters = 8;
		filters = new Filter[numFilters];
		filters[0] = new ShiftDownFilter();
		filters[1] = new ShiftUpFilter();
		filters[2] = new ConvolveFilter();
		filters[3] = new BlurHFilter();
		filters[4] = new BlurVFilter();
		filters[5] = new ThingFilter();
		filters[6] = new WaterFilter();
		filters[7] = new ClearFilter();
		filters[3].setEnabled(true);
		filters[4].setEnabled(true);

		int numActions = 1;
		actions = new Action[numActions];
		int numParticles = getIntParameter("numParticles", 100);
		Particles particles = new Particles(numParticles, width/2, height/2, width, height);
		actions[0] = particles;
		actions[0].setEnabled(true);
		particles.rate = getIntParameter("rate", 100);
		particles.speed = (getIntParameter("speed", 0) << 8) / 10;
		particles.angle = getIntParameter("angle", 0);
		particles.spread = getIntParameter("spread", 360);
		particles.gravity = getIntParameter("gravity", 0);
		particles.color = getIntParameter("color", 255);
		particles.scatter = getIntParameter("scatter", 0);
		particles.hscatter = getIntParameter("hscatter", 0);
		particles.vscatter = getIntParameter("vscatter", 0);
		particles.randomness = getIntParameter("randomness", 0);
		particles.size = getIntParameter("size", 2);
		particles.x = getIntParameter("x", width/2);
		particles.y = getIntParameter("y", height/2);
		particles.lifetime = getIntParameter("lifetime", 50);
		particles.speedVariation = (getIntParameter("speedVariation", 0) << 8) / 10;
		particles.decay = getIntParameter("decay", 0);
		startAnimation = getIntParameter("startAnimation", 1) != 0;
		int colormap = getIntParameter("colormap", 4);
		setGradient(colormap);
		String actions = getParameter("actions");
		if (actions != null) {
			String s = "duchvtwc";
			for (int i = 0; i < s.length(); i++)
				filters[i].setEnabled(actions.indexOf(s.charAt(i)) != -1);
		}
	}

	private int getIntParameter(String name, int defaultValue) {
		if (name != null) {
			try {
				return Integer.parseInt(getParameter(name));
			}
			catch (NumberFormatException e) {
			}
		}
		return defaultValue;
	}
	
	public void setGradient(int n) {
		n = Math.max(0, Math.min(n, gradients.length-1));
		byte[] r = new byte[256];
		byte[] g = new byte[256];
		byte[] b = new byte[256];
		for (int i = 0; i < 256; i++) {
			int rgb = ((Gradient)gradients[n]).getColor(i/255.0);
			r[i] = (byte)((rgb >> 16) & 0xff);
			g[i] = (byte)((rgb >> 8) & 0xff);
			b[i] = (byte)(rgb & 0xff);
		}
		colorModel = new IndexColorModel(8, 256, r, g, b);
	}
	
	public Dimension getPreferredSize() {
		return new Dimension(width, height);
	}
	
	public void start() {
		if (startAnimation && thread == null) {
			thread = new Thread(this);
			thread.start();
		}
	}
	
	public void stop() {
		if (thread != null) {
			thread.stop();
			thread = null;
		}
	}
	
	public void run() {
		try {
			running = true;
			while (running) {
				repaintImage();
//				synchronized(this) {
//					wait();
//				}
				Thread.sleep(5);
			}
		}
		catch (InterruptedException e) {
		}
		thread = null;
	}
	
	public void update(Graphics g) {
		paint(g);
	}

	public synchronized void paint(Graphics g) {
		Dimension size = getSize();
		int w = size.width;
		int h = size.height;
		if (newImage || image == null)
			image = makeImage(w, h);
		g.drawImage(image, 0, 0, this);
		if (!running) {
			String s = "Click to Start";
			FontMetrics fm = g.getFontMetrics();
			int x = (size.width-fm.stringWidth(s))/2;
			int y = (size.height-fm.getAscent())/2;
			g.setColor(Color.white);
			g.drawString(s, x, y);
		}
		notify();
	}

	private Image makeImage(int w, int h) {
		if (pixels1 == null) {
			int i = 0;
			pixels1 = new byte[width*height];
			pixels2 = new byte[width*height];
			for (i = 0; i < actions.length; i++)
				if (actions[i].isEnabled())
					actions[i].apply(pixels1, width, height);
		} else {
			for (int i = 0; i < filters.length; i++) {
				if (filters[i].isEnabled()) {
					filters[i].apply(pixels1, pixels2, width, height);
					byte[] t = pixels1;
					pixels1 = pixels2;
					pixels2 = t;
				}
			}
			for (int i = 0; i < actions.length; i++)
				if (actions[i].isEnabled())
					actions[i].apply(pixels1, width, height);
		}
		newImage = false;
		if (image == null) {
			image = createImage(source = new MemoryImageSource(w, h, colorModel, pixels1, 0, w));
			source.setAnimated(true);
		} else
			source.newPixels(pixels1, colorModel, 0, w);
		return image;
	}
	
	private void repaintImage() {
		newImage = true;
		repaint();
	}
	
	public void mousePressed(MouseEvent e) {
		requestFocus();
		startAnimation = true;
		if (thread == null && !running)
			start();
		else
			running = false;
	}
	
	public void mouseReleased(MouseEvent e) {
	}
	
	public void mouseClicked(MouseEvent e) {
	}
	
	public void mouseEntered(MouseEvent e) {
	}
	
	public void mouseExited(MouseEvent e) {
	}
	
}

class Thing {
	private boolean enabled;

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public boolean isEnabled() {
		return enabled;
	}
}

class Action extends Thing {
	public void apply(byte[] pixels, int width, int height) {
	}
}

class Filter extends Action {
	public void apply(byte[] in, byte[] out, int width, int height) {
	}
}

/*
class TextAction extends Action {
	private Font font = new Font("sansserif", Font.PLAIN, 72);
	
	public void apply(byte[] pixels, int width, int height) {
	}
}

class ImageAction extends Action {
	private Image image;
	
	public ImageAction(Image image) {
		this.image = image;
	}
	
	public void apply(byte[] pixels, int width, int height) {
	}
}
*/

/**
 * A particle system. We use fixed point integer maths with 8 fractional bits everywhere
 * for speed, hence the frequent shifts by 8.
 */
class Particles extends Action {
	public Particle[] particles;
	public int rate = 100;
	public int angle = 0;	// 0 degrees is north
	public int spread = 90;
	public int gravity = (1 << 8);
	public int lifetime = 65;
	public int scatter = 0;
	public int hscatter = 0;
	public int vscatter = 0;
	public int x;
	public int y;
	public int speed = 1;
	public int size;
	public int width;
	public int height;
	public int speedVariation = 0;
	public int decay = 0;
	public int randomness = (7 << 8);
	public int color;
	public int numParticles;
	private static int[] sinTable, cosTable;

	static {
		sinTable = new int[360];
		cosTable = new int[360];
		for (int i = 0; i < 360; i++) {
			double angle = 2*Math.PI*i/360;
			sinTable[i] = (int)(256 * Math.sin(angle));
			cosTable[i] = (int)(256 * Math.cos(angle));
		}
	}

	public Particles(int numParticles, int x, int y, int width, int height) {
		this.numParticles = numParticles;
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
		particles = new Particle[numParticles];
		for (int i = 0; i < numParticles; i++) {
			particles[i] = new Particle(this, width, height);
			newParticle(particles[i]);
		}
	}

	public double gaussian() {
		double sum = 0;
		for (int i = 0; i < 12; i++) {
			sum += Math.random();
		}
		return (sum-6)/3.0;
	}
	
	public void newParticle(Particle particle) {
		particle.color = color;
		particle.size = size;
		particle.lifetime = (int)(Math.random()*lifetime);
		particle.randomness = randomness;
		particle.x = x;
		particle.y = y;
		if (scatter != 0) {
			int a = ((int)(Math.random() * 360)) % 360;
			double distance = scatter * Math.random() / 256;
			particle.x += (int)(cosTable[a] * distance);
			particle.y += (int)(-sinTable[a] * distance);
		}
		if (hscatter != 0)
			particle.x += (int)(hscatter * (Math.random()-0.5));
		if (vscatter != 0)
			particle.y += (int)(vscatter * (Math.random()-0.5));
		int a = (angle + 450 - spread/2 + (int)(Math.random() * spread)) % 360;
		int s = speed + (int)(speedVariation * gaussian());
		particle.vx = ((cosTable[a] * s) >> 8);
		particle.vy = -((sinTable[a] * s) >> 8);

		particle.x <<= 8;
		particle.y <<= 8;
	}
	
	public void apply(byte[] pixels, int width, int height) {
		for (int i = 0; i < particles.length; i++) {
			Particle p = particles[i];
			if (p.lifetime < 0) {
				newParticle(p);
			}
			p.paint(pixels, width, height);
			p.move(width, height);
			p.color -= decay;
			if (p.color < 0)
				p.color = 0;
		}
	}

	public String toString() {
		return "Particles";
	}
}

class Particle {
	protected int x, y;
	protected int vx, vy;
	public int size;
	public int color = 255;
	public int randomness = 0;
	public int lifetime = -1;
	private Particles particles;
	
	public Particle(Particles particles, int width, int height) {
		this.particles = particles;
	}
	
	public void move(int width, int height) {
		if (randomness != 0) {
			vx += (int)(Math.random() * randomness)-randomness/2;
			vy += (int)(Math.random() * randomness)-randomness/2;
		}
		x += vx;
		y += vy;
		vy += particles.gravity;
		lifetime--;
	}
	
	/*
	 * How to draw circles of small sizes
	 */
	public int[] circle1 = { 0, 1 };
	public int[] circle3 = { 0, 1, -1, 3, 0, 1 };
	public int[] circle5 = { -1, 3, -2, 5, -2, 5, -2, 5, -1, 3 };
	public int[] circle7 = { -1, 3, -2, 5, -3, 7, -3, 7, -3, 7, -2, 5, -1, 3 };
	public int[][] circles = { circle1, circle3, circle5, circle7 };
	
	public void paint(byte[] pixels, int width, int height) {
		byte pixel = (byte)color;
		int[] c = circles[Math.min(size, circles.length)];
		int my = (y >> 8)-size;
		for (int i = 0; i < c.length; i += 2, my++) {
			if (my < 0)
				continue;
			else if (my >= height)
				break;
			int x1 = Math.max(0, (x >> 8)+c[i]);
			int x2 = Math.min(width-1, x1+c[i+1]);
			int j = my*width+x1;
			for (int mx = x1; mx <= x2; mx++)
				pixels[j++] = pixel;
		}
	}
}

/**
 * Scroll the image down
 */
class ShiftDownFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		int i = 0;
		int j = 0;
		for (int x = 0; x < width; x++)
			out[j++] = 0;
		for (int y = 1; y < height; y++) {
			for (int x = 0; x < width; x++) {
				out[j++] = in[i++];
			}
		}
	}

	public String toString() {
		return "Move Down";
	}
}

/**
 * Scroll the image up
 */
class ShiftUpFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		int i = width*height-1;
		int j = i;
		for (int x = 0; x < width; x++)
			out[j--] = 0;
		for (int y = 1; y < height; y++) {
			for (int x = 0; x < width; x++) {
				out[j--] = in[i--];
			}
		}
	}

	public String toString() {
		return "Move Up";
	}
}

/**
 * Zoom the image out to the left and right
 */
class ZoomInVFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		int i = 0;
		int j = 0;
		int height2 = height/2;
		for (int x = 0; x < width; x++)
			out[j++] = 0;
		j = width;
		for (int y = 1; y < height2; y++) {
			for (int x = 0; x < width; x++) {
				out[j++] = in[i++];
			}
		}
		i += 2*width;
		for (int y = 1; y < height2; y++) {
			for (int x = 0; x < width; x++) {
				out[j++] = in[i++];
			}
		}
		for (int x = 0; x < width; x++)
			out[j++] = 0;
	}

	public String toString() {
		return "Move In Vertical";
	}
}

/**
 * Zoom the image out sideways
 */
class ZoomInHFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		int i = 0;
		int j = 0;
		int height2 = height/2;
		for (int x = 0; x < width; x++)
			out[j++] = 0;
		j = width;
		for (int y = 1; y < height2; y++) {
			for (int x = 0; x < width; x++) {
				out[j++] = in[i++];
			}
		}
		i += 2*width;
		for (int y = 1; y < height2; y++) {
			for (int x = 0; x < width; x++) {
				out[j++] = in[i++];
			}
		}
		for (int x = 0; x < width; x++)
			out[j++] = 0;
	}

	public String toString() {
		return "Move In Horizontal";
	}
}

/**
 * Do a general convolution on the image (this is much slower than the blur filters)
 */
class ConvolveFilter extends Filter {
	protected int[] kernel = {
		-3, 0, 0, 0, -3,
		0, 0, 0, 0, 0,
		-3, 0, 50, 0, -3,
		0, 0, 0, 0, 0,
		-3, 0, 0, 0, -3,
	};
	int target = 40;

	public ConvolveFilter() {
	}
	
	public ConvolveFilter(int[] kernel, int target) {
		this.kernel = kernel;
		this.target = target;
	}
	
	public void apply(byte[] in, byte[] out, int width, int height) {
		int index = 0;
		int rows = 5;
		int cols = 5;
		int rows2 = rows/2;
		int cols2 = cols/2;

		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int t = 0;

				for (int row = -rows2; row <= rows2; row++) {
					int iy = y+row;
					int ioffset;
					if (0 <= iy && iy < height)
						ioffset = iy*width;
					else
						ioffset = y*width;
					int moffset = cols*(row+rows2)+cols2;
					for (int col = -cols2; col <= cols2; col++) {
						int f = kernel[moffset+col];

						if (f != 0) {
							int ix = x+col;
							if (!(0 <= ix && ix < width))
								ix = x;
							t += f * (in[ioffset+ix] & 0xff);
						}
					}
				}
				t /= target;
				if (t > 255)
					t = 255;
				out[index++] = (byte)t;
			}
		}
	}

	public String toString() {
		return "Convolve";
	}
}

/**
 * Blur horizontally
 */
class BlurHFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		for (int y = 0; y < height; y++) {
			int index = y*width;
			out[index] = (byte)(((in[index] & 0xff) + (in[index+1] & 0xff))/3);
			index++;
			for (int x = 1; x < width-1; x++) {
				out[index] = (byte)(((in[index-1] & 0xff) + (in[index] & 0xff) + (in[index+1] & 0xff))/3);
				index++;
			}
			out[index] = (byte)(((in[index-1] & 0xff) + (in[index] & 0xff))/3);
		}
	}

	public String toString() {
		return "Blur Horizontally";
	}
}

/**
 * Blur vertically
 */
class BlurVFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		for (int x = 0; x < width; x++) {
			int index = x;
			out[index] = (byte)(((in[index] & 0xff) + (in[index+width] & 0xff))/3);
			index += width;
			for (int y = 1; y < height-1; y++) {
				out[index] = (byte)(((in[index-width] & 0xff) + (in[index] & 0xff) + (in[index+width] & 0xff))/3);
				index += width;
			}
			out[index] = (byte)(((in[index-width] & 0xff) + (in[index] & 0xff))/3);
		}
	}

	public String toString() {
		return "Blur Vertically";
	}
}

/**
 * A sort of water-ripple type effect
 */
class WaterFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		for (int y = 1; y < height-1; y++) {
			int index = y*width;
			index++;
			for (int x = 1; x < width-1; x++) {
				int n = (byte)(
					((in[index-1] & 0xff) + (in[index+1] & 0xff) + (in[index-width] & 0xff) + (in[index+width] & 0xff))/2
					 - (out[index] & 0xff));
				n -= (byte)(n >> 6);
				out[index] = (byte)ImageMath.clamp(n, 0, 255);
				index++;
			}
			index++;
		}
	}

	public String toString() {
		return "Ripple";
	}
}

/**
 * Clears the image to black - you only see the moving particles with this one.
 */
class ClearFilter extends Filter {
	public void apply(byte[] in, byte[] out, int width, int height) {
		int index = 0;
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				out[index] = 0;
				index++;
			}
		}
	}

	public String toString() {
		return "Clear";
	}
}

class ThingFilter extends ConvolveFilter {
/*
	protected static int[] kernel = {
		10, 0, 10, 0, 10,
		0, 0, 0, 0, 0,
		0, 10, 0, 10, 0,
		0, 0, 0, 0, 0,
		10, 0, 10, 0, 10,
	};
*/
	protected static int[] kernel = {
		10, 0, 0, 0, 10,
		0, 10, 0, 10, 0,
		0, 0, 10, 0, 0,
		0, 10, 0, 10, 0,
		10, 0, 0, 0, 10,
	};
	
	public ThingFilter() {
		super(kernel, 120);
	}

	public String toString() {
		return "Thing";
	}
}
