/** 
  jQuery.bubble
 
  In case you edit/look this as an example, please remeber
  that "ownskit" is my library name. So please don't use 
  that library name in your own projects.
  
  @license FreeBSD
  @author Jari Pennanen 2009, 2010.
  @version 1.1 dev

**/

(function($) {	
	$.widget("ownskit.bubble", {
		options: {
			header : '',
			content : '',
			width: '',
			height: '',
			styleClass : '',
			animationSpeed : 'fast',
			animationSpeedReposition : 0,
			sizingSpace : 130,
			defaultState : 'hide',
			closeOnOutsideClick : true,
			followOnScroll : true,
			followOnResize : true,
			
			// TODO: Group id that allowes to share bubble DOM
			
			offset : 0,
			
			offsetTL :   { left: 0, top: 0 },
			offsetTC : { left: 0, top: 0 },
			offsetTR :  { left: 0, top: 0 }, 
			
			offsetBL :   { left: 0, top: 0 },
			offsetBC : { left: 0, top: 0 },
			offsetBR :  { left: 0, top: 0 },
			
			openerPosition : false, // Function,Object,False,'tr','tl','tc','br','bc','bl'
			openerBindPin : 'click',
			openerBindShow : 'mouseenter',
			openerBindHide : 'mouseleave',
			pinReasons : 'openerbindhide', // hiding reason's that are ignored when pinned
			onPin : function () {},
			onPinRemoved : function () {},
			onShow : function (reason) {}, // reason: pin, openerbindshow
			onHide : function (reason) {}, // reason: clickoutside, escape, toggle, openerbindhide, unpin
			onContentLoaded : false
		},
		_init : function() {
			// constructor
			var o = this.options, self = this;
			
			if (!$.ownskit.bubble.id)
				$.ownskit.bubble.id = 0;
				
			if (!o.content && !o.header && !o.onContentLoaded && $(this.element).attr('title')) {
				o.header = $(this.element).attr('title');
				$(this.element).attr('title', '');
			}
			
			this.id = "ownskit-bubble"+$.ownskit.bubble.id++;
			this._pinned = false;
			
			this._createOpener();
			this._createBubble();
			
			this._globalMouseDownHandler = function (e) {
				
				// If hits the bubble
				if (self.bubble.get(0) == e.target || self.bubble.has(e.target).get(0))
				{
					self.pin(0);
					return true;
					
				// If hits opener
				} else if (self.opener.get(0) == e.target || self.opener.has(e.target).get(0)) {
					return true;
					
				// If hit outside the bubble, then hide.
				} else {
					self.hide(o.animationSpeed, 'clickoutside');
					return true;
				}
			};
			
			this._globalKeyDownHandler = function (e) {
				switch (e.which) {
					case 27: /* ESC button */
						self.hide(undefined, 'escape');
						
						return false;
						break;
				}
				return true;
			};

			
			if (o.followOnScroll) {
				var scrollWait = 0;
				$(window).scroll(function () {
					if (!scrollWait)
						if (self.bubble.hasClass('ownskit-bubble-open'))
							scrollWait = setTimeout(function () {
								self.repositionBubble(0);
								scrollWait = 0;
							}, 200);
				});
			}
			
			if (o.followOnResize) {
				var resizeWait = 0;
				$(window).resize(function () {
					if (!resizeWait)				
						if (self.bubble.hasClass('ownskit-bubble-open'))
							resizeWait = setTimeout(function () {
								self.repositionBubble(0);
								resizeWait = 0;
							}, 200);
				});
			}
			
			return this;
		},
		
		_createOpener : function() {
			var o = this.options, self = this;
			
			this.opener = this.element.addClass('ownskit-bubble-opener');
			
			this._openerBindShowHandler =  function () {
				self.show(o.animationSpeed, 'openerbindshow');
				return false;
			}
			
			this._openerBindHideHandler = function () {
				self.hide(o.animationSpeed, 'openerbindhide');
				return false;
			}
			
			this._openerBindPinHandler = function () {
				self.pinToggle();
				return false;
			}
			
			if (o.openerBindShow)
				this.opener.bind(o.openerBindShow, this._openerBindShowHandler);
			
			if (o.openerBindHide)
				this.opener.bind(o.openerBindHide, this._openerBindHideHandler);
			
			if (o.openerBindPin)
				this.opener.bind(o.openerBindPin, this._openerBindPinHandler);
			
			return this;
		},
		
		_createBubble : function() {
			var o = this.options, self = this;
			var header = this.options.header;
			var content = this.options.content;

			this.bubble = $('' +
				'<div id="'+self.id+'" class="ownskit-bubble ownskit-shadow-one">' + 
				'	<div class="ownskit-bubble-tip"></div>' + 
				'	<div class="ownskit-corner-a"></div>' + 
				'	<div class="ownskit-corner-b"></div>' + 
				'	<div class="ownskit-shadow-two">' + 
				'		<div class="ownskit-shadow-three">' + 
				'			<div class="ownskit-shadow-four">' + 
				'			' + 
				'				<div class="ownskit-container">' + 
				'					<a class="ownskit-close" href="#">X</a> ' + 
				'					<h2 class="ownskit-header"></h2> ' + // TODO: Make h2 element customizable
				'					<div class="ownskit-content"></div>' + 
				'				</div>' + 
				'				' + 
				'			</div>' + 
				'		</div>' + 
				'	</div>' + 
				'</div>' +
			'').css({position: 'absolute'});
			
			// Bubble tip
			this.bubbleTip = this.bubble.find('.ownskit-bubble-tip').eq(0);
			this.bubbleTip.click(function(event) {
				self.opener.focus();
				event.stopPropagation();
				return false;
			});
			
			// Calculate the position and add to document
			this.bubble = this.bubble.prependTo(document.body);
			
			// Set dimensions
			this.bubbleContainer = this.bubble.find('.ownskit-container').eq(0);
			this.width(o.width);
			this.height(o.height);
			
			// header
			this.bubbleHeader = this.bubble.find('.ownskit-header').eq(0);
			this.bubbleHeader.append(header);
			if (!header)
				this.bubbleHeader.hide(0);
			
			// content
			this.bubbleContent = this.bubble.find('.ownskit-content').eq(0);
			this.bubbleContent.append(content);
			if (o.onContentLoaded !== false)
				this.bubbleContent.append(o.onContentLoaded.call(this.opener));
			
			// User gave custom styleClass
			this.bubble.addClass(o.styleClass);
			
			// Close button event
			this.bubbleClose = this.bubble.find('.ownskit-close');
			this.bubbleClose.click(function (e) {
				return self._doClickClose(e, this);
			});
			
			// Default state
			if (o.defaultState == 'hide')
				this.hide(0, 'default');
			else if (o.defaultState == 'show')
				this.show(0);
			else
				alert('Bubble has unknown default state: ' + o.defaultState);
			
			return this;
		},
		
		_doClickClose : function (e) {
			this.hide(this.options.animationSpeed, 'close');
			return false;
		},
		
		_openerPosition : function (openerPosition) {
			var o = this.options, self = this;
			if (o.openerPosition !== false)
				openerPosition = o.openerPosition;
				
			switch (openerPosition) {
				// Top Right
				case 'tr':
					return { left: (this.opener.offset().left + this.opener.width()), 
							 top: (this.opener.offset().top) };
				case 'tc':
					return { left: (this.opener.offset().left + Math.round(this.opener.width()/2)), 
							 top: (this.opener.offset().top) };
				case 'tl':
					return { left: (this.opener.offset().left), 
							 top: (this.opener.offset().top) };
				// Centroid
				case 'c':
					return { left: (this.opener.offset().left + this.opener.width()/2), 
							 top: (this.opener.offset().top + this.opener.height()/2) };
				// Bottom Right
				case 'br':
					return { left: (this.opener.offset().left + this.opener.width()), 
							 top: (this.opener.offset().top + this.opener.height()) };
				case 'bc':
					return { left: (this.opener.offset().left + Math.round(this.opener.width()/2)), 
							 top: (this.opener.offset().top + this.opener.height()) };
				case 'bl':
					return { left: (this.opener.offset().left), 
							 top: (this.opener.offset().top + this.opener.height()) };
				default:
					if (typeof openerPosition == 'function')
						return openerPosition(this);
					if (openerPosition.top && openerPosition.left)
						return { top : openerPosition.top, left: openerPosition.left };
					alert('Bubble has unknown openerPosition: '+ openerPosition);
			}
		},
		
		_openerRelativeToDoc : function () {
			// Determines the openers position relative to document
			var bodyWidth = $(document).width();//document.documentElement.clientWidth;
			var bodyHeight = $(document).height();//document.documentElement.clientHeight;
			var scrollX = 0;//$(window).scrollLeft();//document.documentElement.scrollLeft || document.body.scrollLeft;
			var scrollY = 0;//$(window).scrollTop();//document.documentElement.scrollTop || document.body.scrollTop;
			
			function inBoundary(value, boundaries) {
				// Returns index of boundary where value resides,
				// if value is before boundaries, it is 0, 
				// if value is afterwards, it is length of boundaries
				/*
					Examples:
					console.log("-50", inBoundary(-50, [0,100,200,300]));	// 0 (-50 is between -infinity, 0)
					console.log("50",  inBoundary( 50, [0,100,200,300])); 	// 1 (50 is between 0,100)
					console.log("150", inBoundary(150, [0,100,200,300])); 	// 2
					console.log("250", inBoundary(250, [0,100,200,300]));   // 3
					console.log("350", inBoundary(350, [0,100,200,300]));   // 4 (350 is between 300, infinity)
				*/
			
				var preBoundary = boundaries[0];
				var foundInBoundary = 0;
				$.each(boundaries, function (i, boundary) {
					if (value < preBoundary)
						return false; // Break (notice the function above)
					
					foundInBoundary = i;
					preBoundary = boundary;
				});
				
				if (value >= preBoundary)
					return boundaries.length;
				
				return foundInBoundary;
			}
			
			var o = this._openerPosition('c');
			
			// We will split the screen in vertical axis to 3 pieces
			var vB = inBoundary(o.left, [scrollX, scrollX+Math.round(bodyWidth/3), scrollX+Math.round(bodyWidth/3)*2, scrollX+bodyWidth]);
			
			// and in horizontal axis to 2 pieces
			var hB = inBoundary(o.top, [scrollY, scrollY+Math.round(bodyHeight/2), scrollY+bodyHeight]);
			
			return [vB,hB];
		},
		
		content : function(newContent) {
			this.bubbleContent.empty();
			this.bubbleContent.append(newContent);
			return this;
		},
		
		// Getter setter width
		width : function(newValue) {
			if (typeof newValue == "undefined")
				return this.bubbleContainer.width();
			return this.bubbleContainer.width(newValue);
		},
		
		// Getter setter height
		height : function(newValue) {
			if (typeof newValue == "undefined")
				return this.bubbleContainer.height();
			return this.bubbleContainer.height(newValue);
		},
		
		resize : function (width, height, complete) {
			var o = this.options;
			var sizer = {
				width: width,
				height: height
			};
			this.bubbleContainer.animate(sizer, o.animationSpeed, complete);
		},
		
		getContent : function () {
			return this.bubbleContent;
		},
		
		header : function(newHeader) {
			this.bubbleHeader.empty();
			this.bubbleHeader.show(0);
			this.bubbleHeader.append(newHeader);
			return this;
		},
		
		_positionTip : function(newPosition) {
			this.bubble.removeClass('ownskit-bl ownskit-bc ownskit-br ownskit-tl ownskit-tc ownskit-tr').addClass("ownskit-"+newPosition);
			return this.bubbleTip;
		},
		
		_positionBubble : function(offsetsCoef) {
			var o = this.options;
			var orel = this._openerRelativeToDoc();
			var vB = orel[0];
			var hB = orel[1];
			var position = this._openerPosition('c');
			var offsetTop = 0, offsetLeft = 0;
			
			this.bubbleTip.css({left: ''});
			
			// Now that we know where the opener is relative to screen, we can determine it's position.
			if        (vB <= 1 && hB <= 1) {
				position = this._openerPosition('br');
				this._positionTip('tl');
				offsetLeft = o.offsetTL.left + o.offset;
				offsetTop = o.offsetTL.top + o.offset;
			} else if (vB <= 2 && hB <= 1) {
				position = this._openerPosition('bc');
				this._positionTip('tc');//.css({left: Math.round(this.bubble.outerWidth()/2) - Math.round(this.bubbleTip.width()/2)});
				position.left -= Math.round(this.bubble.width()/2);
				offsetLeft = o.offsetTC.left;
				offsetTop = o.offsetTC.top + o.offset;
			} else if (vB <= 4 && hB <= 1) {
				position = this._openerPosition('bl');
				this._positionTip('tr');	
				position.left -= this.bubble.width();
				offsetLeft = o.offsetTR.left - o.offset;
				offsetTop = o.offsetTR.top + o.offset;
				
			} else if (vB <= 1 && hB <= 4) {
				position = this._openerPosition('tr');
				this._positionTip('bl');
				position.top -= this.bubble.height();
				offsetLeft = o.offsetBL.left + o.offset;
				offsetTop = o.offsetBL.top - o.offset;
			} else if (vB <= 2 && hB <= 4) {
				position = this._openerPosition('tc');
				this._positionTip('bc');//.css({left: Math.round(this.bubble.outerWidth()/2) - Math.round(this.bubbleTip.width()/2)});
				position.top -= this.bubble.height();
				position.left -= Math.round(this.bubble.width()/2);
				offsetLeft = o.offsetBC.left;
				offsetTop = o.offsetBC.top - o.offset;
			} else if (vB <= 4 && hB <= 4) {
				position = this._openerPosition('tl');
				this._positionTip('br');
				position.top -= this.bubble.height();
				position.left -= this.bubble.width();
				offsetLeft = o.offsetBR.left - o.offset;
				offsetTop = o.offsetBR.top - o.offset;
			}
			
			position.top += offsetTop * offsetsCoef;
			position.left += offsetLeft * offsetsCoef;
			
			return position;
		},
		
		_sizeBubble : function(pos) {
			var o = this.options;
			var orel = this._openerRelativeToDoc();
			var vB = orel[0];
			var hB = orel[1];

			var winHeight = $(window).height();
			var scrollT = $(window).scrollTop();
			var scrollB = scrollT + winHeight;
			
			// Opens downwards
			if (hB <= 1) {
				return {
					maxHeight : scrollB - pos.top - o.sizingSpace
				};
			// Opens upwards
			} else {
				return {
					maxHeight : pos.top - scrollT - o.sizingSpace
				};
			}
			
		},
		
		repositionBubble : function (speed) {
			// Defaults to speed in options
			if (typeof speed == 'undefined')
				speed = this.options.animationSpeedReposition;
			var newSize = this._sizeBubble(this._openerPosition('c'));
			this.bubbleContent.stop(true, true).animate(newSize, speed);
			
			var newPos = this._positionBubble(1);
			this.bubble.animate(newPos, speed);
		},
		
		show : function(speed, reason) {
			var self = this;
			
			// TODO: Assumption: block does not block pin!
			if ($.ownskit.bubble.block && reason != 'pin')
				return;

			this.repositionBubble(0);
				
			// If already open, no need to try
			//if (self.bubble.hasClass('ownskit-bubble-open') && !self.bubble.hasClass('ownskit-bubble-closing'))
			//	return false;

			// Defaults to speed in options
			if (typeof speed == 'undefined')
				speed = this.options.animationSpeed;
				
			// Shows the bubble gracefully
			this.bubble.stop().css( $.extend(this._positionBubble(1), {display: 'block'}) );
			this.bubble.addClass('ownskit-bubble-opening');
			this.bubble.animate($.extend({opacity : 1}, this._positionBubble(1)), speed, function() {
				self.bubble.addClass('ownskit-bubble-open');
				self.bubble.removeClass('ownskit-bubble-opening');
				self.bubble.css({ opacity : '' });
				
				// Do we allow the automatical closing on clicks outside?
				if (self.options.closeOnOutsideClick) {
					$(document.body).unbind("keydown", self._globalKeyDownHandler);
					$(document.body).unbind("mousedown", self._globalMouseDownHandler);
					$(document.body).bind("keydown", self._globalKeyDownHandler);
					$(document.body).bind("mousedown", self._globalMouseDownHandler);
				}
				
				self.options.onShow.call(self.opener);
			});
			
			return this;
		},
		
		hide : function(speed, reason) {
			var self = this;
			
			// Pinning ignores certain hiding reasons
			if (self._pinned && !$.inArray(reason, self.options.pinReasons.split(" ")))
				return;
			
			// Bubble is closed and pinning is not used anymore
			if (self._pinned)
				self.unpin(speed);
			
			// Defaults to speed in options
			if (typeof speed == 'undefined')
				speed = this.options.animationSpeed;
			
			// Hides the bubble gracefully
			this.bubble.addClass('ownskit-bubble-closing');
			this.bubble.stop().animate($.extend({opacity : 0}, this._positionBubble(1)), speed, function() {
				self.bubble.removeClass('ownskit-bubble-closing');
				self.bubble.removeClass('ownskit-bubble-open');
				self.bubble.css({display: 'none'});
				
				self.options.onHide.call(self.opener, reason);
			});
			
			$(document.body).unbind('keydown', self._globalKeyDownHandler); 
			$(document.body).unbind('mousedown', self._globalMouseDownHandler); 

			return this;
		},
			
		toggle : function(speed) {
			// Defaults to speed in options
			if (typeof speed == 'undefined')
				speed = this.options.animationSpeed;
				
			this.bubble.stop(true, true);

			if (this.bubble.hasClass('ownskit-bubble-open'))
				this.hide(speed, 'toggle');
			else
				this.show(speed);
				
			return this;
		},
		
		// Pinnable
		isPinned : function () {
			return self._pinned;
		},
		
		pinToggle : function (speed) {
			
			if (this._pinned) {
				this.unpin();
			} else {
				this.pin();
			}
			
			return this;
		},
		
		pin : function(speed) {
			var o = this.options;
			if (typeof speed == 'undefined')
				speed = o.animationSpeed;
			
			this._pinned = true;
			this.bubble.addClass('ownskit-bubble-pinned');
			$(this.opener).addClass('ownskit-bubble-pinned');
			
			this.show(speed, 'pin');
			
			o.onPin.call(this.opener);
			return this;
		},
		
		unpin : function (speed) {
			var o = this.options;
			if (typeof speed == 'undefined')
				speed = o.animationSpeed;
				
			this._pinned = false;
			this.bubble.removeClass('ownskit-bubble-pinned');
			$(this.opener).removeClass('ownskit-bubble-pinned');
			
			o.onPinRemoved.call(this.opener);
			
			this.hide(speed, 'unpin');
			return this;
		},
		
		destroy : function() {
			var o = this.options;
			// TODO: NOT TESTED!
			/*
			$(this.opener).unbind(o.openerBindHide, this._openerBindHideHandler);
			$(this.opener).unbind(o.openerBindShow, this._openerBindShowHandler);
			$(this.opener).unbind(o.openerBindPin, this._openerBindPinHandler);
			this.bubble.remove();
			
			$.widget.prototype.destroy.apply(this, arguments);
			*/
			return this;
		}
	});
	
	$.extend($.ownskit.bubble, {
		version: "@VERSION",
		block : false
	});
	
})(jQuery);
