Number.implement({
	norm: function(lower, upper) {
		return (this / (upper - lower));
	}
});

var twoPi = Math.PI * 2;

var Swarms = new Class({
	Implements: Options,
	
	Binds: [
		'draw',
		'iterate'
	],
	
	options: {
		fillStyle: 'rgba(34, 34, 34, 0.2)',
		clearBackground: true,
		framerate: 60,
		cssClass: 'swarms',
		drawCOGs: true,
		drawDetails: true,
		maxParticles: 100
	},

	swarmCount: 0,
	swarms: [],
	
	initialize: function(options, data) {
		this.setOptions(options);
		
		this.element = new Element('canvas', {
			'class': 'cssClass',
			width: 640,
			height: 480
		});
		this.context = this.element.getContext('2d');
		
		this.swarmCount = $random(1, 3);
	},
	
	toElement: function() {
		return this.element;
	},
	
	draw: function() {
		if (!this.element.getParent()) return;
		
		if (!this.elSize) this.elSize = this.element.getSize();
		
		for (var i = 0; i < this.swarmCount; i++) {
			this.swarms[i] = new Swarm({
				width: this.elSize.x,
				height: this.elSize.y,
				drawCOG: this.options.drawCOGs,
				pMax: this.options.maxParticles
			}, this.context);
		}
		
		window.setInterval(this.iterate, 1000 / this.options.framerate);
	},
	
	iterate: function() {
		var ctx = this.context,
				siz = this.elSize,
				pCount = 0;
		if (this.options.clearBackground) {
			ctx.fillStyle = this.options.fillStyle;
			ctx.fillRect(0, 0, siz.x, siz.y);
		}
		this.swarms.each(function(swarm) {
			if (swarm.pCount < 1) {
				swarm.respawn();
			}
			pCount += swarm.options.pCount;
			swarm.move(ctx);
		});
		if (this.options.drawDetails && ctx.fillText) {
			ctx.fillStyle = 'rgba(255,255,255,0.5)';
			ctx.fillText(pCount + ' particles', 6, 16);
		}
	}
});

var Swarm = new Class({
	Implements: Options,
	
	Binds: [
		'spawnParticle',
		'move',
		'respawn'
	],
	
	options: {
		pCount: 0,
		pMax: 100,
		pInitial: $random(10, 30),
		
		x: 0,
		y: 0,
		vx: 0,
		vy: 0,
		
		padding: 40,
		
		width: 0,
		height: 0,
		
		drawCOG: false
	},
	
	particles: [],
	
	initialize: function(options, ctx) {
		this.setOptions(options);
		var opt = this.options;
		
		opt.x = $random(opt.padding, opt.width - opt.padding);
		opt.y = $random(opt.padding, opt.height - opt.padding);
		opt.vx = $random(-4, 4);
		opt.vy = $random(-4, 4);
		
		this.respawn(ctx);
	},
	
	respawn: function(ctx) {
		var opt = this.options;
		for (var i = 0; i < opt.pInitial; i++) {
			this.spawnParticle(opt.x, opt.y, $random(0, 250), $random(0, 9) >= 5, ctx);
		}
	},
	
	spawnParticle: function(px, py, age, sex, ctx) {
		for (var i = 0; i < this.options.pMax; i++) {
			if (!this.particles[i]) {
				var pt = new Particle({
					x: px,
					y: py,
					age: age,
					sex: sex
				});
				pt.move(this.options.x, this.options.y, ctx);
				this.options.pCount += 1;
				this.particles[i] = pt;
				return true;
			}
		}
		return false;
	},
	
	move: function(ctx) {
		var opt = this.options;
		
		opt.x += opt.vx * $random(0, 3);
		opt.y += opt.vy * $random(0, 3);
		
		opt.vx += $random(-1, 1);
		opt.vy += $random(-1, 1);
		
		opt.vx = opt.vx.limit(-1, 1);
		opt.vy = opt.vy.limit(-1, 1);
		
		if ((opt.x < opt.padding) || (opt.x > opt.width - opt.padding)) {
			var tmp = opt.width / 8;
			opt.x = $random(tmp, tmp * 7);
		}
		if ((opt.y < opt.padding) || (opt.y > opt.height - opt.padding)) {
			var tmp = opt.height / 8;
			opt.y = $random(tmp, tmp * 7);
		}
		
		var deadCount = 0;
		for (var i = 0; i < opt.pCount; i++) {
			var part = this.particles[i];
			if (part) {
				if (part.options.age >= 1000) {
					delete this.particles[i];
					delete part;
					opt.pCount -= 1;
				} else {
					part.move(opt.x, opt.y, ctx);
					
					if ((part.options.age < 250) || (part.options.age > 750) || (part.options.spawnTime > 0)) {
						// We can't spawn, 'cos we're not the right age or have spawned too recently
					} else {
						var chkPart, chkHalfSize;
						// We can spawn -- check if we will.
						for (var j = i + 1; j < opt.pCount; j++) {
							chkPart = this.particles[j];
							if (chkPart) {
								chkHalfSize = chkPart.options.size / 2;
								if ((part.options.sex !== chkPart.options.sex) &&
										(chkPart.options.spawnTime === 0) &&
										(chkPart.options.age > 250) &&
										(chkPart.options.age < 750) &&
										(part.options.x < chkPart.options.x + chkHalfSize) &&
										(part.options.x > chkPart.options.x - chkHalfSize) &&
										(part.options.y < chkPart.options.y + chkHalfSize) &&
										(part.options.y > chkPart.options.y - chkHalfSize) &&
										($random(0, 10) > 7)
									 ) {
									if (this.spawnParticle(part.options.x, part.options.y, 0, $random(0, 9) >= 5, ctx)) {
										part.options.vx = part.options.vx / 2;
										part.options.vy = part.options.vy / 2;
										chkPart.options.vx = chkPart.options.vx / 2;
										chkPart.options.vy = chkPart.options.vy / 2;
										part.options.spawnTime = 60;
										chkPart.options.spawnTime = 60;
									}
								}
							}
						}
					}
				}
			}
		}
		
		if (opt.drawCOG) {
			ctx.fillStyle = 'rgba(255, 255, 255, 0.025)';
			ctx.beginPath();
			ctx.arc(opt.x, opt.y, 100, 0, twoPi, false);
			ctx.closePath();
			ctx.fill();
		}
	}
});

