/*global console, loadFirebugConsole, window, opensocial, MyOpenSpace, gadgets, jQuery, $, document, navigator */
/*jslint evil: true */

/**
 * OSB - OpenSocial Bridge.
 * @author Simon Hanukaev
 */
var OSB = {
	// Main configuration parameters:
	DEV_MODE:						false,
	FRAMEWORK_PAGE:					"http://www.3d3r.com/simon/osb/pages/framework.html",
	INITIAL_PAGE:					"http://www.3d3r.com/simon/osb/pages/main.html",
	TEMPLATES_URL:					"http://www.3d3r.com/simon/osb/templates/templates.html",
	UNAUTHORIZED_PAGE:				null,
	LOADING_IMAGE_URL:				"http://www.3d3r.com/simon/osb/images/ajaxLoaderLarge.gif",
	INVITATION_ICON_URL:			"http://www.3d3r.com/simon/osb/images/invitation_icon.gif",
	PUBLIC_MESSAGE_ICON_URL:		"http://www.3d3r.com/simon/osb/images/public_message_icon.gif",
	PRIVATE_MESSAGE_ICON_URL:		"http://www.3d3r.com/simon/osb/images/private_message_icon.gif",
	NOTIFICATION_MESSAGE_ICON_URL:	"http://www.3d3r.com/simon/osb/images/notification_icon.gif",
	EMAIL_MESSAGE_ICON_URL:			"",
	MAIN_CONTAINER_ID:				"mainContainer",
	AJAX_LINK_CLASS:				"ajaxLink",
	AJAX_FORM_CLASS:				"ajaxForm",
	INVITATION_LINK_TEXT:			"Invite friends",
	OPENSOCIAL_VERSION:				0.7,
	urlParameters: {
		// parameter names sent to back-end server.
		VIEWER_ID:					"viewerid",
		APP_ID:						"appid",
		RECIPIENT_ID:				"recipientid",
		DOMAIN:						"domain"
	},
	
	// internal variables, updated on initialization stage.
	APP_ID:							"",					// Updated on initialization stage
	OWNER_ID:						"",					// Updated on initialization stage
	VIEWER_ID:						"",					// Updated on initialization stage
	DOMAIN:							"",					// Updated on initialization stage
	APP_NAME:						"",
	GADGET_URL_PARAMS:				{},
	owner:							null,
	viewer:							null,
	viewerFriends:					[],
	friendsData:					[]					// Fetched from container on initialization stage
	//invitationControllerButtons:	[]
};

/**
 * Capabilities of different opensocial containers.
 * Initialized on OSB initialization.
 * @private
 */
OSB.Capabilities = (function() {
	var container = null;
	var capabilities = {
		requestShareApp: {
			hi5:		"nativeController",
		//	myspace are now supports Multiple Recipient App Invites
		//	myspace:	"singleId",
			orkut:		"singleId"
		},
		requestSendMessage: {
			myspace:	"singleId",
			orkut:		"singleId"
		},
		newMessage: {
			
		},
		messageTypeLegend: {
			myspace: {
				"public":		"comment",
				"private":		"message",
				"notification":	"bulletin",
				"email":		"email"
			}
		}
	};
	
	function initialize() {
		console.debug("OSB.Capabilities.initialize()");
		container = /^(\w+)/.exec(OSB.DOMAIN)[1].toLowerCase();
		console.debug("container = "+container);
		this.container = container;
	}
	
	function getCapability(capability) {
		var result = capabilities[capability][container];
		if (result === undefined) {
			result = "";
		}
		return result;
	}
	
	return {
		initialize:		initialize,
		getCapability:	getCapability
	};
})();

// Firebug console handling
(function () {
	if (typeof loadFirebugConsole == 'function') {
		// Firebug ver 1.2
		loadFirebugConsole();
		var originalDebug = window.console.debug;
		window.console.debug = function() {
			if (arguments.callee.caller !== null) {
				var funcName = arguments.callee.caller;
				var args = Array.prototype.slice.call(arguments);
				args.unshift(funcName, "-");
				originalDebug.apply(console, args);
			} else {
				originalDebug.apply(console, arguments);
			}
		};
	}  else {
		// Firebug ver < 1.2
		// Define dummy functions for all other browsers
		if (!("console" in window) || !("firebug" in console)) {
		    var names = ["log", "debug", "info", "warn", "error", "assert",
						 "dir", "dirxml", "group", "groupEnd", "time", "timeEnd",
						 "count", "trace", "profile", "profileEnd"];
		    window.console = {};
			var dumpFunc = function() {};
		    for (var i = 0; i < names.length; i++) {
				window.console[names[i]] = dumpFunc;
		    }
		}
	}
})();

/**
 * OSB initialization method. Make sure that you run this
 * method after gadgets script have been loaded and initialized.
 * example:
 * gadgets.util.registerOnLoadHandler(function() {
 *		OSB.initialize();
 * });
 */
OSB.initialize = function() {
	console.debug("##### OSB.PageLoader.initialize() #####");
	console.debug("OSB object reference ",OSB);
	
	// set application parameters
	try {
		OSB.DOMAIN = opensocial.getEnvironment().getDomain();
		console.debug("domain: "+OSB.DOMAIN);
		
		OSB.Capabilities.initialize();
		
		// Get User Preferences from gadget (defined in gadget XML)
		console.debug("gadgets = ",gadgets);
		
		var prefs = new gadgets.Prefs();
		console.debug("new gadgets.Prefs() -> ", prefs);
		
		var urlParameters = gadgets.util.getUrlParameters();
		console.debug("gadgets.util.getUrlParameters() -> ", urlParameters);
		
		var params = gadgets.views.getParams();
		console.debug("gadgets.views.getParams() -> ", params);
		// same as previous:
		// var params = opensocial.getEnvironment().getParams();
		
		var container = opensocial.Container.get();
		console.debug("opensocial.Container.get() -> ", container);
		
		switch (OSB.DOMAIN) {
			case "myspace.com":
				if (OSB.OPENSOCIAL_VERSION == 0.7) {
					OSB.OWNER_ID = params.ownerid;
					OSB.VIEWER_ID = params.viewerid;
				} else if (OSB.OPENSOCIAL_VERSION == 0.8) {
					OSB.OWNER_ID = params.ownerId;
					OSB.VIEWER_ID = params.viewerId;
				}
				OSB.APP_ID = params.appid;
				OSB.APP_NAME = opensocial.getEnvironment().currentApplication.getDisplayName();
				// NOT WORKING IN MY SPACE!!!
				// http://developer.myspace.com/Community/forums/t/5497.aspx
				console.debug("Check next object in MySpace: ", prefs.getString("INITIAL_PAGE"));
				if (params.appParams) {
					OSB.GADGET_URL_PARAMS = params.appParams;
				}
				break;
			case "hi5.com":
				// OSB.APP_ID = "TODO";
				OSB.OWNER_ID = container.owner.getId();
				OSB.VIEWER_ID = container.viewer.getId();
				for (var prop in params) {
					if (params.hasOwnProperty(prop) && prop != "Hi5AuthToken") {
						OSB.GADGET_URL_PARAMS[prop] = params[prop];
					}
				}
				break;
			case "orkut.com":
				OSB.APP_ID = urlParameters.gadgetId;
				OSB.OWNER_ID = urlParameters.gadgetOwner;
				OSB.VIEWER_ID = urlParameters.gadgetViewer;
				break;
		}
	} catch (e) {
		console.error("Error while initializing OSB: " + e.name + " - " + e.message);
	}
	
	// print to console all initialized properties of OSB
	console.group("OSB Initialized Constants");
	for (var prop in OSB) {
		if (OSB.hasOwnProperty(prop) && typeof OSB[prop] != "object" && typeof OSB[prop] != "function") {
			console.debug("OSB."+prop+" = "+OSB[prop]);
		}
	}
	console.groupEnd();
	
	// Initiate Asynchronous Calls
	var totalCalls = 3;
	var completedCalls = 0;
	var errors = [];
	
	var $loaderImage, $pageframework;
	
	function handleUnauthorizedViewer() {
		if (OSB.UNAUTHORIZED_PAGE) {
			console.debug("loading OSB.UNAUTHORIZED_PAGE...");
			$pageframework.html("<div id='" + OSB.MAIN_CONTAINER_ID + "'></div>");
			OSB.PageLoader.requestPage(OSB.UNAUTHORIZED_PAGE);
		} else {
			$pageframework.html('<p style="text-align:center;font-weight:bold;">Please click "Add App" to use this application.</p>');
		}
	}
	
	function callCompleted(error) {
		var i;
		
		console.debug("callCompleted(",error,")");
		if (error) {
			console.error("Errors occurred during call...");
			errors.push(error);
		}
		completedCalls++;
		console.debug("completedCalls = " + completedCalls);
		if (completedCalls == totalCalls) {
			// If there was no errors during AJAX calls
			if (errors.length === 0) {
				var pageframework = $pageframework.get(0);
				var pageframeworkExtended = OSB.FBJSExtender.extendElement(pageframework);
				pageframeworkExtended.setInnerFBML(OSB.getFrameworkPage.responseHtml);
				
				// Send OSB.INITIAL_PAGE request with viewer parameters
				var viewerGender = OSB.viewer.osbGetField("gender");
				console.debug("viewerGender = ", viewerGender);
				var viewerCountry = OSB.viewer.osbGetField("country");
				console.debug("viewerCountry = ", viewerCountry);
				var viewerAge = OSB.viewer.getField(opensocial.Person.Field.AGE);
				console.debug("viewerAge = ", viewerAge);
				var params = "gender=" + viewerGender + "&country=" + viewerCountry + "&age=" + viewerAge;
				OSB.PageLoader.requestPage(OSB.INITIAL_PAGE, params);
			} else {
				var unauthorizedError = false;
				for (i = 0; i < errors.length; i++) {
					if (errors[i].errorCode == opensocial.ResponseItem.Error.UNAUTHORIZED) {
						unauthorizedError = true;
					}
				}
				if (unauthorizedError) {
					console.debug("Unauthorized error occurred, need to add the app.");
					handleUnauthorizedViewer();
				} else {
					var errorHTML = '<p style="font-weight:bold; color:#FF0000; margin-bottom:10px;">Sorry, an error occurred while loading application.</p>';
					errorHTML += '<ul style="color:#FF0000; margin-bottom:10px;">';
					for (i = 0; i < errors.length; i++) {
						errorHTML += '<li style="display:block; margin-bottom:5px;">' +
									'error occured ' + errors[i].errorContext + '<br/>' +
									'error code: ' + errors[i].errorCode + '<br/>' +
									'error message: ' + errors[i].errorMessage +
								'</li>';
					}
					errorHTML += '</ul>';
					errorHTML += '<p style="font-weight:bold; color:#000000;">Our TOP priority is solving this issue but we need your help!<br/>Please copy the message above and send it to support_g@productmadness.com.</p>';
					$pageframework.html('<div>' + errorHTML + '</div>');
				}
			}
			$loaderImage.remove();
			$pageframework.css("visibility", "visible");
		}
	}
	
	try {
		$pageframework = $('<div id="pageframework"></div>');
		$pageframework.css("visibility","hidden");
		$("body").append($pageframework);
		
		//console.debug("Checking for VIEWER permission...");
		//var hasPermission = opensocial.hasPermission(opensocial.Permission.VIEWER);
		//if (hasPermission) {
		//	console.debug("viewer has permission");
		
			console.debug("Initiating remote requests...");
			
			$loaderImage = $('<div id="osbLoaderImage" style="text-align:center;"><img src='+OSB.LOADING_IMAGE_URL+' alt="loading"/></div>');
			$("body").append($loaderImage);
	
			OSB.getFrameworkPage(callCompleted);
			OSB.getOpensocialData(callCompleted);
			OSB.templates.loadTemplates(callCompleted);
			
		//} else {
		//	console.debug("viewer don't has permission");
		//	handleUnauthorizedViewer();
		//}
	} catch (err) {
		console.error("OSB Error while sending remote requests: " + err);
	}
};

