package server;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;

/**
* An HTTP server that lets clients chat from a web browser.
*
*
@author akishore
*
*/
public class ApplicationServer extends NanoHTTPD {

      
// Constants

      
private static final int DEFAULT_PORT = 4000;
      
private static final String FILE_PATH = "src/client/";
      
private static final String HTML_FILE = "index.html";
      
private static final String CID_REGEXP = "\\/\\*CLIENT_ID\\*\\/";
       private static final String[] FILES = {
              
"constants.js", "controller.js", "model.js", "prototype.js",
              
"scrollpane.js", "style.css", "view.js"
      
};

       private static final String BASE_URI = "/";
      
private static final String REFRESH_URI = "/refresh";
      
private static final String SEND_URI = "/send";

      
private static final String ID_ARG = "id";
      
private static final String MSG_ARG = "msg";

      
// Utilities

      
private static final Random random = new Random();

      
// Fields

      
private final int port;

      
/**
        * Maps client ID to a queue of messages to be sent *to* that client.
        * So when the client refreshes, these messages are sent to him/her.
        * The ConcurrentMap interface provides additional convenience methods,
        * e.g. putIfAbsent(), to handle concurrency even better.
        */
      
private final ConcurrentMap<String, Queue<String>> messages;

      
// ...any additional fields you need

