/*
 * LICENCE[[
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1/CeCILL 2.O 
 *
 * The contents of this file are subject to the Mozilla Public License Version 
 * 1.1 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, 
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 
 * for the specific language governing rights and limitations under the 
 * License. 
 * 
 * The Original Code is utc.fr code. 
 * 
 * The Initial Developer of the Original Code is 
 * david.rivron@utc.fr
 * 
 * Portions created by the Initial Developer are Copyright (C) 2012
 * the Initial Developer. All Rights Reserved. 
 * 
 * Contributor(s): 
 * 
 * 
 * Alternatively, the contents of this file may be used under the terms of 
 * either of the GNU General Public License Version 2 or later (the "GPL"), 
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 
 * or the CeCILL Licence Version 2.0 (http://www.cecill.info/licences.en.html), 
 * in which case the provisions of the GPL, the LGPL or the CeCILL are applicable 
 * instead of those above. If you wish to allow use of your version of this file 
 * only under the terms of either the GPL or the LGPL, and not to allow others 
 * to use your version of this file under the terms of the MPL, indicate your 
 * decision by deleting the provisions above and replace them with the notice 
 * and other provisions required by the GPL or the LGPL. If you do not delete 
 * the provisions above, a recipient may use your version of this file under 
 * the terms of any one of the MPL, the GPL, the LGPL or the CeCILL.
 * ]]LICENCE
 */
 
 var util = require('util'),
 	events = require('events'),
	url = require('url');
 	net = require('net'),
 	fs = require('fs'),
	http = require('http'),
	sio = require('socket.io');

/**
 * Serveur de collaboration
 * 
 * Englobe :
 *  - une communication avec le serveur Java (communication "système")
 *  - un serveur HTTP pour l'écoute des requêtes Upgrade (et pouvant servir de proxy au serveur Java)
 *  - les communication WebSocket
 * 
 * Il produit trois évènements à destination des MessagesHandler : init, message, close
 */
