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

/**
 * A BuddyChatServer keeps a list of available BuddyChat users and makes that list
 * available to each user that has registered itself on the list.  The list is a list
 * of users who are willing to "chat" with other users.  Each user provides a port
 * on which it can be contacted; an ip address for the user is taken from the user's
 * connection to the server.  The user also specifies a "handle", which is the name
 * under which the user will appear in the buddy lists.  The server creates a "secret" 
 * for each user, which is a password that must be sent by other users who wish to 
 * connect to that user.  When the user connects to the server, the server sends a
 * list of all users that are connected to the server.  Whenever a user is added to
 * or deleted from the list, that change is sent to all connected users so that
 * they can keep their buddy lists updated.
 * <p>The protocol for establishing a connection from a client to the server is:
 * (1) server sends string "BuddyChatServer"; (2) client sends string "BuddyChatClient";
 * (3) client sends its handle; (4) client sends its port; (5) server creates a
 * secret for the client and sends it to the client.  All messages are terminated
 * by line feeds.
 * <p>Once a connection is established, the server sends a list of connected users
 * to the client.  (This list does not inlude the client itself.)  The format for
 * a list of users is (1) the word "clients" on a line by itself; (2) one line of infor
 * for each client; (3) the word "endclients" on a line by itself.  The client
 * info consits of  pieces of information: the client's handle, ip address, port
 * number, and secret.  These are separated by "~" characters.  There are no spaces
 * or "~" characters in the info (the user's handle is modified to make this true,
 * if necessary).
 * <p>The connection between server and client remains open until the client
 * closes it (or the server goes down).  The client can send the following two
 * messages to the server: (1) "ping" (and the server responds by sending 
 * "pingresponse" -- this is a way of checking that the connection is still in
 * place); (2) "refresh" (and the server responds by sending the complete list
 * of connected clients, omitting the client who sent the "refresh" command).
 * The server sends the following additional messages, with no response expected
 * from the client. (1) "ping"; (2) "addclient" followed on the next line by
 * the info string for the newly added client; (3) "removeclient" followed on
 * the next line by the info string of the client who has been removed.
 * <p>In addition to this, the server can be shut down gracefully by connecting
 * to the server, receciving the message "BuddyChatServer", sending "BuddyChatClient",
 * then sending the shutdown string as a message.  The shutdown string is a
 * random-looking string that is not likely to be used as a handle.  It is stored,
 * if possible, in a file named ".BuddyChatServer_shutdown_string_port" in the home
 * directory of the user who runs the server, where "port" is replaced by the
 * port number on which the server is listening. If it is not possilbe to save the
 * shutdown string, a default string is used.  The file is deleted when the server
 * shuts down.
 * <p>The server listens for client connections on a default port number, but a
 * different listening port can be specified on the command line.
 */
public class BuddyChatServer {
   
   private static final int DEFAULT_PORT = 12001;
   private static final String DEFAULT_SHUTDOWN_MESSAGE ="skRl@(Gjfd908.89d&*hgfd";
   
   private static String shutdownString;  // The actual shutdown string.

   private static int listeningPort;
   private static ServerSocket listener;  
   
   private static ClientList clients; // Connected clients; ClientList is a nested class.
   
   private volatile static boolean isShutDown;
   
   
   /**
    * Main routine starts a listener and listens for connections until the
    * server is shut down or an error occurs.
    * @param args an alternative listening port can be specified on the command line.
    */
   public static void main(String[] args) {
      listeningPort = DEFAULT_PORT;
      if (args.length > 0) {
         try {
             int p = Integer.parseInt(args[0]);
             if (p <= 0 || p > 65535)
                throw new NumberFormatException();
             listeningPort = p;
         }
         catch (NumberFormatException e) {
         }
      }
      try {
         listener = new ServerSocket(listeningPort);
      }
      catch (Exception e) {
         System.out.println("Can't create listening socket on port " + listeningPort);
         System.exit(1);
      }
      System.out.println("Listening on port " + listeningPort);
      clients = new ClientList();
      getShutdownString();
      try {
         while (true) { // Listen until error occurs or socket is closed.
            Socket socket = listener.accept();
            clients.add( socket );
         }
      }
      catch (Throwable e) {
         if (!isShutDown) { // Don't report an error after normal shutdown.
            System.out.println("Server closed with error:");
            System.out.println(e);
         }
      }
      finally {
         System.out.println("Shutting down.");
         clients.shutDown();
      }
   }
   
