function ZoneFx() {
}

/**
 * View zone effect: fade out the old content and fade in the new content.
 * @param zoneHolder the zone div
 * @param html the html of the new content
 * @param props optional properties. Use to overwrite default settings
 * @param callback the optional callback function
 */
ZoneFx.viewZoneCrossFade = function(zoneHolder, html, props, callback) {
	var opt=jQuery.extend({speed: 750}, props);
	
	// for image maps we need to make sure that the view is not zoomed as all images
	// are rendered in a non zoomed variation by default
	if(currentZoom!=1) {
		minimize();
		initializeImagePos();
	}
	
	var fxHelper=new FxHelper(zoneHolder);
	fxHelper.prepareContent(html);
	
	var anchorOld=fxHelper.oldContent.is("a") ? fxHelper.oldContent : $("a", fxHelper.oldContent);
	var anchorNew=fxHelper.newContent.is("a") ? fxHelper.newContent : $("a", fxHelper.newContent);
	anchorNew.css({opacity: 0});
	
	// in case there already is a global waitbox, do nothing
	var waitBox=$("#WaitBox");
	if(waitBox.size()==0) {
		var imgBox=$("#ImageBox");
		imgBox.append('<div id="WaitBox" style="position:absolute; left:'+((imgBox.width()/2)-16)+'px; top:'+((imgBox.height()/2)-16)+'px; width:32px; height:32px;z-index:1;">'+
				'<img src="'+contextRelative("images/ajax-loader.gif")+'" border="0" style="width:32px;height:32px;"></div>');
		waitBox=$("#WaitBox");
	}
	
	var imgOld=$("img", fxHelper.oldContent);
	var imgNew=$("img", fxHelper.newContent);
	imgNew.one("load", function() {
		if (isCentered()) {
			var imageBoxWidth = $('#ImageBox').width();
			setImagePosX(((imageBoxWidth-imgNew.width())/2)+imagePosLeft);
		}
		else {
			setImagePosX(imagePosLeft+imageBorder);
		}
		setImagePosY(imagePosTop+imageBorder);
		initializeImagePos();

		// we want to have crossfading only in case the room is changed; when the room
		// remains the same, the crossfading effect looks "bad". However, as room changing
		// information is not available here, we just check whether the old and new
		// image sizes are different; in case they are, we assume that the room was
		// changed and perform the fadeout then to achieve the desired crossfading
		if(imgOld.width()!=imgNew.width() || imgOld.height()!=imgNew.height()) {
			anchorOld.animate({opacity: 0}, opt.speed);
		}
		
		anchorNew.animate({opacity: 1}, opt.speed, function() {
			waitBox.remove();
			
			// we need to invoke fxHelper.finished() first in order
			// to remove the old content; the upcoming functions
			// is looking for some identifiers that exist in the old
			// and in the new content, so we need to remove the old content
			fxHelper.finished();
			
			setNewImageLoaded();
			// instead of setting the title of the <a> tag which is then
			// used in the lightbox as title, we set the #caption box
			// manually so that the <a> title is not displayed as tooltip
			$(".lightbox").lightbox({displayTitle: false,
				fileLoadingImage: contextRelative(jQuery.fn.lightbox.defaults.fileLoadingImage),
				fileBottomNavCloseImage: contextRelative('scripts/base/jquery-lightbox/images/close.gif')});
			initializeLightbox();

			// we need to invoke the callback manually as fxHelper.finished() has been called already
			if(callback) callback(fxHelper.zoneHolder);
		});
	});
}

/**
 * Room zone effect: fade out the old content and fade in the new content.
 * @param zoneHolder the zone div
 * @param html the html of the new content
 * @param props optional properties. Use to overwrite default settings
 * @param callback the optional callback function
 */
ZoneFx.roomZoneCrossFade = function(zoneHolder, html, props, callback) {
	var opt=jQuery.extend({speed: 750}, props);
	
	var fxHelper=new FxHelper(zoneHolder);
	fxHelper.prepareContent(html);

	fxHelper.newContent.css({opacity: 0});
	fxHelper.newContent.animate({opacity: 1}, opt.speed);
	
	fxHelper.oldContent.animate({opacity: 0}, opt.speed, function() {
		fxHelper.finished(callback);
	});
}

/**
 * Product list zone effect: fade out the old content and fade in the new content.
 * @param zoneHolder the zone div
 * @param html the html of the new content
 * @param props optional properties. Use to overwrite default settings
 * @param callback the optional callback function
 */
