/***********************************************************************************************************************
 *
 * Copyright (C) 2010 by the Stratosphere project (http://stratosphere.eu)
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 **********************************************************************************************************************/

package eu.stratosphere.nephele.discovery;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import eu.stratosphere.nephele.configuration.GlobalConfiguration;
import eu.stratosphere.nephele.util.StringUtils;

/**
 * The discovery service allows task managers to discover a job manager
 * through an IPv4 broadcast or IPv6 multicast. The service has two components:
 * A server component that runs at the job manager and listens for incoming
 * requests and a client component a task manager can use to issue discovery
 * requests.
 * <p>
 * The discovery service uses the <code>discoveryservice.magicnumber</code> configuration parameter. It needs to be set
 * to any number. Task managers discover the job manager only if their magic number matches. This allows running two
 * Nephele setups on the same cluster without interference of the {@link DiscoveryService}s.
 * 
 * @author warneke
 * @author Dominic Battre
 */
public class DiscoveryService implements Runnable {

	/**
	 * Number of retries before discovery is considered to be failed.
	 */
	private static final int DISCOVERFAILURERETRIES = 10;

	/**
	 * Timeout (in msec) for the client socket.
	 */
	private static final int CLIENTSOCKETTIMEOUT = 1000;

	/**
	 * The IPv6 multicast address for link-local all-nodes.
	 */
	private static final String IPV6MULTICASTADDRESS = "FF02::1";

	/**
	 * The key to retrieve the discovery service's magic number from the configuration.
	 */
	private static final String MAGICNUMBER_KEY = "discoveryservice.magicnumber";

	/**
	 * The default magic number.
	 */
	private static final int DEFAULT_MAGICNUMBER = 0;

	/**
	 * The key to retrieve the network port the discovery service listens on for incoming connections from the
	 * configuration.
	 */
	public static final String DISCOVERYPORT_KEY = "discoveryservice.port";

	/**
	 * The default network port the discovery service listens on for incoming connections.
	 */
	private static final int DEFAULT_DISCOVERYPORT = 7001;

	/**
	 * Flag indicating whether to use IPv6 or not.
	 * <p>
	 * This flag used to be set depending on the "java.net.preferIPv4Stack" system property, which was enforced by the
	 * startup scripts. The hint was necessary for cloud setups in combination with the now removed cloud manager.
	 * 
	 * @see {@link https://github.com/stratosphere/stratosphere/issues/189}
	 */
	private static final boolean USE_IPV6 = false;

	/**
	 * ID for job manager lookup request packets.
	 */
	private static final int JM_LOOKUP_REQUEST_ID = 0;

	/**
	 * ID for job manager lookup reply packets.
	 */
	private static final int JM_LOOKUP_REPLY_ID = 1;

	/**
	 * ID for task manager address request packets.
	 */
	private static final int TM_ADDRESS_REQUEST_ID = 2;

	/**
	 * ID for task manager address reply packets.
	 */
	private static final int TM_ADDRESS_REPLY_ID = 3;

	/**
	 * The default size of response datagram packets.
	 */
	private static final int RESPONSE_PACKET_SIZE = 64;

	/**
	 * The offset inside a packet to the magic number field.
	 */
	private static final int MAGIC_NUMBER_OFFSET = 0;

	/**
	 * The offset inside a packet to the packet ID field.
	 */
	private static final int PACKET_ID_OFFSET = 4;

	/**
	 * The offset inside a packet to the packet type ID field.
	 */
	private static final int PACKET_TYPE_ID_OFFSET = 8;

	/**
	 * The offset inside a packet to the actual payload.
	 */
	private static final int PAYLOAD_OFFSET = 12;

	/**
	 * The log object used for debugging.
	 */
	private static final Log LOG = LogFactory.getLog(DiscoveryService.class);

	/**
	 * Singleton instance of the discovery service.
	 */
	private static DiscoveryService discoveryService = null;