OSB.getFrameworkPage = function(callback) {
	console.debug("OSB.getFrameworkPage()");
	this.responseHtml = null;
	
	function processResponse(responeData) {
		console.debug("OSB.getFrameworkPage.processResponse(",responeData,")");
		var responseHtml = responeData.text;
		OSB.getFrameworkPage.responseHtml = responseHtml;
		if (responeData.errors && responeData.errors.length) {
			callback({
				errorContext:	'while getting framework page',
				errorCode:		'NA',
				errorMessage:	responeData.errors.join(', ')
			});
		} else {
			callback(0);
		}
	}
	
	// Add passed in parameters and regular parameters to framework page request
	var params = OSB.GADGET_URL_PARAMS;
	params[OSB.urlParameters.VIEWER_ID] = OSB.VIEWER_ID;
	params[OSB.urlParameters.APP_ID] = OSB.APP_ID;
	params[OSB.urlParameters.DOMAIN] = OSB.DOMAIN;
	
	var options = {
		params:	jQuery.param(params)
	};
	OSB.sendRequestToServer(OSB.FRAMEWORK_PAGE, options, processResponse);
};

/**
 * Objects that handles all the application Page Framework.
 */
OSB.PageLoader = (function() {
	
	function processResponse(responeData) {
		console.debug("OSB.PageLoader.processResponse(",responeData,")");
		var responseHtml = responeData.text;
		var mainContainer = document.getElementById(OSB.MAIN_CONTAINER_ID);
		var mainContainerExtended = OSB.FBJSExtender.extendElement(mainContainer);
		mainContainerExtended.setInnerFBML(responseHtml);
	}

	/**
	 * Sending request to the server and reloads the page framework with servers response.
	 * @param {String} url URL of the requested page.
	 * @param {String} params Additional parameters in querystring format.
	 * @param {String} method Sending method - POST or GET supported.
	 */
	function requestPage(url, params, method) {
		console.debug("requestPage("+url+", "+params+", "+method+")");
		// Add viewer id and app id to the request.
		// If there is no ? sign then append ? and then parameter, otherwise append & and then
		// parameters.
		var additionalParameter = ""+
					OSB.urlParameters.VIEWER_ID+"="+OSB.VIEWER_ID+
				"&"+OSB.urlParameters.APP_ID+"="+OSB.APP_ID+
				"&"+OSB.urlParameters.DOMAIN+"="+OSB.DOMAIN;
		url = OSB.utils.appendUrlParameters(url, additionalParameter);
		
		var options = {};
		if (method !== undefined) {
			options.method = method;
		}
		if (params !== undefined) {
			options.params = params;
		}
		
		OSB.sendRequestToServer(url, options, processResponse);
	}
	
	function ajaxLinkClickHandler(event) {
		event.preventDefault();
		event.stopPropagation();
		var href = $(this).attr("href");
		requestPage(href);	
	}
	
	function formSubmitHandler(event) {
		event.preventDefault();
		event.stopPropagation();
		var params = $(this).serialize();
		var url = $(this).attr("action");
		var method = $(this).attr("method");
		requestPage(url, params, method);
	}
	
	/**
	 * Bind event listeners to all ajax links, forms and invitation controller
	 * generated buttons.
	 */
	function attachAjaxHandlers($jQueryContext) {
		console.debug("attachAjaxHandlers()");
		
		$("a." + OSB.AJAX_LINK_CLASS, $jQueryContext).bind("click", ajaxLinkClickHandler);
		$("form." + OSB.AJAX_FORM_CLASS, $jQueryContext).bind("submit", formSubmitHandler);
		//$(".invitationController").bind("click", invitationControllerHandler);
	}
	
/*	function invitationControllerHandler(event) {
		event.preventDefault();
		event.stopPropagation();
		var id = $(this).attr("id");
		var index = /\d+/.exec(id)[0];
		var data = createControllerData(OSB.invitationControllerButtons[index]);
		
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);

		switch (true) {
			case $(this).hasClass("button"):
				InvitationController.executeButtonController(controllerData);
				break;
			case $(this).hasClass("small"):
				InvitationController.openSmallInvitationController(event, controllerData);
				break;
			case $(this).hasClass("full"):
				InvitationController.openFullInvitationController(controllerData);
				break;
		}
	}*/
	
	/**
	 * Gets HTML string and adds enclosing tag to all specific tags empty elements
	 * e.g.: tag <pm:button ... /> will be replaced by <pm:button ... ></pm:button>.
	 * returns new HTML string
	 * @param {String} html
	 * @return New html string.
	 */
	function addEnclosingTag(html) {
		console.debug("addEnclosingTag()");
		var code = "";
		var newHtml = "";
		var startIndex = 0;
		var nameSpaceTagsRegExp = /<(pm|fb):([\w\-]+)(?:\s*)([^>]*?)\/\>/ig;
		var tagResult;
		
		while ((tagResult = nameSpaceTagsRegExp.exec(html)) !== null) {
			// Extract code that is preceding specific tag and append it to newHtml.
			code = html.substring(startIndex, tagResult.index);
			newHtml += code;
			// Change the start index for the next iteration.
			startIndex = nameSpaceTagsRegExp.lastIndex;
			
			var prefix = tagResult[1];
			var tagName = tagResult[2];
			var attributesStr = tagResult[3];
			
			newHtml += "<"+prefix+":"+tagName+" "+attributesStr+"></"+prefix+":"+tagName+">";
		}
		// Append the last code part.
		code = html.substring(startIndex);
		newHtml += code;
		
		return newHtml;
	}
	
	function addFormParameters(jQueryContext) {
		console.debug("addFormParameters(jQueryContext)");
		var $forms = jQueryContext.find("form");
		$forms.each(function(index, domElement) {
			var $vieweridInput	= $('<input type="hidden" name="'+OSB.urlParameters.VIEWER_ID+'" value="'+OSB.VIEWER_ID+'"/>');
			var $appidInput		= $('<input type="hidden" name="'+OSB.urlParameters.APP_ID+'" value="'+OSB.APP_ID+'"/>');
			var $domainInput	= $('<input type="hidden" name="'+OSB.urlParameters.DOMAIN+'" value="'+OSB.DOMAIN+'"/>');
			$(this).prepend($domainInput);
			$(this).prepend($appidInput);
			$(this).prepend($vieweridInput);
		});
	}
	
	return {
		requestPage:		requestPage,
		attachAjaxHandlers:	attachAjaxHandlers,
		addEnclosingTag:	addEnclosingTag,
		addFormParameters:	addFormParameters
	};
})();

