Source: WindowManager.js

"use strict";

import {EventManager} from "./EventManager.js";
import {WindowSession} from "./WindowSession.js";
import {WindowMessage} from "./WindowMessage.js";

/**
 * Message utils takes care of messaging between multiple windows. Messages can be transfered direcly or forwarded between windows.
 *
 * Is responsible for keeping track of all sessions, handles message reception, message forwarding and message sending.
 *
 * When a window is open it cannot be acessed we need to wait for the ready message.
 * 
 * @class
 * @param {String} type Type of this window.
 * @param {Boolean} initialize Indicates if the manager should be automatically initialized, by default its true. If you want to manually initialize the manager later to avoid loosing message set to false.
 */
function WindowManager(type, initialize)
{
	var self = this;

	/** 
	 * Type of this window manager object.
	 *
	 * @type {String}
	 */
	this.type = type;

	/**
	 * UUID of this window manager.
	 *
	 * Used to identify this window on communication with another windows.
	 *
	 * @type {String}
	 */
	this.uuid = WindowManager.generateUUID();

	/**
	 * Map of known sessions.
	 *
	 * Indexed by the UUID of the session.
	 *
	 * @type {Object}
	 */
	this.sessions = {};

	/**
	 * List of sessions in waiting state.
	 *
	 * These still havent reached the READY state.
	 *
	 * @type {Array}
	 */
	this.waitingSessions = [];

	/**
	 * On broadcast message callback, receives data and authentication as parameters.
	 *
	 * Called when a broadcast message arrives, onBroadcastMessage(data, authentication).
	 *
	 * type {Function}
	 */
	this.onBroadcastMessage = null;

	/**
	 * Global manager on message callback, receives type, data and authentication as parameters.
	 *
	 * This onMessage callback is always called before the individual session callback.
	 *
	 * It might be used for globally handling authentication tasks.
	 *
	 * type {Function}
	 */
	this.onMessage = null;

	/**
	 * Event manager containing the message handling events for this manager.
	 *
	 * @type {EventManager}
	 */
	this.manager = new EventManager();
	this.manager.add(window, "message", function(event)
	{
		//console.log("TabTalk: Window message event fired.", event);

		var message = event.data;

		if(message.action === WindowMessage.READY)
		{
			return;
		}

		//Messages that need to be redirected
		if(message.destinationUUID !== undefined && message.destinationUUID !== self.uuid)
		{
			console.warn("TabTalk: Destination UUID diferent from self, destination is " + message.destinationUUID, message);

			var session = self.sessions[message.destinationUUID];

			if(session !== undefined)
			{
				message.hops.push(self.uuid);
				session.send(message);
				console.log("TabTalk: Redirected message to destination.", session, message);
			}
			else
			{
				console.warn("TabTalk: Unknown destination, cannot redirect message.");
			}

			return;
		}
		//Messages to be processed
		else
		{
			//Session closed
			if(message.action === WindowMessage.CLOSED)
			{
				var session = self.sessions[message.originUUID];

				if(session !== undefined)
				{
					session.setStatus(WindowSession.CLOSED);
					delete self.sessions[message.originUUID];
				}
				else
				{
					console.warn("TabTalk: Unknown closed origin session.")
				}
			}
			//Lookup
			else if(message.action === WindowMessage.LOOKUP)
			{
				console.log("TabTalk: WindowManager lookup request received from " + message.originType + ".", message);

				var found = false;
				var response;

				for(var i in self.sessions)
				{
					var session = self.sessions[i];

					if(session.type === message.destinationType)
					{
						var response = new WindowMessage(0, WindowMessage.LOOKUP_FOUND, self.type, self.uuid, undefined, undefined,
						{
							uuid:session.uuid,
							type:session.type
						});
						found = true;
						break;
					}
				}

				if(found === false)
				{
					response = new WindowMessage(0, WindowMessage.LOOKUP_NOT_FOUND, self.type, self.uuid);
				}

				var session = self.sessions[message.originUUID];
				if(session !== undefined)
				{
					session.send(response);
					console.log("TabTalk: Response to lookup request sent.", response);
				}
				else
				{
					console.warn("TabTalk: Unknown lookup origin session.");
				}
			}
			//Connect message
			else if(message.action === WindowMessage.CONNECT)
			{
				var gatewayUUID = message.hops.pop();
				var gateway = self.sessions[gatewayUUID];

				if(gateway !== undefined)
				{
					var session = new WindowSession(self);
					session.uuid = message.originUUID;
					session.type = message.originType;
					session.gateway = gateway;
					session.waitReady();
					session.acknowledge();
					console.warn("TabTalk: Connect message received, creating a new session.", message, session);
				}
				else
				{
					console.error("TabTalk: Connect message received, but the gateway is unknown.", message);
				}
			}
			//Broadcast
			else if(message.action === WindowMessage.BROADCAST)
			{
				console.log("TabTalk: WindowManager broadcast message received " + message.originType + ".", message);

				if(self.onBroadcastMessage !== null)
				{
					self.onBroadcastMessage(message.data, message.authentication);
				}

				//Add manager uuid to the hop list
				message.hops.push(self.uuid);

				for(var i in self.sessions)
				{
					var session = self.sessions[i];

					//Forward message only to sessions that are not in the hop list
					if(session.uuid !== message.originUUID && message.hops.indexOf(session.uuid) === -1)
					{
						session.send(message);

						if(session.onBroadcastMessage !== null)
						{
							session.onBroadcastMessage(message.data, message.authentication);
						}
					}
				}
			}
			//Messages
			else if(message.action === WindowMessage.MESSAGE)
			{
				console.log("TabTalk: WindowManager message received " + message.originType + ".", message);

				if(self.onMessage !== null)
				{
					self.onMessage(message.originType, message.data, message.authentication);
				}

				var session = self.sessions[message.originUUID];
				if(session !== undefined)
				{
					session.received.push(message);
					
					if(session.onMessage !== null)
					{
						session.onMessage(message.data, message.authentication);
					}
				}
				else
				{
					console.warn("TabTalk: Unknown origin session.", message);
				}
			}
			else
			{
				console.warn("TabTalk: Unknown message type.", message);
			}
		}
	});

	this.manager.add(window, "beforeunload", function(event)
	{
		for(var i in self.sessions)
		{
			self.sessions[i].close();
		}
	});

	if(initialize !== false)
	{
		this.initialize();
	}
}