	/**
	 * The network port the discovery service listens on for incoming connections.
	 */
	private final int discoveryPort;

	/**
	 * The magic number used to identify this instance of the discovery service.
	 */
	private final int magicNumber;

	/**
	 * The network address the IPC is bound to, possibly <code>null</code>.
	 */
	private final InetAddress ipcAddress;

	/**
	 * The network port that is announced for the job manager's IPC service.
	 */
	private final int ipcPort;

	/**
	 * The thread executing the receive operation on the discovery port.
	 */
	private Thread listeningThread = null;

	/**
	 * The datagram socket of the discovery server.
	 */
	private DatagramSocket serverSocket = null;

	/**
	 * Flag to check whether the service is running
	 */
	private volatile boolean isRunning = false;

	/**
	 * Constructs a new {@link DiscoveryService} object and stores
	 * the job manager's IPC port.
	 * 
	 * @param ipcAddress
	 *        the network address the IPC is bound to, possibly <code>null</code>
	 * @param ipcPort
	 *        the network port that is announced for the job manager's IPC service
	 */
	private DiscoveryService(final InetAddress ipcAddress, final int ipcPort) {

		this.discoveryPort = GlobalConfiguration.getInteger(DISCOVERYPORT_KEY, DEFAULT_DISCOVERYPORT);
		this.magicNumber = GlobalConfiguration.getInteger(MAGICNUMBER_KEY, DEFAULT_MAGICNUMBER);

		this.ipcAddress = ipcAddress;
		this.ipcPort = ipcPort;
	}

	/**
	 * Starts a new discovery service.
	 * 
	 * @param ipcAddress
	 *        the network address the IPC is bound to, possibly <code>null</code>
	 * @param ipcPort
	 *        the network port that is announced for the job manager's IPC service.
	 * @throws DiscoveryException
	 *         thrown if the discovery service could not be started because
	 *         of network difficulties
	 */
	public static synchronized void startDiscoveryService(final InetAddress ipcAddress, final int ipcPort)
			throws DiscoveryException {

		if (discoveryService == null) {
			discoveryService = new DiscoveryService(ipcAddress, ipcPort);
		}

		if (!discoveryService.isRunning()) {
			discoveryService.startService();
		}
	}

	/**
	 * Checks whether the discovery service is running.
	 * 
	 * @return <code>true</code> if the service is running, <code>false</code> otherwise
	 */
	public boolean isRunning() {

		return this.isRunning;
	}

	/**
	 * Stops the discovery service.
	 */
	public static synchronized void stopDiscoveryService() {

		if (discoveryService != null) {
			if (discoveryService.isRunning()) {
				discoveryService.stopService();
			}
		}

	}

	/**
	 * Auxiliary method to start the discovery service.
	 * 
	 * @throws DiscoveryException
	 *         thrown if the discovery service could not be started because
	 *         of network difficulties
	 */
	private void startService() throws DiscoveryException {

		try {
			this.serverSocket = new DatagramSocket(this.discoveryPort, this.ipcAddress);
		} catch (SocketException e) {
			throw new DiscoveryException(e.toString());
		}

		LOG.info("Discovery service socket is bound to " + this.serverSocket.getLocalSocketAddress());

		this.isRunning = true;

		this.listeningThread = new Thread(this, "Discovery Service Thread");
		this.listeningThread.start();
	}

	/**
	 * Auxiliary method to stop the discovery service.
	 */
	private void stopService() {

		if (LOG.isDebugEnabled()) {
			LOG.debug("Stopping discovery service on port" + this.discoveryPort);
		}

		this.isRunning = false;

		this.listeningThread.interrupt();

		// Close the server socket
		this.serverSocket.close();
	}