/**
 * sendRequestToServer( url, [options], [callback] )
 *  
 * Sends request to server using gadgets.io.makeRequest API. If callback function
 * not supplied, uses dummy function (otherwise the gadgets script returns error).
 * 
 * options parameter is a JSON with three properties:
 * 		params - url encoded string with parameters
 * 		method - POST or GET
 * 		type - TEXT or JSON
 * 
 * @param {String} url
 * @param {Function} options object: {params:"key=value&key=value",method:"POST",type:"JSON"}
 * @param {Function} callback
 */
OSB.sendRequestToServer = function(url, options, callback) {
	console.debug("sendRequestToServer(",url,", ",options,", ",callback,")");
	if (arguments.length == 2) {
		if (typeof arguments[1] == "function") {
			callback = arguments[1];
			options = {};
		}
	}
	var method = "GET";
	var data = "";
	var type = "";
	if (options !== undefined) {
		if (options.method !== undefined) {
			method = options.method.toUpperCase();
		}
		if (options.params !== undefined) {
			data = options.params;
		}
		if (options.type !== undefined) {
			type = options.type;
		}
	}
	// just dummy function as callback (if no specified gadget returns error)
	if (callback === undefined) {
		callback = function(responeData) {};
	}
	
	console.debug("url = "+url);
	console.debug("method = "+method);
	console.debug("data = "+data);
	console.debug("type = "+type);
	
	var params = {};
	if (method == "POST") {
		params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
		params[gadgets.io.RequestParameters.POST_DATA] = data;
	} else {
		params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.GET;
		if (data) {
			url = OSB.utils.appendUrlParameters(url, data);
		}
	}
	var dataType = gadgets.io.ContentType.TEXT;
	switch (type) {
		case "TEXT": dataType = gadgets.io.ContentType.TEXT; break;
		case "JSON": dataType = gadgets.io.ContentType.JSON; break;
	}
	params[gadgets.io.RequestParameters.CONTENT_TYPE] = dataType;
	gadgets.io.makeRequest(url, function(responeData) {
		if (OSB.Capabilities.container == "myspace" && responeData && (responeData.errorMessage || responeData.errorCode)) {
			responeData.errors = [responeData.errorCode + ": " + responeData.errorMessage];
		}
		callback(responeData);
	}, params);
};

/**
 * OSB.specificTags object responsible for parsing specific tags.
 * Its public method parse() takes one argument that is jQuery Context object and
 * and seeks that object for specific tags. After finding all specific tags it
 * parses them and replaces them with new jQuery objects.
 * Please note, as a result of IE's incorrect namespace rendering all specifig tags
 * must be closed (i.e.: <pm:button ... ></pm:button>). empty elements will cause
 * problems - i.e: <pm:button ... /> will cause problems in rendering. So be sure
 * to use addEnclosingTag(html) method to automatically enclose all empty tags.
 */
