package org.wikiwebserver.handler.http;

import java.io.*;
import java.net.*;
import java.text.ParseException;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;

import org.wikiwebserver.core.Privilege;
import org.wikiwebserver.core.SecurityMan;
import org.wikiwebserver.core.interfaces.ConnectionHandler;
import org.wikiwebserver.core.interfaces.HandlerConfiguration;

import org.wikiwebserver.handler.http.interfaces.HTTPConfig;

/**
 * This class handles HTTP connections between the WikiWebServer and
 * clients (web browsers).
 * 
 * Each connection is processed in a unique thread.  The run method
 * is responsible for serving each request.
 * Data sent from the client web browser is read from the socket using
 * the HTTPInputStream.  The object requested is then found and sent to the
 * client web browser using the HTTPOutputStream.
 * If an exception is thrown, it is repackaged as an HTTPException
 * and returned to the client web browser.
 * 
 * @author Michael Gardiner
 */
public class HTTPHandler implements ConnectionHandler {
    
    private static final boolean DEBUG = false;
    
    public static final int READ_TIMEOUT = 5000;   
    
    private volatile boolean running = true;    
    private volatile boolean lastRequest = false;    
    
    private Thread runner;
    private Socket socket; 
    
    private HTTPConfig httpConfig;
    
    private HTTPInputStream in;
    private HTTPOutputStream out;
    
    private String protocol = "http";    
    private String sourceAddress;    
    private String serviceHost = "localhost";
    private int servicePort = 8080;       
    
    // Reused objects for HTTP 1.1 persistent connections
    private HTTPRequest currentRequest;    
    private HTTPResponse currentResponse;
    
    
    public void configure(HandlerConfiguration config) {
        if (config instanceof HTTPConfig) {
            this.httpConfig = (HTTPConfig) config;
        }
        else throw new IllegalArgumentException("HTTPConfig required, " +
            config.getClass().getName() + " provided");
    }
    
    public InetAddress getInetAddress() {
        if (this.socket == null) return null;
        return this.socket.getInetAddress();
    }  
        
    public void handle(Socket socket) {
        this.socket = socket;         
        
        // This thread is used for handling the connection
        // We keep a handle on this thread so we can kill it if required
        runner = Thread.currentThread();
        
        // Name the thread to help with logging
        if (this.getHTTPConfig() != null) {
            runner.setName(this.getHTTPConfig().newIdentity("ThreadID"));
        }    
        
        // Connection level try catch
        try {
            
            // Connection related fields which will span all requests
            this.sourceAddress = getInetAddress().getHostAddress();
            this.protocol = (this.socket instanceof SSLSocket) ? "https" : "http"; 
            this.serviceHost = socket.getLocalAddress().getHostAddress();
            this.servicePort = socket.getLocalPort();
            
            // Log this connection if configuration supports it
            if (this.getHTTPConfig() != null && this.getHTTPConfig().getHTTPMonitor() != null) {
                this.httpConfig.getHTTPMonitor().logConnection(getSourceAddress());
            }
            
            // Configure connection
            socket.setSoTimeout(READ_TIMEOUT);
            //socket.setReuseAddress(true);
            //socket.setReceiveBufferSize(1024); 
            //socket.setSendBufferSize(2048);
            //socket.setTcpNoDelay(true);             
            
            // Create in
            InputStream in = new BufferedInputStream(socket.getInputStream(), 1024);
            this.in = new HTTPInputStream(this, in);
            
            // Create out
            this.out = new HTTPOutputStream(this, socket.getOutputStream());          

            
            boolean isReusable = true;     
            
            // This loop services multiple, pipelined request from the client web browser.
            while (isReusable) {            
                
                // Request level try catch
                try {
                    // Here we process the socket
                    // This involves reading the request sent from the client
                    // finding out what the request is for
                    // and returning a response
                    serve();    
                    
                    // Encourage out to cache response
                    this.out.cache();
                 
                } catch (SocketTimeoutException ex) {
                    throw ex; // Socket read timeout
                } catch (EOFException ex) {
                    throw ex; // Socket closed by client                   
                } catch (SocketException ex) {
                    throw ex; // Socket failure (disconnected stream)
                } catch (SSLException ex) {
                    throw ex; // Bad SSL data
                } catch (InterruptedException ex) {
                    throw ex; // this.forceStop() interrupted thread     
                } catch (HTTPException ex) {
                	ex.printStackTrace();
                    // Tell the browser about this exception                    
                    if (this.out != null) this.out.write(ex);
                    else throw ex;                   
                } catch (Throwable ex) {
                    ex.printStackTrace();
                    logException("Error while serving", ex);
                    // Tell the browser about this exception                    
                    if (this.out != null) this.out.write(ex);
                    else throw ex;
                } 
                finally {
                    
                    // Flush the response
                    if (this.out != null) {
                        this.out.finish(); 
                        this.out.flush();
                    }
                    
                    // Determine if we can handle another request on this connection
                    isReusable = this.in.isActive() && this.out != null && this.isReusable();           
                    
                    if (isReusable) {
                        // The previous request may have taken a while to complete and
                        // the handling thread may have been dropped to a lower priority.
                        // Set the priority to normal for this new request.                        
                        if (this.runner.getPriority() < Thread.NORM_PRIORITY) {
                            this.runner.setPriority(Thread.NORM_PRIORITY);  
                        }                        
                    }
                    
                    // Check HTTP request is valid
                    if (this.in.isValidRequest()) {
                        // Log this request if configuration supports it
                        if (getHTTPConfig() != null && 
                            getHTTPConfig().getHTTPMonitor() != null) {
                            
                            // Log the request
                            getHTTPConfig().getHTTPMonitor().logRequest(getSourceAddress(), getRequest(), getResponse());
                            // Store statistical information
                            getHTTPConfig().getHTTPMonitor().updateStatistics(getSourceAddress(), getRequest(), getResponse());
                        }
                    }
                }
            }
        } catch (SocketTimeoutException ex) {
            // Connection idle
        } catch (ParseException ex) {
            // Invalid / non HTTP request 
            logException("Failed to parse HTTP header", ex);
        } catch (EOFException ex) {
            // Connection closed by client
            //logException("Connection closed by client", ex);              
        } catch (IOException ex) {
            // Connection problem
            //logException("Connection Error", ex);           
        } catch (Exception ex) {
            // Other problem
            ex.printStackTrace();            
            logException("General Error", ex);
        } catch (Throwable ex) {
            // More serious problem
            ex.printStackTrace();            
            logException("Serious Error", ex);
        } 
        finally {
            this.forceClose();
        }
    }   

