/**
 * Copyright 2007 Ben Bucksch
 * BSD license
 */
package org.bucksch.httpserver;
import java.io.*;
import java.net.*;
import java.util.*;

import org.bucksch.zeipis.all.Util;

public class HTTPServer
{
  public HTTPServer(int port)
    throws IOException
  {
    myListeningSocket = new ServerSocket(port);
    myHandlers = Util.newMap();
  }
  // Start listening for requests, in a new thread
  public void startInThread()
  {
    new Thread("HTTPServer: listening on port " + myListeningSocket.getLocalPort()) {
      public void run() {
        loop();
      }
    }.start();
  }
  // Blocks infinitely
  public void loop()
  {
    while(true)
    {
      try {
        final Socket clientSocket = myListeningSocket.accept(); // blocks until there's a request
        new Thread("HTTPServer: request from " + clientSocket.getRemoteSocketAddress()) {
          public void run() {
            request(clientSocket);
          };
        }.start();
      } catch (Exception e) {
        System.err.println(e);
        e.printStackTrace();
      }
    }
  }
  // New request from a client.
  // Called by loop(). Runs in a new thread.
  public void request(Socket clientSocket)
  {
    try {
      clientSocket.getInetAddress().getHostName();
      debug("New HTTP request from " + clientSocket.getRemoteSocketAddress());
      InputStream in = clientSocket.getInputStream();
      OutputStream out = clientSocket.getOutputStream();
      BufferedReader headerReader = new BufferedReader(new InputStreamReader(in));

      // Parse first "HTTP/1.0 GET http://www.example.com/foo" line
      String headerLine = headerReader.readLine();
      debug("first line: " + headerLine);
      if (headerLine.startsWith("HTTP/1.0 ") || headerLine.startsWith("HTTP/1.1 "))
        headerLine = headerLine.substring(9);
      if (headerLine.startsWith("HTTP "))
        headerLine = headerLine.substring(5);
      if (headerLine.endsWith(" HTTP/1.0") || headerLine.endsWith(" HTTP/1.1"))
        headerLine = headerLine.substring(0, headerLine.length() - 9);
      int urlPos = headerLine.indexOf(" ");
      if (urlPos == -1)
        throw new HTTPException(400, "HTTP request malformed", headerLine);
      urlPos++;
      String method = headerLine.substring(0, urlPos - 1);
      String urlStr = headerLine.substring(urlPos);
      debug("Method: " + method + ", url: <" + urlStr + ">");
      URL baseURL = new URL("http://" + clientSocket.getLocalAddress() + ":" + clientSocket.getLocalPort() + "/");
      URL url = new URL(baseURL, urlStr);
      String urlPath = url.getPath().substring(1);
      debug("full url: <" + url + ">, path: " + url.getPath());

      // Parse headers "Foo: Bar"
      Map<String, String> headers = Util.newMap();
      while ( (headerLine = headerReader.readLine().trim()) .length() > 0)
      {
        debug("header line: " + headerLine);
        int colonPos = headerLine.indexOf(": ");
        if (colonPos == -1)
          continue; // workaround for <http://xbmc.org/trac/ticket/7277>
          //throw new HTTPException(400, "Malformed header", headerLine);
        String headerName = headerLine.substring(0, colonPos);
        String headerValue = headerLine.substring(colonPos + 2);
        if (headerName.length() == 0)
          throw new HTTPException(400, "Malformed header", headerLine);
        headers.put(headerName, headerValue);
      }
      debug("Parsing headers done");

      HTTPRequest request = new HTTPRequest(method, url, headers, clientSocket.getInetAddress(), in, out);

      // Dispatch to handler
      HTTPHandler handler = null;
      String handlerPrefix = null;
      for (String prefix : myHandlers.keySet())
      {
        if (urlPath.startsWith(prefix))
        {
          if (handler != null && prefix.length() < handlerPrefix.length()) // find most specific URL prefix / handler
            continue;
          handler = myHandlers.get(prefix);
          handlerPrefix = prefix;
        }
      }
      if (handler != null)
      {
        request.setPathPrefix(handlerPrefix);
        request.setPathRest(urlPath.substring(handlerPrefix.length()));
        debug("Using handler " + handler + " for " + request.getURLPrefix() + ", with url rest " + request.getURLRest());
        handler.handle(request);
      }
      else
        throw new HTTPException(404, "This URL is unknown", url.toString());
    } catch (Exception e) {
      //System.err.println("When processing request from " + clientSocket.getRemoteSocketAddress() + ":");
      e.printStackTrace();
      try {
        sendError(e, clientSocket.getOutputStream());
      } catch (IOException e1) {
        System.err.println(e1);
        e1.printStackTrace();
      }
    }
  }
  protected void sendError(Exception e, OutputStream out)
    throws IOException
  {
    BufferedWriter response = new BufferedWriter(new OutputStreamWriter(out));
    int responseCode = 400; // TODO correct? See RFC 2616
    if (e instanceof HTTPException)
      responseCode = ((HTTPException) e).errorCode();
    response.write("HTTP/1.0 " + responseCode + " " + e.getMessage() + newline);
    response.write("Error: " + Util.getFullExceptionMessage(e) + newline);
    //response.write("Debug-Info: " + e.toString() + newline);
    if (e instanceof HTTPException && ((HTTPException) e).badData() != null)
      response.write("Bad-Data: " + ((HTTPException) e).badData());
    response.write(newline);
    response.close();
  }

  public void registerHandler(HTTPHandler handler, String urlPrefix)
  {
    if (myHandlers.containsKey(urlPrefix))
      throw new RuntimeException(urlPrefix + " already registered");
    myHandlers.put(urlPrefix, handler);
    debug("Registering handler for " + urlPrefix + ": " + handler);
  }
  public void unregisterHandler(String urlPrefix)
  {
    myHandlers.remove(urlPrefix);
    debug("Unregistering handler for " + urlPrefix);
  }
  /**
   * Please use unregisterHandler(String urlPrefix) instead,
   * unless you registered several times and want to remove all. 
   */
  public void unregisterHandlerForAllPrefixes(HTTPHandler handler)
  {
    for (String urlPrefix : myHandlers.keySet())
      if (myHandlers.get(urlPrefix) == handler)
        unregisterHandler(urlPrefix);
  }
  
  public void debug(String text)
  {
    System.out.println(text);
  }

  // Data
  protected ServerSocket myListeningSocket;
  protected Map<String, HTTPHandler> myHandlers;
  
  static public String newline = "\r\n";
}
