/**
 * @author Ry Racherbaumer
 */

//
// create closure
//
(function($) {
	//
	// private variables
	//
	var $this = [], targetEl = [], cloneEl = [], boxEl = [], that = [];
	var options = [], targetData = [], preload = [], shadowEl = [];
	var targetElStyle = [];
	var targetEventAction;
	var count = 0;
	//
	// private functions
	//
	// create the box to use
	function createBox(index) {
		// see if we're cloning an element for the box
		if (options[index].cloneElement !== '') {
			// make sure the cloned element exists, if it's not a reference to the targetElement
			if (options[index].cloneElement !== 'this' && ($(options[index].cloneElement, $this[index]).length === 0 || $(options[index].cloneElement).length === 0)) {
				// if not, let the user know in the debug console
				if (console) console.log('BOXER: cloneElement does not exist!');
				return false;
			} else {
				// let's see if the user is trying to clone the targetElement
				if (options[index].cloneElement === 'this') {
					cloneEl[index] = $this[index].clone();
				} else if ($(options[index].cloneElement, $this[index]).length === 0) {
					cloneEl[index] = $(options[index].cloneElement).clone();
				} else {
					cloneEl[index] = $(options[index].cloneElement, $this[index]).clone();
				}
				// see if we need to remove any elements in the clone
				if (options[index].cloneRemoveElements.length > 0) {
					$.each(options[index].cloneRemoveElements, function(i, value){
						$(value, cloneEl[index]).remove();
					});
				}
				// set the box element to the cloned element
				boxEl[index] = cloneEl[index];
			}
		} else {
			// we're not using a clone, so create a new div for the box
			boxEl[index] = $('<div/>');
		}
	}
	// style a box
	function styleBox(index) {
		// position must be absolute
		// set the z-index so that it can display with a shadow
		boxEl[index].css({position:'absolute','z-index':1002});
		// add the specified styles
		if (options[index].boxStyle) boxEl[index].css(options[index].boxStyle);
		if (options[index].boxClass) boxEl[index].addClass(options[index].boxClass);
	}
	// remove a box
	function removeBox(index) {
		// make sure the box exists
		if (boxEl[index]) {
			// remove the box element
			boxEl[index].remove();
			// rebind targetEvent to targetElement
			if (options[index].targetEvent === 'click') {
				targetEl[index].unbind('click', targetEventAction);
				targetEl[index].bind(options[index].targetEvent, {index: index}, targetEventAction);
			} else {
				targetEl[index].unbind('mouseenter', targetEventAction);
				targetEl[index].bind('mouseenter', {index: index}, targetEventAction);
				if (options[index].targetEvent === 'hover') targetEl[index].bind('mouseleave', {index: index}, removeBoxEvent);
			}
			boxEl[index] = null;
		}
	}
	// position a box
	function positionBox(index) {
		// make sure box has default top and left values of 0 if not set or NaN
		if (isNaN(parseInt(boxEl[index].css('top')))) boxEl[index].css('top', 0);
		if (isNaN(parseInt(boxEl[index].css('left')))) boxEl[index].css('left', 0);
		// if a position element is specified and it exists, use it's positioning to calculcate the position of the box
		if (options[index].positionElement !== '' && ($(options[index].positionElement, $this[index]).length > 0 || $(options[index].positionElement).length > 0)) {
			var positionElement, positionOffset, addOffsetTop, addOffsetLeft;
			if ($(options[index].positionElement, $this[index]).length > 0) {
				positionElement = $(options[index].positionElement, $this[index]);
				positionOffset = positionElement.offset({padding:true,border:true});
			} else {
				positionElement = $(options[index].positionElement);
				positionOffset = positionElement.offset({padding:true,border:true});
			}
			if (options[index].positionAlign === 'top') {
				addOffsetTop = positionOffset.top;
				addOffsetLeft = positionOffset.left;
			} else if (options[index].positionAlign === 'bottom') {
				addOffsetTop = positionOffset.top + positionElement.height();
				addOffsetLeft = positionOffset.left;
			} else if (options[index].positionAlign === 'topright') {
				addOffsetTop = positionOffset.top;
				addOffsetLeft = positionOffset.left + positionElement.width();
			} else if (options[index].positionAlign === 'bottomright') {
				addOffsetTop = positionOffset.top + positionElement.height();
				addOffsetLeft = positionOffset.left + positionElement.width();
			}
			boxEl[index].css('top', parseInt(boxEl[index].css('top')) + addOffsetTop);
			boxEl[index].css('left', parseInt(boxEl[index].css('left')) + addOffsetLeft);			
		// otherwise, use the target element's position
		} else {
			var targetOffset = targetEl[index].offset({padding:true,border:true});
			var addOffsetTop, addOffsetLeft;
			if (options[index].positionAlign === 'top') {
				addOffsetTop = targetOffset.top;
				addOffsetLeft = targetOffset.left;
			} else if (options[index].positionAlign === 'bottom') {
				addOffsetTop = targetOffset.top + targetEl[index].height();
				addOffsetLeft = targetOffset.left;
			} else if (options[index].positionAlign === 'topright') {
				addOffsetTop = targetOffset.top;
				addOffsetLeft = targetOffset.left + targetEl[index].width();
			} else if (options[index].positionAlign === 'bottomright') {
				addOffsetTop = targetOffset.top + targetEl[index].height();
				addOffsetLeft = targetOffset.left + targetEl[index].width();
			}
			boxEl[index].css('top', parseInt(boxEl[index].css('top')) + addOffsetTop);
			boxEl[index].css('left', parseInt(boxEl[index].css('left')) + addOffsetLeft);
		}
	}
	// create a box shadow
	function createBoxShadow(index, animate, animateStyle) {
		// create shadow div
		shadowEl[index] = $('<div/>');
		// style the shadow
		shadowEl[index].css({
			position: 'absolute',
			backgroundColor: options[index].shadowColor,
			zIndex: 1001,
			width: boxEl[index].width(),
			height: boxEl[index].height(),
			top: (boxEl[index].offset({padding:true,border:true}).top + options[index].shadowOffset),
			left: (boxEl[index].offset({padding:true,border:true}).left + options[index].shadowOffset)
		});		
		if (animate === true) {
			var shadowAnimation = {};
			for (p in animateStyle) {
				if (p === 'top') shadowAnimation.top = parseInt(animateStyle.top) + options[index].shadowOffset;
				else if (p === 'left') shadowAnimation.left = parseInt(animateStyle.left) + options[index].shadowOffset;
				else if (p === 'height') shadowAnimation.height = parseInt(animateStyle.height);
				else if (p === 'width') shadowAnimation.width = parseInt(animateStyle.width);
				else if (p === 'opacity' && parseFloat(animateStyle.opacity) > 0) {
					shadowEl[index].css('opacity', 0);
					shadowAnimation.opacity = options[index].shadowOpacity;
				}
			}
			// add shadow box to the DOM
			shadowEl[index].appendTo($(document.body));
			// animate the shadow
			shadowEl[index].animate(shadowAnimation, options[index].animationDelay);
		} else {
			// set shadow opacity
			shadowEl[index].css({
				opacity: options[index].shadowOpacity				
			});
			// add shadow box to the DOM
			shadowEl[index].appendTo($(document.body));
		}
	}
	// remove a box shadow
	function removeBoxShadow(index, animate, animateStyle) {
		if (animate === true) {
			var shadowAnimation = {};
			// setup animation style
			for (p in animateStyle) {
				if (p === 'top') shadowAnimation.top = parseInt(animateStyle.top) + options[index].shadowOffset;
				else if (p === 'left') shadowAnimation.left = parseInt(animateStyle.left) + options[index].shadowOffset;
				else if (p === 'height') shadowAnimation.height = parseInt(animateStyle.height);
				else if (p === 'width') shadowAnimation.width = parseInt(animateStyle.width);
			}
			shadowAnimation.opacity = 0;
			// animate shadow
			shadowEl[index].animate(shadowAnimation, options[index].removeAnimationDelay, function(){
				// remove shadow
				shadowEl[index].remove();
			});
		} else {
			// remove shadow if exists
			if (shadowEl[index]) shadowEl[index].stop().remove();
		}
	}
	// get an element's offsets
	function getElementPosition(el, context, fallback) {
		if (context !== undefined && $(el, context).length > 0) {
			return $(el, context).offset({padding:true,border:true});
		} else if ($(el).length > 0 ){
			return $(el).offset({padding:true,border:true});
		} else {
			if (fallback !== undefined)	return fallback.offset({padding:true,border:true});
			else return {top:0,left:0};
		}
	}
	// load content into a box
	function loadBoxContent(index) {
		// if static content is set, append it to the box
		if (options[index].boxContent !== '') {
			boxEl[index].append(options[index].boxContent);
		// if the content url contains something, load the data
		} else if (options[index].boxContentDataURL !== '') {
			// make sure the data isn't already loaded
			if (!preload[index]) {
				// if the targetElement has an id, add it to the boxContentData
				if (targetEl[index].attr('id')) {
					// extend the boxContentData object so as to not overwrite it
					targetData[index] = $.extend({}, options[index].boxContentData, {
						id: targetEl[index].attr('id')
					});
				// otherwise, just copy the boxContentData
				} else {
					targetData[index] = options[index].boxContentData;
				}
				// get the content
				$.get(options[index].boxContentDataURL, targetData[index], function(data, textStatus) {
					preload[index] = data;
					boxEl[index].append(preload[index]);
				});
			} else {
				boxEl[index].append(preload[index]);
			}
		}
	}
	// event to trigger when box is removed
	function removeBoxEvent(event) {
		// get the passed index of the object
		var index = event.data.index;
		// get cursor position
		var px = event.pageX;
		var py = event.pageY;		
		// see if we've switched the box close event from the targetElement to the box
		// if the close event is attached to the box, let's not remove the box if we're still on the targetElement
		if (event.data.box) {
			// get targetElement position/dimensions
			var tx = targetEl[index].offset({padding:true,border:true}).left;
			var ty = targetEl[index].offset({padding:true,border:true}).top;
			var tw = targetEl[index].width();
			var th = targetEl[index].height();
			// see if the cursor is in the targetElement
			if ((px >= tx && px <= (tx + tw)) && (py >= ty && py <= (ty + th))) {
				// unbind it from the box
				boxEl[index].unbind('mouseleave', removeBoxEvent);
				// bind the remove box event to targetElement
				targetEl[index].bind('mouseleave', {index:index}, removeBoxEvent);
				return;
			}
		// the close event is attached to the targetElement
		} else {
			// get box position/dimensions
			var bx = boxEl[index].offset({padding:true,border:true}).left;
			var by = boxEl[index].offset({padding:true,border:true}).top;
			var bw = boxEl[index].width();
			var bh = boxEl[index].height();
			// see if the cursor is in the box
			if ((px >= bx && px <= (bx + bw)) && (py >= by && py <= (by + bh))) {
				// unbind the remove box event from the targetElement
				targetEl[index].unbind('mouseenter', targetEventAction);
				targetEl[index].unbind('mouseleave', removeBoxEvent);
				// bind the remove box event to the box
				boxEl[index].bind('mouseleave', {index:index,box:true}, removeBoxEvent);
				return;
			}
		}
		// if the targetElement got a new class, remove it
		if (options[index].targetClass) targetEl[index].removeClass(options[index].targetClass);
		// if the targetElement got a new style, remove it
		if (targetElStyle[index]) {
			// apply the original style to the targetElement
			if (options[index].targetStyleAnimate === true) {
				// if there's an animation already running, stop it
				targetEl[index].stop();
				targetEl[index].animate(targetElStyle[index], options[index].targetStyleAnimationDelay);
			} else {
				targetEl[index].css(targetElStyle[index]);
			}
		}		
		// see if there's an animation to perform on the box before removal
		if (options[index].removeAnimationStyle) {
			var removeAnimationStyle = {}, removeAnimationEndPosition;
			// get the ending position of the animation
			// check the removeAnimationEndPosition first
			if (options[index].removeAnimationEndPositionElement !== '') removeAnimationEndPosition = getElementPosition(options[index].removeAnimationEndPositionElement, $this[index], boxEl[index]);
			// now check the animationEndPosition
			else if (options[index].animationEndPositionElement !== '') removeAnimationEndPosition = getElementPosition(options[index].animationEndPositionElement, $this[index], boxEl[index]);
			// all else failed, just use the current box position
			else removeAnimationEndPosition = getElementPosition(boxEl[index]);
			// check if top and/or left positions are set on the removeAnimationStyle
			// if top is not set, NaN (for IE) or 0, just set it to the end position top
			if (!options[index].removeAnimationStyle.top || isNaN(parseInt(options[index].removeAnimationStyle.top)) || parseInt(options[index].removeAnimationStyle.top) === 0) {
				removeAnimationStyle = $.extend({}, options[index].removeAnimationStyle, {
					top: removeAnimationEndPosition.top
				});
			// top is set, add to end position
			} else {
				removeAnimationStyle = $.extend({}, options[index].removeAnimationStyle, {
					top: (parseInt(options[index].removeAnimationStyle.top) + removeAnimationEndPosition.top)
				});
			}
			// if left is not set, NaN (for IE) or 0, just set it to the end position left
			if (!options[index].removeAnimationStyle.left || isNaN(parseInt(options[index].removeAnimationStyle.left)) || parseInt(options[index].removeAnimationStyle.left) === 0) {
				removeAnimationStyle = $.extend({}, (removeAnimationStyle ? removeAnimationStyle : options[index].removeAnimationStyle), {
					left: removeAnimationEndPosition.left
				});
			// left is set, add to end position
			} else {
				removeAnimationStyle = $.extend({}, (removeAnimationStyle ? removeAnimationStyle : options[index].removeAnimationStyle), {
					left: (parseInt(options[index].removeAnimationStyle.left) + removeAnimationEndPosition.left) + 'px'
				});
			}
			// perform the remove animation, then remove the box
			boxEl[index].animate(removeAnimationStyle, options[index].removeAnimationDelay, function(){
				// remove the box element
				removeBox(index);
			});
			// remove shadow w/ animation if exists
			if (shadowEl[index]) removeBoxShadow(index, true, removeAnimationStyle);
		} else {
			// remove shadow if exists
			if (shadowEl[index]) removeBoxShadow(index, false);
			// remove the box element
			removeBox(index);
		}
	}
	function targetEventAction(event) {
		// get the passed index of the object
		var index = event.data.index;
		// check if the box already exists
		// if it does, probably waiting for remove animation to finish
		if (boxEl[index]) {
			// if the targetEvent is hover, stop animation and remove the box
			if (options[index].targetEvent === 'hover') {
				boxEl[index].stop().remove();
				boxEl[index] = null;
			// otherwise, don't do anything
			} else return;
		}
		// if the targetEvent is a click, unbind the event and prevent the default action
		if (options[index].targetEvent === 'click') {
			// unbind the event until the box element is removed
			targetEl[index].unbind('click', targetEventAction);
			// prevent default action
			targetEl[index].bind('click', function(event){event.preventDefault();})				
		}
		// create the box
		createBox(index);
		// style the box
		styleBox(index);
		// position the box
		positionBox(index);
		// let's determine how we're going to remove the box from the screen
		// if the targetevent is click, let's remove the box with a click for now
		if (options[index].targetEvent === 'click' || options[index].targetEvent === 'hoverclick') boxEl[index].bind('click', {index:index}, removeBoxEvent(index));
		// always add the new box to the body tag
		boxEl[index].appendTo($(document.body));
		// let's see if the targetElement is getting a new class
		if (options[index].targetClass) targetEl[index].addClass(options[index].targetClass);
		// let's see if the targetElement is getting a new style
		if (options[index].targetStyle) {
			// if so, let's get the original values of what we're changing
			if (!targetElStyle[index]) {
				targetElStyle[index] = {};
				for (p in options[index].targetStyle) {
					eval('targetElStyle[index].' + p + ' = targetEl[index].css("' + p + '")');
				}
			}
			// apply the style to the targetElement
			if (options[index].targetStyleAnimate === true) {
				// stop any running animations
				targetEl[index].stop();
				// reset the style if possible
				if (targetElStyle[index]) targetEl[index].css(targetElStyle[index]);
				// animate the style
				targetEl[index].animate(options[index].targetStyle, options[index].targetStyleAnimationDelay);
			} else {
				targetEl[index].css(options[index].targetStyle);
			}				
		}
		// let's process animations
		if (options[index].animate === true) {
			var animationStyle = {}, animationEndPosition;
			// see if there's an animation end position
			if (options[index].animationEndPositionElement !== '') {
				// if so, get new position
				animationEndPosition = getElementPosition(options[index].animationEndPositionElement, $this[index], targetEl[index]);
				// check if top and/or left positions are set on the animateStyle
				// if top is not set, NaN (for IE) or 0, just set it to the end position top
				if (!options[index].animationStyle.top || isNaN(parseInt(options[index].animationStyle.top)) || parseInt(options[index].animationStyle.top) === 0) {
					animationStyle = $.extend({}, options[index].animationStyle, {
						top: animationEndPosition.top + 'px'
					});
				// top is set, add to end position
				} else {
					animationStyle = $.extend({}, options[index].animationStyle, {
						top: (parseInt(options[index].animationStyle.top) + animationEndPosition.top)
					});
				}
				// if left is not set, NaN (for IE) or 0, just set it to the end position left
				if (!options[index].animationStyle.left || isNaN(parseInt(options[index].animationStyle.left)) || parseInt(options[index].animationStyle.left) === 0) {
					animationStyle = $.extend({}, (animationStyle ? animationStyle : options[index].animationStyle), {
						left: animationEndPosition.left + 'px'
					});
				// left is set, add to end position
				} else {
					animationStyle = $.extend({}, (animationStyle ? animationStyle : options[index].animationStyle), {
						left: (parseInt(options[index].animationStyle.left) + animationEndPosition.left)
					});
				}
				// animate the box
				boxEl[index].animate(animationStyle, options[index].animationDelay, function() {
					// load content into box, if set
					loadBoxContent(index);
				});
				// if shadow, animate it
				if (options[index].shadow === true) createBoxShadow(index, true, animationStyle);
			// animation end position not set, use targetElement position
			} else {
				// check if top position is set and it's valid
				if (options[index].animationStyle.top && !isNaN(parseInt(options[index].animationStyle.top))) {
					// if valid, use it
					animationStyle = $.extend({}, options[index].animationStyle, {
						top: (parseInt(options[index].animationStyle.top) + boxEl[index].offset({padding:true,border:true}).top)
					});
				} else {
					// if not valid, just use targetElement's top position
					animationStyle = $.extend({}, options[index].animationStyle, {
						top: boxEl[index].offset({padding:true,border:true}).top
					});
				}
				// check if left position is set and it's valid
				if (options[index].animationStyle.left && !isNaN(parseInt(options[index].animationStyle.left))) {
					// if valid, use it
					animationStyle = $.extend({}, (animationStyle ? animationStyle : options[index].animationStyle), {
						left: (parseInt(options[index].animationStyle.left) + boxEl[index].offset({padding:true,border:true}).left)
					});
				} else {
					// if not valid, just use targetElement's top position
					animationStyle = $.extend({}, (animationStyle ? animationStyle : options[index].animationStyle), {
						left: boxEl[index].offset({padding:true,border:true}).left
					});
				}				
				// animate the box
				boxEl[index].animate(animationStyle, options[index].animationDelay, function() {
					// load content into box, if set
					loadBoxContent(index);			
				});
				// if shadow, animate it
				if (options[index].shadow === true) createBoxShadow(index, true, animationStyle);
			}
		// no animations to process, let's see if there's data to load/display				
		} else {				
			// if shadow, render it
			if (options[index].shadow === true) createBoxShadow(index, false);
			// load content into box, if set
			loadBoxContent(index);
		}
		// if the targetEvent is a click, make sure the browser event doesn't do anything
		if (options[index].targetEvent === 'click') event.preventDefault();
	}
	//
	// plugin definition
	//
 	$.fn.boxer = function(option) {
		// build main options before element iteration
		var opts = $.extend({}, $.fn.boxer.defaults, option);
		// iterate through each matched element
		return this.each(function(index) {
			// get the element
			$this[count] = $(this);
			options[count] = opts;
			// make sure we have a target element
			if (options[count].targetElement === '') {
				// if no target element is specified, use the current element
				targetEl[count] = $(this);
			} else {
				// get the target element
				targetEl[count] = $(options[count].targetElement, $this[count]);
			}
			// make sure the target element exists
			if (targetEl[count].length === 0) {
				// if not, let the user know in the debug console
				if (console) console.log('BOXER: You must specify a valid target element!');
			} else {
				// bind the targetEvent to the targetElement
				if (options[count].targetEvent === 'click') {
					targetEl[count].bind(options[count].targetEvent, {index:count}, targetEventAction);
				} else if (options[count].targetEvent === 'hover' || options[count].targetEvent === 'hoverclick') {
					targetEl[count].bind('mouseenter', {index:count}, targetEventAction);
					if (options[count].targetEvent === 'hover') {
						targetEl[count].bind('mouseleave', {index:count}, removeBoxEvent);
					}
				}
				// if we're getting content from a data url, let's prep the data to send to the url
				if (options[count].boxContentDataURL !== '') {
					// if the targetElement has an id, add it to the boxContentData
					if (targetEl[count].attr('id')) {
						// extend the boxContentData object so as to not overwrite it
						targetData[count] = $.extend({}, options[count].boxContentData, {
							id: targetEl[count].attr('id')
						});
					// otherwise, just copy the boxContentData
					} else {
						targetData[count] = options[count].boxContentData;
					}
				}
				// let's see if there's content to preload
				if (options[count].boxContentDataURL !== '' && options[count].boxContentDataPreload === true) {
					var truecount = count;
					$.get(options[count].boxContentDataURL, targetData[count], function(data, textStatus) {
						preload[truecount] = data;
					});
				}
			}
			// add to the count of returned elements
			count++;			
		});
	};
	//
	// plugin defaults
	//
	$.fn.boxer.defaults = {
		// the element that will trigger the creation of the box
		// CSS selector
		// must be set
		targetElement: '',
		// the event of the target element that will trigger the box creation
		// currently supported: 'click', 'hover'
		// default: click
		targetEvent: 'click',
		// the element to clone that will be used for the box
		// defaults to nothing
		cloneElement: '',
		// an array of elements to remove from the cloned element
		// CSS selectors
		// defaults to nothing
		cloneRemoveElements: [],
		// the element from which to take calculations from for the box's position
		// if animate is true, the element will be used to calculate the position for the start of the animation
		// if animate is false, the element will be used to calculate where to render the box
		// CSS selector
		// defaults to targetElement
		positionElement: '',
		// where to take position from
		// default takes position from positionElement top
		// other possible values: 'bottom', 'bottomright', 'topright'
		positionAlign: 'top',
		// use animation to create the box
		// default: false
		animate: false,
		// the styling to transition to with animation
		// defaults to no change
		animationStyle: '',
		// the element from which to take calculations from for the box's end position (if animating)
		// not required if animate is false
		// if animate is true and this option is not specified,
		// the end animation position calculations will be taken from the positionElement
		// CSS selector
		// defaults to targetElement
		animationEndPositionElement: '',		
		// the time that the animation will take
		// default: 500 milliseconds
		animationDelay: 500,
		// the style of the box to create
		// this is used when animate is false
		// defaults to nothing
		boxStyle: null,
		boxClass: null,
		// content to append to the box element after it is created/animated
		// html/text to display in the box as opposed to getting content from the data URL
		boxContent: '',
		// the url from which to get the data
		boxContentDataURL: '',
		// additional parameters to pass to the data url
		boxContentData: {},
		// whether or not to preload the content for the box
		boxContentDataPreload: true,
		// the animation to perform on the box before removing it
		// defaults to nothing
		removeAnimationStyle: null,
		// the length of time the remove animation will take
		removeAnimationDelay: 500,
		// the element from which to take calculations from for the box's position
		// CSS selector
		// defaults to animationEndPositionElement, then targetElement
		removeAnimationEndPositionElement: '',
		// target style/animation stuff
		targetClass: null,
		targetStyle: null,
		targetStyleAnimate: false,
		targetStyleAnimationDelay: 500,
		// shadow stuff
		shadow: false,
		shadowColor: '#000',
		shadowOpacity: 0.4,
		shadowOffset: 5
	};
//
// end of closure
//
})(jQuery);
