package com.jpexs.proxy;

import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import javax.net.ssl.SSLHandshakeException;

class Handler implements Runnable
{
    static final boolean DEBUG = false;
    
    Client client = null;
    Socket socket = null;
    Request request = null;
    Reply reply = null;
    HttpRelay http = null;
    int currentLength = -1;
    int contentLength = -1;
    long idle = 0;
    double bytesPerSecond = 0;

    List<Replacement> replacements;
    CatchedListener catchedListener;
    List<String> catchedContentTypes;
    ReplacedListener replacedListener;

    static int curId=0;
    int id;

    /**
     * Create a Handler.
     */
    Handler(Socket socket,List<Replacement> replacements, List<String> catchedContentTypes, CatchedListener catchedListener, ReplacedListener replacedListener)
    {
        curId++;
        id=curId;
        this.socket = socket;
        this.replacements = replacements;
        this.catchedListener = catchedListener;
        this.catchedContentTypes = catchedContentTypes;
        this.replacedListener = replacedListener;
    }

    /**
     * Close all connections associated with this handler.
     */
    synchronized void close()
    {
	if (client != null)
	{
	    client.close();
	    client = null;
	}
	if (http != null)
	{
	    http.close();
	    http = null;
	}
    }

    /**
     * Flush all data to the client.
     */
    void flush()
    {
	if (client != null)
	{
	    try
	    {
		client.getOutputStream().flush();
	    }
	    catch (IOException e)
	    {

	    }
	}
    }

    public void run()
    {
	boolean keepAlive = false;
	Exception reason = null;

	Thread.currentThread().setName("Handler("
					 + socket.getInetAddress().getHostAddress()
					 + ")");

	try
	{
	    client = new Client(socket);
	    client.setTimeout(ProxyConfig.readTimeout);
	}
	catch (IOException e)
	{


	    return;
	}

	try
	{
      boolean secure=false;
      int securePort=443;
      String secureServer="";
	    do
	    {
		request = null;
		reply = null;
		idle = System.currentTimeMillis();

		try
		{
		    request = client.read();
          if(secure){
             request.addSecureHostToURL(secureServer);
          }
		}
      catch(SSLHandshakeException she)
      {
         she.printStackTrace();         
         break;
      }
		catch (IOException e)
		{

		    break;
		}

      
      if(request.getCommand().equals("CONNECT")){
         secureServer=request.getHost();
         securePort=request.getPort();
         if((ProxyConfig.httpsMode==ProxyConfig.HTTPS_FILTER)||((ProxyConfig.httpsMode==ProxyConfig.HTTPS_FILTERLIST)&&(ProxyConfig.enabledHttpsServers.contains(secureServer)))){
            secure=true;
            reply=new Reply();
            reply.statusLine = "HTTP/1.0 200 Connection established";
            reply.setHeaderField("Proxy-agent", ProxyConfig.appName);
            try {
               client.write(reply);
            } catch (IOException ex) {

            }
            client.promoteToServerSSL();
            keepAlive=true;
            continue;
         }
      }

		idle = 0;

		try
		{
		    keepAlive = processRequest(secure,secureServer,securePort);
		}
		catch (IOException ioe)
		{
		    reason = ioe;
		    keepAlive = false;
		}

		if (request != null && reply != null)
		{
		    // XXX insert the number of bytes read into the
		    // reply content-length for logging.
		    if (reply != null && currentLength > 0)
		    {
			reply.setHeaderField("Content-length", currentLength);
		    }

		    
		}
	    }
	    while (keepAlive);
	}
	finally
	{
	    
	}

	if (reason != null && reason.getMessage().indexOf("Broken pipe") == -1)
	{
	    if (client != null && request != null)
	    {
		error(client.getOutputStream(), reason, request);
	    }

	}
	
	close();
    }