ZoneFx.productListCrossFade = function(zoneHolder, html, props, callback) {
	var opt=jQuery.extend({speed: 750, orientation: "vertical"}, props);
	
	var fxHelper=new FxHelper(zoneHolder);
	fxHelper.prepareContent(html);
	
	// we will adjust some CSS values. jquery does this by creating
	// an inline style attribute. Save the current style attribute
	// and reset it later to undo the changes
	// Note: we assume that newContent consists of only one element
	fxHelper.saveAttr(fxHelper.newContent, "style");
	if(opt.orientation=="vertical") fxHelper.alignProductListBottomContent(true, false);
	
	fxHelper.saveAttr($('#ProductList'), "style");
	$('#ProductList').css("overflow", "hidden");
	
	// IE7 needs to have a width specified for the top component, otherwise
	// the bottom component is placed to the right. To avoid specifying
	// a width in the css just for this purpose, we set it here
	if (opt.orientation=="vertical" && $.browser.msie && $.browser.version.substr(0,1)==7) {
		fxHelper.oldContent.css({width: $('#ProductList').width()+"px"});
	}
	
	fxHelper.newContent.css({opacity: 0});
	fxHelper.newContent.animate({opacity: 1}, opt.speed);
	
	// reset scroll position as the new list might be shorter
	$('#ProductList').scrollTop(0).scrollLeft(0);
	
	fxHelper.oldContent.animate({opacity: 0}, opt.speed, function() {
		fxHelper.finished(callback);
	});
}

/**
 * Product list zone effect: slide view from old content to new content.
 * The new content may be placed to the left or to the right (set
 * slideToLeft property to true for this)
 * @param zoneHolder the zone div
 * @param html the html of the new content
 * @param props optional properties. Use to overwrite default settings
 * @param callback the optional callback function
 */
ZoneFx.productListSlide = function(zoneHolder, html, props, callback) {
	var opt=jQuery.extend({speed: 750, slideToLeft: false}, props);
	
	var fxHelper=new FxHelper(zoneHolder);
	fxHelper.prepareContent(html, opt.slideToLeft);
	
	// we will adjust some CSS values. jquery does this by creating
	// an inline style attribute. Save the current style attribute
	// and reset it later to undo the changes
	// Note: we assume that newContent consists of only one element
	fxHelper.saveAttr(fxHelper.newContent, "style");
	fxHelper.alignProductListBottomContent(true, true);
	
	fxHelper.saveAttr($('#ProductList'), "style");
	$('#ProductList').css("overflow", "hidden");
	
	var listWidth=$('#ProductList').width()+"px";
	
	// we need to increase the width of the new content so that the maximum scrolling position
	// is large enough to move the new content into position and that there is enough border at the right
	fxHelper.bottomContent.css({width: listWidth});
	
	var scrollto=listWidth;
	if(opt.slideToLeft) {
		scrollto="0px";
		$('#ProductList').scrollLeft($('#ProductList').width());
	}
	
	$('#ProductList').animate({scrollLeft: scrollto, scrollTop: "0px"}, opt.speed, function() {
		fxHelper.finished(callback);
	});
}

function FxHelper(zoneHolder) {
	this.zoneHolder=zoneHolder;
	
	this.oldContent=null;
	this.topContent=null;
	this.newContent=null;
	this.bottomContent=null;
	
	this.attrStack=[];
	this.callback;
}

/**
 * Converts the specified html string to nodes and inserts
 * them into the zoneHolder node. The topContent and bottomContent
 * variables are set according to the "insertBeforeOldContent"
 * parameter.
 * @param html the html content
 * @param insertBeforeOldContent (optional) if set to true, then
 *        the new content is placed before the existing nodes,
 *        otherwise the new nodes are appended at the end
 */