var Particle = new Class({
	Implements: Options,
	
	Binds: [
		'move'
	],
	
	options: {
		colourFemale: 'rgba(255, 49, 166, ',
		colourMale: 'rgba(49, 153, 255, ',
		colourSpawning: 'rgba(255, 255, 166, ',
		colourDying: 'rgba(0, 0, 0, 0.5)',
		
		magic: 12, // A magic number. :)
		x: 0, y: 0, // Position
		vx: 0, vy: 0, // Velocity
		age: 0,
		spawnTime: 0,
		sex: false,
		size: 12
	},
	
	initialize: function(options) {
		this.setOptions(options);
		this.options.x += $random(-this.options.magic, this.options.magic);
		this.options.y += $random(-this.options.magic, this.options.magic);
		this.options.vx = $random(-2, 2);
		this.options.vy = $random(-2, 2);
	},
	
	move: function(gx, gy, ctx) {
		var opt = this.options;
		opt.vx += 0.1 * Math.random() * (gx - opt.x).limit(-1, 1);
		opt.vy += 0.1 * Math.random() * (gy - opt.y).limit(-1, 1);
		
		var nAge = opt.age.norm(0, 1000);
		
		var maxVel = 4 - (nAge * 3);
		if (opt.spawnTime > 0) {
			maxVel = maxVel / (opt.spawnTime / 2);
		}
		opt.vx = opt.vx.limit(-maxVel, maxVel);
		opt.vy = opt.vy.limit(-maxVel, maxVel);
		
		opt.x += opt.vx;
		opt.y += opt.vy;
		
		opt.age += 1;
		
		var drawSize = 1 + nAge * opt.magic;
		var opacity = 1 - nAge;
		
		ctx.fillStyle = (opt.spawnTime > 59 ? opt.colourSpawning : (opt.sex ? opt.colourFemale : opt.colourMale)) + opacity + ')';
		if (opt.age > 995) ctx.fillStyle = opt.colourDying;
		
		if (opt.spawnTime > 0) {
			drawSize *= 1.5;
		}
		ctx.beginPath();
		ctx.arc(opt.x, opt.y, drawSize, 0, twoPi, false);
		ctx.closePath();
		ctx.fill();
		
		opt.spawnTime = Math.max(0, opt.spawnTime - 1);
	}
});
