package worms;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.*;
import java.util.Random;

/**
 * Trailing dots that change colors and fade and stuff
 * @author Neal Ehardt
 */
@SuppressWarnings("serial")
public class Worms extends AnimBase implements MouseListener{
	
	Dot[] dots;
	static Rectangle bounds;
	static Random rand;
	boolean paused=false;
	
	/**
	 * Keeps tracks of the changing color of the dots and background.
	 * Chooses a new color to approach every 'timeframe' frames and
	 * then fades towards it over the course of that timespan.
	 * @author Neal Ehardt
	 */
	class Colorizer {
		/** time between each color change */
		int timeframe = 100;
		/** keeps each primary color value (red, green, blue)*/
		class PrimaryColor {
			int curColor;
			int increment;
			/** seek a new, random destination value */
			public void refresh() {
				increment = (rand.nextInt(256)-curColor)/timeframe;
			}
			/** take a step towards destination value */
			public void tick() {
				curColor += increment;
			}
		}
		
		PrimaryColor[] rgb;
		/** frames remaining until next color change */
		int counter;
		
		public Colorizer() {
			rgb = new PrimaryColor[3];
			for(int i = 0; i < rgb.length; ++i)
				rgb[i] = new PrimaryColor();
		}
		public void tick() {
			if(counter == 0) {
				counter = timeframe;
				for(PrimaryColor i: rgb)
					i.refresh();
			}
			for(PrimaryColor i: rgb)
				i.tick();
			--counter;
		}
		public Color getColor() {
			return getColor(255);
		}
		public Color getColor(int alpha) {
			return new Color(rgb[0].curColor, rgb[1].curColor, rgb[2].curColor, alpha);
		}
	}
	
	Colorizer dotColorizer;
	Colorizer bgColorizer;

	public void initialize(Graphics g) {
		addMouseListener(this);
		rand = new Random();
		dotColorizer = new Colorizer();
		bgColorizer = new Colorizer();
		bounds = getBounds();
		g.setColor(Color.BLACK);
		g.fillRect(0,0,bounds.width,bounds.height);
		dots = new Dot[40];
		short radius = (short)(Math.min(bounds.width, bounds.height)/100);
		for(int i = 0; i < dots.length; ++i)
			dots[i] = new Dot(rand.nextDouble()*bounds.width, rand.nextDouble()*bounds.height, radius);
	}
	
	public void onEnterFrame(Graphics g) {
		if(!paused) {
			//fade
			g.setColor(bgColorizer.getColor(10));
			g.fillRect(0,0,bounds.width,bounds.height);
			
			for(int i = 0; i < dots.length; ++i)
				for(int j = i+1; j < dots.length; ++j)
					dots[i].attract(dots[j]);
			dotColorizer.tick();
			bgColorizer.tick();
			g.setColor(dotColorizer.getColor());
			for(Dot i: dots)
				i.changeOverTime();
			for(Dot i: dots)
				i.paint(g);
		}
	}
	
	/**
	 * Meant to be used with multiple instances; Dots will attract
	 * toward each other, stopping IDEAL_DIST pixels apart.  If they
	 * are closer than IDEAL_DIST they will repel.
	 * @author Neal Ehardt
	 */
	class Dot {
		Point2D.Double location;
		Point2D.Double velocity;
		short radius;
		/** distance apart that dots fight to attain */
		static final double IDEAL_DIST = 100;
		
		Dot(double x, double y, short radius) {
			location = new Point2D.Double(x,y);
			velocity = new Point2D.Double(0,0);
			this.radius = radius;
		}
		
		/** attraction between this dot and a given one */
		public void attract(Dot d) {
			double xDist = d.location.x-location.x;
			double yDist = d.location.y-location.y;
			double dist = Math.sqrt(xDist*xDist + yDist*yDist); //distance between the dots
			double angle = Math.atan2(yDist, xDist);
			/* this is the crazy part.  It's a slight modification of the function
			 * y = sin(4atan(-(2^(x-1)*1.272...)))
			 * The magic number 1.272 is so that the max will be at (2,1) and the min at (0,-1)
			 * Graph it and the attraction makes sense.
			 * 'dist' is substituted for x and scaled to fit IDEAL_DIST.
			 * It is divided by 256 to make it move slowly.
			 */ 
			double netChange = Math.sin( 4*Math.atan(-Math.pow(2,((dist/IDEAL_DIST-1)*1.271553337574))) )/128;
			
			//velocity changes
			double xChange = Math.cos(angle)*netChange;
			double yChange = Math.sin(angle)*netChange;
			
			//apply changes
			velocity.x += xChange;
			velocity.y += yChange;
			d.velocity.x -= xChange;
			d.velocity.y -= yChange;
		}
		
		/** move autonomously */
		public void changeOverTime() {
			//x movement
			location.x += velocity.x;
			//bounds check
			if(location.x < 0) {
				location.x *= -1;
				velocity.x *= -1;
			} else if(location.x > bounds.width) {
				location.x = 2*bounds.width-location.x;
				velocity.x *= -1;
			}
			
			//y movement
			location.y += velocity.y;
			//bounds check
			if(location.y < 0) {
				location.y *= -1;
				velocity.y *= -1;
			} else if(location.y > bounds.height) {
				location.y = 2*bounds.height-location.y;
				velocity.y *= -1;
			}
		}
		
		public void paint(Graphics g) {
			//fills in with extra draws if the dot is moving fast
			int iterations = (int)( Math.max(Math.abs(velocity.x), Math.abs(velocity.y))/(radius/2d) );
			double xIncrement, yIncrement;
			if(iterations == 0) //no extra draws
				xIncrement = yIncrement = 0;
			else { //x and y distance between each draw
				xIncrement = velocity.x/iterations;
				yIncrement = velocity.y/iterations;
			}
			for(double i = 0; i <= iterations; ++i)
				g.fillOval( (int)(location.x-radius-i*xIncrement),
					(int)(location.y-radius-i*yIncrement),
					radius*2, radius*2 );
		}
	}

	/** pause/unpause by clicking */
	public void mouseClicked(MouseEvent e) {
		paused = !paused;
	}
	public void mousePressed(MouseEvent e) {}
	public void mouseReleased(MouseEvent e) {}
	public void mouseEntered(MouseEvent e) {}
	public void mouseExited(MouseEvent e) {}
}
