package org.bench4q.recorder.httpcapture.generator;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bench4q.recorder.httpcapture.Action;
import org.bench4q.recorder.httpcapture.Config;
import org.bench4q.recorder.httpcapture.HeaderValue;
import org.bench4q.recorder.httpcapture.HttpRequestHeader;
import org.bench4q.recorder.httpcapture.HttpTestCase;
import org.bench4q.recorder.httpcapture.IScriptAdapter;
import org.bench4q.recorder.httpcapture.Param;
import org.bench4q.recorder.httpcapture.ProxyServer;
import org.bench4q.recorder.httpcapture.ResponseModel;
import org.bench4q.recorder.httpcapture.Utils;
import org.bench4q.recorder.httpcapture.Utils.UserException;
import org.bench4q.share.models.agent.RunScenarioModel;
import org.bench4q.share.models.agent.scriptrecord.BehaviorModel;

public abstract class AbstractCodeGenerator implements IScriptGenerator,
		ProxyServer.Observer, Runnable {
	private Config config = Config.getConfig();
	private static final Log log;
	private static String cpRspTo;
	private static boolean cpRspToStdout;
	private static boolean cpRspToFile;
	private static final String[] MIME_DEFAULTS;
	static HashMap<String, String> mimeTypes;
	protected static final String END_STATEMENT;
	protected static String jtidyConfigFile;
	protected long assertNumber = 0L;
	protected boolean headersExist = false;
	private IScriptAdapter scriptAdapter;
	private String testName;
	private boolean ignoreNextResponse;
	private Pattern[] namePatterns;
	private LinkedList<BehaviorModel> outstandingInserts = new LinkedList<BehaviorModel>();
	private Thread insertThread;
	private String charset;
	private long timeElapsedSinceLastestRequest;
	private Date latestRequestDate;
	private int requestCount;
	private boolean firstHtmlResponse;

	static final boolean $assertionsDisabled = !(AbstractCodeGenerator.class
			.desiredAssertionStatus());

	private static boolean isCpRspToStdout() {
		return cpRspToStdout;
	}

	private static boolean isCpRspToFile() {
		return cpRspToFile;
	}

	protected int getRequestCount() {
		return requestCount;
	}

	private boolean isFirstHtmlResponse() {
		return firstHtmlResponse;
	}

	private void setFirstHtmlResponse(boolean firstHtmlResponse) {
		this.firstHtmlResponse = firstHtmlResponse;
	}

	public AbstractCodeGenerator(IScriptAdapter adapter, String[] nameRegExps) {
		this.reset();
		this.scriptAdapter = adapter;
		this.namePatterns = new Pattern[nameRegExps.length];
		for (int i = 0; i < nameRegExps.length; ++i)
			this.namePatterns[i] = Pattern.compile(nameRegExps[i], 8);

		this.testName = this.config.getProperty("test.default_testname",
				"Bench4QTest");
		this.insertThread = new Thread(this);
		this.insertThread.start();
	}

	private void reset() {
		this.latestRequestDate = new Date();
		this.assertNumber = 0;
		this.requestCount = 0;
		this.latestRequestDate = new Date();
		this.timeElapsedSinceLastestRequest = 0L;
		this.setFirstHtmlResponse(true);
	}

	public IScriptAdapter getScriptAdapter() {
		return this.scriptAdapter;
	}

	private void setTestName(String testName) {
		this.testName = testName;
	}

	private void setTestPath(String testPath) {
	}

	public long getTimeElapsedSinceLastestRequest() {
		return this.timeElapsedSinceLastestRequest;
	}

	public boolean isFirstRequest() {
		return this.requestCount == 0;
	}

	public String parseTestName() {
		return this.testName;
	}

	public boolean doSave(String path, String fileName) {
		String name = fileName;
		int dotPos = fileName.indexOf(".");
		RunScenarioModel runScenarioModel = this.scriptAdapter
				.getRunScenarioModel();
		FileWriter fileWriter;

		if (dotPos > -1)
			name = fileName.substring(0, dotPos);
		setTestName(name);
		setTestPath(path);

		if (runScenarioModel == null) {
		}

		try {
			fileWriter = new FileWriter(new File(path
					+ System.getProperty("file.separator") + fileName));
			Marshaller marshaller = JAXBContext.newInstance(
					runScenarioModel.getClass()).createMarshaller();
			marshaller.marshal(runScenarioModel, fileWriter);
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

			return true;
		} catch (JAXBException e) {
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}

	}

	public void doLoad() {
		setTestName(parseTestName());
	}

	public void close() {
		this.insertThread.interrupt();
	}

	public void processRequest(HttpRequestHeader header, byte[] requestBody)
			throws Exception {
		// doParseRequest(header, requestBody);
	}

	private void doParseRequest(HttpRequestHeader header, byte[] requestBody, int respCode, String respContentType)
			throws UserException, UnsupportedEncodingException {
		Param[] params;
		int i;
		String name;
		String value;
		Date d = new Date();
		this.timeElapsedSinceLastestRequest = (d.getTime() - this.latestRequestDate
				.getTime());
		this.latestRequestDate = d;

		if ((header.method.toLowerCase().equals("get"))
				&& (urlIgnored(header.url))) {
			log.debug("Ignoring GET request: \"" + header.url + "\"");
			this.ignoreNextResponse = true;
			return;
		}

		boolean isMultiPartRequest = header.contentType
				.startsWith("multipart/form-data");
		boolean isFormRequest = header.contentType
				.startsWith("application/x-www-form-urlencoded");
		Action action = new Action();

		String newCharset = null;
		newCharset = doForCharset(header.contentType);

		doHeaders(createHeaders(header));

		String url = header.url;
		log.debug("  recording url: " + url);

		String method = header.method.toLowerCase();
		if (method.equals("get")) {
			method = "Get";
		} else if (method.equals("post")) {
			method = "Post";
		}
		int pos = url.indexOf("?");
		if ((pos != -1) && (url.length() > pos + 1)) {
			setQueryStringParams(action, url.substring(pos + 1));
			action.setUrl(url.substring(0, pos));
		} else {
			action.setUrl(url);
		}
		if (requestBody.length > 0) {
			if (isMultiPartRequest) {
				setMultiPartData(action, header, requestBody);
			} else if (!isFormRequest) {
				if (newCharset != null)
					doSetData(new String(requestBody, newCharset));
				else
					doSetData(new String(requestBody));
			} else if (newCharset != null && !newCharset.equals("")) {
				setBodyParams(action, new String(requestBody, newCharset));
			} else {
				setBodyParams(action, new String(requestBody));
			}

		}

		if (action.getParamsCount() > 0) {
			params = action.getParams();
			for (i = 0; i < params.length; ++i) {
				name = params[i].name;
				value = params[i].value;
				if ((name.indexOf("+") != -1) || (name.indexOf("%") != -1))
					params[i].name = HttpTestCase.staticUrlDecode(name);
				if ((value.indexOf("+") != -1) || (value.indexOf("%") != -1))
					params[i].value = HttpTestCase.staticUrlDecode(value);
			}
			doParameterList(params);
		}

		if (action.getQueryStringParamCount() > 0) {
			params = action.getQueryStringParams();

			for (i = 0; i < params.length; ++i) {
				name = params[i].name;
				value = params[i].value;
				if ((name.indexOf("+") != -1) || (name.indexOf("%") != -1))
					params[i].name = HttpTestCase.staticUrlDecode(name);
				if ((value.indexOf("+") != -1) || (value.indexOf("%") != -1))
					params[i].value = HttpTestCase.staticUrlDecode(value);
			}
			doQueryStringParameterList(params);
		}

		if (action.getBodyParamCount() > 0) {
			params = action.getBodyParams();

			for (i = 0; i < params.length; ++i) {
				name = params[i].name;
				value = params[i].value;
				if ((name.indexOf("+") != -1) || (name.indexOf("%") != -1))
					params[i].name = HttpTestCase.staticUrlDecode(name);
				if ((value.indexOf("+") != -1) || (value.indexOf("%") != -1))
					params[i].value = HttpTestCase.staticUrlDecode(value);
			}
			doBodyParameterList(params);
		}

		String url_str = header.url;
		if (requestBody.length > 0) {
			url_str = url_str + '?' + new String(requestBody);
		}

		if (!(isMultiPartRequest)) {
			doTestUrlMessage(HttpTestCase.staticUrlDecode(url_str).trim());
		}

		String data_str = "";
		String cont_len_str = "";

		if (isMultiPartRequest) {
			method = method + "MultiPart";
			data_str = ", data";
			cont_len_str = ", " + header.contentLength;
			doSetData(action.getMultiPartData());
		}
		doCallUrl(action, method, data_str, cont_len_str,respCode,respContentType);
		this.requestCount++;
	}

	private String doForCharset(String contentType) throws UserException {
		Matcher charsetParser = Pattern.compile("charset=([a-z0-9_\\-]+)", 2)
				.matcher(contentType);
		String newCharset = "";
		if (charsetParser.matches()) {
			newCharset = charsetParser.group();
			if (!(newCharset.equals(this.charset))) {
				doSetCharset(newCharset);
			} else if (this.charset != null) {
				doSetCharset(this.charset);
			}

		}
		return newCharset;
	}

	private HeaderValue[] createHeaders(HttpRequestHeader header) {
		Vector<HeaderValue> v = new Vector<HeaderValue>();
		if (header.contentType.length() > 0)
			v.add(new HeaderValue("Content-Type", header.contentType));
		if (header.accept.length() > 0)
			v.add(new HeaderValue("Accept", header.accept));
		if (header.referer.length() > 0)
			v.add(new HeaderValue("Referer", header.referer));
		if (header.pragmaNoCache)
			v.add(new HeaderValue("Cache-Control", "no-cache"));
		HeaderValue[] hv = new HeaderValue[v.size()];
		return ((HeaderValue[]) (HeaderValue[]) v.toArray(hv));
	}

	public void processResponse(HttpRequestHeader header, byte[] requestBody,
			ResponseModel response) throws Exception {
		if (this.ignoreNextResponse) {
			log.debug("Ignoring response");
			this.ignoreNextResponse = false;
			return;
		}
		ResponseParser responseParser = new ResponseParser(response);
		ResponseHeader responseHeader = responseParser.getResponseHeader();
		doAssertResponse(responseHeader.getRespCode());
		if (responseHeader.isValidResponseHeader()) {
			if (responseHeader.isGoodRequest()
					&& responseHeader.isHtmlContentType()
					&& hasHtmlTag(responseParser)) {
				dealWithHtmlResponse(header, responseParser);
				doParseRequest(header, requestBody, responseHeader.getRespCode(), responseHeader.getContentType());
				
				response.setResponse(responseParser.encodeCoent(doParseHtmlContent(responseParser.getResponseBody(), header.url)));
				setStruts(new String(response.getResponse()).toLowerCase().indexOf(
						"org.apache.struts.taglib.html.token") > 0);
				doEndTransaction();
				
			}
			setStruts(new String(response.getResponse()).toLowerCase().indexOf(
					"org.apache.struts.taglib.html.token") > 0);
			doEndTransaction();
		} else {
			log.debug("Ignoring response because content type is not known: "
					+ responseHeader.getRespCode());
		}
		doParseRequest(header, requestBody, responseHeader.getRespCode(), responseHeader.getContentType());
	}

	private boolean hasHtmlTag(ResponseParser responseParser) {
		return responseParser.getResponseBody() == null ? false
				: responseParser.getResponseBody().contains("<html");
	}

	private void dealWithHtmlResponse(HttpRequestHeader header,
			ResponseParser responseParser) throws UserException {
		if (!isFirstHtmlResponse()) {
			this.getScriptAdapter().addPage();
		}
		this.getScriptAdapter().resetChildrenUrls();
		this.setFirstHtmlResponse(false);
		doTidyCode(HttpTestCase.staticUrlDecode(header.url));
		copyResponseToSpecificOut(header.url);
	}

	private void copyResponseToSpecificOut(String requestUrl)
			throws UserException {
		if (isCpRspToStdout())
			doResponseForStdOut(HttpTestCase.staticUrlDecode(requestUrl).trim());
		else if (isCpRspToFile())
			doResponseForFile();
	}

	public void run() {
		try {
			while (!(Thread.interrupted())) {
				synchronized (this.outstandingInserts) {
					this.outstandingInserts.wait();
					while (this.outstandingInserts.size() > 0) {
						BehaviorModel behaviorModel = this.outstandingInserts
								.removeFirst();
						this.getScriptAdapter().insertUserBehaviorsToScenario(
								behaviorModel);
					}
				}
			}
		} catch (InterruptedException e) {
			log.trace("Thread dead");
		}
	}

	public abstract void doAssertResponse(int paramString)
			throws Utils.UserException;

	public abstract void doCallUrl(Action action, String paramString2,
			String paramString3, String paramString4, int paramInt, String paramString5)
			throws Utils.UserException;

	public abstract void doEndTransaction() throws Utils.UserException;

	public abstract void setStruts(boolean paramBoolean);

	public abstract void doSetCharset(String paramString)
			throws Utils.UserException;

	public abstract void doParameterList(Param[] paramArrayOfParam)
			throws Utils.UserException;

	public abstract void doQueryStringParameterList(Param[] paramArrayOfParam)
			throws Utils.UserException;

	public abstract void doBodyParameterList(Param[] paramArrayOfParam)
			throws Utils.UserException;

	public abstract void doResponseForFile() throws Utils.UserException;

	public abstract void doResponseForStdOut(String paramString)
			throws Utils.UserException;

	public abstract void doSetData(String paramString)
			throws Utils.UserException;

	public abstract void doTestUrlMessage(String paramString)
			throws Utils.UserException;

	public abstract void doTidyCode(String paramString)
			throws Utils.UserException;

	/**
	 * @param responseBody
	 * @param rootUrl
	 * @return new response body
	 */
	public abstract String doParseHtmlContent(String responseBody, String rootUrl);

	public abstract void doHeaders(HeaderValue[] paramArrayOfHeaderValue);

	protected void insert(BehaviorModel behaviorModel)
			throws Utils.UserException {
		synchronized (this.outstandingInserts) {
			this.outstandingInserts.add(behaviorModel);
			this.outstandingInserts.notifyAll();
		}
	}

	private void setMultiPartData(Action action, HttpRequestHeader header,
			byte[] strarray) {
		String str = new String(strarray);
		int begin = header.contentType.indexOf("boundary=");
		int end = header.contentType.indexOf("; ", begin);
		if (end == -1)
			end = header.contentType.length();
		String boundary = header.contentType.substring(begin + 9, end);

		String[] parts = str.split("--" + boundary);
		if ((!($assertionsDisabled)) && (!(parts[0].equals(""))))
			throw new AssertionError();

		Pattern re = Pattern
				.compile(
						"\r\nContent-Disposition: form-data; name=\"([^\"]+)\"[^\r\n]*\r\n\r\n(.*)\r\n",
						32);
		for (int i = 1; i < parts.length - 1; ++i) {
			Matcher m = re.matcher(parts[i]);
			boolean ok = m.matches();
			if ((!($assertionsDisabled)) && (!(ok)))
				throw new AssertionError();
			Param p = new Param();
			p.name = m.group(1);
			p.value = m.group(2);
			action.addBodyParam(p);
		}

		if ((!($assertionsDisabled))
				&& (!(parts[(parts.length - 1)].equals("--\r\n"))))
			throw new AssertionError();
	}

	private void setQueryStringParams(Action action, String str) {
		List<Param> params = Utils.getParams(str);
		Iterator<Param> iter = params.iterator();
		while (iter.hasNext())
			action.addQueryStringParam((Param) iter.next());
	}

	private void setBodyParams(Action action, String str) {
		List<Param> params = Utils.getParams(str);
		Iterator<Param> iter = params.iterator();
		while (iter.hasNext())
			action.addBodyParam((Param) iter.next());
	}

	private boolean urlIgnored(String str) {
		boolean ignore = false;

		List<Pattern> incPats = this.config.getIncludePatterns();
		List<Pattern> excPats = this.config.getExcludePatterns();
		if ((!(ignore)) && (incPats.size() != 0)) {
			log.debug("urlIgnored: checking for include matches");
			ignore = this.checkMatch(incPats.iterator(), str, false, true);
		}
		if ((!(ignore)) && (excPats.size() != 0)) {
			log.debug("urlIgnored: checking for exclude matches");
			ignore = this.checkMatch(excPats.iterator(), str, true, ignore);
		}
		return ignore;
	}

	private boolean checkMatch(Iterator<Pattern> iter, String str, boolean res,
			boolean def) {
		while (iter.hasNext()) {
			Matcher m = ((Pattern) iter.next()).matcher(str);
			log.debug("checkMatch: for \"" + str + "\" with pattern: \""
					+ m.pattern().pattern() + "\"");
			if (m.find()) {
				log.debug("checkMatch: Found match, returning: " + res);
				return res;
			}
		}
		log.debug("checkMatch: Didn't find match, returning: " + def);
		return def;
	}

	static {
		log = LogFactory.getLog(AbstractCodeGenerator.class);

		cpRspTo = System.getProperty("http.cpRspTo");

		cpRspToFile = false;
		MIME_DEFAULTS = new String[] { "text/plain", "text/html",
				"text/comma-separated-values" };

		END_STATEMENT = ";" + EOL;

		jtidyConfigFile = System.getProperty("jtidy.config");

		if (cpRspTo != null)
			if (cpRspTo.toLowerCase().compareTo("stdout") == 0)
				cpRspToStdout = true;
			else if (cpRspTo.toLowerCase().compareTo("file") == 0)
				cpRspToFile = true;

		mimeTypes = new HashMap<String, String>();
		for (int i = 0; i < MIME_DEFAULTS.length; ++i)
			mimeTypes.put(MIME_DEFAULTS[i], MIME_DEFAULTS[i]);
	}
}