	/**
	 * Creates a new job manager lookup request packet.
	 * 
	 * @param magicNumber
	 *        the magic number to identify this discovery service
	 * @return a new job manager lookup request packet
	 */
	private static DatagramPacket createJobManagerLookupRequestPacket(final int magicNumber) {

		final byte[] bytes = new byte[12];
		integerToByteArray(magicNumber, MAGIC_NUMBER_OFFSET, bytes);
		integerToByteArray(generateRandomPacketID(), PACKET_ID_OFFSET, bytes);
		integerToByteArray(JM_LOOKUP_REQUEST_ID, PACKET_TYPE_ID_OFFSET, bytes);

		return new DatagramPacket(bytes, bytes.length);
	}

	/**
	 * Creates a new job manager lookup reply packet.
	 * 
	 * @param ipcPort
	 *        the port of the job manager's IPC server
	 * @param magicNumber
	 *        the magic number to identify this discovery service
	 * @return a new job manager lookup reply packet
	 */
	private static DatagramPacket createJobManagerLookupReplyPacket(final int ipcPort, final int magicNumber) {

		final byte[] bytes = new byte[16];
		integerToByteArray(magicNumber, MAGIC_NUMBER_OFFSET, bytes);
		integerToByteArray(generateRandomPacketID(), PACKET_ID_OFFSET, bytes);
		integerToByteArray(JM_LOOKUP_REPLY_ID, PACKET_TYPE_ID_OFFSET, bytes);
		integerToByteArray(ipcPort, PAYLOAD_OFFSET, bytes);

		return new DatagramPacket(bytes, bytes.length);
	}

	/**
	 * Creates a new task manager address request packet.
	 * 
	 * @param magicNumber
	 *        the magic number to identify this discovery service
	 * @return a new task manager address request packet
	 */
	private static DatagramPacket createTaskManagerAddressRequestPacket(final int magicNumber) {

		final byte[] bytes = new byte[12];
		integerToByteArray(magicNumber, MAGIC_NUMBER_OFFSET, bytes);
		integerToByteArray(generateRandomPacketID(), PACKET_ID_OFFSET, bytes);
		integerToByteArray(TM_ADDRESS_REQUEST_ID, PACKET_TYPE_ID_OFFSET, bytes);

		return new DatagramPacket(bytes, bytes.length);
	}

	/**
	 * Creates a new task manager address reply packet.
	 * 
	 * @param taskManagerAddress
	 *        the address of the task manager which sent the request
	 * @param magicNumber
	 *        the magic number to identify this discovery service
	 * @return a new task manager address reply packet
	 */
	private static DatagramPacket createTaskManagerAddressReplyPacket(final InetAddress taskManagerAddress,
			final int magicNumber) {

		final byte[] addr = taskManagerAddress.getAddress();
		final byte[] bytes = new byte[20 + addr.length];
		integerToByteArray(magicNumber, MAGIC_NUMBER_OFFSET, bytes);
		integerToByteArray(generateRandomPacketID(), PACKET_ID_OFFSET, bytes);
		integerToByteArray(TM_ADDRESS_REPLY_ID, PACKET_TYPE_ID_OFFSET, bytes);
		integerToByteArray(addr.length, PAYLOAD_OFFSET, bytes);
		System.arraycopy(addr, 0, bytes, PAYLOAD_OFFSET + 4, addr.length);

		return new DatagramPacket(bytes, bytes.length);
	}

