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.RequestQueue');
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;
	/**
	 * @protected
	 * @type {spv.RequestQueue}
	 */
	this._requestQueue = new spv.RequestQueue();
	/**
	 * @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;
	/**
	 * @private
	 * @type {string}
	 */
	this._item_interpreter_options = valid_settings.item_interpreter_options;
};




/**
 * 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 {Array<string>} resource_keys
 * @param {string} category
 * @param {string} model
 * @param {string} language
 * @param {string} model_group
 * @param {function(Object<string,spv.ds.ItemResources>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getItemResources = function(
	item_ids,
	resource_keys,
	category,
	model,
	language,
	model_group,
	on_success,
	on_failure)
{
	spv.assert.isArray(item_ids, 'item_ids');
	spv.assert.isArray(resource_keys, 'resource_keys');
	spv.assert.isString(category, 'category');
	spv.assert.isString(model, 'model');
	spv.assert.isString(language, 'language');
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	this._protocol.getItemResources(
		item_ids,
		resource_keys,
		category,
		model,
		language,
		model_group,
		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 that = this;
	this._requestQueue.push(function (wrap) {
		that._protocol.sessionAddGuiGroupSubscriptions(
			that.getSessionId(),
			group_ids,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)) );
	});
};



/**
 * @export
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.getBuildInfo = function(
	on_success,
	on_failure )
{
	var that = this;
	this._requestQueue.push(function(wrap) {
		that._protocol.getBuildInfo(
			wrap(on_success),
			wrap(that.errorHandler(on_failure)) );
	});
};




/**
 * @export
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionClearGuiGroupSubscriptions = function(
	on_success,
	on_failure )
{
	var that = this;
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionClearGuiGroupSubscriptions(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)) );
	});
};




/**
 * 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();
	};
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionBip(
			that.getSessionId(),
			'',
			suggestion.internal_data,
			that.getLanguage(),
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * @param {spv.ds.FlexImageParameters} input
 * @param {function(spv.ds.FlexImageResult)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetFlexImage = function(
	input,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetFlexImage(
			that.getSessionId(),
			input,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGeneratePdf(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetConfig(
			that.getSessionId(),
			encoding,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionGetConfigNames(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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);
};

/**
* @export
* @param {string} item_id
* @return {string}
*/
spv.ds.DsClient.prototype.sessionGetCampaignText = function(item_id)
{
	spv.assert.isNonEmptyString(item_id,'item_id');
	this.assertInitializedSessionWithConfig();
	return this._session_state.getCampaignText(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');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetItemInfo(
			that.getSessionId(),
			item_ids,
			that.getLanguage(),
			wrap(on_success),
			wrap(that.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');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionGetMediaKit(
			that.getSessionId(),
			serie_index,
			frame_index,
			file_type,
			separate_background,
			separate_shadow,
			all_frames,
			use_hd,
			image_width,
			image_height,
			wrap(on_success),
			wrap(that.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');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetFacebookShareData(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.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');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionGetFreshConfigs(
			that.getSessionId(),
			limit,
			scale_width,
			scale_height,
			wrap(on_success),
			wrap(that.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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetImage(
			that.getSessionId(),
			img_params,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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');
	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGetSummary(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionGetUserConfigImage(
			that.getSessionId(),
			config_name,
			img_params,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * @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;
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionGetPresentationStructure(
			that.getSessionId(),
			filter_name,
			query,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}




/**
 * 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);
	};
	this._requestQueue.clear();
	this._requestQueue.push( function(wrap) {
		that._protocol.sessionCreate(
			menu_query,
			readmore_mode,
			that.getLanguage(),
			that._session_ttl,
			that._origin,
			that._initial_category,
			that._item_interpreter_options,
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * 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.");
	}

	this._requestQueue.clear();
	this._requestQueue.push( function(wrap) {
		that._protocol.sessionResume(
			session_id,
			that.getLanguage(),
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)) );
	});
	/**
	 * @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);
	}
};




/**
 * 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);
	};
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionLoadConfig(
			that.getSessionId(),
			config,
			that.getLanguage(),
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)));
	});
};


/**
 * 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.sessionLoadConfigMemorizeAll = 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);
};
this._requestQueue.push(function(wrap) {
	that.assertInitializedSession();
	that._protocol.sessionLoadConfigMemorizeAll(
		that.getSessionId(),
		config,
		that.getLanguage(),
		wrap(on_success_wrapper),
		wrap(that.errorHandler(on_failure)));
});
};


/**
 * 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);
	};
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionLoadConfigByName(
				that.getSessionId(),
				config_name,
				wrap(on_success_wrapper),
				wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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);
	};
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionLoadPublicConfig(
			that.getSessionId(),
			public_config_id,
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionGeneratePublicConfig(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionLogin(
			that.getSessionId(),
			user_name,
			password,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionLogout(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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(number)} on_success
 * @param {function(Error)} on_failure
 * @param {string=} language
 */
spv.ds.DsClient.prototype.sessionCreateUser = function(
		user_name,
		password,
		on_success,
		on_failure,
		language)
{
	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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionCreateUser(
			that.getSessionId(),
			user_name,
			password,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)),
			language);
	});
}



/**
 * Sets a users GDPR consent in the user database.
 *
 * @export
 * @param {number} user_id
 * @param {string} consent_id
 * @param {string} consent_text
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSetUserConsent = function(
	user_id,
	consent_id,
	consent_text,
	on_success,
	on_failure)
{
spv.assert.isNumber(user_id, 'user_id');
spv.assert.isNonEmptyString(consent_id, 'consent_id');
spv.assert.isNonEmptyString(consent_text, 'consent_text');
spv.assert.isFunction(on_success, 'on_success');
spv.assert.isFunction(on_failure, 'on_failure');


var that = this;
this._requestQueue.push(function(wrap) {
	that.assertInitializedSession();
	that._protocol.sessionSetUserConsent(
			user_id,
			consent_id,
			consent_text,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}




/**
 * Gets a users GDPR consents from a session id. Used when existing user logs in.
 *
 * @export
 * @param {string} session_id
 * @param {function(Array.<spv.ds.ipprot_nova.output.UserConsent>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetUserConsents = function(
	session_id,
	on_success,
	on_failure)
{
spv.assert.isNonEmptyString(session_id, 'session_id');
spv.assert.isFunction(on_success, 'on_success');
spv.assert.isFunction(on_failure, 'on_failure');


var that = this;
this._requestQueue.push(function(wrap) {
	that.assertInitializedSession();
	that._protocol.sessionGetUserConsents(
			session_id,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}



/**
 * Sets a users GDPR consent from session id in the user database.
 *
 * @export
 * @param {string} session_id
 * @param {string} consent_id
 * @param {string} consent_text
 * @param {Function} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionSetUserConsentSession = function(
	session_id,
	consent_id,
	consent_text,
	on_success,
	on_failure)
{
spv.assert.isNonEmptyString(session_id, 'session_id');
spv.assert.isNonEmptyString(consent_id, 'consent_id');
spv.assert.isNonEmptyString(consent_text, 'consent_text');
spv.assert.isFunction(on_success, 'on_success');
spv.assert.isFunction(on_failure, 'on_failure');


var that = this;
this._requestQueue.push(function(wrap) {
	that.assertInitializedSession();
	that._protocol.sessionSetUserConsentSession(
			session_id,
			consent_id,
			consent_text,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionHasLoggedInUser(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionSendResetPasswordMail(
			that.getSessionId(),
			password_form_url,
			user_name,
			language,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionRemoveConfigByName(
			that.getSessionId(),
			config_name,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionSaveConfigByName(
			that.getSessionId(),
			config_name,
			allow_overwrite,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.sessionSetPriceLocalization(
			that.getSessionId(),
			price_localization_id,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * @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');

	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();
	};
	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);
	};
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSessionWithConfig();
		that._protocol.sessionToggleItem(
			that.getSessionId(),
			item_id,
			category_id,
			that.getLanguage(),
			wrap(on_success_wrapper),
			wrap(on_resolvers),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * To be called after loading a config to retrieve the potential changes
 * made during loading.
 *
 * A stored configuration may no longer be possible to load with the exact
 * specification. For instance, an option may have been removed since the
 * configuration was first built. The system will try to adapt it to the current
 * logic, in which case you'll use this method to present the user with the
 * changes made by the system.
 *
 * @param {function(Array.<spv.ds.Suggestion>)} on_success
 * @param {function(Error)} on_failure
 */
spv.ds.DsClient.prototype.sessionGetInitialConsequenceOfChange = function(
	on_success,
	on_failure)
{
	var that = this;
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionGetInitialConsequenceOfChange(
			that.getSessionId(),
			that.getLanguage(),
			wrap(wrapped_on_success),
			wrap(that.errorHandler(on_failure))
		);
	});

	/** @param {Array.<spv.ds.impl.GoalStateResolver>} goal_resolvers */
	function wrapped_on_success(goal_resolvers) {
		var suggestions = that.constructSuggestions(goal_resolvers);
		on_success(suggestions);
	}
};



/**
 * 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;
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionSendHeartBeat(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
};



/**
 * 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');

	var that = this;

	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2Save(
			that.getSessionId(),
			config_id,
			config_description,
			user_config_aux,
			config_storage_aux,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * 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');

	var that = this;

	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2SaveAs(
			that.getSessionId(),
			config_name,
			config_description,
			user_config_aux,
			config_storage_aux,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * Preconditions: Initialized session.
 *
 * @param {string} session_id
 * @param {number} user_config_id
 * @param {string} new_name
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 *
 * @export
 */
spv.ds.DsClient.prototype.userConfigV2ChangeName = function(
	session_id,
	user_config_id,
	new_name,
	on_success,
	on_failure)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;

	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2ChangeName(
			session_id,
			user_config_id,
			new_name,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * 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');

	var that = this;

	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2Delete(
			that.getSessionId(),
			config_id,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * 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');

	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2List(
			that.getSessionId(),
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}


/**
 * Preconditions: Initialized session.
 *
 * The callback on_coc is optional. If provided, it will be called independently
 * of on_success and on_failure. I.e. if there is coc and the load was successful,
 * both on_success and on_coc will be called.
 *
 * @export
 *
 * @param {number} config_id
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 * @param {function(Array.<spv.ds.Suggestion>=)} on_coc (optional)
 */
spv.ds.DsClient.prototype.userConfigV2Load = function(
	config_id,
	on_success,
	on_failure,
	on_coc)
{
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.userConfigV2Load(
			that.getSessionId(),
			config_id,
			wrap(on_config_loaded),
			wrap(that.errorHandler(on_failure))
		);
	});

	function on_config_loaded()
	{
		if(on_coc) {
			that.sessionGetInitialConsequenceOfChange(
				function(suggestions) {
					bopit(function() {
						on_coc(suggestions);
						on_success();
					});
				},
				on_failure
			);
		}
		else {
			bopit(on_success);
		}
	}
	function bopit(on_success) {
		that.sessionUpdateBop(on_success, on_failure);
	}
};



/**
 * @param {string} public_config_id
 * @param {function()} on_success
 * @param {function(Error)} on_failure
 * @param {function(Array.<spv.ds.Suggestion>=)} on_coc (optional)
 */
spv.ds.DsClient.prototype.publicConfigV2Load = function(
	public_config_id,
	on_success,
	on_failure,
	on_coc
 ){
	spv.assert.isFunction(on_success, 'on_success');
	spv.assert.isFunction(on_failure, 'on_failure');

	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.publicConfigV2Load(
			that.getSessionId(),
			public_config_id,
			wrap(on_config_loaded),
			wrap(that.errorHandler(on_failure))
		);
	});

	function on_config_loaded()
	{
		if(on_coc) {
			that.sessionGetInitialConsequenceOfChange(
				function(suggestions) {
					bopit(function() {
						on_coc(suggestions);
						on_success();
					});
				},
				on_failure
			);
		}
		else {
			bopit(on_success);
		}
	}
	function bopit(on_success) {
		that.sessionUpdateBop(on_success, on_failure);
	}
};



/**
 * @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');

	var that = this;
	this._requestQueue.push(function(wrap) {
		that.assertInitializedSession();
		that._protocol.publicConfigV2Save(
			that.getSessionId(),
			description,
			config_storage_aux,
			wrap(on_success),
			wrap(that.errorHandler(on_failure)));
	});
}




/**
 * @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;
	}
};




/**
 * @protected
 * @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.
 *
 * @protected
 * @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();
	};
	this._requestQueue.push(function(wrap) {
		that._protocol.sessionUpdateBop(
			that.getSessionId(),
			that.getLanguage(),
			wrap(on_success_wrapper),
			wrap(that.errorHandler(on_failure)));
	});
};




/**
 * 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;
};