//------------------------------------------------------------------------------
//
// File NetworkSocket.java
//
// Created by:
//          Tamer Elsharnouby.    (sharno@cs.umd.edu)
// 
// Course :
//          CMSC417 Fall 2002
//
// Description :
//   This file provides an unreliable network layer; specifically, it provides
//   so-called LRD sockets that are exactly like standard network sockets except
//   that they subject the messages to
//    - loss, reordering, duplicatation with specified probabilities
//    - specified maximum message lifetime
//    - specified maximum message size
//    - transmission delay corresponding to specified bandwidth
//   
//   Each LRD socket contains a standard UDP network socket, an outgoing message
//   buffer, and a "sender" thread that sends messages into the UDP socket.
//   A message sent into the LRD socket is appended to the tail of the outgoing
//   message buffer if there is space.
//   The sender thread sends the messages of the outgoing message buffer after
//   subjecting them to possible errors (loss, reordering, duplication),
//   the maximum message lifetime, and transmission delay.
//   Specifically, the sender thread does the following:
//     - Wakes up whenever the outgoing message buffer becomes nonempty
//       or upon closing the LRD socket.
//     - If the outgoing message buffer is empty, it goes back to sleep.
//     - If the outgoing message buffer is not empty, it probabilistically
//       subjects the head message to errors, deletes the message if its age
//       exceeds the max lifetime, and, if the message is not deleted, sleeps
//       for a duration equal to the message transmission time (which depends
//       on message size and bandwidth).
//   The receive method on the LRD socket is directly mapped to the receive
//   method on the UDP socket; that is, a message received at the UDP socket
//   is immediately available for reception at the LRD socket.
//   
//   The following parameters can be updated during execution:
//     - loss probability
//     - reordering probability
//     - duplication probability
//     - bandwidth
//
//   This file contains three classes:
//     Class NetworkSocket    // LRD socket
//     Class NetworkSender    // sender thread
//     Class Message          // message structure
//----------------------------------------------------------------------------- 

import java.net.*;
import java.lang.*;
import java.util.*;
import java.io.*;
import java.sql.*;


//  Class NetworkSocket
//  
//  Attributes :
//  ------------
//  localPort   : Local port of the LRD socket.
//  udpSocket   : UDP Socket associated with LRD socket.
//  remoteAddr  : IP address of the destination host.
//  remotePort  : Destination port.
//  outBuf      : Buffer of outgoing messages.
//  outBufSize  : Size of outBuf in bytes.
//  outBufUsed  : Used portion of outBuf in bytes.
//  msgMaxSize  : msg maximum size. LRD socket discards msgs with 
//                size > msgMaxSize
//  msgMaxLifetime : msg maximum lifetime. LRD socket discards msgs that stay in
//                   outBuf longer than msgMaxLifetime
//  inverseBW   : Inverse of bandwidth. 
//                msg transmission time = msg size (in bytes) * inverseBW
//  lossProb    : Probability of losing a msg.
//  duplicateProb : Probability of duplicating a msg.
//  reorderProb : Probability of reordering of a msg.
//  maxNoOfErrs : Max number of errors (reorder, duplicates) that LRD socket can
//                apply to a msg.
//  generator   : Uniform [0,1] random number generator.
//------------------------------------------------------------------------------
class NetworkSocket extends Object {
    private int            localPort = 0;
    private DatagramSocket udpSocket = null;
    
    private InetAddress    remoteAddr;
    private int            remotePort;
    private Vector         outBuf =  new Vector(32);
    private int            outBufSize = 64*1024;
    private int            outBufUsed = 0;
        
    private static int      msgMaxSize    = 512;
    public  static int      msgMaxLifetime= 30000;

    public  static double    inverseBW     = 0.01;
    private static double    lossProb      = 0.0;
    private static double    duplicateProb = 0.0;
    private static double    reorderProb   = 0.0;
    private static final int maxNoOfErrs   = 5;

    private Random           generator = new Random(); 
    public  boolean          contWork  = true;
    
 
    // Method: NetworkSocket
    // 
    // Constructor of the class. It creates a UDP socket, binds it to
    // remote address, and launches the NetworkSender thread.
    //--------------------------------------------------------------------------
    NetworkSocket(int aLocalPort, String remoteDN, int aRemotePort){
	try {
	    localPort = aLocalPort;
	    udpSocket= new DatagramSocket(localPort);   // Creates local socket
	    remoteAddr = InetAddress.getByName(remoteDN);
	} catch(UnknownHostException uhe) {
	    throw new Error("Unknown host or invalid connection port.");
	} catch(SocketException se) {
	    throw new Error("Socket could not be bound to the specified " +
			    "local port number.");
	} catch (Exception e) {
	    throw new  Error("NetworkSocket construction failed.");
	}    
	remotePort = aRemotePort;
	new NetworkSender(this, udpSocket).start();
    }
    

    // Method: close
    // Closes the UDP socket and destroys the NetworkSender thread.
    //--------------------------------------------------------------------------
    synchronized public void close() {
	contWork = false;
	notifyAll();  // wakes up NetworkSender if waiting
	udpSocket.close();
	System.out.println("LRD socket and its UDP socket are closed");
    }

    // Method: getMsgMaxSize
    //--------------------------------------------------------------------------
    synchronized public int getMsgMaxSize() {
	return msgMaxSize;
    }