	/**
	 * Returns the network address with which the task manager shall announce itself to the job manager. To determine
	 * the address this method exchanges packets with the job manager.
	 * 
	 * @param jobManagerAddress
	 *        the address of the job manager
	 * @return the address with which the task manager shall announce itself to the job manager
	 * @throws DiscoveryException
	 *         thrown if an error occurs during the packet exchange
	 */
	public static InetAddress getTaskManagerAddress(final InetAddress jobManagerAddress) throws DiscoveryException {

		final int magicNumber = GlobalConfiguration.getInteger(MAGICNUMBER_KEY, DEFAULT_MAGICNUMBER);
		final int discoveryPort = GlobalConfiguration.getInteger(DISCOVERYPORT_KEY, DEFAULT_DISCOVERYPORT);

		InetAddress taskManagerAddress = null;
		DatagramSocket socket = null;

		try {

			socket = new DatagramSocket();
			LOG.debug("Setting socket timeout to " + CLIENTSOCKETTIMEOUT);
			socket.setSoTimeout(CLIENTSOCKETTIMEOUT);

			final DatagramPacket responsePacket = new DatagramPacket(new byte[RESPONSE_PACKET_SIZE],
				RESPONSE_PACKET_SIZE);

			for (int retries = 0; retries < DISCOVERFAILURERETRIES; retries++) {

				final DatagramPacket addressRequest = createTaskManagerAddressRequestPacket(magicNumber);
				addressRequest.setAddress(jobManagerAddress);
				addressRequest.setPort(discoveryPort);

				LOG.debug("Sending Task Manager address request to " + addressRequest.getSocketAddress());
				socket.send(addressRequest);

				try {
					socket.receive(responsePacket);
				} catch (SocketTimeoutException ste) {
					LOG.warn("Timeout wainting for task manager address reply. Retrying...");
					continue;
				}

				if (!isPacketForUs(responsePacket, magicNumber)) {
					LOG.warn("Received packet which is not destined to this Nephele setup");
					continue;
				}

				final int packetTypeID = getPacketTypeID(responsePacket);
				if (packetTypeID != TM_ADDRESS_REPLY_ID) {
					LOG.warn("Received response of unknown type " + packetTypeID + ", discarding...");
					continue;
				}

				taskManagerAddress = extractInetAddress(responsePacket);
				break;
			}

		} catch (IOException ioe) {
			throw new DiscoveryException(StringUtils.stringifyException(ioe));
		} finally {
			if (socket != null) {
				socket.close();
			}
		}

		if (taskManagerAddress == null) {
			throw new DiscoveryException("Unable to obtain task manager address");
		}

		return taskManagerAddress;
	}

	/**
	 * Attempts to retrieve the job managers address in the network through an
	 * IP broadcast. This method should be called by the task manager.
	 * 
	 * @return the socket address of the job manager in the network
	 * @throws DiscoveryException
	 *         thrown if the job manager's socket address could not be
	 *         discovered
	 */
	public static InetSocketAddress getJobManagerAddress() throws DiscoveryException {

		final int magicNumber = GlobalConfiguration.getInteger(MAGICNUMBER_KEY, DEFAULT_MAGICNUMBER);
		final int discoveryPort = GlobalConfiguration.getInteger(DISCOVERYPORT_KEY, DEFAULT_DISCOVERYPORT);

		InetSocketAddress jobManagerAddress = null;
		DatagramSocket socket = null;

		try {

			final Set<InetAddress> targetAddresses = getBroadcastAddresses();

			if (targetAddresses.isEmpty()) {
				throw new DiscoveryException("Could not find any broadcast addresses available to this host");
			}

			socket = new DatagramSocket();

			LOG.debug("Setting socket timeout to " + CLIENTSOCKETTIMEOUT);
			socket.setSoTimeout(CLIENTSOCKETTIMEOUT);

			final DatagramPacket responsePacket = new DatagramPacket(new byte[RESPONSE_PACKET_SIZE],
				RESPONSE_PACKET_SIZE);

			for (int retries = 0; retries < DISCOVERFAILURERETRIES; retries++) {

				final DatagramPacket lookupRequest = createJobManagerLookupRequestPacket(magicNumber);

				for (InetAddress broadcast : targetAddresses) {
					lookupRequest.setAddress(broadcast);
					lookupRequest.setPort(discoveryPort);
					LOG.debug("Sending discovery request to " + lookupRequest.getSocketAddress());
					socket.send(lookupRequest);
				}

				try {
					socket.receive(responsePacket);
				} catch (SocketTimeoutException ste) {
					LOG.debug("Timeout wainting for discovery reply. Retrying...");
					continue;
				}

				if (!isPacketForUs(responsePacket, magicNumber)) {
					LOG.debug("Received packet which is not destined to this Nephele setup");
					continue;
				}

				final int packetTypeID = getPacketTypeID(responsePacket);
				if (packetTypeID != JM_LOOKUP_REPLY_ID) {
					LOG.debug("Received unexpected packet type " + packetTypeID + ", discarding... ");
					continue;
				}

				final int ipcPort = extractIpcPort(responsePacket);

				// Replace port from discovery service with the actual RPC port
				// of the job manager
				if (USE_IPV6) {
					// TODO: No connection possible unless we remove the scope identifier
					if (responsePacket.getAddress() instanceof Inet6Address) {
						try {
							jobManagerAddress = new InetSocketAddress(InetAddress.getByAddress(responsePacket
								.getAddress()
								.getAddress()), ipcPort);
						} catch (UnknownHostException e) {
							throw new DiscoveryException(StringUtils.stringifyException(e));
						}
					} else {
						throw new DiscoveryException(responsePacket.getAddress() + " is not a valid IPv6 address");
					}
				} else {
					jobManagerAddress = new InetSocketAddress(responsePacket.getAddress(), ipcPort);
				}
				LOG.debug("Discovered job manager at " + jobManagerAddress);
				break;
			}

		} catch (IOException ioe) {
			throw new DiscoveryException(ioe.toString());
		} finally {
			if (socket != null) {
				socket.close();
			}
		}

		if (jobManagerAddress == null) {
			LOG.debug("Unable to discover Jobmanager via IP broadcast");
			throw new DiscoveryException("Unable to discover JobManager via IP broadcast!");
		}

		return jobManagerAddress;
	}

