ds/DsClient.js

/**
 * @fileoverview Client layer for DS. The DS stack will roughly consist of
 *               Layer 1: GUI, Layer 2: Client, Layer 3: Server
 *
 * NOTE: All public/exported functions in this module should implement runtime
 *       type checking for arguments to allow easier debugging when calling
 *       from uncompiled code.
 *
 * @author anders.rejdebrant@spark-vision.com (Anders Rejdebrant)
 */

goog.provide('spv.ds.DsClient');

goog.require('spv.assert');
goog.require('spv.ds.Config');
goog.require('spv.ds.ConfigLoadResult');
goog.require('spv.ds.Disclaimer')
goog.require('spv.ds.DsClientSettings');
goog.require('spv.ds.FacebookShareData');
goog.require('spv.ds.FreshConfig');
goog.require('spv.ds.ImageParams');
goog.require('spv.ds.ImageResult');
goog.require('spv.ds.ImageSerieInfo');
goog.require('spv.ds.IntroPage');
goog.require('spv.ds.MenuQuery');
goog.require('spv.ds.PresentationStructureQuery');
goog.require('spv.ds.SavedConfig');
goog.require('spv.ds.SessionStateUpdate');
goog.require('spv.ds.Suggestion');
goog.require('spv.ds.Summary');
goog.require('spv.ds.assert');
goog.require('spv.ds.impl.GoalStateResolver');
goog.require('spv.ds.impl.IPPNovaAdaptor');
goog.require('spv.ds.impl.IDsProtocol');
goog.require('spv.ds.impl.SessionInitData');
goog.require('spv.ds.impl.SessionState');


goog.require('spv.ds.ipprot_nova.input.BopReadMoreMode');


/**
 * DsClient provides a layer of abstraction between GUI code and
 * session-/protocol-handling.
 *
 * The configurator system is stateful. Session state is shared between server
 * and client, this is a source of complexity and can hopefully be removed at
 * some point in the future. DsClient provides synchronization of session state
 * between server and client by centralizing management of the shared state
 * inside this class.
 *
 * Member functions are stateful if the member function name begins with
 * "session". Other member functions are state- and session-less and can be
 * called at any time, in any order.
 *
 * Most session dependent functions also require a loaded configuration.
 * Preconditions should be documented for each member function in this class.
 *
 * The configurator system can be simplified as the following three phases:
 * A. The user finds the configurator application on the web or in the showroom.
 * B. The user choose and change the product components (called items) until
 *    the user is satisfied or leaved the application.
 * C. The user saves the configuration for a later time or choose to place an
 *    order or request a sales contact for the built configuration.
 *
 * Technical usage example 1, Starting a new configuration session and
 * interacting with the user. (Step-by-step example of phase B):
 *
 * 1. Construct an instance of spv.ds.Client
 * 2. Call sessionInit
 * 3. Call getIntroPage to retrieve the available start configurations
 * 4. Generate images for the start configurations using getConfigImage
 * 5. Display the available start configuration images to the user using the
 *    appropriate techniques for the GUI implementation.
 * 6. The user chooses a start configuration
 * 7. Send that start configuration to sessionLoadConfig
 * 8. On success, call sessionGetRootMenuItems and sessionGetImage
 * 9. Populate the GUI root menu with menu items and wait for the image response
 * 10. On image success, load and display the configuration image
 * 11. The user makes a choice about which menu to open
 * 12. Call sessionToggleItem for the user chosen MenuItem
 *     (both menus and product components are MenuItems)
 * 13. On success, call sessionGetStateUpdate to populate the currently open
 *     menu with menu items.
 * 14. The user clicks an item in the currently open menu, send that MenuItem to
 *     sessionToggleItem and handle response.
 * 15. On success, update the currently open menu with new MenuItems (most menu
 *     items will usually be unchanged, but some may change state and some may
 *     disappear while others may be added). Call sessionGetImage to get the new
 *     configuration image and wait for response.
 * 16. On image success, load and display the configuration image
 * 17. The user clicks a new item and the sessionToggleItem cycle continues
 *     until the user closes the application or decides to place an order for
 *     the built configuration. (this step is customized action for each
 *     configurator application, eg. cars and bathroom products require
 *     different types of shopping cart solutions)
 *
 *
 *
 *
 * @export
 * @constructor
 * @param {!string} service_url
 * @param {!spv.ds.DsClientSettings=} in_settings
*/
spv.ds.DsClient = function(service_url, in_settings)
{
	// Default settings.
	var default_settings = new spv.ds.DsClientSettings();
	default_settings.use_jsonp = false;
	default_settings.debug_print = false;

	/** @type {!spv.ds.DsClientSettings} */
	var settings = in_settings ? default_settings.overloadWith( in_settings ) : default_settings;

	var valid_settings = new spv.ds.DsClientValidatedSettings( settings );

	/**
	 * @protected
	 * @type {spv.ds.impl.IDsProtocol}
	 */
	this._protocol = new spv.ds.impl.IPPNovaAdaptor(service_url, valid_settings.use_jsonp);

	/**
	 * @private
	 * @type {boolean}
	 */
	this._debug_print = valid_settings.debug_print;
	/**
	 * @private
	 * @type {boolean}
	 */
	this._session_is_initialized = false;

	/**
	 * @private
	 * @type {string}
	 */
	this._language = "";
	/**
	 * @private
	 * @type {null|string}
	 */
	this._session_id = null;
	/**
	 * @protected
	 * @type {spv.ds.impl.SessionState}
	 */
	this._session_state = null;
	/**
	 * @private
	 * @type {Array.<Function>}
	 */
	this._session_request_queue = [];
	/**
	 * @private
	 * @type {boolean}
	 */
	this._session_request_in_progress = false;
	/**
	 * @private
	 * @type {?function( Error )}
	 */
	this._on_unauthorized_session = valid_settings.on_unauthorized_session;
	/**
	 * @private
	 * @type {?function( Error )}
	 */
	this._on_service_unavailable = valid_settings.on_service_unavailable;
	/**
	 * @private
	 * @type {?function( Error )}
	 */
	this._on_service_failure = valid_settings.on_service_failure;
	/**
	 * @private
	 * @type {number}
	 */
	this._session_ttl = valid_settings.session_ttl;
	/**
	 * @private
	 * @type {string}
	 */
	this._origin = valid_settings.origin;
	/**
	 * @private
	 * @type {string}
	 */
	this._initial_category = valid_settings.initial_category;
};