OSB.SpecificTags = (function() {
	var invitationControllerTags = [
		'multi_friend_selector',
		'condensed_invitation',
		'small_invitation',
		'full_invitation',
		'button'
	];

	function switchTagName(parsedTagElement, tagName, attributes) {
		// Check if the tag is <fb:multi_friend_selector condensed="true"/>
		// And if it is, switch its tagName to condensed_invitation.
		if (tagName == "multi_friend_selector" &&	attributes.condensed &&	OSB.utils.bool(attributes.condensed)) {
			tagName = "condensed_invitation";
		}
		// Check if there are Invitation Controllers that does not fit container
		// capabilities. i.e.: Containers (like Hi5) with capability 
		// requestShareApp = "nativeController". These containers have their owner
		// UI invitation controllers. Hence all OSB invitation controllers should
		// be converted to simple links which will open native controller.
		if (OSB.Capabilities.getCapability("requestShareApp") == "nativeController" && jQuery.inArray(tagName, invitationControllerTags) != -1) {
			// Check if the tag inside <fb:request-form>, we have previously
			// converted it to <form class="fb_request_form">
			// Also check invite attribute of <fb:request-form>, we have previously
			// converted it to jQuery data.
			var $fb_request_form = $(parsedTagElement).parents(".fb_request_form");
			if ($fb_request_form.length && $fb_request_form.data("invite")) {
				tagName = "native_controller";
			} else if ("invitation_or_comment" in attributes && (attributes.invitation_or_comment == "invitation" || attributes.invitation_or_comment == "dynamic")) {
				tagName = "native_controller";
			}
		}
		return tagName;
	}

	/*
	 * Defined specific tags and their attributes with default values.
	 * Attributes which not specified here will be skipped.
	 * Tags not defined here, will not be parsed. So to enable tag parsing
	 * without any attributes just include its name with empty object.
	 */
	var DefaultAttributes = function() {
		// Common attributes from invitation/comment controllers
		this.contollersCommon = {
			invitation_or_comment:	"invitation",
			inv_content:			"",
			cmt_content:			"",
			exclude_ids:			[],
			server:					"",
			load_response_page:		false,
			additional_form_params:	[],
			message_type:			"public",
			message_title:			"",
			filter:					""
		};
		// http://wiki.developers.facebook.com/index.php/Fb:request-form
		this.request_form = {
			content:			"",
			action:				"",
			method:				"POST",
			type:				"",
			invite:				false
		};
		this.button = {
			pop_up:				false,
			type:				"all",
			num_of_random:		3,
			specific_id:		null
		};
		this.full_invitation = {
			pop_up:				false,
			num_of_rows:		3,
			friends_per_row:	6,
			controller_title:	"Select Friends.",
			button_text:		"Send Invitation"
		};
		this.small_invitation = {
			pop_up:				false,
			num_of_random:		3
		};
		this.condensed_invitation = {
			mode:				"2part",
			unselected_rows:	8,
			selected_rows:		4,
			button_text:		"Send Invitation",
			num_of_preselected:	0,
			max:				null
		};
		this.multi_friend_selector = {
			rows:				3,
			cols:				6,			
			actiontext:			"Select Friends.",
			button_text:		"Send Invitation",
			condensed:			false,
			max:				null
		};
		this.native_controller = {
			inv_content:			"",				
			message_title:			""
		};
		this.name = {
			uid:				null,
			linked:				true,
			useyou:				true,
			reflexive:			false
		};
		this.profile_pic = {
			uid:				null,
			linked:				true,
			width:				"auto",
			height:				"auto"
		};
		this.iframe = {
			src:				null,
			frameborder:		0,		// (1|0)
			scrolling:			"auto",	// (yes|no|auto)
			style:				"",
			width:				"",
			height:				"",
			name:				""
		};
		this.application_name = {};
		this.swf = {
			src:				"",	height:				"",	width:				"",
			id:					"",	name:				"",	play:				"",
			loop:				"",	menu:				"",	quality:			"",
			scale:				"",	align:				"",	salign:				"",
			wmode:				"",	bgcolor:			"",	flashvars:			"",
			allowscriptaccess:	"",	allowfullscreen:	"", swfsrc:				""
		};
		this.activity = {
			priority:			"low",
			title:				""
		};
		this.notification = {
			recipients:			null, content:			null, media_items:			null,
			button0_surface:	null, button0_label:	null, button0_params:		null,
			button1_surface:	null, button1_label:	null, button1_params:		null,
			jscallbackfunction: null
		};
		this.js_string = {
			"var":				null
		};
	};
	
	/**
	 * Returns merge of default and passed attributes for specific tag.
	 * All the default attributes for each specific tag defined in DefaultAttributes
	 * object. Passed attributes must be one of the defined attributes.
	 * @param {String} tagName The name of specific tag for which to get attributes.
	 * @param {Object} newAttributes Attributes to overwrite the default ones.
	 * @return {Object} Merged attributes
	 * @private
	 */
	function getAttributes(tagName, newAttributes) {
		// Check that supplied tagName is defined
		console.debug("getAttributes(tagName = ",tagName,", tagAttributes = ",newAttributes,")");
		var DefaultAttributesObj = new DefaultAttributes();
		if (DefaultAttributesObj[tagName] === undefined) {
			console.info("OSB info: There is no such tag '"+tagName+"'!");
			return null;
		}
				
		// Create object with suitable default attributes
		var tagAttributes = DefaultAttributesObj[tagName];
		var attr;
		
		// Add invitation/comment controller common default attribures
		if (jQuery.inArray(tagName, invitationControllerTags) != -1) {
			for (attr in DefaultAttributesObj.contollersCommon) {
				if (DefaultAttributesObj.contollersCommon.hasOwnProperty(attr)) {
					tagAttributes[attr] = DefaultAttributesObj.contollersCommon[attr];
				}
			}
		}
		console.debug("default tagAttributes = ",tagAttributes);

		function setAttribute(attr) {
			tagAttributes[attr] = newAttributes[attr];		
		}
		
		// Change default attributes according to supplied attributes (newAttributes)
		for (attr in newAttributes) {
			if (attr in tagAttributes) {
				setAttribute(attr);
			} else {
				// console.warn("Specific tag '"+tagName+"' does not support '"+attr+" attribute!");
			}
		}
		
		console.debug("Final tagAttributes = ",tagAttributes);
		return tagAttributes;
	}

	function specificTagFactory(tagName, attributes, parsedTagElement) {
		console.debug("specificTagFactory("+tagName+",",attributes,")");
		
		try {
			attributes = getAttributes(tagName, attributes);
			if (attributes === null) {
				return null;
			}
			return eval(tagName).call(parsedTagElement, attributes);
		} catch (e) {
			console.error("OSB Error: Occured in specificTagFactory(): " + e);
			return null;
		}
	}

	function parse(jQueryContext) {
		console.group("OSB.specificTags.parse(jQueryContext)");
		
		var userAgent = navigator.userAgent;
		var isOpera = /Opera/.test(userAgent);
		
		// Get all specifig tags, i.e.: tags that starts with namespace prefix "pm" or "fb".
		var $specificTags = $("*",jQueryContext).filter(function() {
			// in IE the tagName is not "pm:tagName" but just "tagName"!
			var IEtagName = false;
			if (this.outerHTML) {
				var outerHtml = this.outerHTML;
				// IE appends some tag with defined xml namespace
				IEtagName = /^<\?xml:namespace[\s\S]*?\><(?:pm|fb):[\w\-]+/i.test(outerHtml);
				//var IEtagName = /\<\/pm:\w+\>$/i.test(outerHtml);
			}
			return (/(?:pm|fb):[\w\-]+/i).test(this.tagName) || IEtagName;
		});
		console.debug("found "+$specificTags.length+" specificTags");
		
		// Process each found specific tag.
		$specificTags.each(function(index) {
			console.group("Processing "+index+"th specific tag.");
			console.debug("this.tagName = "+this.tagName);
			
			// Get the local name of the tag, i.e.: the name of the tag after the "pm:".
			var tagName = /(?:pm|fb):([\w\-]+)/i.exec(this.tagName);
			if (tagName !== null) {
				tagName = tagName[1];
				// Opera renders tag names with underscore as separate attribute:
				// <pm:full_controller ... > renders as <pm:full _controller="" ... />
				// Lucky Opera knows outerHTML! :)
				if (isOpera) {
					var lostPart = /(?:pm|fb):[\w]+ (_\w+)/i.exec(this.outerHTML);
					if (lostPart) {
						tagName += lostPart[1];			
					}
				}
			} else {
				// in IE the tagName is not "pm:tagName" but just "tagName"!
				tagName = this.tagName;
			}
			
			tagName = tagName.toLowerCase();
			tagName = tagName.replace(/-/g,"_");
			console.debug("Local tagName = "+tagName);
			
			// Get tag's attributes.
			var attributes = {};
			var tagAttributes = this.attributes;
			for (var i = 0; i < tagAttributes.length; i++) {
				attributes[tagAttributes[i].nodeName] = tagAttributes[i].nodeValue;
			}
			
			tagName = switchTagName(this, tagName, attributes);
			
			// Finally, get the replace the specific tag with rendered contents.
			var newJQuery = specificTagFactory(tagName, attributes, this);
			if (newJQuery) {
				newJQuery.css("visibility", "visible");
				$(this).replaceWith(newJQuery);
			} else {
				// Just remove the tag.
				$(this).replaceWith(null);
			}
			//replaceSpecificTag($(this),tagName,attributes);
			//$(this).replaceWith(getHtmlOfSpecificTag(tagName, attributes));
			console.groupEnd();
		});
		
		console.groupEnd();
	}
	
	function createControllerData(attributes, parsedTagElement) {
		console.debug("createControllerData(",attributes,")");
		var PARAMETERS_MAP = {
			// tag attributes:		controllerData Object keys
			invitation_or_comment:	"invitationCommentFlag",
			inv_content:			"content",
			cmt_content:			"content",
			exclude_ids:			"excludeId",
			server:					"serverUrl",
			additional_form_params:	"additionalFormParams",
			load_response_page:		"loadResponsePage",
			message_type:			"messageType",
			message_title:			"messageTitle",
			filter:					"filter",
			max:					"max",
			
			num_of_rows:			"maxFriendsRows",
			rows:					"maxFriendsRows",
			friends_per_row:		"friendsPerRow",
			cols:					"friendsPerRow",
			controller_title:		"controllerTitle",
			actiontext:				"controllerTitle",
			type:					"buttonType",
			num_of_random:			"numOfRandomFriends",
			specific_id:			"specificUserId",
			pop_up:					"popUp",
			
			mode:					"mode",
			unselected_rows:		"unselectedRows",
			selected_rows:			"selectedRows",
			button_text:			"buttonText",
			num_of_preselected:		"preselected"
		};
		var data = {};
		for (var attr in attributes) {
			if (attr in PARAMETERS_MAP) {
				var controllerDataKey = PARAMETERS_MAP[attr];
				var controllerDataValue = attributes[attr];
				// content property can be set using inv_content or cmt_content
				// if one of them already set, then the second one will be ignored.
				if (controllerDataKey == "content" && data[controllerDataKey]) {
					continue;
				}
				data[controllerDataKey] = controllerDataValue;
				// console.debug(attr + " -> " + controllerDataKey+"="+controllerDataValue);
			} else {
				// console.error("The attribute '"+attr+"' is illegal!");
			}
		}
		
		// TODO consider to change to closest(expr), requires jQuery 1.3
		var $fb_request_form = $(parsedTagElement).parents(".fb_request_form");
		if ($fb_request_form.length) {
			console.debug("controller wrapped by fb:request_form, overwriting attributes");
			$fb_request_form = $fb_request_form.eq(0);
			var content = $fb_request_form.data("content");
			var type = $fb_request_form.data("type");
			var invite = $fb_request_form.data("invite");
			if (!invite) {
				// set type of the message - public/private/notification/email
				data.messageType = type;
			}
			data.buttonText = "Send "+type+(invite?" invitation":" message");
			data.messageTitle = "You have a "+type+" "+(invite?" invitation.":" message.");
			data.content = content;
			data.loadResponsePage = true;
			data.serverUrl = $fb_request_form.attr("action");
			data.invitationCommentFlag = invite?"dynamic":"comment";
			// Add input elements inside fb_request_form as additional parameters
			var additionalFormParams = "";
			$fb_request_form.find("input").each(function(i) {
				if (i > 0) {
					additionalFormParams += ",";
				}
				additionalFormParams += $(this).attr("name");
			});
			data.additionalFormParams = additionalFormParams;
		}
		
		console.debug("Final controller data = ",data);
		
		return data;
	}
	
	// -----------------------------------------------
	// ------------ Specific Tag Functions -----------
	// -----------------------------------------------
	
	function request_form(attributes) {
		var resultJQuery = $('<form action="'+attributes.action+'" method="'+attributes.method+'" class="fb_request_form"></form>');
		resultJQuery.append($(this).contents());
		resultJQuery.data("content", attributes.content);
		resultJQuery.data("type", attributes.type);
		resultJQuery.data("invite", OSB.utils.bool(attributes.invite));
		return resultJQuery;
	}
	
	function button(attributes) {
		var data = createControllerData(attributes, this);
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);	
			
		console.debug("Replacing specific tag with button controller.");
		//controllerIndex = OSB.invitationControllerButtons.push(attributes) - 1;
		var resultJQuery = $('<a href="#">' + OSB.INVITATION_LINK_TEXT + '</a>');
		resultJQuery.bind("click", function(event){
			event.preventDefault();
			event.stopPropagation();
			InvitationController.executeButtonController(controllerData);
		});
		return resultJQuery;
	}

	function full_invitation(attributes) {
		var data = createControllerData(attributes, this);
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);			

		console.debug("Replacing specific tag with full_invitation controller.");
		var resultJQuery;
		if (!OSB.utils.bool(attributes.pop_up)) {
			resultJQuery = InvitationController.buildFullInvitationController(controllerData);
		} else {
			//controllerIndex = OSB.invitationControllerButtons.push(attributes) - 1;
			resultJQuery = $('<a href="#">' + OSB.INVITATION_LINK_TEXT + '</a>');
			resultJQuery.bind("click", function(event){
				event.preventDefault();
				event.stopPropagation();
				InvitationController.openFullInvitationController(controllerData);
			});
		}
		return resultJQuery;
	}

	function small_invitation(attributes) {
		var data = createControllerData(attributes, this);
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);
					
		console.debug("Replacing specific tag with small_invitation controller.");
		var resultJQuery;
		if (!OSB.utils.bool(attributes.pop_up)) {
			resultJQuery = InvitationController.buildSmallInvitationController(controllerData);
		} else {
			//controllerIndex = OSB.invitationControllerButtons.push(attributes) - 1;
			resultJQuery = $('<a href="#">' + OSB.INVITATION_LINK_TEXT + '</a>');
			resultJQuery.bind("click", function(event){
				event.preventDefault();
				event.stopPropagation();
				InvitationController.openSmallInvitationController(event, controllerData);
			});
		}
		return resultJQuery;
	}

	function condensed_invitation(attributes) {
		var data = createControllerData(attributes, this);
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);			

		console.debug("Replacing specific tag with condensed_invitation controller.");
		var resultJQuery = InvitationController.buildCondensedInvitationController(controllerData);
		return resultJQuery;
	}
	
	function multi_friend_selector(attributes) {
		var data = createControllerData(attributes, this);
		var InvitationController = new OSB.InvitationController();
		var controllerData = new InvitationController.ControllerData();
		controllerData.set(data);			

		console.debug("Replacing specific tag with multi_friend_selector controller.");
		var resultJQuery = InvitationController.buildMultiFriendSelector(controllerData);
		return resultJQuery;
	}
	
	function native_controller(attributes) {
		console.debug("Replacing specific tag with native controller.");
		var data = createControllerData(attributes, this);
		var resultJQuery = $('<a href="#">' + OSB.INVITATION_LINK_TEXT + '</a>');
		resultJQuery.bind("click", function(event){
			event.preventDefault();
			event.stopPropagation();
//			var VIEWER_FRIENDS = null;
//			if (OSB.OPENSOCIAL_VERSION == 0.7) {
//				VIEWER_FRIENDS = opensocial.DataRequest.Group.VIEWER_FRIENDS;
//			}
//			else if (OSB.OPENSOCIAL_VERSION == 0.8) {
//				var idSpecParams = {};
//				idSpecParams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER;
//				idSpecParams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.FRIENDS;
//				VIEWER_FRIENDS = opensocial.newIdSpec(idSpecParams);
//			}
			var message = opensocial.newMessage(data.content);
			message.setField(opensocial.Message.Field.TITLE, data.messageTitle);
			OSB.requestShareApp("VIEWER_FRIENDS", message, function() {
				//console.debug("requestShareApp responseItem = ",responseItem);
				//sendInfoToServer(selectedIds);
			});
		});		
		return resultJQuery;
	}

	function name(attributes) {
		var uid = attributes.uid;
		if (!uid) { throw "OSB Error: attribute uid required for tag pm:name"; }
		var displayName = "";
		var profileUrl = "";
		var person = OSB.getPerson(uid);
		// Get name and profileUrl from person Object.
		if (!person) { throw "Did not find person with uid = "+uid; }
		displayName = person.getDisplayName();
		profileUrl = person.getField(opensocial.Person.Field.PROFILE_URL);
		// Check for given attributies to format the name.
		if (OSB.viewer.getId() == uid && OSB.utils.bool(attributes.useyou)) {
			displayName = OSB.utils.bool(attributes.reflexive) ? "yourself" : "you";
		}
		var resultJQuery;
		if (OSB.utils.bool(attributes.linked)) {
			resultJQuery = $("<a href=" + profileUrl + ">" + displayName + "</a>");
		} else {
			resultJQuery = $(document.createTextNode(displayName));
		}		
		return resultJQuery;
	}

	function profile_pic(attributes) {
		var uid = attributes.uid;
		if (!uid) { throw "OSB Error: attribute uid required for tag pm:profile_pic"; }
		var name = "";
		var profileUrl = "";
		var thumbnailUrl = "";
		var person = OSB.getPerson(uid);
		if (!person) { throw "Did not find person with uid = "+uid; }
		name = person.getDisplayName();
		profileUrl = person.getField(opensocial.Person.Field.PROFILE_URL);
		thumbnailUrl = person.getField(opensocial.Person.Field.THUMBNAIL_URL);
		var $img = $("<img alt='"+name+"' src='"+thumbnailUrl+"'/>");
		$img.css({
			width:	attributes.width,
			height:	attributes.height,
			border:	"none"
		});
		var resultJQuery;
		if (OSB.utils.bool(attributes.linked)) {
			resultJQuery = $("<a href=" + profileUrl + "></a>").append($img);
		} else {
			resultJQuery = $img;
		}		
		return resultJQuery;
	}
	
	function application_name(attributes) {
		var resultJQuery = $(document.createTextNode(OSB.APP_NAME));
		return resultJQuery;
	}

	function iframe(attributes) {
		if (!attributes.src) { throw "OSB Error: attribute src required for tag pm:iframe"; }
		var resultJQuery = $('<iframe src="'+attributes.src+'" ' +
				'frameborder="'+attributes.frameborder+'" ' +
				'scrolling="'+attributes.scrolling+'" ' +
				(attributes.style ? 'style="'+attributes.style+'" ' : "") +
				(attributes.width ? 'width="'+attributes.width+'" ' : "") +
				(attributes.height ? 'height="'+attributes.height+'" ' : "") +
				(attributes.name ? 'name="'+attributes.name+'" ' : "") +
				'></iframe>');
		return resultJQuery;
	}
	
	function activity(attributes) {
		console.debug("activity(",attributes,")");
		var params = {};
		params[opensocial.Activity.Field.TITLE] = attributes.title;
		var activityObj = opensocial.newActivity(params);
		var priority;
		if (attributes.priority == "low") {
			priority = opensocial.CreateActivityPriority.LOW;
		} else {
			priority = opensocial.CreateActivityPriority.HIGH;
		}
		console.debug("activity = ",activityObj);
		console.debug("priority = ",priority);
		opensocial.requestCreateActivity(activityObj, priority, function(responseItem) {
			console.debug("requestCreateActivity responseItem = ",responseItem);
		});
		return null;
	}
	
	function createFlashObjectHtml(attributes) {
		var objectParams = ["play","loop","menu","quality","scale","salign","wmode",
				"bgcolor","flashvars","allowfullscreen","allowscriptaccess"];
		var embedAttributes = ["src","width","height","name","play","loop","menu",
				"quality","scale","salign","wmode","bgcolor","flashvars",
				"allowfullscreen","allowscriptaccess"];
		
		attributes.flashvars += (attributes.flashvars ? "&":"")+
								"appid="+OSB.APP_ID+
								"&viewerid="+OSB.VIEWER_ID+
								"&ownerid="+OSB.OWNER_ID;
		
		// create <object> tag
		var objectTag = '<object '+
				(attributes.width ? 'width="'+attributes.width+'" ' : '')+
				(attributes.height ? 'height="'+attributes.height+'" ' : '')+
				(attributes.id ? 'id="'+attributes.id+'" ' : '')+
				(attributes.align ? 'align="'+attributes.align+'" ' : '')+
				'classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" '+
				'codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0">';
		
		// create <param> tags
		var paramTags = '<param name="movie" value="'+attributes.src+'"></param>';
		jQuery.each(objectParams, function(i, value) {
			if (attributes[value]) {
				paramTags += '<param name="'+value+'" value="'+attributes[value]+'"></param>';
			}
		});		

		// Create <embed> tag
		var embedTag = "<embed ";
		jQuery.each(embedAttributes, function(i, value) {
			if (attributes[value]) {
				embedTag += value+'="'+attributes[value]+'" ';
			}
		});
		embedTag += 'pluginspage="http://www.macromedia.com/go/getflashplayer" '+
					'type="application/x-shockwave-flash"></embed>';
		
		return objectTag+paramTags+embedTag+"</object>";
	}
	
	/**
	 * swf - Shockwave Flash object.
	 * @param {Object} attributes defined
	 */
	function swf(attributes) {
		console.debug("creating swf tag...");
		if (!attributes.src && !attributes.swfsrc) { throw "attribute src required for tag pm:swf"; }
		if (attributes.swfsrc) {
			attributes.src = attributes.swfsrc;
		}
		
		var flashObjectHtml = createFlashObjectHtml(attributes);
		return $(flashObjectHtml);
	}
	
	function notification(attributes) {
		var notificationObj = {
			content:		attributes.content,
			mediaItems:		attributes.media_items
		};
		if (attributes.button0_surface && attributes.button0_label) {
			notificationObj.buttons = [{
				surface:	attributes.button0_surface,
				label:		attributes.button0_label,
				params:		eval("("+attributes.button0_params+")")
			}];
			if (attributes.button1_surface && attributes.button1_label) {
				notificationObj.buttons.push({
					surface:	attributes.button1_surface,
					label:		attributes.button1_label,
					params:		eval("("+attributes.button1_params+")")
				});
			}
		}
		OSB.requestCreateNotification(
				attributes.recipients,
				notificationObj,
				eval("("+attributes.jscallbackfunction+")")
			);
		return null;
	}
	
	function js_string(attributes) {
		if (!attributes["var"]) {
			throw "attribute var required for fb:js-string";
		}
		var jsVar = attributes["var"];
		window[jsVar] = $(this);
		console.debug("window["+jsVar+"]="+window[jsVar]);
		return null;
	}
	
	return {
		parse:					parse,
		getAttributes:			getAttributes,
		createFlashObjectHtml:	createFlashObjectHtml
	};
})();

