/**
 * 
 */
package org.richfaces.test.staging;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.richfaces.test.TestException;

/**
 * This class represent single connection ( request ) to the virtual server. These instance should not be created directly, but by the {@link StagingServer#getConnection(URL)}.
 * method only.
 * Since instance have been created, additional request parameters and headers can be set.
 * @author asmirnov
 * 
 */
public class StagingConnection {

	private static final Logger log = ServerLogger.SERVER.getLogger();

	private final StagingServer server;

	final URL url;

	private ConnectionRequest request;

	private ConnectionResponse response;

	private final RequestChain servlet;

	private HttpMethod method = HttpMethod.GET;

	private static final Cookie[] COOKIE = new Cookie[] {};

	private List<Cookie> cookies = new ArrayList<Cookie>();

	private Map<String, String[]> requestParameters = new HashMap<String, String[]>();

	private final String pathInfo;

	private final String servletPath;

	private boolean finished = false;

	private boolean started = false;

	private String queryString;

	private HttpServletRequest requestProxy;

	private HttpServletResponse responseProxy;

	/**
	 * Create connection instance.
	 * @param localServer virtual server instance.
	 * @param url request URL.
	 */
	StagingConnection(StagingServer localServer, URL url) {
		this.server = localServer;
		this.url = url;
		// TODO - context path support.
		String path = url.getPath();
		servlet = localServer.getServlet(path);
		if (null == servlet) {
			throw new IllegalArgumentException();
		}
		this.pathInfo = servlet.getPathInfo(path);
		this.servletPath = servlet.getServletPath(path);
		this.request = new ConnectionRequest();
		this.response = new ConnectionResponse();
		this.request.setAttribute("javax.servlet.include.path_info",
				this.pathInfo);
		this.request.setAttribute("javax.servlet.include.servlet_path",
				this.servletPath);
		setQueryString(url.getQuery());
		if (null != getQueryString()) {
			parseFormParameters(queryString);
		}
		
		// Create proxy objects.
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if(null == loader){
			loader = this.getClass().getClassLoader();
		}
		requestProxy = (HttpServletRequest) Proxy.newProxyInstance(loader, new Class[]{HttpServletRequest.class}, server.getInvocationHandler(request));
		responseProxy = (HttpServletResponse) Proxy.newProxyInstance(loader, new Class[]{HttpServletResponse.class}, server.getInvocationHandler(response));
	}

	/**
	 * Parse 'application/x-www-form-urlencoded' string with parameters name/value pairs,
	 * as it expected after a form submit.
	 * @param queryString URL query string or POST content.
	 */
	public void parseFormParameters(String queryString) {
		String[] queryParams = queryString.split("&");
		for (int i = 0; i < queryParams.length; i++) {
			try {
				String par = queryParams[i];
				int eqIndex = par.indexOf('=');
				if (eqIndex >= 0) {
					// decode url-decoded values.
					String name = URLDecoder.decode(par.substring(0, eqIndex),
							request.getCharacterEncoding());
					String value = URLDecoder.decode(
							par.substring(eqIndex + 1), request
									.getCharacterEncoding());
					addRequestParameter(name, value);
				} else {
					addRequestParameter(URLDecoder.decode(par, request
							.getCharacterEncoding()), null);
				}
			} catch (UnsupportedEncodingException e) {
				throw new TestException(e);
			}
		}
	}

	/**
	 * @return the finished
	 */
	public boolean isFinished() {
		return finished;
	}

	/**
	 * @return the started
	 */
	public boolean isStarted() {
		return started;
	}

//	private void checkStarted() {
//		if (!isFinished()) {
//			throw new TestException("request have not been started");
//		}
//	}

//	private void checkNotStarted() {
//		if (isStarted()) {
//			throw new TestException("request was started, no parameters changes allowed");
//		}
//	}

	/**
	 * Execute this connection request on the associated servlet or filter chain.
	 * @throws TestException if any errors were during execution.
	 */
	public void execute() {
		if (isStarted() || isFinished()) {
			throw new TestException(
					"request have already been executed");
		}
		start();
		try {
			this.servlet.execute(request, response);
		} catch (ServletException e) {
			throw new TestException("Error execute request ",e);
		} catch (IOException e) {
			throw new TestException("IO Error during request execution",e);
		} finally {
			finish();
		}
	}

	/**
	 * Finish request to the this connection, inform server listeners about request status.
	 */
	public void finish() {
		server.requestFinished(request);
		finished = true;
	}

