package org.bench4q.recorder.httpcapture;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

import org.apache.commons.httpclient.ChunkedOutputStream;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.log4j.Logger;
import org.bench4q.recorder.httpcapture.Utils.SilentException;

public class RequestHandler implements Runnable {
	private static final Logger log = Logger.getLogger(RequestHandler.class);
	private Config config = Config.getConfig();
	/**
	 * request from browser
	 */
	private InputStream clientIn;
	/**
	 * response from remote server
	 */
	private InputStream serverIn;
	/**
	 * send response from proxy to browser
	 */
	private OutputStream clientOut;
	/**
	 * send request from proxy to remote server
	 */
	private OutputStream serverOut;
	private HttpRequestHeader header;
	private ProxyServer proxyServer;
	private Socket clientSocket;
	private HttpURLConnection serverConnection;
	private ByteArrayOutputStream buffer;
	private static Object mutex = new Object();

	public RequestHandler(ProxyServer proxyServer, Socket s) {
		assert (s != null);
		this.clientSocket = s;
		this.buffer = new ByteArrayOutputStream();
		this.proxyServer = proxyServer;
	}

	private void initClientServerConnections(Socket s) throws Throwable {
		this.clientIn = new BufferedInputStream(s.getInputStream());
		this.clientOut = s.getOutputStream();
		try {
			this.header = new HttpRequestHeader(this.clientIn);
		} catch (IOException e) {
			if (this.header != null) {
				log.error("truncated request from browser: " + this.header.url,
						e);
				System.out.println(this.header.url);
			}
			throw new Utils.SilentException();
		}
		if (!this.header.version.startsWith("HTTP"))
			throw new Utils.UserException(
					"Bench4Q only supports the HTTP protocol.");
		URL url = new URL(this.header.url);
		System.out.println(this.header.url);
		Config.ProxySettings proxy = this.config.getProxySettings();
		int port;
		String host;
		if (proxy != null) {
			host = proxy.host;
			port = proxy.port;
		} else {
			host = url.getHost();
			port = url.getPort();
		}

		if (port < 1)
			port = 80;
		serverConnection = (HttpURLConnection) url.openConnection();

		serverConnection.addRequestProperty("host", host);
		serverConnection.addRequestProperty("port", Integer.toString(port));
	}

	private String stripProxyInfoFromRequestHeader()
			throws MalformedURLException {
		String res = "";
		String origUrl = this.header.url;
		URL url = new URL("https://" + origUrl);
		this.header.url = url.getFile();
		res = this.header.toString();
		this.header.url = origUrl;
		return res;
	}

	public void run() {
		try {
			try {
				int rs = this.clientSocket.getReceiveBufferSize();
				int ss = this.clientSocket.getSendBufferSize();
				int BUF_SIZE = rs < ss ? ss : rs;

				byte[] buf = new byte[BUF_SIZE];
				try {
					initClientServerConnections(this.clientSocket);
				} catch (SilentException e) {
					return;
				}

				this.header.buildRequestHeader(serverConnection);
				log.trace("set request header");
				byte[] requestBody;
				if (this.header.contentLength > 0) {
					serverConnection.setDoOutput(true);
					this.serverOut = serverConnection.getOutputStream();
					this.buffer.reset();
					int len = 0;
					int num = 0;
					while (num < this.header.contentLength) {
						try {
							len = readRequestBody(buf);
						} catch (SocketException e) {
							log.info("truncated request from browser: "
									+ e.getMessage());
							throw new Utils.SilentException();
						}
						if (len == 0 || len == -1) {
							System.out.println("Read " + num
									+ "byte from requestBody");
							break;
						}
						log.trace("read " + Integer.toString(len) + " bytes");
						this.serverOut.write(buf, 0, len);
						this.buffer.write(buf, 0, len);
						log.trace("wrote " + Integer.toString(len) + " bytes");
						num += len;
					}
					// close send request to remote server
					serverOut.flush();
					serverOut.close();
					requestBody = this.buffer.toByteArray();
					log.trace("transferred rest of request body");
				} else {
					requestBody = new byte[0];
					log.trace("no request body");
				}

				this.clientSocket.shutdownInput();

				this.serverIn = serverConnection.getInputStream();
				synchronized (mutex) {
					// this.proxyServer.processRequest(this.header,
					// requestBody);
					// log.trace("processed request");

					this.buffer.reset();
					int len;
					while ((len = this.serverIn.read(buf, 0, buf.length)) >= 0) {
						log.trace("read " + Integer.toString(len));
						// this.clientOut.write(buf, 0, len);
						this.buffer.write(buf, 0, len);
					}
					log.trace("transferred response len:" + len);

					ResponseModel responseModel = new ResponseModel(
							this.buffer.toByteArray(), serverConnection);
					this.proxyServer.processResponse(this.header, requestBody,
							responseModel);
					try {
						this.clientOut.write(changeHeaderToByte(
								serverConnection.getHeaderFields(),
								responseModel));

						this.clientOut.write(responseModel.getResponse());
					} catch (SocketException e) {
						log.info("browser stopped listening: " + e.getMessage()
								+ this.buffer.toString());
						throw new Utils.SilentException();
					}
					log.trace("processed response");
				}

			} catch (Exception e) {
				log.error(e, e);
			} finally {
				this.clientSocket.close();
				log.trace("closed client socket");
			}

			this.clientSocket.close();
			log.trace("closed client socket");
		} catch (Throwable localThrowable) {
			log.error(localThrowable, localThrowable);
		}
	}

	private byte[] changeHeaderToByte(Map<String, List<String>> headerMap,
			ResponseModel responseModel) {
		StringBuilder sb = new StringBuilder();

		for (String key : headerMap.keySet()) {
			if (key == null) {
				sb.append(changeList2String(headerMap.get(key)) + "\r\n");
				continue;
			}
			if(key.equals("Content-Length") && headerMap.get(key).get(0).equals("-1")){
				sb.append("Content-Length: "+responseModel.getResponse().length +"\r\n");
				continue;
			}
			if (key.equals("Transfer-Encoding") && headerMap.get(key).get(0).equals("chunked")) {
				responseModel.setResponse(buildResponseBodyToChunked(responseModel.getResponse()));
			}
			sb.append(key + ": " + changeList2String(headerMap.get(key))
					+ "\r\n");
		}
		sb.append("\r\n");
		return sb.toString().getBytes();
	}

	private byte[] buildResponseBodyToChunked(byte[] responseBytes) {
		ChunkedOutputStream chunkedOutputStream = null;
		try {
			ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
			chunkedOutputStream = new ChunkedOutputStream(byteArrayOutputStream);
			chunkedOutputStream.write(responseBytes,0,responseBytes.length);
			chunkedOutputStream.finish();
			chunkedOutputStream.flush();
			
			return byteArrayOutputStream.toByteArray();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally{
			if(chunkedOutputStream != null){
				try {
					chunkedOutputStream.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		return responseBytes;
	}
	private String changeList2String(List<String> headers) {
		if (headers == null)
			return "";
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < headers.size(); i++) {
			sb.append(headers.get(i));
		}
		return sb.toString();
	}

	private int readRequestBody(byte[] buf) throws IOException {
		int alreadyReadLength = 0;
		int ch = -1;
		while (alreadyReadLength < this.header.contentLength) {
			if ((ch = this.clientIn.read()) != -1) {
				buf[alreadyReadLength] = (byte) ch;
			}
			alreadyReadLength++;
		}
		return alreadyReadLength;
	}
}
