/*
 * Author Coderfengyun
 * 
 * And th parmas has some special split.
 * Like, QueryParams and bodyParams should be split by ';'
 * And Headers should be split by '|;'
 */
package org.bench4q.agent.plugin.basic.http;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.log4j.Logger;
import org.bench4q.agent.plugin.ParameterBarn;
import org.bench4q.agent.plugin.Constructor;
import org.bench4q.agent.plugin.Parameter;
import org.bench4q.agent.plugin.Plugin;
import org.bench4q.agent.plugin.basic.http.BatchRequest.BatchItem;
import org.bench4q.agent.plugin.behavior.Behavior;
import org.bench4q.agent.plugin.behavior.BehaviorType;
import org.bench4q.agent.utils.ParameterParser;
import org.bench4q.agent.utils.Type.SupportTypes;
import org.bench4q.agent.utils.types.Table;
import com.google.gson.Gson;

@Plugin("Http")
public class HttpPlugin extends ParameterBarn {
	private HttpClient httpClient;
	private Map<String, String> variables = new HashMap<String, String>();

	public HttpClient getHttpClient() {
		return httpClient;
	}

	private void setHttpClient(HttpClient httpClient) {
		this.httpClient = httpClient;
	}

	@Constructor
	public HttpPlugin(
			@Parameter(type = SupportTypes.Field, value = "testId") String testId) {
		super(testId);
		this.setHttpClient(new HttpClient());
	}

	void setHeaders(HttpMethod method, String headers) {
		if (headers != null) {
			List<NameValuePair> nvPairs = parseHeaders(headers);
			for (NameValuePair nv : nvPairs) {
				method.addRequestHeader(nv.getName(), nv.getValue());
			}
		}
	}

	List<NameValuePair> parseHeaders(String value) {
		List<NameValuePair> nvPairs = new LinkedList<NameValuePair>();
		Table headerTable = ParameterParser.buildTable(value,
				new LinkedList<String>() {
					private static final long serialVersionUID = 3498984411571605459L;
					{
						add("header");
						add("value");
					}
				});
		for (List<String> entry : headerTable.getRealContent()) {
			if (entry.size() != headerTable.getColumnCount()) {
				continue;
			}
			nvPairs.add(new NameValuePair(entry.get(0).trim(), entry.get(1)
					.trim()));
		}
		return nvPairs;
	}

	private NameValuePair[] setInputParameters(List<String> inputParameters) {
		Set<NameValuePair> res = new HashSet<NameValuePair>();
		Iterator<String> paramIter = inputParameters.iterator();

		while (paramIter.hasNext()) {
			String token = paramIter.next();
			int index = token.indexOf('=');
			res.add(new NameValuePair(token.substring(0, index), token
					.substring(index + 1)));
		}
		return res.toArray(new NameValuePair[res.size()]);
	}

	private static String completeUri(String uri) {
		String scheme = uri.substring(0, 5);
		if (!scheme.equals("http:") && !scheme.equals("https"))
			uri = "http://" + uri;
		return uri;
	}

	@Behavior(value = "Get", type = BehaviorType.USER_BEHAVIOR)
	public List<HttpReturn> batchRequest(
			@Parameter(value = "requests", type = SupportTypes.Table) String requests) {
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		List<HttpReturn> result = new LinkedList<HttpReturn>();
		try {
			Gson gson = new Gson();
			BatchRequest batchRequest = gson.fromJson(requests,
					BatchRequest.class);
			List<Future<HttpReturn>> futures = new LinkedList<Future<HttpReturn>>();
			for (BatchItem item : batchRequest.getBatchItems()) {
				Callable<HttpReturn> task = buildTasks(item);
				if (task == null) {
					continue;
				}
				futures.add(executorService.submit(task));
			}
			for (Future<HttpReturn> future : futures) {
				try {
					result.add(future.get());
				} catch (Exception e) {
					Logger.getLogger(HttpPlugin.class).error(e, e);
					result.add(new HttpReturn(false, 404, 0, ""));
				}
			}
			return result;
		} finally {
			executorService.shutdownNow();
		}

	}

