/*
 * @author			Daniyal Hamid
 * @created			12-October-2010
 * @revised			20-November-2010
 * @description		"Generic Popups" script lets you create different kinds of Popups; 
 * 					such as Tooltips, Message Boxes, Alerts, Prompts tc. with support for 
 * 					complex structures. The functionality of the plug-in is similar to 
 * 					that of jQuery, therefore, the learning curve is relatively easier.
 * 
 * @license			This JavaScript file is a commercial file, available for purchase at 
 *					http://codecanyon.net/user/daniyal/portfolio. Any illegal copying, 
 *					distribution, packaging or re-production of this script for commercial or 
 *					personal use is strictly prohibited and will be considered theft.
 *
 * @copyright		The author is the first owner of copyright and reserves all rights to
 *					all written work contained in this file. Distribution, re-production
 *					or commercial use of the written work in this file, without the author's 
 * 					signed permission, prior consent or a valid license, is strictly prohibited.
 *					The author is protected by the "Copyright, Designs and Patents Act 1988" of 
 *					the United Kingdom. Any infringement of the copyright, in or outside of the
 *					United Kingdom, may result in a lawsuit.
 */

(function($) {
	
	// resize from within the script
	var autoResize = false;
	
	var total = 0;
	
	// visible popups
	var activePopups = [];
	
	// pub methods
	var pub = {
		
		/*
		 * Initializes the popup.
		 * 
		 * @param	options 	(object, optional) Popup options.
		 */
		init: function(options) {
			return this.each(function() {
				var popup = $(this), popupData = popup.data('options');
				
				// set defaults
				if(null == options) {
					options = {};
				}
				
				// validate
				if(null != options.htmlBlock && typeof options.htmlBlock !== 'string') {
					throw new Error('Invalid CSS selector specified for htmlBlock!');
				}
				
				total += 1;
				
				// properties
				options = $.extend({}, options, {uid: "popup_" + total, position: null, visible: false, lockTo: null});
		
				// set popup display options on first run
				popup.css({
					opacity: 0,
					display: 'none'
				});
				
				
				// if 'popupData' hasn't been set
				// store popup data inside popup with the namespace 'options'
				if(!popupData) {
					popup.data('options', $.extend({htmlBlock: null}, options));
				}
				
				// bind custom events
				popup.bind({
					'click.popup': function() {
						var popup = $(this), popupData = $(this).data('options');
						$(this).trigger('regionClick.popup', [popupData.lockTo, popupData]);
					},
					'mouseenter.popup': function() {
						var popup = $(this), popupData = $(this).data('options');
						$(this).trigger('regionOver.popup', [popupData.lockTo, popupData]);
					},
					'mouseleave.popup': function() {
						var popup = $(this), popupData = $(this).data('options');
						$(this).trigger('regionOut.popup', [popupData.lockTo, popupData]);
					}
				});
				
				if(autoResize) {
					$(window).bind('resize.popup', pub.reposition);
				}
			});
		},
		
		 
		/*
		 * Destroys all the data associations and bindings associated to a popup element.
		 * 
		 * @param	incHandlers (object) Array of handlers to unbind.
		 */
		destroy: function(incHandlers) {
			return this.each(function(){
				var popup = $(this), popupData = popup.data('options');
				
				// if popup is currently visible
				if(popupData.visible) {
					// remove it from active popups
					$.each(activePopups, function(i) {
						var popupActive = $(this);
						
						// if popup is active
						if(popupActive[0] === popup[0]) {
							// remove
							activePopups.splice(i, 1);
							// break loop
							return false;
						}
					});
				}
				
				// remove popup properties
				popup.removeData('options');
				
				// unbind popup events
				popup.unbind('.popup');
				
				// unbind handler events
				if(null != incHandlers) {
					$.each(incHandlers, function(i, handler) {
						handler.unbind('.popup');
					});
				}
			});
		},
		
		/*
		 * Assigns an event handler which triggers a (show, hide or toggle) action on a popup.
		 * 
		 * @param	options 	(object) Assign options.
		 */
		assign: function(handler, event, action, options) {
			return this.each(function() {
				var popup = $(this), popupData = popup.data('options');
				
				// validate
				if(null == handler || typeof handler !== 'object') {
					throw new Error('"' + handler + '" is not a valid event handler object! An event handler object must either be an HTML element, window or document!');
				}
				if(!/\b(?:on)?(?:blur|change|click|dblclick|error|focus|focusin|focusout|keydown|keypress|keyup|load|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|mousewheel|resize|scroll|select|submit|unload)\b/i.test(event)) {
					throw new Error('The specified event "' + event + '" cannot be recognized!');
				}
				if(!/toggle|show|hide/i.test(action)) {
					throw new Error('You must specify action as "show", "hide" or "toggle" only!');
				}
			
				// event handler
				handler.bind(String(event + '.popup'), {popup: popup}, function(e) {
					var popup = e.data.popup;
					
					if(handler !== window) {
						e.preventDefault();
					}
					
					if(null == options) {
						options = {};
					}
					
					// vars
					var perform = action;
					var position = (null == options.position) ? {} : options.position;
					var condition = (null == options.condition || !$.isFunction(options.condition)) ? null : options.condition;
					var showOptions = (null == options.showOptions) ? {handler: handler} : $.extend({}, options.showOptions, {handler: handler});
					var hideOptions = (null == options.hideOptions) ? {} : options.hideOptions;
					
					// if toggle; determine whether to 'show' or 'hide'
					if(perform == 'toggle') {
						var visible = (null != popupData.lockTo && popupData.lockTo[0] === handler[0]) ? popupData.visible : false;
						
						perform = 'hide';
						
						if(!visible) {
							perform = 'show';
						}
					}
					
					// show
					if(perform == 'show') {						
						// if there's a condition...
						if($.isFunction(condition)) {
							// if condition is true;
							if(condition.apply(this, [popup, perform])) {
								pub.position.apply(popup, [position]);
								pub.show.apply(popup, [showOptions]);
							}
						}
						else {
							pub.position.apply(popup, [position]);
							pub.show.apply(popup, [showOptions]);
						}
						
					}
					// hide
					else {
						// if there's a condition...
						if($.isFunction(condition)) {
							// if condition is false;
							if(!condition.apply(this, [popup, perform])) {
								pub.hide.apply(popup, [hideOptions]);
							}
						}
						else {
							pub.hide.apply(popup, [hideOptions]);
						}
					}
					
					// bind custom event to handler
					handler.trigger('trigger.popup', [handler, perform, popupData]);
				});
				
			});
		},
		
		
		/*
		 * Shows the popup.
		 * 
		 * @param	options 	(object, optional) Show options.
		 */
		show: function(options) {
			var popup = $(this), popupData = popup.data('options');
			var handlerLock = popupData.lockTo;
			
			// set defaults
			if(null == options) {
				options = {};
			}
			
			var opacity = (null == options.opacity) ? 1 : options.opacity;
			var fade = (null == options.fade) ? true : options.fade;
			var animation = (null == options.animation) ? {} : options.animation;
			var duration = (null == options.duration) ? ((fade) ? 400 : 5) : options.duration;
			var handler = (null == options.handler) ? null : options.handler;
			
			// validate
			if(typeof opacity !== 'number' || opacity < 0 || opacity > 1) {
				throw new Error('Expecting decimal value for "opacity" between 0 and 1; encountered "' + opacity + '" which is of type: ' + typeof(opacity));
			}
			if(typeof fade !== 'boolean') {
				throw new Error('Expecting boolean value for "fade"; encountered ' + typeof(fade));
			}
			if(typeof animation !== 'object') {
				throw new Error('Animation settings must be in the form of an object literal!');
			}
			if(typeof duration !== 'number' || duration < 0) {
				throw new Error('Expecting a positive numeric value for "duration"; encountered value "' + duration + '" which is of type: ' + typeof(duration));
			}
			if(handler != null && typeof handler !== 'object') {
				throw new Error('"' + handler + '" is not a valid event handler object! An event handler object must either be an HTML element, window or document!');
			}
			
			// cancel currently running/pending transitions
			popup.stop(true, false);
			
			if(null == popupData.lockTo) {
				popupData.lockTo = handler;
			}
			else {
				if(null != handler) {
					// if handlers are same
					if(handler[0] === handlerLock[0]) {
						// ...and display properties to set are same too, then no point in setting again...
						if(popup.css('opacity') == opacity && $.isEmptyObject(animation)) {
							return;
						}
					}
					else {
						popupData.lockTo = handler;
						// if is different then reset opacity of current before showing it
						popup.css('opacity', 0);
					}
					
				}
			}
			
			
			// trigger event: beforeHide
			popup.trigger('beforeShow.popup', [handlerLock, popupData, ((popup.css('opacity') > 0) ? true : false)]);
			popup.css('display', 'block');
			
			// show
			popup.animate(
				$.extend(animation, {'opacity': opacity}), 
				duration, 
				((fade) ? 'linear' : null), 
				function() {
					popup.trigger('afterShow.popup', [handlerLock, popupData, true]);
				}
			);
			
			popupData.visible = true;
			
			// add to active popups
			activePopups.push(popup);
		},
		
		/*
		 * Hides the popup.
		 * 
		 * @param	options 	(object, optional) Show options.
		 */
		hide: function(options) {
			var popup = $(this), popupData = popup.data('options');
			var handlerLock = popupData.lockTo;
			
			// set defaults
			if(null == options) {
				options = {};
			}
			
			var fade = (null == options.fade) ? true : options.fade;
			var animation = (null == options.animation) ? {} : options.animation;
			var duration = (null == options.duration) ? ((fade) ? 400 : 5) : options.duration;
			
			// validate
			if(typeof fade !== 'boolean') {
				throw new Error('Expecting boolean value for "fade"; encountered ' + typeof(fade));
			}
			if(typeof animation !== 'object') {
				throw new Error('Animation settings must be in the form of an object literal!');
			}
			if(typeof duration !== 'number' || duration < 0) {
				throw new Error('Expecting a positive numeric value for "duration"; encountered value "' + duration + '" which is of type: ' + typeof(duration));
			}
			
			// cancel currently running/pending transitions
			popup.stop(true, false);
			
			// trigger event: beforeHide
			popup.trigger('beforeHide.popup', [handlerLock, popupData, ((popup.css('opacity') > 0) ? true : false)]);
			
			// hide
			popup.animate(
				$.extend(animation, {'opacity': 0}), 
				duration, 
				((fade) ? 'linear' : null), 
				function() {
					popup.trigger('afterHide.popup', [handlerLock, popupData, false]);
					popup.css('display', 'none');
				}
			);
			
			popupData.visible = false;
			
			// remove from active popups
			$.each(activePopups, function(i) {
				var popupActive = $(this);
				
				// if popup is active
				if(popupActive[0] === popup[0]) {
					// remove
					activePopups.splice(i, 1);
					// break loop
					return false;
				}
			});
		},
		
		/*
		 * Positions the Popup.
		 *
		 * @param	options		(object, optional) Positioning options.
		 */
		position: function(options) {
			return this.each(function() {
				var popup = $(this), popupData = popup.data('options');
				
				// if no options specified then set options to default!
				if(null == options) {
					options = {};
				}
				
				// set defaults
				var relativeTo = (null == options.relativeTo || (/^(?:body|html)$/i).test(options.relativeTo.nodeName) ? window : options.relativeTo);
				
				var position = (null == options.position) ? 'absolute' : options.position;
				
				var x = (null == options.x) ? 'left' : options.x;
				var y = (null == options.y) ? 'top' : options.y;
				var z = (null == options.z) ? 'auto' : options.z;
				
				var offsetX = (null == options.offsetX) ? 0 : options.offsetX;
				var offsetY = (null == options.offsetY) ? 0 : options.offsetY;
				
				var inBoundX = (null == options.inBoundX) ? true : options.inBoundX;
				var inBoundY = (null == options.inBoundY) ? true : options.inBoundY;
				
				var injectIn = (null == options.injectIn) ? null : ((options.injectIn === window || (/^(?:body|html)$/i).test(options.injectIn.nodeName)) ? document.body : options.injectIn);
				
				// validate				
				if(null == relativeTo || typeof relativeTo !== 'object') {
					throw new Error('The object to position your popup relative to must either be an HTML element, window or document!');
				}
				if(!/\b(?:absolute|fixed|relative|static|inherit)\b/.test(position)) {
					throw new Error('"' + position + '" is not a valid CSS position property value!');
				}
				if(z != 'auto' && z != '' && typeof z !== 'number') {
					throw new Error('"' + z + '" is not a valid CSS z-index property value!');
				}
				if(typeof offsetX !== 'number') {
					throw new Error('Expecting numeric value for "offsetX"; encountered ' + typeof(offsetX));
				}
				if(typeof offsetY !== 'number') {
					throw new Error('Expecting numeric value for "offsetY"; encountered ' + typeof(offsetY));
				}
				if(typeof inBoundX !== 'boolean') {
					throw new Error('Expecting boolean value for "inBoundX"; encountered ' + typeof(inBoundX));
				}
				if(typeof inBoundY !== 'boolean') {
					throw new Error('Expecting boolean value for "inBoundY"; encountered ' + typeof(inBoundY));
				}				
				if(typeof injectIn !== 'object') {
					throw new Error('The block you want to inject the popup into must be an HTML element!');
				}
				
				// IE-7 z-index FIX
				if(($.browser.msie && $.browser.version <= 7 && z == 'auto')) {
					z = '';
				}
				
				// reset coordinates
				popup.css({
					'width': '',
					'height': '',
					'left': '',
					'top': ''
				});
				
				// backup position for repositioning
				popupData.position = {
					relativeTo: relativeTo,
					position: position,
					x: x,
					y: y,
					z: z,
					offsetX: offsetX,
					offsetY: offsetY,
					inBoundX: inBoundX,
					inBoundY: inBoundY,
					injectIn: injectIn
				};
				
				if(popup.css('position') != position) {
					popup.css('position', position);
				}
				
				
				var relSize = {
					width: $(relativeTo).width(),
					outerWidth: (relativeTo == window) ? $(window).width() : $(relativeTo).outerWidth(),
					
					height: $(relativeTo).height(),
					outerHeight: (relativeTo == window) ? $(window).height() : $(relativeTo).outerHeight(),
					
					left: (relativeTo == window) ? 0 : $(relativeTo).position().left,
					top: (relativeTo == window) ? 0 : $(relativeTo).position().top
				};
				
				var popupSize = {
					width: $(popup).width(),
					outerWidth: $(popup).outerWidth(),
					
					height: $(popup).height(),
					outerHeight: $(popup).outerHeight(),
					
					left: $(popup).position().left,
					top: $(popup).position().top
				};
				
				var leftValue = 0, topValue = 0;
				var widthValue = popupSize.outerWidth, heightValue = popupSize.outerHeight;
				
				var documentBound = {
					width: (($.browser.msie)
						 ? ((position == 'absolute') ? (document.body || document.documentElement).scrollWidth : (document.body || document.documentElement).clientWidth)
						 : ((position == 'absolute') ? $(document).width() : $(window).width())
					),
					height: (($.browser.msie)
						 ? ((position == 'absolute') ? (document.body || document.documentElement).scrollHeight : (document.html || document.documentElement).clientHeight)
						 : ((position == 'absolute') ? $(document).height() : $(window).height())
					)
				};
				
				// inject
				if(null != injectIn && $(injectIn) != popup.parent()) {
					popup.appendTo($(injectIn));
				}
				
				// x-plane position (from left)
				switch(x) {
					case 'left':
						leftValue = relSize.left-(widthValue+(popupSize.outerWidth-popupSize.width));
					break;
					case 'leftEdge':
						leftValue = relSize.left;
					break;
					case 'right':
						leftValue = (relativeTo == window) ? documentBound.width : (relSize.left+relSize.outerWidth);
					break;
					case 'rightEdge':
						leftValue = ((relativeTo == window) ? documentBound.width : (relSize.left+relSize.outerWidth))-(widthValue+(popupSize.outerWidth-popupSize.width));
					break;
					case 'center':
						leftValue = relSize.left-((widthValue/2)-((((relativeTo == window) ? documentBound.width : relSize.outerWidth)-(popupSize.outerWidth-popupSize.width))/2));
					break;
					case 'overlay':						
						leftValue = relSize.left;
						widthValue = ((relativeTo == window) ? documentBound.width : relSize.outerWidth)-(popupSize.outerWidth-popupSize.width);
					break;
					default:
						throw new Error('"' + x + '" is not a valid horizontal/x position value!');
					break;
				}
				
				// add x-plane offset
				if(null != leftValue) {
					leftValue += offsetX;
				}
				
				// prevent x-plane clipping
				if(inBoundX) {
					// leftValue goes beyond left edge
					if(leftValue < 0) {
						leftValue = 0;
					}
					// leftValue goes beyond right edge
					else if((leftValue+widthValue) > documentBound.width) {
						leftValue = documentBound.width-(widthValue+(popupSize.outerWidth-popupSize.width));
					}
				}
				
				
				// y-plane position (from top)
				switch(y) {
					case 'top':
						topValue = relSize.top-(heightValue+(popupSize.outerHeight-popupSize.height));
					break;
					case 'topEdge':
						topValue = relSize.top;
					break;
					case 'bottom':
						topValue = (relativeTo == window) ? documentBound.height : (relSize.top+relSize.outerHeight);
					break;
					case 'bottomEdge':				
						topValue = ((relativeTo == window) ? documentBound.height : (relSize.top+relSize.outerHeight))-(heightValue+(popupSize.outerHeight-popupSize.height));
					break;
					case 'center':
						topValue = relSize.top-((heightValue/2)-((((relativeTo == window) ? documentBound.height : relSize.outerHeight)-(popupSize.outerHeight-popupSize.height))/2));
					break;
					case 'overlay':
						topValue = relSize.top;
						heightValue = ((relativeTo == window) ? documentBound.height : relSize.outerHeight)-(popupSize.outerHeight-popupSize.height);
					break;
					default:
						throw new Error('"' + y + '" is not a valid vertical/y position value!');
					break;
				}
				
				// add y-plane offset
				if(null != topValue) {
					topValue += offsetY;
				}
				
				// prevent y-plane clipping
				if(inBoundY) {
					// topValue goes beyond top edge
					if(topValue < 0) {
						topValue = 0;
					}
					// topValue goes beyond bottom edge
					else if((topValue+heightValue) > documentBound.height) {
						topValue = documentBound.height-(heightValue+(popupSize.outerHeight-popupSize.height));
					}
				}
				
				// IE Fix: in IE Ready event seems to fire early at times rendering width and height values < 0
				// which results in a logical error!
				if(widthValue < 0) {
					widthValue = 'auto';
				}
				if(heightValue < 0) {
					heightValue = 'auto';
				}
				
				// apply				
				popup.css({
					width: widthValue,
					height: heightValue,
					left: leftValue,
					top: topValue,
					zIndex: z
				});
				
			});
		},
		
		/*
		 * Repositions all visible popups.
		 */
		reposition: function() {
			// cycle through all visible handlers and use their position to update position
			$.each(activePopups, function(i) {
				var popup = $(this), popupData = popup.data('options');
				
				// update position
				pub.position.apply(popup, [popupData.position]);
			});
		},
	
		/*
		 * Sets or returns the content of the popup based on the type of request.
		 *
		 * @param	html		(object, optional) Positioning options.
		 */
		html: function(html) {
			var popup = $(this), popupData = popup.data('options');
			
			var selector = popup.find(popupData.htmlBlock).eq(0);
			
			// validate
			if(selector.length == 0) {
				throw new Error('Cannot set HTML for Popup (uid: "' + this.uid + '") because no popup HTML block/tag was specified or the HTML block could not be found inside the popup structure.');
			}
			
			if(null == html) {
				return selector.html();
			}
			else {
				selector.html(html);
			}
		}
	
	};
	
	
	/*
	 * Creates a new popup from the specified popup selector.
	 *
	 * @param	method		(string) Name of the method to call on the selected popup element.
	 * @param	options		(mixed, optional) Argument(s) for the specified method.
	 */
	$.fn.popup = function(method) {
		// Method calling logic
		if(pub[method]) {
			return pub[method].apply(this, Array.prototype.slice.call(arguments, 1));
		}
		else if(typeof method === 'object' || !method) {
			return pub.init.apply(this, arguments);
		}
		else {
			throw new Error('Method ' +  method + ' does not exist on jQuery.popup');
		}
	};

		
})(jQuery);