    // Method: getHeadMsg
    // Used by the NetworkSender thread. This method:
    //     - If outBuf is empty, waits until it is woken up (which happens
    //       when outBuf becomes nonempty or LRD socket is closed).
    //     - If outBuf is not empty or upon waking up,
    //         it probabilistically subjects the head message to errors,
    //         returns the head msg of outBuf (if msg not reordered or lost)
    //         or null (otherwise).
    //--------------------------------------------------------------------------
    synchronized public Object getHeadMsg() 
	throws IllegalMonitorStateException, InterruptedException {
	Message msg = null;
	if (outBuf.isEmpty())
	    wait();	
	try {
	    msg =  ((Message) outBuf.elementAt(0));
	    if (msg == null) // LRD socket closed
		return null;
	    outBuf.removeElementAt(0);
	    outBufUsed = outBufUsed - msg.data.getLength();
	    if (msg.errorsCount == maxNoOfErrs)  // no errors if msg already 
		return msg;                      // subjected to maxNoOfErrs.
	    if  (generator.nextDouble() < lossProb) // lose msg
		return null;		
	    if ((!outBuf.isEmpty()) &&(generator.nextDouble() < reorderProb)) {
		// reorder msg
		msg.errorsCount++;
		outBuf.insertElementAt(msg, ((int) (outBuf.size() * 
						    generator.nextDouble())));
		outBufUsed = outBufUsed + msg.data.getLength();
		return null;
	    }		
	    if ((generator.nextDouble() < duplicateProb) &&
		(outBufUsed +  msg.data.getLength() <= outBufSize)) {
		// duplicate msg 
		Message duplicateMsg = 
		    new Message(new DatagramPacket (msg.data.getData(), 
						    msg.data.getLength(), 
						    remoteAddr, remotePort));
		duplicateMsg.errorsCount = msg.errorsCount + 1;
		// outBuf.addElement(duplicateMsg);
		outBuf.insertElementAt(duplicateMsg, ((int) (outBuf.size() * 
						    generator.nextDouble())));
		outBufUsed = outBufUsed +  duplicateMsg.data.getLength();
	    }
	} catch (ArrayIndexOutOfBoundsException aiobe) {
	    return null;
	}
	return msg;
    }
  
    // Method: send
    // Discards the msg if it is larger than msgMaxSize; otherwise makes a copy
    // of the msg, inserts it at outBuf tail, and notifies NetworkSender
    // that outBuf is not empty (in case NetworkSender is waiting).
    // 
    // Parameters :
    // aData       : data to be sent.
    // len         : length of data in bytes.
    //--------------------------------------------------------------------------
    synchronized public void send(byte[] aData, int len) {
	if (len > msgMaxSize ) // discard packet if it is too large
	    return;
	byte data[] = new byte[len];
	for (int i=0; i<len; i++)
	    data[i] = aData[i];
	outBuf.addElement( new Message
	    (new DatagramPacket (data, len, remoteAddr, remotePort)));
	notifyAll();
	outBufUsed = outBufUsed + len;
    }
    

    // Method: receive
    // Directly maps to receive method on the UDP socket.
    // 
    // Parameters : 
    // dp         : datagram packet received.
    // timeout    : Timeout (in ms). This method throws InterruptedIOException
    //              if no datagram packet is received within the timeout.
    // throws     :
    // InterruptedIOException : in case of timeout flag being raised.
    // IOException            : in case of IO error.
    //--------------------------------------------------------------------------
    public void receive(DatagramPacket dp, int timeout) 
	throws IOException, InterruptedIOException {
	udpSocket.setSoTimeout(timeout);
	udpSocket.receive(dp);
    }
}


//  Class NetworkSender
//  
//   This thread sends the messages of outBuf, subjecting them to probabilistic
//   errors (loss, reordering, duplication), maximum message lifetime, and
//   transmission delay.  Specifically, it does the following:
//     - calls getHeadMsg to get either the head msg of outBuf or null
//       (note: getHeadMsg waits until outBuf is not empty or LRD socket is 
//        closed and returns null if msg is lost/reordered or LRD socket is 
//        closed).
//     - If msg not null
//       - Sleeps for msg transmission duration (= msg size * inverse bandwidth)
//       - Discards msg if overaged (i.e., in outBuf for more than 
//         msgMaxLifetime)
//       - Sends the msg on UDP socket.
//
//  Attributes :
//  ------------
//  ns         : LRD socket which launched this thread.
//  udpSocket  : UDP socket created in the LRD socket.
//------------------------------------------------------------------------------
class NetworkSender extends Thread {
    private NetworkSocket ns = null;
    private DatagramSocket udpSocket = null;
    
    NetworkSender (NetworkSocket aNs, DatagramSocket aUDPSocket) {
	ns = aNs;
	udpSocket = aUDPSocket;
    }
    
    public void run() {    
	if(udpSocket!= null) 
	    while (ns.contWork) {
		try {
		    Message msg = (Message) ns.getHeadMsg();
		    if (msg != null){ 			  
			sleep ((int)(NetworkSocket.inverseBW *  
				     msg.data.getLength()));
			if (msg.timestamp.getTime() + ns.msgMaxLifetime > 
			    (new java.util.Date()).getTime())
			    udpSocket.send(msg.data);
		    }
		} catch (InterruptedException ie) {
		} catch (IllegalMonitorStateException imse) {
		    throw new Error("Not an owner of the  monitor.");
		} catch(Exception f) {
		    throw new Error("Sending message "+ f.getMessage() + 
				    " failed");
		}
	    }
	System.out.println("NetworkSender is closed.");
    } 
}



//  Class Message
//  
//  Stores msg data and records msg's timestamp (determines msg lifetime) and 
//  number of errors affected the msg.
//
//  Attributes :
//  ------------
//  data       : Msg data 
//  timestamp  : Timestamp of msg creation
//  errorsCount: Number of errors applied to this msg.
//------------------------------------------------------------------------------
class Message extends Object{
    public DatagramPacket data = null;
    public Timestamp timestamp = null;
    public int errorsCount = 0;
    Message(DatagramPacket aData) {
	data = aData;
	timestamp = new Timestamp((new java.util.Date()).getTime());
    }
}