	/**
	 * Start request to the this connection, inform server listeners about request status.
	 * No request parameters changes allowed after connection start.
	 */
	public void start() {
		log.fine("start " + getRequestMethod() + " request processing for file "
				+ url.getFile());
		log.fine("request parameters: " + requestParameters);
		server.requestStarted(request);
		started = true;
	}

	/**
	 * Get request HTTP methos ( GET, POST etc ).
	 * @return the method
	 */
	public HttpMethod getRequestMethod() {
		return method;
	}

	/**
	 * Set request HTTP methos ( GET, POST etc ).
	 * @param method
	 *            the method to set
	 */
	public void setRequestMethod(HttpMethod method) {
//		checkNotStarted();
		this.method = method;
	}

	/**
	 * Get request url.
	 * @return the url
	 */
	public URL getUrl() {
		return url;
	}

	/**
	 * Append additional request parameter.
	 * @param name
	 * @param value
	 */
	public void addRequestParameter(String name, String value) {
//		checkNotStarted();
		String[] values = requestParameters.get(name);
		if (null == values) {
			values = new String[1];
		} else {
			String[] newValues = new String[values.length + 1];
			System.arraycopy(values, 0, newValues, 0, values.length);
			values = newValues;
		}
		values[values.length - 1] = value;
		requestParameters.put(name, values);
	}

	/**
	 * Get content of the response as String. 
	 * @return content of the response writer or String created from the ServletOutputStream with current response encoding.
	 * @throws TestException
	 * 	          if has an unsupported encoding.
	 */
	public String getContentAsString() {
//		checkStarted();
		String content = response.getWriterContent();
		if (null == content) {
			byte[] streamContent = response.getStreamContent();
			if (null != streamContent) {
				String encoding = response.getCharacterEncoding();
				if (null != encoding) {
					try {
						content = new String(streamContent, encoding);
					} catch (UnsupportedEncodingException e) {
						throw new TestException(e);
					}
				} else {
					content = new String(streamContent);
				}
			}
		}
		return content;
	}

	/**
	 * Get content of the response as byte array.
	 * @return content of the ServletOutputStream or convert String, collected by response writer, with current response encoding.
	 * @throws TestException
	 * 	          if response has unsupported encoding.
	 */
	public byte[] getResponseBody() {
//		checkStarted();
		byte[] content = response.getStreamContent();
		if (null == content) {
			String writerContent = response.getWriterContent();
			if (null != writerContent) {
				try {
					content = writerContent.getBytes(response
							.getCharacterEncoding());
				} catch (UnsupportedEncodingException e) {
					content = writerContent.getBytes();
				}
			} else {
				content = new byte[0];
			}
		}
		return content;
	}

	/**
	 * List of the {@link Cookie} used by the request or response ( There are same cookies for both request and response ). 
	 * @return the cookies
	 */
	public List<Cookie> getCookies() {
		return cookies;
	}

	/**
	 * request object for the this connection.
	 * @return the request
	 */
	public HttpServletRequest getRequest() {
		return requestProxy;
	}

	/**
	 * response object for the this connection.
	 * @return the response
	 */
	public HttpServletResponse getResponse() {
		return responseProxy;
	}

	/**
	 * @return encoding used to write response.
	 */
	public String getResponseCharacterEncoding() {
//		checkStarted();
		return response.getCharacterEncoding();
	}

	/**
	 * @return content type ( eg 'text/html' ) of the response.
	 */
	public String getResponseContentType() {
//		checkStarted();
		return response.getContentType();
	}

	/**
	 * @return HTTP status code of the response.
	 */
	public int getResponseStatus() {
//		checkStarted();
		return response.getStatus();
	}

	/**
	 * @return HTTP error message.
	 */
	public String getErrorMessage() {
//		checkStarted();
		return response.getErrorMessage();
	}

	/**
	 * Set request Query string. This method does not parse query string, {@link #parseFormParameters(String)} should be used.
	 * @param queryString
	 *            the queryString to set
	 */
	public void setQueryString(String queryString) {
//		checkNotStarted();
		this.queryString = queryString;
	}

	/**
	 * @return the queryString
	 */
	public String getQueryString() {
		return queryString;
	}

	/**
	 * Get HTTP response headers.
	 * @return headers name-values map.
	 */
	public Map<String, String[]> getResponseHeaders() {
//		checkStarted();
		return response.getHeaders();
	}

