package test;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import com.ibm.rational.test.lt.kernel.action.impl.KAction;
import com.ibm.rational.test.lt.kernel.logging.impl.KLog;
import com.ibm.rational.test.lt.kernel.services.ITestExecutionServices;
import com.ibm.rational.test.lt.kernel.statistics.IScalar;
import com.ibm.rational.test.lt.kernel.statistics.IStat;
import com.ibm.rational.test.lt.kernel.statistics.IStatTree;
import com.ibm.rational.test.lt.kernel.statistics.impl.StatType;

/**
 * CCBlast
 * 
 * The Custom Code Blaster is designed to hit a single request
 * on a web server as hard as possible.
 * 
 * This version supports parameters through RPT test variables.
 * The arguments should be passed in as follows:
 * 
 * arg[0]	hostname
 * arg[1]	url
 * arg[2]	port
 * arg[3]	connections
 * 
 * If any of these arguments is not supplied then the values
 * coded directly below are used.
 * 
 * If you wish to control the connections by the number of RPT users
 * then set connections to 1.
 * 
 * Suggested schedule settings for maximum throughput:
 *     Users:                    1 (note connections below)
 *     Duration:                 As desired.
 *     Test Log:                 NONE
 *     Problem Determination:    NONE
 *     
 *     
 * @author kmooney
 */