/**
 * Get the session id or throw if null
 *
 * @export
 * @return {string}
 */
spv.ds.DsClient.prototype.getSessionId = function()
{
	if (null == this._session_id)
	{
		throw new Error('Session id is null, session is not initialized.');
	}
	return this._session_id;
};




/**
 * Create a ImageParams instance with default values, however, defaults are
 * likely not good enough for most contexts.
 *
 * Preconditions: None.
 *
 * @export
 * @return {spv.ds.ImageParams}
 */
spv.ds.DsClient.prototype.constructDefaultImageParams = function()
{
	return new spv.ds.ImageParams();
};




/**
 * Requests an image for a serialized configuration.
 *
 * Preconditions: None.
 *
 * @export
 * @param {spv.ds.Config} conf
 * @param {spv.ds.ImageParams} img_params
 * @param {function(spv.ds.ImageResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getConfigImage = function(
		conf,
		img_params,
		on_success,
		on_failure)
{
	spv.ds.assert.isConfig(conf, 'conf');
	spv.ds.assert.isImageParams(img_params, 'img_params');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.getConfigImage(
		conf,
		img_params,
		on_success,
		this.errorHandler(on_failure) );
};




/**
 * Requests the "Intro page" data including available start-configurations,
 * typically one config for each car model.
 *
 * Preconditions: None.
 *
 * @export
 * @param {function(spv.ds.IntroPage)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getIntroPage = function(on_success, on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.getIntroPage(
		on_success,
		this.errorHandler(on_failure) );
};




/**
 * @param {string} item_id
 * @param {string} model
 * @param {string} category
 * @param {function(Array.<spv.ds.ReadMoreResource>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getReadMore = function(
	item_id,
	model,
	category,
	language,
	on_success,
	on_failure)
{
	this._protocol.getReadMore(
		item_id,
		model,
		category,
		language,
		on_success,
		on_failure );
};



/**
 * @param {Array<string>} item_ids
 * @param {string} category
 * @param {string} model
 * @param {string} language
 * @param {function(Object<string,spv.ds.ItemResources>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getItemResources = function(
	item_ids,
	category,
	model,
	language,
	on_success,
	on_failure)
{
	this._protocol.getItemResources(
		item_ids,
		category,
		model,
		language,
		on_success,
		on_failure);
}





/**
 * Gets all configurations stored in the database by the given username.
 * This is a heavy operation and is included here for utility purposes.
 *
 * @export
 * @param {string} username
 * @param {function(Array.<spv.ds.SavedConfig>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getUserConfigList = function(
	username,
	on_success,
	on_failure )
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.getUserConfigList(
		username,
		on_success,
		this.errorHandler(on_failure) );
};




/**
 * @export
 * @param {Array.<string>} group_ids
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionAddGuiGroupSubscriptions = function(
	group_ids,
	on_success,
	on_failure )
{
	var client = this;

	var on_failure_wrapper = this.constructFailureWrapperForQueue(
		this.errorHandler(on_failure) );

	function on_success_wrapper()
	{
		on_success();
		client.sessionDequeueNextRequest();
	}

	function session_request()
	{
		client._protocol.sessionAddGuiGroupSubscriptions(
			client.getSessionId(),
			group_ids,
			on_success_wrapper,
			on_failure_wrapper );
	}

	this.sessionEnqueueRequest(session_request);
};




/**
 * @export
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionClearGuiGroupSubscriptions = function(
	on_success,
	on_failure )
{
	var client = this;

	var on_failure_wrapper = this.constructFailureWrapperForQueue(
		this.errorHandler(on_failure) );

	function on_success_wrapper()
	{
		on_success();
		client.sessionDequeueNextRequest();
	}

	function session_request()
	{
		client._protocol.sessionClearGuiGroupSubscriptions(
			client.getSessionId(),
			on_success_wrapper,
			on_failure_wrapper );
	}

	this.sessionEnqueueRequest(session_request);
};




/**
 * Apply a configuration change suggestion on the current configuration.
 *
 * Preconditions: Initialized session, loaded configuration, caller code has a
 *                valid instance of spv.ds.Suggestion retrieved by calling
 *                sessionToggleItem, no requests that modify the session
 *                configuration has been dispatched between suggestion
 *                construction and the call to this function.
 *
 * @export
 * @param {spv.ds.Suggestion} suggestion
 *        The suggestion to be applied to the current configuration.
 * @param {Function} on_success
 *        Called when the suggestion has been successfully applied.
 * @param {function(Error)} on_failure
 *        Called on any kind of failure.
 */