	/**
	 * Extracts an IPC port from the given datagram packet. The datagram packet must be of the type
	 * <code>JM_LOOKUP_REPLY_PACKET_ID</code>.
	 * 
	 * @param packet
	 *        the packet to extract the IPC port from.
	 * @return the extracted IPC port or <code>-1</code> if the port could not be extracted
	 */
	private static int extractIpcPort(DatagramPacket packet) {

		final byte[] data = packet.getData();

		if (data == null) {
			return -1;
		}

		if (packet.getLength() < (PAYLOAD_OFFSET + 4)) {
			return -1;
		}

		return byteArrayToInteger(data, PAYLOAD_OFFSET);
	}

	/**
	 * Extracts an {@link InetAddress} object from the given datagram packet. The datagram packet must be of the type
	 * <code>TM_ADDRESS_REPLY_PACKET_ID</code>.
	 * 
	 * @param packet
	 *        the packet to extract the address from
	 * @return the extracted address or <code>null</code> if it could not be extracted
	 */
	private static InetAddress extractInetAddress(DatagramPacket packet) {

		final byte[] data = packet.getData();

		if (data == null) {
			return null;
		}

		if (packet.getLength() < PAYLOAD_OFFSET + 8) {
			return null;
		}

		final int len = byteArrayToInteger(data, PAYLOAD_OFFSET);

		final byte[] addr = new byte[len];
		System.arraycopy(data, PAYLOAD_OFFSET + 4, addr, 0, len);

		InetAddress inetAddress = null;

		try {
			inetAddress = InetAddress.getByAddress(addr);
		} catch (UnknownHostException e) {
			return null;
		}

		return inetAddress;
	}

