

(function($){

     // jquery plugin hook
     $.fn.tagcloud = function(options){
        
         // overwrite defaults with user-specified
         var opts = $.extend($.fn.tagcloud.defaults, options);
         opts.drawing_interval = 1/(opts.fps/1000);
         
         //create a new class for every matching element
         $(this).each(function(){
                          $('ul', $(this)).css('display', 'none');
                          new TagCloudClass($(this), opts);
                      });
         return this;         
     };


     //default values for setup
     $.fn.tagcloud.defaults = {
         zoom: 90,
         max_zoom: 120,
         min_zoom: 25,
         zoom_factor: 2, //multiplication factor for wheel delta
         rotate_factor: -0.45, // multiplication factor for rotation
         fps: 10, // frames per second
         centrex: 130, // set centre of display
         centrey: 100,
         min_font_size: 9, //font limits and units
         max_font_size: 18,
         font_units: 'px',
         random_points: 0         
     };

     var TagCloudClass = function(el, options){
         $(el).css('position', 'relative');

         // general values
         var eyez = -500;

         // set rotation (in this case, 5degrees)
         var rad = Math.PI/180;
         var global_cos = Math.cos(0);

         var hex = new Array("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f");

         // per-instance values
         var dirty = true;
         var container = $(el);
         var id_stub = 'tc_' + $(el).attr('id') + "_";
         var opts = options;
         var zoom = opts.zoom;
         var depth;
         var lastx = 0;
         var lasty = 0;
	 var points = [];
         points['data'] = [];
         var drawing_interval;
         var cmx = options.centrex; 
         var cmy = options.centrey;
         var bg_colour, fg_colour;
         if (options.background_colour){
             bg_colour = parsecolour(options.background_colour);
         }else{
             bg_colour = parsecolour($(el).css('background-color'));
         }
         if (options.foreground_colour){
             fg_colour = parsecolour(options.foreground_colour);
         }else{
             fg_colour = parsecolour($(el).css('color'));
         }
         
         function parsecolour(colour){
             function parse_rgb_colour(colour){
                 rgb = colour.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
                 if(rgb.length > 3){
                     return {"r" : parseInt(rgb[1]), "g": parseInt(rgb[2]), "b" : parseInt(rgb[3])};                 
                 }else{  
                     return {"r":0, "g":0, "b":0};
                 }
             }
             function parse_hex_colour(colour){
                 var r = 0, g = 0, b = 0;
                 if(colour.length > 4)
                 {
                     r = parseInt(colour.substr(1,2), 16);
                     g = parseInt(colour.substr(3,2), 16);
                     b = parseInt(colour.substr(5,2), 16);
                 }
                 else
                 {
                     r = parseInt(colour.substr(1,1)+colour.substr(1,1), 16);
                     g = parseInt(colour.substr(2,1)+colour.substr(2,1), 16);
                     b = parseInt(colour.substr(3,1)+colour.substr(3,1), 16);
                 }
                 return {"r" : r, "g" : g, "b" : b};
             }

             if(colour.substr(0, 1) === '#')
             {
                 return parse_hex_colour(colour);
             }
             else if (colour.substr(0,3) === 'rgb')
             {
                 return parse_rgb_colour(colour);
             }
             else{
                 //somehow we've got a plain old string as a colour
                 if(window.console != undefined)
                     console.log("unable to parse:'" + colour + "' please ensure background and foreground colors for the container are set as hex values");
                 return null;
             }
         }
             
         function getcolour(num){
             if(num>256){num=256;}
             if(num<0){num=0;}

             var r = getshade(bg_colour.r, fg_colour.r, num);
             var g = getshade(bg_colour.g, fg_colour.g, num);
             var b = getshade(bg_colour.b, fg_colour.b, num);

             var ret  = "rgb(" + r + ", "+ g + ", " + b + ")"; 
             return ret;
         }

         function getshade(lbound, ubound, dist){
             var scope = ubound - lbound;
             var dist_percent = scope / 100;
             var shade = Math.round(lbound + (dist * dist_percent));
             return shade;
         }

         
         //drawing and rotation...				
         function rotx(s, c){
             for(var p in points.data)
             {
	         var temp = s * points.data[p].y + c * points.data[p].z;
	         points.data[p].y = c * points.data[p].y - s * points.data[p].z;
	         points.data[p].z = temp;
             }
         }

         function roty(s, c){
             for(var p in points.data){
	         var temp = - s * points.data[p].x + c * points.data[p].z;
	         points.data[p].x = c * points.data[p].x + s * points.data[p].z;
	         points.data[p].z = temp; 
             }
         }

         function rotz(s, c){
             for(var p in points.data) {
	         var temp = s * points.data[p].x + c * points.data[p].y;
	         points.data[p].x = c * points.data[p].x - s * points.data[p].y;
	         points.data[p].y = temp;
             }
         }

         function zoomed(by){
             zoom += by * opts.zoom_factor;

             if (zoom > opts.max_zoom) {
	         zoom = opts.max_zoom;
             }
             if (zoom < opts.min_zoom) {
	         zoom = opts.min_zoom;
             }

             depth = -(zoom * (eyez - opts.max_zoom) / 100) + eyez;
         }

         function moved(mx,my){
             if(mx>lastx){
                 var angle = mx - lastx;
	         var sin = -Math.sin(angle*options.rotate_factor*rad);
                 var cos = Math.cos(angle*options.rotate_factor*rad);
	         roty(sin, cos);
             }
             if(mx<lastx){
                 var angle = lastx - mx;
	         var sin = Math.sin(angle*options.rotate_factor*rad);
                 var cos = Math.cos(angle*options.rotate_factor*rad);
	         roty(sin, cos);			
             }
             if(my>lasty){
                 var angle = my - lasty;
	         var sin = Math.sin(angle*options.rotate_factor*rad);
                 var cos = Math.cos(angle*options.rotate_factor*rad);
	         rotx(sin, cos);
             }
             if(my<lasty){
                 var angle = lasty - my;
	         var sin = -Math.sin(angle*options.rotate_factor*rad);
                 var cos = Math.cos(angle*options.rotate_factor*rad);
	         rotx(sin, cos);
             }

             lastx = mx;
             lasty = my;
             dirty = true;
         }

         function draw(){
             // calculate 2D coordinates
             if(dirty){
                 var smallz = 10000; var bigz = -10000;
                 for(var r_p in points.data){
	             if(points.data[r_p].z < smallz){smallz = points.data[r_p].z;}
	             if(points.data[r_p].z > bigz){bigz = points.data[r_p].z;}
                 }
                 var minz = Math.min(smallz, bigz);
                 var maxz = Math.max(smallz, bigz);
                 var diffz = maxz - minz;
                 
                 for(var s_p in points.data){ 
                     //normalise depth
	             var u = (depth - eyez)/(points.data[s_p].z - eyez);

                     // calculate normalised grey value
                     var dist = Math.round(((maxz - points.data[s_p].z)/diffz) * 100);
                     var dist_colour = getcolour(dist);
                     //set new 2d positions for the data
                     $('#'+points.data[s_p].id + ' a', container).css('color',dist_colour);
                     $('#'+points.data[s_p].id, container).css('z-index',dist);
                     $('#'+points.data[s_p].id, container).css('left', u * points.data[s_p].x + cmx - points.data[s_p].cwidth);
                     $('#'+points.data[s_p].id, container).css('top', u * points.data[s_p].y + cmy); 
                 }			
                 dirty = false;
             }
         }

         // number of elements we're adding and placeholders for range values
         points.count = $('li a', container).length;
         points.largest = 1;
         points.smallest = 0;


         $('li a', container).each(function(idx, val){

                                       var sz = parseInt($(this).attr('rel'));
                                       if(sz == 0) 
                                           sz = 1;

                                       points.data[idx] = {
                                           id:id_stub + idx,
                                           size:sz
                                       };

                                       
                                       var h = -1 + 2*(idx)/(points.count-1);
                                       points.data[idx].theta = Math.acos(h);
                                       if(idx == 0 || idx == points.count-1){
                                           points.data[idx].phi = 0;
                                       }
                                       else{
                                           points.data[idx].phi = (points.data[idx-1].phi + 3.6/Math.sqrt(points.count*(1-Math.pow(h,2)))) % (2 * Math.PI);
                                       }

                                       points.data[idx].x = Math.cos(points.data[idx].phi) * Math.sin(points.data[idx].theta) * (cmx/2);
                                       points.data[idx].y = Math.sin(points.data[idx].phi) * Math.sin(points.data[idx].theta) * (cmy/2);
                                       points.data[idx].z = Math.cos(points.data[idx].theta) * (cmx/2);

                                       if(sz > points.largest) points.largest = sz;
                                       if(sz < points.smallest) points.smallest = sz;

                                       container.append('<div id="'+ id_stub + idx +'" class="point" style="position:absolute;display:none;"><a href=' + $(this).attr('href')  + '>' + $(this).html()  + '</a></div>');
                                   });

         //if required to do so (by opts.random_points being > 0) we need to generate some random points on the sphere
         //bit cheezy, but can make more sparse data sets look a bit more believable
         if(opts.random_points > 0){
             for(b=0; b<opts.random_points; b++){
                 points.count++;                                                  
                 points.data[points.count] = {
                     id:id_stub + points.count,
                     size:1
                 };
                 points.data[points.count].theta = Math.random() * 2 * Math.PI;
                 points.data[points.count].phi = Math.random() * 2 * Math.PI;
                 points.data[points.count].x = Math.cos(points.data[points.count].phi) * Math.sin(points.data[points.count].theta) * (cmx/2);
                 points.data[points.count].y = Math.sin(points.data[points.count].phi) * Math.sin(points.data[points.count].theta) * (cmy/2);
                 points.data[points.count].z = Math.cos(points.data[points.count].theta) * (cmx/2);
                 container.append('<div id="'+ id_stub + points.count +'" class="point" style="position:absolute;"><a>.</a></div>');
             }
         }
         
         //tag size and font size ranges 
         var sz_range = points.largest - points.smallest + 1; 
         var sz_n_range = opts.max_font_size - opts.min_font_size + 1;
         
         //set font size to normalised tag size
         for(var p in points.data){
             var sz = points.data[p].size;
             var sz_n = parseInt((sz / sz_range) * sz_n_range) + opts.min_font_size;
             if(!$('#' + points.data[p].id, container).hasClass('background')){
                 $('#' + points.data[p].id, container).css('font-size', sz_n); 
             }
             //store element width / 2 so we can centre the text around the point later.
             points.data[p].cwidth = $('#' + points.data[p].id, container).width()/2;
         }
         // bin original html
         $('ul', container).remove();

         //set up initial view
         depth = -(zoom * (eyez - opts.max_zoom) / 100) + eyez;
         moved(cmx, cmy);
         draw();

         //show all the points
         $('.point',container).css('display','inherit');


         //call draw every so often
         drawing_interval = setInterval(draw, opts.drawing_interval);

         //events to change position of items
         container.mousemove(function(evt){
                                 moved(evt.clientX, evt.clientY);
                             });
         container.mousewheel(function(evt, delta){
                                  zoomed(delta);
                                  evt.preventDefault();
                                  return false;
                              });
         
     };
          
 })(jQuery);