/**
 * This objects fetches friends data from Opensocial server and populates 
 * OSB.friendsData array with this data. If callback function supplied it will be invoked
 * when all data is set.
 * @param {function} callback
 */
OSB.getOpensocialData = function(callback) {
	console.debug("getOpensocialData(callback)");
	
	function addFriendsToDataRequest(dataRequest, offset, numOfFetchedFriends) {
		console.debug("OSB.getOpensocialData.addFriendsToDataRequest(",dataRequest,",",offset,",",numOfFetchedFriends,")");
		if (offset === undefined) {
			offset = 0;
		}
		if (OSB.Capabilities.container == "myspace") {
			offset++;
		}
		if (numOfFetchedFriends === undefined) {
			numOfFetchedFriends = 500;
		}
		var VIEWER_FRIENDS;
		if (OSB.OPENSOCIAL_VERSION == 0.7) {
			VIEWER_FRIENDS = opensocial.DataRequest.Group.VIEWER_FRIENDS;
		} else if (OSB.OPENSOCIAL_VERSION == 0.8) {
			var idSpecParams = {};
			idSpecParams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER;
			idSpecParams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.FRIENDS;
			VIEWER_FRIENDS = opensocial.newIdSpec(idSpecParams);
		}
		console.debug("VIEWER_FRIENDS = ",VIEWER_FRIENDS);
		
		// Optional parameter PROFILE_DETAILS not implemented for viewer friends in MySpace
		var friends_opt_params = {};
		friends_opt_params[opensocial.DataRequest.PeopleRequestFields.FIRST] = offset;
		friends_opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = numOfFetchedFriends;
		var friendRequest = dataRequest.newFetchPeopleRequest(VIEWER_FRIENDS, friends_opt_params);
		
		// Must supply the second parameter in the add method, otherwise Hi5 does not fetches friends.
		dataRequest.add(friendRequest, "viewerFriends");
	}
	
	function processFreindsData(dataResponse) {
		var friendsResponseItem = dataResponse.get("viewerFriends");
		console.debug("friendsResponseItem:", friendsResponseItem);
		if(!friendsResponseItem.hadError()) {
			var friends = friendsResponseItem.getData();
			var numOfFetchedFriends = friends.size();
			var numOfTotalFriends = friends.getTotalSize();
			var offsetFromTotalFriends = friends.getOffset();
			if (OSB.Capabilities.container == "myspace") {
				offsetFromTotalFriends--;
			}
			var friendsArray = friends.asArray();
			console.debug("number of total friends before pagination - ",numOfTotalFriends);
			console.debug("number of fetched friends - ",numOfFetchedFriends);
			console.debug("offset from total friends collection - ",offsetFromTotalFriends);
			console.debug("friends opensocial.Collection - ",{'friendsCollection':friends});
			console.debug("friends array - ",{'friendsArray':friendsArray});
			OSB.viewerFriends = OSB.viewerFriends.concat(friendsArray);
			friends.each(
				function(friendData) {
					var friendName = friendData.getDisplayName();
					var friendId = friendData.getField(opensocial.Person.Field.ID);
					var friendThumbnailUrl = friendData.getField(opensocial.Person.Field.THUMBNAIL_URL);
					var hasApp = friendData.getField(opensocial.Person.Field.HAS_APP);
					//var friendGender = friendData.getField(opensocial.Person.Field.GENDER);
					//console.debug("friendGender = ",friendGender);
					
					OSB.friendsData.push({
						friendName:		friendName,
						friendId:		friendId,
						thumbnailUrl:	friendThumbnailUrl,
						hasApp:			hasApp,
						selected:		false
					});
				}
			);
			
			// If there is more friends to fetchm fetch them
			if (offsetFromTotalFriends + numOfFetchedFriends < numOfTotalFriends) {
				var dataRequest = opensocial.newDataRequest();
				addFriendsToDataRequest(dataRequest, offsetFromTotalFriends + numOfFetchedFriends, numOfFetchedFriends);
				console.debug("Sending dataRequest...");
				dataRequest.send(processFreindsData);
			} else {
				// imitate getById opensocial.Collection function
				OSB.viewerFriends.getById = function(uid) {
					for (var i = 0; i < this.length; i++) {
						var person = this[i];
						if (person.getId() == uid) {
							return person;
						}
					}
					return null;
				};
				
				console.debug("OSB.viewerFriends: ",{'viewerFriends':OSB.viewerFriends});
				console.debug("OSB.friendsData: ",{'friendsData':OSB.friendsData});
				
				// Invoke callback function
				// ------------------------
				if (callback !== undefined) {
					callback(0);
				}		
			}
		} else {
			console.error("OSB - Error when getting viewerFriends data from OpenSocial:\n"+
				"Error message: "+friendsResponseItem.getErrorMessage()+"\n"+
				"Error code: "+friendsResponseItem.getErrorCode());
			if (callback !== undefined) {
				callback({
					errorContext:	'while getting friends data',
					errorCode:		friendsResponseItem.getErrorCode(),
					errorMessage:	friendsResponseItem.getErrorMessage()
				});
			}
		}
	}
	
	/**
	 * Initialize received friends data.
	 * @param {opensocial.DataResponse} dataResponse This object contains the requested
	 * server data mapped to the requested keys.
	 */
	function initFriendsData(dataResponse) {
		console.debug("OSB.getOpensocialData.initFriendsData(",dataResponse,")");
		
		if (dataResponse.hadError()) {
			console.debug("OSB Error in Opensocial dataResponse.getErrorMessage() - " + dataResponse.getErrorMessage());
		}
		
		// Get Owner
		// ---------
		var ownerResponseItem = dataResponse.get("owner");
		console.debug("ownerResponseItem:", ownerResponseItem);
		if(!ownerResponseItem.hadError()) {
			OSB.owner = ownerResponseItem.getData();
			console.debug("OSB.owner = ", OSB.owner);
		}
		else {
			console.error("OSB - Error when getting owner data from OpenSocial:\n"+
				"Error message: "+ownerResponseItem.getErrorMessage()+"\n"+
				"Error code: "+ownerResponseItem.getErrorCode());
			if (callback !== undefined) {
				callback({
					errorContext:	'while getting owner data',
					errorCode:		ownerResponseItem.getErrorCode(),
					errorMessage:	ownerResponseItem.getErrorMessage()
				});
				return;
			}
		}
		
		// Get Viewer
		// ----------
		var viewerResponseItem = dataResponse.get("viewer");
		console.debug("viewerResponseItem:", viewerResponseItem);
		if(!viewerResponseItem.hadError()) {
			OSB.viewer = viewerResponseItem.getData();
			console.debug("OSB.viewer = ", OSB.viewer);
			
			OSB.viewer.osbGetField = OSB.Person.getField;
		} else {
			console.error("OSB - Error when getting viewer data from OpenSocial:\n"+
				"Error message: "+viewerResponseItem.getErrorMessage()+"\n"+
				"Error code: "+viewerResponseItem.getErrorCode());
			if (callback !== undefined) {
				callback({
					errorContext:	'while getting viewer data',
					errorCode:		viewerResponseItem.getErrorCode(),
					errorMessage:	viewerResponseItem.getErrorMessage()
				});
				return;
			}
		}		
		
		// Debug viewer's PROFILE_DETAILS
		// ------------------------------
		var viewerGender = OSB.viewer.osbGetField("gender");
		console.debug("viewerGender = ",viewerGender);
		var viewerCountry = OSB.viewer.osbGetField("country");
		console.debug("viewerCountry = ",viewerCountry);
		var viewerAge = OSB.viewer.getField(opensocial.Person.Field.AGE);
		console.debug("viewerAge = ",viewerAge);
		
		// Get Viewer Friends
		// ------------------
		processFreindsData(dataResponse);
	}
	
	/*
	 * Get VIEWER's friends data from server
	 */
	function sendOpensocialRequest() {
		console.debug("sendOpensocialRequest()");
		// var container = opensocial.Container.get();
		var dataRequest = opensocial.newDataRequest();
		var opt_params = {};
		// desirableParams for viewer/owner PROFILE_DETAILS, not all of them may be supported!
		var desirableParams = [
			opensocial.Person.Field.AGE,
			opensocial.Person.Field.GENDER,
			opensocial.Person.Field.CURRENT_LOCATION,
			opensocial.Person.Field.ADDRESSES,
			opensocial.Person.Field.NAME,
			opensocial.Person.Field.HAS_APP
		];
		opt_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS] = [];
		// filter only supported fields from the desirableParams
		for (var i = 0; i < desirableParams.length; i++) {
			if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON,desirableParams[i])) {
				opt_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS].push(desirableParams[i]);
			} else {
				console.info("OSB info: Person.Field - "+desirableParams[i]+" is not supported");
			}
		}
		console.debug("opt_params = ",opt_params);
		
		// Create request object according to the opensocial version
		// ---------------------------------------------------------
		var OWNER;
		var VIEWER;
		var FRIENDS;
		if (OSB.OPENSOCIAL_VERSION == 0.7) {
			OWNER = opensocial.DataRequest.PersonId.OWNER;
			VIEWER = opensocial.DataRequest.PersonId.VIEWER;
		} else if (OSB.OPENSOCIAL_VERSION == 0.8) {
			OWNER = opensocial.IdSpec.PersonId.OWNER;
			VIEWER = opensocial.IdSpec.PersonId.VIEWER;
			FRIENDS = opensocial.IdSpec.GroupId.FRIENDS;
			var USER_ID = opensocial.IdSpec.Field.USER_ID;
			var GROUP_ID = opensocial.IdSpec.Field.GROUP_ID;
			console.debug("USER_ID = ",USER_ID);
			console.debug("GROUP_ID = ",GROUP_ID);
			console.debug("FRIENDS = ",FRIENDS);
		}
		console.debug("OWNER = ",OWNER);
		console.debug("VIEWER = ",VIEWER);
		var ownerRequest = dataRequest.newFetchPersonRequest(OWNER,opt_params);
		var viewerRequest = dataRequest.newFetchPersonRequest(VIEWER,opt_params);
		
		addFriendsToDataRequest(dataRequest);
		
		dataRequest.add(ownerRequest,"owner");
		dataRequest.add(viewerRequest,"viewer");
		console.debug("Sending dataRequest...");
		dataRequest.send(initFriendsData);
	}
	
	sendOpensocialRequest();
};

