/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Erik Byström (erik.bystrom@gmail.com)
 * http://slackers.se/2009/jstagcloud-js-canvas-3d-tag-cloud
 */


jQuery.fn.tagcloud = function (options) {
	return this.each(function () {
		// quick canvas init
		var self = this;		
		this.ctx = this.getContext('2d');
		
		this.angle = [0,0,0];
		this.velocity = [0,0,0];
		this.mouseIn = false;
		
		// method definitions
		this.toColor = function(color) {
			if (color.length == 3)
				return 'rgb('+color[0]+','+color[1]+','+color[2]+')';
			return 'rgba('+color[0]+','+color[1]+','+color[2]+','+color[3]+')';
		}
		
		this.clear = function() {
			this.ctx.fillStyle = this.toColor(options.background, 0);
			this.ctx.fillRect(0,0,this.width,this.height);			
		}
		
		/**
		 * Positions all texts some where on the sphere. 
		 */
		this.build = function() {
			for(var i=0;i<this.options.data.length;i++) {
				var data = this.options.data[i];
				
				data.width = this.ctx.measureText(data.tag);
				data.tpos = [0,0,0];
				
				var s = Math.random()*Math.PI*2;
				var t = Math.random()*Math.PI*2;
				//r(s,t)=(cos(t)*sin(s),sin(t)*sin(s),cos(s))
				data.pos = [
				            Math.cos(t)*Math.sin(s),
				            Math.sin(t)*Math.sin(s),
				            Math.cos(s)
				            ];
			}
		}
		
		/**
		 * Transform and sort the result
		 */
		this.transform = function() {
			var x0 = this.width * 0.5;
			var y0 = this.height * 0.5;	
			
			// build matrix
			var cx = Math.cos(this.angle[0]);
			var sx = Math.sin(this.angle[0]);
			var cy = Math.cos(this.angle[1]);
			var sy = Math.sin(this.angle[1]);
			var cz = Math.cos(this.angle[2]);
			var sz = Math.sin(this.angle[2]);
			
			var rotation = [
			                [cy*cz, cx*sz+sx*sy*cz, sx*sz-cx*sy*cz],
			                [-cy*sz, cx*cz-sx*sy*sz, cx*sy*sz+sx*cz],
			                [sy, -sx*cy, cx*cy]
			               ];
			
			for(var i=0;i<this.options.data.length;i++) {
				var node = this.options.data[i];
				var c = this.options.textcolor;
				
				for (var j=0;j<3;j++) {
					node.tpos[j] = 0;
					for (var k=0;k<3;k++) {
						node.tpos[j] += rotation[j][k]*node.pos[k];
					}
				}
				
				node.screenX = (1 + node.tpos[0]/(2+node.tpos[2]))*x0;
				node.screenY = (1 + node.tpos[1]/(2+node.tpos[2]))*y0;
				
				node.textSize = 8 + (1.0 - node.tpos[2])*10;				
				node.textColor = 'rgba('+c[0]+','+c[1]+','+c[2]+','+(1-(1+node.tpos[2])*0.4)+')';								
			}			
			
			this.options.data.sort(function (a,b) {
				return (a.tpos[2] < b.tpos[2]) ? -1 : ( (a.tpos[2] > b.tpos[2]) ? 1 : 0);
			});
		}
		
		/**
		 * Renders one frame of the cloud.
		 */
		this.render = function() {			
			for(var i=0;i<this.options.data.length;i++) {
				var node = this.options.data[i];				
				this.ctx.fillStyle = node.textColor;				
				
				var x = node.screenX;
				var y = node.screenY;
				
				this.ctx.drawText(node.tag, x - node.width*0.5, y, node.textSize + 'px Verdana');
				node.width = this.ctx.measureText(node.tag);
			}
			if (self.mouseIn) {
				for(var i=0;i<this.options.data.length;i++) {
					var node = this.options.data[i];					

					var x = node.screenX;
					var y = node.screenY;
					var w = node.width;
					var h = node.textSize;
					
					if (x-w*0.4 <= self.mouseX && self.mouseX <= x+w*0.6 && y <= self.mouseY && self.mouseY <= y+h) {
						this.ctx.strokeRect(node.screenX-w*0.5-1, node.screenY-h*0.8-1, w+2, h+2);
						this.selected = node;
						return;
					}
				}
			}		
			this.selected = false;
		}
		
		/**
		 * Called on mousemove events
		 */
		this.mousemove = function(event) {
			this.mouseIn = true;
			
			var x = event.clientX;
			var y = event.clientY;

			var dx = 2*(x / this.width - 0.5);
			var dy = 2*(y / this.height - 0.5);			
			
			if (-0.25 <= dx && dx <= 0.25) {
				dx = 0;
			}
			
			if (-0.25 <= dy && dy <= 0.25) {
				dy = 0;
			}
			
			this.velocity[0] = dy;
			this.velocity[1] = -dx;
			
			this.mouseX = x;
			this.mouseY = y;			
		}
		
		/**
		 * Update the animation
		 */
		this.update = function() {
			self.transform();
			self.clear();
			self.render();			
			
			for (var i=0;i<self.angle.length;i++) {
				self.angle[i] += self.velocity[i] * self.dt;
				if (!self.mouseIn) {
					self.velocity[i] = self.velocity[i]*0.95;
				}
			}
		}

		/**
		 * Handle a URL click
		 */
		this.handleurl = function() {
			if (this.selected) {
				window.location.href =this.options.baseurl + this.selected.url; 
			}
		}
		
		/**
		 * Toggle animation and detect text-clicks
		 */
		this.toggle = function() {
			if (this.running) {
				this.stop();
			} else {
				this.start();
			}
		}
				
		/**
		 * Start animation
		 */
		this.start = function() {			
			this.update();
			this.handle = setInterval(this.update, this.options.interval);			
			this.running = true;
		}
		
		/**
		 * Stop animation
		 */
		this.stop = function() {
			clearInterval(this.handle);
			this.running = false;
		}
		
		// setup some methods for increased compatibillity
		this.ctx.drawText = function(text, x, y, font) {
			if (this.fillText) {
				this.font = font;				
				this.strokeText(text, x, y);
			} else {
				this.mozTextStyle = font;
				this.translate(x, y);
				this.mozDrawText(text);
				this.translate(-x, -y);				
			}
		}
		
		if (!this.ctx.measureText) {
			this.ctx.measureText = this.ctx.mozMeasureText;
		}		
		
		// fix
		this.options = options;
		this.options.textcolor.push(0);
		this.dt = 0.001 * (options.interval || 50)
		
		// "main"
		jQuery(this).click(this.handleurl);
		jQuery(this).mousemove(this.mousemove);
		jQuery(this).mouseout(function() {this.mouseIn = false;});
		
		this.build();
		this.start();

	});
};
