
function visualizer(canv_id)
{
	this.canvas_id = canv_id;
	this.width = $("#"+canv_id).attr('width');
	this.height = $("#"+canv_id).attr('height');
	this.sim = new simulation(15, this.width, this.height);
	this.compressedWave;
	this.samplesPerBar = 16;
	this.maxOldWaves = 10;
	this.oldWaves = new Queue();
	this.stepThetaAngle = 0.3; 
	this.prevStartAngle = 0;
	this.shiftin = new vector(-this.width/2, -this.height/2);
	this.shiftout = new vector(this.width/2, this.height/2);
	this.eqMaxes = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];

	this.getY = function(idx, d, h, cy)
	{
		var val= d[idx];
		var y = val*-h/2 + cy;
		return y;
	}

	this.compressData = function(data)
	{
		var r = new Array(data.length/this.samplesPerBar);
		for(var i = 0; i < r.length; i++) r[i] = 0;
		for(var i = 0; i < data.length; i++)
		{
			var j = Math.floor(i / this.samplesPerBar);
			r[j] += data[i]/this.samplesPerBar;
		}
		return r;
	}
	this.clearCanvas = function(g)
	{
		g.fillStyle = 'rgb(0,0,0)';
		g.fillRect(0, 0, this.width, this.height);
	}

	this.drawWave = function(g)
	{
		var len = this.curWaveData.length;

		var lw = this.width/len;
		var cp = this.curWaveData.slice();
		this.prevStartAngle += this.stepThetaAngle;
		
		this.oldWaves.enqueue({ang: this.prevStartAngle, data:cp});
		if(this.oldWaves.length > this.maxOldWaves)
			this.oldWaves.dequeue();
		//these variables need to be copied to the local
	        //context so that they can be 'closed' by the foreach callback
		//maybe foreach isnt worth it
		var shiftin = this.shiftin;
		var shiftout = this.shiftout;
		var maxOldWaves = this.maxOldWaves;
		var height = this.height;
		var getY = this.getY;
		this.oldWaves.foreach(function (o, i)
				{
					var v = o.data;
					var theta = o.ang;
					var frac = (i+1)/maxOldWaves; 
					g.strokeStyle = hsv2rgb(0.86,1.0,0.87*frac);
					var h = height/8*frac;
					var rotMatrix = new matrix(theta);
					var tp = new vector(0, getY(0, v, h, height/2));
					tp = tp.add(shiftin);
					tp = rotMatrix.transformVector(tp);
					tp = tp.add(shiftout);
					g.beginPath();
					g.moveTo(tp.x, tp.y);
					for(var i=1;i<len;i++)
					{
						tp = new vector(i*lw, getY(i, v, h, height/2));
						tp = tp.add(shiftin);
						tp = rotMatrix.transformVector(tp);
						tp = tp.add(shiftout);
						g.lineTo(tp.x, tp.y);
					}
					g.stroke();
				});
		while(this.prevStartAngle > 2*Math.PI)
			this.prevStartAngle -= 2*Math.PI;
	}


	this.drawEQBar = function(g, x, h, w, m)
	{
		g.fillStyle = '#649632';

		g.fillRect(x, this.height-h, w, h);
		g.strokeStyle = 'rgb(255,0,0)';
		g.beginPath();
		g.moveTo(x, this.height-m);
		g.lineTo(x + w, this.height-m);
		g.stroke();
	}

	this.drawEQ = function(g)
	{	
		var eq = this.compressData(this.curEQData);
		for(var i = 0; i < 16; i++)
		{
			if(eq[i]>this.eqMaxes[i])
			       this.eqMaxes[i] = eq[i];
			else this.eqMaxes[i]*=0.99;
		}
		g.fillStyle = '#649632';
		var w = (this.width - 4)/eq.length;
		//draw bars
		for(var i = 0; i < eq.length; i++)
		{
			this.drawEQBar(g, 4 + i*w, this.height*eq[i], w - 4, this.height*this.eqMaxes[i]);
		}

	}

	this.curWaveData;
	this.curEQData;
	this.drawCanvas = function(waveData, eqData)
	{
		this.curWaveData = waveData;
		this.curEQData = eqData;
		var canvas = document.getElementById(this.canvas_id);
		if(canvas.getContext)
		{
			var g = canvas.getContext('2d');
			this.clearCanvas(g);
			this.drawWave(g);
			this.drawEQ(g);	
			
			this.compressedWave = this.compressData(this.curWaveData);
			this.sim.draw(g, this.compressedWave);
			this.sim.step();
		}		
	}
}