	/**
	 * Returns the set of broadcast addresses available to the network interfaces of this host. In case of IPv6 the set
	 * contains the IPv6 multicast address to reach all nodes on the local link. Moreover, all addresses of the loopback
	 * interfaces are added to the set.
	 * 
	 * @return (possibly empty) set of broadcast addresses reachable by this host
	 */
	private static Set<InetAddress> getBroadcastAddresses() {

		final Set<InetAddress> broadcastAddresses = new HashSet<InetAddress>();

		// get all network interfaces
		Enumeration<NetworkInterface> ie = null;
		try {
			ie = NetworkInterface.getNetworkInterfaces();
		} catch (SocketException e) {
			LOG.error("Could not collect network interfaces of host", e);
			return broadcastAddresses;
		}

		while (ie.hasMoreElements()) {
			NetworkInterface nic = ie.nextElement();
			try {
				if (!nic.isUp()) {
					continue;
				}

				if (nic.isLoopback()) {
					for (InterfaceAddress adr : nic.getInterfaceAddresses()) {
						broadcastAddresses.add(adr.getAddress());
					}
				} else {

					// check all IPs bound to network interfaces
					for (InterfaceAddress adr : nic.getInterfaceAddresses()) {

						if (adr == null) {
							continue;
						}

						// collect all broadcast addresses
						if (USE_IPV6) {
							try {
								final InetAddress interfaceAddress = adr.getAddress();
								if (interfaceAddress instanceof Inet6Address) {
									final Inet6Address ipv6Address = (Inet6Address) interfaceAddress;
									final InetAddress multicastAddress = InetAddress.getByName(IPV6MULTICASTADDRESS
										+ "%"
										+ Integer.toString(ipv6Address.getScopeId()));
									broadcastAddresses.add(multicastAddress);
								}

							} catch (UnknownHostException e) {
								LOG.error(e);
							}
						} else {
							final InetAddress broadcast = adr.getBroadcast();
							if (broadcast != null) {
								broadcastAddresses.add(broadcast);
							}
						}
					}
				}

			} catch (SocketException e) {
				LOG.error("Socket exception when checking " + nic.getName() + ". " + "Ignoring this device.", e);
			}
		}

		return broadcastAddresses;
	}

	/**
	 * Server side implementation of Discovery Service.
	 */
	@Override
	public void run() {

		final DatagramPacket requestPacket = new DatagramPacket(new byte[64], 64);

		final Map<Integer, Long> packetIDMap = new HashMap<Integer, Long>();

		while (this.isRunning) {

			try {
				this.serverSocket.receive(requestPacket);

				if (!isPacketForUs(requestPacket, this.magicNumber)) {
					LOG.debug("Received request packet which is not destined to this Nephele setup");
					continue;
				}

				final Integer packetID = Integer.valueOf(extractPacketID(requestPacket));
				if (packetIDMap.containsKey(packetID)) {
					LOG.debug("Request with ID " + packetID.intValue() + " already answered, discarding...");
					continue;
				} else {

					final long currentTime = System.currentTimeMillis();

					// Remove old entries
					final Iterator<Map.Entry<Integer, Long>> it = packetIDMap.entrySet().iterator();
					while (it.hasNext()) {

						final Map.Entry<Integer, Long> entry = it.next();
						if ((entry.getValue().longValue() + 5000L) < currentTime) {
							it.remove();
						}
					}

					packetIDMap.put(packetID, Long.valueOf(currentTime));
				}

				final int packetTypeID = getPacketTypeID(requestPacket);
				if (packetTypeID == JM_LOOKUP_REQUEST_ID) {

					LOG.debug("Received job manager lookup request from " + requestPacket.getSocketAddress());
					final DatagramPacket responsePacket = createJobManagerLookupReplyPacket(this.ipcPort,
						this.magicNumber);
					responsePacket.setAddress(requestPacket.getAddress());
					responsePacket.setPort(requestPacket.getPort());

					this.serverSocket.send(responsePacket);

				} else if (packetTypeID == TM_ADDRESS_REQUEST_ID) {
					LOG.debug("Received task manager address request from " + requestPacket.getSocketAddress());
					final DatagramPacket responsePacket = createTaskManagerAddressReplyPacket(requestPacket
						.getAddress(), this.magicNumber);
					responsePacket.setAddress(requestPacket.getAddress());
					responsePacket.setPort(requestPacket.getPort());

					this.serverSocket.send(responsePacket);

				} else {
					LOG.debug("Received packet of unknown type " + packetTypeID + ", discarding...");
				}

			} catch (SocketTimeoutException ste) {
				LOG.debug("Discovery service: socket timeout");
			} catch (IOException ioe) {
				if (this.isRunning) { // Ignore exception when service has been stopped
					LOG.error("Discovery service stopped working with IOException:\n" + ioe.toString());
				}
				break;
			}
		}

		// Close the socket finally
		this.serverSocket.close();
	}