   /**
    * Tries to create a unique shutdown string and write it to a file,
    * so that it can be used by the BuddyChatServerShutdown program.
    */
   private static void getShutdownString() {
      File file = new File(System.getProperty("user.home"), 
            ".BuddyChatServer_shutdown_string_" + listeningPort);
      shutdownString = DEFAULT_SHUTDOWN_MESSAGE + Math.random();
      try {
         if (file.createNewFile()) {
            file.deleteOnExit();
            PrintWriter out = new PrintWriter(new FileWriter(file));
            out.println(shutdownString);
            out.close();
         }
         else {
            BufferedReader in = new BufferedReader(new FileReader(file));
            String line = in.readLine();
            if (line.startsWith(DEFAULT_SHUTDOWN_MESSAGE))
               shutdownString = line;
         }
      }
      catch (Exception e) {
         shutdownString = DEFAULT_SHUTDOWN_MESSAGE;
      }
   }
   

   /**
    * Utility routine for converting the ip address of an InetAddress 
    * to its usual string form.
    */
   private static String convertAddress(InetAddress ip) {
      byte[] bytes = ip.getAddress();
      if (bytes.length == 4) {
         String addr = "" + ( (int)bytes[0] & 0xFF );
         for (int i = 1; i < 4; i++)
            addr += "." + ( (int)bytes[i] & 0xFF );
         return addr;
      }
      else if (bytes.length == 16) {
         String[] hex = new String[16];
         for (int i = 0; i < 16; i++)
            hex[i] = Integer.toHexString( (int)bytes[i] & 0xFF );
         String addr = "" + hex[0] + hex[1];
         for (int i = 2; i < 16; i += 2)
            addr += ":" + hex[i] + hex[i+1];
         return addr;
      }
      else
         throw new IllegalArgumentException("Unknown IP address type");
   }
      

   /**
    * A list of all the currently connected clients, with some routines for
    * adding and removing clients.  The routines are synchronized, since
    * they can be called from multiple client threads.
    */
   private static class ClientList {
      
      ArrayList<Client> clientList = new ArrayList<Client>(); // The clients.
      
      /**
       * Add a new client, already connected through the socket.  No information
       * has been exchanged over the socket, so the identity of the client is
       * unknown.  When the client thread has obtained client info, it will
       * call the announceConnection() method.  Clients that are not yet fully
       * connected have c.info == null; such clients are not part of the client
       * lists that have been sent to other clients.
       */
      synchronized void add(Socket socket) {
         Client c = new Client(socket);
         System.out.println("Client " + c.clientNumber + " created.");
         clientList.add(c);
      }
      
      /**
       * Remove a client.  This is called by the client thread when the connection
       * to the client closes.  If the client was fully connected (client.info != null),
       * then a report is sent to other clients.  However, no messages are sent
       * if the server is shutting down.
       */
      synchronized void remove(Client client) {
         System.out.println("Client " + client.clientNumber + " removed.");
         if (!isShutDown && clientList.remove(client) && client.info != null) {
            for (Client c : clientList)
               c.clientRemoved(client);
         }
      }
      
      /**
       * This is called when info about a client has been obtained.  It sends
       * a report of the newly added client to all connected clients.
       */
      synchronized void announceConnection(Client newlyConnectedClient) {
         System.out.println("Client " + newlyConnectedClient.clientNumber + 
               " connection established with info " + newlyConnectedClient.info);
         for (Client c : clientList)
            c.clientAdded(newlyConnectedClient);
      }
      
