/**
 * LBi Dialogs module
 *
 * @module    dialog
 * @version   3.10.100216
 * @requires  LBi, jQuery
 * @author    LBi Lost Boys
 */
(function($){
	
	var	ACTION_CONFIRM = "confirm";
	var	ACTION_CANCEL = "cancel";
	var	ACTION_CLOSE = "close";

	var	CSS_INVISIBLE = { visibility: 'hidden', display: 'block' };
	var	CSS_NODISPLAY = { display: 'none', visibility: 'visible' };
	var	CSS_ABSOLUTE = { position: 'absolute', left:0, top:0 };
		
	var	REG_ALIGNRIGHT = new RegExp("right");
	var	REG_VALIGNTOP = new RegExp("top");

	var limit = function(value, min, max) {
		return Math.min(Math.max(value, min), max);
	};
	
	/**
	 * The LBi.Dialogs class manages the flow of dialogs in a given interface. A default 
	 * Dialog class is provided, which may be subclassed for customized behavior. Different
	 * managers may be instanciated, but in general you'll only need one, along with one
	 * or more registered Dialog classes.
	 * 
	 * @class LBi.Dialogs
	 * @constructor
	 * @param {Object} settings Optional settings
	 * @return {LBi.Dialogs} Dialogs manager
	 */
	var Dialogs = function(settings) {
		this.applySettings(settings);
		this.types = {};
		this.dialogs = {};
		
		var relations = this.settings.relations || new LBi.LinkRelations();
		relations.subscribe(this.regRelation, this.handleClick.bind(this));
	};

	Dialogs.prototype = {
		constructor: Dialogs,

		/**
		 * Applies settings and creates a few regexes for later use
		 * @private
		 */
		applySettings:function(settings){
			this.settings = $.extend({}, Dialogs.Defaults, settings);
			this.regRelation = new RegExp("(^|\\s)" + this.settings.prefix);
			this.regAction = new RegExp(this.settings.prefix + "([^ ]+)");
		},
		
		/**
		 * Public method for retrieving settings properties.
		 *
		 * @param {String} name The name of the property
		 * @return {Object} property value
		 */
		getProperty:function(name) {
			return this.settings[name];
		},
		
		/**
		 * Main click handler
		 * @private
		 */
		handleClick:function(e) {
			var link = e.target;
			var rel = link.rel;
			var type = this.regAction.exec(rel)[1];
			var current = this.currentDialog;
			
			if(this.currentDialog) {
				this.currentDialog.close();
				this.setCurrent(null);
			}

			switch (type) {
				case ACTION_CLOSE:
					if(current) { 
						current.close(); 
					}
				break;
				case ACTION_CONFIRM:
					if(current) {
						current.confirm(true);
					}
				break;
				case ACTION_CANCEL:
					if(current) {
						current.confirm(false);
					}
				break;
				default:
					this.open(type, link);
				break;
			}

			e.preventDefault();
		},

		/**
		 * Opens a dialog of the given type, positioning itself on the given origin. Any currently
		 * visibile dialog is closed automatically. Normally this method is called automatically for 
		 * dialog related links. It may however be called manually for specific purposes.
		 *
		 * @param {String} type Dialog type, without the dialog- prefix.
		 * @param {Element} origin The element that requested the dialog.
		 */	
		open:function(type, origin) {
			var dialog = this.getDialog(type);
			this.setCurrent(dialog);
			dialog.open(origin);
			
			if(!this.closeHandler) {
				this.closeHandler = this.close.bind(this);
				$(document).bind('click', this.closeHandler);
			}
			
			return dialog;
		},
		
		/**
		 * Closes the current dialog. Called automatically, but may be called manually for 
		 * specific purposes.
		 * 
		 * @param {Event} e Optional event object as passed by any mouse related event.
		 */
		close:function(e) {
			var current = this.currentDialog;
			if(current) {
				var targeted = e? current.targetOf(e) : false;
				if(!targeted) {
					current.close();
					this.setCurrent(null);
					if(this.closeHandler) {
						$(document).unbind('click', this.closeHandler);
						this.closeHandler = null;
					}
				}
			}
		},

		/**
		 * Finds the dialog class associated with the given type, and returns an instance
		 * @private
		 */
		getDialog:function(type) {
			if(!this.dialogs[type]) {
				var Constructor = this.types[type] || this.settings.dialogClass; 
				var element = document.getElementById(this.settings.prefix + type);
				this.dialogs[type] = new Constructor(element, this, type);
			}

			return this.dialogs[type];
		},

		/**
		 * Registers a new dialog class for use in a rel attribute. The type attribute corresponds
		 * to the suffix of a dialog ID ("dialog-" is automatically added), and links that dialog 
		 * to the given dialog class. "confirm", "cancel" and "close" are reserved types, they
		 * are used by the dialog manager internally. A document may already contain the dialog's 
		 * html code. If not, it will be generated automatically.
		 *
		 * @param {String} type Dialog type, without the dialog- prefix.
		 * @param {LBi.Dialog} dialog Subclasses of LBi.Dialog.
		 * @return {LBi.Dialog} dialog Returns the dialog for easy chaining.
		 */	
		register:function(type, DialogClass) {
			this.types[type] = DialogClass;
			return DialogClass;
		},

		/**
		 * Stores a reference to the current dialog
		 * @private
		 */
		setCurrent:function(dialog) {
			this.currentDialog = dialog;
		},

		/**
		 * Toggles the overlay behind modal dialogs
		 * @private
		 */
		toggleOverlay:function(toggle) {
			if(!this.overlay) {
				var Overlay = this.settings.overlayClass;
				this.overlay = new Overlay(this);
			}

			this.overlay.toggle(toggle);
		}
	};

	LBi.namespace('Dialogs', Dialogs);

	/**
	 * The base LBi.Dialog class provides dialog functionality for the given element. if no element is 
	 * passed to it, the dialog will be created from the template property as specified for the 
	 * LBi.Dialogs instance that manages this dialog. Subclasses may customize most of the dialog's 
	 * functionality. Dialog instances are managed automatically by the Dialogs class, and should not 
	 * be created manually. Use the LBi.Dialogs.register method to add dialog subclasses.
	 * 
	 * @class LBi.Dialog
	 * @constructor
	 * @param {Node} node The dialog's root element.
	 * @param {LBi.Dialogs} manager The dialog's manager
	 * @param {String} type The dialog type, excluding the "dialog-" prefix.
	 * @return {LBi.Dialog} Dialog instance
	 */
	var Dialog = function(element, manager, type) {
		this.container = element;
		this.$container = $(element);
		this.manager = manager;
		this.type = type;

		this.orientation = manager.getProperty('orientation');
		this.offset = manager.getProperty('offset');
		this.modal = manager.getProperty('modal');
		this.template = manager.getProperty('template');
	};

	Dialog.prototype = {
		constructor: Dialog,

		/**
		 * Activate is called on a dialog immediately before a dialog is made visible, and before
		 * it is hidden. Subclasses may use this method as a hook to initiate functionality.
		 *
		 * @method activate
		 * @param {boolean} activate True when the dialog is opened, false otherwise.
		 */
		activate: function(activate) { },

		/**
		 * Confirm is called when either the confirm or cancel button of an active dialog is clicked. 
		 * The confirm button should be a link with rel="dialog-confirm", cancel with rel="dialog-cancel".
		 * Subclasses may use this method as a hook to initiate (or cancel) functionality.
		 *
		 * @method confirm
		 * @param {boolean} activate True when the dialog is opened, false otherwise.
		 */
		confirm: function(confirm) { },
		
		/**
		 * Shows the dialog, using the LBi.Animation as specified for the LBi.Dialogs instance that manages it.
		 * @method show
		 */
		show:function() {			
			this.toggle(true);
		},

		/**
		 * Hides the dialog, using the LBi.Animation as specified for the LBi.Dialogs instance that manages it.
		 * @method hide
		 */
		hide:function() { 
			this.toggle(false); 
		},

		/**
		 * Toggles the dialog's visibility
		 * @private
		 */
		toggle:function(toggle) {
			var settings = this.manager.settings;
			var animation = settings.animation;
			animation.run(this.container, toggle, {
				duration: settings.animationTime
			});
		},

		/**
		 * Creates the dialog html from the template property.
		 * @private
		 */
		create:function() {
			var prefix = this.manager.getProperty('prefix');
			this.$container = $(this.template);
			this.$container.attr('id', prefix + this.type);
			this.container = this.$container[0];
			$('body').append(this.$container);
		},

		/**
		 * Opens the dialog on the given origin. This method is automatically called via the LBi.Dialogs
		 * instance that manages this dialog, which ensures the proper class is used for it. Take care when
		 * calling this method manually.
		 * 
		 * @method open
		 * @param {Node} origin The node that triggered the dialog, most often a link.
		 */
		open:function(origin) {
			if(!this.container) {
				this.create();
			}

			this.origin = origin;
			if(this.modal) {
				this.manager.toggleOverlay(true);
			}

			this.activate(true);
			this.$container.css(CSS_INVISIBLE);
			this.$container.css(this.getPosition(origin));
			this.$container.css(CSS_NODISPLAY);
			this.show();
		},

		/**
		 * Closes the dialog.
		 * @private
		 */
		close:function() {
			if(this.modal) {
				this.manager.toggleOverlay(false);
			}

			this.activate(false);
			this.hide();
		},

		/**
		 * Redraws the dialog on the position it ought to have. Calling this method may be required
		 * when dynamic content changes alter the dialog's dimensions.
		 * 
		 * @method redraw
		 */
		redraw:function() {
			this.$container.css(
				this.getPosition(this.origin)
			);
		},

		/**
		 * Finds the optimal position for the dialog, based on available space, orientation and the 
		 * origin's position. This method is called automatically.
		 *
		 * @param {Node} origin The link that was clicked to open the dialog.
		 * @return {Object} offset Offset object with left and top px properties.
		 */
		getPosition:function(origin) {
			var doc = document.documentElement;
			var offset = this.offset;
			var $offset = $(origin).offset();
			var offsetLeft = $offset.left;
			var offsetTop = $offset.top;
			
			var dialogWidth = this.$container.width();
			var dialogHeight = this.$container.height();

			var scrollTop = window.pageYOffset || doc.scrollTop;
			var scrollLeft = window.pageXOffset || doc.scrollLeft;

			var minLeft = scrollLeft + offset;
			var minTop = scrollTop + offset;
			var maxLeft = (window.innerWidth || doc.clientWidth) + scrollLeft - dialogWidth - offset; 
			var maxTop = (window.innerHeight || doc.clientHeight) + scrollTop - dialogHeight - offset;

			var x = REG_ALIGNRIGHT.test(this.orientation)? 
				(offsetLeft + origin.offsetWidth + offset) : 
				(offsetLeft - dialogWidth - offset);

			var y = REG_VALIGNTOP.test(this.orientation)? 
				offsetTop : 
				(offsetTop + origin.offsetHeight - dialogHeight);
			
			return {
				left: limit(x, minLeft, maxLeft) + 'px',
				top: limit(y, minTop, maxTop) + 'px'
			};
		},

		/**
		 * Finds the top left coordiantes to make the dialog appear centered on screen.
		 * 
		 * @return {Object} offset Offset object with left and top px properties.
		 */
		getCenter: function() {
			var doc = document.documentElement;
			var scrollTop = window.pageYOffset || doc.scrollTop;
			var scrollLeft = window.pageXOffset || doc.scrollLeft;
			return {
				left: scrollLeft + ((window.innerWidth || doc.clientWidth) - this.$container.width())/2 + 'px',
				top: scrollTop + ((window.innerHeight || doc.clientHeight) +  - this.$container.height())/2 + 'px'
			};
		},

		/**
		 * Checks whether the event's target is related to the dialog: the dialog itself or
		 * anything in it, the dialog's opening link, and anything if modal. Used by the 
		 * Dialogs class' generic close handler to test whether a dialog may be closed.
		 * 
		 * @param {Event} e
		 * @return {boolean} targeted
		 */
		targetOf:function(e) {
			if(this.modal) {
				return true;
			}

			var node = e.target;
			while(node) {
				if(node == this.container || node == this.origin) {
					return true;
				}

				node = node.parentNode;
			}
			return false;
		}
	};

	LBi.namespace('Dialog', Dialog);

	/**
	 * The centered dialog class extends the default dialog, and centers itself when opened.
	 * 
	 * @class LBi.CenteredDialog
	 * @constructor
	 * @extends LBi.Dialog
	 * @param {jQuery} element The dialog's root element in a jQuery result.
	 * @param {LBi.Dialogs} manager The dialog's manager
	 * @param {String} type The dialog type, excluding the "dialog-" prefix.
	 * @return {LBi.Dialog} dialog
	 */
	LBi.CenteredDialog = LBi.Class.extend(
		Dialog, null, {
		getPosition:function() {
			return this.getCenter();
		}
	});

	/**
	 * The overlay class can be used to block interaction with a page for modal dialogs and 
	 * other kinds of popups. Overlays are created automatically by LBi.Dialogs instances when needed,
	 * and should not be created manually. See the LBi.Dialogs.Defaults for specific overlay settings.
	 *
	 * @class LBi.Overlay
	 * @constructor
	 * @param {LBi.Dialogs} manager The overlay's managing Dialogs instance.
	 * @return {LBi.Overlay} overlay
	 */
	var Overlay = function(manager) {
		this.manager = manager;
	};

	Overlay.prototype = {
		constructor: Overlay,

		/**
		 * Shows the overlay using a fade. Subclasses may opt to override this method.
		 * @method show
		 */
		show:function() { 
			this.$container.fadeIn(); 
		},

		/**
		 * Hides the overlay using a fade. Subclasses may opt to override this method.
		 * @method hide
		 */
		hide:function() { 
			this.$container.fadeOut(); 
		},
		
		/**
		 * Creats the overlay element.
		 * This method is called via toggle, and should not be called manually.
		 * @private
		 */
		create:function() {
			var template = this.manager.getProperty('overlay');
			this.$container = $(template);
			this.$container.css(CSS_ABSOLUTE);
			this.container = this.$container[0];
			$('body').append(this.$container);
		},
		
		/**
		 * Calculates the page's height for the overlay to cover it.
		 * This method is called via toggle, and should not be called manually.
		 * @private
		 */
		getHeight:function() {
			var height = document.documentElement.scrollHeight || document.body.scrollHeight;
			var winHeight = window.innerHeight || document.documentElement.clientHeight;
			return (height < winHeight)? winHeight : height;
		},
		
		/**
		 * Toggles the overlay. If it was not yet represented by an element in the DOM, it will be
		 * created using the "overlay" template property, as specified for the LBi.Dialogs instance
		 * that manages the overlay. 
		 *
		 * @param {boolean} toggle
		 */
		toggle:function(toggle) {
			if(!this.container) {
				this.create();
			}

			if(toggle) {
				var height = this.getHeight();
				this.$container.css({ height: height + 'px' });
				this.show();
			} else {
				this.hide();
			}
		}
	};

	LBi.namespace('Overlay', Overlay);

	/**
	 * Dialog manager defaults. Any of these settings may be overruled using the settings parameter
	 * that is passed to dialog manager instances.
	 *
	 * @static
	 * @class LBi.Dialogs.Defaults
	 */
	Dialogs.Defaults = {
		/**
		 * Template of which dialogs are created if they're not already in the html.
		 * @property template
		 * @type String
		 * @default '&lt;div class="dialog">&lt;/div>'
		 */
		template: '<div class="dialog"></div>',
		
		/**
		 * Default dialog class used for dialogs that are not registered to a specific class.
		 * @property dialogClass
		 * @type Class
		 * @default LBi.Dialog
		 */
		dialogClass: Dialog,
		
		/**
		 * Global dialog prefix, used in rel attributes and for IDs.
		 * @property prefix
		 * @type String
		 * @default 'dialog-'
		 */
		prefix: 'dialog-',
		
		/**
		 * Orientation, any combo of left, right, top and bottom.
		 * @property orientation
		 * @type String
		 * @default 'right top'
		 */
		orientation: 'right top',
		
		/**
		 * Spacing applied between links and dialogs that are opened next to them.
		 * @property offset
		 * @type number
		 * @default 10
		 */
		offset: 10,
		
		/**
		 * Defines whether dialogs are modal or not.
		 * @property modal
		 * @type boolean
		 * @default false
		 */
		modal: false,
		
		/**
		 * Template of which the modal overlay is created.
		 * @property overlay
		 * @type String
		 * @default '&lt;div id="overlay">&lt;/div>'
		 */
		overlay: '<div id="overlay"></div>',
		
		/**
		 * Defines the class of which the overlay is created, for subclassing purposes.
		 * @property overlayClass
		 * @type Class
		 * @default LBi.Overlay
		 */
		overlayClass: Overlay,

		/**
		 * The animation used for toggling the visibility of a dialog
		 * @property animation
		 * @type LBi.Animation
		 * @default LBi.Animation.FADE
		 */
		animation: LBi.Animation.FADE,

		/**
		 * The duration of the animation (if any)
		 * @property animationTime
		 * @type number
		 * @default 200
		 */
		animationTime: 200,
		
		/**
		 * Reference to an optional shared LBi.LinkRelations instance. An instance is automatically created if this property is left to null.
		 * @property relations
		 * @type Object
		 * @default null
		 */
		relations: null
	};

})(jQuery);