package org.etsi.its.network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.etsi.common.ByteHelper;

/**
 * This class implements an UDP client/server
 */
public class UdpImpl implements IUdpImpl {

	/**
	 * Module version
	 */
	protected static final String Version = "1.0.0.0";

	/**
	 * Logger instance
	 */
	protected final static Logger _logger = Logger.getLogger("org.etsi.its");

	/**
	 * The IPv4 address
	 */
	private InetAddress _serveur;

	/**
	 * The listener port to receive datas packets
	 */
	private int _localPort;

	/**
	 * The remote port to send datas packets
	 */
	private int _remotePort;

	private DatagramSocket _socket;

	/**
	 * Datagram packet for sending datas
	 */
	private DatagramPacket _dataSent;

	/**
	 * Datagram packet for receiving datas
	 */
	private IUdpImplEvent _asyncReceive;

	/**
	 * Internal thread for asynchronous receive operation
	 */
	private AsynchronousReceive _asynchronousReceive;

	/**
	 * The size of the buffer for receiving datas
	 * Defailt value: 4096
	 */
	private int _bufferReceiveSize;

	/**
	 * @return the _bufferReceiveSize
	 */
	public final int getBufferReceiveSize() {
		return _bufferReceiveSize;
	}

	/**
	 * @param bufferReceiveSize the _bufferReceiveSize to set
	 */
	public final void setBufferReceiveSize(int bufferReceiveSize) {
		_bufferReceiveSize = bufferReceiveSize;
	}

	/**
	 * Constructor
	 */
	public UdpImpl() {
		_logger.entering("UdpImpl", "UdpImpl", String.format("Version: %s", Version));

		_serveur = null;
		_localPort = -1;
		_remotePort = -1;
		_socket = null;
		_dataSent = null;
		_asyncReceive = null;
		_asynchronousReceive = null;
		_bufferReceiveSize = 4096;
	} // End of Constructor

	/**
	 * Initialises the new instance
	 * 
	 * @param address The IPv4 address
	 * @param remotePort The remote port to send datas packets
	 * @param localPort The listener port to receive datas packets
	 * @throws UnknownHostException Thrown to indicate that the IP address of a host could not be determined
	 * @throws SocketException Thrown to indicate that there is an error in the underlying protocol, such as a TCP error
	 */
	@Override
	public void initialize(final String address, final int remotePort, final int localPort) throws UnknownHostException, SocketException {
		_logger.entering("UdpImpl", "initialize", String.format("%s - %d - %d", address, remotePort, localPort));

		initialize(address, remotePort, localPort, null);
	} // End of method initialize

	/**
	 * Initialise the new instance
	 * 
	 * @param address The IPv4 address
	 * @param remotePort The remote port to send datas packets
	 * @param localPort The listener port to receive datas packets
	 * @param asyncReceive A reference to an object implementing the callback <code>onReceive</code>, used for asynchronous receive operations
	 * @throws UnknownHostException Thrown to indicate that the IP address of a host could not be determined
	 * @throws SocketException Thrown to indicate that there is an error in the underlying protocol, such as a TCP error
	 * 
	 * @see IUdpImplEvent
	 */
	@Override
	public void initialize(final String address, final int remotePort, final int localPort, final IUdpImplEvent asyncReceive) throws UnknownHostException, SocketException
	{
		_logger.entering("UdpImpl", "initialize", String.format("%s - %d - %d - %s", address, remotePort, localPort, (asyncReceive == null) ? "(null)" : asyncReceive.toString()));

		// Sanity checks
		if ((address == null) || address.isEmpty() || ((remotePort <= 0) && (localPort <= 0))) {
			throw new IllegalArgumentException();
		}

		// Initialize Udp part
		_serveur = InetAddress.getByName(address);
		_localPort = localPort;
		_remotePort = remotePort;
		if (localPort > 0) {
			_socket = new DatagramSocket(_localPort);
		} else {
			_socket = new DatagramSocket();
		}
		// Set callback mechanism
		_asyncReceive = asyncReceive;
		if (_asyncReceive != null) {
			// Initiate the thread
			_asynchronousReceive = new AsynchronousReceive();
			_asynchronousReceive.start();
		}
	} // End of method initialize

	/**
	 * Un-initialises the current instance
	 */
	@Override
	public void uninitialize() {
		_logger.entering("UdpImpl", "uninitialize");

		  // Sanity checks
		if (_socket == null) {
			return;
		}
		_socket.close();
		_socket = null;
		
		if (_asynchronousReceive != null) {
			try {
				_asynchronousReceive.Terminate();
				_asynchronousReceive = null;
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}		
	} // End of method uninitialize

	/**
	 * Un-initialises the current instance
	 */
	@Override
	public void send(byte[] buffer) throws IOException {
		_logger.entering("UdpImpl", "send");

		// Sanity checks
		if (buffer == null) {
			throw new IllegalArgumentException();
		}
		
		if (buffer.length != 0) {
			_dataSent = new DatagramPacket(buffer, buffer.length, _serveur, _remotePort);
			_socket.send(_dataSent);
		}
	}

	/**
	 * Receives datas in synchronous mode
	 * 
	 * note that this method cannot be called if asynchronous receive mode is used
	 * @return The datas packet on success, null otherwise
	 * @throws IOException Signals that an I/O exception of some sort has occurred
	 */
	@Override
	public byte[] receive() throws IOException {
		_logger.entering("UdpImpl", "receive");

		if (_asyncReceive != null) {
			throw new RuntimeException("Asynchronous mode shall be used to receive data");
		}
		DatagramPacket dataRecieved = new DatagramPacket(new byte[4096], 4096);
		_socket.receive(dataRecieved);
		return ByteHelper.extract(dataRecieved.getData(), dataRecieved.getOffset(), dataRecieved.getLength());
	} // End of method receive

	/**
	 * Thread used for asynchronous receive operation
	 */
	private class AsynchronousReceive extends Thread {

		private boolean _terminateThread;

		/**
		 * Constructor
		 */
		public AsynchronousReceive() {
			_logger.entering("AsynchronousReceive", "AsynchronousReceive");
			_terminateThread = false;
			
		} // End of Constructor

		/**
		 * Terminates asynchronous receive operation
		 * @throws InterruptedException Thrown when a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted, either before or during the activity
		 */
		public void Terminate() throws InterruptedException {
			_logger.entering("AsynchronousReceive", "Terminate");

			_terminateThread = true;			
			join();
		} // End of method Terminate

		@Override
		public void run() {
			while (!_terminateThread) {
				try {
					byte[] buffer = new byte[_bufferReceiveSize];
					DatagramPacket dataRecieved = new DatagramPacket(buffer, buffer.length);
					_socket.receive(dataRecieved);
					_logger.info(String.format("Receive %d bytes from %s", dataRecieved.getLength(), dataRecieved.getSocketAddress().toString()));
					if (_logger.isLoggable(Level.ALL)) ByteHelper.dump("Receive:", dataRecieved.getData());
					_asyncReceive.onReceive(ByteHelper.extract(dataRecieved.getData(), dataRecieved.getOffset(), dataRecieved.getLength()));
				} catch (java.net.SocketException s) {
					_terminateThread = true;
				} catch (IOException e) {
					_terminateThread = true;
				} catch (Exception e) {
					e.printStackTrace();
					_terminateThread = true;
				}
			}
		}
		
	}
}
