package net.famzah.jdbench;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.*;

import com.google.gson.Gson;

import static net.famzah.jdbench.OfyService.ofy;

import com.googlecode.objectify.Key;

@SuppressWarnings("serial")
public class BenchmarkServlet extends HttpServlet {
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
		
		resp.setContentType("application/json");

		Gson gson = new Gson();
		HashMap<String, String> result = new HashMap<>();
		
		int count = Integer.parseInt(req.getParameter("count"));
		int req_size = Integer.parseInt(req.getParameter("size"));

		if (count <= 0 || req_size <= 0) {
			result.put("error", "Input must be positive integer numbers greater than 0");
			resp.getWriter().println(gson.toJson(result));
			return;
		}
		
		if (count * req_size > 100*1000*1000) {
			result.put("error", "Total of count * requested size must be less than 100 MB");
			resp.getWriter().println(gson.toJson(result));
			return;
		}
		
		if (count > 10000) {
			count = 10000;
		}
		if (req_size > 1*1000*1000) {
			req_size = 1*1000*1000; // max 1 MB entity
		}
		
		int size = req_size/4;
		if (size <= 0) { // if user requested less than 4 bytes
			size = 1;
		}
		
		int now_ut = (int) (System.currentTimeMillis() / 1000L);
		
		result.put("count", Integer.toString(count));
		result.put("requested size", Integer.toString(req_size));
		result.put("entity int array size", Integer.toString(size));

		// add, fetch, update, delete
		// XXX: We do not "fetch", nor "update", nor "delete" OLD existing entities
		this.accountTest("add", this.addEntities(count, size, now_ut, 0),
				count, result);
		this.accountTest("get", this.getEntities(count, size, now_ut, 0),
				count, result);
		this.accountTest("update", this.addEntities(count, size, now_ut, 9999),
				count, result);
		this.accountTest("get2", this.getEntities(count, size, now_ut, 9999),
				count, result);
		this.accountTest("delete", this.deleteEntities(count, size, now_ut),
				count, result);
		
		//resp.getWriter().println(gson.toJson(result));
		List<String> sortedKeys=new ArrayList<String>(result.keySet());
		Collections.sort(sortedKeys);
		for (String key : sortedKeys) {
			resp.getWriter().printf("%s : %s\n", key, result.get(key));
		}
	}
	
	private void accountTest(String name, long elapsed_time, int count, HashMap<String, String> result) {
		result.put(String.format("test-%s-total-time-ms", name),
				Long.toString(elapsed_time));
		result.put(String.format("test-%s-per-entity-time-ms", name),
				String.format("%.2f", (double)elapsed_time / (double)count));
	}
	
	private String getEntityKey(int idx, int size, int now_ut) {
		return this.md5sum(String.format(
				"%d-%d-%d", idx, size, now_ut
				));
	}
	
	private long addEntities(int count, int size, int now_ut, int prefix) {
		ResizeableEntity all_obj[] = new ResizeableEntity[count];
		for (int idx = 0; idx < count; ++idx) {
			ResizeableEntity obj = new ResizeableEntity();
			obj.key = this.getEntityKey(idx, size, now_ut);
			obj.array = new int[size];
			for (int el = 0; el < size; ++el) {
				obj.array[el] = el + idx + size + prefix;
			}
			all_obj[idx] = obj;
		}
		
		long start = System.currentTimeMillis();
		ofy().save().entities(all_obj).now();
		long end = System.currentTimeMillis();
		ofy().clear(); // clear Objectify session cache
		
		return end-start;
	}

	private long getEntities(int count, int size, int now_ut, int prefix) {
		Map <Key<ResizeableEntity>, ResizeableEntity> fetched_objs;
		
		long start = System.currentTimeMillis();
		fetched_objs = ofy().load().keys(this.populateKeys(count, size, now_ut));
		long end = System.currentTimeMillis();
		ofy().clear(); // clear Objectify session cache
		
		int idx = 0;
		for (ResizeableEntity obj : fetched_objs.values()) {
			for (int el = 0; el < size; ++el) {
				int exp_value = el + idx + size + prefix;
				if (obj.array[el] != exp_value) {
					throw new IllegalStateException(String.format(
							"Fetched entity #%d value %d is not the expected %d",
							idx, obj.array[el], exp_value
							));
				}
			}
			++idx;
		}
		
		return end-start;
	}
	
	private ArrayList<Key<ResizeableEntity>> populateKeys(int count, int size, int now_ut) {
		ArrayList<Key<ResizeableEntity>> obj_keys = new ArrayList<Key<ResizeableEntity>>();
		for (int idx = 0; idx < count; ++idx) {
			obj_keys.add(Key.create(
					ResizeableEntity.class, this.getEntityKey(idx, size, now_ut)
					));
		}
		return obj_keys;
	}
	
	private long deleteEntities(int count, int size, int now_ut) {
		long start = System.currentTimeMillis();
		ofy().delete().keys(this.populateKeys(count, size, now_ut)).now();
		long end = System.currentTimeMillis();
		ofy().clear(); // clear Objectify session cache
		
		return end-start;
	}
	
	// http://stackoverflow.com/a/332101/198219
	private String md5sum(String input) {
		byte[] bytesOfMessage;
		try {
			bytesOfMessage = input.getBytes("UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new IllegalStateException("getBytes in UTF-8: " + e.getMessage());
		}
		MessageDigest md;
		try {
			md = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("MessageDigest.getInstance(MD5): " + e.getMessage());
		}
		byte[] thedigest = md.digest(bytesOfMessage);
		
		return this.byteToHex(thedigest);
	}
	
	// http://stackoverflow.com/questions/2817752/java-code-to-convert-byte-to-hexadecimal
	private String byteToHex(byte[] bytes) {
	    StringBuilder sb = new StringBuilder();
	    for (byte b : bytes) {
	        sb.append(String.format("%02X", b));
	    }
	    return sb.toString();
	}
}