    private void serve() throws Exception {      
        
        // Prepare HTTP response (need to be ready for errors)
        this.currentResponse = this.out.startResponse();        
        
        // Read the HTTP request sent from the client (Blocking)
        try {
            this.currentRequest = this.in.readRequest();
            if (DEBUG) System.out.println(this.currentRequest);
            
            // Configure HTTP response version
            int major = this.currentRequest.getVersionMajor();
            int minor = this.currentRequest.getVersionMinor();
            
            if (major > 1 || (major == 1 && minor >= 1)) {
                major = 1; minor = 1;
            }

            this.currentResponse.setVersionMajor(major);
            this.currentResponse.setVersionMinor(minor);
            
        } catch (IOException ex) {
            throw ex;
        } catch (Exception ex) {
            HTTPException httpEx = new HTTPException(400, "Malformed Request");
            httpEx.initCause(ex);
            throw httpEx;
        }             
        
        // Ready to process request
        this.currentRequest.setStartTime(System.currentTimeMillis());  
        

        // Check URL is specified
        if (this.currentRequest.getUri() == null) {
            throw new HTTPException(400, "No URL specified");
        }
        
        // Depending on the method, do different things      
        String method = this.currentRequest.getMethod().toUpperCase();        
        
        // Read form data in the uri (after question mark)
        this.in.readURLQuery(this.currentRequest);
        
        if (method.equals("TRACE")) {
            this.out.write(this.currentRequest);
            return;
        }         
        
        if (method.equals("POST")) {
            // Read form data posted as content after the HTTP header
        	HTTPPostReader postReader = new HTTPPostReader(this);
        	postReader.readPOST(this.currentRequest);
        } 
        
        if (this.getHTTPConfig() != null) {
            
            // Session handling
            if (this.getHTTPConfig().getSessionFinder() != null) {
    
                // Look at the request and get the session object (if available)
                Object sessionObject = this.getHTTPConfig()
                    .getSessionFinder().findSession(this); 
                
                // Store the session so it can be accessed by HTTPResponders
                this.currentRequest.setSession(sessionObject);
            }
            
            // Object finding
            if (this.getHTTPConfig().getObjectFinder() != null) {
                
                // Look at the request and get the requested object
                Object obj = this.getHTTPConfig().getObjectFinder().findObject(this); 
                
                // Store response object (for logging)
                this.currentResponse.setData(obj);
                
                // Send the response object to the client                
                this.out.write(obj);        
                
            }
            else throw new HTTPException(500, "ObjectFinder not available");
        }
        else throw new HTTPException(500, "HTTPConfiguration not available");        

    }
    