	/**
	 * Serializes and writes the given integer number to the provided byte array.
	 * 
	 * @param integerToSerialize
	 *        the integer number of serialize
	 * @param offset
	 *        the offset at which to start writing inside the byte array
	 * @param byteArray
	 *        the byte array to write to
	 */
	private static void integerToByteArray(final int integerToSerialize, final int offset, final byte[] byteArray) {

		for (int i = 0; i < 4; ++i) {
			final int shift = i << 3; // i * 8
			byteArray[(offset + 3) - i] = (byte) ((integerToSerialize & (0xff << shift)) >>> shift);
		}
	}

	/**
	 * Reads and deserializes an integer number from the given byte array.
	 * 
	 * @param byteArray
	 *        the byte array to read from
	 * @param offset
	 *        the offset at which to start reading the byte array
	 * @return the deserialized integer number
	 */
	private static int byteArrayToInteger(final byte[] byteArray, final int offset) {

		int integer = 0;

		for (int i = 0; i < 4; ++i) {
			integer |= (byteArray[(offset + 3) - i] & 0xff) << (i << 3);
		}

		return integer;
	}

	/**
	 * Extracts the datagram packet's magic number and checks it matches with the local magic number.
	 * 
	 * @param packet
	 *        the packet to check
	 * @param magicNumber
	 *        the magic number identifying the discovery service
	 * @return <code>true</code> if the packet carries the magic number expected by the local service, otherwise
	 *         <code>false</code>
	 */
	private static boolean isPacketForUs(final DatagramPacket packet, final int magicNumber) {

		final byte[] data = packet.getData();

		if (data == null) {
			return false;
		}

		if (packet.getLength() < (MAGIC_NUMBER_OFFSET + 4)) {
			return false;
		}

		if (byteArrayToInteger(data, MAGIC_NUMBER_OFFSET) != magicNumber) {
			return false;
		}

		return true;
	}

	/**
	 * Extracts the packet type ID from the given datagram packet.
	 * 
	 * @param packet
	 *        the packet to extract the type ID from
	 * @return the extracted packet type ID or <code>-1</code> if the ID could not be extracted
	 */
	private static int getPacketTypeID(final DatagramPacket packet) {

		final byte[] data = packet.getData();

		if (data == null) {
			return -1;
		}

		if (packet.getLength() < (PACKET_TYPE_ID_OFFSET + 4)) {
			return -1;
		}

		return byteArrayToInteger(data, PACKET_TYPE_ID_OFFSET);
	}

	/**
	 * Generates a random packet ID.
	 * 
	 * @return a random packet ID
	 */
	private static int generateRandomPacketID() {

		return (int) (Math.random() * (double) Integer.MAX_VALUE);
	}

	/**
	 * Extracts the packet ID from the given packet.
	 * 
	 * @param packet
	 *        the packet to extract the ID from
	 * @return the extracted ID or <code>-1</code> if the ID could not be extracted
	 */
	private static int extractPacketID(final DatagramPacket packet) {

		final byte[] data = packet.getData();

		if (data == null) {
			return -1;
		}

		if (data.length < (PACKET_ID_OFFSET + 4)) {
			return -1;
		}

		return byteArrayToInteger(data, PACKET_ID_OFFSET);
	}
}