function simulation(n, w, h)
{
	//http://www.vergenet.net/~conrad/boids/pseudocode.html
	this.width = w;
	this.height = h;
	this.numBoids = n;
	this.boids = new Array();
	this.rule1Scale = 0.005;
	this.rule2Distance = 20;
	this.rule3Distance = 50;
	this.rule3Scale = 1/20;
	this.rule4Scale = 15;
	this.lastStepTime = (new Date).getTime();
	this.rule1 = function(idx)
			{
				//attract each boid towards the center of the flock
				var b = this.boids[idx];
				var center = new vector(0,0);
				for(var i = 0; i < this.boids.length; i++)
				{
					if(i == idx) continue;
					var ob = this.boids[i];
					center = center.add(ob.pos);
				}
				center = center.scale(1/(this.boids.length - 1));
				return b.pos.clone().subtract(center).scale(-this.rule1Scale);
			}

	this.rule2 = function(idx)
			{
				var b = this.boids[idx];
				//avoid nearby boids
				var v = new vector(0,0);
				for(var i = 0; i < this.boids.length; i++)
				{
					if(i == idx) continue;
					var ob = this.boids[i]
					var diff = b.pos.clone().subtract(ob.pos);
					var dist = diff.length();
					if(dist < this.rule2Distance)
					{
						v = v.add(diff);
					}
				}
				return v;
			}
	this.rule3 = function(idx)
			{
				//match velocity with nearby boids
				var b = this.boids[idx];
				var v = new vector(0,0);
				for(var i = 0; i < this.boids.length; i++)
				{
					if(i == idx) continue;
					var ob = this.boids[i];
					if(b.pos.distance(ob.pos) < this.rule3Distance)
					{
						v = v.add(ob.vel);
					}
				}
				return v.subtract(b.vel).scale(this.rule3Scale);
			}
	this.rule4 = function(idx)
			{
				//limit to the box
				var b = this.boids[idx];
				var v = new vector(0,0);
	
				if(b.pos.x < 20)
					v.x = 1;
				else if(b.pos.x > this.width - 20)
					v.x = -1;
				if(b.pos.y < 20)
					v.y = 1;
				else if(b.pos.y > this.height - 20)
					v.y = -1;

				return v.scale(this.rule4Scale);

			}
	this.step = function()
			{
				var ct = (new Date).getTime();
				var dt = (ct - this.lastStepTime)/1000;
				this.lastStepTime = ct;
				for(var i = 0; i < this.boids.length; i++)
				{
					var v1 = this.rule1(i);
					var v2 = this.rule2(i);
					var v3 = this.rule3(i);
					var v4 = this.rule4(i);

					var b = this.boids[i];
					b.updateVelocity(v1.add(v2).add(v3).add(v4), dt);
					b.updatePosition(dt);
				}

			}
	this.draw = function(g, compressedWave)
			{
				for(var i = 0; i < this.boids.length; i++)
				{
					var b = this.boids[i];
					b.draw(g, compressedWave);
				}
			}
	//initialization
	for(var i = 0; i < this.numBoids; i++)
	{
		this.boids.push(new boid(this.width, this.height));
	}

}

function matrix(theta)
{
	this.a = Math.cos(theta);
	this.b = -Math.sin(theta);
	this.c = Math.sin(theta);
	this.d = Math.cos(theta);
	
	this.transformVector = function(v)
			{
				return new vector(this.a*v.x+this.b*v.y, this.c*v.x+this.d*v.y);
			}
}
	

function vector(ix,iy)
{
	this.x = ix;
	this.y = iy;
	this.add = function(vec)
			{
				this.x += vec.x;
				this.y += vec.y;
				return this;
			}
	this.subtract = function(vec)
			{
				this.x -= vec.x;
				this.y -= vec.y;
				return this;
			}
	this.scale = function(s)
			{
				this.x *= s;
				this.y *= s;
				return this;
			}
	this.length = function()
			{
				return Math.sqrt(this.x*this.x + this.y*this.y);
			}
	this.clone = function()
			{
				return new vector(this.x,this.y);
			}
	this.distanceSquared = function(v)
				{
					var dx = this.x - v.x;
					var dy = this.y - v.y;
					return dx*dx + dy*dy;
				}
	this.distance = function(v)
				{
					return Math.sqrt(this.distanceSquared(v));
				}
	this.normalize = function()
			{
				return this.scale(1/this.length());
			}
	this.getPerpindicular = function()
			{
				return new vector(-this.y, this.x);
			}

}

