/*!
 * ${copyright}
 */

// Provides control sap.ui.core.ComponentContainer.
sap.ui.define([
	'sap/ui/base/ManagedObject',
	'./Control',
	'./Component',
	'./library',
	"./ComponentContainerRenderer",
	"sap/base/Log",
	"sap/ui/core/Configuration"
],
	function(
		ManagedObject,
		Control,
		Component,
		library,
		ComponentContainerRenderer,
		Log,
		Configuration
	) {
	"use strict";


	var ComponentLifecycle = library.ComponentLifecycle;


	/**
	 * Constructor for a new ComponentContainer.
	 *
	 * @param {string} [sId] id for the new control, generated automatically if no id is given
	 * @param {object} [mSettings] initial settings for the new control
	 *
	 * @class Container that embeds a <code>sap/ui/core/UIComponent</code> in a control tree.
	 *
	 * <b>Concerning asynchronous component loading:</b>
	 *
	 * To activate a fully asynchronous loading behavior of components and their dependencies,
	 * the property <code>async</code> needs to be set to <code>true</code> and
	 * the <code>manifest</code> property needs to be set to a 'truthy' value, e.g. <code>true</code> or a URL to the manifest location.
	 * If both options are correctly set, the component factory will load and evaluate the component manifest first.
	 * In this way, the additional dependencies of the Component are already known before the Component preload/controller is loaded.
	 * Both the component preload/controller and the additional dependencies can thus be loaded asynchronously and in parallel.
	 *
	 * Sample usage of the ComponentContainer:
	 *
	 * <pre>
	 *     &lt;!-- inside XML view -->
	 *     ...
	 *     &lt;core:ComponentContainer
	 *         usage="someComponent"
	 *         manifest="true"
	 *         async="true"
	 *     />
	 * </pre>
	 *
	 * See also {@link module:sap/ui/core/ComponentSupport}.
	 *
	 * @extends sap.ui.core.Control
	 * @version ${version}
	 *
	 * @public
	 * @alias sap.ui.core.ComponentContainer
	 */
	var ComponentContainer = Control.extend("sap.ui.core.ComponentContainer", /** @lends sap.ui.core.ComponentContainer.prototype */ {
		metadata : {
			interfaces: [
				"sap.ui.core.IPlaceholderSupport"
			],
			library : "sap.ui.core",
			properties : {

				/**
				 * Component name, the package where the component is contained. This property can only be applied initially.
				 */
				name : {type : "string", defaultValue : null},

				/**
				 * The URL of the component. This property can only be applied initially.
				 */
				url : {type : "sap.ui.core.URI", defaultValue : null},

				/**
				 * Flag whether the component should be created sync (default) or async. The default
				 * will be async when initially the property <code>manifest</code> is set to a truthy
				 * value and for the property <code>async</code> no value has been specified.
				 * This property can only be applied initially.
				 */
				async : {type : "boolean", defaultValue : false},

				/**
				 * Enable/disable validation handling by MessageManager for this component.
				 * The resulting Messages will be propagated to the controls.
				 * This property can only be applied initially.
				 */
				handleValidation : {type : "boolean", defaultValue : false},

				/**
				 * The settings object passed to the component when created. This property can only be applied initially.
				 */
				settings : {type : "object", defaultValue : null},

				/**
				 * Defines whether binding information is propagated to the component.
				 */
				propagateModel : {type : "boolean", defaultValue : false},

				/**
				 * Container width in CSS size
				 */
				width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : null},

				/**
				 * Container height in CSS size
				 */
				height : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : null},

				/**
				 * Lifecycle behavior for the Component associated by the <code>ComponentContainer</code>.
				 * The default behavior is <code>Legacy</code>. This  means that the <code>ComponentContainer</code>
				 * takes care that the Component is destroyed when the <code>ComponentContainer</code> is destroyed,
				 * but it is <b>not</b> destroyed when a new Component is associated.
				 * If you use the <code>usage</code> property to create the Component,
				 * the default behavior is <code>Container</code>. This means that
				 * the Component is destroyed when the <code>ComponentContainer</code> is destroyed or a new
				 * Component is associated.
				 * This property must only be applied before a component instance is created.
				 */
				lifecycle : {type : "sap.ui.core.ComponentLifecycle", defaultValue : ComponentLifecycle.Legacy},

				/**
				 * Flag, whether to auto-prefix the ID of the nested Component or not. If
				 * this property is set to true the ID of the Component will be prefixed
				 * with the ID of the ComponentContainer followed by a single dash.
				 * This property can only be applied initially.
				 */
				autoPrefixId : {type : "boolean", defaultValue: false},

				/**
				 * The name of a component usage as configured in the app descriptor of the component
				 * owning this <code>ComponentContainer</code>.
				 *
				 * The configuration from the named usage will be used to create a component instance for this
				 * <code>ComponentContainer</code>. If there's no owning component or if its app descriptor
				 * does not contain a usage with the given name, an error will be logged.
				 *
				 * See {@link topic:346599f0890d4dfaaa11c6b4ffa96312 Using and Nesting Components} for more
				 * information about component usages.
				 *
				 * This property can only be applied initially.
				 */
				usage : {type : "string", defaultValue : null},

				/**
				 * Controls when and from where to load the manifest for the Component.
				 * When set to any truthy value, the manifest will be loaded asynchronously by default
				 * and evaluated before the Component controller, if it is set to a falsy value
				 * other than <code>undefined</code>, the manifest will be loaded after the controller.
				 * A non-empty string value will be interpreted as the URL location from where to load the manifest.
				 * A non-null object value will be interpreted as manifest content.
				 * This property can only be applied initially.
				 */
				manifest: {type : "any" /* type: "string|boolean|object" */, defaultValue : null}

			},
			associations : {

				/**
				 * The component displayed in this ComponentContainer.
				 */
				component : {type : "sap.ui.core.UIComponent", multiple : false}
			},
			events : {

				/**
				 * Fired when the component instance has been created by the
				 * ComponentContainer.
				 * @since 1.50
				 */
				componentCreated : {
					parameters : {
						/**
						 * Reference to the created component instance
						 */
						component : { type: "sap.ui.core.UIComponent" }
					}
				},
				/**
				 * Fired when the creation of the component instance has failed.
				 *
				 * By default, the <code>ComponentContainer</code> also logs the error that occurred.
				 * Since 1.83, this default behavior can be prevented by calling <code>preventDefault()</code>
				 * on the event object.
				 *
				 * @since 1.60
				 */
				componentFailed : {
					allowPreventDefault: true,
					parameters : {
						/**
						 * The reason object as returned by the component promise
						 */
						reason : { type: "object" }
					}
				}
			},
			designtime: "sap/ui/core/designtime/ComponentContainer.designtime"
		},
		renderer: ComponentContainerRenderer
	});


	/*
	 * Helper function to set the new Component of the container.
	 */
	function setContainerComponent(oComponentContainer, vComponent, bSuppressInvalidate, bDestroyOldComponent) {
		// find the reference to the current component and to the old component
		var oComponent = typeof vComponent === "string" ? Component.get(vComponent) : vComponent;
		var oOldComponent = oComponentContainer.getComponentInstance();
		// if there is no difference between the old and the new component just skip this setter
		if (oOldComponent !== oComponent) {
			// unlink the old component from the container
			if (oOldComponent) {
				oOldComponent.setContainer(undefined);
				if (bDestroyOldComponent) {
					oOldComponent.destroy();
				} else {
					// cleanup the propagated properties in case of not destroying the component
					oComponentContainer._propagateProperties(true, oOldComponent, ManagedObject._oEmptyPropagatedProperties, true);
				}
			}
			// set the new component
			oComponentContainer.setAssociation("component", oComponent, bSuppressInvalidate);
			// cross link the new component and propagate the properties (models)
			oComponent = oComponentContainer.getComponentInstance();
			if (oComponent) {
				oComponent.setContainer(oComponentContainer);
				oComponentContainer.propagateProperties(true); //propagate all
			}
		}
	}


	/**
	 * Returns the real component instance which is associated with the container.
	 * @return {sap.ui.core.UIComponent} the component instance
	 */
	ComponentContainer.prototype.getComponentInstance = function () {
		var sComponentId = this.getComponent();
		return sComponentId && Component.get(sComponentId);
	};

	// Delegate registered by the ComponentContainer#showPlaceholder function
	var oPlaceholderDelegate = {
		/**
		 * @this {sap.ui.core.ComponentContainer}
		 * @private
		 */
		"onAfterRendering": function() {
			// check whether the placeholder is still active. If yes, show the placeholder again
			if (this._placeholder) {
				this._placeholder.show(this);
			}
		}
	};

	/**
	 * Shows the provided placeholder on the component container.
	 *
	 * @param {object} mSettings Object containing the placeholder object
	 * @param {sap.ui.core.Placeholder} mSettings.placeholder The placeholder instance
	 * @return {Promise} Promise that resolves with the placeholder
	 *
	 * @private
	 * @ui5-restricted SAPUI5 Distribution libraries only
	 * @since 1.91
	 */
	ComponentContainer.prototype.showPlaceholder = function(mSettings) {
		var pLoaded;

		if (!Configuration.getPlaceholder()) {
			return;
		}

		if (this._placeholder) {
			this.hidePlaceholder();
		}

		if (mSettings.placeholder) {
			this._placeholder = mSettings.placeholder;
			pLoaded = this._placeholder._load();
		} else {
			pLoaded = Promise.resolve();
		}

		if (this.getDomRef() && this._placeholder) {
			this._placeholder.show(this);
		}

		// Add an event delegate to reinsert the placeholder after it's removed after a rerendering
		this.addEventDelegate(oPlaceholderDelegate, this);

		return pLoaded;
	};

	/**
	 * Hides the placeholder that is shown on the component container.
	 *
	 * @private
	 * @ui5-restricted SAP internal apps
	 * @since 1.91
	 */
	ComponentContainer.prototype.hidePlaceholder = function() {
		if (this._placeholder) {
			this._placeholder.hide();

			// remove the delegate because the placeholder is hidden
			this.removeEventDelegate(oPlaceholderDelegate);
			this._placeholder = undefined;
		}
	};

	/**
	 * Sets the component of the container. Depending on the ComponentContainer's
	 * lifecycle this might destroy the old associated Component.
	 *
	 * Once the component is associated with the container the cross connection
	 * to the component will be set and the models will be propagated if defined.
	 * If the <code>usage</code> property is set the ComponentLifecycle is processed like a "Container" lifecycle.
	 *
	 * @param {string|sap.ui.core.UIComponent} vComponent ID of an element which becomes the new target of this component association. Alternatively, an element instance may be given.
	 * @return {this} the reference to <code>this</code> in order to allow method chaining
	 * @public
	 */
	ComponentContainer.prototype.setComponent = function(vComponent, bSuppressInvalidate) {
		setContainerComponent(this, vComponent, bSuppressInvalidate,
			this.getLifecycle() === ComponentLifecycle.Container
			|| (typeof this.getUsage() === "string" && this.getUsage() && this.getLifecycle() === ComponentLifecycle.Legacy)
		);
		return this;
	};


	/*
	 * overrule and adopt initial values
	 */
	ComponentContainer.prototype.applySettings = function(mSettings, oScope) {
		if (mSettings) {
			// The "manifest" property has type "any" to be able to handle string|boolean|object.
			// When using the ComponentContainer in a declarative way (e.g. XMLView), boolean values
			// are passed as string. Therefore this type conversion needs to be done manually.
			// As this use-case is only relevant initially the handling is done in "applySettings"
			// instead of overriding "setManifest".
			if (mSettings.manifest === "true" || mSettings.manifest === "false") {
				mSettings.manifest = mSettings.manifest === "true";
			}

			// a truthy value for the manifest property will set the property
			// async to true if not provided initially
			if (mSettings.manifest && mSettings.async === undefined) {
				mSettings.async = true;
			}
		}
		Control.prototype.applySettings.apply(this, arguments);
	};

	/*
	 * Helper to create the settings object for the Component Factory or the
	 * createComponent function.
	 */
	function createComponentConfig(oComponentContainer) {
		var sName = oComponentContainer.getName();
		var vManifest = oComponentContainer.getManifest();
		var sUrl = oComponentContainer.getUrl();
		var mSettings = oComponentContainer.getSettings();
		var mConfig = {
			name: sName ? sName : undefined,
			manifest: vManifest !== null ? vManifest : false,
			async: oComponentContainer.getAsync(),
			url: sUrl ? sUrl : undefined,
			handleValidation: oComponentContainer.getHandleValidation(),
			settings: mSettings !== null ? mSettings : undefined
		};
		return mConfig;
	}

	/**
	 * Private helper to create the component instance based on the
	 * configuration of the Component Container
	 * @return {Promise|sap.ui.core.Component} a Promise for async and for sync scenarios a Component instance
	 * @private
	 */
	ComponentContainer.prototype._createComponent = function() {
		// determine the owner component
		var oOwnerComponent = Component.getOwnerComponentFor(this),
			sUsageId = this.getUsage(),
			mConfig = createComponentConfig(this);

		// First, enhance the config object with "usage" definition from manifest
		if (sUsageId) {
			if (oOwnerComponent) {
				mConfig = oOwnerComponent._enhanceWithUsageConfig(sUsageId, mConfig);
			} else {
				Log.error("ComponentContainer \"" + this.getId() + "\" does have a \"usage\", but no owner component!");
			}
		}

		// Then, prefix component ID with the container ID, as the ID might come from
		// the usage configuration in the manifest
		if (this.getAutoPrefixId()) {
			if (mConfig.id) {
				mConfig.id = this.getId() + "-" + mConfig.id;
			}
			if (mConfig.settings && mConfig.settings.id) {
				mConfig.settings.id = this.getId() + "-" + mConfig.settings.id;
			}
		}

		// Finally, create the component instance
		return Component._createComponent(mConfig, oOwnerComponent);
	};

	/*
	 * delegate the onBeforeRendering to the component instance
	 */
	ComponentContainer.prototype.onBeforeRendering = function() {

		// check if we have already a valid component instance
		// in this case we skip the component creation via props
		// ==> not in applySettings to make sure that components are lazy instantiated,
		//     e.g. in case of invisible containers the component will not be created
		//     immediately in the constructor.
		var oComponent = this.getComponentInstance(),
			sUsage = this.getUsage(),
			sName = this.getName(),
			sManifest = this.getManifest();
		if (!this._oComponentPromise && !oComponent && (sUsage || sName || sManifest)) {
			// create the component instance with the local configuration
			oComponent = this._createComponent();
			// check whether it is needed to delay to set the component or not
			if (oComponent instanceof Promise) {
				this._oComponentPromise = oComponent;
				oComponent.then(function(oComponent) {
					delete this._oComponentPromise;
					// set the component and invalidate to ensure a re-rendering!
					this.setComponent(oComponent);
					// notify listeners that a new component instance has been created
					this.fireComponentCreated({
						component: oComponent
					});
				}.bind(this), function(oReason) {
					delete this._oComponentPromise;
					// listeners can prevent the default log entry
					if ( this.fireComponentFailed({ reason: oReason }) ) {
						Log.error("Failed to load component for container " + this.getId(), oReason);
					}
				}.bind(this));
			} else if (oComponent) {
				this.setComponent(oComponent, true);
				// notify listeners that a new component instance has been created
				this.fireComponentCreated({
					component: oComponent
				});
			} else {
				this.fireComponentFailed({
					reason: new Error("The component could not be created.")
				});
			}
		}

		// delegate the onBeforeRendering to the component instance
		if (oComponent && oComponent.onBeforeRendering) {
			oComponent.onBeforeRendering();
		}

	};

	/*
	 * delegate the onAfterRendering to the component instance
	 */
	ComponentContainer.prototype.onAfterRendering = function() {
		var oComponent = this.getComponentInstance();
		if (oComponent && oComponent.onAfterRendering) {
			oComponent.onAfterRendering();
		}
	};


	/*
	 * once the container is destroyed we remove the reference to the container
	 * in the component and destroy the component unless its lifecycle is managed
	 * by the application.
	 */
	ComponentContainer.prototype.exit = function() {
		setContainerComponent(this, undefined, true,
			this.getLifecycle() !== ComponentLifecycle.Application);
	};


	/*
	 * overridden to support property propagation to the associated component
	 */
	ComponentContainer.prototype.propagateProperties = function (vName) {
		var oComponent = this.getComponentInstance();
		if (oComponent && this.getPropagateModel()) {
			this._propagateProperties(vName, oComponent);
		}
		Control.prototype.propagateProperties.apply(this, arguments);
	};

	/*
	 * overridden to support contextual settings propagation to the associated component
	 * no need to call the parent prototype method as there are no aggregations to propagate to
	 */
	ComponentContainer.prototype._propagateContextualSettings = function () {
		var oComponent = this.getComponentInstance();
		if (oComponent) {
			oComponent._applyContextualSettings(this._getContextualSettings());
		}
	};

	return ComponentContainer;

});