	private Callable<HttpReturn> buildTasks(final BatchItem item) {
		// switch (item.getMethod().toLowerCase()) {
		// case "get":
		// return new Callable<HttpReturn>() {
		// @Override
		// public HttpReturn call() throws Exception {
		// return get(item.getUrl(), item.getQueryParams(),
		// item.getHeaders(),
		// item.getRespVarsToSaveInSession());
		// }
		// };
		// case "post":
		// return new Callable<HttpReturn>() {
		// @Override
		// public HttpReturn call() throws Exception {
		// return post(item.getUrl(), item.getQueryParams(),
		// item.getHeaders(), item.getBodyContent(),
		// item.getBodyparameters(),
		// item.getRespVarsToSaveInSession());
		// }
		// };
		// case "put":
		// return new Callable<HttpReturn>() {
		// @Override
		// public HttpReturn call() throws Exception {
		// return put(item.getUrl(), item.getQueryParams(),
		// item.getHeaders(), item.getBodyContent(),
		// item.getBodyparameters(),
		// item.getRespVarsToSaveInSession());
		// }
		// };
		// case "delete":
		// return new Callable<HttpReturn>() {
		// @Override
		// public HttpReturn call() throws Exception {
		// return delete(item.getUrl(), item.getQueryParams(),
		// item.getHeaders(),
		// item.getRespVarsToSaveInSession());
		// }
		// };
		// default:
		// return null;
		// }
		return null;
	}

	/**
	 * 
	 * @param url
	 * @param queryParams
	 *            (NField) QueryParams should be split by ';'
	 * @param headers
	 *            (Table)has two columns, such as header and value
	 * 
	 * @param respVarsToSaveInSession
	 *            (Table) has four columns, such as varName,
	 *            varRegularExpression, leftBoundry, and rightBoundry
	 * @return
	 */
	@Behavior(value = "Get", type = BehaviorType.USER_BEHAVIOR)
	public HttpReturn get(
			@Parameter(value = "url", type = SupportTypes.Field) String url,
			@Parameter(value = "queryParams", type = SupportTypes.NField) String queryParams,
			@Parameter(value = "headers", type = SupportTypes.Table) String headers,
			@Parameter(value = "respVarsToSaveInSession", type = SupportTypes.Table) String respVarsToSaveInSession) {

		GetMethod method = new GetMethod(completeUri(url));
		if (isValid(queryParams)) {
			method.setQueryString(setInputParameters(ParameterParser
					.buildNField(queryParams)));
		}
		setHeaders(method, headers);
		method.getParams().makeLenient();
		method.setFollowRedirects(false);
		return excuteMethod(method, respVarsToSaveInSession);
	}

	private HttpReturn excuteMethod(HttpMethod method,
			String respVarsToSaveInSession) {
		int responseCode = -1;
		long contentLength = 0;
		String contentType = "";
		try {
			System.out.println(method.getURI().toString());
			responseCode = this.getHttpClient().executeMethod(method);
			method.getStatusLine().toString();
			Header[] responseHeaders = method.getResponseHeaders();
			if (method.getResponseHeader("Content-Length") != null) {
				contentLength = Long.parseLong(method.getResponseHeader(
						"Content-Length").getValue());
			}
			if (method.getResponseHeader("Content-Type") != null) {
				contentType = method.getResponseHeader("Content-Type")
						.getValue();
			}
			if (isValid(respVarsToSaveInSession)) {
				doExtractResponseVariablesAndSave(respVarsToSaveInSession,
						method.getResponseBodyAsString());
			}
			return new HttpReturn((responseCode % 100) < 4, responseCode,
					method.getURI().toString(), contentLength, contentType,
					responseHeaders);
		} catch (HttpException e) {
			Logger.getLogger(HttpPlugin.class).error(e, e);
			return new HttpReturn(false, 400, contentLength, contentType);
		} catch (IOException e) {
			Logger.getLogger(HttpPlugin.class).error(e, e);
			return new HttpReturn(false, 400, contentLength, contentType);
		} finally {
			method.releaseConnection();
		}
	}

	private Map<String, String> doExtractResponseVariablesAndSave(
			String respVarsToSaveInSession, String responseBodyAsString) {
		Map<String, String> keyValues = new LinkedHashMap<String, String>();
		List<List<String>> varExtractExpressionList = ParameterParser
				.buildTable(
						respVarsToSaveInSession,
						Arrays.asList("varName", "varRegularExpression",
								"leftBoundry", "rightBoundry"))
				.getRealContent();
		for (List<String> row : varExtractExpressionList) {
			keyValues.putAll(doExtractParamByRow(row, responseBodyAsString));
		}
		this.variables.putAll(keyValues);
		return keyValues;
	}

	private Map<String, String> doExtractParamByRow(List<String> row,
			String responseBody) {
		Map<String, String> result = new LinkedHashMap<String, String>();
		if (row.size() <= 3) {
			return result;
		}
		String varName = row.get(0);
		String varExpression = row.get(1);
		String varLeftBoundry = row.get(2);
		String varRightBoundry = row.get(3);

		if (!isValid(varExpression)) {
			result.put(
					varName,
					extractExactlyValueWith(varLeftBoundry, varRightBoundry,
							responseBody));
			return result;
		}

		Matcher matcher = Pattern.compile(varExpression).matcher(responseBody);
		if (!matcher.find()) {
			result.put(
					varName,
					extractExactlyValueWith(varLeftBoundry, varRightBoundry,
							responseBody));
			return result;
		}
		result.put(
				varName,
				extractExactlyValueWith(varLeftBoundry, varRightBoundry,
						matcher.group()));
		return result;
	}