var CollabServer = module.exports = function (pOptions) {
	events.EventEmitter.call(this);
	
	if (pOptions.logPath) {
		var vLogStream = fs.createWriteStream(pOptions.logPath);
		Object.defineProperty(process, 'stdout', { value: vLogStream });
		Object.defineProperty(process, 'stderr', { value: vLogStream });
	}
	/*
	 * Initialisation du serveur
	 */
	var vSelf = this;
	var vClients = this.clients = {};
	this.options = pOptions;
	
	var vWsConnectUrl = pOptions.connectUrl || 'ws://localhost:' + pOptions.port + "/ws/";
	if (vWsConnectUrl[vWsConnectUrl.length - 1] == '/') vWsConnectUrl = vWsConnectUrl.substring(0, vWsConnectUrl.length - 1);
	vWsConnectUrl = url.parse(vWsConnectUrl);
	
	/*
	 * Initialisation du serveur HTTP
	 */
	var vHttpServer = http.createServer(function(pRequest, pResponse) {
		pResponse.statusCode = 404;
		pRequest.on('end', function () {
			pResponse.end();
		});
	}).listen(pOptions.port);
	
	// L'URL pour la connection WebSocket est envoyé au serveur Java lorsque le serveur HTTP est prêt
	vHttpServer.on('listening', function () {
			 
		if (pOptions.systemClientPort) {
			// Connexion au serveur Java
			var vSystemSocket = net.connect(pOptions.systemClientPort, function() {
				createServer(vSystemSocket);
			});
			
			// Lorsque le serveur est client du serveur Java, sa durée de vie correspond à celle du socket de communication
			vSystemSocket.on('close', function () {
				process.exit(0)
			});
		} else if (pOptions.systemServerPort) {
			// Ecoute de connexion en provenance du serveur Java
			net.createServer.call(this, function (pSystemSocket) {
				createServer(pSystemSocket);
			}).listen(pOptions.systemServerPort);
					
			if (pOptions.systemConnectUrl) {
				var vConnectRequest = http.get(url.parse(pOptions.systemConnectUrl));
			}
		}
	});
		
	function createServer (pSystemSocket) {
		vSelf.systemSocket = pSystemSocket;
		
		if (pOptions.systemClientPort) pSystemSocket.write(JSON.stringify({ type: 'ack' }));
		else if (pOptions.systemServerPort) {
			pSystemSocket.write(JSON.stringify({ type: 'context', wsUrl: vWsConnectUrl.href}));
		}
		
		var vSystemMessagesBuffer = '';
		// Initialisation de l'écoute des messages systèmes
		pSystemSocket.on('data', function (pData) {
			
				vSystemMessagesBuffer += pData.toString();
				if (vSystemMessagesBuffer[0] != '{') pData.toString();
				var vObjectEnd = vSystemMessagesBuffer.indexOf('}');
				while (vObjectEnd != -1) {
					try {
						var vRawMessage = vSystemMessagesBuffer.substr(0, vObjectEnd+1);
						var vMessage = JSON.parse(vRawMessage);
					} catch (e) {
						console.error('Invalid system message:', e);
						return;
					}
					vSelf.onSystemMessage(vMessage, pSystemSocket);
					vSystemMessagesBuffer = vSystemMessagesBuffer.substr(vRawMessage.length);
					vObjectEnd = vSystemMessagesBuffer.indexOf('}');
				}
		});
		
		pSystemSocket.on('close', function (pRawMessage) {
			for (var vClientId in vClients) {
				var vClient = vClients[vClientId];
				if (vClient.socket) vClient.socket.disconnect();
			}
			vSelf.logClients();
		});


		/*
		 * Initialisation du support des WebSockets sur le serveur HTTP
		 */
		var vWsServer = sio.listen(vHttpServer, {
			transports: [ 'websocket', 'xhr-polling' ],
			'resource': vWsConnectUrl.pathname,
			'close timeout': 300,
			'heartbeats': true,
			'browser client': false,
			'log level': pOptions.logDebug ? 3 : 2,
			'log colors': !pOptions.logPath
		}).sockets;
		
		// Connexion d'un client
		vWsServer.on('connection', function (pSocket) {
			
			// Si il n'y a pas de message init avant une seconde, le client est déconnecté
			var noInitTimeout = setTimeout(function() {
				pSocket.disconnect();
			}, 1000);
			
			// Message init : initialisation du client et vérification des permissions initiales
			pSocket.on('init', function(pMessage) {
				vSelf.logMessage(pMessage);
				var vClient = vClients[pMessage.clientId];
				if (vClient && vClient.token == pMessage.token) {
				 	clearTimeout(noInitTimeout);
					delete vClient.token;
				 	vClient.socket = pSocket;
				 	vClient.pendingPerms = {};
				 	vClient.perms = {};
				 	vSelf.emit('init', vClient, pMessage);
				} else {
					console.error('init message discarded:', pMessage);
					pSocket.disconnect();
				}
				vSelf.logClients();
			});
			
						
			pSocket.on('message', function (pMessage) {
				vSelf.logMessage(pMessage);
				var vClient = vClients[pMessage.clientId];
				if (vClient && vClient.socket == pSocket) vSelf.emit('message', vClient, pMessage);
				else console.error('message discarded:', pMessage);
				vSelf.logClients();
			});

			pSocket.on('disconnect', function() {
				for (var vClientId in vClients) {
					if (vClients[vClientId].socket == pSocket) {
						vSelf.emit('close', vClients[vClientId]);
						delete vClients[vClientId];
						vSelf.logClients();
						break;
					}
				}
			});	
		});
		
		// Invalidation des permissions sur les workspaces qui date de plus de 10min
		setInterval(function () {
			var vNow = Date.now();
			for (var vClientId in vClients) {
				var vClient = vClients[vClientId];
				for (var vWspCd in vClient.perms) {
					for (var vPerm in vClient.perms[vWspCd]) {
						if (vNow - vClient.perms[vWspCd][vPerm] > 600000) {
							if (!(vWspCd in vClient.pendingPerms)) vClient.pendingPerms[vWspCd]  = {};
							vClient.pendingPerms[vWspCd][vPerm] = [];
							pSystemSocket.write(JSON.stringify({ type: 'checkPerm', perm: vPerm, wspCd: vWspCd, account: vClient.account, clientId: vClient.clientId }));
						}
					}
				}
			}
		}.bind(this), 60000)

	}
}

util.inherits(CollabServer, events.EventEmitter);

