
import java.io.*;
import java.net.*;


/**
 * This class supports basic, text-based communication between two computers
 * on the network.  For opening a connection, a SimpleNet object can run
 * in either server mode -- where it waits for an incoming connection -- or in
 * client mode -- where it tries to make a connection to a waiting server.  Use
 * the listen() method to run as a server; use connect() to run as a client.
 * Once the connection has been opened, it makes no difference whether server
 * or client mode was used.  Lines of text can be transmitted in either
 * direction.  Use the send() method to transmit a line of text.  Receiving
 * messages is more complicated, since they can arrive asynchronously.  Also,
 * a connection can be closed asynchronously from the other side.  And when
 * operating in server mode, the connection is opened asynchronously.  To support
 * asynchronous operation, a SimpleNet object must have an "observer" that
 * implements the SimpleNetObserver interface.  This interface defines several
 * methods that are called by the SimpleNet object when network events occur.
 * See that interface for more information.
 */
public class SimpleNet {
	
	/**
	 * Possible state that a SimpleNet object can be in.  The states are mostly used
	 * internally in this class, but you can find out the state by calling getState().
	 */
	public final static int IDLE = 1;
	public final static int WAITING_FOR_CONNECTION = 2;
	public final static int CONNECTING = 3;
	public final static int CONNECTED = 4;
	public final static int CLOSING = 5;
	
	private SimpleNetObserver owner;
	private int state;
	private volatile PrintWriter out;
	private ConnectionHandler connectionHandler; // ConnectionHandler is a nested class, defined below.
	
	/**
	 * Create a SimpleNet object that can be used for basic two-way text-based network connections.
	 * @param observer A non-null object that implements the SimpleNetObserver interface.  When
	 * certain network events occur, the SimpleNet object will notify the observer by calling
	 * one of the methods defined in the interface.
	 * @throws IllegalArgumentException if observer is null
	 */
	public SimpleNet(SimpleNetObserver observer) {
		if (observer == null)
			throw new IllegalArgumentException("A SimpleNet object requires a non-null SimpleNetObserver");
		owner = observer;
		state = IDLE;
	}
	
	/**
	 * Open a connection in server mode.  The SimpleNet object will wait for an incoming connection
	 * request.  This method returns immediately, without waiting for the connection to open.  The
	 * observer will be notified when the connection opens by calling its connectionOpened() method.
	 * (Or, if an error occurs, its connectionClosedWithError() method will be called instead.)
	 * @param portNumber the port number on which the server will listen.  A network server must
	 * have a port number in the range 1 to 65535.  Numbers less than 1024 are reserved for system
	 * use, and attempting to use one will produce an error.  It is also an error to try to use
	 * a port number that is already being used by another server.
	 * @throws IllegalStateException if this SimpleNet object is already connected or opening a connection
	 */
	synchronized public void listen(int portNumber) {
		if (state != IDLE)
			throw new IllegalStateException("Attempt to open a connection while not in idle state.");
		state = WAITING_FOR_CONNECTION;
		connectionHandler = new ConnectionHandler(portNumber);
		connectionHandler.start();
	}
	
	/**
	 * Open a connection in client mode.  The SimpleNet object will appempt to connect to a server
	 * that is listening on a specified computer and at a specified port number.  This method returns
	 * immediately, without waiting for the connection to open.  The observer will be notified
	 * when the connection opens by calling its connectionOpened() method.  (Or, if an error occurs,
	 * its connectionClosedWithError() method will be called instead.)
	 * @param hostNameOrIP  the host name (such as "math.hws.edu") or IP address (such as "172.30.10.23")
	 * of the computer when the server is listeneing
	 * @param portNumber the port number where the server is listening
	 * @throws IllegalStateException if this SimpleNet object is already connected or opening a connection
	 */
	synchronized public void connect(String hostNameOrIP, int portNumber) {
		if (state != IDLE)
			throw new IllegalStateException("Attempt to open a connection while not in idle state.");
		state = CONNECTING;
		connectionHandler = new ConnectionHandler(hostNameOrIP,portNumber);
		connectionHandler.start();
	}
	
	/**
	 * Closes the current connection, if any.  This method returns immediately.
	 * The observer will be notified when the connection closes by calling its
	 * connectionClosed() method.  Note that this method can be called while
	 * the connection is still in the process of being opened.  In that case,
	 * the connection attempt will be aborted; the connectionClosed()
	 * method in the observer will still be called in this case.
	 */
	synchronized public void close() {
		if (state == IDLE)
			return;  // ignore close command if there is no connection
		state = CLOSING;
		connectionHandler.abort();
	}
	
	/**
	 * Transmit a message to the other side of the connection.  Attempts to
	 * transmit data when no connection is opened are simply ignored.
	 * @param message the text to be transmited
	 */
	synchronized public void send(String message) { 
		if (out == null || out.checkError() || state != CONNECTED)
			return;  
		out.println(message);
		out.flush();
		if (out.checkError())
			close();
	}
	
