/*
	David Tate

	java BeaconServer serverPort keyAddress keyPort
	
	A server that manages BeaconClients, allowing
		them to see each other.
	The server recieves two types of messages:
		LOGIN and LOGOUT.  It spawns a new thread
		each time it recieves a request. 
	It detects a crashed client everytime it sends
		any message out because in trying to connect
		it will detect that the BeaconClient is 
		no longer alive, and will do a LOGOUT 
		operation on it.
*/

import java.net.*;
import java.io.*;
import java.util.*;
import java.text.*;

public class BeaconServer {
	private final int INIT_NUM_USERS = 50;
	private final static int DEFAULT_PORT = 5000;

	private Vector userList;	//list of all online users
	private int port;				//port we are listening on

	private String keyAddress;
	private int keyPort;
	
	BeaconServer (int port, String keyAddress, int keyPort) {
		this.port = port;
		this.keyAddress = keyAddress;
		this.keyPort = keyPort;

		userList = new Vector( INIT_NUM_USERS );

		System.out.println ("BeaconServer listening from port: " + this.port);
		System.out.println("");

		ServerSocket server = null;
		try {
			server = new ServerSocket (this.port);	
			Thread t;
			while ( true )  {
				Socket s = server.accept();	//block here until client calls

				try {
					t = new MessageHandler(s);	
				}
				catch (InvalidRequestException ire) {
					System.out.println("caught invalid request, ignoring");
					continue;
				}

				t.start();
			}
		}
		catch (BindException e) {
			System.out.println("Could not start server because the port " + this.port + "is occupied");
			System.out.println(e);
			e.printStackTrace();
			System.exit(1);
		}
		catch (IOException e) {
			System.out.println(e);	
			e.printStackTrace();
			System.exit(1);
		}
		finally {
				try {
					if (server != null) server.close();
				}
				catch (IOException ie) { /* already closed, so ignore */ }
		}
	}

	class MessageHandler extends Thread {
		private Socket conn;				//socket the client issued the request from
		private ObjectInputStream in; //the streams of this request
		private ObjectOutputStream out;

		MessageHandler (Socket s) throws InvalidRequestException {
			this.conn = s;	
			try {
				out = new ObjectOutputStream (conn.getOutputStream());
				out.flush();
				in = new ObjectInputStream (conn.getInputStream());
			}
			catch (IOException e) {
				e.printStackTrace();
				//bad request so we throw exception instead of bringing entire server down
				throw (new InvalidRequestException ("Couldn't get streams from client request.  Ignoring this request") );
			}
		}

		public void run() {
			try {
				Object o = in.readObject();
				BeaconMessage msg = (BeaconMessage) o;
		
				DateFormat fmt = DateFormat.getDateTimeInstance (DateFormat.SHORT, DateFormat.SHORT);	
				Date now = new Date();	
		
				//we print out each message recieved here, could log to a file, etc.
				System.out.println ( fmt.format(now) + " => " + msg.toString() );

				int type = msg.getType();
				switch (type) {
					case BeaconMessage.LOGIN:
						handleLogin (msg);
						break;
					case BeaconMessage.LOGOUT:
						handleLogout (msg);
						break;
				}
			}			
			catch (OptionalDataException ode) {
				System.out.println(ode);
				ode.printStackTrace();
         }
         catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				cnfe.printStackTrace();
         }
         catch (IOException ioe) {
				System.out.println(ioe);
				ioe.printStackTrace();
         }
			finally {
				try {
					if (conn != null) conn.close();
				}
				catch (IOException e) {/* already closed, so ignore */}
			}
		}

		private void handleLogin(BeaconMessage msg) {
			//login protocol: 4 parts

			try {
				//send keyserver information
				Object[] args = new Object[2];
				args[0] = keyAddress;
				args[1] = new Integer(keyPort);

				out.writeObject(new BeaconMessage(
					BeaconMessage.KEYSERVER_INFO, 
					args, 
					msg.getName(), 
					msg.getAddress(), 
					msg.getPort()));	
	
				//send list of online users to new user
				synchronized (userList) {
					Iterator iter = userList.iterator();
					while ( iter.hasNext() ) {
						BeaconState user = (BeaconState) iter.next();		
			
						out.writeObject(new BeaconMessage(
							BeaconMessage.USER, 
							user.getName(), 
							user.getAddress(), 
							user.getPort()));	
						out.flush();
					}
				}

				//end login procedure
				out.writeObject(new BeaconMessage(
					BeaconMessage.END_LOGIN, 
					msg.getName(), 
					msg.getAddress(), 
					msg.getPort()));

				out.flush();

				//send all users a message saying new user has joined
				sendToAll(new BeaconMessage(
					BeaconMessage.LOGIN, 
					msg.getName(), 
					msg.getAddress(), 
					msg.getPort()));

				//add user to internal list of online users so they recieve future messages
				BeaconState state = new BeaconState (msg.getName(), msg.getAddress(), msg.getPort());
				userList.addElement(state);
			}
			catch (IOException e) {
				System.out.println(e);
				e.printStackTrace();
			}
		}

		private void handleLogout(BeaconMessage msg) {
			BeaconState leftUser = new BeaconState(msg.getName(), msg.getAddress(), msg.getPort());
			userList.removeElement(leftUser);
			sendToAll(new BeaconMessage(
				BeaconMessage.LOGOUT, 
				msg.getName(), 
				msg.getAddress(), 
				msg.getPort()));
		}

		public void sendToAll (BeaconMessage message) {
			//for each online user connect and send message to them	
			synchronized (userList) {
				BeaconState user = null;
				try {
					Iterator iter = userList.iterator();	//snapshot
					while (iter.hasNext()) {
						user = (BeaconState) iter.next();		
						Socket s = new Socket(user.getAddress(), user.getPort());
						ObjectOutputStream outUser = new ObjectOutputStream (s.getOutputStream());
						out.flush();
						outUser.writeObject(message);	
						outUser.flush();
					}
				}
				catch (SocketException se) {
					/* If we are here then we cannot connect to the user and it has crashed */
					handleLogout(new BeaconMessage(
						BeaconMessage.LOGOUT, 
						user.getName(), 
						user.getAddress(), 
						user.getPort()));
				}
				catch (IOException e) {
					System.out.println(e);
					e.printStackTrace();
					System.out.println ("Failed to send message to " + user);
				}
			}				
		} //end sendToAll
	}	//end MessageHandler

	public static void main (String[] args) {
		int port = DEFAULT_PORT;
		String keyAddress = null;
		int keyPort = -1;

		try {
			port = Integer.parseInt(args[0]);
			keyAddress = args[1];
			keyPort = Integer.parseInt(args[2]);
		}
		catch (Exception e) {
			System.out.println("Usage: java BeaconServer serverPort keyAddress keyPort");
			System.exit(1);
		}

		BeaconServer server = new BeaconServer (port, keyAddress, keyPort);
	}
}

