/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.node;

import static java.util.concurrent.TimeUnit.MINUTES;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;

import freenet.support.Fields;
import freenet.support.Logger;
import freenet.support.Ticker;
import freenet.support.io.Closer;

/**
 * A class to estimate the node's average uptime. Every 5 minutes (with a fixed offset), we write
 * an integer (the number of minutes since the epoch) to the end of a file. We rotate it when it
 * gets huge. We read it to figure out how many of the 5 minute slots in the last X period are occupied.
 *
 * @author toad
 */
public class UptimeEstimator implements Runnable {

	/**
	 * Five minutes in milliseconds.
	 */
	static final long PERIOD = MINUTES.toMillis(5);

	Ticker ticker;

	/** For each 5 minute slot in the last 48 hours, were we online? */
	private boolean[] wasOnline = new boolean[48*12]; //48 hours * 12 5-minute slots/hour
	/** Whether the node was online for each 5 minute slot in the last week, */
	private boolean[] wasOnlineWeek = new boolean[7*24*12]; //7 days/week * 24 hours/day * 12 5-minute slots/hour

	/** Which slot are we up to? We rotate around the array. Slots before us are before us,
	 * slots after us are also before us (it wraps around). */
	private int slot;

	/** The file we are writing to */
	private File logFile;

	/** The previous file. We have read this. When logFile reaches 48 hours, we dump the prevFile,
	 * move the logFile over it, and write to a new logFile.
	 */
	private File prevFile;

	/** We write to disk every 5 minutes. The offset is derived from the node's identity. */
	private long timeOffset;

	public UptimeEstimator(ProgramDirectory runDir, Ticker ticker, byte[] bs) {
		this.ticker = ticker;
		logFile = runDir.file("uptime.dat");
		prevFile = runDir.file("uptime.old.dat");
		timeOffset = (int)
			((((double)(Math.abs(Fields.hashCode(bs, bs.length / 2, bs.length - bs.length / 2)))) /  Integer.MAX_VALUE)
			* PERIOD);
	}

	public void start() {
		long now = System.currentTimeMillis();
		int fiveMinutesSinceEpoch = (int)(now / PERIOD);
		int base = fiveMinutesSinceEpoch - wasOnlineWeek.length;
		// Read both files.
		readData(prevFile, base);
		readData(logFile, base);
		schedule(System.currentTimeMillis());
		System.out.println("Created uptime estimator, time offset is "+timeOffset+" uptime at startup is "+new DecimalFormat("0.00").format(getUptime()));
	}

	private void readData(File file, int base) {
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			DataInputStream dis = new DataInputStream(fis);
			try {
				while(true) {
					int offset = dis.readInt();
					if(offset < base) continue;
					int slotNo = offset - base;
					if(slotNo == wasOnlineWeek.length)
						break; // Reached the end, restarted within the same timeslot.
					if(slotNo > wasOnlineWeek.length || slotNo < 0) {
						Logger.error(this, "Corrupt data read from uptime file "+file+": 5-minutes-from-epoch is now "+(base+wasOnlineWeek.length)+" but read "+slotNo);
						break;
					}
					wasOnline[slotNo % wasOnline.length] = wasOnlineWeek[slotNo] = true;
				}
			} catch (EOFException e) {
				// Finished
			} finally {
				Closer.close(dis);
			}
		} catch (IOException e) {
			Logger.error(this, "Unable to read old uptime file: "+file+" - we will assume we weren't online during that period");
		} finally {
			Closer.close(fis);
		}
	}

	@Override
	public void run() {
		synchronized(this) {
			wasOnlineWeek[slot] = true;
			wasOnline[slot % wasOnline.length] = true;
			slot = (slot + 1) % wasOnlineWeek.length;
		}
		long now = System.currentTimeMillis();
		if(logFile.length() > wasOnlineWeek.length*4L) {
			prevFile.delete();
			logFile.renameTo(prevFile);
		}
		FileOutputStream fos = null;
		DataOutputStream dos = null;
		int fiveMinutesSinceEpoch = (int)(now / PERIOD);
		try {
			fos = new FileOutputStream(logFile, true);
			dos = new DataOutputStream(fos);
			dos.writeInt(fiveMinutesSinceEpoch);
		} catch (FileNotFoundException e) {
			Logger.error(this, "Unable to create or access "+logFile+" : "+e, e);
		} catch (IOException e) {
			Logger.error(this, "Unable to write to uptime estimator log file: "+logFile);
		} finally {
			Closer.close(dos);
			Closer.close(fos);
			// Schedule next time
			schedule(now);
		}
	}

	private void schedule(long now) {
		long nextTime = (((now / PERIOD)) * (PERIOD)) + timeOffset;
		if(nextTime < now) nextTime += PERIOD;
		ticker.queueTimedJob(this, nextTime - System.currentTimeMillis());
	}

	/**
	 * Get the node's uptime fraction over the selected uptime array.
	 * @return fraction between 0.0 and 1.0.
	 */
	private synchronized double getUptime(boolean[] uptime) {
		int upCount = 0;
		for(boolean sample : uptime) if(sample) upCount++;
		return ((double) upCount) / ((double) uptime.length);
	}

	/**
	 * Get the node's uptime fraction over the past 48 hours.
	 * @return fraction between 0.0 and 1.0.
	 */
	public synchronized double getUptime() {
		return getUptime(wasOnline);
	}

	/**
	 * Get the node's uptime fraction over the past 168 hours.
	 * @return fraction between 0.0 and 1.0.
	 */
	public synchronized double getUptimeWeek() {
		return getUptime(wasOnlineWeek);
	}
}
