/* 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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.Message;
import freenet.io.comm.NotConnectedException;
import freenet.io.xfer.BlockTransmitter;
import freenet.io.xfer.BlockTransmitter.BlockTransmitterCompletion;
import freenet.io.xfer.PartiallyReceivedBlock;
import freenet.keys.CHKBlock;
import freenet.keys.Key;
import freenet.keys.KeyBlock;
import freenet.keys.NodeCHK;
import freenet.keys.NodeSSK;
import freenet.keys.SSKBlock;
import freenet.support.LRUMap;
import freenet.support.ListUtils;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.SerialExecutor;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;

// FIXME it is ESSENTIAL that we delete the ULPR data on requestors etc once we have found the key.
// Otherwise it will be much too easy to trace a request if an attacker busts the node afterwards.
// We can use an HMAC or something to authenticate offers.

// LOCKING: Always take the FailureTable lock first if you need both. Take the FailureTableEntry 
// lock only on cheap internal operations.

/**
 * Tracks recently DNFed keys, where they were routed to, what the location was at the time, who requested them.
 * Implements Ultra-Lightweight Persistent Requests: Refuse requests for a key for 10 minutes after it's DNFed 
 * (UNLESS we find a better route for the request), and when it is found, offer it to those who've asked for it
 * in the last hour.
 * LOCKING: Do not lock PeerNode before FailureTable/FailureTableEntry.
 * @author toad
 */
public class FailureTable {
	
	private static volatile boolean logMINOR;
	//private static volatile boolean logDEBUG;

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

	/** FailureTableEntry's by key. Note that we push an entry only when sentTime changes. */
	private final LRUMap<Key,FailureTableEntry> entriesByKey;
	/** BlockOfferList by key. Synchronized on self, as it doesn't interact with the main FT. */
	private final LRUMap<Key,BlockOfferList> blockOfferListByKey;
	private final Node node;
	
	/** Maximum number of keys to track */
	static final int MAX_ENTRIES = 20*1000;
	/** Maximum number of offers to track */
	static final int MAX_OFFERS = 10*1000;
	/** Terminate a request if there was a DNF on the same key less than 10 minutes ago.
	 * Maximum time for any FailureTable i.e. for this period after a DNF, we will avoid the node that 
	 * DNFed. */
	static final long REJECT_TIME = MINUTES.toMillis(10);
	/** Maximum time for a RecentlyFailed. I.e. until this period expires, we take a request into account
	 * when deciding whether we have recently failed to this peer. If we get a DNF, we use this figure.
	 * If we get a RF, we use what it tells us, which can be less than this. Most other failures use
	 * shorter periods. */
	static final long RECENTLY_FAILED_TIME = MINUTES.toMillis(30);
	/** After 1 hour we forget about an entry completely */
	static final long MAX_LIFETIME = MINUTES.toMillis(60);
	/** Offers expire after 10 minutes */
	static final long OFFER_EXPIRY_TIME = MINUTES.toMillis(10);
	/** HMAC key for the offer authenticator */
	final byte[] offerAuthenticatorKey;
	/** Clean up old data every 10 minutes to save memory and improve privacy */
	static final long CLEANUP_PERIOD = MINUTES.toMillis(10);

	FailureTable(Node node) {
		entriesByKey = LRUMap.createSafeMap();
		blockOfferListByKey = LRUMap.createSafeMap();
		this.node = node;
		offerAuthenticatorKey = new byte[32];
		node.random.nextBytes(offerAuthenticatorKey);
		offerExecutor = new SerialExecutor(NativeThread.HIGH_PRIORITY);
		node.ticker.queueTimedJob(new FailureTableCleaner(), CLEANUP_PERIOD);
	}
	
	public void start() {
		offerExecutor.start(node.executor, "FailureTable offers executor for "+node.getDarknetPortNumber());
	}
	