    boolean processRequest(boolean secure,String secureHost,int securePort) throws IOException
    {
	boolean keepAlive = false;
	
	while (reply == null)
	{
	    //boolean secure = false;
	    boolean uncompress = false;


	    if (request.getCommand().equals("CONNECT"))
	    {
		secure = true;
	    }
	    else if (request.getURL().startsWith("/"))
	    {
		
	    }
	    else if (request.getURL().startsWith("https://")&&(secure))
	    {

	    }
	    else if (! request.getURL().startsWith("http://"))
	    {

		return false;
	    }

	    /* Client wants Keep-Alive */
	    if (ProxyConfig.proxyKeepAlive)
	    {
		keepAlive = (request.containsHeaderField("Proxy-Connection")
			     && request.getHeaderField("Proxy-Connection").equals("Keep-Alive"));      
	    }

	    /* Filter the request. */
       //deleted

		/* None found.  Use http or https relay. */
		if (secure)
		{
		    http = createHttpsRelay(secureHost,securePort);
		}
		else
		{
		    http = createHttpRelay();
		}
	    try
	    {
     http.sendRequest(request);
		if (http instanceof Http)
		{
		    ((Http)http).setTimeout(ProxyConfig.readTimeout);
		}
		reply = http.recvReply(request);
	    }
	    catch (RetryRequestException e)
	    {
		http.close();
		http = null;
		continue; /* XXX */
	    }

	    /* Guess content-type if there aren't any headers.
	       Probably an upgraded HTTP/0.9 reply. */
	    if (reply.headerCount() == 0)
	    {
		String url = request.getURL();
		if (url.endsWith("/")
		    || url.endsWith(".html") || url.endsWith(".htm"))
		{
		    reply.setHeaderField("Content-type", "text/html");
		}
	    }



	    /* Filter the reply. */
	    if (false)
	    {
		/* uncompress gzip encoded html so it can be filtered */
		if (!ProxyConfig.dontUncompress
		    && "text/html".equals(reply.getHeaderField("Content-type")))
		{
		    String encoding = reply.getHeaderField("Content-Encoding");
		    if (encoding != null && encoding.indexOf("gzip") != -1)
		    {
			reply.removeHeaderField("Content-Encoding");
			reply.removeHeaderField("Content-length");
			uncompress = true;
		    }
		}

		//filter(reply);
	    }


       if(request.containsHeaderField("Connection")&&(request.getHeaderField("Connection").toLowerCase().equals("keep-alive"))&& reply.containsHeaderField("Connection")
			     && reply.getHeaderField("Connection").equals("Keep-Alive")){
         keepAlive=true;
      }
	    reply.removeHeaderField("Proxy-Connection");
	    if (keepAlive && reply.containsHeaderField("Content-length"))
	    {
		reply.setHeaderField("Proxy-Connection", "Keep-Alive");
	    }
	    else
	    {
		keepAlive = false;
	    }

	    currentLength = -1;
	    contentLength = -1;
	    try
	    {
		contentLength = Integer.parseInt(reply.getHeaderField("Content-length"));
	    }
	    catch (NumberFormatException e)
	    {
	    }



	    if (http instanceof HttpsThrough)
	    {
		HttpsThrough https = (HttpsThrough) http;
		int timeout = ProxyConfig.readTimeout;
		
		client.write(reply);


		try
		{
		    client.setTimeout(timeout);
		    https.setTimeout(timeout);
		
		    Copy cp = new Copy(client.getInputStream(), https.getOutputStream());
		    ReusableThread thread = Server.getThread();
		    thread.setRunnable(cp);
		    flushCopy(https.getInputStream(), client.getOutputStream(), -1, true);
		    client.close();
		}
		catch (InterruptedIOException iioe)
		{
		    // ignore socket timeout exceptions
		}
	    }
	    else if (reply.hasContent())
	    {
		 try
		{
		    processContent(uncompress);
		}
		catch (IOException e)
		{
		    if (http instanceof Http)
		    {
			((Http)http).reallyClose();
		    }
		    else
		    {
			http.close();
		    }
		    http = null;

		    client.close();
		    client = null;

		    throw e;
		    //return false; /* XXX */
		}
		/* Document contains no data. */
		if (contentLength == 0)
		{
		    client.close();
		}
	    }
	    else
	    {
		client.write(reply);
	    }

	    http.close();
	}

	return keepAlive;
    }

    HttpRelay createHttpsRelay(String secureHost,int securePort) throws IOException
    {
	HttpRelay http;

   if((ProxyConfig.httpsMode==ProxyConfig.HTTPS_FILTER)||((ProxyConfig.httpsMode==ProxyConfig.HTTPS_FILTERLIST)&&(ProxyConfig.enabledHttpsServers.contains(secureHost))))
      {
         if(ProxyConfig.useHTTPSProxy){
            http=Https.open(ProxyConfig.httpsProxyHost,ProxyConfig.httpsProxyPort,true);
             Request connectReq=new Request(null);
             connectReq.setStatusLine("CONNECT "+secureHost+":"+securePort+" HTTP/1.1");
             connectReq.setCommand("CONNECT");
             connectReq.setURL(secureHost+":"+securePort);
             connectReq.setProtocol("HTTP/1.1");
            try {
               http.sendRequest(connectReq);
               Reply rep=http.recvReply(connectReq);               
            } catch (RetryRequestException ex) {

            }
            ((Https)http).promoteToClientSSL();
         }else{
          http=Https.open(secureHost,securePort,false);
          ((Https)http).promoteToClientSSL();
         }
          /*http = new Http(request.getHost(),request.getPort(),ProxyConfig.useHTTPSProxy);
          if(ProxyConfig.useHTTPSProxy){
             Request connectReq=new Request(client);
             connectReq.setCommand("CONNECT");
             connectReq.setURL(secureHost+":"+securePort);
             connectReq.setProtocol("HTTP/1.1");
            try {
               http.sendRequest(connectReq);
               http.recvReply(connectReq);
            } catch (RetryRequestException ex) {

            }
          }
          ((Http)http).promoteToClientSSL();*/
      }else{
         if(ProxyConfig.useHTTPSProxy)
         {
            http = new HttpsThrough(ProxyConfig.httpsProxyHost,
                  ProxyConfig.httpsProxyPort,
                  true);
         }else{
            http = new HttpsThrough(secureHost,securePort);
         }
      }

	    
	

	return http;
    }
    
