var Field = function(){ this.init.apply(this, arguments); };
Field.prototype = {
	particles: [],
	permissivity: 1,
	maxSpeed: 0,
	nextID: 0,
	scale: .3,
	interval: 31,
	gridSize: 0,
	grid: [],

	init: function(el, options){
		Object.extend(this, options || {});
		this.width = el.offsetWidth;
		this.height = el.offsetHeight;
		this.el = el;
		this.el.style.position = 'relative';
		this.update();
	},
	
	update: function(){
		for(var i=this.particles.length-1; i>=0; i--) this.particles[i].update();
		this.timer = window.setTimeout(this.update.bind(this), this.interval);
	},
	
	addParticle: function(options){
		if(!options) options = {};
		var particle = new Particle(options.x || Math.random()*this.width, options.y || Math.random()*this.height, this, options);
		if(this.bouncyWalls)
			particle.lockToBoundry(0, this.width, 0, this.height);
		this.particles.push(particle);
		return particle;
	},
	
	getNextID: function(){
		return this.nextID++;
	},
	
	getGridLocation: function(x, y){
		return [
			Math.floor(x / this.gridSize) || 0,
			Math.floor(y / this.gridSize) || 0
		]
	},
	
	getParticlesAtGridLocation: function(x, y){
		if(!this.grid[x] || !this.grid[x][y]) return false;
		return this.grid[x][y];
	}
	
}

