/*!
 * jQuery Radmenu (Radial Menu) Plugin
 * version: 1.0.0 (14-MAY-2011)
 * @requires v1.4.2 or later
 * 
 * Author: Nirvana Tikku - ntikku@gmail.com - @ntikku
 * Documentation:
 * 		http://www.tikku.com/jquery-radmenu-plugin
 * 
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html 
 * 
 */

;(function($){
	
	// radmenu namespace
	
	var RADMENU = ".radmenu", // events are radmenu.{event} - guarantee no NS collision
	
		OPTS = "options"+RADMENU,
		
		PREVOPTS = "prevoptions"+RADMENU,
		
		RADMENU_CLASS = "ui-radmenu-parent"; 
		
	// private defaults
	var defaults = {
		
		// radial menu container properties
		
		listClass: "list",
		
		itemClass: "item",
		
		activeItemClass: "active",
		
		// active item selection properties
		
		selectEvent: null, // click, mouseenter etc
		
		onSelect: function($selected){},
		
		// initial setup properties
		
		radius: 10, // in pixels
		
		angleOffset: 0, // in degrees
		
		centerX: 0,
		
		centerY: 0,
		
		// animation properties
		
		animSpeed: 500,
		
		animEasing: "swing",
		
		// scaling properties and method
		
		initialScale: 1,
		
		scaleAnimSpeed: 0,
		
		scaleAnimEasing: "swing",
		
		scaleAnimOpts: {},
		
		// example onScaleItem: $item.css("font-size", factor+"em");
		onScaleItem: function($item, factor, coords){},
		
		// public events 
		
		afterAnimation: function($m){},
		
		onShow: function($items){$items.show();},
		
		onHide: function($items){$items.hide();},
		
		onNext: function($items){return true;},
		
		onPrev: function($items){return true;},
		
		// rotation property and fine-tuning event 
		
		rotate: false,
		
		getRotation: function(degrees, index, numItems){return degrees;}
		
	};

	// radial menu container setup defaults
	$.radmenu = {
		
		container: {
			
			html: "<div></div>",
			
			css: { "position": "relative" },
			
			clz: "radial_div",
			
			itemClz: "radial_div_item"
			
		}
		
	};
	
	/**
	 * jQuery Radmenu Plugin
	 * 	@params 
	 * 		> input, dealt with by type
	 * 	if empty - assumes initialization
	 * 	if object - assumes initialization
	 * 	if string - assumes trigger method
	 * 	if number - select a particular menu item
	 */ 
	$.fn.radmenu = function(input, param){
		
		try {
			
			var $this = $(this);
			
			var type = typeof input;
			
			if(arguments.length==0 || type=="object") 
			
				return init($this, input);
				
			else if(type=="string")
			
				return (input=="items" || input=="opts") ?
					$this.triggerHandler(input+RADMENU) :
					$this.trigger(input+RADMENU, param || null);
					
			else if(type=="number")
				
				return $this.trigger("select"+RADMENU,input);
				
		} catch (e) { 
			
			return "error : "+e; 
			
		}
		
	};
	
	/**
	 * private :: init fn
	 * @params
	 * 	$menu - the jQuery obj / array w/ menu target
	 *  opts - options object, to be merged with defaults
	 */
	function init($menu, opts){
		
		var o = $.extend({}, defaults, opts);
		
		return $menu.each(function(m){
			
			var $this = $(this);
			
			if(!$this.hasClass(RADMENU_CLASS)) {
			
				var $list = $this.find("."+o.listClass);
			
				$list.find("."+o.itemClass).hide(); // ensure its hidden
			
				// set the options within the data for the elem & bind evts
				$this.data(OPTS, updateRadius(o, o.initialScale, o.radius));
			
				for(e in MENU) 
					$this.bind(e+RADMENU, $this, MENU[e]);
				
				$this.addClass(RADMENU_CLASS);
			
			}
			
		});
		
	};

	/**
	 * selects a menu item - this method provides
	 * functionality for nested radial menus
	 * @param 
	 * 	evt - the event object
	 * triggers select event on radmenu container
	 * 	using the index of the 'target object'
	 */
	function selectMenuitem(evt){ 
		
		var $this = $(this);
		
		var $element = $(evt.target);
		
		var container = $.radmenu.container;
		
		if(!$element.hasClass(container.itemClz))
			$element = $element.closest("."+container.itemClz);
			
		var isInNested = $element.parents("."+container.itemClz).length>0;
		
		var index = $element.index();
		
		if(!isInNested)
			$this.parents("."+container.clz).radmenu(index);
			
		else 
			$this.radmenu(index);
			
		cancelBubble(evt);
		
	};
	
	/**
	 * cancel event bubbling - x-browser friendly
	 * @param
	 * 	evt - the event object
	 */
	function cancelBubble(evt){
		
		if(!$.support.opacity) 
			window.event.cancelBubble = true;
			
		else 
			evt.stopPropagation();
			
	};
	
	/**
	 * All the events bound to the radial menu instance
	 */
	var MENU = {
		opts: function(evt) { 
			
			return getMenu(evt).opts;
			
		},
		show: function(evt, fn){ // fn = user input onshow
			
			var $m = getMenu(evt);
			
			var container = $.radmenu.container;
			
			// clear any existing radial menus within the menu
			$m.menu.find("."+container.clz).remove();
			
			// grab the desired menu items to be used in building the radmenu
			var $menuitems = $m.menu.find("."+$m.opts.itemClass);
			
			// create a div that will be the radmenu & create the HTML for the items
			var $radialMenu = $(container.html)
								.addClass(container.clz)
								.css(container.css)
								.html(buildMenuHTML($menuitems, $m.opts));
				
			// assign a selection event if the user has specified something
			var $menuitems = $radialMenu.find("."+container.itemClz);
			
			if($m.opts.selectEvent!=null)
				$menuitems.bind($m.opts.selectEvent,selectMenuitem);
				
			// append the radmenu items inside the menu 
			$radialMenu.appendTo($m.menu);
			
			if(typeof(fn) == "function") 
				fn($menuitems); // allow passing in a method
				
			else
				$m.opts.onShow($menuitems); // user can do what they want
			
			cancelBubble(evt);
			
		},
		hide: function(evt){ 
			
			var $m = getMenu(evt);
			
			// remove the radmenu that was built and appended inside the menu
			var $menu = $m.menu.find("."+$.radmenu.container.clz);
			
			$m.opts.onHide($menu.find("."+$.radmenu.container.itemClz));
			
			$m.menu.remove();
			
			cancelBubble(evt);
			
		},
		select: function(evt, selectIndex){
			
			var $m = getMenu(evt);
			
			// with a specific index specified, grab the item
			var $selected = $($m.raditems().get(selectIndex));
			
			// remove the active class on the elements siblings
			$selected.siblings().removeClass($m.opts.activeItemClass);
			
			// add the active class on the selected item
			$selected.addClass($m.opts.activeItemClass);
			
			// pass the selected item to a customizable function
			$m.opts.onSelect($selected);
			
			cancelBubble(evt);
			
		},
		moveSelected: function(evt, selectedIndex){ // clockwise
			
			var $m = getMenu(evt);
		
      for (var i = selectedIndex; i < $m.raditems().length; i++) {
			  // switch the first and last items and then animate
			  switchItems($m, $m.raditems().length-1, 0);
      }
			
		},
    next: function(evt, fn){ // clockwise
			
			var $m = getMenu(evt);
			
			if( !$m.opts.onNext($m) ) return;
			
			// switch the first and last items and then animate
			switchItems($m, $m.raditems().length-1, 0);
			
		},
		prev: function(evt){ // anticlockwise
			
			var $m = getMenu(evt);
			
			if( !$m.opts.onPrev($m) ) return;
			
			// switch the last and first items and then animate
			switchItems($m, 0, $m.raditems().length-1);
			
		},
		shuffle: function(evt){
			
			var $m = getMenu(evt);
			
			var len = $m.raditems().length;
			
			// swap some random item with another random item, and add some shuffling effects
			switchItems($m, rnd(len), rnd(len));
			
		},
		destroy: function(evt){
			
			var $m = getMenu(evt);
			
			$m.menu.data(OPTS, null)
				.data(PREVOPTS, null)
				.removeClass(RADMENU_CLASS)
				.unbind(RADMENU);
			
			return $m.menu;
			
		},
		items: function(evt){
			
			return getMenu(evt).raditems();
			
		},
		scale: function(evt, factor){
			
			var $m = getMenu(evt);
			
			if(factor){
				
				var o = $m.opts;
				
				var container = $.radmenu.container;
				
				var prevOpts = $m.menu.data(PREVOPTS);
				
				if(!prevOpts) $m.menu.data(PREVOPTS, prevOpts=o);
				
				// get the radial menu items
				var $items = $m.menu.find("."+container.itemClz);
				
				var updatedRadiusOpts = updateRadius(o, factor, prevOpts.radius);
				
				$m.menu.data(OPTS, updatedRadiusOpts); // save the radius for anim purposes
				
				$items.each(function(i){ // for each item update the x,y + css
					
					var $this = $(this);
					
					var coords = getCoords(i, $items.length, updatedRadiusOpts);
					
					var animOpts = {
						
						top: coords.top,
						
						left: coords.left
						
					};
					
					if(typeof(o.scaleAnimOpts) == "object") {
						
						animOpts = $.extend({}, o.scaleAnimOpts, animOpts);
						
					}
					
					$this.animate(animOpts, o.scaleAnimSpeed, o.scaleAnimEasing);
					
					$m.opts.onScaleItem($this, factor, coords);
					
				});
				
			}
			
			return $m.menu;
		}
	};
	
	// simply multiples the radius by a factor
	function updateRadius(opts, radius, factor){
		
		return $.extend({},opts,{radius:(factor*radius)});
		
	};
	
	// random int offset 
	function rnd(i){
		
		return parseInt( Math.random() * i );
		
	};
	
	/**
	 * getMenu - this is a method that returns all
	 *	the required objects within a given method
	 *	that is subscribed to the radmenu object.
	 *	
	 * @params
	 * 	evt - the event object
	 * @return
	 * 	Object
	 * 		> menu - jQueryfied menu
	 * 		> opts - the options
	 * 		> raditems - the radial menu items
	 */
	function getMenu(evt){
		
		var $menu = evt.data;
		
		return {
			
			menu: $menu, 
			
			opts: $menu.data(OPTS),
			
			raditems: function(){
				
				// you will want to trigger raditems() if the contents get modified
				return $menu.find("."+$.radmenu.container.itemClz);
				
			}
			
		};
	};
	
	/**
	 * Switch Items -- this method is used in re-evaluating the (x,y) coords
	 * 	as a result of swapping the position of items. The intent with this
	 * 	is that the plugin will reoganize the items (in the container, 
	 *	as opposed to the original dom elements) such that the elements
	 *	positions, when selected, remains fixed around the circle.
	 *	Based on given indexes the items are swapped and then animated. The 
	 *	most typical case involves removing the first and swapping the last
	 *	and vice versa.
	 * 
	 * @params
	 * 	$m - the menu package
	 * 	remove - the index of the menuitem to replace in the swap
	 * 	add - the index of the menuitem to use in the swap (a placeholder)
	 */
	function switchItems($m, remove, add){
		
		if(remove==add) add = remove - 1; // ensure that we don't lose any items
		
		var $remove = $($m.raditems()[remove]); // grab the replacement item
		
		var toAddto = $m.raditems()[add]; // grab the placeholder 
		
		// insertion is dependent on index of items
		if(remove>add) 
			$remove.insertBefore(toAddto);
			
		else
		 	$remove.insertAfter(toAddto);
		
		animateWheel($m, (remove<add)); // posOffset = 5:neat, 10:fireworksesque, 15:subtleish
		
	};
	
	/**
	 * buildMenuHTML - returns string instead of objects
	 * 		for performance 
	 * 
	 * @params
	 * 	$menuitems - the jQueryified menu items
	 * 	opts - the radial menu's options
	 * @return
	 * 	String
	 * 		> each item is wrapped with an 
	 * 			absolute positioned div at an
	 * 			offset determined by it's location
	 * 			on a circle
	 */
	function buildMenuHTML($menuitems, opts){
		
		var ret = [];
		
		$menuitems.each(function(i){ // for each item we will want to build the HTML
			
			var $this = $(this);
			
			var coords = getCoords(i, $menuitems.length, opts); // each item has a position
			
			var rotationHTML = "transform:rotate("+coords.angle+"deg); ";
			
			ret.push("<div class='"+$.radmenu.container.itemClz+"' "); // outer container for the div
			
			// after getting the coordinates, absolute position element at (x,y)
			ret.push("style='");
			ret.push("position:absolute;display:none;");
			ret.push("left:"+coords.left+"px;");
			ret.push("top:"+coords.top+"px;");

			if(opts.rotate) {
				
				for(rot in XForm.opts)
					ret.push(XForm.opts[rot]+rotationHTML);
					
			}
			
			ret.push("'>");
		
			ret.push($this.html()); // append the HTML _within_ the user's defined 'item'
		
			ret.push("</div>");
			
		});
		
		return ret.join("");
		
	};
	
	/**
	 * Get the radians value of an angle given a 
	 * particular slice of the selection
	 * 
	 * 	@params
	 * 		iIdx - the instance index
	 * 		iNum - the number of menu items
	 */
	function getAngleAtIndex(iIdx, iNum){
		
		return 2 * Math.PI * parseFloat(iIdx/iNum); // radians

	};
	
	/**
	 * getCoords - returns coordinates for menuitems, as 
	 * 	well as an animation object with the appropriate rotation
	 *	as per config
	 * 
	 * 	@params
	 * 		iIdx - the instance index (1st, 2nd, 3rd, etc..)
	 * 		iNum - the number of menuitems to distribute
	 * 		oOpts - the options provided by the user customizations
	 * 		bClockwise - a flag for the animation object 
	 * 	@return
	 * 		Object - (x, y) coords & the angle in degrees
	 */
	function getCoords(iIdx, iNum, oOpts, bClockwise){
		
		var radius = oOpts.radius; // user specified radius
		
		var angle = getAngleAtIndex(iIdx, iNum);
		
		angle += toRadians(oOpts.angleOffset); // provide flexibility of angle
		
		//	assuming: hypotenuse (hyp) = radius
		//
		//	opposite	|\	hypotenuse
		//				| \
		//		90deg	|__\	(*theta* - angle)
		//				adjacent
		//
		//	x-axis offset: cos(theta) = adjacent / hypotenuse
		//		==> adjacent = left = cos(theta) * radius
		//	y-axis offset: sin(theta) = opposite / hypotenuse
		//		==> opposite = top = sin(theta) * radius
		
		var l = oOpts.centerX + ( Math.cos( angle ) * radius ), // "left"
			// angle is rounded to 2dp to fix a bug
			t = oOpts.centerY + ( Math.sin( parseInt(angle*100)/100 ) * radius ); // "top"
			
		var degrees = oOpts.rotate ? oOpts.getRotation( angle * 180 / Math.PI, iIdx, iNum ) : 0;

		// NOTE: why not just simply rotate to the angle? buggy
		// the element that gets shifted cycles through an unnecessary revolution 
		// i.e. >360deg -> >0 deg
		var slice = oOpts.rotate ? ( getAngleAtIndex(1, iNum) * 180 / Math.PI ) : 0;

		var rotation = ( bClockwise==true ? "-=" : "+=" ) + slice;
		
		return {
			
			left: l, 
			
			top: t, 
			
			angle: degrees, 
			
			animObj:{
				
				left: l, 
				
				top: t, 
				
				radrotate: rotation
			
			} 
			
		}; // return the x,y coords
		
	};
	
	/**
	 * simple method to convert degrees to radians
	 */
	function toRadians(degrees){
		
		return degrees * Math.PI / 180;
		
	};
	
	/**
	 * animateWheel - performs animation on menu items within 
	 *	the container elements
	 * 
	 * @params
	 * 	$m - object holding menu & options
	 * 	iPosOffset - the position offset for the initial menuitem
	 * 	bClockwise - for the animation, clockwise or counter
	 */
	function animateWheel($m, bClockwise){
		
		// get the menu from the $m menu package
		var $menuitems = $m.raditems();
		
		// get a handle on the number of items
		var len = $menuitems.length;
		
		// for each item, we're going to animate left/top attributes
		$menuitems.each(function(i){
			
			var $this = $(this);
			
			// establish the new coordinates with a customizable offset
			var coords = getCoords(i, len, $m.opts, bClockwise);
			
			// playing with this is fun - this basically just
			// performs the animation with new coordinates 
			
			$this.animate(
				coords.animObj, 
				$m.opts.animSpeed, 
				$m.opts.animEasing, 
				function(){
					if(i==(len-1) ){
						// allow the user to do something after completing an animation
						$m.opts.afterAnimation($m);
					}
				}
			);
		});
	};
	
	/**
	 * Transform Utils
	 */
	var XForm = {};
	
	// local cache of the appropriate transform to use
	XForm.attr = undefined;
	
	// Safari, Chrome, FF 3.5+, IE 9+, and Opera 11+
	XForm.opts = ["","-webkit-","-moz-","-ms-","-o-"];
	XForm.cssattrs = ["","Webkit","Moz","ms","O"];
	
	/**
	 * Get the relevant CSS attr and cache it 
	 */
	XForm.getCSSAttr = function($elm){ 
		
		if( this.attr ) 
			return this.attr;
			
		return this.attr = (function(){
			
			for(var ii=0; ii<XForm.cssattrs.length; ii++){
				
				var opt = XForm.cssattrs[ii]+"Transform";
				
				if( $elm[0].style[opt] )
					return opt;
				
			}
			
			return "transform";
			
		})();
	
	};
	
	/**
	 * Deduce which transform is applicable
	 * and extract the attr's value
	 * 	@params
	 * 	  $elm - jQuerified element which the 
	 * 		css attribute is evaluated against
	 */
	XForm.getTransformValue = function($elm){
		
		return jQuery.style( $elm[0], XForm.getCSSAttr($elm) );
	
	};
	
	/**
	 * jQuery Proxy object
	 */
	var _ = {};

	_.cur = $.fx.prototype.cur;
	
	/**
	 * jQuery Proxy Method: 
	 * 	We need to override this method in order for the "rotate([x]deg)" 
	 * 	css value to be parsed and passed through to the step fn numerically 
	 * 
	 * Credit due: Zachary Johnson www.zachstronaut.com 
	 * 		from https://github.com/zachstronaut/jquery-animate-css-rotate-scale
	 */
    $.fx.prototype.cur = function() {
	
        if ( this.prop == "radrotate" ) {
	
			var $elm = $(this.elem);
	
			var style = XForm.getTransformValue($elm) || 'none';
			
	        if (style) {
		
                var m = style.match(/rotate\(([^)]+)\)/);

                if (m && m[1])	{
	
					return parseFloat( m[1] );
					
				}

            }

            return 0;

        }

        return _.cur.apply(this, arguments);

    };	

	//
	// use a custom animation property - radrotate
	//
	$.fx.step.radrotate = function(fx) {
			
		var $elm = $(fx.elem);
		
        $elm.css(XForm.getCSSAttr($elm), "rotate("+ fx.now +"deg)");
		
    };
	
})(jQuery);

