/*!
 * OpenUI5
 * (c) Copyright 2009-2026 SAP SE or an SAP affiliate company.
 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
 */

sap.ui.define([
	"sap/ui/core/EventBus",
	"sap/base/util/isPlainObject",
	"sap/base/Log"
],
function (
	EventBus,
	isPlainObject,
	Log
) {
	"use strict";

	var oInstance;
	var s_UI5_MARKER = '______UI5______';

	/**
	 * @class
	 * Responsible for the communication between different window objects.
	 *
	 * <h3>Overview</h3>
	 * This class is a singleton. The class instance can be retrieved as follows:
	 * <ul>
	 *   <li>via the constructor <code>new sap.ui.core.postmessage.Bus()</code></li>
	 *   <li>via the static method <code>sap.ui.core.postmessage.Bus.getInstance()</code></li>
	 * </ul>
	 *
	 * For supported data types for payload messages, see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm}.
	 *
	 * @extends sap.ui.core.EventBus
	 * @alias sap.ui.core.postmessage.Bus
	 * @author SAP SE
	 * @since 1.56.0
	 * @version 1.120.46
	 * @private
	 * @ui5-restricted sap.ui.core.support, sap.ui.support, sap.ui.rta
	 */
	var PostMessageBus = EventBus.extend("sap.ui.core.postmessage.Bus", {
		constructor: function () {
			if (oInstance) {
				return oInstance;
			}

			oInstance = this;

			EventBus.apply(this, arguments);

			this._aAcceptedOrigins = [
				// Accept messages from the same origin automatically
				window.location.origin
			];
			this._aDeclinedOrigins = [];
			this._oPendingProcess = null;
			this._aEventQueue = [];

			this._receiver = this._receiver.bind(this);
			window.addEventListener('message', this._receiver);
		}
	});

	PostMessageBus.event = {
		CONNECT: '______CONNECT______',
		READY: '______READY______',
		ACCEPTED: '______ACCEPTED______',
		DECLINED: '______DECLINED______'
	};

	PostMessageBus.prototype.destroy = function () {
		window.removeEventListener('message', this._receiver);
		this._aEventQueue = []; // Erase event queue
		EventBus.prototype.destroy.apply(this, arguments);
		oInstance = undefined;
		this.bIsDestroyed = true;
	};

	/**
	 * Returns an instance of the class.
	 * @returns {sap.ui.core.postmessage.Bus} An instance of the class.
	 * @static
	 * @public
	 */
	PostMessageBus.getInstance = function () {
		if (!oInstance) {
			oInstance = new PostMessageBus();
		}
		return oInstance;
	};

	/**
	 * Publish message into cross-window communication channel
	 * @param {object} mParameters - Map with params
	 * @param {Window} mParameters.target - Window object of receiving window
	 * @param {string} mParameters.origin - Origin of the receiving window, e.g. http://example.com
	 * @param {string} mParameters.channelId - Channel identifier
	 * @param {string} mParameters.eventId - Event identifier
	 * @param {*} [mParameters.data] - Payload data. For supported data types see - {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm}
	 * @throws {TypeError} when invalid data is specified
	 * @public
	 */
	PostMessageBus.prototype.publish = function (mParameters) {
		var oTarget = mParameters.target;
		var sOrigin = mParameters.origin;
		var sChannelId = mParameters.channelId;
		var sEventId = mParameters.eventId;
		var vData = mParameters.data;

		// Defaults for READY event
		if (sEventId === PostMessageBus.event.READY) {
			if (!oTarget) {
				if (window.opener && window.opener !== window) {
					oTarget = window.opener;
				} else if (window.parent !== window) {
					oTarget = window.parent;
				} else {
					return; // Ignore ready event when there is no valid target
				}
			}
			if (!sOrigin) {
				sOrigin = '*';
			}
		}

		// Validation
		if (
			(typeof window === "undefined") || !(oTarget != null && oTarget === oTarget.window)
			|| oTarget === window // avoid self-messaging
		) {
			throw TypeError("Target must be a window object and has to differ from current window");
		}
		if (typeof sOrigin !== "string") {
			throw TypeError("Origin must be a string");
		}
		if (typeof sChannelId !== "string") {
			throw TypeError("ChannelId must be a string");
		}
		if (typeof sEventId !== "string") {
			throw TypeError("EventId must be a string");
		}

		// Accept host immediately when message is sent
		if (
			![
				PostMessageBus.event.READY,
				PostMessageBus.event.ACCEPTED,
				PostMessageBus.event.DECLINED
			].includes(sEventId)
			&& sOrigin !== '*'
			&& !this._aAcceptedOrigins.includes(sOrigin)
		) {
			this._aAcceptedOrigins.push(sOrigin);
		}

		var mMessage = {
			origin: sOrigin,
			channelId: sChannelId,
			eventId: sEventId,
			data: vData
		};

		mMessage[s_UI5_MARKER] = true;

		oTarget.postMessage(mMessage, sOrigin);
	};

	/**
	 * Attaches an event handler to the event with the given identifier on the given event channel
	 *
	 * @name sap.ui.core.postmessage.Bus.prototype.subscribe
	 * @function
	 *
	 * @param {string}
	 *            [sChannelId] Channel of the event to subscribe to. If not given, the default channel is used.
	 *                         The channel <code>"sap.ui"</code> is reserved by the UI5 framework. An application can listen to
	 *                         events on this channel, but is not allowed to publish its own events there.
	 * @param {string}
	 *            sEventId Identifier of the event to listen for
	 * @param {function}
	 *            fnFunction Handler function to call when the event occurs; this function is called in the context of the
	 *                       <code>oListener</code> instance (if present) or on the event bus instance. Arguments:
	 * <pre>
	 * - {string} sChannelId - Channel ID
	 * - {string} sEventId - Event ID
	 * - {object} mParameters - Event parameters
	 *     - {Event} originalEvent - Post message original event
	 *     - {string} channelId - Channel ID
	 *     - {string} eventId - Event ID
	 *     - {Window} source - Sender window
	 *     - {string} origin - Sender origin, e.g. https://example.com
	 *     - {*} [data] - Payload data
	 * </pre>
	 * @param {object}
	 *            [oListener] Object that wants to be notified when the event occurs (<code>this</code> context within the
	 *                        handler function). If it is not specified, the handler function is called in the context of the event bus.
	 * @return {this} Returns <code>this</code> to allow method chaining
	 * @public
	 */

	PostMessageBus.prototype._callListener = function (fnCallback, oListener, sChannelId, sEventId, mData) {
		fnCallback.call(oListener, mData);
	};

	/**
	 * Gets translation from the message bundle
	 * @param {string} sKey - Translation key
	 * @param {string[]} [aParameters] - Translation placeholders
	 * @return {string} - returns resolved string
	 * @private
	 */
	PostMessageBus.prototype._getText = function (sKey, aParameters) {
		return sap.ui.getCore().getLibraryResourceBundle(true)
		.then(function(oLibraryResourceBundle) { return oLibraryResourceBundle.getText(sKey, aParameters); });
	};

	/**
	 * Event handler for receiving incoming post messages
	 * @param {Event} oEvent - Browser post message event
	 * @private
	 */
	PostMessageBus.prototype._receiver = function (oEvent) {
		var mData = oEvent.data;

		if (
			// Ignore non-UI5 messages
			!isPlainObject(mData)
			|| !mData.hasOwnProperty(s_UI5_MARKER)
		) {
			return;
		}

		if (this._oPendingProcess) {
			this._aEventQueue.push(oEvent);
		} else {
			this._oPendingProcess = this._processEvent(oEvent);
		}
	};

	/**
	 * Process specified event
	 * @param {Event} oEvent - Browser post message event
	 * @returns {Promise} - resolves when finished
	 * @private
	 */
	PostMessageBus.prototype._processEvent = function (oEvent) {
		return new Promise(function (fnResolve, fnReject) {
			var mData = oEvent.data;
			var sOrigin = oEvent.origin;

			// Ignore messages from disabled hosts
			if (this._aDeclinedOrigins.includes(sOrigin)) {
				fnResolve();
				return;
			}

			switch (mData.eventId) {
				case PostMessageBus.event.CONNECT: {
					if (typeof mData.data !== "string") {
						this.publish({
							target: oEvent.source,
							origin: oEvent.origin,
							channelId: mData.channelId,
							eventId: PostMessageBus.event.DECLINED
						});
						fnResolve();
					} else if (this._aAcceptedOrigins.includes(sOrigin)) {
						this.publish({
							target: oEvent.source,
							origin: oEvent.origin,
							channelId: mData.channelId,
							eventId: PostMessageBus.event.ACCEPTED
						});
						fnResolve();
					} else {
						// Show dialog
						sap.ui.require(["sap/ui/core/postmessage/confirmationDialog"], function (confirmationDialog) {
							this._getText('PostMessage.Message', [mData.data, sOrigin])
							.then(function(sText) {
								return confirmationDialog(sText);
							})
							.then(
								function () {
									this.addAcceptedOrigin(sOrigin);
									this.publish({
										target: oEvent.source,
										origin: oEvent.origin,
										channelId: mData.channelId,
										eventId: PostMessageBus.event.ACCEPTED
									});
								}.bind(this),
								function () {
									this.addDeclinedOrigin(sOrigin);
									this.publish({
										target: oEvent.source,
										origin: oEvent.origin,
										channelId: mData.channelId,
										eventId: PostMessageBus.event.DECLINED
									});
								}.bind(this)
							)
							.then(fnResolve);
						}.bind(this), fnReject);
					}
					break;
				}
				case PostMessageBus.event.ACCEPTED:
				case PostMessageBus.event.DECLINED:
				case PostMessageBus.event.READY: {
					oEvent.data.data = undefined; // sanitize data payload for system events
					this._emitMessage(oEvent);
					fnResolve();
					break;
				}
				default: {
					if (this._aAcceptedOrigins.includes(sOrigin)) {
						this._emitMessage(oEvent);
					}
					fnResolve();
				}
			}
		}.bind(this))
		.catch(function (vError) {
			var sMessage;
			var sDetail;

			if (typeof vError === 'string') {
				sMessage = vError;
			} else if (vError instanceof Error) {
				sMessage = vError.message;
				sDetail = vError.stack;
			} else {
				sMessage = 'Some unexpected error happened during post message processing';
			}

			Log.error(sMessage, sDetail, 'sap.ui.core.postmessage.Bus');
		})
		.then(function () {
			this._oPendingProcess = (
				this._aEventQueue.length > 0
				? this._processEvent(this._aEventQueue.shift())
				: null
			);
		}.bind(this));
	};

	/**
	 * Sends transformed event to subscribers
	 * @param {Event} oEvent - Browser post message event
	 * @private
	 */
	PostMessageBus.prototype._emitMessage = function (oEvent) {
		var sChannelId = oEvent.data.channelId;
		var sEventId = oEvent.data.eventId;

		EventBus.prototype.publish.call(this, sChannelId, sEventId, {
			originalEvent: oEvent,
			channelId: sChannelId,
			eventId: sEventId,
			source: oEvent.source,
			origin: oEvent.origin,
			data: oEvent.data.data
		});
	};

	/**
	 * Get list of accepted origins
	 * @return {string[]} - List of origins, e.g. ['http://example.com:8080', 'http://example.com', ...]
	 */
	PostMessageBus.prototype.getAcceptedOrigins = function () {
		return this._aAcceptedOrigins.slice();
	};

	/**
	 * Replace current list with accepted origins with a new one
	 * @param {string[]} aOrigins - List of origins, e.g. ['http://example.com:8080', 'http://example.com', ...]
	 */
	PostMessageBus.prototype.setAcceptedOrigins = function (aOrigins) {
		if (!Array.isArray(aOrigins)) {
			throw new TypeError('Expected an array, but got ' + typeof aOrigins);
		}
		this._aAcceptedOrigins = aOrigins.slice();
	};

	/**
	 * Add specified origin to the existing list of accepted origins
	 * @param {string} sOrigin - Origin name, e.g. http://example.com:8080
	 */
	PostMessageBus.prototype.addAcceptedOrigin = function (sOrigin) {
		if (typeof sOrigin !== 'string') {
			throw new TypeError('Expected a string, but got ' + typeof sOrigin);
		}
		if (!this._aAcceptedOrigins.includes(sOrigin)) {
			this._aAcceptedOrigins.push(sOrigin);
		}
	};

	/**
	 * Erase list of accepted origins
	 */
	PostMessageBus.prototype.resetAcceptedOrigins = function () {
		this.setAcceptedOrigins([]);
	};

	/**
	 * Get list of declined origins
	 * @return {string[]} - List of origins, e.g. ['http://example.com:8080', 'http://example.com', ...]
	 */
	PostMessageBus.prototype.getDeclinedOrigins = function () {
		return this._aDeclinedOrigins.slice();
	};

	/**
	 * Replace current list with declined origins with a new one
	 * @param {string[]} aOrigins - List of origins, e.g. ['http://example.com:8080', 'http://example.com', ...]
	 */
	PostMessageBus.prototype.setDeclinedOrigins = function (aOrigins) {
		if (!Array.isArray(aOrigins)) {
			throw new TypeError('Expected an array, but got ' + typeof aOrigins);
		}
		this._aDeclinedOrigins = aOrigins.slice();
	};

	/**
	 * Add specified origin to the existing list of declined origins
	 * @param {string} sOrigin - Origin name, e.g. http://example.com:8080
	 */
	PostMessageBus.prototype.addDeclinedOrigin = function (sOrigin) {
		if (typeof sOrigin !== 'string') {
			throw new TypeError('Expected a string, but got ' + typeof sOrigin);
		}
		if (!this._aDeclinedOrigins.includes(sOrigin)) {
			this._aDeclinedOrigins.push(sOrigin);
		}
	};

	/**
	 * Erase list of declined origins
	 */
	PostMessageBus.prototype.resetDeclinedOrigins = function () {
		this.setDeclinedOrigins([]);
	};

	return PostMessageBus;
});