spv.ds.DsClient.prototype.sessionApplySuggestion = function(
		suggestion,
		on_success,
		on_failure)
{
	spv.ds.assert.isSuggestion(suggestion, 'suggestion');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSessionWithConfig();
	var that = this;
	// TODO: DSClient sessionBip (@protected), since there's a bit of code duplication here.
	/** @param {spv.ds.SessionStateUpdate} ssu
	  * @param {spv.ds.impl.AuxData} aux_data */
	var on_success_wrapper = function(ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	var session_request = function()
	{
		try
		{
			that._protocol.sessionBip(
					that.getSessionId(),
					'',
					suggestion.internal_data,
					that.getLanguage(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Create a pdf using current session state and returns the pdf url through a
 * callback if a pdf was generated successfully.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {function(string)} on_success    Callback taking the generated pdf url
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGeneratePdf = function(
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/** @param {string} pdf_url */
	var on_success_wrapper = function(pdf_url)
	{
		on_success(pdf_url);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSessionWithConfig();
			that._protocol.sessionGeneratePdf(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Get the root menu items for the current configuration
 *
 * Preconditions: Initialized session, loaded config.
 *
 * @export
 * @return {Array.<spv.ds.MenuItem>}
 */
spv.ds.DsClient.prototype.sessionGetRootMenuItems = function()
{
	this.assertInitializedSessionWithConfig();
	return this._session_state.getRootMenuItems();
};




/**
 * Requests the current session config
 *
 * Available encodings are:
 *   "" : internal encoding, only for debug
 *
 * Preconditions: Initialized session, loaded config.
 *
 * @export
 * @param {string} encoding
 * @param {function(spv.ds.Config)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetConfig = function(
		encoding,
		on_success,
		on_failure)
{
	spv.assert.isString(encoding, 'encoding');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/** @param {spv.ds.Config} config */
	var on_success_wrapper = function(config)
	{
		on_success(config);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSessionWithConfig();
			that._protocol.sessionGetConfig(
					that.getSessionId(),
					encoding,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Get the saved configs for the logged in user
 *
 * Preconditions: Initialized session, logged in user.
 *
 * @export
 * @param {function(Array.<string>)} on_success
 *        A callback taking an array of names of saved configurations.
 *        The configuration names can be used to remove or load configs for the
 *        logged in user by sessionRemoveConfigByName and
 *        sessionLoadConfigByName.
 *
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetConfigNames = function(
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/** @param {Array.<string>} config_names */
	var on_success_wrapper = function(config_names)
	{
		on_success(config_names);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionGetConfigNames(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Get 'short_text' text for item_id or empty string on failure
 *
 * Texts are configuration dependent.
 * In practice updating texts is only needed after loading a configuration, not
 * when simply toggling an item in the configuration.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {string} item_id
 * @return {string}
 */
spv.ds.DsClient.prototype.sessionGetShortText = function(item_id)
{
	spv.assert.isNonEmptyString(item_id, 'item_id');
	this.assertInitializedSessionWithConfig();
	return this._session_state.getShortText(item_id);
};




/**
 * Get 'medium_text' text for item_id or empty string on failure
 *
 * Texts are configuration dependent.
 * In practice updating texts is only needed after loading a configuration, not
 * when simply toggling an item in the configuration.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {string} item_id
 * @return {string}
 */
spv.ds.DsClient.prototype.sessionGetMediumText = function(item_id)
{
	spv.assert.isNonEmptyString(item_id, 'item_id');
	this.assertInitializedSessionWithConfig();
	return this._session_state.getMediumText(item_id);
};




/**
 * Get 'long_text' text for item_id or empty string on failure
 *
 * Texts are configuration dependent.
 * In practice updating texts is only needed after loading a configuration, not
 * when simply toggling an item in the configuration.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {string} item_id
 * @return {string}
 */
spv.ds.DsClient.prototype.sessionGetLongText = function(item_id)
{
	spv.assert.isNonEmptyString(item_id, 'item_id');
	this.assertInitializedSessionWithConfig();
	return this._session_state.getLongText(item_id);
};




/**
 * Gets the disclaimer to be displayed when the configurator is started.
 *
 * @export
 * @return {spv.ds.Disclaimer}
 */
spv.ds.DsClient.prototype.sessionGetIntroDisclaimer = function()
{
	var state_update = this.sessionGetStateUpdate();
	var disclaimer_id = "INTRO_DISCLAIMER";
	return new spv.ds.Disclaimer( this.sessionGetShortText( disclaimer_id ) );
};




/**
 * Get the disclaimer specific for a model.
 *
 * If no model id is passed, it'll default to the active configuration's model.
 *
 * @export
 * @param {string=} model_id
 * @return {spv.ds.Disclaimer}
 */
spv.ds.DsClient.prototype.sessionGetModelDisclaimer = function( model_id )
{
	var id = model_id || this.sessionGetStateUpdate().model_id;
	var disclaimer_id = id + "_DISCLAIMER";
	return new spv.ds.Disclaimer( this.sessionGetShortText( disclaimer_id ) );
};




/**
 * Get base urls needed for most HTTP GET paths to images
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @return {spv.ds.UrlSettings}
 */
spv.ds.DsClient.prototype.sessionGetUrlSettings = function()
{
	this.assertInitializedSession();
	return this._session_state.getUrlSettings();
};




/**
 * Get image serie info for the current configuration
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @return {Array.<spv.ds.ImageSerieInfo>}
 */
spv.ds.DsClient.prototype.sessionGetImageSerieInfos = function()
{
	this.assertInitializedSessionWithConfig();
	return this._session_state.getImageSerieInfos();
};




/**
 * Get complete item info for items by item id
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {Array.<string>} item_ids
 * @param {function(Array.<spv.ds.MenuItem>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetItemInfo = function(
		item_ids,
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSessionWithConfig();
	this._protocol.sessionGetItemInfo(
			this.getSessionId(),
			item_ids,
			this.getLanguage(),
			on_success,
			this.errorHandler(on_failure) );
};




/**
 * Gets an URL to a zipfile containing the images.
 *
 * @export
 * @param {number} serie_index
 * @param {number} frame_index
 * @param {string} file_type
 * @param {boolean} separate_background
 * @param {boolean} separate_shadow
 * @param {boolean} all_frames
 * @param {boolean} use_hd
 * @param {number} image_width
 * @param {number} image_height
 * @param {function(string)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetMediaKit = function(
	serie_index,
	frame_index,
	file_type,
	separate_background,
	separate_shadow,
	all_frames,
	use_hd,
	image_width,
	image_height,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	this._protocol.sessionGetMediaKit(
		this.getSessionId(),
		serie_index,
		frame_index,
		file_type,
		separate_background,
		separate_shadow,
		all_frames,
		use_hd,
		image_width,
		image_height,
		on_success,
		this.errorHandler(on_failure));
};




/**
 * Requests the text, images etc used for facebook sharing.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {function(spv.ds.FacebookShareData)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetFacebookShareData = function(
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSessionWithConfig();
	this._protocol.sessionGetFacebookShareData(
			this.getSessionId(),
			on_success,
			this.errorHandler(on_failure) );
};




/**
 * Requests the most recently built car configurations.
 *
 * @export
 * @param {number} limit Max number of configs in result
 * @param {number} scale_width
 * @param {number} scale_height
 * @param {function(Array.<spv.ds.FreshConfig>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getFreshConfigs = function(
		limit,
		scale_width,
		scale_height,
		on_success,
		on_failure)
{
	spv.assert.isNumber(limit, 'limit');
	spv.assert.isNumber(scale_width, 'scale_width');
	spv.assert.isNumber(scale_height, 'scale_height');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.getFreshConfigs(
			limit,
			scale_width,
			scale_height,
			on_success,
			this.errorHandler(on_failure) );
};




/**
 * Requests the most recently built car configurations.
 *
 * @deprecated
 * @export
 * @param {number} limit Max number of configs in result
 * @param {number} scale_width
 * @param {number} scale_height
 * @param {function(Array.<spv.ds.FreshConfig>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetFreshConfigs = function(
		limit,
		scale_width,
		scale_height,
		on_success,
		on_failure)
{
	spv.assert.isNumber(limit, 'limit');
	spv.assert.isNumber(scale_width, 'scale_width');
	spv.assert.isNumber(scale_height, 'scale_height');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();
	this._protocol.sessionGetFreshConfigs(
			this.getSessionId(),
			limit,
			scale_width,
			scale_height,
			on_success,
			this.errorHandler(on_failure));
};



/**
 * Builds an image for the current configuration state in this session.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {spv.ds.ImageParams} img_params
 * @param {function(spv.ds.ImageResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetImage = function(
		img_params,
		on_success,
		on_failure)
{
	spv.ds.assert.isImageParams(img_params, 'img_params');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/** @param {spv.ds.ImageResult} img_result */
	var on_success_wrapper = function(img_result)
	{
		on_success(img_result);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSessionWithConfig();
			that._protocol.sessionGetImage(
					that.getSessionId(),
					img_params,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Get the summary for the current session and configuration
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @param {function(spv.ds.Summary)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetSummary = function(on_success, on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSessionWithConfig();
	this._protocol.sessionGetSummary(
			this.getSessionId(),
			on_success,
			this.errorHandler(on_failure) );
};




/**
 * Returns the auxiliary data arriving with the latest bop.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @protected
 * @return {Object.<string, Object>}
 */
spv.ds.DsClient.prototype.sessionGetAuxiliaryData = function()
{
	this.assertInitializedSessionWithConfig();
	return this._session_state.getAuxiliaryData();
};




/**
 * Get the last state update. Call this function on successful load of configs
 * or on successful sessionToggleItem.
 *
 * Preconditions: Initialized session, loaded configuration.
 *
 * @export
 * @return {spv.ds.SessionStateUpdate}
 */
spv.ds.DsClient.prototype.sessionGetStateUpdate = function()
{
	this.assertInitializedSessionWithConfig();
	return this._session_state.getUpdate();
};




/**
 * Requests an image for a saved configuration
 *
 * Preconditions: Initialized session, logged in user.
 *
 * @export
 * @param {string} config_name
 * @param {spv.ds.ImageParams} img_params
 * @param {function(spv.ds.ImageResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetUserConfigImage = function(
		config_name,
		img_params,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(config_name, 'config_name');
	spv.ds.assert.isImageParams(img_params, 'img_params');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/** @param {spv.ds.ImageResult} img_result */
	var on_success_wrapper = function(img_result)
	{
		on_success(img_result);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionGetUserConfigImage(
					that.getSessionId(),
					config_name,
					img_params,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};



/**
 * @param {string} filter_name
 * @param {spv.ds.PresentationStructureQuery} query
 * @param {function(spv.ds.PresentationStructure)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetPresentationStructure = function(
	filter_name,
	query,
	on_success,
	on_failure )
{
	var that = this;

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	/** @param {spv.ds.PresentationStructure} presentation_structure */
	function on_success_wrapper( presentation_structure )
	{
		that.sessionDequeueNextRequest();
		on_success( presentation_structure );
	}

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		that._protocol.sessionGetPresentationStructure(
			that.getSessionId(),
			filter_name,
			query,
			on_success_wrapper,
			on_failure_wrapper );
	}
}




/**
 * Initialize session.
 * This must be called before calling any other member function with "session".
 * For most other session calls a configuration needs to be loaded as well.
 *
 * This is not done in the constructor in order to allow state-/session-less
 * calls without initializing a session.
 *
 * Preconditions: None.
 *
 * @export
 * @param {string} menu_query
 * @param {spv.ds.ipprot_nova.input.BopReadMoreMode} readmore_mode
 * @param {function(spv.ds.IntroPage)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionInit = function(
		menu_query,
		readmore_mode,
		on_success,
		on_failure)
{
	spv.assert.isString(menu_query, 'menu_query');
	spv.assert.isNonEmptyString(readmore_mode, 'readmore_mode');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/**
	 * @param {spv.ds.impl.SessionInitData} init_data
	 */
	var on_success_wrapper = function(init_data)
	{
		that._session_state = new spv.ds.impl.SessionState(init_data);
		that._session_is_initialized = true;
		that._session_id = init_data.session_id;
		on_success(init_data.intro_page);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.sessionResetRequestQueue();
			that._protocol.sessionCreate(
					menu_query,
					readmore_mode,
					that.getLanguage(),
					that._session_ttl,
					that._origin,
					that._initial_category,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};



/**
 * Initialize with existing session.
 * Used instead of sessionInit.
 *
 * Preconditions: Client not initialized.
 *
 * @export
 * @param {string} session_id
 * @param {function(spv.ds.IntroPage)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionResume = function(
		session_id,
		on_success,
		on_failure)
{
	spv.assert.isString( session_id, 'session_id' );
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;

	if (this._session_is_initialized)
	{
		throw new Error("Cannot resume session when the client is already initialized.");
	}

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	var session_request = function()
	{
		try
		{
			that.sessionResetRequestQueue();
			that._protocol.sessionResume(
					session_id,
					that.getLanguage(),
					on_success_wrapper,
					on_failure_wrapper );
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);

	/**
	 * @param {spv.ds.impl.SessionInitData} init_data
	 */
	function on_success_wrapper(init_data)
	{
		that._session_state = new spv.ds.impl.SessionState(init_data);
		that._session_is_initialized = true;
		that._session_id = init_data.session_id;
		on_success(init_data.intro_page);
		that.sessionDequeueNextRequest();
	}
};




/**
 * Loads a configuration into the current session and replaces to current
 * configuration state.
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {spv.ds.Config} config
 * @param {function(spv.ds.ConfigLoadResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionLoadConfig = function(
		config,
		on_success,
		on_failure)
{
	spv.ds.assert.isConfig(config, 'config');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/**
	 * @param {spv.ds.ConfigLoadResult} result
	 * @param {spv.ds.SessionStateUpdate} ssu
	 * @param {spv.ds.impl.AuxData} aux_data
	 */
	var on_success_wrapper = function(result, ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success(result);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionLoadConfig(
					that.getSessionId(),
					config,
					that.getLanguage(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Loads a configuration into the current session and replaces to current
 * configuration state. The config is referenced by it's name retrieved from
 * sessionGetConfigNames.
 *
 * Preconditions: Initialized session, logged in user.
 *
 * @export
 * @param {string} config_name
 * @param {function(spv.ds.ConfigLoadResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionLoadConfigByName = function(
		config_name,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(config_name, 'config_name');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/**
	 * @param {spv.ds.ConfigLoadResult} result
	 * @param {spv.ds.SessionStateUpdate} ssu
	 * @param {spv.ds.impl.AuxData} aux_data
	 */
	var on_success_wrapper = function(result, ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success(result);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionLoadConfigByName(
					that.getSessionId(),
					config_name,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Loads a configuration from the "public" database into the current session
 * replacing the current configuration state.
 *
 * "Public Configs" are anonymous configurations saved without any user
 * information that can be loaded by a unique key.
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {string} public_config_id
 * @param {function(spv.ds.ConfigLoadResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionLoadPublicConfig = function(
		public_config_id,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(public_config_id, 'public_config_id');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	/**
	 * @param {spv.ds.ConfigLoadResult} result
	 * @param {spv.ds.SessionStateUpdate} ssu
	 * @param {spv.ds.impl.AuxData} aux_data
	 */
	var on_success_wrapper = function(result, ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success(result);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionLoadPublicConfig(
					that.getSessionId(),
					public_config_id,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};



/**
 * Save the current configuration for the logged in user as a public config.
 * The generated name is returned as a string to the on_success function.
 *
 * Preconditions: Initialized session, loaded configuration, logged in user.
 *
 * @export
 * @param {function(string)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.generatePublicConfig = function(
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	var on_success_wrapper = function(conf_name)
	{
		on_success(conf_name);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSessionWithConfig();
			that._protocol.sessionGeneratePublicConfig(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};



/**
 * User login
 * Password may be required depending on backend settings.
 *
 * May create the user if it doesn't exist, depending on backend settings.
 *
 * success and failure callbacks are always at the end.
 *
 * sessionLogin( session_id, name, password, success, failure );
 * sessionLogin( session_id, name, success, failure );
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {string} user_name
 * @param {string|Function} password_or_success
 * @param {Function|function(Error)} success_or_failure
 * @param {function(Error)=} failure
 */
spv.ds.DsClient.prototype.sessionLogin = function(
		user_name,
		password_or_success,
		success_or_failure,
		failure )
{
	/** @type {Function} **/
	var on_success;
	/** @type {function(Error)} **/
	var on_failure;
	/** @type {string} **/
	var password;

	/* Argument swapping to make password an optional parameter.
	 * (For backwards compatibility)
	 */
	if( arguments.length === 3 )
	{
		password = "";
		on_success = /** @type {Function} */ (password_or_success);
		on_failure = /** @type {function(Error)} */ (success_or_failure);
	}
	else if( arguments.length === 4 )
	{
		password = /** @type {string} */ (password_or_success);
		on_success = /** @type {Function} */ (success_or_failure);
		on_failure = /** @type {function(Error)} */ (failure);
	}

	spv.assert.isNonEmptyString(user_name, 'user_name');
	spv.assert.isString(password, 'password');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionLogin(
					that.getSessionId(),
					user_name,
					password,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Logout the user from the session.
 *
 * @export
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionLogout = function(
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionLogout(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Create a new user in the user database.
 *
 * May not be required to create a user, depending on backend settings.
 *
 * @export
 * @param {string} user_name
 * @param {string} password
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionCreateUser = function(
		user_name,
		password,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(user_name, 'user_name');
	spv.assert.isNonEmptyString(password, 'password');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');


	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionCreateUser(
					that.getSessionId(),
					user_name,
					password,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	}
}



/**
 * Check with backend if the session has a logged in user.
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {function(boolean)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionHasLoggedInUser = function(
		on_success,
		on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');


	var that = this;
	var on_success_wrapper = function(b)
	{
		on_success(b);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionHasLoggedInUser(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	}
};




/**
 * Precondition: Initialized session
 *
 * @export
 * @param {string} password_form_url
 * @param {string} user_name
 * @param {string} language
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSendResetPasswordMail = function(
		password_form_url,
		user_name,
		language,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(password_form_url, 'password_form_url');
	spv.assert.isNonEmptyString(user_name, 'user_name');
	spv.assert.isNonEmptyString(language, 'language');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionSendResetPasswordMail(
					that.getSessionId(),
					password_form_url,
					user_name,
					language,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
}




/**
 * Resets user password.
 *
 * Token is retrieved via email, sent by sessionSendResetPasswordMail.
 *
 * @export
 * @param {string} token
 * @param {string} new_password
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.resetPassword = function(
		token,
		new_password,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(token, 'token');
	spv.assert.isNonEmptyString(new_password, 'new_password');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.resetPassword( token, new_password, on_success, on_failure );
}




/**
 * Remove a saved configuration for the logged in user
 *
 * Preconditions: Initialized session, logged in user.
 *
 * @export
 * @param {string} config_name    Name retrieved by sessionGetConfigNames
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionRemoveConfigByName = function(
		config_name,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(config_name, 'config_name');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionRemoveConfigByName(
					that.getSessionId(),
					config_name,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Save the current configuration for the logged in user
 *
 * Preconditions: Initialized session, loaded configuration, logged in user.
 *
 * @export
 * @param {string} config_name
 * @param {boolean} allow_overwrite
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSaveConfigByName = function(
		config_name,
		allow_overwrite,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(config_name, 'config_name');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSessionWithConfig();
			that._protocol.sessionSaveConfigByName(
					that.getSessionId(),
					config_name,
					allow_overwrite,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Choose which collection of price rules to use for the current session
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {string} price_localization_id
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSetPriceLocalization = function(
		price_localization_id,
		on_success,
		on_failure)
{
	spv.assert.isNonEmptyString(price_localization_id, 'price_localization_id');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that.assertInitializedSession();
			that._protocol.sessionSetPriceLocalization(
					that.getSessionId(),
					price_localization_id,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * @param {string} language
 * @param {function()} on_success
 * @param {function()} on_failure
 */
spv.ds.DsClient.prototype.setLanguage = function( language, on_success, on_failure )
{
	this._language = language;
	on_success();
};

/**
 * @export
 * @return {string}
 */
spv.ds.DsClient.prototype.getLanguage = function()
{
	return this._language;
}




/**
 * Try to change item state for one item in the current configuration.
 * An item has two states, On or Off, eg. an item is part of the current
 * configuration or it is not. The result of this operation can only be
 * determined by the server. Any call to this function may result in suggestions
 * being returned through the callback on_suggestions. If suggestions
 *
 * Use DsClient.setLanguage in order to specify language. Leaving it undefined
 * will cause the backend to resolve a preferred instead.
 *
 * Preconditions: Initialized session, loaded configuration, caller code has a
 *                valid instance of spv.ds.MenuItem.
 *
 * @export
 * @param {spv.ds.MenuItem} menu_item
 *        Menu item to toggle on or off. User code should not modify or
 *        construct instances of MenuItem, only pass MenuItem instances returned
 *        by this class.
 *
 * @param {Function} on_success
 *        Will be called on toggle success if toggle has no important side
 *        effects. Caller code should call sessionGetStateUpdate on this object
 *        to get the latest session state update.
 *
 * @param {function(Array.<spv.ds.Suggestion>, spv.ds.BipAttemptResultCode, string)} on_suggestions
 *        Will be called if item toggle has important side effects or if
 *        relevant alternative configuration states has been found.
 *
 * @param {function(Error)} on_failure
 *        Called on any kind of failure.
 */
spv.ds.DsClient.prototype.sessionToggleItem = function(
		menu_item,
		on_success,
		on_suggestions,
		on_failure)
{
	spv.ds.assert.isMenuItem(menu_item, 'menu_item');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_suggestions, 'on_suggestions');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSessionWithConfig();

	var that = this;
	var category_id = menu_item.category;
	var item_id = menu_item.id;

	/** @param {spv.ds.SessionStateUpdate} ssu
	  * @param {spv.ds.impl.AuxData} aux_data */
	var on_success_wrapper = function(ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_resolvers = function(goal_resolvers, attempt_code, resolvers_display_text)
	{
		var suggestions = that.constructSuggestions(goal_resolvers);
		on_suggestions(suggestions, attempt_code, resolvers_display_text);
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	var session_request = function()
	{
		try
		{
			that._protocol.sessionToggleItem(
					that.getSessionId(),
					item_id,
					category_id,
					that.getLanguage(),
					on_success_wrapper,
					on_resolvers,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};

	this.sessionEnqueueRequest(session_request);
};




/**
 * Call this function to keep the session alive
 *
 * Preconditions: Initialized session.
 *
 * @export
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSendHeartBeat = function(on_success, on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;
	/** @param {string} unimportant_data */
	var on_success_wrapper = function(unimportant_data)
	{
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that._protocol.sessionSendHeartBeat(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};



/**
 * Preconditions: Initialized session.
 *
 * @param {number} config_id
 * @param {string} config_description
 * @param {Object.<string,string>} user_config_aux
 * @param {Object.<string,string>} config_storage_aux
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 *
 * @export
 */
spv.ds.DsClient.prototype.userConfigV2Save = function(
	config_id,
	config_description,
	user_config_aux,
	config_storage_aux,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.userConfigV2Save(
					that.getSessionId(),
					config_id,
					config_description,
					user_config_aux,
					config_storage_aux,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
}


/**
 * Preconditions: Initialized session.
 *
 * @param {string} config_name
 * @param {string} config_description
 * @param {Object.<string,string>} user_config_aux
 * @param {Object.<string,string>} config_storage_aux
 * @param {function(number)} on_success
 * @param {function(Error)} on_failure
 *
 * @export
 */
spv.ds.DsClient.prototype.userConfigV2SaveAs = function(
	config_name,
	config_description,
	user_config_aux,
	config_storage_aux,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function(configId)
	{
		on_success(configId);
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.userConfigV2SaveAs(
					that.getSessionId(),
					config_name,
					config_description,
					user_config_aux,
					config_storage_aux,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
}


/**
 * Preconditions: Initialized session.
 *
 * @param {number} config_id
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 *
 * @export
 */
spv.ds.DsClient.prototype.userConfigV2Delete = function(
	config_id,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function()
	{
		on_success();
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.userConfigV2Delete(
					that.getSessionId(),
					config_id,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
}


/**
 * Preconditions: Initialized session.
 *
 * @export
 *
 * @param {function(Array<spv.ds.UserConfigInfo>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.userConfigV2List = function(
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	/**
	 * @param {Array<spv.ds.UserConfigInfo>} list
	 */
	var on_success_wrapper = function( list )
	{
		on_success( list );
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.userConfigV2List(
					that.getSessionId(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
}


/**
 * Preconditions: Initialized session.
 *
 * @export
 *
 * @param {number} config_id
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.userConfigV2Load = function(
	config_id,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function()
	{
		that.sessionUpdateBop( on_success, on_failure );
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.userConfigV2Load(
					that.getSessionId(),
					config_id,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	}
};



/**
 * @param {string} public_config_id
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.publicConfigV2Load = function(
	public_config_id,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function()
	{
		that.sessionUpdateBop( on_success, on_failure );
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.publicConfigV2Load(
				that.getSessionId(),
				public_config_id,
				on_success_wrapper,
				on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	}
};



/**
 * @param {string} description
 * @param {Object.<string, string>} config_storage_aux
 * @param {function(spv.ds.PublicConfigInfo)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.publicConfigV2Save = function(
	description,
	config_storage_aux,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this.assertInitializedSession();

	var that = this;

	var on_success_wrapper = function( result )
	{
		on_success( result );
		that.sessionDequeueNextRequest();
	};

	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );

	this.sessionEnqueueRequest(session_request);

	function session_request()
	{
		try
		{
			that._protocol.publicConfigV2Save(
					that.getSessionId(),
					description,
					config_storage_aux,
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	}
}




/**
 * @protected
 * @param {function()} on_heartbeat
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionAutomaticHeartbeat = function(on_heartbeat, on_failure) {
	var that = this;
	this.stopAutomaticHeartbeat();
	if( 0 < this._automatic_heartbeat_interval && this._session_is_initialized )
	{
		this.sessionSendHeartBeat( repeat, handleFailure );
	}
	function repeat() {
		spv.assert( !that._heartbeat_timeout_id );
		that._heartbeat_timeout_id = setTimeout(
			function() {
				that.sessionAutomaticHeartbeat(on_heartbeat, handleFailure);
			},
			that._automatic_heartbeat_interval );
		on_heartbeat();
	}
	function handleFailure( err )
	{
		/** Keep heartbeating if no connection is made with the server.
		 * User's internet connection may be temporarily down. */
		if( err.status === 'ServiceUnavailable' || err.connectionStatusCode === 0 )
			repeat();
		else
			on_failure(err);
	}
};




/**
 * Starts the automatic heartbeat for the session.
 * Don't forget to call stopAutomaticHeartbeat if you attempt to destroy or
 * replace the DsClient object.
 * If you don't, there'll be an orphaned DsClient object heartbeating forever.
 * (Well, until the browser session ends. )
 * @public
 * @param {number} interval How often the heartbeat is triggered, in milliseconds.
 * @param {function()} on_heartbeat This callback is called for every heartbeat.
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.startAutomaticHeartbeat = function(
		interval,
		on_heartbeat,
		on_failure )
{
	spv.assert( 0 <= interval );
	this._automatic_heartbeat_interval = interval;
	this.sessionAutomaticHeartbeat( on_heartbeat, on_failure );
};




/**
 * @public
 */
spv.ds.DsClient.prototype.stopAutomaticHeartbeat = function()
{
	if( this._heartbeat_timeout_id ) {
		clearTimeout( this._heartbeat_timeout_id );
		this._heartbeat_timeout_id = undefined;
	}
};




/**
 * @private
 * @param {spv.ds.impl.GoalStateResolver} goal_resolver
 * @return {spv.ds.Suggestion}
 */
spv.ds.DsClient.prototype.constructSuggestion = function(goal_resolver)
{
	var items_to_add = goal_resolver.items_to_add;
	var items_to_remove = goal_resolver.items_to_remove;
	var internal_data = gatherItemIdsForActivation(goal_resolver);
	var result = new spv.ds.Suggestion(
			items_to_add,
			items_to_remove,
			internal_data);
	return result;
};




/**
 * Convert state resolvers to suggestions for user presentation and bind each
 * suggestion to a .activate function to allow hiding of implemention.
 *
 * @private
 * @param {Array.<spv.ds.impl.GoalStateResolver>} goal_resolvers
 * @return {Array.<spv.ds.Suggestion>}
 */
spv.ds.DsClient.prototype.constructSuggestions = function(goal_resolvers)
{
	/** @type {Array.<spv.ds.Suggestion>} */
	var result = [];
	for (var i = 0; i < goal_resolvers.length; i++)
	{
		var goal_resolver = goal_resolvers[i];
		result.push(this.constructSuggestion(goal_resolver));
	}
	return result;
};




/**
 * Requests a fresh bop and stores the new state in the client.
 *
 * @protected
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionUpdateBop = function( on_success, on_failure )
{
	var that = this;
	/** @param {spv.ds.SessionStateUpdate} ssu
	  * @param {spv.ds.impl.AuxData} aux_data */
	var on_success_wrapper = function(ssu, aux_data)
	{
		that._session_state.update(ssu, aux_data);
		on_success();
		that.sessionDequeueNextRequest();
	};
	var on_failure_wrapper =
		this.constructFailureWrapperForQueue( this.errorHandler(on_failure) );
	var session_request = function()
	{
		try
		{
			that._protocol.sessionUpdateBop(
					that.getSessionId(),
					that.getLanguage(),
					on_success_wrapper,
					on_failure_wrapper);
		}
		catch (err)
		{
			on_failure_wrapper(err);
		}
	};
	this.sessionEnqueueRequest(session_request);
};




/**
 * Constructs the standard failure callback wrapper for queued session requests
 *
 * @protected
 * @param {function(Error)} wrap_me    The outer failure handler to wrap
 * @return {function(Error)}
 */
spv.ds.DsClient.prototype.constructFailureWrapperForQueue = function(wrap_me)
{
	var that = this;
	/** @param {Error} err */
	var on_failure_wrapper = function(err)
	{
		try
		{
			wrap_me(err);
		}
		finally
		{
			that.sessionDequeueNextRequest();
		}
	};
	return on_failure_wrapper;
};




/**
 * Session requests that require synchronization must use this function
 * This function is not allowed to throw exceptions.
 *
 * @protected
 */
spv.ds.DsClient.prototype.sessionDequeueNextRequest = function()
{
	var next_request = this._session_request_queue.shift();
	if (!next_request)
	{
		this._session_request_in_progress = false;
		return;
	}
	this._session_request_in_progress = true;
	setTimeout(next_request, 0);
};




/**
 * Session requests that require synchronization must use this function
 *
 * @protected
 * @param {Function} request
 */
spv.ds.DsClient.prototype.sessionEnqueueRequest = function(request)
{
	this._session_request_queue.push(request);
	if (this._session_request_in_progress)
	{
		return;
	}
	this.sessionDequeueNextRequest();
};




/**
 * Resets session request queue
 *
 * @protected
 */
spv.ds.DsClient.prototype.sessionResetRequestQueue = function()
{
	this._session_request_queue.length = 0;
	this._session_request_in_progress = false;
};








/**
 * Throw exception if session is not initialized.
 *
 * @protected
 */
spv.ds.DsClient.prototype.assertInitializedSession = function()
{
	if (!this._session_is_initialized)
	{
		throw new Error('Session is not initialized');
	}
};




/**
 * Throw exception if session is not initialized or is missing a configuration.
 *
 * @protected
 */
spv.ds.DsClient.prototype.assertInitializedSessionWithConfig = function()
{
	this.assertInitializedSession();
	this._session_state.assertHasLoadedConfig();
};




/**
 * @param {!string} str
 * @private
 */
spv.ds.DsClient.prototype.debugPrint = function( str )
{
	console.log( "DsClient: " + str );
};


/**
 * @param {function(Error)} on_failure
 * @protected
 * @return {function(Error)}
 */
spv.ds.DsClient.prototype.errorHandler = function( on_failure )
{
	var client = this;
	return function client_error_handler( err )
	{
		if( err )
		{
			if( err.status === 'UnauthorizedSession' && client._on_unauthorized_session )
			{
				client._on_unauthorized_session( err );
			}
			else if( err.status === 'ServiceUnavailable' && client._on_service_unavailable )
			{
				client._on_service_unavailable( err );
			}
			else if( err.status === 'ServiceFailure' && client._on_service_failure )
			{
				client._on_service_failure( err );
			}
		}

		return on_failure( err );
	}
};






// -----------------------------------------------------------------------------
// Private utility functions.
//
// These look like members of global scope but they are not as long as you
// compile with an outer function wrapper. /AR




/**
 * @private
 * @param {spv.ds.impl.GoalStateResolver} goal_resolver
 * @return {Array.<string>}
 */
var gatherItemIdsForActivation = function(goal_resolver)
{
	var menu_items = goal_resolver.items_to_add;
	/** @type {Array.<string>} */
	var result = [];
	for (var i = 0; i < menu_items.length; i++)
	{
		var menu_item = menu_items[i];
		result.push(menu_item.id);
	}
	return result;
};