Ext.namespace('Zarafa.core.ui.notifier');

/**
 * @class Zarafa.core.ui.notifier.SliderContainer
 * @extends Object
 *
 * Special container to be used by the {@link Zarafa.core.ui.notifier.SliderNotifyPlugin}.
 * This supports holding multiple notification messages within a single DIV element,
 * and slide new ones into the container, and old ones out. The code is structured so only
 * one animation occurs at a time, and the container can be positioned anywhere on the screen.
 */
Zarafa.core.ui.notifier.SliderContainer = Ext.extend(Object, {
	/**
	 * @cfg {Ext.Element} parentContainer The parent container in which the
	 * slider will be positioned.
	 */
	parentContainer : undefined,

	/**
	 * @cfg {String} containerCls The CSS class to be applied on the {@link #container}.
	 */
	containerCls : 'zarafa-notifier-container',

	/**
	 * @cfg {String} itemCls The CSS class to be applied to the DIV wrapper around
	 * the messages added in {@link #createMessage}.
	 */
	itemCls : 'zarafa-notifier-container-item',

	/**
	 * @cfg {String} containerPosition The position of the container
	 * in which the notifier will be shown. This can be any of the following values:
	 * - 'tl': The top left corner
	 * - 't': The center of the top edge (default)
	 * - 'tr': The top right corner
	 * - 'l': The center of the left edge
	 * - 'c': In the center of the element
	 * - 'r': The center of the right edge
	 * - 'bl': The bottom left corner
	 * - 'b': The center of the bottom edge
	 * - 'br': The bottom right corner
	 */
	containerPosition : 't',

	/**
	 * @cfg {Char} slideinDirection the animation slidein direction for notification
	 */
	slideinDirection : 't',

	/**
	 * @cfg {Char} slideoutDirection the animation slideout direction for notification
	 */
	slideoutDirection : 't',

	/**
	 * @cfg {Boolean} animatedContainerResize True to animate the resizing of the container
	 * when a new notification is added into the container. This defaults to 'false' if
	 * {@link #slideinDirection} is 't', 'true' otherwise.
	 */
	animatedContainerResize : false,

	/**
	 * The container in which the sliders will be placed.
	 * @property
	 * @type Ext.Element
	 * @private
	 */
	container : undefined,

	/**
	 * The array of {@link Ext.Element Elements} which are pending to be displayed in the {@link #container}.
	 * Because we cannot animate two messages simultaneously, we keep the list of pending messages here, which
	 * serves as FIFO queue. Whenever a message has been animated, this list will be checked to see if a message
	 * is in the queue to be animated.
	 * @property
	 * @type Array
	 * @private
	 */
	pendingMessages : undefined,

	/**
	 * The array of {@link Ext.Element Elements} which are currently displayed in the {@link #container}.
	 * @property
	 * @type Array
	 * @private
	 */
	stackedMessages : undefined,

	/**
	 * Used by {@link #showNextMessage} to check if there currently is a Message which is being animated.
	 * When this field is set to 'true', then no other message is allowed to start animating.
	 * @property
	 * @type Array
	 * @private
	 */
	animating : false,

	/**
	 * @constructor
	 * @param {Object} config Configuration object
	 */
	constructor : function(config)
	{
		Ext.apply(this, config);

		this.container = this.getContainer();
		this.pendingMessages = [];
		this.stackedMessages = [];

		// update animatedContainerResize depending on configuration
		if (!Ext.isDefined(config.animatedContainerResize)) {
			this.animatedContainerResize = this.slideinDirection !== 't';
		}
	},

	/**
	 * Add a new slider to the container. This will animate the slider into the {@link #container}.
	 * @param {String} html The HTML string for the slider to create
	 * @param {Number} timeout (optional) If provided, the message will be removed automatically after the timeout
	 * @return {Ext.Element} The slider element which was created
	 */
	createMessage : function(html, timeout)
	{
		// Construct the Ext.Element object
		var element = Ext.DomHelper.append(this.container, { html : html, cls : this.itemCls, style : 'visibility: hidden;' }, true);

		// Push it to pending messages
		this.pendingMessages.push(element);
		element.timeout = timeout;
		element.slider = this;

		// Check if there are no pending messages, and we can direct display this new one.
		this.showNextMessage();

		return element;
	},

	/**
	 * Update a message which was previously created using {@link #createMessage}.
	 * @param {Ext.Element} The element to update
	 * @param {String} html The HTML string for the slider to update
	 * @return {Ext.Element} The slider element which was updated
	 * @private
	 */
	updateMessage : function(element, html)
	{
		element.dom.innerHTML = html;
		return element;
	},

	/**
	 * Remove a message which was previously created using {@link #createMessage}.
	 * @param {Ext.Element} element The element to remove
	 */
	removeMessage : function(element)
	{
		this.animating = true;

		if (this.stackedMessages.indexOf(element) >= 0) {
			element.ghost(this.slideoutDirection, {
				remove : true,
				stopFx : true,
				callback : this.onRemoveComplete.createDelegate(this, [ element ]),
				scope : this
			});
		}
	},

	/**
	 * Called whenever a message has been added to the {@link #pendingMessages} queue, or when
	 * a message has been removed from the {@link #stackedMessages}. When we still have pending
	 * messages in {@link #pendingMessages},  we first check if we are currently busy
	 * {@link #animating animating} a message, or if there is no room for a new message to appear.
	 * When neither problem occurs, then we start animating the new message into the screen.
	 *
	 * @private
	 */
	showNextMessage : function()
	{
		var element = this.pendingMessages[0];
		var msgHeight = element.getHeight();
		var ctHeight = this.container.getHeight();
		var maxHeight = this.parentContainer.getHeight();

		// The <body> element might sometimes have height 0, compensate this
		// by asking for the document height in that case.
		if (maxHeight === 0 && this.parentContainer.dom.tagName === 'BODY') {
			maxHeight = document.height;
		}

		if (this.animating || (ctHeight + msgHeight) > maxHeight) {
			return;
		}
		this.animating = true;

		// Update the dimensions of the container to make room for the new element.
		// All existing messages will be nicely shifted the require height higher.
		var newHeight = ctHeight + msgHeight;
		this.updateContainer(newHeight, this.animatedContainerResize);

		// Store the original height of the message for later,
		// we need it for onMessageSlideOutComplete() when the real height has been reset,
		// and we need to resize the container.
		element.height = msgHeight;

		// Move the element to the next queue
		this.pendingMessages.remove(element);
		this.stackedMessages.push(element);

		this.animMessage(element);
	},

	/**
	 * Start the animation which will {@link Ext.Fx#slideIn slidein} the given element.
	 * If the slider was configured with a timeout, it will {@link Ext.Fx#pause show} the
	 * element for the given time and then {@link Ext.Fx#ghost remove} it again.
	 * @param {Ext.Element} element The element to show
	 * @private
	 */
	animMessage : function(element)
	{
		element = element.slideIn(this.slideinDirection, {
			callback : this.onShowComplete,
			scope : this
		});

		// Check if this element must automatically be removed
		// after a certain timeout.
		if (Ext.isNumber(element.timeout)) {
			element.pause(element.timeout, {
				callback : this.onPauseComplete,
				scope : this
			}).ghost(this.slideoutDirection, {
				remove : true,
				callback : this.onRemoveComplete.createDelegate(this, [ element ]),
				scope : this
			});
		}
	},

	/**
	 * Event handler which is called when the {@link Ext.Fx#slideIn slideIn} animation has been
	 * completed. This will reset the {@link #animating} flag, and call {@link #showNextMessage}
	 * to start animating any pending messages.
	 *
	 * @private
	 */
	onShowComplete : function()
	{
		this.animating = false;

		// Show the next message if there are pending messages,
		// and the parent container still exists (it might have been
		// deleted while the element was animating).
		if (!Ext.isEmpty(this.pendingMessages) && this.parentContainer.dom) {
			this.showNextMessage();
		}
	},

	/**
	 * Event handler which is called when the {@link Ext.Fx#pause pause} has been completed.
	 * This will set the {@link #animating} field again to prepare for the {@link Ext.Fx#ghost ghost}
	 * animation.
	 *
	 * @private
	 */
	onPauseComplete : function()
	{
		this.animating = true;
	},

	/**
	 * Event handler which is called when the {@link Ext.Fx#ghost ghost} animation has been
	 * completed. This will resize the {@link #container} to make sure it will only
	 * contain enough room for the visible {@link #stackedMessages messages}.
	 * @param {Ext.Element} element The element which was removed
	 * @private
	 */
	onRemoveComplete : function(element)
	{
		var newHeight = this.container.getHeight() - element.height;
		var index = this.stackedMessages.indexOf(element);

		// Technically shouldn't happen...
		if (index < 0) {
			return;
		}

		// Remove the element from stack
		this.stackedMessages.splice(index, 1);

		this.animating = false;

		if (newHeight < 0) {
			newHeight = 0;
		}

		// After cleaning up, we need to check if the parent container
		// still exists, as it might have been deleted while the element
		// was animating.
		if (!this.parentContainer.dom) {
			return;
		}

		// Don't animate the repositioning, otherwise all visible
		// messages will be bumbed above and then animated down again.
		// By not animating, the messages will remain exactly where they
		// are.
		this.updateContainer(newHeight, false);

		if (!Ext.isEmpty(this.pendingMessages)) {
			this.showNextMessage();
		}
	},
																		    
	/**
	 * Obtain the {@link Ext.Element container} in which this slider will operate.
	 * This uses the {@link #parentContainer} to find if any slider container already
	 * exists, if not, it will create one at the {@link #containerPosition}.
	 * @return {Ext.Element} The slider container
	 * @private
	 */
	getContainer : function()
	{
		var parentId = this.parentContainer.id;
		var sliderCls = this.containerCls + ' ' + this.containerCls + '-' + this.containerPosition;
		var sliderId = parentId + '-' + this.containerCls + '-' + this.containerPosition;

		var ct = Ext.DomQuery.select('#' + sliderId).shift();
		if (!ct) {
			// Insert the notifier message into the given container
			ct = Ext.DomHelper.insertFirst(this.parentContainer, { id : sliderId, cls : sliderCls }, true);

			// Set initial height to 0
			ct.setHeight(0);

			// Position the container to the correct location
			ct.alignTo(this.parentContainer, this.containerPosition);
		} else {
			ct = Ext.get(ct);
		}

		return ct;
	},

	/**
	 * Update the {@link #container} with a new height. This will recalculate the
	 * position of the {@link #container} to make sure the container remains fixed
	 * to the {@link #containerPosition.
	 *
	 * @param {Number} height The new height which must be applied to the container
	 * @param {Boolean} animate True when the resize/repositioning of the container
	 * should be animated.
	 * @private
	 */
	updateContainer : function(height, animate)
	{
		// The position of the element should be: br-br, t-t, etc, to ensure
		// that the bottom-right corner of the container is in the bottom-right
		// corner of the parentContainer.
		var position = this.containerPosition + '-' + this.containerPosition;

		// Update positioning of the container
		this.container.setHeight(height);
		this.container.alignTo(this.parentContainer, position, undefined, animate);
	}
});