/**
 * Initialized the manager message handler, checks for the opener window and sends acknowledge message to waiting sessions.
 *
 * By default it is automatically called on the constructor. But sometimes it might be usefull to initialize the window later on to avoid loosing messages.
 *
 * Be carefull to specify all the global callbacks before calling this method.
 *
 * @return {WindowSession} If this window was opened by another one it returns the respective session. Returns null otherwise.
 */
WindowManager.prototype.initialize = function()
{
	this.manager.create();
	return this.checkOpener();
};

/**
 * Log to the console a list of all known sessions.
 *
 * Useful for debug purposes.
 */
WindowManager.prototype.logSessions = function()
{
	console.log("TabTalk: List of known sessions:");

	for(var i in this.sessions)
	{
		var session = this.sessions[i];

		console.log("     " + session.uuid + " | " + session.type + " -> " + (session.gateway === null ? "*" : session.gateway.uuid));
	}
};

/**
 * Broadcast a message to all available sessions.
 *
 * The message will be passed further on by the other windows that receive it.
 *
 * @param {WindowMessage} data Data to be broadcasted.
 * @param {String} authentication Authentication information.
 */
WindowManager.prototype.broadcast = function(data, authentication)
{
	var message = new WindowMessage(0, WindowMessage.BROADCAST, this.manager.type, this.manager.uuid, undefined, undefined, data, authentication);
	message.hops.push(this.uuid);

	for(var i in this.sessions)
	{
		this.sessions[i].send(message);
	}
};

/**
 * Send an acknowledge message.
 *
 * Used to send a signal indicating the parent window that it is ready to receive data.
 *
 * @return {WindowSession} Session fo the opener window, null if the window was not opened.
 */
WindowManager.prototype.checkOpener = function()
{
	if(this.wasOpened())
	{
		var session = new WindowSession(this);
		session.window = window.opener;
		session.acknowledge();
		session.waitReady();
		return session;
	}

	return null;
};

/**
 * Create a new session with a new window from URL and type.
 *
 * Before opening a new window it searches for a window of the same type in the known sessions.
 *
 * If a session of the same type already exists it is returned.
 *
 * @param {String} url URL of the window.
 * @param {String} type Type of the window to open (Optional).
 * @return {WindowSession} Session created to open a new window.
 */
WindowManager.prototype.openSession = function(url, type)
{
	//Search in the current sessions
	for(var i in this.sessions)
	{
		var session = this.sessions[i];
		if(session.type === type)
		{
			console.warn("TabTalk: A session of the type " + type + " already exists.");
			return session;
		}
	}

	var session = new WindowSession(this);
	session.type = type;

	//Lookup the session
	if(type !== undefined)
	{
		this.lookup(type, function(gateway, uuid, type)
		{
			//Not found
			if(gateway === null)
			{
				if(url !== null)
				{	
					session.url = url;
					session.window = window.open(url);
					session.waitReady();
				}
			}
			//Found
			else
			{
				session.gateway = gateway;
				session.uuid = uuid;
				session.type = type;
				session.waitReady();
				session.connect();
			}
		});
	}

	return session;
};

/**
 * Get a session from the manager from its type.
 *
 * @param {String} type Type of the window to get.
 * @return {WindowSession} Session of the type specified, null if none was found.
 */