FxHelper.prototype.prepareContent = function(html, insertBeforeOldContent) {
	// use a temporary tmpZoneHolder to convert html string to nodes
	var tmpZoneHolder=this.zoneHolder.cloneNode(false);
	tmpZoneHolder.innerHTML = html;
	this.newContent=$(tmpZoneHolder).children();
	this.bottomContent=this.newContent; // normally the old content is above the new content

	this.oldContent=$(this.zoneHolder).children();
	this.topContent=this.oldContent; // normally the old content is above the new content
	
	if(insertBeforeOldContent) {
		this.topContent=this.newContent;
		this.bottomContent=this.oldContent;
		var before=this.zoneHolder.firstChild;
		for(var i=0; i<tmpZoneHolder.childNodes.length; i++) {
			this.zoneHolder.insertBefore(tmpZoneHolder.firstChild, before);
		}
	}
	else {
		for(var i=0; i<tmpZoneHolder.childNodes.length; i++) {
			this.zoneHolder.appendChild(tmpZoneHolder.firstChild);
		}
	}
}

/**
 * Modifies the old and new content nodes so that they are visually
 * placed on each other with their top left corner.
 * @param top if true, then bottom component is moved to the top
 * @param right if true, then the bottom component is moved to the right
 */
FxHelper.prototype.alignProductListBottomContent = function(top, right) {
	// note: we need to place the top and bottom (ie. normally "old" and "new")
	// content above each other to have the proper fading effect. We could use
	// absolute positioning, however, this doesn't work as we use a scrollpanel.
	// Solution: set the bottom element to relative position and move
	// it upwards. We also need to adjust the top element to "float: left"
	// so that the bottom element start at its own row
	this.topContent.css({float: "left"});
	
	var h="-"+this.topContent.height()+"px";
	var listWidth=$('#ProductList').width()+"px";
	// IE6 and IE7 have problems when setting the position to relative,
	// then the content will appear outside of the scrollpanel.
	// Workaround: change the margin-top setting (note: does not work in Firefox)
	var props={};
	if ($.browser.msie && $.browser.version.substr(0,1)<8) {
		if(top) props["margin-top"]=h;
		if(right) props["margin-left"]=listWidth;
	}
	else {
		props["position"]="relative";
		if(top) props["top"]=h;
		if(right) props["left"]=listWidth;
	}
	
	this.bottomContent.css(props);
}

/**
 * Utility function that removes the old content, restores the attributes
 * and then (optionally) invokes the specified callback function.
 */
FxHelper.prototype.finished = function(callback) {
	// old content is not used anymore, it can be removed now
	if(this.oldContent) {
		this.oldContent.remove();
		this.oldContent=null;
	}
	
	// reset the temporary changes to attributes by reverting
	// to the previous value
	this.restoreAttrs();
	
	if(callback) callback(this.zoneHolder);
}

/**
 * Saves the value of the specified attribute of the first matched element.
 * This function checks whether the element specified has been saved
 * already by this FxHelper or by another by, e.g. because another effect
 * is also active. In this case, the current value is not changed, only
 * an internal use count is increased.
 */
FxHelper.prototype.saveAttr = function(element, attr) {
    var saveCount=element.attr("_sc_"+attr);
    if(saveCount==undefined) {
    	// not saved before
    	element.attr("_sc_"+attr, 1);
    	
    	var value=element.attr(attr);
    	if(value!=undefined) element.attr("_sv_"+attr, value);
    }
    else {
		// already saved by this effect or another one, just increase use count
    	var saveCountInt=parseInt(saveCount)+1;
    	element.attr("_sc_"+attr, saveCountInt);
    }
    
    this.attrStack.push({element: element, attrName: attr});
}

/**
 * Restores all attributes and then clears the saved state. Note that
 * this function takes into account parallel effects, ie. when another
 * effect has also saved the state for an element, the state is not
 * restored. Only the last user of the element will restore the original
 * state when calling this function.
 */
FxHelper.prototype.restoreAttrs = function() {
	for(var i=this.attrStack.length-1; i>=0; i--) {
		var entry=this.attrStack[i];
		
		var saveCount=entry.element.attr("_sc_"+entry.attrName);
		if(saveCount==undefined || parseInt(saveCount)<2) {
			// restore element
			var value=entry.element.attr("_sv_"+entry.attrName);
			entry.element.removeAttr(entry.attrName);
			
			if(value==undefined) entry.element.removeAttr(entry.attrName);
			else {
				entry.element.attr(entry.attrName, value);
				entry.element.removeAttr("_sv_"+entry.attrName);
			}
			
			// clear use count
			entry.element.removeAttr("_sc_"+entry.attrName);
		}
		else {
			// decrease use count
			var saveCountInt=parseInt(saveCount)-1;
			entry.element.attr("_sc_"+entry.attrName, saveCountInt);
		}
	}
	this.attrStack=[];
}
