package fr.umlv.ir2.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.HashMap;

import static fr.umlv.ir2.util.Converter.LONG_SIZE;
import static fr.umlv.ir2.util.Converter.byteArrayToLong;
import static fr.umlv.ir2.util.Converter.longToByteArray;

/**
 * This class offers several implementations of a UDP based server that 
 * sums long primitive operands sent by its clients.
 * Both server and protocols are quite stupid: the server successively 
 * receives operands from its client (byte array representation of long 
 * values in network order -- big endian) and sums these operands together 
 * with sending it as a response to the client. The server considers that  
 * receiving the value 0 means the end of the current sum. It then resets 
 * its internal counter and waits to start a new sum.
 * @author duris
 *
 */
public class SumServer {

	private final DatagramSocket ds;

	/**
	 * Creates a sum server, bound to the specified port.
	 * @param port the port number.
	 * @throws SocketException
	 */
	public SumServer(int port) throws SocketException {
		ds = new DatagramSocket(port);
	}

	/**
	 * Launch a "basic" version of this sum server, ignoring distinctions 
	 * between data received from its clients. Basically, this server takes 
	 * only into account received datagram, not considering potentially 
	 * interleaving clients. Thus, operands of two client could be added 
	 * together and mixed.
	 * @throws IOException
	 */
	public void launchBasic() throws IOException {
		byte[] receiveBuffer = new byte[LONG_SIZE];
		byte[] sendBuffer = new byte[LONG_SIZE];

		DatagramPacket dp = new DatagramPacket(receiveBuffer, 0, LONG_SIZE);
		long counter = 0;
		while(!Thread.currentThread().isInterrupted()) {
			ds.receive(dp);
			try {
				long lu = byteArrayToLong(receiveBuffer);
				System.err.println(lu + " bytes read from " + dp.getSocketAddress());
				counter += lu;
				longToByteArray(counter, sendBuffer);
				dp.setData(sendBuffer);
				ds.send(dp);
				System.err.println(counter + " is the sum sent to " + dp.getSocketAddress());
				// client explicitly sends 0 to reset the counter
				if (lu == 0) {
					counter = 0;
					System.err.println("Counter reset");
				}
			} catch (IllegalArgumentException iae) {
				// if incompatible number of bytes in arrays, simply ignore the datagram 
				iae.printStackTrace();
			}
			dp.setData(receiveBuffer);
		}
	}

	/**
	 * Launch a version of this sum server that prevents operands 
	 * of distinct clients to be mixed in a single sum, by pseudo-connecting 
	 * the first client until the end of its addition.
	 * @throws IOException
	 */
	public void launchPseudo() throws IOException {
		byte[] receiveBuffer = new byte[LONG_SIZE];
		byte[] sendBuffer = new byte[LONG_SIZE];

		DatagramPacket dp = new DatagramPacket(receiveBuffer, 0, LONG_SIZE);
		long counter = 0;
		while(!Thread.currentThread().isInterrupted()) {
			try {
				ds.receive(dp);
				if (!ds.isConnected()) {
					ds.connect(dp.getSocketAddress());
					ds.setSoTimeout(30000); // if nothing received within 30 sec, timeout
					System.out.println("Starts pseudo-connection with " + dp.getSocketAddress());
				}
				long lu = byteArrayToLong(receiveBuffer);
				System.err.println(lu + " bytes read from " + dp.getSocketAddress());
				counter += lu;
				longToByteArray(counter, sendBuffer);
				dp.setData(sendBuffer);
				ds.send(dp);
				System.err.println(counter + " is the sum sent to " + dp.getSocketAddress());
				// client explicitly sends 0 to reset the counter
				if (lu == 0) {
					ds.disconnect();
					ds.setSoTimeout(0); // if no pseudo connected client, infinite timeout
					System.out.println("Pseudo-connection ended with " + dp.getSocketAddress());
					counter = 0;
					System.err.println("Counter reset");
				}
			} catch (IllegalArgumentException iae) {
				// if incompatible number of bytes in arrays, simply ignore the datagram 
				iae.printStackTrace();
			} catch (SocketTimeoutException ste) {
				ds.disconnect();
				ds.setSoTimeout(0); // if no pseudo connected client, infinite timeout
				System.out.println("Pseudo-connection arborted");
				counter = 0;
				System.err.println("Counter reset");
			}
			dp.setData(receiveBuffer);
		}
	}

	/**
	 * Launch a version of this sum server allowing serveral clients 
	 * to be served simultaneously, without mixing their operands. 
	 * This relies on an association (map) of operand received and 
	 * current sum to client identifier (ip+port).
	 * @throws IOException
	 */
	public void launchMap() throws IOException {
		HashMap<SocketAddress, Long> map = new HashMap<SocketAddress, Long>();
		byte[] receiveBuffer = new byte[LONG_SIZE];
		byte[] sendBuffer = new byte[LONG_SIZE];

		DatagramPacket dp = new DatagramPacket(receiveBuffer, 0, LONG_SIZE);
		while(!Thread.currentThread().isInterrupted()) {
			ds.receive(dp);
			try {
				Long value = byteArrayToLong(receiveBuffer);
				SocketAddress client = dp.getSocketAddress();
				System.err.println(value + " bytes read from " + client);
				Long counter = map.get(client);
				if(counter == null)
					counter = 0L;
				counter += value;
				longToByteArray(counter, sendBuffer);
				dp.setData(sendBuffer);
				ds.send(dp);
				System.err.println(counter + " is the sum sent to " + client);
				// client explicitly sends 0 to reset the counter
				if (value == 0) {
					map.remove(client);
					System.err.println("Counter reset");
				} else {
					map.put(client,counter);
				}
			} catch (IllegalArgumentException iae) {
				// if incompatible number of bytes in arrays, simply ignore the datagram 
				iae.printStackTrace();
			}
			dp.setData(receiveBuffer);
		}
	}

	public static void usage() {
		System.out.println("java fr.umlv.ir2.udp.SumServer <listeningPort>");
	}

	/**
	 * @param args the port number to bound the server
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		if (args.length < 1) {
			usage();
			System.exit(0);
		}
		int port = Integer.parseInt(args[0]);
		SumServer server = new SumServer(port);

		/* with basic, operands of several clients could be 
		 * merged together */
		// server.launchBasic();  

		/* with "pseudo", we have to manually pseudo-connect 
		 *	and pseudo-disconnect the client. If the server is pseudo
		 * connected to a given client, all data sent by other clients 
		 * are lost */
		// server.launchPseudo();

		/* with "map" the server associates each received operand 
		 * (and current counter) to its corresponding client, 
		 * that is ip+port */  
		server.launchMap();

	}

}
