/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.base.proxy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.rmi.RemoteException;
import java.util.Arrays;

import org.ow2.jonas.service.ServiceException;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Class that is handling the client socket.
 * @author Florent Benoit
 */
public class HttpSocketHandler implements Runnable {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(HttpSocketHandler.class);

    /**
     * Thread count.
     */
    private static int counter = 0;

    /**
     * Link to the OnDemand service.
     */
    private HttpOnDemandProxy onDemandProxy = null;

    /**
	 * Client Socket.
	 */
	private Socket clientSocket = null;

	/**
	 * Client socket input stream.
	 */
	private InputStream clientInputStream = null;

	/**
	 * Client socket output stream.
	 */
	private OutputStream clientOutputStream = null;

    /**
     * Remote connection output stream.
     */
    private OutputStream remoteOutputStream = null;

    /**
     * Remote connection input stream.
     */
    private InputStream remoteInputStream = null;

    /**
     * A request has been sent to the web container in order to be started ?
     */
    private boolean webContainerStartRequested = false;


	/**
	 * Create a new handler for the given socket.
     * @param onDemandProxy the http on demand proxy
	 * @param clientSocket the given client socket
	 * @throws HttpOnDemandProxyException if unable to get streams
	 */
    public HttpSocketHandler(final HttpOnDemandProxy onDemandProxy, final Socket clientSocket)
            throws HttpOnDemandProxyException {
        this.onDemandProxy = onDemandProxy;
        this.clientSocket = clientSocket;


        // Get client's input stream
        try {
            this.clientInputStream = clientSocket.getInputStream();
        } catch (IOException e) {
            throw new HttpOnDemandProxyException("Cannot get input stream", e);
        }

        // Get client's output stream
        try {
            this.clientOutputStream = clientSocket.getOutputStream();
        } catch (IOException e) {
            throw new HttpOnDemandProxyException("Cannot get output stream", e);
        }
    }