       // Constructors

      
public ApplicationServer() throws IOException {
              
this(DEFAULT_PORT);
      
}

      
public ApplicationServer(int port) throws IOException {
              
super(port);
              
this.port = port;

              
// important: using a ConcurrentHashMap because multiple threads
               // (from concurrent HTTP sessions) may be accessing map
              
this.messages = new ConcurrentHashMap<String, Queue<String>>();

              
// if the server needs to be configured somehow...
              
configure();
      
}

      
// Public methods

      
public int getPort() {
              
return port;
      
}

      
/**
        * This is the sole NanoHTTPD method we need to override. Given a URI
        * and parameters (key-value pairs), we'll return the appropriate HTTP
        * response.
        */
      
@Override
      
public Response serve(String uri, Map<String, String> params) {
               assert uri != null;
              
assert params != null;

              
// serve the response based on the uri
              
if (uri.equals(BASE_URI)) {
                      
return serveBase(params);
              
} else if (uri.equals(REFRESH_URI)) {
                      
return serveRefresh(params);
              
} else if (uri.equals(SEND_URI)) {
                      
return serveSend(params);
              
} else if (isKnownFile(uri.substring(1))) { // strip leading slash
                      
return serveFile(uri);
              
}

              
System.err.println("Received bad uri: " + uri);
              
return badResponse();
      
}

      
// Serve methods

      
/**
        * Generates a random ID for this client and serves the index.html
        * page to the client.
        */
      
private Response serveBase(Map<String, String> params) {
               // parameters are ignored
              
try {
                      
// try to load the HTML page, may throw an IOException
                      
String html = loadFile(FILE_PATH + HTML_FILE);

                      
// make a random ID for this user
                      
int id = random.nextInt(Integer.MAX_VALUE);
                       String strId = Integer.toString
(id);

                       System.out.println
("New user! " + strId);

                      
// make a queue for this user to receive messages.
                       // important: using a ConcurrentLinkedQueue in case multiple
                       // threads try to get/put messages at the same time.
                      
messages.putIfAbsent(strId, new ConcurrentLinkedQueue<String>());

                      
// set the HTML page to have this ID, and return it
                      
html = html.replaceAll(CID_REGEXP + ".*" + CID_REGEXP, strId);
                      
return goodResponse(html);
              
} catch (IOException e) {
                      
System.err.println("The HTML page failed to load:");
                       e.printStackTrace
(System.err);

                      
return badResponse();
              
}
       }

      
/**
        * Given an ID argument in the parameters, serves the messages that
        * have been queued for the client with the given ID. The messages are
        * separated by newlines (\n). If the client has no queued messages,
        * serves the empty string.
        */
      
private Response serveRefresh(Map<String, String> params) {
              
try {
                      
checkParam(params, ID_ARG);
              
} catch (IllegalArgumentException e) {
                      
System.err.println("Bad refresh request: " + e.getMessage());
                      
return badResponse();
              
}

              
return goodResponse(fetchMessages(params.get(ID_ARG)));
      
}

      
/**
        * Given an ID argument and a message argument in the parameters, sends
        * the given message appropriately. Always serves an empty string.
        */
      
private Response serveSend(Map<String, String> params) {
              
try {
                      
checkParam(params, ID_ARG);
                       checkParam
(params, MSG_ARG);
              
} catch (IllegalArgumentException e) {
                      
System.err.println("Bad send request: " + e.getMessage());
                      
return badResponse();
              
}

              
sendMessage(params.get(ID_ARG), params.get(MSG_ARG));
              
return goodResponse();
      
}

      
/**
        * Serves the file at the given URI, which represents a filename. The
        * filename must have an extension (i.e. must have a dot).
        */
      
private Response serveFile(String uri) {
               assert uri.contains("."); // must be a file with an extension

               // figure out the MIME type based on the file extension
              
String mime = "text/plain"; // default, if unknown extension
              
String ext = uri.substring(uri.lastIndexOf('.'));

              
if (ext.equals(".js")) {
                      
mime = "text/javascript";
              
} else if (ext.equals(".css")) {
                      
mime = "text/css";
              
}

              
try {
                      
return new Response(HTTP_OK, mime, loadFile(FILE_PATH + uri));
              
} catch (IOException e) {
                      
System.err.println("Unknown file to serve: " + uri);
                      
return badResponse();
              
}
       }

      
// Helper methods

      
/**
        * Returns the contents of the given file, or throws an IOException if
        * the file could not be read for any reason. Note that the filename
        * should be relative to the runtime classpath (in Eclipse, this means
        * the project directory), not to this class's file path.
        */
      
private static String loadFile(String filename) throws IOException {
              
BufferedReader file = new BufferedReader(new FileReader(filename));

               StringBuilder text =
new StringBuilder();
               String line = file.readLine
();

              
while (line != null) {
                      
text.append(line);
                       text.append
("\n");
                       line = file.readLine
();
              
}

              
return text.toString();
      
}

      
/**
        * Returns the messages currently queued for the client with the given
        * ID, and removes those messages from the queue. If the ID is not
        * found, or if there are no messages queued for this client, returns
        * an empty string. The messages are separated by a newline (\n).
        */
      
private String fetchMessages(String id) {
              
// using a StringBuilder is much faster than repeatedly using the
               // string concatenation (+) operator
              
StringBuilder message = new StringBuilder();
               Queue<String> queue = messages.get
(id);

              
if (queue != null) {
                      
while (!queue.isEmpty()) {
                              
message.append(queue.poll()); // poll() removes the message too
                              
message.append("\n");
                      
}
               }

              
return message.toString();
      
}

      
/**
        * Sends the given message from the client with the given ID to every
        * other client on the server.
        */
      
private void sendMessage(String id, String message) {
              
// add the message to every queue in our map
              
for (Queue<String> queue : messages.values()) {
                      
queue.add(id + ": " + message);
              
}

              
// TODO Task 2: Modify this method so that sending a message no
               // longer broadcasts it to every other client, and instead
               // broadcasts Friendly's response to only this client. Make sure
               // to still broadcast the original message to the client who sent
               // it, so that they may see their own message.
      
}

      
/**
        * Configures the application server on startup.
        */
      
private void configure() {
              
// TODO Task 3: Modify this method to initialize your Friendly chat
               // server with the responses specified in your XML file. You might
               // like to use the XmlConfiguration stub, shown below.

//              try {
//                      // it can be better to load files via class resources instead
//                      // of directly; just note that the file name is then relative
//                      // to this class's Java file, not the project folder.
//
//                      // ... = new XmlConfiguration(
//                      //              ApplicationServer.class.getResourceAsStream(/*XML_FILE*/));
//                      // ...
//
//              } catch (ParserConfigurationException e) {
//                      System.err.println("Native parser error:");
//                      e.printStackTrace(System.err);
//              } catch (SAXException e) {
//                      System.err.println("XML file failed to parse:");
//                      e.printStackTrace(System.err);
//              } catch (IOException e) {
//                      System.err.println("XML file not found:");
//                      e.printStackTrace(System.err);
//              }
      
}

      
private void checkParam(Map<String, String> params, String param) {
              
if (!params.containsKey(param)) {
                      
throw new IllegalArgumentException("missing param: " + param);
              
} else if (params.get(param).equals("")) {
                      
throw new IllegalArgumentException("missing value for " + param);
              
}
       }

      
/**
        * Returns true iff the given filename represents a known server file.
        */
      
private boolean isKnownFile(String filename) {
               for (String known : FILES) {
                      
if (known.equals(filename)) {
                              
return true;
                      
}
               }

              
return false;
      
}

      
// Main method

      
/**
        * Attempts to run the application server, either on the port given as
        * the first argument, or the default port. The server will run until
        * you press enter or until you forcefully terminate the program.
        */
      
public static void main(String[] args) throws IOException {
              
ApplicationServer server = null;

              
if (args.length >= 1) {
                      
// try to see if first argument is a valid port number
                      
try {
                              
server = new ApplicationServer(Integer.parseInt(args[0]));
                      
} catch (NumberFormatException e) {
                              
System.err.println("Invalid port number: " + args[0]);
                      
} catch (IOException e) {
                              
System.err.println("Could not listen on port " + args[0]);
                      
}
               }
else {
                      
// no arguments given, use default port
                      
try {
                              
server = new ApplicationServer();
                      
} catch (IOException e) {
                              
System.err.println("Could not listen on default port");
                      
}
               }

              
if (server != null) {
                      
System.out.println("Server running on port " + server.getPort() + "...");
                       System.out.println
("Press enter to end.");
                       System.in.read
(); // may throw an IOException here if user quits
              
}
       }

}