      /**
       * Return a copy of the ArrayLisit of clients.  (The copy won't be
       * modified asynchronously like the original list can be.)
       */
      synchronized ArrayList<Client> copy() {
         ArrayList<Client> c = new ArrayList<Client>();
         for (Client client : clientList)
            c.add(client);
         return c;
      }
      
      /**
       * This is called by the main program to shut down connections to all
       * the clients when the server is shutting down. This will allow all
       * the client threads to die, so that the program can end.
       */
      synchronized void shutDown() {
         for (Client client : clientList)
            client.shutDown();
      }
      
   } // end nested class ClientList
   
   
   /**
    * Each connected client is represented by an object of type Client.
    * Each client has a connected socket and TWO threads, one for reading from 
    * the socket and one for writing to it.  The writing thread is the
    * main thread, which is also responsible for setting up the connection.
    */
   private static class Client {
      
      static int clientsCreated;  // How many clients have been created.
      int clientNumber;  // Clients are numbered 1, 2, ... as they are created.
      volatile String info; // Info string that contains all important info about
                            // this client, of the form handle~ip~port~secret;
                            // this is the info string that is sent to other clients.
                            // It is constructed by the main client thread.
      String messageOut = "";  // Message waiting to be sent by writer thread.
                               // Note that access to messageOut is synchronized on
                               // this Client object.  Whenever a message is added
                               // to this string, notify() is used to wake the writer
                               // thread so that it can send the message.  It is
                               // possible that several messages might be added to
                               // the string before it gets sent.
      ClientThread clientThread;  // Writer thread, also sets up connection.
      ReaderThread readerThread;
      String secret;
      Socket socket;
      volatile boolean connected;
      volatile boolean closed;
      
      /**
       * Construct a client; make its "secret" string, and create its main thread.
       */
      Client(Socket socket) {
         clientsCreated++;
         clientNumber = clientsCreated;
         secret = clientNumber + "!" + Math.random();
         this.socket = socket;
         clientThread = new ClientThread();
         clientThread.start();
      }
      
      /**
       * Called by the ClientList when a client is removed.
       * Sends an announce of the removed client to this client.
       */
      void clientRemoved(Client c) {
         if (c != this) {
            send("removeclient\n" + c.info + '\n');
         }
      }
      
      /**
       * Called by the ClientList when a new client (with its info string)
       * becomes available. Sends an announce of the new client to this client.
       */
      void clientAdded(Client c) {
         if (c != this) {
            send("addclient\n" + c.info + '\n');
         }
      }
      
      /**
       * Called by ClientList when the server is shutting down;
       * Closes this client's socket (which terminates the reader thread)
       * and wakes up the writer thread so it can terminate.
       */
      synchronized void shutDown() {
         if (! closed) {
            closed = true;
            try {
               socket.close();
            }
            catch (Exception e) {
            }
            synchronized(this) {
               notify();  // Notifies writer thread.
            }
         }
      }
      
      /**
       * Called when either the reader or writer thread shuts down.  This
       * can happen because of an error or because the connection is closed
       * from the other side.  (It will also be called during server shutdown.)
       * If connection is already closed, nothing is done.
       */
      synchronized void close() {
         if (!closed) {
            closed = true;
            try {
               socket.close();
            }
            catch (Exception e) {
            }
            notify();
            clients.remove(this);
         }
      }
      
      /**
       * Schedule a message to be sent by the writer thread.  This method does
       * NOT actually send the message, so it does not block.  (Note: NO line
       * feed is added to the message!)
       */
      synchronized void send(String message) {
         messageOut += message;  // Add message onto the waiting outgoing string.
         notify();  // Wake up writer thread so it can send the message.
      }
      
