// Cronicle Server Discovery Layer
// Copyright (c) 2015 Joseph Huckaby
// Released under the MIT License

var dgram = require("dgram");
var os = require('os');
var Netmask = require('netmask').Netmask;

var Class = require("pixl-class");
var Tools = require("pixl-tools");

module.exports = Class.create({
	
	nearbyServers: null,
	lastDiscoveryBroadcast: 0,
	
	setupDiscovery: function(callback) {
		// setup auto-discovery system
		// listen for UDP pings, and broadcast our own ping
		var self = this;
		
		this.nearbyServers = {};
		this.lastDiscoveryBroadcast = 0;
		
		// disable if port is unset
		if (!this.server.config.get('udp_broadcast_port')) {
			if (callback) callback();
			return;
		}
		
		// guess best broadcast IP
		this.broadcastIP = this.server.config.get('broadcast_ip') || this.calcBroadcastIP();
		this.logDebug(4, "Using broadcast IP: " + this.broadcastIP );
		
		// start UDP socket listener
		this.logDebug(4, "Starting UDP server on port: " + this.server.config.get('udp_broadcast_port'));
		var listener = this.discoveryListener = dgram.createSocket("udp4");
		
		listener.on("message", function (msg, rinfo) {
			self.discoveryReceive( msg, rinfo );
		} );
		
		listener.on("error", function (err) {
			self.logError('udp', "UDP socket listener error: " + err);
			self.discoveryListener = null;
		} );
		
		listener.bind( this.server.config.get('udp_broadcast_port'), function() {
			if (callback) callback();
		} );
	},
	
	discoveryTick: function() {
		// broadcast pings every N
		if (!this.discoveryListener) return;
		var now = Tools.timeNow(true);
		
		if (now - this.lastDiscoveryBroadcast >= this.server.config.get('master_ping_freq')) {
			this.lastDiscoveryBroadcast = now;
			
			// only broadcast if not part of a cluster
			if (!this.multi.cluster) {
				this.discoveryBroadcast( 'heartbeat', {
					hostname: this.server.hostname,
					ip: this.server.ip
				} );
			}
			
			// prune servers who have stopped broadcasting
			for (var hostname in this.nearbyServers) {
				var server = this.nearbyServers[hostname];
				if (now - server.now >= this.server.config.get('master_ping_timeout')) {
					delete this.nearbyServers[hostname];
					if (this.multi.master) {
						this.authSocketEmit( 'update', { nearby: this.nearbyServers } );
					}
				}
			}
		}
	},
	
	discoveryBroadcast: function(type, message, callback) {
		// broadcast message via UDP
		var self = this;
		
		message.action = type;
		this.logDebug(10, "Broadcasting message: " + type, message);
		
		var client = dgram.createSocket('udp4');
		var message = Buffer.from( JSON.stringify(message) + "\n" );
		client.bind( 0, function() {
			client.setBroadcast( true );			
			client.send(message, 0, message.length, self.server.config.get('udp_broadcast_port'), self.broadcastIP, function(err) {
				if (err) self.logDebug(9, "UDP broadcast failed: " + err);
				client.close();
				if (callback) callback();
			} );
		} );
	},
	
	discoveryReceive: function(msg, rinfo) {
		// receive UDP message from another server
		this.logDebug(10, "Received UDP message: " + msg + " from " + rinfo.address + ":" + rinfo.port);
		
		var text = msg.toString();
		if (text.match(/^\{/)) {
			// appears to be JSON
			var json = null;
			try { json = JSON.parse(text); }
			catch (e) {
				this.logError(9, "Failed to parse UDP JSON message: " + e);
			}
			if (json && json.action) {
				switch (json.action) {
					
					case 'heartbeat':
						if (json.hostname && (json.hostname != this.server.hostname)) {
							json.now = Tools.timeNow();
							delete json.action;
							
							if (!this.nearbyServers[ json.hostname ]) {
								// first time we've seen this server
								this.nearbyServers[ json.hostname ] = json;
								if (this.multi.master) {
									this.logDebug(6, "Discovered nearby server: " + json.hostname, json);
									this.authSocketEmit( 'update', { nearby: this.nearbyServers } );
								}
							}
							else {
								// update from existing server
								this.nearbyServers[ json.hostname ] = json;
							}
							this.logDebug(10, "Received heartbeat from: " + json.hostname, json);
						}
					break;
					
				} // switch action
			} // got json
		} // appears to be json
	},
	
	calcBroadcastIP: function() {
		// Attempt to determine server's Broadcast IP, using the first LAN IP and Netmask
		// https://en.wikipedia.org/wiki/Broadcast_address
		var ifaces = os.networkInterfaces();
		var addrs = [];
		for (var key in ifaces) {
			if (ifaces[key] && ifaces[key].length) {
				Array.from(ifaces[key]).forEach( function(item) { addrs.push(item); } );
			}
		}
		var addr = Tools.findObject( addrs, { family: 'IPv4', internal: false } );
		if (addr && addr.address && addr.address.match(/^\d+\.\d+\.\d+\.\d+$/) && addr.netmask && addr.netmask.match(/^\d+\.\d+\.\d+\.\d+$/)) {
			// well that was easy
			var ip = addr.address;
			var mask = addr.netmask;
			
			var block = null;
			try { block = new Netmask( ip + '/' + mask ); }
			catch (err) {;}
			
			if (block && block.broadcast && block.broadcast.match(/^\d+\.\d+\.\d+\.\d+$/)) {
				return block.broadcast;
			}
		}
		return '255.255.255.255';
	},
	
	shutdownDiscovery: function() {
		// shutdown
		var self = this;
		
		// shutdown UDP listener
		if (this.discoveryListener) {
			this.logDebug(3, "Shutting down UDP server");
			this.discoveryListener.close();
		}
	}
	
});
