package freenet.node;

import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.math.TrivialRunningAverage;

/** A record of stats during a single hour */
public class HourlyStatsRecord {
	private static final int N_DISTANCE_GROUPS = 16;
	private final boolean completeHour;
	private boolean finishedReporting;

	/**(Logarithmic) routing distances grouped by HTL*/
	private StatsLine[] byHTL;

	/**HTL grouped by (logarithmic) routing distance*/
	private StatsLine[] byDist;

	private Date beginTime;
	private final Node node;

	/** Public constructor.
	  *
	  * @param completeHour Whether this record began at the start of the hour
	  */
	public HourlyStatsRecord(Node node, boolean completeHour) {
		this.node = node;
		this.completeHour = completeHour;
		finishedReporting = false;
		byHTL = new StatsLine[node.maxHTL() + 1];
		for (int i = 0; i < byHTL.length; i++) byHTL[i] = new StatsLine();
		byDist = new StatsLine[N_DISTANCE_GROUPS];
		for (int i = 0; i < byDist.length; i++) byDist[i] = new StatsLine();

		beginTime = new Date();
	}

	/** Mark this record as complete and stop recording. */
	public synchronized void markFinal() {
		finishedReporting = true;
	}

	/** Report an incoming accepted remote request.
	  *
	  * @param ssk Whether the request was an ssk
	  * @param success Whether the request succeeded
	  * @param local If the request succeeded, whether it succeeded locally
	  * @param htl The htl counter the request had when it arrived
	  * @param location The routing location of the request
	  */
	public synchronized void remoteRequest(boolean ssk, boolean success, boolean local,
			int htl, double location) {
		if (finishedReporting) throw new IllegalStateException(
				"Attempted to modify completed stats record.");
		if (htl < 0) throw new IllegalArgumentException("Invalid HTL.");
		if (location < 0 || location > 1)
			throw new IllegalArgumentException("Invalid location.");
		htl = Math.min(htl, node.maxHTL());
		double rawDist = Location.distance(node.getLocation(), location);
		if (rawDist <= 0.0) rawDist = Double.MIN_VALUE;
		double logDist = Math.log(rawDist) / Math.log(2.0);
		assert logDist < (-1.0 + 0x1.0p-1022/* Double.MIN_NORMAL */);
		int distBucket = ((int)Math.floor(-1 * logDist));
		if (distBucket >= byDist.length) distBucket = byDist.length - 1;
		
		if(ssk) {
			byHTL[htl].locDiffSSK.report(logDist);
		} else {
			byHTL[htl].locDiffCHK.report(logDist);
		}

		if (success) {
			if (ssk) {
				if (local) {
					byHTL[htl].sskLocalSuccess.report(logDist);
					byDist[distBucket].sskLocalSuccess.report(htl);
				} else {
					byHTL[htl].sskRemoteSuccess.report(logDist);
					byDist[distBucket].sskRemoteSuccess.report(htl);
				}
			} else {
				if (local) {
					byHTL[htl].chkLocalSuccess.report(logDist);
					byDist[distBucket].chkLocalSuccess.report(htl);
				} else {
					byHTL[htl].chkRemoteSuccess.report(logDist);
					byDist[distBucket].chkRemoteSuccess.report(htl);
				}
			}
		} else {
			if (ssk) {
				byHTL[htl].sskFailure.report(logDist);
				byDist[distBucket].sskFailure.report(htl);
			} else {
				byHTL[htl].chkFailure.report(logDist);
				byDist[distBucket].chkFailure.report(htl);
			}
		}
	}

	public void log() {
		Logger.normal(this, toString());
	}

	private static SimpleDateFormat utcDateTime;
	static {
		utcDateTime = new SimpleDateFormat("yyyyMMdd HH:mm:ss.SSS");
		utcDateTime.setTimeZone(TimeZone.getTimeZone("UTC"));
	}

	private static final DecimalFormat fix3p3pct = new DecimalFormat("##0.000%");
	private static final DecimalFormat fix4p = new DecimalFormat("#.0000");

	private static double fixNaN(double d) {
		if (Double.isNaN(d)) return 0.;
		return d;
	}

	@Override
	public synchronized String toString() {
		StringBuilder s = new StringBuilder();
		s.append("HourlyStats: Report for hour beginning with UTC ");
		s.append(utcDateTime.format(beginTime, new StringBuffer(), new FieldPosition(0))).append("\n");
		s.append("HourlyStats: Node uptime (ms):\t").append(node.getUptime()).append("\n");
		s.append("HourlyStats: build:\t").append(Version.buildNumber()).append("\n");
		s.append("HourlyStats: CompleteHour: ").append(completeHour);
		s.append("\tFinished: ").append(finishedReporting).append("\n");

		for (int i = byHTL.length - 1; i >= 0; i--) {
			s.append("HourlyStats: HTL\t").append(i).append("\t");
			s.append(byHTL[i].toString()).append("\n");
		}
		for (int i = 0; i < byDist.length; i++) {
			s.append("HourlyStats: logDist\t").append(i).append("\t");
			s.append(byDist[i].toString()).append("\n");
		}
		return s.toString();
	}