    /**
     * Analyze the request sent by the client.
     * @throws HttpOnDemandProxyException if request cannot be analyzed
     */
	protected void analyzeRequest() throws HttpOnDemandProxyException {

        // Analyze stream in order to get the context
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        // The request has been found ?
        boolean requestFound = false;

        // Initialize data
        int bytesRead;
        byte[] buffer = new byte[128];

        // First line submitted by the client
        String firstLine = null;
        try {
            while (!requestFound && (bytesRead = clientInputStream.read(buffer)) > 0) {
                // Search if we've the CRLF
                String line = new String(buffer);
                String[] splitLines = line.split("\r\n");

                // Keep the analyzed bytes for later.
                byteArrayOutputStream.write(buffer, 0, bytesRead);

                // We've a first line, keep it ?
                if (splitLines.length >= 1) {
                    firstLine = splitLines[0];
                    logger.debug("Request is: ''{0}''", firstLine);
                    requestFound = true;
                }
            }
        } catch (IOException e) {
            cleanup("Unable to handle request", e);
            throw new ServiceException("Unable to handle request", e);
        }

        // Wrong request
        if (firstLine == null || !requestFound) {
            cleanup("Invalid header");
            return;
        }

        // Analyse context asked
        String[] args = firstLine.split(" ");
        String contextValue = null;

        String askedURL = null;

        if (args.length > 1) {
            // get second token (line is like : GET /myContext/.... HTTP1/0)
            askedURL = args[1];

            // Request should begin by a /
            if (askedURL.startsWith("/")) {
                // search the second / or go to the end
                int index = askedURL.substring(1).indexOf("/");
                if (index == -1) {
                    // take the whole part
                    contextValue = askedURL.substring(1);
                } else {
                    // extract the part
                    contextValue = askedURL.substring(1, index + 1);
                }
            }

        }

        if (contextValue == null) {
            logger.warn("Unable to find context in the given request ''{0}''", firstLine);
        }

        // Analyze the request and start the web container if not started or deploy the module if not started.

        // Handle special requests
        if ("jonasOnDemandProxy".equals(contextValue)) {
            if ("/jonasOnDemandProxy/wait.gif".equals(askedURL)) {
                sendWaitGif();
            }
            return;
        }


        // Check if web container is launched ?
        if (!onDemandProxy.getWebContainerService().isInternalContainerStarted()) {

            // Redirect to a page telling that web container is starting...
            webContainerIsStarting();

            // Start request not yet asked
            synchronized (this) {
                if (!webContainerStartRequested) {
                    webContainerStartRequested = true;
                    startWebContainer();
                }
            }
            return;
        }



        // We know the context, check if it is available on our list
        if (contextValue != null) {



            logger.debug("Extracted context ''{0}'' from the line", contextValue, firstLine);

            // managed by ourself
            if (onDemandProxy.isAvailableContext(contextValue)) {
                // Not yet deployed
                if (!onDemandProxy.isContextDeployed(contextValue)) {

                    // Get Context info
                    ContextInfo contextInfo = onDemandProxy.getContextInfo(contextValue);

                    // needs to start or not the application ?
                    boolean startApplication = false;

                    // not yet send a start request ?
                    if (!contextInfo.isStarting()) {
                        startApplication = true;
                        contextInfo.setStarting(true);
                    }

                    // Redirect to a page telling that the context is starting...
                    contextIsStarting(contextValue);

                    // Needs to start the application
                    if (startApplication) {

                        // get war file
                        String warFile = contextInfo.getWarFile();

                        // start the context
                        try {
                            onDemandProxy.getWebContainerService().registerWar(warFile);
                        } catch (RemoteException e) {
                            cleanup("Unable to deploy application '" + warFile + "'.", e);
                        }
                    }
                    return;

                }
            }
        }

        // Now connect on the web container
        Socket remoteServerSocket;
        try {
            remoteServerSocket = new Socket(onDemandProxy.getServerSocket()
                    .getInetAddress(), onDemandProxy.getRedirectPortNumber());
        } catch (IOException e) {
            // exception while connecting to the remote host
            cleanup("Unable to connect to the remote host '" + onDemandProxy.getServerSocket()
                    .getInetAddress() + "' on the port number '" + onDemandProxy.getRedirectPortNumber() + "'", e);
            return;
        }

        // Get streams of the remote host
        try {
            remoteOutputStream = remoteServerSocket.getOutputStream();
        } catch (IOException e) {
            cleanup("Unable to get remote stream of server socket '" + remoteServerSocket + "'", e);
            return;
        }

        // remote input stream
        try {
            remoteInputStream = remoteServerSocket.getInputStream();
        } catch (IOException e) {
            cleanup("Unable to get remote stream of server socket '" + remoteServerSocket + "'", e);
            return;
        }

        // Define a thread that will copy stream of the client to the remote stream
        // It will first send the analyzed bytes, then it will send the remaining bytes
        CopyingStream t = new CopyingStream(this, byteArrayOutputStream, clientInputStream, remoteOutputStream);
        t.setName(t.getClass().getName() + "-" + (counter++));
        t.start();

        // Copy the answer of the remote host to the client.
        byte[] buf = new byte[1024];
        int count;
        try {
            while (((count = remoteInputStream.read(buf)) > 0)) {
                clientOutputStream.write(buf, 0, count);
            }
        } catch (Exception e) {
            cleanup("Unable to handle data from server to the client", e);
            return;
        }

        cleanup();
    }


    public void cleanup() {

        try {
            if (clientInputStream != null) {
                clientInputStream.close();
            }
        } catch (IOException e) {
            logger.debug("Unable to close stream", e);
        }

        try {
            if (clientOutputStream != null) {
                clientOutputStream.close();
            }
        } catch (IOException e) {
            logger.debug("Unable to close stream", e);
        }

        try {
            if (remoteOutputStream != null) {
                remoteOutputStream.close();
            }
        } catch (IOException e) {
            logger.debug("Unable to close stream", e);
        }

        try {
            if (remoteInputStream != null) {
                remoteInputStream.close();
            }
        } catch (IOException e) {
            logger.debug("Unable to close stream", e);
        }

        try {
            clientSocket.close();
        } catch (IOException e) {
            logger.debug("Unable to close socket", e);
        }

    }