    public boolean isReusable() {
        
        if (this.lastRequest) return false;

        boolean isReusable = false;   
        
        if (currentRequest == null || currentRequest.getHeaders() == null) return false;

        String reqConnType = currentRequest.getHeaders().getFirst("Connection");
        if (reqConnType != null && reqConnType.toLowerCase().contains("keep-alive")) {
            isReusable = true;
        }
        
        String resConnType = currentResponse.getHeaders().getFirst("Connection");
        if (resConnType != null && resConnType.equalsIgnoreCase("close")) {
            isReusable = false;
        }

        return isReusable;
    }    
    
    public void setTCPNoDelay(boolean on) throws SocketException {
        socket.setTcpNoDelay(on);
    }
    
    private void logException(String detail, Throwable ex) {
        Exception wrappedEx = new Exception(detail);
        wrappedEx.initCause(ex);
        logException(wrappedEx);
    }    
    
    private void logException(Throwable ex) {
        try {
            // Store this exception in the response
            this.currentResponse.setException(ex);
        } catch (NullPointerException ex2) { /* unavailable */ }     
        
        // Log this request if configuration supports it
        if (this.getHTTPConfig() != null && 
            this.getHTTPConfig().getHTTPMonitor() != null) {
            
            this.getHTTPConfig().getHTTPMonitor().logException(ex, getRequest());
        }
        else ex.printStackTrace();
    }    

    public HTTPInputStream getInputStream() {
        return this.in;
    }

    public HTTPOutputStream getOutputStream() {
        return this.out;
    }
    
    public HTTPRequest getRequest() {
        return this.currentRequest;
    }

    public HTTPResponse getResponse() {
        return this.currentResponse;
    } 
    
    public HTTPConfig getHTTPConfig() {
        return this.httpConfig;
    }
    
    public void forceClose() {

        //  Close the connection, we are done
        try { this.out.close(); } catch (Exception ex) {}  
        try { this.in.close(); } catch (Exception ex) {}  
        try { this.socket.close(); } catch (Exception ex) {} 
        
        if (Thread.currentThread().equals(this.runner)) {
            this.running = false;
        }        
    }
        
    public String getProtocol() {
        return this.protocol;
    } 
    
    public String getSourceAddress() {
        return this.sourceAddress;
    }       
    
    public String getServiceHost() {
        return this.serviceHost;
    }   
        
    public int getServicePort() {
        return this.servicePort;
    }   
    
    
    public String getServiceAddress() {
        String host = null;
        try {
            host = this.currentRequest.getHeaders().getFirst("Host");
        } catch (NullPointerException exReq) { 
            // Occurs if request not processed yet
            host = getServiceHost() + ":" + getServicePort();
        }
        return getProtocol() + "://" + host;
    }    
    
    public long getExecutionTime() {
        try {
            long startTime = currentRequest.getStartTime();
            return System.currentTimeMillis() - startTime;
        } catch (Exception e) {
            // Still processing request
        }
        return 0;
    }
    
    public void gracefulClose() {
        this.lastRequest = true;
    }
     
    @SuppressWarnings("deprecation")
    public void forceStop() {
        
        // Not started or stopped
        if (this.runner == null) return;
        
        StackTraceElement[] stack = this.runner.getStackTrace();
        if (stack == null || stack.length == 0) {
            this.running = false;
            return;
        }
        
        // Only interrupt non privileged classes (trust others)
        Privilege privilege = SecurityMan.getCodePrivilege(this.runner, false);
        if (privilege.isBelow(Privilege.MODERATOR)) {
            
            // Interrupt connection thread
            this.runner.interrupt();    
            System.err.println("Warning: Interrupting connection thread " + runner.getName());              
            
            // Allow 100 ms to finish
            try { Thread.sleep(100); } catch (InterruptedException ex) {}        
            
            // If still not finished kill the thread
            if (this.running && this.runner.isAlive()) {
                // Last resort
                String info = "Dynamic class interrupted.";
                HTTPException dci = new HTTPException(500, info);                  
                try {                   
                    this.getOutputStream().write(dci);
                    this.forceClose();
                } catch (Exception ex) {}
                
                // TODO Find a better way to force a thread to stop
                System.err.println("Warning: Forcing connection thread "  +
                                    runner.getName() + " to stop.");                
                this.runner.stop();
                
                this.logException("Thread forced to stop", dci);
                this.socket = null;                
            }
            this.running = false;   
        }
    }  
}