function boid(canv_w, canv_h)
{	
	this.radius = 4;
	this.color = '#dd6101';
	this.otherColor = '#888888';
	this.maxVelocity = 20;
	this.pos = new vector(Math.random()*canv_w, Math.random()*canv_h);
	this.vel = new vector(Math.random()*this.maxVelocity, Math.random()*this.maxVelocity);
	this.limitVelocity = function()
				{
					var length = this.vel.length();
					if(length > this.maxVelocity)
					{
						this.vel.scale(this.maxVelocity/length);
					}
				}
	this.maxPrevPositions = 16;
	this.maxWaveHeight = 50;
	this.prevPositions = new Queue();
	this.draw = function(g, compressedWave)//g is a graphics context
			{
				g.fillStyle = this.color;
				g.beginPath();
				g.arc(this.pos.x+this.radius/2, this.pos.y, this.radius, 0, 2*Math.PI, true);
				g.fill();
				if(this.prevPositions.length > 0)
				{
					var pp = this.prevPositions.first.val;
					var pa = pp;
					var waveHeight = this.maxWaveHeight;
					var colorWave = this.color;
					var otherColor = this.otherColor;
					this.prevPositions.foreach(function (v, i)
					{
						if(i==0) return;//skip this first one
						var ca = v; 
						var dir = pa.clone().subtract(ca);
						dir = dir.normalize();
						dir = dir.getPerpindicular();
						var len = compressedWave[i]*waveHeight;
						dir = dir.scale(len);
						var cp = ca.clone().add(dir);

						g.strokeStyle = colorWave;
						g.beginPath();
						g.moveTo(pp.x, pp.y);
						g.lineTo(cp.x, cp.y);
						g.stroke();
					
				/*
		   				g.strokeStyle = otherColor;
						g.beginPath();
						g.moveTo(pa.x, pa.y);
						g.lineTo(ca.x, ca.y);
						g.stroke();
				*/
						pp = cp;
						pp = cp;
						pa = ca;
					});
				}
			}
	this.addPosition = function()
				{
					this.prevPositions.enqueue(this.pos.clone());
					if(this.prevPositions.length > this.maxPrevPositions)
					{
						this.prevPositions.dequeue();
					}
				}
	this.updatePosition = function(t)
				{
					this.addPosition();
					this.pos = this.pos.add(this.vel.clone().scale(1));
				}
	this.updateVelocity = function(acc, t)
				{
					this.vel = this.vel.add(acc.scale(1));
					this.limitVelocity();
				}
	//initialization
	this.limitVelocity();


}

function Node(v, n)
{
	this.val = v;
	this.next = n;
}

function Queue()
{
	this.first = null;
	this.last = null;
	this.length = 0;
	this.enqueue = function(v)
			{
				var node = new Node(v, null);
				if(this.first == null)
				{
					this.first = node;
					this.last = node;
				}
				else
				{
					this.last.next = node;
					this.last = node;
				}
				this.length++;
			}
	this.dequeue  = function()
			{
				if(this.first == null)
					return null;
			
				var rv = this.first.val;
				this.first = this.first.next;
				if(this.first.next == null) this.last = null;	
				this.length--;
				return rv;
			}
	this.foreach = function(f)
			{
				if(typeof f != "function")
					throw new TypeError();
				var c = this.first;
				var i = 0;
				while(c != null)
				{
					f.call(this, c.val, i);
					c = c.next;
					i++;
				}
			}
	
}

function hsv2rgb(h,s,v) {
	// Adapted from http://www.easyrgb.com/math.html
	// hsv values = 0 - 1, rgb values = 0 - 255
	var r, g, b;
	var RGB = new Array();
	if(s==0)
	{
		RGB['red']=RGB['green']=RGB['blue']=Math.round(v*255);
	}
	else
	{
		// h must be < 1
		var var_h = h * 6;
		if (var_h==6) var_h = 0;
		//Or ... var_i = floor( var_h )
		var var_i = Math.floor( var_h );
		var var_1 = v*(1-s);
		var var_2 = v*(1-s*(var_h-var_i));
		var var_3 = v*(1-s*(1-(var_h-var_i)));
		if(var_i==0)
		{ 
			var_r = v; 
			var_g = var_3; 
			var_b = var_1;
		}else if(var_i==1){ 
			var_r = var_2;
			var_g = v;
			var_b = var_1;
		}else if(var_i==2){
			var_r = var_1;
			var_g = v;
			var_b = var_3
		}else if(var_i==3){
			var_r = var_1;
			var_g = var_2;
			var_b = v;
		}else if (var_i==4){
			var_r = var_3;
			var_g = var_1;
			var_b = v;
		}else{ 
			var_r = v;
			var_g = var_1;
			var_b = var_2
		}
		//rgb results = 0 ÷ 255  
		RGB['red']=Math.round(var_r * 255);
		RGB['green']=Math.round(var_g * 255);
		RGB['blue']=Math.round(var_b * 255);
	}
	return "rgb("+RGB['red']+","+RGB['green']+","+RGB['blue']+")";  
}



