package freenet.node.probe;

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

import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.SubConfig;
import freenet.io.comm.AsyncMessageFilterCallback;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.DisconnectedException;
import freenet.io.comm.Message;
import freenet.io.comm.MessageFilter;
import freenet.io.comm.NotConnectedException;
import freenet.io.comm.PeerContext;
import freenet.node.Location;
import freenet.node.Node;
import freenet.node.OpennetManager;
import freenet.node.PeerNode;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.api.BooleanCallback;
import freenet.support.api.LongCallback;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Handles starting, routing, and responding to Metropolis-Hastings corrected probes.
 *
 * Possible future additions to these probes' results include:
 * <ul>
 * <li>Starting a regular request for a key.</li>
 * <li>Success rates for remote requests by HTL; perhaps over some larger amount of time than the past hour.</li>
 * </ul>
 *
 * @see freenet.node.probe Explanation of Metropolis-Hastings correction
 */
public class Probe implements ByteCounter {

	private static volatile boolean logMINOR;
	private static volatile boolean logDEBUG;
	private static volatile boolean logWARNING;

	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logWARNING = Logger.shouldLog(Logger.LogLevel.WARNING, this);
				logMINOR = Logger.shouldLog(Logger.LogLevel.MINOR, this);
				logDEBUG = Logger.shouldLog(Logger.LogLevel.DEBUG, this);
			}
		});
	}

	private final static String SOURCE_DISCONNECT = "Previous step in probe chain no longer connected.";

	/**
	 * Maximum hopsToLive value to clamp requests to.
	 */
	public static final byte MAX_HTL = 70;

	/**
	 * Maximum number of forwarding attempts to make before failing with DISCONNECTED.
	 */
	public static final int MAX_SEND_ATTEMPTS = 50;

	/**
	 * Probability of HTL decrement at HTL = 1.
	 */
	public static final float DECREMENT_PROBABILITY = 0.2f;

	/**
	 * In ms, per HTL above HTL = 1.
	 */
	public static final long TIMEOUT_PER_HTL = SECONDS.toMillis(3);

	/**
	 * In ms, to account for probabilistic decrement at HTL = 1.
	 */
	public static final long TIMEOUT_HTL1 = (long) (TIMEOUT_PER_HTL / DECREMENT_PROBABILITY);

	/**
	 * To make the timing less obvious when a node responds with a local result instead of forwarding at
	 * HTL = 1, delay for a number of milliseconds, specifically an exponential distribution with this constant as
	 * its mean.
	 */
	public static final long WAIT_BASE = SECONDS.toMillis(1);

	/**
	 * Maximum number of milliseconds to wait before sending a response.
	 */
	public static final long WAIT_MAX = SECONDS.toMillis(2);

	/**
	 * Maximum number of probes accepted from a single peer in the past minute.
	 */
	public final int COUNTER_MAX_PEER = 10;

	/**
	 * Maximum number of probes started locally in the past minute. This is the maximum conceivable value; the
	 * probes should be used with a number of requests per minute closer to the per-peer limit times the minimum
	 * expected number of peers. Around this value, and certainly above it, remote OVERLOADs may start coming
	 * in, which are not useful. The Metropolis-Hastings correction makes behavior potentially inconsistent, so
	 * keeping an eye on remote OVERLOADs is wise.
	 */
	public final int COUNTER_MAX_LOCAL = COUNTER_MAX_PEER * OpennetManager.MAX_PEERS_FOR_SCALING;

	/**
	 * Number of accepted probes in the last minute, keyed by peer.
	 */
	private final Map<PeerNode, Counter> accepted;

	private final Node node;

	private final Timer timer;

	//Whether to respond to different types of probe requests.
	private volatile boolean respondBandwidth;
	private volatile boolean respondBuild;
	private volatile boolean respondIdentifier;
	private volatile boolean respondLinkLengths;
	private volatile boolean respondLocation;
	private volatile boolean respondStoreSize;
	private volatile boolean respondUptime;
	private volatile boolean respondRejectStats;
	private volatile boolean respondOverallBulkOutputCapacityUsage;

	private volatile long probeIdentifier;

	/**
	 * Applies multiplicative Gaussian noise of mean 1.0 and the specified sigma to the input value.
	 * @param input Value to apply noise to.
	 * @param sigma Proportion change at one standard deviation.
	 * @return Value +/- Gaussian percentage.
	 */
	private final double randomNoise(final double input, final double sigma) {
		return node.getNodeStats().randomNoise(input, sigma);
	}

	/**
	 * Counts as probe request transfer.
	 * @param bytes Bytes received.
	 */
	@Override
	public void sentBytes(int bytes) {
		node.getNodeStats().probeRequestCtr.sentBytes(bytes);
	}

	/**
	 * Counts as probe request transfer.
	 * @param bytes Bytes received.
	 */
	@Override
	public void receivedBytes(int bytes) {
		node.getNodeStats().probeRequestCtr.receivedBytes(bytes);
	}

	/**
	 * No payload in probes.
	 * @param bytes Ignored.
	 */
	@Override
	public void sentPayload(int bytes) {}

	public Probe(final Node node) {
		this.node = node;
		this.accepted = Collections.synchronizedMap(new HashMap<PeerNode, Counter>());
		this.timer = new Timer(true);

		int sortOrder = 0;
		final SubConfig nodeConfig = node.getConfig().get("node");

		nodeConfig.register("probeBandwidth", true, sortOrder++, true, true, "Node.probeBandwidthShort",
			"Node.probeBandwidthLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondBandwidth;
			}

			@Override
			public void set(Boolean val) {
				respondBandwidth = val;
			}
		});
		respondBandwidth = nodeConfig.getBoolean("probeBandwidth");
		nodeConfig.register("probeBuild", true, sortOrder++, true, true, "Node.probeBuildShort",
			"Node.probeBuildLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondBuild;
			}

			@Override
			public void set(Boolean val) {
				respondBuild = val;
			}
		});
		respondBuild = nodeConfig.getBoolean("probeBuild");
		nodeConfig.register("probeIdentifier", true, sortOrder++, true, true,
			"Node.probeRespondIdentifierShort", "Node.probeRespondIdentifierLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondIdentifier;
			}

			@Override
			public void set(Boolean val) {
				respondIdentifier = val;
			}
		});
		respondIdentifier = nodeConfig.getBoolean("probeIdentifier");
		nodeConfig.register("probeLinkLengths", true, sortOrder++, true, true, "Node.probeLinkLengthsShort",
			"Node.probeLinkLengthsLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondLinkLengths;
			}

			@Override
			public void set(Boolean val) {
				respondLinkLengths = val;
			}
		});
		respondLinkLengths = nodeConfig.getBoolean("probeLinkLengths");
		nodeConfig.register("probeLocation", true, sortOrder++, true, true, "Node.probeLocationShort",
			"Node.probeLocationLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondLocation;
			}

			@Override
			public void set(Boolean val) {
				respondLocation = val;
			}
		});
		respondLocation = nodeConfig.getBoolean("probeLocation");
		nodeConfig.register("probeStoreSize", true, sortOrder++, true, true, "Node.probeStoreSizeShort",
			"Node.probeStoreSizeLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondStoreSize;
			}

			@Override
			public void set(Boolean val) {
				respondStoreSize = val;
			}
		});
		respondStoreSize = nodeConfig.getBoolean("probeStoreSize");
		nodeConfig.register("probeUptime", true, sortOrder++, true, true, "Node.probeUptimeShort",
			"Node.probeUptimeLong", new BooleanCallback() {
			@Override
			public Boolean get() {
				return respondUptime;
			}

			@Override
			public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
				respondUptime = val;
			}
		});
		respondUptime = nodeConfig.getBoolean("probeUptime");
		nodeConfig.register("probeRejectStats", true, sortOrder++, true, true, "Node.probeRejectStatsShort",
				"Node.probeRejectStatsLong", new BooleanCallback() {
				@Override
				public Boolean get() {
					return respondRejectStats;
				}

				@Override
				public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
					respondRejectStats = val;
				}
			});
			respondRejectStats = nodeConfig.getBoolean("probeRejectStats");
			
		nodeConfig.register("probeOverallBulkOutputCapacityUsage", true, sortOrder++, true, true, "Node.respondOverallBulkOutputCapacityUsage",
				"Node.respondOverallBulkOutputCapacityUsageLong", new BooleanCallback() {

					@Override
					public Boolean get() {
						return respondOverallBulkOutputCapacityUsage;
					}

					@Override
					public void set(Boolean val)
							throws InvalidConfigValueException,
							NodeNeedRestartException {
						respondOverallBulkOutputCapacityUsage = val;
					}
			
		});
		respondOverallBulkOutputCapacityUsage = nodeConfig.getBoolean("probeOverallBulkOutputCapacityUsage");

		nodeConfig.register("identifier", -1, sortOrder++, true, true, "Node.probeIdentifierShort",
			"Node.probeIdentifierLong", new LongCallback() {
			@Override
			public Long get() {
				return probeIdentifier;
			}

			@Override
			public void set(Long val) {
				probeIdentifier = val;
				//-1 is reserved for picking a random value; don't pick it randomly.
				while(probeIdentifier == -1) probeIdentifier = node.getRandom().nextLong();
			}
		}, false);
		probeIdentifier = nodeConfig.getLong("identifier");

		/*
		 * set() is not used when setting up an option with its default value, so do so manually to avoid using
		 * an identifier of -1.
		 */
		try {
			if(probeIdentifier == -1) {
				nodeConfig.getOption("identifier").setValue("-1");
				//TODO: Store config here as it has changed?
				node.getConfig().store();
			}
		} catch (InvalidConfigValueException e) {
			Logger.error(Probe.class, "node.identifier set() unexpectedly threw.", e);
		} catch (NodeNeedRestartException e) {
			Logger.error(Probe.class, "node.identifier set() unexpectedly threw.", e);
		}
	}

	/**
	 * Sends an outgoing probe request.
	 * @param htl htl for this outgoing probe: should be [1, MAX_HTL]
	 * @param listener will be called with results.
	 * @see Listener
	 */
	public void start(final byte htl, final long uid, final Type type, final Listener listener) {
		request(DMT.createProbeRequest(htl, uid, type), null, listener);
	}

	/**
	 * Processes an incoming probe request; relays results back to source.
	 * If the probe has a positive HTL, routes with MH correction and probabilistically decrements HTL.
	 * If the probe comes to have an HTL of zero: (an incoming HTL of less than one is discarded.)
	 * Returns (as node settings allow) exactly one of:
	 * <ul>
	 *         <li>unique identifier and integer 7-day uptime percentage</li>
	 *         <li>uptime: 48-hour percentage or 7-day percentage</li>
	 *         <li>output bandwidth</li>
	 *         <li>store size</li>
	 *         <li>link lengths</li>
	 *         <li>location</li>
	 *         <li>build number</li>
	 * </ul>
	 *
	 * @param message probe request, containing HTL
	 */
	public void request(Message message, PeerNode source) {
		request(message, source, new ResultRelay(source, message.getLong(DMT.UID)));
	}

	/**
	 * Processes a probe request, calling the listener with any results.
	 * @param source node from which the probe request was received. If null, it is considered to have been sent
	 * by the local node.
	 * @param listener listener for probe response.
	 */
	private void request(final Message message, final PeerNode source, final Listener listener) {
		final Long uid = message.getLong(DMT.UID);
		final byte typeCode = message.getByte(DMT.TYPE);
		final Type type;
		if (Type.isValid(typeCode)) {
			type = Type.valueOf(typeCode);
			if (logDEBUG) Logger.debug(Probe.class, "Probe type is " + type.name() + ".");
		} else {
			if (logMINOR) Logger.minor(Probe.class, "Invalid probe type " + typeCode + ".");
			listener.onError(Error.UNRECOGNIZED_TYPE, typeCode, true);
			return;
		}
		byte htl = message.getByte(DMT.HTL);
		if (htl < 1) {
			if (logWARNING) {
				Logger.warning(Probe.class, "Received out-of-bounds HTL of " + htl + " from " +
				    source.getIdentityString() + " (" + source.userToString() + "); discarding.");
			}
			return;
		} else if (htl > MAX_HTL) {
			if (logMINOR) {
				Logger.minor(Probe.class, "Received out-of-bounds HTL of " + htl + " from " +
				    source.getIdentityString() + " (" + source.userToString() + "); interpreting as " +
				    MAX_HTL + ".");
			}
			htl = MAX_HTL;
		}
		boolean availableSlot = true;
		TimerTask task = null;
		//Allocate one of this peer's probe request slots for 60 seconds; send an overload if none are available.
		synchronized (accepted) {
			//If no counter exists for the current source, add one.
			if (!accepted.containsKey(source)) {
				// Null source is started locally.
				accepted.put(source, new Counter(source == null ? COUNTER_MAX_LOCAL : COUNTER_MAX_PEER));
			}
			final Counter counter = accepted.get(source);
			if (counter.value() == counter.maxAccepted) {
				//Set a flag instead of sending inside the lock.
				availableSlot = false;
			} else {
				//There's a free slot; increment the counter.
				counter.increment();
				task = new TimerTask() {
					@Override
					public void run() {
						synchronized (accepted) {
							counter.decrement();
							/* Once the counter hits zero, there's no reason to keep it around as it
							 * can just be recreated when this peer sends another probe request
							 * without changing behavior. To do otherwise would accumulate counters
							 * at zero over time.
							 */
							if (counter.value() == 0) {
								accepted.remove(source);
							}
						}
					}
				};
			}
		}
		if (!availableSlot) {
			//Send an overload error back to the source.
			if (logDEBUG) Logger.debug(Probe.class, "Already accepted maximum number of probes; rejecting incoming.");
			listener.onError(Error.OVERLOAD, null, true);
			return;
		}
		//One-minute window on acceptance; free up this probe slot in 60 seconds.
		timer.schedule(task, MINUTES.toMillis(1));

		/*
		 * Route to a peer, using Metropolis-Hastings correction and ignoring backoff to get a more uniform
		 * endpoint distribution. HTL is decremented before routing so that it's possible to respond locally without
		 * attempting to route first. Send a local response if HTL is zero now or becomes zero while trying to route.
		 * During routing HTL decrements if a candidate is rejected by the Metropolis-Hastings correction.
		 */
		htl = probabilisticDecrement(htl);
		if (htl == 0 || !route(type, uid, htl, listener)) {
			long wait = WAIT_MAX;
			while (wait >= WAIT_MAX) wait = (long)(-Math.log(node.getRandom().nextDouble()) * WAIT_BASE / Math.E);
			timer.schedule(new TimerTask() {
				@Override
				public void run() {
					respond(type, listener);
				}
			}, wait);
		}
	}

	/**
	 * Attempts to route the message to a peer. If the maximum number of send attempts is exceeded, fails with the
	 * error CANNOT_FORWARD.
	 * @return True if no further action needed; false if HTL decremented to zero and a local response is needed.
	 */
	private boolean route(final Type type, final long uid, byte htl, final Listener listener) {
		//Recreate the request so that any sub-messages or unintended fields are not forwarded.
		final Message message = DMT.createProbeRequest(htl, uid, type);
		PeerNode[] peers;
		//Degree of the local node.
		int degree;
		PeerNode candidate;
		/*
		 * Attempt to forward until success or until reaching the send attempt limit.
		 */
		for (int sendAttempts = 0; sendAttempts < MAX_SEND_ATTEMPTS; sendAttempts++) {
			peers = node.getConnectedPeers();
			degree = peers.length;
			//Can't handle a probe request if not connected to peers.
			if (degree == 0 ) {
				if (logMINOR) {
					Logger.minor(Probe.class, "Aborting probe request: no connections.");
				}

				/*
				 * If this is a locally-started request, not a relayed one, give an error.
				 * Otherwise, in this case there's nowhere to send the error.
				 */
				listener.onError(Error.DISCONNECTED, null, true);
				return true;
			}

			candidate = peers[node.getRandom().nextInt(degree)];

			if (candidate.isConnected()) {
				//acceptProbability is the MH correction.
				float acceptProbability;
				int candidateDegree = candidate.getDegree();
				/* Candidate's degree is unknown; fall back to random walk by accepting this candidate
				 * regardless of its degree.
				 */
				if (candidateDegree == 0) acceptProbability = 1.0f;
				else acceptProbability = (float)degree / candidateDegree;

				if (logDEBUG) Logger.debug(Probe.class, "acceptProbability is " + acceptProbability);
				if (node.getRandom().nextFloat() < acceptProbability) {
					if (logDEBUG) Logger.debug(Probe.class, "Accepted candidate.");
					//Filter for response to this probe with requested result type.
					final MessageFilter filter = createResponseFilter(type, candidate, uid, htl);
					message.set(DMT.HTL, htl);
					try {
						node.getUSM().addAsyncFilter(filter, new ResultListener(listener), this);
						if (logDEBUG) Logger.debug(Probe.class, "Sending.");
						candidate.sendAsync(message, null, this);
						return true;
					} catch (NotConnectedException e) {
						if (logMINOR) Logger.minor(Probe.class, "Peer became disconnected between check and send attempt.", e);
						// Peer no longer connected - sending was not successful. Try again.
					} catch (DisconnectedException e) {
						if (logMINOR) Logger.minor(Probe.class, "Peer became disconnected while attempting to add filter.", e);
						// Peer no longer connected - cannot send. Try again.
					}
				} else {
					/*
					 * Metropolis-Hastings correction rejected - decrement HTL so that it can run out depending on
					 * relative degrees.
					 */
					htl = probabilisticDecrement(htl);

					if (htl == 0) return false;
				}
			} else {
				if (logMINOR) Logger.minor(Probe.class, "Peer in connectedPeers was not connected.", new Exception());
			}
		}

		// Send attempt limit reached.
		if (logWARNING) {
			Logger.warning(Probe.class, "Aborting probe request: send attempt limit reached.");
		}

		listener.onError(Error.CANNOT_FORWARD, null, true);
		return true;
	}

	/**
	 * @param type probe result type requested.
	 * @param candidate node to filter for response from.
	 * @param uid probe request uid, also to be used in any result.
	 * @param htl current probe HTL; used to calculate timeout.
	 * @return filter for the requested result type, probe error, and probe refusal.
	 */
	private static MessageFilter createResponseFilter(final Type type, final PeerNode candidate, final long uid, final byte htl) {
		final long timeout = (htl - 1) * TIMEOUT_PER_HTL + TIMEOUT_HTL1;
		final MessageFilter filter = createFilter(candidate, uid, timeout);

		switch (type) {
			case BANDWIDTH: filter.setType(DMT.ProbeBandwidth); break;
			case BUILD: filter.setType(DMT.ProbeBuild); break;
			case IDENTIFIER: filter.setType(DMT.ProbeIdentifier); break;
			case LINK_LENGTHS: filter.setType(DMT.ProbeLinkLengths); break;
			case LOCATION: filter.setType(DMT.ProbeLocation); break;
			case STORE_SIZE: filter.setType(DMT.ProbeStoreSize); break;
			case UPTIME_48H:
			case UPTIME_7D: filter.setType(DMT.ProbeUptime); break;
			case REJECT_STATS: filter.setType(DMT.ProbeRejectStats); break;
			case OVERALL_BULK_OUTPUT_CAPACITY_USAGE: filter.setType(DMT.ProbeOverallBulkOutputCapacityUsage); break;
			default: throw new UnsupportedOperationException("Missing filter for " + type.name());
		}

		//Refusal or an error should also be listened for so it can be relayed.
		filter.or(createFilter(candidate, uid, timeout).setType(DMT.ProbeRefused)
		      .or(createFilter(candidate, uid, timeout).setType(DMT.ProbeError)));

		return filter;
	}

	private static MessageFilter createFilter(final PeerNode source, final long uid, final long timeout) {
		return MessageFilter.create().setSource(source).setField(DMT.UID, uid).setTimeout(timeout);
	}

	/**
	 * Depending on node settings, sends a message to source containing either a refusal or the requested result.
	 */
	private void respond(final Type type, final Listener listener) {

		if (!respondTo(type)) {
			listener.onRefused();
			return;
		}

		/*
		 * This adds noise to the results to make information less identifiable. The goal is making it difficult
		 * to determine which value a node actually has; that any given value could mean a small range of common
		 * values. Different result types have different sigma values such that one sigma contains multiple
		 * reasonable values.
		 */
		switch (type) {
		case BANDWIDTH:
			/*
			 * 5% noise:
			 * Reasonable output bandwidth limit is 20 KiB and people are likely to set limits in increments
			 * of 1 KiB. 1 KiB / 20 KiB = 0.05 sigma.
			 * 1,024 (2^10) bytes per KiB.
			 */
			listener.onOutputBandwidth((float)randomNoise((double)node.getOutputBandwidthLimit()/(1 << 10), 0.05));
			break;
		case BUILD:
			listener.onBuild(node.getNodeUpdater().getMainVersion());
			break;
		case IDENTIFIER:
			/*
			 * 5% noise:
			 * Reasonable uptime percentage is at least ~40 hours a week, or ~20%. This uptime is
			 * quantized so only something above a full percentage point (0.01 * 168 hours = 1.68 hours) of
			 * change will be guaranteed (from a percentage with a decimal component close to zero) to be
			 * reflected. 1% / 20% = 0.05 sigma.
			 *
			 * 7-day uptime with random noise, then quantized. Quantization is to make it very, very
			 * difficult to get useful information out of any given result because it is included with an
			 * identifier,
			 */
			long percent = Math.round(randomNoise(100*node.getUptimeEstimator().getUptimeWeek(), 0.05));
			//Clamp to byte.
			if (percent > Byte.MAX_VALUE) percent = Byte.MAX_VALUE;
			else if (percent < Byte.MIN_VALUE) percent = Byte.MIN_VALUE;
			listener.onIdentifier(probeIdentifier, (byte)percent);
			break;
		case LINK_LENGTHS:
			PeerNode[] peers = node.getConnectedPeers();
			float[] linkLengths = new float[peers.length];
			int i = 0;
			/*
			 * 1% noise:
			 * Link lengths are in the range [0.0, 0.5], and any change is enough to make the
			 * match not exact between locations. Taking as an example a link length of 0.2. and with the
			 * assumption that a change of 0.002 is enough to make it still useful for statistics but not
			 * useful for identification, 0.002 change / 0.2 link length = 0.01 sigma.
			 */
			double myLoc = node.getLocation();
			for (PeerNode peer : peers) {
				double peerLoc = peer.getLocation();
				if (Location.isValid(peerLoc)) {
					linkLengths[i++] = (float)randomNoise(Location.distance(myLoc, peerLoc), 0.01);
				}
			}
			linkLengths = java.util.Arrays.copyOf(linkLengths, i);
			java.util.Arrays.sort(linkLengths);
			listener.onLinkLengths(linkLengths);
			break;
		case LOCATION:
			listener.onLocation((float)node.getLocation());
			break;
		case STORE_SIZE:
			/*
			 * 5% noise:
			 * Reasonable datastore size is 20 GiB, and size is likely set in, at most, increments of 1 GiB.
			 * 1 GiB / 20 GiB = 0.05 sigma.
			 * 1,073,741,824 bytes (2^30) per GiB.
			 */
			listener.onStoreSize((float)randomNoise((double)node.getStoreSize()/(1 << 30), 0.05));
			break;
		case UPTIME_48H:
			/*
			 * 8% noise:
			 * Continuing with the assumption that reasonable weekly uptime is around 40 hours, this allows
			 * for 6 hours per day, 12 hours per 48 hours, or 25%. A half-hour seems a sufficient amount of
			 * ambiguity, so 0.5 hours / 48 hours ~= 1%, and 1% / 25% = 0.04 sigma.
			 */
			listener.onUptime((float)randomNoise(100*node.getUptimeEstimator().getUptime(), 0.04));
			break;
		case UPTIME_7D:
			/*
			 * 2.4% noise:
			 * As a 168-hour uptime covers a longer period 1 hour of ambiguity seems sufficient.
			 * 1 hour / 168 hours ~= 0.6%, and 0.6% / 20% = 0.03 sigma.
			 */
			listener.onUptime((float)randomNoise(100*node.getUptimeEstimator().getUptimeWeek(), 0.03));
			break;
		case REJECT_STATS:
			byte[] stats = node.getNodeStats().getNoisyRejectStats();
			listener.onRejectStats(stats);
			break;
		case OVERALL_BULK_OUTPUT_CAPACITY_USAGE:
			byte bandwidthClass = 
				DMT.bandwidthClassForCapacityUsage(node.getOutputBandwidthLimit());
			listener.onOverallBulkOutputCapacity(bandwidthClass, 
					(float)randomNoise(node.getNodeStats().getBandwidthLiabilityUsage(), 0.1));
			break;
		default:
			throw new UnsupportedOperationException("Missing response for " + type.name());
		}
	}

	private boolean respondTo(Type type) {
		switch (type){
		case BANDWIDTH: return respondBandwidth;
		case BUILD: return respondBuild;
		case IDENTIFIER: return respondIdentifier;
		case LINK_LENGTHS: return respondLinkLengths;
		case LOCATION: return respondLocation;
		case STORE_SIZE: return respondStoreSize;
		case UPTIME_48H:
		case UPTIME_7D: return respondUptime;
		case REJECT_STATS: return respondRejectStats;
		case OVERALL_BULK_OUTPUT_CAPACITY_USAGE: return respondOverallBulkOutputCapacityUsage;
		default: throw new UnsupportedOperationException("Missing permissions check for " + type.name());
		}
	}

	/**
	 * Decrements 20% of the time at HTL 1; otherwise always. This is to protect the responding node, whereas the
	 * anonymity of the node which initiated the request is not a concern.
	 * @param htl current HTL
	 * @return new HTL
	 */
	private byte probabilisticDecrement(byte htl) {
		assert htl > 0;
		if (htl == 1) {
			if (node.getRandom().nextFloat() < DECREMENT_PROBABILITY) return 0;
			return 1;
		}
		return (byte)(htl - 1);
	}

	/**
	 * Filter listener which determines the type of result and calls the appropriate probe listener method.
	 */
	private class ResultListener implements AsyncMessageFilterCallback {

		private final Listener listener;

		/**
		 * @param listener to call appropriate methods for events such as matched messages or timeout.
		 */
		public ResultListener(Listener listener) {
			this.listener = listener;
		}

		@Override
		public void onDisconnect(PeerContext context) {
			if (logDEBUG) Logger.debug(Probe.class, "Next node in chain disconnected.");
			listener.onError(Error.DISCONNECTED, null, true);
		}

		/**
		 * Parses provided message and calls appropriate Probe.Listener method for the type of result.
		 * @param message Probe result.
		 */
		@Override
		public void onMatched(Message message) {
			if(logDEBUG) Logger.debug(Probe.class, "Matched " + message.getSpec().getName());
			if (message.getSpec().equals(DMT.ProbeBandwidth)) {
				listener.onOutputBandwidth(message.getFloat(DMT.OUTPUT_BANDWIDTH_UPPER_LIMIT));
			} else if (message.getSpec().equals(DMT.ProbeBuild)) {
				listener.onBuild(message.getInt(DMT.BUILD));
			} else if (message.getSpec().equals(DMT.ProbeIdentifier)) {
				listener.onIdentifier(message.getLong(DMT.PROBE_IDENTIFIER), message.getByte(DMT.UPTIME_PERCENT));
			} else if (message.getSpec().equals(DMT.ProbeLinkLengths)) {
				listener.onLinkLengths(message.getFloatArray(DMT.LINK_LENGTHS));
			} else if (message.getSpec().equals(DMT.ProbeLocation)) {
				listener.onLocation(message.getFloat(DMT.LOCATION));
			} else if (message.getSpec().equals(DMT.ProbeStoreSize)) {
				listener.onStoreSize(message.getFloat(DMT.STORE_SIZE));
			} else if (message.getSpec().equals(DMT.ProbeUptime)) {
				listener.onUptime(message.getFloat(DMT.UPTIME_PERCENT));
			} else if (message.getSpec().equals(DMT.ProbeRejectStats)) {
				listener.onRejectStats(message.getShortBufferBytes(DMT.REJECT_STATS));
			} else if (message.getSpec().equals(DMT.ProbeOverallBulkOutputCapacityUsage)) {
				listener.onOverallBulkOutputCapacity(message.getByte(DMT.OUTPUT_BANDWIDTH_CLASS), message.getFloat(DMT.CAPACITY_USAGE));
			} else if (message.getSpec().equals(DMT.ProbeError)) {
				final byte rawError = message.getByte(DMT.TYPE);
				if (Error.isValid(rawError)) {
					listener.onError(Error.valueOf(rawError), null, false);
				} else {
					//Not recognized locally.
					listener.onError(Error.UNKNOWN, rawError, false);
				}
			} else if (message.getSpec().equals(DMT.ProbeRefused)) {
				listener.onRefused();
			}  else {
				throw new UnsupportedOperationException("Missing handling for " + message.getSpec().getName());
			}
		}

		@Override
		public void onRestarted(PeerContext context) {}

		@Override
		public void onTimeout() {
			if (logDEBUG) Logger.debug(Probe.class, "Timed out.");
			listener.onError(Error.TIMEOUT, null, true);
		}

		@Override
		public boolean shouldTimeout() {
			return false;
		}
	}

	/**
	 * Listener which relays responses to the node specified during construction. Used for received probe requests.
	 * This leads to reconstructing the messages, but removes potentially harmful sub-messages and also removes the
	 * need for duplicate message sending code elsewhere, If the result includes a trace this would be the place
	 * to add local results to it.
	 */
	private class ResultRelay implements Listener {

		private final PeerNode source;
		private final Long uid;

		/**
		 * @param source peer from which the request was received and to which send the response.
		 * @throws IllegalArgumentException if source is null.
		 */
		public ResultRelay(PeerNode source, Long uid) {
			this.source = source;
			this.uid = uid;
		}

		private void send(Message message) {
			if (!source.isConnected()) {
				if (logDEBUG) Logger.debug(Probe.class, SOURCE_DISCONNECT);
				return;
			}
			if (logDEBUG) Logger.debug(Probe.class, "Relaying " + message.getSpec().getName() + " back" +
			                                          " to " + source.userToString());
			try {
				source.sendAsync(message, null, Probe.this);
			} catch (NotConnectedException e) {
				if (logDEBUG) Logger.debug(Probe.class, SOURCE_DISCONNECT, e);
			}
		}

		@Override
		public void onError(Error error, Byte code, boolean local) {
			send(DMT.createProbeError(uid, error));
		}

		@Override
		public void onRefused() {
			send(DMT.createProbeRefused(uid));
		}

		@Override
		public void onOutputBandwidth(float outputBandwidth) {
			send(DMT.createProbeBandwidth(uid, outputBandwidth));
		}

		@Override
		public void onBuild(int build) {
			send(DMT.createProbeBuild(uid, build));
		}

		@Override
		public void onIdentifier(long identifier, byte uptimePercentage) {
			send(DMT.createProbeIdentifier(uid, identifier, uptimePercentage));
		}

		@Override
		public void onLinkLengths(float[] linkLengths) {
			send(DMT.createProbeLinkLengths(uid, linkLengths));
		}

		@Override
		public void onLocation(float location) {
			send(DMT.createProbeLocation(uid, location));
		}

		@Override
		public void onStoreSize(float storeSize) {
			send(DMT.createProbeStoreSize(uid, storeSize));
		}

		@Override
		public void onUptime(float uptimePercentage) {
			send(DMT.createProbeUptime(uid, uptimePercentage));
		}

		@Override
		public void onRejectStats(byte[] stats) {
			if(stats.length < 4) {
				Logger.warning(this, "Unknown length for stats: "+stats.length);
				onError(Error.UNKNOWN, Error.UNKNOWN.code, true);
			} else {
				if(stats.length > 4)
					stats = Arrays.copyOf(stats, 4);
				send(DMT.createProbeRejectStats(uid, stats));
			}
		}

		@Override
		public void onOverallBulkOutputCapacity(
				byte bandwidthClassForCapacityUsage, float capacityUsage) {
			send(DMT.createProbeOverallBulkOutputCapacityUsage(uid, bandwidthClassForCapacityUsage, capacityUsage));
			// TODO Auto-generated method stub
			
		}
	}
}