/**
 * OSB Person object wraps opensocial.Person object and allows simple access to
 * Persons fields which have implementations differencies across different
 * containers.
 */
OSB.Person = (function() {
	/**
	 * OSB.Person.getField method wraps opensocial.Person.getField and gives
	 * simple acess to persons fields despite inplementation differencies across
	 * different containers.
	 * @param {String} fieldName
	 */
	function getField(fieldName) {
		if (OSB.OPENSOCIAL_VERSION == 0.7) {
			switch (fieldName) {
				case "country":
					if (OSB.Capabilities.container == "myspace") {
						// The Myspace way
						return this.getField(MyOpenSpace.Person.Field.COUNTRY);
					} else {
						// The regular - right way
						return this.getField(opensocial.Person.Field.ADDRESSES)[0].getField(opensocial.Address.Field.COUNTRY);
					}
					break;
				case "gender":
					if (OSB.Capabilities.container == "myspace") {
						// The Myspace way
						return this.getField(opensocial.Person.Field.GENDER);
					} else {
						// The regular - right way
						return this.getField(opensocial.Person.Field.GENDER).getDisplayValue();
					}
					break;
			}
		} else if (OSB.OPENSOCIAL_VERSION == 0.8) {
			switch (fieldName) {
				case "country":
					if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON, opensocial.Person.Field.CURRENT_LOCATION)) {
						return this.getField(opensocial.Person.Field.CURRENT_LOCATION).getField(opensocial.Address.Field.COUNTRY);
					} else {
						return "";
					}
					break;
				case "gender":
					if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON, opensocial.Person.Field.GENDER)) {
						return this.getField(opensocial.Person.Field.GENDER).getDisplayValue();
					} else {
						return "";
					}
					break;
			}
		}
	}
	
	return {getField:	getField};
})();