	/**
	 * Called when we route to a node and it fails for some reason, but we continue the request.
	 * Normally the timeout will be the time it took to route to that node and wait for its 
	 * response / timeout waiting for its response.
	 * @param key
	 * @param routedTo
	 * @param htl
	 * @param rfTimeout
	 * @param ftTimeout
	 */
	public void onFailed(Key key, PeerNode routedTo, short htl, long rfTimeout, long ftTimeout) {
		if(ftTimeout < 0 || ftTimeout > REJECT_TIME) {
			Logger.error(this, "Bogus timeout "+ftTimeout, new Exception("error"));
			ftTimeout = Math.max(Math.min(REJECT_TIME, ftTimeout), 0);
		}
		if(rfTimeout < 0 || rfTimeout > RECENTLY_FAILED_TIME) {
			if(rfTimeout > 0)
				Logger.error(this, "Bogus timeout "+rfTimeout, new Exception("error"));
			rfTimeout = Math.max(Math.min(RECENTLY_FAILED_TIME, rfTimeout), 0);
		}
		if(!(node.enableULPRDataPropagation || node.enablePerNodeFailureTables)) return;
		long now = System.currentTimeMillis();
		FailureTableEntry entry;
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null)
				entry = new FailureTableEntry(key);
			entriesByKey.push(key, entry);
			// LOCKING: Taking PeerNode then FT/FTE will deadlock.
			// However this should not happen.
			// We have to do this inside the lock to prevent race condition with the cleaner causing us to get dropped because isEmpty() before updating.
			entry.failedTo(routedTo, rfTimeout, ftTimeout, now, htl);

