package com.prolixtech.jaminid;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

import com.prolixtech.utils.SingletonLogger;
import com.prolixtech.utils.Suspender;

/**
 * The connection is a thread-of-service spanwed by the Daemon. The connection
 * handles the IO part of the Response/Oracle/Request paradigm by creating the
 * request and response objects, and calling the Oracle at the appropriate time
 * to extract the output which will be send to the client.
 * 
 * @author Constantinos Michael
 * 
 * 
 * 
 * TODO
 */
public class Connection extends Thread {
    private Socket activeSocket;

    private Daemon masterDaemon;

    private InputStream socketInput;

    private OutputStream socketOutput;

    private static SimpleDateFormat GMTDateFormatter = new java.text.SimpleDateFormat(
            "d MMM yyyy hh:mm:ss z", Locale.US);

    private boolean isalive = true;


    private static final long lineTimeout = 5000L;
    private static final long lineCheckInterval = 100L;
    
    /**
     * default constructor to create a Connection
     * 
     * @param activeSocket
     *            the socket on which the connection was madee
     * @param masterDaemon
     *            the daemon that spanwed this connection
     * @throws IOException
     */
    public Connection(Socket activeSocket, Daemon masterDaemon)
            throws IOException {
        
        this.activeSocket = activeSocket;
        this.masterDaemon = masterDaemon;
        InetAddress ina = activeSocket.getInetAddress();
        this.setName("CN " + ina);
        SingletonLogger.Instance().fine("Connection from " + ina);
        socketInput = activeSocket.getInputStream();
        socketOutput = activeSocket.getOutputStream();
        start();

    }

    public String getIPAddressString(){
        return this.activeSocket.getInetAddress().toString();
    }
    
    /**
     * The thread runs here. This essentially allows the Daemon to serve
     * multiple threads. ALL IO is carried out here.
     */
    public void run() {
        try {
            
            printlog("Processing Connection.");

            CONNECTED:
            while(true){
            StringBuffer requestInputBuffer = new StringBuffer();

            int byteIn = -1;
            int CRLFState = 0;
            boolean doubleCRLFpassed = false;
            Request serviceRequest = new Request(masterDaemon, this);
            int expectedBodySize = 0;
            int bodyCharsParsed = 0;
            INPUTLOOP:
            do {
                if(socketInput.available()==0){	            
                    Timer a = new Timer();
                    ExpireTask timerTask = new ExpireTask();
                    a.schedule(timerTask, lineTimeout);
                    while(timerTask.running){
                        if(socketInput.available()>0) timerTask.running=false;
                        Suspender.suspendMillis(lineCheckInterval);
	                }
                    if(socketInput.available()==0){break INPUTLOOP;}
                }
                
                byteIn = socketInput.read();
                if (byteIn > 0) {

                    requestInputBuffer.append((char) byteIn);
                    if (doubleCRLFpassed)
                        bodyCharsParsed++;

                    // LINEAR CRLF State Detection
                    // case 0: none.
                    // case 1: \r
                    // case 2: \r\n
                    // case 3: \r\n\r
                    // case 4: \r\n\r\n
                    char thischar = ((char) byteIn);

                    switch (CRLFState) {
                    case 0:
                        if ("\r".charAt(0) == (thischar)) {
                            CRLFState++;
                        }
                        break;
                    case 1:
                        if ("\n".charAt(0) == (thischar)) {
                            CRLFState++;
                        } else {
                            CRLFState = 0;
                        }
                        break;
                    case 2:
                        if ("\r".charAt(0) == (thischar)) {
                            CRLFState++;
                        } else {
                            CRLFState = 0;
                        }
                        break;
                    case 3:
                        if ("\n".charAt(0) == (thischar)) {
                            CRLFState++;
                            doubleCRLFpassed = true;
                            serviceRequest.addRequestLines(requestInputBuffer
                                    .toString());

                            requestInputBuffer = new StringBuffer();

                            expectedBodySize = serviceRequest.switchToBody();
                        } else {
                            CRLFState = 0;
                        }
                        break;
                    }

                }

                // System.out.println(doubleCRLFpassed + " B/E: " +
                // bodyCharsParsed + " : " + expectedBodySize);
                // && byteIn!=-1 && socketInput.available()>0
            } while (!doubleCRLFpassed || doubleCRLFpassed
                    && bodyCharsParsed < expectedBodySize || doubleCRLFpassed
                    && byteIn != -1 && socketInput.available() > 0);

            printlog("Request: " + requestInputBuffer.toString());
            serviceRequest.addRequestLines(requestInputBuffer.toString());

            serviceRequest.switchToCompleted();

            Response serviceResponse = new Response(Protocol.OK, masterDaemon
                    .getProtocol(), this.socketOutput, serviceRequest);

            serviceResponse.setHeaderLine(ProtocolResponseHeader.Content_Type, "text/html");


            serviceResponse.setHeaderLine(ProtocolResponseHeader.Date,
                    getGMTString(new Date()));

            ContentOracle chout = masterDaemon.getOracle();
            
            String oracleOutput = null;

            chout = chout.getDelegatedOracle(serviceRequest);

            try {
                oracleOutput = chout.demultiplex(serviceRequest,
                        serviceResponse);
            } catch (Exception e) {
                SingletonLogger.Instance().severe("User Oracle FAILED");
                e.printStackTrace();
                if (chout != ContentOracle.Instance()) {
                    try {
                        oracleOutput = ContentOracle.Instance().demultiplex(
                                serviceRequest, serviceResponse);
                    } catch (Exception f) {
                        f.printStackTrace();
                        SingletonLogger.Instance().severe(
                                "Fallback Oracle FAILED");
                    }
                }

            }

            serviceResponse.publishHeader();

            if (chout instanceof StreamingOracle) {
                StreamingOracle shout = (StreamingOracle) chout;
                while (isalive) {

                    Suspender.suspendMillis(shout.getStreamInterval());
                    this.sendString(shout.getNextBatch(serviceRequest,
                            serviceResponse));
                }
            } else {
                this.sendString(oracleOutput);
            }

            socketOutput.flush();
            
            if( ((String) serviceRequest.getHeader().get(ProtocolResponseHeader.Connection)).equalsIgnoreCase("Close") ){

                serviceResponse.setHeaderLine(ProtocolResponseHeader.Connection, "close");
                socketOutput.close();
                printlog("Connection Done.");
                break CONNECTED;
            }
        }
            
            

        } catch (Exception e) {
            printlog("Exception: " + e);
        } finally {
            try {
                activeSocket.close();
            } catch (Exception e) {
                System.err.println("Socket not closed");
            }
        }

    }

    /**
     * a shortcut method
     * 
     * @param message
     *            the message to print
     */
    private void printlog(String message) {
        masterDaemon.printlog("[" + activeSocket.getInetAddress() + "] "
                + message);
    }

    /**
     * Sends a string to the client
     * 
     * @param string
     * @throws IOException
     */
    protected void sendString(Object string) throws IOException {
        if (string == null)
            return;
        socketOutput.write((string.toString()).getBytes());
    }

    protected void sendString(byte[] bytes) {
        try {
            socketOutput.write(bytes);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            this.isalive = false;
        }
    }

    public static String getGMTString(Date d) {

        return Connection.GMTDateFormatter.format(d);
    }

}


class ExpireTask extends TimerTask {
    boolean running = true;

    public void run() {
        running = false;
    }
}