/** 
 * HTML templates of the Invitation Controllers.
 * @TODO - migrate all invitation controllers to this Object.
 */
OSB.templates = (function() {
	
	var callback = null;
	var templates = {
		$condensedController: null
	};

	function processResponse(responeData) {
		console.debug("OSB.templates.processResponse(",responeData,")");

		var responseHtml = responeData.text;

		var bodyInnerHtml = /<body[^\>]*\>([\s\S]*?)<\/body\>/.exec(responseHtml);
		if (bodyInnerHtml) {
			console.debug("Got bodyOuterHtml");
			bodyInnerHtml  = bodyInnerHtml[1];
			var $container = $("<div>"+bodyInnerHtml+"</div>");
			
			templates.$condensedController = $(".condensedInvitationController" ,$container);
			templates.$condensedController.find("ul").empty();
			templates.$fullInvitationController = $(".fullInvitationController", $container);
			templates.$fullInvitationController.find(".friends").empty();
			templates.$smallInvitationController = $(".smallInvitationController", $container);
			templates.$multiFriendSelector = $(".multiFriendSelector", $container);
			templates.$multiFriendSelector.find(".friends").empty();
		} else {
			console.error("OSB Error: Can't extract templates body inner html");
		}
		
		if (callback !== null) {
			if (responeData.errors && responeData.errors.length) {
				callback({
					errorContext:	'while getting templates',
					errorCode:		'NA',
					errorMessage:	responeData.errors.join(', ')
				});
			} else {
				callback(0);
			}
		}
	}

	function loadTemplates(_callback) {
		if (_callback !== undefined) {
			callback = _callback;
		}
		console.debug("OSB.templates.loadTemplates()");
		OSB.sendRequestToServer(OSB.TEMPLATES_URL, processResponse);
	}
	
	function getTemplate(template) {
		// Clone the object so the original one won't be modified.
		return templates["$"+template].clone();
	}
	
	return {
		loadTemplates:	loadTemplates,
		getTemplate:	getTemplate
	};
})();


/**
 * The opensocial.requestShareApp(recipients, message, opt_callback) wrapper.
 * Takes care about implementation differencies in containers.
 * See opensocial.requestShareApp for parameters reference.
 * @param {Array.<String>, String} recipients
 * @param {opensocial.Message} message
 * @param {Function} opt_callback
 */
OSB.requestShareApp = function(recipients, message, opt_callback, index) {
	console.debug("OSB.requestShareApp(",recipients,",",message,",opt_callback,"+index+")");
	switch (OSB.Capabilities.getCapability("requestShareApp")) {
		case "singleId":			// MySpace implementation
			if (index === undefined) {
				index = 0;
				console.debug("executing requestShareApp - singleId implementation");
			}
			if (index < recipients.length) {
				var recipientUserId = recipients[index];
				console.debug("recipientUserId = "+recipientUserId);
				opensocial.requestShareApp(recipientUserId, message, function(responseItem) {
					console.debug("responseItem = ",responseItem);
					// In MySpace if (responseItem == 0) then message/invitation sending
					// was cancelled by the user when prompted. And (responseItem == 1) if
					// the message/invitation was sent.
					// In opensocial-0.8 MySpace responseItem has it's hadError(). And when no error the responseItem.getData()
					// may return 0 or 1 to specify that the send action was canceled or sent respectively.
					// In Orkut responseItem has it's hadError() and getErrorCode()
					if (responseItem === 0 ||	// MySpace opensocial-0.7
							(responseItem.hadError && !responseItem.hadError() && responseItem.getData() === 0) || // MySpace opensocial-0.8
							(responseItem.hadError && responseItem.hadError()) ) { // Orkut opensocial-0.7
						if (responseItem === 0 || (!responseItem.hadError() && responseItem.getData() === 0)) {
							console.debug("Got responseItem == 0, the sending was cancelled");
						} else {
							console.warn("Error occured in requestShareApp: "+responseItem.getErrorCode());
						}
						recipients.splice(index, 1);
					} else {
						index++;
					}
					OSB.requestShareApp(recipients, message, opt_callback, index);
				});
			} else {
				// after all messages have been sent, send info to the server
				if (opt_callback) {
					opt_callback(recipients);
				}
			}
			break;
		case "nativeController":	// Hi5 implementation
			console.debug("executing requestShareApp - nativeController implementation");
//			var VIEWER_FRIENDS = null;
//			if (OSB.OPENSOCIAL_VERSION == 0.7) {
//				VIEWER_FRIENDS = opensocial.DataRequest.Group.VIEWER_FRIENDS;
//			}
//			else if (OSB.OPENSOCIAL_VERSION == 0.8) {
//				var idSpecParams = {};
//				idSpecParams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER;
//				idSpecParams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.FRIENDS;
//				VIEWER_FRIENDS = opensocial.newIdSpec(idSpecParams);
//			}
//			if (recipients != VIEWER_FRIENDS) {
//				console.warn("OSB Warn: This container does not supports custom invitation " + 
//						"controllers with defined friends ids. " +
//						"It supports sending invitation to VIEWER_FRIENDS with native " + 
//						"controller only.");
//				recipients = VIEWER_FRIENDS;
//			}
			opensocial.requestShareApp(recipients, message, function(responseItem) {
				console.debug("requestShareApp responseItem = ",responseItem);
				if (opt_callback) {
					opt_callback(recipients);
				}
			});
			break;
		default:
			console.debug("executing requestShareApp - default implementation");
			opensocial.requestShareApp(recipients, message, function(responseItem) {
				// Check to what users the invitation was sent and alter recipients
				// array accordingly.
				console.debug("requestShareApp responseItem = ",responseItem);
				// MySpace opensocial-0.8 implementation
				if (responseItem.responseValues && responseItem.responseValues.success) {
					recipients = responseItem.responseValues.success;
					console.debug("final recipients = ",recipients);
				}
				if (opt_callback) {
					opt_callback(recipients);
				}
			});
			break;
	}
};