      /**
       * Schedule the client list to be sent by the writer thread.  This method
       * does not actually send the list, so it does not block.
       */
      synchronized void sendClientList() {
         ArrayList<Client> c = clients.copy();
         messageOut += "clients\n";
         for (Client client : c)
            if (client != this)
               messageOut += client.info + '\n'; // Add info to outgoing string.
         messageOut += "endclients\n";
         notify(); // Wake up writer thread so it can send the message.
      }
      
      /**
       * Defines the main client thread, which is responsible for setting up
       * the connection, starting the reader thread, and then writing all messages
       * to the client.
       */
      class ClientThread extends Thread {
         public void run() {
            try {
               String ip = convertAddress(socket.getInetAddress());
               PrintWriter out;
               BufferedReader in;
               out = new PrintWriter(socket.getOutputStream());
               in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
               out.println("BuddyChatServer");
               out.flush();
               if (out.checkError())
                  throw new Exception("Error while trying to send handshake to client");
               String handshake = in.readLine();
               if (! "BuddyChatClient".equals(handshake))
                  throw new Exception("Client did not properly identify itself.");
               String handle = in.readLine();
               if (handle.equals(shutdownString)) {
                  out.println("shutting down");
                  out.flush();
                  isShutDown = true;
                  listener.close();
                  return;
               }
               handle = handle.replaceAll("~","-");  // Make sure handle contains no "~"
               String portString = in.readLine();
               int port;
               try {
                  port = Integer.parseInt(portString);
               }
               catch (NumberFormatException e) {
                  throw new Exception("Did not receive port number from client.");
               }
               if (port <= 0 || port > 65535)
                  throw new Exception("Illegal port number received from client.");
               out.println(secret);
               out.flush();
               if (out.checkError())
                  throw new Exception("Error while sending initial data to client.");
               info = handle + "~" + ip + "~" + port + "~" + secret;
               info = info.replaceAll(" ","_");  // Make sure info contains no spaces.
               connected = true;
               clients.announceConnection(Client.this);  // Connection has been set up.
               readerThread = new ReaderThread(in);
               readerThread.start();
               sendClientList();  // First message will be the client list.
               while (!closed && !isShutDown) {
                  String messageToSend;
                  synchronized(Client.this) { // Get the message; don't synchronize the actual send.
                     messageToSend = messageOut;
                     messageOut = "";
                  }
                  if (closed || isShutDown)
                     break;
                  if (messageToSend.length() == 0)
                     messageToSend = "ping\n"; // if there is no message, send a "ping"
                  out.print(messageToSend);
                  out.flush();
                  if (out.checkError())
                     throw new Exception("Error while sending to client.");
                  synchronized(Client.this) {
                     if (!closed && !isShutDown && messageOut.length() == 0) {
                        try { // sleep for about 10 minutes or until notified of a new message.
                           Client.this.wait(10*(50+(int)(15*Math.random()))*1000);
                        }
                        catch (InterruptedException e) {
                        }
                     }
                  }
               }
            }
            catch (Exception e) {
               if (!closed && ! isShutDown)
                  System.out.println("Client " + clientNumber + " error: " + e);
            }
            finally {
               close();
            }
         }
      }
      
      /**
       * Defines a relatively simple thread that reads messages from the client
       * and responds to them.  This will end when the client closes the connection,
       * or when the socket is closed on this side (for example, when server is shutting
       * down).
       */
      class ReaderThread extends Thread {
         BufferedReader in;
         ReaderThread(BufferedReader in) {
            this.in = in;
         }
         public void run() {
            try {
               while (true) {
                  String messageIn = in.readLine();
                  if (messageIn == null)
                     break;  // connection closed from other side
                  else if (messageIn.equals("ping"))
                     send("pingresponse\n");
                  else if (messageIn.equals("refresh"))
                     sendClientList();
                  else
                     throw new Exception("Illegal data received from client");
               }
            }
            catch (Exception e) {
               if (!closed && !isShutDown)
                  System.out.println("Client " + clientNumber + " error: " + e);
            }
            finally {
               close();
            }
         }
      } 
      
   } // end nested class Client
   

}