    HttpRelay createHttpRelay() throws IOException
    {
	HttpRelay http;
	
	if (ProxyConfig.useHTTPProxy)
	{
	    http = Http.open(ProxyConfig.httpProxyHost,
			      ProxyConfig.httpProxyPort,
			      true);
	}
	else
	{
	    http = Http.open(request.getHost(), request.getPort());
	}

	return http;
    }

    InputStream readChunkedTransfer(InputStream in) throws IOException
    {
	ByteArrayOutputStream chunks = new ByteArrayOutputStream(8192);
	int size = 0;

	contentLength = 0;
	while ((size = reply.getChunkSize(in)) > 0)
	{
	    contentLength += size;
	    copy(in, chunks, size, true);
	    reply.readLine(in);
	}
	reply.getChunkedFooter(in);

	reply.removeHeaderField("Transfer-Encoding");
	reply.setHeaderField("Content-length", contentLength);

	return new ByteArrayInputStream(chunks.toByteArray());
    }

private static DateFormat httpDateFormat() {
        DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        return httpDateFormat;
    }

void disableReplyCaching() {
        reply.removeHeaderField("Cache-Control");
        reply.removeHeaderField("Last-Modified");
        reply.removeHeaderField("Expires");
        reply.removeHeaderField("Date");
        reply.removeHeaderField("ETag");
        reply.removeHeaderField("Pragma");

        reply.setHeaderField("Pragma", "no-cache");
        reply.setHeaderField("Cache-Control", "no-cache, must-revalidate");
        Calendar now = Calendar.getInstance();
        reply.setHeaderField("Expires", httpDateFormat().format(now.getTime()));//"Sat, 26 Jul 1997 05:00:00 GMT");
        reply.setHeaderField("Last-Modified", "Sat, 26 Jul 1997 05:00:00 GMT");
    }

void processContent(boolean uncompress) throws IOException
    {
	InputStream in;
	boolean chunked = false;

	if (reply.containsHeaderField("Transfer-Encoding")
	    && reply.getTransferEncoding().equals("chunked"))
	{
	    in = readChunkedTransfer(reply.getContent());
	    chunked = true;
	}
	else
	{
	    in = reply.getContent();
	}

	if (in == null)
	{

	    return;
	}
	else if (uncompress)
	{
	    in = new GZIPInputStream(in);
	}

	String url = request.getURL();
        boolean replaced = false;
        for (Replacement r : replacements) {
            if (r.matches(url)) {
                r.lastAccess = Calendar.getInstance();
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                try {
                    FileInputStream fis = new FileInputStream(r.targetFile);
                    byte buf[] = new byte[2048];
                    int pos = 0;
                    while ((pos = fis.read(buf)) > 0) {
                        buffer.write(buf, 0, pos);
                    }
                    fis.close();
                    buffer.close();
                } catch (IOException ex) {
                    
                }
                byte bytes[] = buffer.toByteArray();
                contentLength = bytes.length;
                reply.setHeaderField("Content-length", contentLength);
                disableReplyCaching();
                client.write(reply);
                copy(new ByteArrayInputStream(bytes),
                        client.getOutputStream(), contentLength, false);
                replaced = true;
                if (replacedListener != null) {
                    replacedListener.replaced(r, request.getURL(), reply.getContentType());
                }
                break;
            }
        }

        if (!replaced) {

            String contentType = reply.getHeaderField("Content-type");
            if (reply.getStatusCode() == 200) {
                if (contentType != null) {
                    for (String ct : catchedContentTypes) {
                        String convContentType = contentType;
                        if (convContentType.contains(";"))
                            convContentType = convContentType.substring(0, convContentType.indexOf(";"));

                        if (ct.toLowerCase().equals(convContentType.toLowerCase())) {
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            copy(in, baos, contentLength, true);
                            if (catchedListener != null) {
                                catchedListener.catched(ct, request.getURL(), new ByteArrayInputStream(baos.toByteArray()));
                            }
                            in = new ByteArrayInputStream(baos.toByteArray());
                            disableReplyCaching();
                            break;
                        }
                    }
                }
            }
            client.write(reply);
            copy(in, client.getOutputStream(), contentLength, true);
        }
    }