	private String extractExactlyValueWith(String varLeftBoundry,
			String varRightBoundry, String matchedVariable) {
		int indexOfLB = matchedVariable.indexOf(varLeftBoundry);
		int indexOfRB = matchedVariable.indexOf(varRightBoundry);
		return matchedVariable.substring(indexOfLB + varLeftBoundry.length(),
				indexOfRB);
	}

	/**
	 * 
	 * @param url
	 * @param queryParams
	 * @param headers
	 * @param bodyContent
	 *            , when the Content-Length in header G.T. zero, then use this
	 * @param bodyParams
	 *            , else use this
	 * @return
	 */
	@Behavior(value = "Post", type = BehaviorType.USER_BEHAVIOR)
	public HttpReturn post(
			@Parameter(value = "url", type = SupportTypes.Field) String url,
			@Parameter(value = "queryParams", type = SupportTypes.NField) String queryParams,
			@Parameter(value = "headers", type = SupportTypes.Table) String headers,
			@Parameter(value = "bodyContent", type = SupportTypes.Field) String bodyContent,
			@Parameter(value = "bodyparameters", type = SupportTypes.NField) String bodyParams,
			@Parameter(value = "respVarsToSaveInSession", type = SupportTypes.Table) String respVarToSaveInSession) {
		PostMethod method = new PostMethod(completeUri(url));
		setHeaders(method, headers);
		if (isValid(queryParams)) {
			method.setQueryString(setInputParameters(ParameterParser
					.buildNField(queryParams)));
		}
		if (isValid(bodyParams)) {
			method.setRequestBody(setInputParameters(ParameterParser
					.buildNField(bodyParams)));
		}
		String contentType = getMethodContentType(method);
		if (isValid(bodyContent) && isValid(contentType)) {
			try {
				method.setRequestEntity(new StringRequestEntity(bodyContent,
						contentType, null));
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		method.getParams().makeLenient();
		method.setFollowRedirects(false);
		return excuteMethod(method, respVarToSaveInSession);
	}

	private String getMethodContentType(HttpMethod method) {
		Header contentTypeHeader = method.getRequestHeader("Content-Type");
		String contentType = null;
		if (contentTypeHeader != null
				&& !contentTypeHeader.toExternalForm().isEmpty()) {
			contentType = contentTypeHeader.toExternalForm();
		}
		return contentType;
	}

	private boolean isValid(String content) {
		return content != null && content.length() != 0;
	}

	@Behavior(value = "Put", type = BehaviorType.USER_BEHAVIOR)
	public HttpReturn put(
			@Parameter(value = "url", type = SupportTypes.Field) String url,
			@Parameter(value = "queryParams", type = SupportTypes.NField) String queryParams,
			@Parameter(value = "headers", type = SupportTypes.Table) String headers,
			@Parameter(value = "bodyContent", type = SupportTypes.Field) String bodyContent,
			@Parameter(value = "bodyparameters", type = SupportTypes.NField) String bodyParams,
			@Parameter(value = "respVarsToSaveInSession", type = SupportTypes.Table) String respVarToSaveInSession) {
		PutMethod method = new PutMethod(completeUri(url));
		setHeaders(method, headers);
		if (isValid(queryParams)) {
			method.setQueryString(setInputParameters(ParameterParser
					.buildNField(queryParams)));
		}
		String contentType = getMethodContentType(method);
		if (isValid(bodyContent) && isValid(contentType)) {
			try {
				method.setRequestEntity(new StringRequestEntity(bodyContent,
						contentType, null));
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		}
		method.getParams().makeLenient();
		return excuteMethod(method, respVarToSaveInSession);
	}

	@Behavior(value = "Delete", type = BehaviorType.USER_BEHAVIOR)
	public HttpReturn delete(
			@Parameter(value = "url", type = SupportTypes.Field) String url,
			@Parameter(value = "queryParams", type = SupportTypes.NField) String queryParams,
			@Parameter(value = "headers", type = SupportTypes.Table) String headers,
			@Parameter(value = "respVarsToSaveInSession", type = SupportTypes.Table) String respVarToSaveInSession) {
		DeleteMethod method = new DeleteMethod(completeUri(url));
		setHeaders(method, headers);
		if (isValid(queryParams)) {
			method.setQueryString(setInputParameters(ParameterParser
					.buildNField(queryParams)));
		}
		method.getParams().makeLenient();
		return excuteMethod(method, respVarToSaveInSession);
	}

	/**
	 * Implement the BaseParameterization's getValue
	 */
	@Override
	public String getValue(String var) {
		String result = this.variables.get(var);
		return result == null ? "" : result;
	}
}