/**
 * The opensocial.requestSendMessage(recipients, message, opt_callback) wrapper.
 * Takes care about implementation differencies in containers.
 * See opensocial.requestSendMessage for parameter reference.
 * @param {Array.<String>, String} recipients
 * @param {opensocial.Message} message
 * @param {Function} opt_callback
 */
OSB.requestSendMessage = function(recipients, message, opt_callback, index) {
	console.debug("OSB.requestSendMessage(",recipients,",",message,",opt_callback,"+index+")");
	switch (OSB.Capabilities.getCapability("requestSendMessage")) {
		case "singleId":			// MySpace implementation
			if (index === undefined) {
				index = 0;
				console.debug("executing requestSendMessage - singleId implementation");
			}
			if (index < recipients.length) {
				var recipientUserId = recipients[index];
				console.debug("recipientUserId = "+recipientUserId);
				opensocial.requestSendMessage(recipientUserId, message, function(responseItem) {
					console.debug("responseItem = ",responseItem);
					// In MySpace if (responseItem == 0) then message/invitation sending
					// was cancelled by the user when prompted. And (responseItem == 1) if
					// the message/invitation was sent.
					// In opensocial-0.8 MySpace responseItem has it's hadError(). And when no error the responseItem.getData()
					// may return 0 or 1 to specify that the send action was canceled or sent respectively.
					// In Orkut responseItem has it's hadError() and getErrorCode()
					if (responseItem === 0 ||	// MySpace opensocial-0.7
							(responseItem.hadError && !responseItem.hadError() && responseItem.getData() === 0) || // MySpace opensocial-0.8
							(responseItem.hadError && responseItem.hadError()) ) { // Orkut opensocial-0.7
						if (responseItem === 0 || (!responseItem.hadError() && responseItem.getData() === 0)) {
							console.debug("Got responseItem == 0, the sending was cancelled");
						} else {
							console.warn("Error occured in requestSendMessage: "+responseItem.getErrorCode());
						}
						recipients.splice(index, 1);
					} else {
						index++;
					}
					OSB.requestSendMessage(recipients, message, opt_callback, index);
				});
			} else {
				// after all messages have been sent, send info to the server
				if (opt_callback) {
					opt_callback(recipients);
				}
			}
			break;
		default:
			console.debug("executing requestSendMessage - default implementation");
			opensocial.requestSendMessage(recipients, message, function(responseItem) {
				console.debug("responseItem = ",responseItem);
				if (opt_callback) {
					opt_callback(recipients);
				}
			});
			break;
	}
};

/**
 * MyOpenSpace.requestCreateNotification wrapper.
 * @param {String} recipients Comma separated recipients ids.
 * @param {Object} notification Object that holds notification data.
 * @param {Function} callback Function to be executed when the notification
 * request sent. This function passed opensocial.ResponseItem parameter.
 */
OSB.requestCreateNotification = function(recipients, notification, callback) {
	console.group("OSB.requestCreateNotification(",recipients,",",notification,",callback)");
	try {
		if (OSB.Capabilities.container == "myspace") {
			if (typeof recipients == "string") {
				recipients = recipients.split(/\s*,\s*/);
			}
			var param = {};
			if (!notification.content) {
				throw "notification content must be supplied!";
			}
			param[MyOpenSpace.Notification.Field.BODY] = notification.content;
			if (notification.buttons && notification.buttons.length) {
				var buttonsArray = [];
				$.each(notification.buttons, function(index, buttonObject) {
					if (!buttonObject.surface) {
						throw "button surface must be supplied!";
					} else if (buttonObject.surface != "canvas" && buttonObject.surface != "appProfile") {
						throw "illegal button surface: " + buttonObject.surface;
					}
					if (!buttonObject.label || buttonObject.label == "") {
						throw "button label must be supplied";
					}
					var buttonUrlType = "";
					if (buttonObject.surface == "canvas") {
						buttonUrlType = MyOpenSpace.NotificationButton.UrlTypes.CANVAS;
					} else if (buttonObject.surface == "appProfile") {
						buttonUrlType = MyOpenSpace.NotificationButton.UrlTypes.APP_PROFILE;
					}
					var buttonParams = null;
					if (buttonObject.params) {
						buttonParams = buttonObject.params;
					}
					var button = MyOpenSpace.newNotificationButton(
						buttonUrlType,
						buttonObject.label,
						buttonParams);
					buttonsArray.push(button);
				});
				param[MyOpenSpace.Notification.Field.BUTTONS] = buttonsArray;
			}
			if (notification.mediaItems && notification.mediaItems == "profileImage") {
				var mediaItemArray = [opensocial.newMediaItem("", MyOpenSpace.MediaItemHelper.PROFILE_PICTURE)];
				param[MyOpenSpace.Notification.Field.MEDIA_ITEMS] = mediaItemArray;
			}
			var MOSNotification = MyOpenSpace.newNotification(param);
			console.debug("MyOpenSpace notification:",MOSNotification);
			console.groupEnd();
			MyOpenSpace.requestCreateNotification(recipients, MOSNotification, callback);
		} else {
			throw "this container does not supports app-to-user notifications";
		}
	} catch (e) {
		console.error("Error in OSB.requestCreateNotification: " + e);
		console.groupEnd();
	}
};

OSB.getNotificationParams = function() {
	if (OSB.Capabilities.container == "myspace") {
		return gadgets.views.getParams();
	}
	return false;
};

/**
 * Prepares opensocial Message by substituting {input_name} with values of input elements
 * whose name attributes are input_name.
 * @param {opensocial.Message} message The opensocial Message object.
 */
OSB.prepareMessage = function(message) {
	console.debug("OSB.prepareMessage(message)");
	
	function parameterReplacer(matchedString, paramName) {
		console.debug("paramName = "+paramName);
		if (paramName == "sender_name") {
			return OSB.owner.getDisplayName();
		}
		if (paramName == "os_app_id") {
			return OSB.APP_ID;
		}
		var $formElement = $("[name="+paramName+"]");
		if ($formElement.length > 0) {
			var paramValue = "";
			var tagName = $formElement.get(0).tagName.toLowerCase();
			if (tagName == "input") {
				var type = $formElement.attr("type");
				switch (type) {
					case "text":
					case "hidden":
						paramValue = $formElement.val();
						break;
					case "checkbox":
					case "radio":
						paramValue = $formElement.filter(":checked").val();
						break;
				}
			}
			if (tagName == "textarea" || tagName == "select") {
				paramValue = $formElement.val();
			}
			console.debug("paramValue = "+paramValue);
			return paramValue;			
		}
		console.debug("Did not find form input element with name = "+paramName);
		return matchedString;		
	}
	
	var messageBody = message.getField(opensocial.Message.Field.BODY);
	// var messageTitle = message.getField(opensocial.Message.Field.TITLE);
	
	var parameterRegExp = /\{([^\}]+)\}/g;	
	messageBody = messageBody.replace(parameterRegExp, parameterReplacer);
	// messageTitle = messageTitle.replace(parameterRegExp, parameterReplacer);
	
	message.setField(opensocial.Message.Field.BODY, messageBody);
	// message.setField(opensocial.Message.Field.TITLE, messageTitle);
};

/**
 * Gets friend id and returns opensocial Person object. This done by searching for id
 * in already fetched and saved data. The friend must be viewer's or owner's friend.
 * @param {String} uid Friend Id, or strings "viewer"|"owner".
 * @return opensocial.Person
 */
OSB.getPerson = function(uid) {
	console.debug("OSB.getPerson("+uid+")");
	// prepend "myspace.com:" to the person uid.
	if (OSB.Capabilities.container == "myspace") {
		// check that uid has only digits.
		if (/^\d+$/.test(uid)) {
			uid = "myspace.com:"+uid;
		}
	}
	var person = null;
	// Check for owner uid or "owner" string
	if (OSB.owner.getId() == uid || uid == "owner") {
		person = OSB.owner;
	}
	// Check for viewer uid or "viewer" string
	if (!person && OSB.viewer.getId() == uid || uid == "viewer") {
		person = OSB.viewer;
	}
	// Check for already fetched viewers friends.
	if (!person) {
		person = OSB.viewerFriends.getById(uid);
	}
	return person;
};

OSB.utils = {
	bool: function(str) {
		return String(str) == "true" ? true : false;
	},
	appendUrlParameters: function(url, parameters) {
		if (!parameters) {
			return url;
		}
		return url + (/\?/.test(url) ? "&" : "?") + parameters;
	}
};