CollabServer.prototype.onSystemMessage = function (pMessage, pSystemSocket) {
	/**
	 * Handler des messages systèmes (provenant du serveur Java par l'entrée standard)
	 */
	var vSelf = this;
	this.logMessage(pMessage);
	if (pMessage.type == 'newClient') {
		// L'utilisateur est ajouté à la liste globale
		delete pMessage.type;
		var vClient = this.clients[pMessage.clientId] = pMessage;
		vClient.urisStates = {};
		pSystemSocket.write(JSON.stringify({ type: 'ack' }));
	} else if (pMessage.type == 'hasPerm') {
		var vWspCd = pMessage.wspCd || '';
		var vClient = this.clients[pMessage.clientId];
		if (!vClient) return;
		if (pMessage.value) {
			if (!(vWspCd in vClient.perms)) vClient.perms[vWspCd] = {};
			vClient.perms[vWspCd][pMessage.perm] = Date.now();
			var vPendingPerms = vClient.pendingPerms[vWspCd][pMessage.perm];
			delete vClient.pendingPerms[vWspCd][pMessage.perm];
			if (!Object.keys(vClient.pendingPerms[vWspCd]).length) delete vClient.pendingPerms[vWspCd];
			// Traitement des messages en attente de la permission
			vPendingPerms.forEach(function (pCallback) {
				pCallback();
			});
		} else {
			if (vWspCd in vClient.perms) {
				delete vClient.perms[vWspCd][pMessage.perm];
				if (!Object.keys(vClient.perms[vWspCd]).length) delete vClient.perms[vWspCd];
			}
			if (vWspCd in vClient.pendingPerms) {
				delete vClient.pendingPerms[vWspCd][pMessage.perm];
				if (!Object.keys(vClient.pendingPerms[vWspCd]).length) delete vClient.perms[vWspCd];
			}
			vSelf.emit('noPerm', vClient, vWspCd);
		}
	}
	this.logClients();
}

// Mémoriser les perms
CollabServer.prototype.requirePerm = function (pClient, pWspCd, pPerm, pCallback) {
	pWspCd = pWspCd || '';
	if (pClient.pendingPerms && pWspCd in pClient.pendingPerms && pPerm in pClient.pendingPerms[pWspCd]) {
		pClient.pendingPerms[pWspCd][pPerm].push(pCallback);
	} else if (pClient.perms) {
		if (pWspCd in pClient.perms && pPerm in pClient.perms[pWspCd]) pCallback();
		else {
			if (!(pWspCd in pClient.pendingPerms)) pClient.pendingPerms[pWspCd] = {};
			pClient.pendingPerms[pWspCd][pPerm] = [ pCallback ];
			this.systemSocket.write(JSON.stringify({ type: 'checkPerm', perm: pPerm, wspCd: pWspCd, account: pClient.account, clientId: pClient.clientId }));
		}
	}
}

CollabServer.prototype.sendMessage = function (pClient, pWspCd, pMessage) {
	if (!pClient) return;
	var emitMessage = function () {
		this.logMessage(pMessage, pClient.account);
		if (pClient.socket) pClient.socket.emit('message', pMessage);
	}.bind(this);

	if (!pClient.wspCd) {
		// Message concernant le serveur : on ne vérifie pas les permissions (l'expéditeur du message doit avoir les droits admin)
		emitMessage();
	} else if (pClient.pendingPerms && pWspCd in pClient.pendingPerms) {
		pClient.pendingPerms[pWspCd].push(emitMessage);
	} else if (pClient.perms && pWspCd in pClient.perms) {
		emitMessage();
	}
}

CollabServer.prototype.logMessage = function (pMessage, pDest) {
	var vColors = !this.options.logPath;
	var vDestPrefix = pDest ? '-> ' + pDest : '<-';
	var vPrefix = vColors ? '\033[31m' + vDestPrefix + '\033[39m' : vDestPrefix;
	if (this.options.logDebug) console.info(vPrefix, inspect(pMessage, true, vColors));
}

CollabServer.prototype.logClients = function () {
	var vColors = !this.options.logPath;
	console.info(vColors ? '\033[31m{\033[39m' : '{');
	for (var vClientId in this.clients) {
		this.logClient(this.clients[vClientId]);
	}
	console.info(vColors ? '\033[31m}\033[39m' : '}');
}
CollabServer.prototype.logClient = function (pClient) {
	if (!this.options.logDebug) return;
	var vColors = !this.options.logPath;
	var vClientInfo = {};
	for (var i in pClient) {
		if (i != 'socket' && i != 'clientId') vClientInfo[i] = pClient[i];
	}
	vClientInfo.socketId = pClient.socket ? pClient.socket.id : null;
	var vClientPrefix = vColors ? '\033[31m' + pClient.clientId + ':\033[39m' : pClient.clientId;
	console.info(vClientPrefix, inspect(vClientInfo, false, vColors));
}

function inspect (pObject, pOneLine, pColors) {
	var vRepr = util.inspect(pObject, false, null, pColors);
	return pOneLine ? vRepr.replace(/\n\s*/g, ' ') : vRepr;
}