public class CCBlast implements
		com.ibm.rational.test.lt.kernel.custom.ICustomCode2 {
	private String hostname = "iquad1";
	private String url = "/daytrader/servlet/PingServlet";
	private int port = 9080;
	private int connections = 30;
	private final long sleepTime = 1000;
	private final boolean metrics = true;
	private final boolean debug = false;
	private IStatTree rootStat;
	private IStatTree tranStat;
	private IStatTree timeStat;
	private IStatTree completeStat;
	private IStatTree attemptStat;
	private IStat timeDataStat;
	private IStat completeDataStat;
	private IScalar attemptDataStat;

	/**
	 * Instances of this will be created using the no-arg constructor.
	 */
	public CCBlast() {
	}

	/**
	 * For javadoc of ICustomCode2 and ITestExecutionServices interfaces, select 'Help Contents' in the
	 * Help menu and select 'IBM Rational Performance Tester TES'.
	 */
	public String exec(ITestExecutionServices tes, String[] args) {
		KAction action = (KAction)tes;
		
		processArgs(args);

		// Using the correct names tricks the aggregator into working for us
		if (metrics) {
			rootStat = tes.getStatisticsManager().getStatTree();
	        if (rootStat != null) {
	        	tranStat = rootStat.getStat("Transactions", StatType.STRUCTURE);
	        	timeStat = tranStat.getStat("Execution Time", StatType.STRUCTURE);
	        	completeStat = tranStat.getStat("Completed", StatType.STRUCTURE);
	        	attemptStat = tranStat.getStat("Attempts", StatType.STRUCTURE);
	       
	        	timeDataStat = (IStat) timeStat.getStat("Ping", StatType.RANGE);
	        	completeDataStat = (IStat) completeStat.getStat("Ping", StatType.RATE);
	        	attemptDataStat = (IScalar) attemptStat.getStat("Ping", StatType.SCALAR);
	        }
		}

		// Create and start blaster threads
		Blaster[] blasters = null;
		try {
			blasters = new Blaster[connections];
			for (int i = 0; i < connections; i++) {
				blasters[i] = new Blaster();
				blasters[i].setDaemon(true);
				blasters[i].start();
			}

			// Should get an interrupt when schedule shutdown occurs
			while (!action.getStopRequested())
				Thread.sleep(sleepTime);

		} catch(InterruptedException ie) {
			Thread.currentThread().interrupt();
		}

		if (debug)
			KLog.log("Stop requested, shutting down");
		for (int i = 0; i < connections; i++) {
			blasters[i].shutdown();
		}
		if (debug)
			KLog.log("shutdown");
		for (int i = 0; i < connections; i++) {
			try {
				blasters[i].join(1000);
			} catch(InterruptedException ie) {
				Thread.currentThread().interrupt();
			}
		}
		if (debug)
			KLog.log("finished");
		return null;
	}
	public static enum Stats {READSUCCESSES, CONNECTIONSUCCESS, 
		CONNECTIONATTEMPTS, BYTESREAD, READATTEMPTS, BYTESWRITTEN, 
		WRITEATTEMPTS;

		String stringName = null;
		
		public String toString() {
			if (stringName == null)
				stringName = super.toString().toLowerCase();
			return stringName;
		}
	};

	/*
	 * Process arguments.
	 */
	private void processArgs(String args[]) {
		if (debug) {
			KLog.log("Processing " + args.length + " arguments");
			for (int i = 0; i < args.length; i++)
				KLog.log("args[" + i + "] = '" + args[0] + "'");
		}
		if (args.length > 0)
			hostname = args[0];
		if (args.length > 1)
			url = args[1];
		if (args.length > 2)
			port = Integer.parseInt(args[2]);
		if (args.length > 3)
			connections = Integer.parseInt(args[3]);
		}

	public static final int numStats = Stats.values().length;

	/*
	 * Worker
	 */
	public class Blaster extends Thread {
		private IScalar[] statArray = new IScalar[numStats];
//		private static final String READSUCCESSES = "readsuccesses";
//		private static final String CONNECTIONSUCCESS = "connectionsuccess";
//		private static final String CONNECTIONATTEMPTS = "connectionattempts";
//		private static final String BYTESREAD = "bytesread";
//		private static final String READATTEMPTS = "readattempts";
//		private static final String BYTESWRITTEN = "byteswritten";
//		private static final String WRITEATTEMPTS = "writeattempts";

		private static final String LATIN1 = "latin1";
		private static final String SPACE = " ";
		private static final String GET = "GET ";
		private boolean stopRequested = false;
		Socket socket = null;
		OutputStream writer;
		InputStream reader;
		String input;
		byte[] buffer = new byte[8192];
		int result;
		boolean gotContentLength;
		int thisnumread;
		int contentlength;
		int headerSize;
		int lastContentLengthPos;
		int lastHeaderEndPos;
		
		public void run() {
			// Set up requrest headers
			String request;
			String host;
			long start;
			try {
				host = InetAddress.getLocalHost().getHostName();
			} catch(UnknownHostException uhe) {
				logException(uhe);
				host = "";
			}
			final String requestHeaders =
				"HTTP/1.1\r\n"
					+ "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, "
					+ "application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*\r\n"
					+ "Accept-Language: en-us\r\n"
					+ "Accept-Encoding: gzip, deflate\r\n"
					+ "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)\r\n"
					+ "Host: " + host + "\r\n"
					+ "Connection: Keep-Alive\r\n"
					+ "\r\n";
			
			final char headerEndStr[] = { '\r', '\n', '\r', '\n' };
			final char contentLengthStr[] = {
				'C',
				'o',
				'n',
				't',
				'e',
				'n',
				't',
				'-',
				'L',
				'e',
				'n',
				'g',
				't',
				'h' };
			
			if (debug)
				KLog.log("hostname is '" + hostname +
						"' url is '" + url + "'");
			
			StringBuffer requestSB = new StringBuffer();
			requestSB.append(GET);
			requestSB.append(url);
			requestSB.append(SPACE);
			requestSB.append(requestHeaders);
			byte[] bytes = null;
			try {
				bytes = requestSB.toString().getBytes(LATIN1);
			} catch (UnsupportedEncodingException e1) {
				logException(e1);
				return;
			}
			
			// Get the page
			while(!stopRequested) {
				gotContentLength = false;
				thisnumread = contentlength = 0;
				
				// Connect
				if (socket == null)
					socket = socketConnect(hostname, port);
				
				// Write request
				requestSB.setLength(0);
				request = requestSB.toString() + requestHeaders;
				try {
					attemptDataStat.increment();
					incStat(Stats.WRITEATTEMPTS);
					start = System.currentTimeMillis();
					writer.write(bytes);
					writer.flush();
					if (debug)
						KLog.log("write '"  + request + "'");
					incStat(Stats.BYTESWRITTEN, request.length());
					incStat(Stats.READATTEMPTS);
					while ((result = reader.read(buffer)) >= 0) {
						incStat(Stats.BYTESREAD, result);
						thisnumread += result;
						if (debug) {
							StringBuffer sb = new StringBuffer();
							sb.append(new String(buffer, 0, result));
							KLog.log("read " + result +
									" bytes '" + sb.toString() + "'");
						}
						if (!gotContentLength) {
							if (debug)
								KLog.log("Starting search for content length");
							int headerEndPos = lastHeaderEndPos = findString(buffer, headerEndStr, lastHeaderEndPos);
							if (headerEndPos == -1)
								lastHeaderEndPos = headerEndPos = findString(buffer, headerEndStr);
							int contentLengthPos = lastContentLengthPos =
								findString(buffer, contentLengthStr, lastContentLengthPos);
							if (contentLengthPos == -1)
								lastContentLengthPos = contentLengthPos = findString(buffer, contentLengthStr);
							if (headerEndPos >= 0)
								headerSize = headerEndPos + 4;
							if (contentLengthPos >= 0) {
								int i, startPos;
								i = startPos = contentLengthPos + 16;
								int len = 0;
								while (buffer[i] >= '0'
									&& buffer[i] <= '9') {
									i++;
									len++;
								}
								int l = 1;
								int num;
								for (int k = startPos + len - 1; k >= startPos; k--) {
									num = (int) buffer[k] - 48;
									contentlength += (num * l);
									l *= 10;
								}
								gotContentLength = true;
								if (debug)
									KLog.log("Found content length:  " + contentlength);
							}
						}
						
						if (thisnumread - headerSize == contentlength) {
							incStat(Stats.READSUCCESSES);
							if (metrics) {
								timeDataStat.submitDataPoint(System.currentTimeMillis() - start);
								completeDataStat.submitDataPoint(1);
							}
							if (debug)
								KLog.log("Finished reading " + contentlength + " bytes");
							break;
						}
					}
					
					if (result < 0)
						socket = null;
				} catch (Exception e) {
					logException(e);
					socket = null;
				}
			}
		}

		/*
		 * Find location of string in byte array
		 */
		private int findString(byte[] cb, char[] s) {
			return findString(cb, s, 0);
		}
		
		private int findString(byte[] cb, char[] s, int startPos) {
			int result = -1;
			boolean match = false;
			int j = 0;
			for (int i = startPos; i < cb.length && j < s.length; i++)
				if (cb[i] == s[j]) {
					if (!match) {
						result = i;
						match = true;
					}
					j++;
				} else {
					j = 0;
					result = -1;
					match = false;
				}
			return result;	
		}
		
		public void shutdown() {
			stopRequested = true;
		}
		
		private Socket socketConnect(String hostname, int port) {
			Socket socket = null;
			try {
				incStat(Stats.CONNECTIONATTEMPTS);
				socket = new Socket(hostname, port);
				writer = socket.getOutputStream();
				reader = socket.getInputStream();
				incStat(Stats.CONNECTIONSUCCESS);
			} catch (Exception e) {
				logException(e);
			}
			return socket;
		}

		private void incStat(Stats stat, int num) {
			if (metrics) {
				IScalar theStat = getStat(stat);
				theStat.increment(num);
			}
		}


		private IScalar getStat(Stats stat) {
			if (statArray[stat.ordinal()] == null) {
				statArray[stat.ordinal()] = (IScalar) rootStat.getStat(stat.toString(), StatType.STATIC);
			}
			return statArray[stat.ordinal()];
		}

		private void incStat(Stats stat) {
			incStat(stat, 1);
		}

		private void logException(Exception e) {
			if (debug) {
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				e.printStackTrace(new PrintWriter(baos,true));
				KLog.log(baos.toString());
			}
		}
	}

}