	/**
	 * Set charset for the request body.
	 * @param charset
	 * @throws UnsupportedEncodingException
	 */
	public void setRequestCharacterEncoding(String charset) throws UnsupportedEncodingException {
//		checkNotStarted();
		request.setCharacterEncoding(charset);		
	}

	/**
	 * Set HTTP POST/PUT methods uploading content.
	 * @param body
	 */
	public void setRequestBody(String body) {
//		checkNotStarted();
		request.setRequestBody(body);		
	}

	/**
	 * Set HTTP request content type ( eg 'application/x-www-form-urlencoded' or 'text/xml' ).
	 * @param contentType
	 */
	public void setRequestContentType(String contentType) {
//		checkNotStarted();
		request.setContentType(contentType);
		
	}

	/**
	 * Append additional HTTP request headers.
	 * @param headers
	 */
	public void addRequestHeaders(Map<String, String> headers) {
//		checkNotStarted();
		request.addHeaders(headers);		
	}

	private class ConnectionRequest extends StagingHttpRequest {
	
		public Cookie[] getCookies() {
			return cookies.toArray(COOKIE);
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getMethod()
		 */
		public String getMethod() {
			return method.toString();
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getServletPath()
		 */
		public String getServletPath() {
			return servletPath;
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getPathInfo()
		 */
		public String getPathInfo() {
			return pathInfo;
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getQueryString()
		 */
		public String getQueryString() {
			return queryString;
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getRequestURI()
		 */
		public String getRequestURI() {
			return url.getPath();
		}
		
		/* (non-Javadoc)
		 * @see org.richfaces.test.staging.StagingHttpRequest#getServerName()
		 */
		@Override
		public String getServerName() {
			return url.getHost();
		}
		
	
		@Override
		public int getLocalPort() {
			int port = url.getPort();
			if(port < 0){
				port = super.getLocalPort();
			}
			return port;
		}
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
		 */
		public String getParameter(String name) {
			String[] values = requestParameters.get(name);
			if (null != values && values.length > 0) {
				return values[0];
			}
			return null;
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.ServletRequest#getParameterMap()
		 */
		@SuppressWarnings("unchecked")
		public Map getParameterMap() {
			return Collections.unmodifiableMap(requestParameters);
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.ServletRequest#getParameterNames()
		 */
		@SuppressWarnings("unchecked")
		public Enumeration getParameterNames() {
			return Collections.enumeration(requestParameters.keySet());
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.servlet.ServletRequest#getParameterValues(java.lang.String)
		 */
		public String[] getParameterValues(String name) {
			return requestParameters.get(name);
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getSession()
		 */
		public HttpSession getSession() {
			return server.getSession();
		}
	
		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpServletRequest#getSession(boolean)
		 */
		public HttpSession getSession(boolean create) {
			return server.getSession(create);
		}
	
		@Override
		public RequestDispatcher getRequestDispatcher(String path) {
			RequestDispatcher dispatcher = null;
			if (!path.startsWith("/")) {
				try {
					URL absoluteUrl = new URL(url, path);
					path = absoluteUrl.getFile();
				} catch (MalformedURLException e) {
					return null;
				}
			}
			final RequestChain dispatchedServlet = server.getServlet(path);
			if (null != dispatchedServlet) {
				dispatcher = new RequestDispatcher() {
	
					public void forward(ServletRequest request,
							ServletResponse response) throws ServletException,
							IOException {
						response.reset();
						dispatchedServlet.execute(request, response);
					}
	
					public void include(ServletRequest request,
							ServletResponse response) throws ServletException,
							IOException {
						dispatchedServlet.execute(request, response);
					}
	
				};
			}
			return dispatcher;
		}
	
		@Override
		protected void attributeAdded(String name, Object o) {
			server.requestAttributeAdded(this, name, o);
	
		}
	
		@Override
		protected void attributeRemoved(String name, Object removed) {
			server.requestAttributeRemoved(this, name, removed);
	
		}
	
		@Override
		protected void attributeReplaced(String name, Object o) {
			server.requestAttributeReplaced(this, name, o);
	
		}
	
	}

	private class ConnectionResponse extends StagingHttpResponse {
		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.servlet.http.HttpServletResponse#addCookie(javax.servlet.http
		 * .Cookie )
		 */
		public void addCookie(Cookie cookie) {
			cookies.add(cookie);
	
		}
	
	}

}