    /**
     * Send an error message.
     * @param message the given error message
     */
    public void cleanup(final String message) {
        cleanup(message, null);
    }

    /**
     * Send an error message with its exception
     * @param message the given message
     * @param exception the given exception
     */
    public void cleanup(final String message, final Exception exception) {
        HTTPResponse httpResponse = new HTTPResponse("HTTP/1.0 500");

        httpResponse.println(message);

        httpResponse.println("<br>");
        if (exception != null) {
            httpResponse.println(Arrays.asList(exception.getStackTrace()).toString());
        }

       sendBytesToClient(httpResponse);
    }

    /**
     * Send the given response to the client.
     * @param httpResponse the given response
     */
    protected void sendBytesToClient(final HTTPResponse httpResponse) {

        // write content
        try {
            clientOutputStream.write(httpResponse.getContent());
            clientOutputStream.flush();
        } catch (IOException ioe) {
            logger.debug("Unable to send error message. Stream may be already closed.", ioe);
        }

        // Needs to shutdown streams
        try {
            clientSocket.shutdownOutput();
        } catch (IOException e) {
            logger.debug("Unable to close the socket", e);
        }
    }

    /**
     * Asked to start the web container
     */
    protected void startWebContainer() {
        onDemandProxy.startWebContainer();
    }

    /**
     * Tell to the client that the web container is starting.
     */
    protected void webContainerIsStarting() {
        // Send to the client that the web container is starting...
        HTTPResponse httpResponse = new HTTPResponse();
        httpResponse.setRefresh(true);
        String txt = "The HTTP Web Container is starting. Please wait.";
        httpResponse.setTitle(txt);

        txt = "<image src='/jonasOnDemandProxy/wait.gif'><br/><br/>" + txt;

        httpResponse.print(txt);
        sendBytesToClient(httpResponse);
    }

    /**
     * Send the wait gif
     */
    protected void sendWaitGif() {
        String waitGifResource = "/" + HttpSocketHandler.class.getPackage().getName().replace(".", "/") + "/wait.gif";
        InputStream is = null;

        // Send to the client the given gif
        HTTPResponse httpResponse = new HTTPResponse();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = HttpSocketHandler.class.getClassLoader().getResourceAsStream(waitGifResource);
            httpResponse.setContentType("image/gif");
            httpResponse.setRefresh(true);


            byte[] buf = new byte[1024];
            int count;
            try {
                while (((count = is.read(buf)) > 0)) {
                    baos.write(buf, 0, count);
                }
            } catch (Exception e) {
                logger.error("Cannot get data", e);
            }
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                logger.debug("unable to close", e);
            }
        }
        httpResponse.setBodyBytes(baos.toByteArray());
        sendBytesToClient(httpResponse);
    }


    /**
     * Tell to the client that the context is starting.
     */
    protected void contextIsStarting(final String contextName) {

        // Send to the client that the module is starting...
        HTTPResponse httpResponse = new HTTPResponse();
        httpResponse.setRefresh(true);
        String txt = "The .war file associated with the context '/" +  contextName + "' is loading. Please wait.";
        httpResponse.setTitle(txt);

        txt = "<image src='/jonasOnDemandProxy/wait.gif'><br/><br/>" + txt;

        httpResponse.print(txt);
        sendBytesToClient(httpResponse);
    }


    /**
     * Handle the request on the selected socket.
     */
    public void run() {
        try {
            analyzeRequest();
		} catch (HttpOnDemandProxyException e) {
			logger.error("Unable to handle request", e);
		}


	}
}