	public void fillRemoteRequestHTLsBox(HTMLNode html) {
		HTMLNode table = html.addChild("table");
		HTMLNode row = table.addChild("tr");
		row.addChild("th", "HTL");
		row.addChild("th", "CHKs");
		row.addChild("th", "SSKs");
		row = table.addChild("tr");
		char nbsp = (char)160;
		int totalCHKLS = 0;
		int totalCHKRS = 0;
		int totalCHKT = 0;
		int totalSSKLS = 0;
		int totalSSKRS = 0;
		int totalSSKT = 0;
		synchronized(this) {
			for(int htl = byHTL.length - 1; htl >= 0; htl--) {
				row = table.addChild("tr");
				row.addChild("td", Integer.toString(htl));
				StatsLine line = byHTL[htl];
				int chkLS = (int)line.chkLocalSuccess.countReports();
				int chkRS = (int)line.chkRemoteSuccess.countReports();
				int chkF = (int)line.chkFailure.countReports();
				int chkT = chkLS + chkRS + chkF;
				int sskLS = (int)line.sskLocalSuccess.countReports();
				int sskRS = (int)line.sskRemoteSuccess.countReports();
				int sskF = (int)line.sskFailure.countReports();
				int sskT = sskLS + sskRS + sskF;
				
				double locdiffCHK = line.locDiffCHK.currentValue();
				locdiffCHK = Math.pow(2.0, locdiffCHK);
				double locdiffSSK = line.locDiffSSK.currentValue();
				locdiffSSK = Math.pow(2.0, locdiffSSK);

				double chkRate = 0.;
				double sskRate = 0.;
				if (chkT > 0) chkRate = ((double)(chkLS + chkRS)) / (chkT);
				if (sskT > 0) sskRate = ((double)(sskLS + sskRS)) / (sskT);

				row.addChild("td", fix3p3pct.format(chkRate) + nbsp + "(" + chkLS + "," + chkRS + "," + chkT + ")"+nbsp+"("+fix4p.format(locdiffCHK)+")");
				row.addChild("td", fix3p3pct.format(sskRate) + nbsp + "(" + sskLS + "," + sskRS + "," + sskT + ")"+nbsp+"("+fix4p.format(locdiffSSK)+")");

				totalCHKLS += chkLS;
				totalCHKRS+= chkRS;
				totalCHKT += chkT;
				totalSSKLS += sskLS;
				totalSSKRS += sskRS;
				totalSSKT += sskT;
			}
			double totalCHKRate = 0.0;
			double totalSSKRate = 0.0;
			if (totalCHKT > 0) totalCHKRate = ((double)(totalCHKLS + totalCHKRS)) / totalCHKT;
			if (totalSSKT > 0) totalSSKRate = ((double)(totalSSKLS + totalSSKRS)) / totalSSKT;

			row = table.addChild("tr");
			row.addChild("td", "Total");
			row.addChild("td", fix3p3pct.format(totalCHKRate) + nbsp + "("+ totalCHKLS + "," + totalCHKRS + "," + totalCHKT + ")");
			row.addChild("td", fix3p3pct.format(totalSSKRate) + nbsp + "("+ totalSSKLS + "," + totalSSKRS + "," + totalSSKT + ")");
		}
	}

	private class StatsLine {
		TrivialRunningAverage chkLocalSuccess;
		TrivialRunningAverage chkRemoteSuccess;
		TrivialRunningAverage chkFailure;
		TrivialRunningAverage sskLocalSuccess;
		TrivialRunningAverage sskRemoteSuccess;
		TrivialRunningAverage sskFailure;
		TrivialRunningAverage locDiffCHK;
		TrivialRunningAverage locDiffSSK;

		StatsLine() {
			chkLocalSuccess = new TrivialRunningAverage();
			chkRemoteSuccess = new TrivialRunningAverage();
			chkFailure = new TrivialRunningAverage();
			sskLocalSuccess = new TrivialRunningAverage();
			sskRemoteSuccess = new TrivialRunningAverage();
			sskFailure = new TrivialRunningAverage();
			locDiffCHK = new TrivialRunningAverage();
			locDiffSSK = new TrivialRunningAverage();
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();

			sb.append(chkLocalSuccess.countReports()).append("\t");
			sb.append(chkRemoteSuccess.countReports()).append("\t");
			sb.append(chkFailure.countReports()).append("\t");
			sb.append(sskLocalSuccess.countReports()).append("\t");
			sb.append(sskRemoteSuccess.countReports()).append("\t");
			sb.append(sskFailure.countReports()).append("\t");

			sb.append(fix4p.format(fixNaN(chkLocalSuccess.currentValue()))).append("\t");
			sb.append(fix4p.format(fixNaN(chkRemoteSuccess.currentValue()))).append("\t");
			sb.append(fix4p.format(fixNaN(chkFailure.currentValue()))).append("\t");
			sb.append(fix4p.format(fixNaN(sskLocalSuccess.currentValue()))).append("\t");
			sb.append(fix4p.format(fixNaN(sskRemoteSuccess.currentValue()))).append("\t");
			sb.append(fix4p.format(fixNaN(sskFailure.currentValue()))).append("\t");
			return sb.toString();
		}
	}
}