WindowManager.prototype.getSession = function(type)
{
	for(var i in this.sessions)
	{
		var session = this.sessions[i];
		if(session.type === type)
		{
			return session;
		}
	}

	return null;
};

/**
 * Check if a session of a type exists and its available for message exchanging.
 *
 * @param {String} type Type of the window to check.
 * @return {Boolean} True if a session of the type requested exists, false otherwise.
 */
WindowManager.prototype.sessionAvailable = function(type)
{
	for(var i in this.sessions)
	{
		if(this.sessions[i].type === type || message.action !== WindowMessage.CLOSED)
		{
			return true;
		}
	}

	return false;
};

/**
 * Check if a session of a type exists.
 *
 * @param {String} type Type of the window to check.
 * @return {Boolean} True if a session of the type requested exists, false otherwise.
 */
WindowManager.prototype.sessionExists = function(type)
{
	for(var i in this.sessions)
	{
		if(this.sessions[i].type === type)
		{
			return true;
		}
	}

	return false;
};

/**
 * Lookup for a window type.
 *
 * @param {String} type Type of the window to look for.
 * @param {String} onFinish Receives the gateway session, found uuid and type as parameters onFinish(session, uuid, type), if null no window of the type was found.
 */
WindowManager.prototype.lookup = function(type, onFinish)
{
	var message = new WindowMessage(0, WindowMessage.LOOKUP, this.type, this.uuid, type, undefined);
	var sent = 0, received = 0, found = false;

	for(var i in this.sessions)
	{
		console.log("TabTalk: Send lookup message to " + i + ".", message);
		this.sessions[i].send(message);
		sent++;
	}

	if(sent > 0)
	{
		var self = this;
		var manager = new EventManager();
		manager.add(window, "message", function(event)
		{
			var data = event.data;
			if(data.action === WindowMessage.LOOKUP_FOUND)
			{

				if(found === false)
				{
					var session = self.sessions[data.originUUID];
					if(session !== undefined)
					{
						found = true;
						onFinish(session, data.data.uuid, data.data.type);
					}
				}
				console.log("TabTalk: Received lookup FOUND message from " + data.originUUID + ".", data.data);
				received++;
			}
			else if(data.action === WindowMessage.LOOKUP_NOT_FOUND)
			{
				console.log("TabTalk: Received lookup NOT FOUND message from " + data.originUUID + ".");
				received++;
			}

			if(sent === received)
			{
				manager.destroy();

				if(found === false)
				{
					onFinish(null);
				}
			}
		});
		manager.create();
	}
	else
	{
		console.log("TabTalk: No session available to run lookup.");
		onFinish(null);
	}
};

/**
 * Check if this window was opened by another one.
 *
 * @return {Boolean} True if the window was opened by another window, false otherwise.
 */
WindowManager.prototype.wasOpened = function()
{
	return window.opener !== null;
};

/**
 * Dispose the window manager.
 *
 * Should be called when its not used anymore. Destroy all the window events created by the manager.
 */
WindowManager.prototype.dispose = function()
{
	for(var i in this.sessions)
	{
		this.sessions[i].close();
	}

	this.manager.destroy();
};

/**
 * Generate a UUID used to indetify the window manager.
 *
 * .toUpperCase() here flattens concatenated strings to save heap memory space.
 *
 * http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
 *
 * @return {String} UUID generated.
 */
WindowManager.generateUUID = function()
{
	var lut = [];
	for(var i = 0; i < 256; i ++)
	{
		lut[i] = (i < 16 ? "0" : "") + (i).toString(16);
	}

	return function generateUUID()
	{
		var d0 = Math.random() * 0xffffffff | 0;
		var d1 = Math.random() * 0xffffffff | 0;
		var d2 = Math.random() * 0xffffffff | 0;
		var d3 = Math.random() * 0xffffffff | 0;
		var uuid = lut[ d0 & 0xff ] + lut[ d0 >> 8 & 0xff ] + lut[ d0 >> 16 & 0xff ] + lut[ d0 >> 24 & 0xff ] + "-" +
			lut[ d1 & 0xff ] + lut[ d1 >> 8 & 0xff ] + "-" + lut[ d1 >> 16 & 0x0f | 0x40 ] + lut[ d1 >> 24 & 0xff ] + "-" +
			lut[ d2 & 0x3f | 0x80 ] + lut[ d2 >> 8 & 0xff ] + "-" + lut[ d2 >> 16 & 0xff ] + lut[ d2 >> 24 & 0xff ] +
			lut[ d3 & 0xff ] + lut[ d3 >> 8 & 0xff ] + lut[ d3 >> 16 & 0xff ] + lut[ d3 >> 24 & 0xff ];

		return uuid.toUpperCase();
	};
}();

export {WindowManager};