	/**
	 * Returns the current state of this SimpleNet object.  For the most part,
	 * the state information is not needed outside this class, but you can use
	 * this method to inquire the current state if you need it.
	 * @return  the current state, which is one of the contstants
	 * IDLE, CONNECTING, WAITING_FOR_CONNECTION, CONNECTED, or CLOSING, 
	 */
	synchronized public int getState() {
		return state;
	}
	

	//----------------------------------------------------------------------------
	// The remainder of this file is the private implementation part of the class.
	//----------------------------------------------------------------------------

	synchronized private void dataReceived(ConnectionHandler handler, String data) {
		if (state == IDLE || state == CLOSING || handler != connectionHandler)
			return; // ignore possible input from old connection
		owner.connectionDataReceived(this,data);
	}
	
	synchronized void opened(ConnectionHandler handler) {
		if (handler != connectionHandler || (state != WAITING_FOR_CONNECTION && state != CONNECTING))
			return;
		out = connectionHandler.getOutputStream();
		state = CONNECTED;
		owner.connectionOpened(this);
	}
	
	synchronized void closed(ConnectionHandler handler) {
		if (state == IDLE || handler != connectionHandler)
			return;
		if (state == CLOSING)
			owner.connectionClosed(this);
		else
			owner.connectionClosedByPeer(this);
		connectionHandler = null;
		out = null;
		state = IDLE;
	}
	
	synchronized private void error(ConnectionHandler handler, String message, Exception e) {
		if (state == IDLE || connectionHandler != handler)
			return;  // Ignore any left-over error from old connections.
		out = null;
		connectionHandler = null;
		if (state == CLOSING) { // don't send error since owner wants to close anyway
			state = IDLE;
			owner.connectionClosed(this);
		}
		else {
			state = IDLE;
			owner.connectionClosedWithError(this,message + ":  " + e.toString());
		}
		// e.printStackTrace();
	}
	
	private class ConnectionHandler extends Thread {
		
		private int port;
		private String host;
		private boolean runAsServer;

		private volatile Socket connection;
		private volatile ServerSocket listener;
		private volatile boolean aborted;
		private volatile Thread connectionOpener;
		private volatile Exception exceptionWhileConnecting;
		private volatile PrintWriter out;
		private BufferedReader in;	
		
		ConnectionHandler(int portNumber) {
			port = portNumber;
			runAsServer = true;
		}
		
		ConnectionHandler(String hostName, int portNumber) {
			host = hostName;
			port = portNumber;
			runAsServer = false;
		}
		
		void abort() {
			try {
				if (listener != null)
					listener.close();
				else if (connectionOpener != null)
					this.interrupt();
				else if (connection != null) {
					connection.shutdownInput();
					connection.shutdownOutput();
					connection.close();
				}
			}
			catch (Exception e) {
			}
			aborted = true;
		}
		
		PrintWriter getOutputStream() {
			return out;
		}

		public void run() {
			BufferedReader in;
			
			if (!runAsServer) {
				try {
					connectionOpener = new Thread() {
						public void run() {
							try {
								try {
									connection = new Socket(host,port);
								}
								catch (Exception e) {
									exceptionWhileConnecting = e;
								}
								finally {
									synchronized(this) {
										notify();
									}
								}
							}
							catch (Exception e) {
								connection = null;
							}
						}
					};
					connectionOpener.start();
					synchronized(connectionOpener) {
						try {	
							connectionOpener.wait();
						}
						catch (InterruptedException e) { 
							closed(this);
							return;
						}
					}
					connectionOpener = null;
					if (exceptionWhileConnecting != null)
						throw exceptionWhileConnecting;
				}
				catch (Exception e) {
					error(this,"Error while attempting to connect to " + host, e);
					return;
				}
			}
			else {
				try {
					listener = new ServerSocket(port); 
					connection = listener.accept();
					listener.close();
					listener = null;
				}
				catch (Exception e) {
					error(this,"Error while waiting for connection request", e);
					return;
				}
			}
			
			if (aborted)
				return;
						
			try {
				in = new BufferedReader(new InputStreamReader( connection.getInputStream() ));
				out = new PrintWriter(connection.getOutputStream());
			}
			catch (Exception e) {
				error(this,"Error while creating network input/ouput streams", e);
				return;
			}

			if (aborted)
				return;

			opened(this);
			
			try {
				while (true) { 
					String input = in.readLine();
					if (input == null || aborted)
						break;
					dataReceived(this,input);
				}
			}
			catch (Exception e) {
				error(this,"An error occured while connected", e);
			}
			finally {
				closed(this); 
				if (connection != null) {
					try {
						connection.close();  // Make sure connection is properly closed.
					}
					catch (Exception e) {
					}
				}
			}

		} // end run()
		
	} // end nested class ConnectionHandler
	
}
