'use strict';

/**
 * Licensed Materials - Property of IBM
 * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2021
 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 */
define(['jquery', '../../lib/@waca/dashboard-common/dist/core/Model', './WidgetModel', './LayoutModel', './ModelUtils', 'underscore', './EventGroups', './properties/PropertiesModel', '../../lib/@waca/core-client/js/core-client/utils/UniqueId'], function ($, BaseModel, WidgetModel, LayoutModel, ModelUtils, _, EventGroupCollection, PropertiesModel, UniqueId) {

	function createLayoutModel_default(spec, env) {
		return new LayoutModel(spec, env.boardModel, env.logger);
	}

	var Model = BaseModel.extend({
		nestedCollections: {
			eventGroups: EventGroupCollection
		},

		nestedModels: {
			properties: PropertiesModel
		},

		init: function init(boardSpec, options) {
			var _this = this;

			this._initBoardSpec = boardSpec;

			options = options || {};

			this.logger = options.logger;

			// TODO: remove depenendency on dashboardApi here. currently needed to be passed to the "pageContext" board model extension
			this.dashboardApi = options.dashboardApi;

			this.createLayoutModel = options.createLayoutModel || createLayoutModel_default;

			// Make sure the extensions are processed before we call the parents init
			if (options.whitelistAttrs) {
				this.whitelistAttrs = options.whitelistAttrs;
			} else {
				this.whitelistAttrs = ['name', 'layout', 'theme', 'version', 'eventGroups', 'datasetShaping', 'properties', 'content'];
			}

			var excludedProperties = ['dashboardColorSet', 'customColors', 'localCache', 'defaultLocale', 'fredIsRed'];
			this.content = ModelUtils.initializeContentModel(boardSpec, excludedProperties);

			options.boardModel = this;
			if (!boardSpec.properties) {
				boardSpec.properties = {};
			}

			// Need to set these since the boardModel is passed as the options when building the layout model.
			// Weird, but don't want to change the flow of layout model creation in R release
			this.defaultLocale = boardSpec.properties.defaultLocale;
			if (this.dashboardApi) {
				var userProfileService = this.dashboardApi.getGlassCoreSvc('.UserProfile');
				if (userProfileService) {
					this.contentLocale = userProfileService.preferences.contentLocale;
				}
			}

			_.extend(options, this.getLanguageModelOptions());

			this._updateNestedInfoWithExtensions(options.boardModelExtensions);

			Model.inherited('init', this, arguments);
			this.eventRouter = options.eventRouter;

			this._autoCreateExtensionModelsAndCollections(options.boardModelExtensions, boardSpec);

			this.id = options.id;
			this.name = options.name || this.name;
			this.widgetRegistry = options.widgetRegistry;
			this.layoutExtensions = options.layoutExtensions;

			this.widgetInstances = {};

			// TODO: Add reference to cleanup item here when it is created in Jira.
			// CADBC-832 is item that caused it to change. Eventually this block
			// of code will go away.
			Object.keys(boardSpec.widgets || {}).forEach(function (widgetId) {
				_this.createLegacyWidgetModel(boardSpec.widgets[widgetId]);
			});

			// Explore expects the BoardModel to populate the widgetInstances
			// within this init method but because of changes to how widgets are
			// instantiated they are now created from the LayoutModel though still
			// from a method in this class (see createLegacyWidgetModel below)
			// This doesn't work in Explore's case so the following continues
			// to allow Explore to function properly
			if (this.createLayoutModel !== createLayoutModel_default) {
				this._instantiateWidgetModels(boardSpec.layout.items);
			}

			this.layout = this.createLayoutModel(boardSpec.layout, {
				boardModel: this,
				logger: this.logger,
				dashboardApi: this.dashboardApi
			});

			if (!this.eventGroups) {
				this.set({
					eventGroups: new EventGroupCollection()
				}, {
					silent: true
				});
			}

			// cache the extensions, the boardmodel editor needs this to re-create the model after a manual edit.
			this.boardModelExtensions = options.boardModelExtensions;

			// The first layout item will be displayed by default. Keep the selected layout property in sync.
			if (this.layout.items && this.layout.items.length > 0) {
				var item = this.layout.items[0];
				this.setSelectedLayout(item.id);
			}

			// Keep the selected layout in sync.
			this.eventRouter && this.eventRouter.on('tab:tabChanged', this.onTabChanged, this);

			this.isUpgraded = options.isUpgraded;

			this.type = 'dashboard';
		},

		_instantiateWidgetModels: function _instantiateWidgetModels() {
			var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];

			for (var i = 0; i < items.length; i++) {
				var item = items[i];
				if (item.items && item.items.length) {
					this._instantiateWidgetModels(item.items);
				} else {
					if (item.type === 'widget' && item.features && item.features['Models_internal']) {
						this.createLegacyWidgetModel(item.features['Models_internal']);
					}
				}
			}
		},


		/**
   * Returns the ContentModel
   * @return {Object}
   */
		getContentModel: function getContentModel() {
			return this.content;
		},


		/**
   * In Endor R7 (11.1.7) onward we now allow for features to load
   * their own data from the board spec. Through this mechanism this method
   * is called by the LayoutModel for specific content model objects that
   * have a 'Models.internal' feature on them.
   * 'Models.internal' is the place that widget data resides for now
   * instead of a top-level "widgets" property in the BoardModel here.
   * The BoardModel will still hold all widget instances though.
   * @param {Object} widgetSpec An object that represents a widget model
   */
		createLegacyWidgetModel: function createLegacyWidgetModel(widgetSpec) {
			if (!this.getWidgetModel(widgetSpec.id)) {
				this._addWidgetModel(widgetSpec, {
					addDashboardTranslatedLocales: false
				});
			}
		},

		/**
   * Adds the board model extension info to the the nestedCollections and nestedModels
   */
		_updateNestedInfoWithExtensions: function _updateNestedInfoWithExtensions(boardModelExtensions) {
			var _this2 = this;

			if (!boardModelExtensions) {
				return;
			}

			boardModelExtensions.forEach(function (extensionInfo) {
				var extensionName = extensionInfo.name;
				_this2.whitelistAttrs.push(extensionName);

				if (extensionInfo.type === 'collection') {
					_this2.nestedCollections[extensionName] = extensionInfo.class;
				} else {
					_this2.nestedModels[extensionName] = extensionInfo.class;
				}
			});
		},

		/**
   * Handles adding the model extension points to the whitelistAttrs array and as nestedModels
   * @param  {Object} boardModelExtensions
   * @param  {Object} boardSpec
   */
		_autoCreateExtensionModelsAndCollections: function _autoCreateExtensionModelsAndCollections(boardModelExtensions, boardSpec) {
			var _this3 = this;

			if (!boardModelExtensions) {
				return;
			}

			boardModelExtensions.forEach(function (extensionInfo) {
				var extensionName = extensionInfo.name;

				/**
     * If it's already in the boardSpec do nothing since the base Model class should of already made sure we were
     * dealing with actual Models and Collections.  If it's missing and autoCreate is set to true then create the
     * Model or Collection
     */
				if (!boardSpec[extensionName] && extensionInfo.autoCreate) {
					if (extensionInfo.type === 'collection') {
						_this3._setCollection(extensionName, boardSpec[extensionName] || null, {
							silent: true,
							logger: _this3.logger,
							boardModel: _this3,
							dashboardApi: _this3.dashboardApi // FIXME - models should not rely on dashboardApi -- currently used by the pageContext
						});
					} else {
						_this3._setNestedModel(extensionName, boardSpec[extensionName] || {}, {
							silent: true,
							logger: _this3.logger,
							boardModel: _this3
						});
					}
				}
			});

			this._registerBoardModelExtensionTriggers(boardModelExtensions);
		},

		setDefaultLocale: function setDefaultLocale(locale) {
			this.defaultLocale = locale;
		},

		setTranslationLocale: function setTranslationLocale(locale) {
			this.properties.set({
				translationModeLocale: locale
			});
			this.translationLocale = locale;
		},

		_registerBoardModelExtensionTriggers: function _registerBoardModelExtensionTriggers(boardModelExtensions) {
			if (boardModelExtensions) {
				boardModelExtensions.forEach(function (extension) {
					if (extension.triggers && extension.triggers.length > 0) {
						extension.triggers.forEach(function (trigger) {
							this[extension.name].on(trigger.eventName, this.trigger.bind(this, trigger.event));
						}, this);
					}
				}, this);
			}
		},

		onTabChanged: function onTabChanged(event) {
			this.setSelectedLayout(event.modelId);
		},

		/**
   * @deprecated, use findWidgetById
   */
		getWidgetModel: function getWidgetModel(id) {
			return this.widgetInstances ? this.widgetInstances[id] : null;
		},

		getTimeLineEpisode: function getTimeLineEpisode(id) {
			return this.timeline ? this.timeline.episodes.get(id) : null;
		},

		setSelectedLayout: function setSelectedLayout(id) {
			this.selectedLayout = id;
		},

		getSelectedLayout: function getSelectedLayout() {
			return this.selectedLayout;
		},

		/**
   * Add a content to the model
   *
   * @param options
   * @param options.model - layout model to add
   * @param options.parentId - id of the parent layout
   * @param [options.insertBefore] - id of the layout to insert before
   * @param options.layoutProperties
   * @param sender - If available, the sender will be added to the event payload.
   * @param payloadData - Additional data that will be included in the event payload.
   * @throws throws an exception if any of the mandatory options are not provided
   */
		addContent: function addContent(options, sender, payloadData) {
			if (!options.parentId) {
				throw new Error('Invalid argument options.parentId provided');
			}

			if (!options.model) {
				throw new Error('Invalid argument options.model provided');
			}

			payloadData = this.checkPayloadData(payloadData);

			var model = Object.assign({}, options.model);

			// if no id provided on the model, generate a unique id
			// TODO: verify that drag and drop works as expected;
			// possibly will need to always refresh the id or check if model does not already exist
			if (!model.id || !options.modelIdsValid) {
				model.id = UniqueId.get('content');
			}

			if (options.layoutProperties && options.layoutProperties.style) {
				model.style = Object.assign({}, model.style || {}, options.layoutProperties.style);
			}

			this.layout.addArray([{
				parentId: options.parentId,
				insertBefore: options.insertBefore,
				model: model
			}], sender, payloadData);

			return model.id;
		},

		/**
   * Remove a content from the model.
   *
   * @param id
   * @param sender
   * @param payloadData - Additional data that will be included in the event payload.
   */
		removeContent: function removeContent(id, sender, payloadData) {
			payloadData = this.checkPayloadData(payloadData);
			this.layout.removeArray([id], sender, payloadData);
		},

		/**
   * Add a widget to the model.
   *
   * @param options
   *		options.model
   *		options.parentId
   *		options.insertBefore
   *		options.layoutProperties
   * @param sender - If available, the sender will be added to the event payload.
   * @param payloadData - Additional data that will be included in the event payload.
   */
		addWidget: function addWidget(options, sender, payloadData) {
			//If no parent id was passed in, cancel the add
			if (!options.parentId) {
				return;
			}

			// Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
			payloadData = this.checkPayloadData(payloadData);

			var model = this._addWidgetModel(options.model, { id: options.id });
			if (options.id) {
				//this is when a widget was already loaded (but not added to canvas)
				//adding to canvas here will use the same id as in the widget for the widget and layout model
				//adding will not result in reloading of the widget, but will just add the required models (and layout view obj, which will contain the
				//pre-loaded widget)
				model.id = options.id;
			}
			var id = model.id;
			// Add to the layout
			var layoutModel = _.extend({}, options.layoutProperties, {
				type: 'widget',
				id: id
			});
			this.layout.addArray([{
				parentId: options.parentId,
				insertBefore: options.insertBefore,
				model: layoutModel
			}], sender, payloadData);

			return this._triggerAddRemove('addWidget', {
				op: 'addWidget',
				parameter: _.extend({}, options, {
					model: model.toJSON(),
					layoutProperties: $.extend(true, {}, layoutModel)
				})
			}, {
				op: 'removeWidget',
				parameter: id
			}, sender, payloadData);
		},

		/**
   *
   * Add a fragment to the model.
   *
   * passed model can contain a parent layout or array of layouts
   *
   * @param options
   *		options.model which contains
   *			{
   *				layout: {
   *					//one parent layout or parent layout with nested layouts
   *					//one or widgets can be one or more of these layouts
   *					id: xxxx
   *					type: yyy
   *					items: [{
   *							id: x1
   *							type: x2
   *						},
   *					 	...
   *					],
   *					style: {
   *						height: aaa
   *						width: bbb
   *						top: ccc
   *						left: ddd
   *					}
   *				},
   *				episodes : [
   *		            {
   *		                "id": "x1",
   *	                	"type": "widget",
   *		                "acts": [
   *		                    {
   *		                        "id": "act1",
   *		                        "timer": 0,
   *		                        "action": "show"
   *		                    },
   *		                    {
   *		                        "id": "act2",
   *		                        "timer": 5000,
   *		                        "action": "hide"
   *		                    }
   *		                ]
   *		            }
   *				widgets : [
   *					{
   *						id: xxxx
   *						type: yyyy
   *						widget model data
   *					},
   *					...
   *				],
   *				datasetShapings : [
   *					{
   *						id: xxxx,
   *						calculations: []
   *					},
   *					...
   *				]
   *			}
   *		options.parentId
   *		options.insertBefore
   *
   * If layout is an array, it should like:
   * layout: [{
   *				id: xxxx
   *				type: yyy
   *				items: [{
   *						id: x1
   *						type: x2
   *					},
   *				 	...
   *				],
   *				style: {
   *					height: aaa
   *					width: bbb
   *					top: ccc
   *					left: ddd
   *				}
   *			}, {
   *				id: xxxx
   *				type: yyy
   *				items: [{
   *						id: x1
   *						type: x2
   *					},
   *				 	...
   *				]
   *			}]
   * @param sender - If available, the sender will be added to the event payload.
   * @param payloadData - Additional data that will be included in the event payload.
   */
		addFragment: function addFragment(options, sender, payloadData) {
			var _this4 = this;

			// TODO this logic should be moved out of the model and into a BoardModelManager class

			// Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
			payloadData = this.checkPayloadData(payloadData);

			//make a copy
			var fragSpec = _.extend({}, options.model);

			var datasources = this.dashboardApi.getFeature('dataSources.deprecated');
			var sourceCollection = datasources ? datasources.getSourcesCollection() : undefined;
			var sourceIdMap = sourceCollection ? sourceCollection.addSourcesForPin(fragSpec, { payloadData: payloadData }) : {};

			//add all widgets in fragment spec
			var widgetIdMap = {};
			_.each(fragSpec.widgets, function (widgetModel) {
				// replace widget ids in fragment with actual ones if the options tell us to
				// in some cases the id is already unique and valid. Mainly in the redo after undo case
				var oldId = widgetModel.id;
				if (!options.modelIdsValid) {
					widgetModel.id = undefined;
				}

				// Update the modelRef to the new sourceId if necessary
				var dataViews = widgetModel.data ? widgetModel.data.dataViews : null;
				if (dataViews) {
					dataViews.forEach(function (dataView) {
						if (sourceIdMap[dataView.modelRef]) {
							dataView.modelRef = sourceIdMap[dataView.modelRef];
						}
					});
				}

				var model = _this4._addWidgetModel(widgetModel);
				//keep a note of them, so that we can update layout spec
				widgetIdMap[oldId] = model.id;
				//update id in widgetModel with the new id from the model
				//That solves the redo (adding pin back) not working problem
				widgetModel.id = model.id;
			});

			//update the position information in layout if layoutProperies is defined
			//layoutProperties is defined when pin is created by clicking on create button
			if (options.layoutProperties && !_.isArray(fragSpec.layout)) {
				fragSpec.layout.style = fragSpec.layout.style || {};
				fragSpec.layout.style = _.extend(fragSpec.layout.style, options.layoutProperties.style);
			}
			//update widget ids in fragment spec to reflect new widget ids if we have not done it already
			if (!options.modelIdsValid) {
				fragSpec.layout = this._updateFragmentSpecLayout(fragSpec.layout, widgetIdMap, options);
			}

			//update drill through definitions
			var drillThroughService = void 0;
			try {
				drillThroughService = this.dashboardApi.getDashboardCoreSvc('DrillThroughService');
			} catch (error) {
				this.logger.info(error);
			}
			if (drillThroughService) {
				drillThroughService.addDrillThroughOnAddFragment(options.model, sourceIdMap, widgetIdMap, fragSpec);
			}

			// Add any custom colors
			if (fragSpec.properties && fragSpec.properties.customColors && fragSpec.properties.customColors.colors) {
				this.properties.customColors.addCustomColor(fragSpec.properties.customColors.colors);
			}

			if (fragSpec.fredIsRed && _.isEmpty(this.fredIsRed.colorMap)) {
				this.fredIsRed.colorMap = fragSpec.fredIsRed.colorMap;
			}

			//create layout using layout spec
			var layoutPayload = this.layout.addArray(this._getAddArrayPayload(fragSpec, options), sender, payloadData);
			//undo relies on just removing parent layout of the fragment
			var modelArray = layoutPayload.prevValue.parameter;

			// update pageContext
			_.each(fragSpec.pageContext, function (pageContextItem) {
				// TODO: For now, only origin=filter and tupleSet or conditions are handled
				if (pageContextItem.origin === 'filter') {
					var _options = void 0;
					if (pageContextItem.tupleSet) {
						_options = {
							values: _.values(JSON.parse(pageContextItem.tupleSet))
						};
						// TODO: Why is pagecontext conditions an array with'from' and 'to' arrays as well? I tried to see if it is possible
						// TODO: to have more than one value in the conditions property from the UI, I couldn't find a way. The code in
						// TODO: LiveWidget:PageContextRangeConditions.js expects values, it doesn't seem to handle arrays. This should be
						// TODO: looked at to be only scalar values, not arrays and handled in the spec schema accordingly so it validates
					} else if (pageContextItem.conditions) {
						_options = {
							'condition': {
								attributeUniqueNames: pageContextItem.conditions[0].attributeUniqueNames,
								from: pageContextItem.conditions[0].from[0],
								to: pageContextItem.conditions[0].to[0]
							}
						};
					} else {
						_this4.logger.error('Error: AddFragment() - Only able to handle pageContext tupleSet and conditions, can not handle ' + JSON.stringify(pageContextItem));
					}
					_options = _.extend(_options, {
						scope: pageContextItem.scope,
						openViewOnLoad: false,
						exclude: pageContextItem.exclude,
						payloadData: payloadData
					});
					_this4.dashboardApi.getCanvasWhenReady().then(function (canvas) {
						canvas.filterApi.addFilter({
							sourceId: pageContextItem.sourceId,
							itemId: pageContextItem.hierarchyUniqueNames[0]
						}, _options);
					}).catch(function (error) {
						_this4.logger.error(error);
					});
				} else {
					_this4.logger.error('Error: AddFragment() - Only able to handle pagecontext.origin=filter, not ' + pageContextItem.origin);
				}
			});

			//trigger event
			return this._triggerAddRemove('addFragment', {
				op: 'addFragment',
				parameter: _.extend({}, options, {
					model: fragSpec,
					modelIdsValid: true,
					widgetIdMap: widgetIdMap
				})
			}, {
				op: 'removeFragment',
				parameter: modelArray
			}, sender, payloadData);
		},

		_getAddArrayPayload: function _getAddArrayPayload(fragSpec, options) {
			var aResult = [];
			if (_.isArray(fragSpec.layout)) {
				_.each(fragSpec.layout, function (entry) {
					aResult.push({
						parentId: options.parentId,
						insertBefore: options.insertBefore,
						model: entry
					});
				});
				return aResult;
			}
			return [{
				parentId: options.parentId,
				insertBefore: options.insertBefore,
				model: fragSpec.layout
			}];
		},

		_updateFragmentSpecLayout: function _updateFragmentSpecLayout(layout, widgetIdMap) {
			var boardModel = this;
			var update = function update(layout, widgetIdMap) {
				if (_.isArray(layout)) {
					_.each(layout, function (item) {
						update(item, widgetIdMap);
					});
				} else {
					if (layout.type !== 'widget') {
						// create a empty model to get an new ID.
						var layoutModel = boardModel.createLayoutModel({
							items: []
						}, {
							boardModel: boardModel,
							logger: boardModel.logger
						});
						layoutModel.off();
						widgetIdMap[layout.id] = layoutModel.id;
						layout.id = layoutModel.id;
					} else {
						layout.id = widgetIdMap[layout.id];
					}
					_.each(layout.items, function (item) {
						update(item, widgetIdMap);
					});
				}
				return layout;
			};

			if (layout) {
				return update(layout, widgetIdMap);
			} else {
				var addLayout = function addLayout(id) {
					return {
						id: id,
						type: 'widget'
					};
				};
				//this just adds widgets to a group layout - but does nothing about location of widgets or style applied
				//not sure if this is the best strategy

				//add a group layout if spec does not contain a layout
				return {
					type: 'group',
					items: _.map(_.values(widgetIdMap), addLayout)
				};
			}
		},

		_getObjectFromArrayById: function _getObjectFromArrayById(array) {
			var object = _.object(_.map(array, function (item) {
				return [item.id, item];
			}));
			return object;
		},
		/**
   *
   * removes a fragment from the model
   *
   * @param modelIds  array of layout identifiers (typically will be one container id)
   *
   * @param sender - If available, the sender will be added to the event payload.
   * @param payloadData - Additional data that will be included in the event payload.
   */
		removeFragment: function removeFragment(modelIds, sender, payloadData) {
			return this.removeLayouts(modelIds, sender, payloadData);
		},

		/**
   * Remove a widget from the model.
   *
   * @param id
   * @param sender
   * @param payloadData - Additional data that will be included in the event payload.
   */
		removeWidget: function removeWidget(id, sender, payloadData) {

			// Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
			payloadData = this.checkPayloadData(payloadData);

			this.trigger('pre:removeWidget', {
				id: id,
				sender: sender,
				data: _.extend({ runtimeOnly: true }, payloadData)
			});

			var payload = this.layout.removeArray([id], sender, payloadData);
			if (payload) {
				var widgetModel = this.widgetInstances[id];
				var evtData = this._triggerAddRemove('removeWidget', {
					op: 'removeWidget',
					parameter: id
				}, {
					op: 'addWidget',
					parameter: {
						parentId: payload.prevValue.parameter[0].parentId,
						model: widgetModel.toJSON(),
						layoutProperties: _.extend({}, payload.prevValue.parameter[0].model)
					}
				}, sender, payloadData);

				this.widgetInstances[id].off('change', this.onWidgetModelChange, this);

				delete this.widgetInstances[id];

				return evtData;
			}
		},

		getUsedCustomColors: function getUsedCustomColors(customColors) {
			var usedColors = Model.inherited('getUsedCustomColors', this, arguments);

			if (this.widgetInstances) {
				for (var widgetModel in this.widgetInstances) {
					usedColors = usedColors.concat(this.widgetInstances[widgetModel].getUsedCustomColors(customColors));
				}
			}

			if (this.layout) {
				usedColors = usedColors.concat(this.layout.getUsedCustomColors(customColors));
			}

			return _.uniq(usedColors, false);
		},

		/**
   * Remove a list of layouts from the board model. This method will also remove all the widgets contained in the layouts being removed
   *
   * @param id - could be a string with one id or an array if IDs
   * @param sender
   * @param payloadData - Additional data that will be included in the event payload.
   */
		removeLayouts: function removeLayouts(id, sender, payloadData) {
			// Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
			payloadData = this.checkPayloadData(payloadData);

			var idArray = _.isArray(id) ? id : [id];

			this.trigger('pre:removeLayouts', {
				idArray: idArray,
				sender: sender,
				data: _.extend({ runtimeOnly: true }, payloadData)
			});

			var widgetIds = this.layout.listWidgets(idArray);
			var payload = this.layout.removeArray(idArray, sender, payloadData);
			if (payload) {
				var removedWidgets = {};
				var widgetId;
				for (var i = 0; i < widgetIds.length; i++) {
					widgetId = widgetIds[i];
					var widgetModel = this.widgetInstances[widgetId];
					if (widgetModel) {
						removedWidgets[widgetId] = widgetModel.toJSON();
						widgetModel.off();
						delete this.widgetInstances[widgetId];
					}
				}

				return this._triggerAddRemove('removeLayouts', {
					op: 'removeLayouts',
					parameter: idArray,
					removedWidgets: removedWidgets
				}, {
					op: 'addLayouts',
					parameter: {
						widgetSpecMap: removedWidgets,
						addLayoutArray: payload.prevValue.parameter
					}

				}, sender, payloadData);
			}
		},

		/**
   * Add layouts to the board. Layouts can contain widgets
   *
   *	options.widgetSpecMap  A map of widget model json to be added. The map key is the id used in the layout to reference the widget
   *	options.addLayoutArray = [{
   *		parentId - layout parent id where to add this layout
   *		model	- layout model json
   *	}]
   *
   * @param options - widgetSpecMap, addLayoutArray
   * @param sender
   * @param payloadData - Additional data that will be included in the event payload.
   */
		addLayouts: function addLayouts(options, sender, payloadData) {
			// Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
			payloadData = this.checkPayloadData(payloadData);

			// Add widgets
			if (options.widgetSpecMap) {
				for (var id in options.widgetSpecMap) {
					if (options.widgetSpecMap.hasOwnProperty(id)) {
						this._addWidgetModel(options.widgetSpecMap[id], { id: id });
					}
				}
			}

			var layout = options.parentId ? this.layout.findModel(options.parentId) : this.layout;
			if (!layout) {
				layout = this.layout;
			}
			// Add layouts
			var payload = layout.addArray(options.addLayoutArray, sender, payloadData);
			return this._triggerAddRemove('addLayouts', {
				op: 'addLayouts',
				parameter: _.extend({}, options, {
					widgetSpecMap: options.widgetSpecMap,
					addLayoutArray: payload.value.parameter
				})
			}, {
				op: 'removeLayouts',
				parameter: payload.prevValue.parameter
			}, sender, payloadData);
		},

		_isTypeRegistered: function _isTypeRegistered(type) {
			var contentTypeRegistry = this.dashboardApi.getFeature('ContentTypeRegistry');
			return contentTypeRegistry.isTypeRegistered(type);
		},


		duplicateLayout: function duplicateLayout(layoutId, sender, payload) {
			var _this5 = this;

			payload = this.checkPayloadData(payload);
			var layoutModel = this.layout.findModel(layoutId);
			var idMap = {};
			var clone = function clone() {
				if (layoutModel) {
					if (_this5._isTypeRegistered(layoutModel.type)) {
						var containerId = layoutModel.getParent().id;
						var content = _this5.dashboardApi.getFeature('Canvas').getContent(layoutId);
						return _this5.dashboardApi.getFeature('Canvas').addContent({
							containerId: containerId,
							spec: content.getFeature('Serializer').toJSON(),
							copyPaste: true
						}).then(function (newContent) {
							var newId = newContent.getId();
							idMap[layoutId] = newId;
							return newId;
						});
					} else {
						var widgetSpecMap = _this5._cloneWidgets(layoutModel, idMap);
						var _clone = _this5._cloneLayout(layoutModel, idMap, payload);
						var options = {
							parentId: layoutModel.getParent().id,
							widgetSpecMap: widgetSpecMap,
							addLayoutArray: [{
								model: _clone.toJSON(),
								insertBefore: layoutModel.type === 'widget' ? null : layoutModel.getNextSiblingId()
							}]
						};
						_this5.addLayouts(options, sender, payload);
						return Promise.resolve(_clone.id);
					}
				}
				return Promise.resolve();
			};
			return clone().then(function (cloneId) {
				// all work in being done in other methods that take care of the undo/redo stack management
				// however we still want to notify interested classes that a duplicate occurred.
				// simplest way is to manually trigger an event and not include a senders context
				// this should bypass the undoduplicateLayout/redo stack since this method did not do anything that needs to be undone.
				// we still send the payload data in case someone else needs to do some undoable work in this transaction.
				_this5.trigger('duplicateLayout', {
					layoutId: layoutId,
					cloneId: cloneId,
					idMap: idMap,
					sender: sender,
					data: payload
				});
				return cloneId;
			});
		},

		duplicateSelection: function duplicateSelection(options, sender, payload) {
			var ids = [];
			var payloadData = this.checkPayloadData(payload);
			payloadData.limitToBounds = [];
			_.each(options.modelIds, function (id) {
				if (options.outBoundIds && options.outBoundIds.filter(function (e) {
					return e.id === id;
				}).length > 0) {
					payloadData.limitToBounds.push(options.outBoundIds.filter(function (e) {
						return e.id === id;
					}));
				}
				ids.push(this.duplicateLayout(id, sender, payloadData));
			}.bind(this));
			return ids;
		},

		updateLayoutType: function updateLayoutType(options, sender, payloadData) {
			payloadData = this.checkPayloadData(payloadData);
			var layout = options.id ? this.layout.findModel(options.id) : this.layout;
			if (layout && options.type) {
				layout.set({
					type: options.type
				}, {
					sender: sender,
					payloadData: payloadData
				});
			}
		},

		/**
   * Update a layout's drop zones only if the layout has at least one drop zone.
   * All existing widgets will go untouched as this is purely a layout drop zone change
   *
   * @param options.id {string} The id of the layout that has the template/drop zones to be removed/added to
   * @param options.model {object} The user selected layout model that contains the drop zones
   * @param sender {object} use if a previous action has taken place and we want to combine this action with it
   * @param payload {object} the payload of a previous action that has taken place
   */
		updateLayoutDropZones: function updateLayoutDropZones(options, sender, payload) {
			/*
    * This little bit of code looks odd but the LayoutModel.findModel() method can potentially return null.
    * For this reason we need to do the check that the layout variable is a falsey value and if
    * so then we use the boardModel's layout instance
    */
			var layout;
			if (options.id) {
				layout = this.layout.findModel(options.id);
			}
			if (!layout) {
				layout = this.layout;
			}

			// Try to get a list of drop zones in the selected layout
			var dropZones = layout.findDropZones();
			// If no drop zones then there is nothing else to do
			if (!dropZones) {
				return payload;
			}

			// Remove the drop zones while saving the payload because it is needed when adding the new drop zones
			payload = this.removeLayouts(dropZones.ids, sender, payload);

			/*
    * Create the array list of drop zone layouts to add
    * Set the 'insertBefore' property to be the first widget as all widgets need to come after drop zone layouts.
    * If there are no widgets then firstWidget returns undefined which is fine
    */
			var firstWidget = layout.listWidgets([layout.id])[0];
			var addArray = [];
			for (var i = 0; i < options.model.items[0].items.length; i++) {
				addArray.push({
					parentId: dropZones.parentId,
					model: options.model.items[0].items[i],
					insertBefore: firstWidget
				});
			}

			payload = this.addLayouts({
				parentId: dropZones.parentId,
				addLayoutArray: addArray
			}, payload.sender, payload.data);

			return payload;
		},

		_cloneWidgets: function _cloneWidgets(layoutModel, idMap) {
			var widgetSpecMap = {};
			var widgets = layoutModel.listWidgets([layoutModel.id]);
			_.each(widgets, function (widgetId) {
				var widgetModel = this.widgetInstances[widgetId];
				var clonedWidget = widgetModel.cloneWidget(idMap);
				widgetSpecMap[clonedWidget.id] = clonedWidget.toJSON();
			}.bind(this));
			return widgetSpecMap;
		},

		_cloneLayout: function _cloneLayout(layoutModel, idMap, payload) {
			var clone = layoutModel.cloneLayout(idMap);
			if ((clone.type === 'widget' || clone.type === 'group') && clone.style && payload.limitToBounds && payload.limitToBounds.length <= 0) {
				// offset layout by 5% / 25px down/right for the cloned
				clone.style.top = clone.incrementStyleValue(clone.style.top);
				clone.style.left = clone.incrementStyleValue(clone.style.left);
			} else if (payload.limitToBounds && payload.limitToBounds.length > 0) {
				payload.limitToBounds[0].forEach(function (element) {
					if (element.bottom) {
						clone.style.top = clone.incrementStyleValue(clone.style.top);
					}
					if (element.right) {
						clone.style.left = clone.incrementStyleValue(clone.style.left);
					}
					if (!element.right && !element.bottom) {
						if (parseFloat(clone.decrementStyleValue(clone.style.top)) > 0) {
							clone.style.top = clone.decrementStyleValue(clone.style.top);
						}

						if (parseFloat(clone.decrementStyleValue(clone.style.left)) > 0) {
							clone.style.left = clone.decrementStyleValue(clone.style.left);
						}
					}
				});
			}
			return clone;
		},

		getLanguageModelOptions: function getLanguageModelOptions() {
			var addDashboardTranslatedLocales = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

			var languageOptions = {
				defaultLocale: this.defaultLocale,
				contentLocale: this.contentLocale,
				translationLocale: this.translationLocale
			};

			// Add in all the locales the dashboard is currently translated in.  This is needed so that new MultilingualAttributes
			// default to the correct locale
			if (addDashboardTranslatedLocales) {
				var translationService = this.dashboardApi.getDashboardCoreSvc('TranslationService');
				languageOptions.availableDashboardLocales = translationService.getSelectedLanguages();
			}

			return languageOptions;
		},

		_addWidgetModel: function _addWidgetModel(widgetSpec) {
			var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

			if (!widgetSpec.name) {
				widgetSpec.name = '';
			}

			//The widget registry supports plugin model classes for times when custom models need to be defined.
			//Live Widget (for example) is defined in an external component and has a custom model class.
			var WidgetModelClass = WidgetModel;

			if (widgetSpec && this.widgetRegistry && this.widgetRegistry[widgetSpec.type] && this.widgetRegistry[widgetSpec.type].ModelClass) {
				WidgetModelClass = this.widgetRegistry[widgetSpec.type].ModelClass;
			}

			var id = options.id,
			    _options$addDashboard = options.addDashboardTranslatedLocales,
			    addDashboardTranslatedLocales = _options$addDashboard === undefined ? true : _options$addDashboard;

			var languageModelOptions = this.getLanguageModelOptions(addDashboardTranslatedLocales);

			var model = new WidgetModelClass(widgetSpec, languageModelOptions);
			model.on('change', this.onWidgetModelChange, this);

			this.widgetInstances[id || model.id] = model;
			return model;
		},

		/**
   * Called when a layout change happens  that we want to propagate. (e.g. layout move, resize, etc..)
   * The layout model takes care of creating the event payload with the undo/redo handler.
   *
   * @param payload
   * @param sender
   */
		onLayoutChange: function onLayoutChange(payload, sender) {
			this.trigger('change:layout', payload, sender);
		},

		/**
   * Helper function used to trigger the add/remove event
   *
   * @param eventName
   * @param value
   * @param prevValue
   * @param sender
   * @param payloadData - Additional data that will be included in the event payload.
   */
		_triggerAddRemove: function _triggerAddRemove(eventName, value, prevValue, sender, payloadData) {
			var payload = {
				value: value,
				prevValue: prevValue,
				sender: sender,
				senderContext: {
					applyFn: this.applyFn.bind(this)
				},
				data: payloadData
			};
			this.trigger(eventName, payload);
			return payload;
		},

		/**
   * Function used to handler the undo/redo for the board model updates
   * @param value
   * @param sender
   */
		applyFn: function applyFn(value, sender, name, payload) {
			if (value.op && typeof this[value.op] === 'function') {
				var args = [value.parameter];
				args.push(sender);
				args.push(payload);
				this[value.op].apply(this, args);
			} else {
				Model.inherited('applyFn', this, arguments);
			}
		},

		/**
   * Handles changes from widget models (which can be added/removed as widgets are added/removed) and notifies
   * listener of the change (so listener does not need to track addition/deletion of widget models
   *
   * @param payload
   */
		onWidgetModelChange: function onWidgetModelChange(payload) {
			var senderContext = payload.senderContext || {};
			var modelId = _.isObject(payload.sender) ? payload.sender.id : payload.sender;

			//TODO: Need to revist this when porting to Endor, why are we overwriting the applyFn?
			//https://github.ibm.com/BusinessAnalytics/dashboard-core/pull/1311
			if (payload && payload.senderContext && this.widgetInstances[modelId]) {
				payload.senderContext.applyFn = function () {
					var model = this.widgetInstances[modelId];
					if (model) {
						model.applyFn.apply(model, arguments);
					}
				}.bind(this);
			}

			this.trigger('widget:change', _.extend({
				modelId: payload.model ? payload.model.id : modelId,
				senderContext: senderContext
			}, payload));
		},

		toJSON: function toJSON() {
			var spec = Model.inherited('toJSON', this, [null, ['layout']]);

			var canvas = this.dashboardApi.getFeature('Canvas');
			var topLevelContent = canvas.findContent({ type: this.layout.type })[0];
			spec.layout = topLevelContent.getFeature('Serializer').toJSON();

			return spec;
		},

		/**
   * Check the payload data
   * Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
   * @param {Object} payloadData
   */
		// TODO: we're actually not "checking" anything in this method, should this be renamed?
		checkPayloadData: function checkPayloadData(payloadData) {
			if (payloadData) {
				return payloadData;
			}
			return {
				undoRedoTransactionId: _.uniqueId('boardModelTransaction')
			};
		},

		/**
   * Find widget model by id and type.
   *
   * @param {string} id - widget id
   *
   * @return {WidgetModel} widget model instance found
   */
		findWidgetById: function findWidgetById(id) {
			return this.widgetInstances ? this.widgetInstances[id] : null;
		},

		/**
   * Find widget model matching one of the ids
   *
   * @param {string[]} ids - widget ids to match
   *
   * @return {WidgetModel} widget model instance found
   */
		findWidgetByIds: function findWidgetByIds(ids) {
			var finding;
			ids.some(function (id) {
				var breakLoop = false;
				var widget = this.findWidgetById(id);
				if (widget) {
					finding = widget;
					breakLoop = true;
				}
				return breakLoop;
			}.bind(this));
			return finding;
		},

		findWidgetByCriteriaFn: function findWidgetByCriteriaFn(criteriaFn) {
			var finding;
			for (var key in this.widgetInstances) {
				if (this.widgetInstances.hasOwnProperty(key)) {
					var widgetInstance = this.widgetInstances[key];
					if (criteriaFn(widgetInstance)) {
						finding = widgetInstance;
						break;
					}
				}
			}
			return finding;
		},

		filterWidgetsByCriteriaFn: function filterWidgetsByCriteriaFn(criteriaFn) {
			var findings = [];
			for (var key in this.widgetInstances) {
				if (this.widgetInstances.hasOwnProperty(key)) {
					var widgetInstance = this.widgetInstances[key];
					if (criteriaFn(widgetInstance)) {
						findings.push(widgetInstance);
					}
				}
			}
			return findings;
		},

		/**
   * Override the base method since we need to deal with our widgetModels and layoutModel which are
   * not real nested models
   */
		getContentReferences: function getContentReferences() {
			var deploymentRefs = Model.inherited('getContentReferences', this, arguments);

			if (this.widgetInstances) {
				for (var widgetModel in this.widgetInstances) {
					deploymentRefs = deploymentRefs.concat(this.widgetInstances[widgetModel].getContentReferences());
				}
			}

			if (this.layout) {
				deploymentRefs = deploymentRefs.concat(this.layout.getContentReferences());
			}

			return _.uniq(deploymentRefs, false, function (ref) {
				return ref.value;
			});
		},

		/**
   * Should only be used by dashboard Serializer feature
   * @return {Object} the inital board spec
   */
		getInitialSpec: function getInitialSpec() {
			return this._initBoardSpec;
		},

		/**
   * Delete the initial board spec, it is invoked only once when canvas is ready
   */
		deleteInitialSpec: function deleteInitialSpec() {
			delete this._initBoardSpec;
		}
	});
	return Model;
});
//# sourceMappingURL=BoardModel.js.map