var Particle = function(){ this.init.apply(this, arguments); };
Particle.prototype = {
	xVel:0,
	yVel:0,
	xAcc:0,
	yAcc:0,
	xForce:0,
	yForce:0,
	radius:5,
	mass:1,
	restraints: [],
	ignoredParticles: {},
	
	init: function(x, y, field, options){
		this.x = x;
		this.y = y;
		this.field = field;
		Object.extend(this, options || {});
		this.id = field.getNextID();
		this.el = document.createElement('img');
		this.el.src = 'ball.gif';
		this.el.style.position = 'absolute';
		this.resize(this.radius);
		this.update();
		field.el.appendChild(this.el);
	},

	update: function(){
		this.move();
		if(this.followMouse) return;
		this.processRestraints();
		this.xVel = this.xVel*this.field.permissivity + this.xForce/this.mass + this.xAcc;
		this.yVel = this.yVel*this.field.permissivity + this.yForce/this.mass + this.yAcc;
		this.x += this.xVel * this.field.scale;
		this.y += this.yVel * this.field.scale;
	},
	
	move: function(){
		this.el.style.left =  this.x + 'px';
		this.el.style.top = this.y + 'px';
	},
	
	resize: function(radius){
		this.radius = radius;
		this.el.style.width = radius * 2 + 'px';
		this.el.style.height = radius * 2 + 'px';
		this.el.style.marginLeft = -radius + 'px';
		this.el.style.marginTop = -radius + 'px';
	},
	
	processRestraints: function(){
		for(var i=this.restraints.length-1; i>=0; i--) this.restraints[i]();
	},
	
	addRestraint: function(fn){
		this.restraints.push(fn);
	},
	
	lockToMouseDown: function(momentum){
		var self = this;
		this.el.style.cursor = 'move';
		this.el.onmousedown = function(e){
			self.followMouse = true;
			Mouse.lastXY = Mouse.getXY(e);
			document.onmousemove = function(e){
				var xy = Mouse.getXY(e);
				var x = xy[0] - Mouse.lastXY[0];
				var y = xy[1] - Mouse.lastXY[1];
				if(momentum) self.nudgeWithVelocity(x, y);
				else self.nudge(x, y);
				Mouse.lastXY = xy;
				return false;
			};
			document.onmouseup =  function(){
				self.followMouse = false;
				document.onmousemove = null;
			};
			return false;
		};
	},
	
	nudge: function(x, y){
		this.x += x;
		this.y += y;
	},
	
	nudgeWithVelocity: function(x, y){
		this.xVel = x;
		this.yVel = y;
		this.nudge(x, y);
	},
	
	lockSpringTo: function(particle, multiplier){
		if(!multiplier) multiplier = .5;
		var currentDistance = this.getDistFrom(particle);
		this.addRestraint(function(){
			var dist = this.getDistFrom(particle);
			var diff = dist - currentDistance;
			this.accelerateTowards(particle, diff*multiplier);
			particle.accelerateTowards(this, diff*multiplier);
		}.bind(this));
	},

	lockGravityTo: function(particle){
		this.addRestraint(function(){
			this.gravitateTowards(particle);
		}.bind(this));
	},
	
	lockToBoundry: function(xMin, xMax, yMin, yMax){
		xMin = xMin + this.radius;
		yMin = yMin + this.radius;
		xMax = xMax - this.radius;
		yMax = yMax - this.radius;
		this.addRestraint(function(){
			if(this.x < xMin){
				this.xVel = -1*this.xVel;
				this.x = xMin;
			}
			else if(this.x > xMax){
				this.xVel = -1*this.xVel;
				this.x = xMax;
			}
			if(this.y < yMin){
				this.yVel = -1*this.yVel;
				this.y = yMin;
			}
			else if(this.y > yMax){
				this.yVel = -1*this.yVel;
				this.y = yMax;
			}
		}.bind(this));
	},
	
	lockToFieldSpeedLimit: function(){
		this.addRestraint(function(){
			if(this.getSpeed() > maxSpeed) this.setSpeed(this.field.maxSpeed);
		}.bind(this));	
	},
	
	lockToXForce: function(f){
		this.xForce = f;
	},
	
	lockToYForce: function(f){
		this.yForce = f;
	},
	
	lockToXAccel: function(a){
		this.xAcc = a;
	},
	
	lockToYAccel: function(a){
		this.yAcc = a;
	},
	
	moveTowards: function(particle, amount){
		var xy = this.getUnitVectorTowards(particle);
		this.x += xy[0] * amount;
		this.y += xy[1] * amount;
	},
	
	accelerateTowards: function(particle, multiplier){
		var xy = this.getUnitVectorTowards(particle);
		this.xForce = xy[0] * multiplier;
		this.yForce = xy[1] * multiplier;
	},
	
	gravitateTowards: function(particle){
		var dist = this.getDistFrom(particle);
		//this.attractTo(particle, this.mass * particle.mass / dist * dist);
		
		this.accelerateTowards(particle, particle.mass / dist * dist);
	},
	
	attractTo: function(particle, force){
		this.accelerateTowards(particle, force / this.mass);
	},
	
	getDistFrom: function(particle){
		var d1 = this.x - particle.x;
		var d2 = this.y - particle.y;
		return Math.sqrt(d1*d1 + d2*d2);
	},
	
	getUnitVectorTowards: function(particle){
		var len = this.getDistFrom(particle);
		return [
			(particle.x - this.x) / len,
			(particle.y - this.y) / len
		];	
	},
	
	getSpeed: function(){
		return Math.sqrt(this.xVel*this.xVel + this.yVel*this.yVel);	
	},
	
	setSpeed: function(speed){
		var currentSpeed = this.getSpeed();
		this.xVel = this.xVel / currentSpeed * speed;
		this.yVel = this.yVel / currentSpeed * speed;
	},
	
	setDirection: function(x, y){
		var currentSpeed = this.getSpeed();
		this.xVel = this.xVel * currentSpeed * x;
		this.yVel = this.yVel * currentSpeed * y;
	},
	
	bounceInto: function(particle){
		var dist = this.getDistFrom(particle);
		var overlap = this.radius + particle.radius - dist;
		if(overlap > 0){
			var s1 = this.getSpeedAfterElasticCollisionWith(particle);
			var s2 = particle.getSpeedAfterElasticCollisionWith(this);
			
			var xy = this.getUnitVectorTowards(particle);
			this.xVel = -xy[0];
			this.yVel = -xy[1];
			particle.xVel = xy[0];
			particle.yVel = xy[1];
			
			this.setSpeed(s1);
			particle.setSpeed(s2);
			
			this.moveAwayFrom(particle, overlap/2);
			particle.moveAwayFrom(this, overlap/2);
		}
	},
	
	getSpeedAfterElasticCollisionWith: function(particle){
		return (this.getSpeed() * (this.mass - particle.mass) + 2 * particle.getSpeed() * particle.mass) / (this.mass + particle.mass)
	},
	
	moveAwayFrom: function(particle, dist){
		var xy = this.getUnitVectorTowards(particle);
		this.x += -xy[0] * dist;
		this.y += -xy[1] * dist;
	},
	
	updateGridLocation: function(){
		var xy = this.field.getGridLocation(this.x, this.y);
		if(this.lastGridLocation)
			this.removeFromGrid();
		this.addToGrid(xy[0], xy[1]);
	},
	
	addToGrid: function(x, y){
		var grid = this.field.grid;
		if(!grid[x]) grid[x] = [];
		if(!grid[x][y]) grid[x][y] = [];
		grid[x][y][this.id] = true;
		this.lastGridLocation = [x, y];
	},
	
	removeFromGrid: function(){
		var x = this.lastGridLocation[0];
		var y = this.lastGridLocation[1];
		delete this.field.grid[x][y][this.id];
	},
	
	bounceOffNearbyParticles: function(){
		if(Math.abs(this.x-this.lastXY[0]) < this.field.scale && Math.abs(this.y-this.lastXY[1]) < this.field.scale) return;
		this.lastXY = [this.x, this.y];	
		var xmin = this.lastGridLocation[0] - 1;
		var ymin = this.lastGridLocation[1] - 1;
		var parts, i, j;
		for(i=xmin; i<xmin+3; i++){
			for(j=ymin; j<ymin+3; j++){
				if(parts = this.field.getParticlesAtGridLocation(i, j)){
					for(var k in parts){
						if(this.id != k){
							this.bounceInto(this.field.particles[k]);
						}
					}
				}
			}
		}
	},
	
	registerToCollisionGrid: function(){
		this.lastXY = [0,0];
		if(this.radius*2 > this.field.gridSize)
			this.field.gridSize = this.radius*2;
		this.updateGridLocation();
		this.lockToGridCollisions();
	},
	
	lockToGridCollisions: function(){
		this.addRestraint(function(){
			this.updateGridLocation();
			this.bounceOffNearbyParticles();
		}.bind(this));
	}
}

Function.prototype.bind = function(toThis, args){
	if(!args) args = [];
	var self = this;
	return function(){ self.apply(toThis, args); }
}

var Mouse = {
	getXY: function(e){
		if(!e) var e = window.event;
		return [e.clientX, e.clientY];
	}
}

Object.extend = function(ob, extension){
	for(var i in extension){
		ob[i] = extension[i];
	}
}