/*!
 * ${copyright}
 */

// Provides class sap.ui.core.ws.WebSocket for WebSocket support with SAP-PCP Protocol
sap.ui.define(['./WebSocket', "sap/base/Log"],
	function(WebSocket, Log) {
	"use strict";

	/**
	 * Creates a new WebSocket connection and uses the pcp-protocol for communication.
	 *
	 * @param {string} sUrl relative or absolute URL for WebSocket connection.
	 * @param {array} [aProtocols] array of protocols as strings, a single protocol as a string.
	 * Protocol(s) should be selected from {@link sap.ui.core.ws.SapPcpWebSocket.SUPPORTED_PROTOCOLS}.
	 *
	 * @public
	 *
	 * @class WebSocket class implementing the pcp-protocol.
	 * @extends sap.ui.core.ws.WebSocket
	 * @author SAP SE
	 * @version ${version}
	 * @alias sap.ui.core.ws.SapPcpWebSocket
	 */
	var SapPcpWebSocket = WebSocket.extend("sap.ui.core.ws.SapPcpWebSocket", /** @lends sap.ui.core.ws.SapPcpWebSocket.prototype */ {

		constructor: function(sUrl, aProtocols) {
			WebSocket.apply(this, arguments);
		}

	});

	/**
	 * The <code>message</code> event is fired, when a message was received.
	 *
	 * @name sap.ui.core.ws.SapPcpWebSocket#message
	 * @event
	 * @param {sap.ui.base.Event} oControlEvent
	 * @param {sap.ui.base.EventProvider} oControlEvent.getSource
	 * @param {object} oControlEvent.getParameters
	 * @param {string} oControlEvent.getParameters.data Received data from the server.
	 * @param {string} oControlEvent.getParameters.pcpFields Received pcpFields as a key-value map.
	 * @public
	 */

	/**
	 * Fires event {@link #event:message message} to attached listeners.
	 *
	 * @param {object} [oParameters] Parameters to pass along with the event
	 * @param {string} [oParameters.data] Received data from the server.
	 * @param {string} [oParameters.pcpFields] Received pcpFields as a key-value map.
	 * @return {this} <code>this</code> to allow method chaining
	 * @protected
	 * @name sap.ui.core.ws.SapPcpWebSocket#fireMessage
	 * @function
	 */

	/**
	 * Protocol versions.
	 *
	 * One (or more) of these have to be selected to create an SapPcpWebSocket connection
	 * (or no protocol at all).
	 *
	 * @enum {string}
	 * @public
	 * @static
	 */
	SapPcpWebSocket.SUPPORTED_PROTOCOLS = {

		/**
		 * Protocol v10.pcp.sap.com
		 * @public
		 * @name sap.ui.core.ws.SapPcpWebSocket.SUPPORTED_PROTOCOLS.v10
		 */
		v10 : "v10.pcp.sap.com"

	};

	/**
	 * RegEx to get pcp-header fields.
	 *
	 * @private
	 */
	SapPcpWebSocket._deserializeRegexp = /((?:[^:\\]|(?:\\.))+):((?:[^:\\\n]|(?:\\.))*)/;

	/**
	 * Separator between header-fields and message body.
	 *
	 * @private
	 */
	SapPcpWebSocket._SEPARATOR = "\n\n";

	/**
	 * pcp-action value.
	 *
	 * @private
	 */
	SapPcpWebSocket._MESSAGE = "MESSAGE";

	/**
	 * Internal handler for open-event.
	 *
	 * @private
	 */
	SapPcpWebSocket.prototype._onopen = function() {
		var bSuccess = false;

		if (this.getProtocol() === "") {
			bSuccess = true;
		} else {
			for (var protocol in SapPcpWebSocket.SUPPORTED_PROTOCOLS) {
				if (SapPcpWebSocket.SUPPORTED_PROTOCOLS.hasOwnProperty(protocol)) {
					if (SapPcpWebSocket.SUPPORTED_PROTOCOLS[protocol] === this.getProtocol()) {
						bSuccess = true;
						break;
					}
				}
			}
		}

		if (bSuccess) {
			this.fireOpen();
		} else {
			Log.error("Unsupported protocol '" + this.getProtocol() + "' selected by the server. " +
				"Connection will be closed.");
			this.close("Unsupported protocol selected by the server");
		}

	};

	/**
	 * Internal handler for message-event.
	 *
	 * @private
	 */
	SapPcpWebSocket.prototype._onmessage = function(oMessageEvent) {
		var iSplitPos = -1,
			oEventData = {};

		if (typeof oMessageEvent.data === "string") {
			iSplitPos = oMessageEvent.data.indexOf(SapPcpWebSocket._SEPARATOR);
		}

		if (iSplitPos !== -1) {
			oEventData.pcpFields = this._extractPcpFields(oMessageEvent.data.substring(0, iSplitPos));
			oEventData.data = oMessageEvent.data.substr(iSplitPos + SapPcpWebSocket._SEPARATOR.length);
		} else {
			Log.warning("Invalid PCP message received: " + oMessageEvent.data);
			oEventData.pcpFields = {};
			oEventData.data = oMessageEvent.data; // Fall back, just pass through original data
		}

		this.fireMessage(oEventData);
	};

	/**
	 * Extracts the pcp-fields from a header string.
	 *
	 * @param {string} sHeader Header as string
	 * @return {object} oPcpFields extracted fields as key-value map
	 * @private
	 */
	SapPcpWebSocket.prototype._extractPcpFields = function(sHeader) {
		var aFields = sHeader.split("\n"),
			aLine = [],
			oPcpFields = {};

		for (var i = 0; i < aFields.length; i++) {
			aLine = aFields[i].match(SapPcpWebSocket._deserializeRegexp);
			if (aLine && aLine.length === 3) {
				oPcpFields[this._unescape(aLine[1])] = this._unescape(aLine[2]);
			}
		}

		return oPcpFields;
	};

	/**
	 * Unescapes a string.
	 *
	 * @param {string} sEscaped escaped string
	 * @return {string} Unescaped string
	 * @private
	 */
	SapPcpWebSocket.prototype._unescape = function(sEscaped) {
		var aParts = sEscaped.split("\u0008"),
			sUnescaped = "";

		for (var i = 0; i < aParts.length; i++) {
			// eslint-disable-next-line no-control-regex -- \x08 is used as separator in socket messages
			aParts[i] = aParts[i].replace(/\\\\/g, "\u0008").replace(/\\:/g, ':').replace(/\\n/g, '\n').replace(/\u0008/g, "\\");
		}

		sUnescaped = aParts.join("\u0008");

		return sUnescaped;
	};

	/**
	 * Serializes pcp-fields into a string.
	 *
	 * @param {object} oPcpFields key-value map with pcp-fields
	 * @param {string} sMessageType message-type, one of 'string', 'blob' or 'arraybuffer'.
	 * @param {string} sPcpAction pcp-action value
	 * @returns {string} serialized pcp-fields
	 * @private
	 */
	SapPcpWebSocket.prototype._serializePcpFields = function(oPcpFields, sMessageType, sPcpAction) {
		var oSerialized = "",
			sFieldName = "",
			sPcpBodyType = "";

		if (sMessageType === 'string') {
			sPcpBodyType = 'text';
		} else if (sMessageType === 'blob' || sMessageType === 'arraybuffer') {
			sPcpBodyType = 'binary';
		}

		if (oPcpFields && typeof oPcpFields === 'object') {
			for (sFieldName in oPcpFields) {
				if (oPcpFields.hasOwnProperty(sFieldName) && sFieldName.indexOf('pcp-') !== 0) {
					oSerialized += this._escape(sFieldName) + ":" + this._escape(String(oPcpFields[sFieldName])) + "\n";
				}
			}
		}

		return "pcp-action:" + sPcpAction + "\npcp-body-type:" + sPcpBodyType + "\n" + oSerialized + "\n";
	};

	/**
	 * Escapes a string.
	 *
	 * @param {string} sUnEscaped unescaped string
	 * @returns {string} Escaped string
	 * @private
	 */
	SapPcpWebSocket.prototype._escape = function(sUnEscaped) {
		return sUnEscaped.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/\n/g, '\\n');
	};

	// Public Methods
	/**
	 * Sends a message and optional pcp-header-fields using the pcp-protocol.
	 *
	 * If the connection is not yet opened, the message will be queued and sent
	 * when the connection is established.
	 *
	 * @param {string|Blob|ArrayBuffer} message message to send
	 * @param {object} [oPcpFields] additional pcp-fields as key-value map
	 * @return {this} Reference to <code>this</code> to allow method chaining
	 * @public
	 */
	SapPcpWebSocket.prototype.send = function(message, oPcpFields) {
		var sMessageType = typeof message,
			sPcpFields = "";

		sPcpFields = this._serializePcpFields(oPcpFields, sMessageType, SapPcpWebSocket._MESSAGE);

		WebSocket.prototype.send.call(this, sPcpFields + message);
		return this;
	};

	return SapPcpWebSocket;

});