			trimEntries(now);
		}
	}
	
	/** When a request finishes with a failure, record who generated the failure
	 * so we don't route to them next time, and also who originated it so we can
	 * send the data back to them if we find them.
	 * ORDERING: You should generally call this *before* calling finish() to 
	 * avoid problems.
	 * LOCKING: NEVER synchronize on PeerNode before calling any FailureTable method.
	 */
	public void onFinalFailure(Key key, PeerNode routedTo, short htl, short origHTL, long rfTimeout, long ftTimeout, PeerNode requestor) {
		if(ftTimeout < -1 || ftTimeout > REJECT_TIME) {
			// -1 is a valid no-op.
			Logger.error(this, "Bogus timeout "+ftTimeout, new Exception("error"));
			ftTimeout = Math.max(Math.min(REJECT_TIME, ftTimeout), 0);
		}
		if(rfTimeout < 0 || rfTimeout > RECENTLY_FAILED_TIME) {
			if(rfTimeout > 0)
				Logger.error(this, "Bogus timeout "+rfTimeout, new Exception("error"));
			rfTimeout = Math.max(Math.min(RECENTLY_FAILED_TIME, rfTimeout), 0);
		}
		if(!(node.enableULPRDataPropagation || node.enablePerNodeFailureTables)) return;
		long now = System.currentTimeMillis();
		FailureTableEntry entry;
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null)
				entry = new FailureTableEntry(key);
			entriesByKey.push(key, entry);

			// LOCKING: Taking PeerNode then FT/FTE will deadlock.
			// However this should not happen.
			// We have to do this inside the lock to prevent race condition with the cleaner causing us to get dropped because isEmpty() before updating.
			
			if(routedTo != null)
				entry.failedTo(routedTo, rfTimeout, ftTimeout, now, htl);
			if(requestor != null)
				entry.addRequestor(requestor, now, origHTL);
			
			trimEntries(now);
		}
	}
	
	private synchronized void trimEntries(long now) {
		while(entriesByKey.size() > MAX_ENTRIES) {
			entriesByKey.popKey();
		}
	}

	// LOCKING: Synchronized on FailureTable because we need to remove self in deleteOffer(). 
	private final class BlockOfferList {
		private BlockOffer[] offers;
		final FailureTableEntry entry;
		
		BlockOfferList(FailureTableEntry entry, BlockOffer offer) {
			this.entry = entry;
			this.offers = new BlockOffer[] { offer };
		}

		public long expires() {
			synchronized(blockOfferListByKey) {
				long last = 0;
				for(BlockOffer offer: offers) {
					if(offer.offeredTime > last) last = offer.offeredTime;
				}
				return last + OFFER_EXPIRY_TIME;
			}
		}

		public boolean isEmpty(long now) {
			synchronized(blockOfferListByKey) {
				for(BlockOffer offer: offers) {
					if(!offer.isExpired(now)) return false;
				}
				return true;
			}
		}

		public void deleteOffer(BlockOffer offer) {
			if(logMINOR) Logger.minor(this, "Deleting "+offer+" from "+this);
			synchronized(blockOfferListByKey) {
				int idx = -1;
				final int offerLength = offers.length;
				for(int i=0;i<offerLength;i++) {
					if(offers[i] == offer) idx = i;
				}
				if(idx < 0) return;
				BlockOffer[] newOffers = new BlockOffer[offerLength - 1];
				if(idx > 0)
					System.arraycopy(offers, 0, newOffers, 0, idx);
				if(idx < newOffers.length)
					System.arraycopy(offers, idx + 1, newOffers, idx, offers.length - idx - 1);
				offers = newOffers;
				if(offers.length > 1) return;
				blockOfferListByKey.removeKey(entry.key);
			}
			node.clientCore.dequeueOfferedKey(entry.key);
		}

		public void addOffer(BlockOffer offer) {
			synchronized(blockOfferListByKey) {
				offers = Arrays.copyOf(offers, offers.length+1);
				offers[offers.length-1] = offer;
			}
		}
		
		@Override
		public String toString() {
			return super.toString()+"("+offers.length+")";
		}
	}
	
	static final class BlockOffer {
		final long offeredTime;
		/** Either offered by or offered to this node */
		final WeakReference<PeerNode> nodeRef;
		/** Authenticator */
		final byte[] authenticator;
		/** Boot ID when the offer was made */
		final long bootID;
		
		BlockOffer(PeerNode pn, long now, byte[] authenticator, long bootID) {
			this.nodeRef = pn.myRef;
			this.offeredTime = now;
			this.authenticator = authenticator;
			this.bootID = bootID;
		}

		public PeerNode getPeerNode() {
			return nodeRef.get();
		}

		public boolean isExpired(long now) {
			return nodeRef.get() == null || now > (offeredTime + OFFER_EXPIRY_TIME);
		}

		public boolean isExpired() {
			return isExpired(System.currentTimeMillis());
		}
	}
	
	/**
	 * Called when a data block is found (after it has been stored; there is a good chance of its being available in the
	 * near future). If there are nodes waiting for it, we will offer it to them. Removes the list of 
	 * nodes that offered the key too (but this is a separate operation).
	 * LOCKING: Never call when locked PeerNode, and try to avoid other locks as
	 * they might cause a deadlock. Schedule off-thread if necessary.
	 */
	public void onFound(KeyBlock block) {
		if(logMINOR) Logger.minor(this, "Found "+block.getKey());
		if(!(node.enableULPRDataPropagation || node.enablePerNodeFailureTables)) {
			if(logMINOR) Logger.minor(this, "Ignoring onFound because enable ULPR = "+node.enableULPRDataPropagation+" and enable failure tables = "+node.enablePerNodeFailureTables);
			return;
		}
		Key key = block.getKey();
		if(key == null) throw new NullPointerException();
		FailureTableEntry entry;
		synchronized(blockOfferListByKey) {
			blockOfferListByKey.removeKey(key);
		}
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null) {
				if(logMINOR) Logger.minor(this, "Key not found in entriesByKey");
				return; // Nobody cares
			}
			entriesByKey.removeKey(key);
		}
		if(logMINOR) Logger.minor(this, "Offering key");
		if(!node.enableULPRDataPropagation) return;
		entry.offer();
	}
	
	/** Run onOffer() on a separate thread since it can block for disk I/O, and we don't want to cause 
	 * transfer timeouts etc because of slow disk. */
	private final SerialExecutor offerExecutor;
	
	/**
	 * Called when we get an offer for a key. If this is an SSK, we will only accept it if we have previously asked for it.
	 * If it is a CHK, we will accept it if we want it.
	 * @param key The key we are being offered.
	 * @param peer The node offering it.
	 * @param authenticator 
	 */
	void onOffer(final Key key, final PeerNode peer, final byte[] authenticator) {
		if(!node.enableULPRDataPropagation) return;
		if(logMINOR)
			Logger.minor(this, "Offered key "+key+" by peer "+peer);
		FailureTableEntry entry;
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null) {
				if(logMINOR) Logger.minor(this, "We didn't ask for the key");
				return; // we haven't asked for it
			}
		}
		offerExecutor.execute(new Runnable() {
			@Override
			public void run() {
				innerOnOffer(key, peer, authenticator);
			}
		}, "onOffer()");
	}

	/**
	 * This method runs on the SerialExecutor. Therefore, any blocking network I/O needs to be scheduled
	 * on a separate thread. However, blocking disk I/O *should happen on this thread*. We deliberately
	 * serialise it, as high latencies can otherwise result.
	 */
	protected void innerOnOffer(Key key, PeerNode peer, byte[] authenticator) {
		if(logMINOR) Logger.minor(this, "Inner on offer for "+key+" from "+peer+" on "+node.getDarknetPortNumber());
		if(key.getRoutingKey() == null) throw new NullPointerException();
		//NB: node.hasKey() executes a datastore fetch
		// If we have the key in the datastore (store or cache), we don't want it.
		// If we have the key in the client cache, we might want it for other nodes,
		// although hopefully the client layer was tripped when we got it.
		if(node.hasKey(key, false, true)) {
			Logger.minor(this, "Already have key");
			return;
		}
		
		// Re-check after potentially long disk I/O.
		FailureTableEntry entry;
		long now = System.currentTimeMillis();
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null) {
				if(logMINOR) Logger.minor(this, "We didn't ask for the key");
				return; // we haven't asked for it
			}
		}

		/*
		 * Accept (subject to later checks) if we asked for it.
		 * Should we accept it if we were asked for it? This is "bidirectional propagation".
		 * It's good because it makes the whole structure much more reliable; it's bad because
		 * it's not entirely under our control - we didn't choose to route it to the node, the node
		 * routed it to us. Now it's found it before we did...
		 * 
		 * Attacks:
		 * - Frost spamming etc: Is it easier to offer data to our peers rather than inserting it? Will
		 * it result in it being propagated further? The peer node would then do the request, rather than
		 * this node doing an insert. Is that beneficial?
		 * 
		 * Not relevant with CHKs anyway.
		 * 
		 * On the plus side, propagation to nodes that have asked is worthwhile because reduced polling 
		 * cost enables more secure messaging systems e.g. outbox polling...
		 * - Social engineering: If a key is unpopular, you can put a different copy of it on different 
		 * nodes. You can then use this to trace the requestor - identify that he is or isn't on the target. 
		 * You can't do this with a regular insert because it will often go several nodes even at htl 0. 
		 * With subscriptions, you might be able to bypass this - but only if you know no other nodes in the
		 * neighbourhood are subscribed. Easier with SSKs; with CHKs you have only binary information of 
		 * whether the person got the key (with social engineering). Hard to exploit on darknet; if you're 
		 * that close to the suspect there are easier ways to get at them e.g. correlation attacks.
		 * 
		 * Conclusion: We should accept the request if:
		 * - We asked for it from that node. (Note that a node might both have asked us and been asked).
		 * - That node asked for it, and it's a CHK.
		 */
		
		boolean weAsked = entry.askedFromPeer(peer, now);
		boolean heAsked = entry.askedByPeer(peer, now);
		if(!(weAsked || heAsked)) {
			if(logMINOR) Logger.minor(this, "Not propagating key: weAsked="+weAsked+" heAsked="+heAsked);
			if(entry.isEmpty(now)) {
				synchronized(this) {
					entriesByKey.removeKey(key);
				}
			}
			return;
		}
		if(entry.isEmpty(now)) {
			synchronized(this) {
				entriesByKey.removeKey(key);
			}
		}
		
		// Valid offer.
		
		// Add to offers list
		
		synchronized(blockOfferListByKey) {			
			if(logMINOR) Logger.minor(this, "Valid offer");
			BlockOfferList bl = blockOfferListByKey.get(key);
			BlockOffer offer = new BlockOffer(peer, now, authenticator, peer.getBootID());
			if(bl == null) {
				bl = new BlockOfferList(entry, offer);
			} else {
				bl.addOffer(offer);
			}
			blockOfferListByKey.push(key, bl);
			trimOffersList(now);
		}
		
		// Accept the offer.
		// Either a peer wants it, in which case we want it for them,
		// or we want it, or we have requested it in the past, in which case
		// we will probably want it in the future.
		// FIXME: Not safe to queue offered keys as realtime????
		// For the same reason that priorities are not safe?
		// But do it at low priorities?
		// Offers mostly happen for SSKs anyway ... reconsider?
		node.clientCore.queueOfferedKey(key, false);
	}

	private void trimOffersList(long now) {
		synchronized(blockOfferListByKey) {
			while(true) {
				if(blockOfferListByKey.isEmpty()) return;
				BlockOfferList bl = blockOfferListByKey.peekValue();
				if(bl.isEmpty(now) || bl.expires() < now || blockOfferListByKey.size() > MAX_OFFERS) {
					if(logMINOR) Logger.minor(this, "Removing block offer list "+bl+" list size now "+blockOfferListByKey.size());
					blockOfferListByKey.popKey();
				} else {
					return;
				}
			}
		}
	}

	/**
	 * We offered a key, a node has responded to the offer. Note that this runs on the incoming
	 * packets thread so should allocate a new thread if it does anything heavy. Note also that
	 * it is responsible for unlocking the UID.
	 * @param key The key to send.
	 * @param isSSK Whether it is an SSK.
	 * @param uid The UID.
	 * @param source The node that asked for the key.
	 * @throws NotConnectedException If the sender ceases to be connected.
	 */
	public void sendOfferedKey(final Key key, final boolean isSSK, final boolean needPubKey, final long uid, final PeerNode source, final OfferReplyTag tag, final boolean realTimeFlag) throws NotConnectedException {
		this.offerExecutor.execute(new Runnable() {
			@Override
			public void run() {
				try {
					innerSendOfferedKey(key, isSSK, needPubKey, uid, source, tag, realTimeFlag);
				} catch (NotConnectedException e) {
					tag.unlockHandler();
					// Too bad.
				} catch (Throwable t) {
					tag.unlockHandler();
					Logger.error(this, "Caught "+t+" sending offered key", t);
				}
			}
		}, "sendOfferedKey");
	}

	/**
	 * This method runs on the SerialExecutor. Therefore, any blocking network I/O needs to be scheduled
	 * on a separate thread. However, blocking disk I/O *should happen on this thread*. We deliberately
	 * serialise it, as high latencies can otherwise result.
	 */
	protected void innerSendOfferedKey(Key key, final boolean isSSK, boolean needPubKey, final long uid, final PeerNode source, final OfferReplyTag tag, final boolean realTimeFlag) throws NotConnectedException {
		if(isSSK) {
			SSKBlock block = node.fetch((NodeSSK)key, false, false, false, false, true, null);
			if(block == null) {
				// Don't have the key
				source.sendAsync(DMT.createFNPGetOfferedKeyInvalid(uid, DMT.GET_OFFERED_KEY_REJECTED_NO_KEY), null, senderCounter);
				tag.unlockHandler();
				return;
			}
			
			final Message data = DMT.createFNPSSKDataFoundData(uid, block.getRawData(), realTimeFlag);
			Message headers = DMT.createFNPSSKDataFoundHeaders(uid, block.getRawHeaders(), realTimeFlag);
			final int dataLength = block.getRawData().length;
			
			source.sendAsync(headers, null, senderCounter);
			
			node.executor.execute(new PrioRunnable() {

				@Override
				public int getPriority() {
					return NativeThread.HIGH_PRIORITY;
				}

				@Override
				public void run() {
					try {
						source.sendSync(data, senderCounter, realTimeFlag);
						senderCounter.sentPayload(dataLength);
					} catch (NotConnectedException e) {
						// :(
					} catch (SyncSendWaitedTooLongException e) {
						// Impossible
					} finally {
						tag.unlockHandler();
					}
				}
				
			}, "Send offered SSK");
			
			if(needPubKey) {
				Message pk = DMT.createFNPSSKPubKey(uid, block.getPubKey(), realTimeFlag);
				source.sendAsync(pk, null, senderCounter);
			}
		} else {
			CHKBlock block = node.fetch((NodeCHK)key, false, false, false, false, true, null);
			if(block == null) {
				// Don't have the key
				source.sendAsync(DMT.createFNPGetOfferedKeyInvalid(uid, DMT.GET_OFFERED_KEY_REJECTED_NO_KEY), null, senderCounter);
				tag.unlockHandler();
				return;
			}
			Message df = DMT.createFNPCHKDataFound(uid, block.getRawHeaders());
			source.sendAsync(df, null, senderCounter);
        	PartiallyReceivedBlock prb =
        		new PartiallyReceivedBlock(Node.PACKETS_IN_BLOCK, Node.PACKET_SIZE, block.getRawData());
        	final BlockTransmitter bt =
        		new BlockTransmitter(node.usm, node.getTicker(), source, uid, prb, senderCounter, BlockTransmitter.NEVER_CASCADE,
        				new BlockTransmitterCompletion() {

					@Override
					public void blockTransferFinished(boolean success) {
						tag.unlockHandler();
					}
					
				}, realTimeFlag, node.nodeStats);
        	node.executor.execute(new PrioRunnable() {

				@Override
				public int getPriority() {
					return NativeThread.HIGH_PRIORITY;
				}

				@Override
				public void run() {
					bt.sendAsync();
				}
        		
        	}, "CHK offer sender");
		}
	}

	public final OfferedKeysByteCounter senderCounter = new OfferedKeysByteCounter();
	
	class OfferedKeysByteCounter implements ByteCounter {

		@Override
		public void receivedBytes(int x) {
			node.nodeStats.offeredKeysSenderReceivedBytes(x);
		}

		@Override
		public void sentBytes(int x) {
			node.nodeStats.offeredKeysSenderSentBytes(x);
		}

		@Override
		public void sentPayload(int x) {
			node.sentPayload(x);
			node.nodeStats.offeredKeysSenderSentBytes(-x);
		}
		
	}
	
	class OfferList {

		OfferList(BlockOfferList offerList) {
			this.offerList = offerList;
			recentOffers = new ArrayList<BlockOffer>();
			expiredOffers = new ArrayList<BlockOffer>();
			long now = System.currentTimeMillis();
			for(BlockOffer offer: offerList.offers) {
				if(!offer.isExpired(now))
					recentOffers.add(offer);
				else
					expiredOffers.add(offer);
			}
			if(logMINOR)
				Logger.minor(this, "Offers: "+recentOffers.size()+" recent "+expiredOffers.size()+" expired");
		}
		
		private final BlockOfferList offerList;
		
		private final List<BlockOffer> recentOffers;
		private final List<BlockOffer> expiredOffers;
		
		/** The last offer we returned */
		private BlockOffer lastOffer;
		
		public BlockOffer getFirstOffer() {
			if(lastOffer != null) {
				throw new IllegalStateException("Last offer not dealt with");
			}
			if(!recentOffers.isEmpty()) {
				return lastOffer = ListUtils.removeRandomBySwapLastSimple(node.random, recentOffers);
			}
			if(!expiredOffers.isEmpty()) {
				return lastOffer = ListUtils.removeRandomBySwapLastSimple(node.random, expiredOffers);
			}
			// No more offers.
			return null;
		}
		
		/**
		 * Delete the last offer - we have used it, successfully or not.
		 */
		public void deleteLastOffer() {
			offerList.deleteOffer(lastOffer);
			lastOffer = null;
		}

		/**
		 * Keep the last offer - we weren't able to use it e.g. because of RejectedOverload.
		 * Maybe it will be useful again in the future.
		 */
		public void keepLastOffer() {
			lastOffer = null;
		}
		
	}
	
	/** Have we had any offers for the key?
	 * @param key The key to check.
	 * @return True if there are any offers, false otherwise.
	 */
	public boolean hadAnyOffers(Key key) {
		synchronized(blockOfferListByKey) {
			return blockOfferListByKey.get(key) != null;
		}
	}

	public OfferList getOffers(Key key) {
		if(!node.enableULPRDataPropagation) return null;
		BlockOfferList bl;
		synchronized(blockOfferListByKey) {
			bl = blockOfferListByKey.get(key);
			if(bl == null) return null;
		}
		return new OfferList(bl);
	}

	/** Called when a node disconnects */
	public void onDisconnect(final PeerNode pn) {
		if(!(node.enableULPRDataPropagation || node.enablePerNodeFailureTables)) return;
		// FIXME do something (off thread if expensive)
	}

	public TimedOutNodesList getTimedOutNodesList(Key key) {
		if(!node.enablePerNodeFailureTables) return null;
		synchronized(this) {
			return entriesByKey.get(key);
		}
	}
	
	public class FailureTableCleaner implements Runnable {

		@Override
		public void run() {
			try {
				realRun();
			} catch (Throwable t) {
				Logger.error(this, "FailureTableCleaner caught "+t, t);
			} finally {
				node.ticker.queueTimedJob(this, CLEANUP_PERIOD);
			}
		}

		private void realRun() {
			if(logMINOR) Logger.minor(this, "Starting FailureTable cleanup");
			long startTime = System.currentTimeMillis();
			FailureTableEntry[] entries;
			synchronized(FailureTable.this) {
				entries = new FailureTableEntry[entriesByKey.size()];
				entriesByKey.valuesToArray(entries);
			}
			for(FailureTableEntry entry: entries) {
				if(entry.cleanup()) {
					synchronized(FailureTable.this) {
						synchronized(entry) {
						if(entry.isEmpty()) {
							if(logMINOR) Logger.minor(this, "Removing entry for "+entry.key);
							entriesByKey.removeKey(entry.key);
						}
						}
					}
				}
			}
			long endTime = System.currentTimeMillis();
			if(logMINOR) Logger.minor(this, "Finished FailureTable cleanup took "+(endTime-startTime)+"ms");
		}
	}

	public boolean peersWantKey(Key key, PeerNode apartFrom) {
		FailureTableEntry entry;
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null) return false; // Nobody cares
		}
		return entry.othersWant(apartFrom);
	}
        
        /** @return The lowest HTL at which any peer has requested this key recently */
	public short minOfferedHTL(Key key, short htl) {
		FailureTableEntry entry;
		synchronized(this) {
			entry = entriesByKey.get(key);
			if(entry == null) return htl;
		}
		return entry.minRequestorHTL(htl);
	}
}