    /**
     * Return the content length.
     */
    int getTotalBytes()
    {
	return contentLength > 0 ? contentLength : 0;
    }

    /**
     * Return the number of bytes read so far.
     */
    int getCurrentBytes()
    {
	return currentLength > 0 ? currentLength : 0;
    }

    /**
     * Send a error message to the client.
     *
     * @param out client
     * @param e exception that occurred
     * @param r request
     */
    void error(OutputStream out, Exception e, Request r)
    {
	StringBuffer buf = new StringBuffer();
	buf.append("While trying to retrieve the URL: <a href=\""+r.getURL()+"\">"+r.getURL()+"</a>\r\n");
	buf.append("<p>\r\nThe following error was encountered:\r\n<p>\r\n");
	buf.append("<ul><li>" + e.toString() + "</ul>\r\n");
	String s = new HttpError(400, buf.toString()).toString();
	try
	{
	    out.write(s.getBytes(), 0, s.length());
	    out.flush();
	}
	catch (Exception ex)
	{
	}
    }

    /**
     * Copy in to out.
     *
     * @param in InputStream
     * @param out OutputStream
     * @param monitored Update the Monitor
     */
    void copy(InputStream in, OutputStream out, int length, boolean monitored)
	throws IOException
    {
	if (length == 0)
	{
	    return;
	}

	int n;
	byte buffer[] = new byte[8192];
	long start = System.currentTimeMillis();
	long now = 0, then = start;
	
	bytesPerSecond = 0;

	if (monitored)
	{
	    currentLength = 0;
	}
	
	for (;;)
	{
	    n = (length > 0) ? Math.min(length, buffer.length) : buffer.length;
	    n = in.read(buffer, 0, n);
	    if (n < 0)
	    {
		break;
	    }
	    
	    out.write(buffer, 0, n);

	    if (monitored)
	    {
		currentLength += n;

	    }

	    now = System.currentTimeMillis();
	    bytesPerSecond = currentLength / ((now - start) / 1000.0);

	    // flush after 1 second
	    if (now - then > 1000)
	    {
		out.flush();
	    }

	    if (length != -1)
	    {
		length -= n;
		if (length == 0)
		{
		    break;
		}
	    }

	    then = now;
	}

	out.flush();

    }

    /**
     * Copy in to out.
     *
     * @param in InputStream
     * @param out OutputStream
     * @param monitored Update the Monitor
     */
    void flushCopy(InputStream in, OutputStream out, int length, boolean monitored)
	throws IOException
    {
	if (length == 0)
	{
	    return;
	}

	int n;
	byte buffer[] = new byte[8192];
	long start = System.currentTimeMillis();
	bytesPerSecond = 0;
	
	if (monitored)
	{
	    currentLength = 0;
	}
	
	for (;;)
	{
	    n = (length > 0) ? Math.min(length, buffer.length) : buffer.length;
	    n = in.read(buffer, 0, n);
	    if (n < 0)
	    {
		break;
	    }

	    out.write(buffer, 0, n);
	    out.flush();
	    if (monitored)
	    {
		currentLength += n;

	    }
	    bytesPerSecond = currentLength / ((System.currentTimeMillis() - start) / 1000.0);
	    if (length != -1)
	    {
		length -= n;
		if (length == 0)
		{
		    break;
		}
	    }
	}
	out.flush();

    }


    /**
     * Return a string represenation of the hander's state.
     */
    public String toString()
    {
	StringBuffer str = new StringBuffer();
	str.append("CLIENT ");
	str.append(socket.getInetAddress().getHostAddress());
	str.append(":");
	str.append(socket.getPort());
	str.append(" - ");
	if (request == null)
	{
	    str.append("idle " + ((System.currentTimeMillis() - idle) / 1000.0) + " sec");
	}
	else
	{
	    if (reply != null && currentLength > 0)
	    {
		str.append("(");
		str.append(currentLength);
		if (contentLength > 0)
		{
		    str.append("/");
		    str.append(contentLength);
		}
		str.append(" ");
		str.append(((int)bytesPerSecond / 1024) + " kB/s");
		str.append(") ");
	    }
	    str.append(request.getURL());
	}
	return str.toString();
    }
}
