package freenet.node;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import freenet.keys.NodeCHK;
import freenet.support.Logger;
import freenet.support.Ticker;

public class RequestTracker {
	
	private static volatile boolean logMINOR;
	private static volatile boolean logDEBUG;

	static {
		Logger.registerClass(RequestTracker.class);
	}
	
	// The runningLocal* are secondary. That is, we take the lock on the
	// corresponding running* when accessing runningLocal*. Local requests
	// have a tag in *both*.
	
	private final HashMap<Long,RequestTag> runningCHKGetUIDsBulk;
	private final HashMap<Long,RequestTag> runningLocalCHKGetUIDsBulk;
	private final HashMap<Long,RequestTag> runningSSKGetUIDsBulk;
	private final HashMap<Long,RequestTag> runningLocalSSKGetUIDsBulk;
	private final HashMap<Long,InsertTag> runningCHKPutUIDsBulk;
	private final HashMap<Long,InsertTag> runningLocalCHKPutUIDsBulk;
	private final HashMap<Long,InsertTag> runningSSKPutUIDsBulk;
	private final HashMap<Long,InsertTag> runningLocalSSKPutUIDsBulk;
	private final HashMap<Long,OfferReplyTag> runningCHKOfferReplyUIDsBulk;
	private final HashMap<Long,OfferReplyTag> runningSSKOfferReplyUIDsBulk;

	private final HashMap<Long,RequestTag> runningCHKGetUIDsRT;
	private final HashMap<Long,RequestTag> runningLocalCHKGetUIDsRT;
	private final HashMap<Long,RequestTag> runningSSKGetUIDsRT;
	private final HashMap<Long,RequestTag> runningLocalSSKGetUIDsRT;
	private final HashMap<Long,InsertTag> runningCHKPutUIDsRT;
	private final HashMap<Long,InsertTag> runningLocalCHKPutUIDsRT;
	private final HashMap<Long,InsertTag> runningSSKPutUIDsRT;
	private final HashMap<Long,InsertTag> runningLocalSSKPutUIDsRT;
	private final HashMap<Long,OfferReplyTag> runningCHKOfferReplyUIDsRT;
	private final HashMap<Long,OfferReplyTag> runningSSKOfferReplyUIDsRT;
	
	private final PeerManager peers;
	private final Ticker ticker;

	/** RequestSender's currently transferring, by key */
	private final HashMap<NodeCHK, RequestSender> transferringRequestSendersRT;
	private final HashMap<NodeCHK, RequestSender> transferringRequestSendersBulk;
	/** UIDs of RequestHandler's currently transferring */
	private final HashSet<Long> transferringRequestHandlers;
	
	RequestTracker(PeerManager peers, Ticker ticker) {
		this.peers = peers;
		this.ticker = ticker;
		runningCHKGetUIDsRT = new HashMap<Long,RequestTag>();
		runningLocalCHKGetUIDsRT = new HashMap<Long,RequestTag>();
		runningSSKGetUIDsRT = new HashMap<Long,RequestTag>();
		runningLocalSSKGetUIDsRT = new HashMap<Long,RequestTag>();
		runningCHKPutUIDsRT = new HashMap<Long,InsertTag>();
		runningLocalCHKPutUIDsRT = new HashMap<Long,InsertTag>();
		runningSSKPutUIDsRT = new HashMap<Long,InsertTag>();
		runningLocalSSKPutUIDsRT = new HashMap<Long,InsertTag>();
		runningCHKOfferReplyUIDsRT = new HashMap<Long,OfferReplyTag>();
		runningSSKOfferReplyUIDsRT = new HashMap<Long,OfferReplyTag>();

		runningCHKGetUIDsBulk = new HashMap<Long,RequestTag>();
		runningLocalCHKGetUIDsBulk = new HashMap<Long,RequestTag>();
		runningSSKGetUIDsBulk = new HashMap<Long,RequestTag>();
		runningLocalSSKGetUIDsBulk = new HashMap<Long,RequestTag>();
		runningCHKPutUIDsBulk = new HashMap<Long,InsertTag>();
		runningLocalCHKPutUIDsBulk = new HashMap<Long,InsertTag>();
		runningSSKPutUIDsBulk = new HashMap<Long,InsertTag>();
		runningLocalSSKPutUIDsBulk = new HashMap<Long,InsertTag>();
		runningCHKOfferReplyUIDsBulk = new HashMap<Long,OfferReplyTag>();
		runningSSKOfferReplyUIDsBulk = new HashMap<Long,OfferReplyTag>();
		
		transferringRequestSendersRT = new HashMap<NodeCHK, RequestSender>();
		transferringRequestSendersBulk = new HashMap<NodeCHK, RequestSender>();
		transferringRequestHandlers = new HashSet<Long>();
	}

	public boolean lockUID(UIDTag tag) {
		return lockUID(tag.uid, tag.isSSK(), tag.isInsert(), tag.isOfferReply(), tag.wasLocal(), tag.realTimeFlag, tag);
	}

	public boolean lockUID(long uid, boolean ssk, boolean insert, boolean offerReply, boolean local, boolean realTimeFlag, UIDTag tag) {
		// If these are switched around, we must remember to remove from both.
		if(offerReply) {
			// local irrelevant for OfferReplyTag's.
			HashMap<Long,OfferReplyTag> map = getOfferTracker(ssk, realTimeFlag);
			return innerLock(map, null, (OfferReplyTag)tag, uid, ssk, insert, offerReply, false);
		} else if(insert) {
			HashMap<Long,InsertTag> overallMap = getInsertTracker(ssk, false, realTimeFlag);
			HashMap<Long,InsertTag> localMap = local ? getInsertTracker(ssk, local, realTimeFlag) : null;
			return innerLock(overallMap, localMap, (InsertTag)tag, uid, ssk, insert, offerReply, local);
		} else {
			HashMap<Long,RequestTag> overallMap = getRequestTracker(ssk,false, realTimeFlag);
			HashMap<Long,RequestTag> localMap = local ? getRequestTracker(ssk,local, realTimeFlag) : null;
			return innerLock(overallMap, localMap, (RequestTag)tag, uid, ssk, insert, offerReply, local);
		}
	}

	private<T extends UIDTag> boolean innerLock(HashMap<Long, T> overallMap, HashMap<Long, T> localMap, T tag, Long uid, boolean ssk, boolean insert, boolean offerReply, boolean local) {
		synchronized(overallMap) {
			if(logMINOR) Logger.minor(this, "Locking "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+overallMap.size(), new Exception("debug"));
			T oldTag = overallMap.get(uid);
			if(oldTag != null) {
				if(oldTag == tag) {
					Logger.error(this, "Tag already registered: "+tag, new Exception("debug"));
				} else {
					return false;
				}
			}
			overallMap.put(uid, tag);
			if(logMINOR) Logger.minor(this, "Locked "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+overallMap.size());
			if(local) {
				if(logMINOR) Logger.minor(this, "Locking (local) "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+localMap.size(), new Exception("debug"));
				oldTag = localMap.get(uid);
				if(oldTag != null) {
					if(oldTag == tag) {
						Logger.error(this, "Tag already registered (local): "+tag, new Exception("debug"));
					} else {
						// Violates the invariant that local requests are always registered on the main (non-local) map too.
						Logger.error(this, "Different tag already registered (local) EVEN THOUGH NOT ON MAIN MAP: "+tag, new Exception("debug"));
						overallMap.remove(uid);
						return false;
					}
				}
				localMap.put(uid, tag);
				if(logMINOR) Logger.minor(this, "Locked (local) "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+localMap.size());
			}
		}
		return true;
	}

	/** Only used by UIDTag. */
	void unlockUID(UIDTag tag, boolean canFail, boolean noRecord) {
		unlockUID(tag.uid, tag.isSSK(), tag.isInsert(), canFail, tag.isOfferReply(), tag.wasLocal(), tag.realTimeFlag, tag, noRecord);
	}

	protected void unlockUID(long uid, boolean ssk, boolean insert, boolean canFail, boolean offerReply, boolean local, boolean realTimeFlag, UIDTag tag, boolean noRecord) {
		if(!noRecord)
			completed(uid);

		if(offerReply) {
			HashMap<Long,OfferReplyTag> map = getOfferTracker(ssk, realTimeFlag);
			innerUnlock(map, null, (OfferReplyTag)tag, uid, ssk, insert, offerReply, false, canFail);
		} else if(insert) {
			HashMap<Long,InsertTag> overallMap = getInsertTracker(ssk, false, realTimeFlag);
			HashMap<Long,InsertTag> localMap = local ? getInsertTracker(ssk,local, realTimeFlag) : null;
			innerUnlock(overallMap, localMap, (InsertTag)tag, uid, ssk, insert, offerReply, local, canFail);
		} else {
			HashMap<Long,RequestTag> overallMap = getRequestTracker(ssk, false, realTimeFlag);
			HashMap<Long,RequestTag> localMap = local ? getRequestTracker(ssk,local, realTimeFlag) : null;
			innerUnlock(overallMap, localMap, (RequestTag)tag, uid, ssk, insert, offerReply, local, canFail);
		}
	}

	/**
	 * Do the actual unlock.
	 * @param <T> The type of the tag.
	 * @param overallMap The overall map for this group of requests. LOCKING:
	 * We use the overallMap as lock for both.
	 * @param localMap The local map if any. We check on overallMap and then
	 * remove from both.
	 * @param tag The tag to remove.
	 * @param uid The UID of the tag.
	 * @param ssk Whether it is an SSK.
	 * @param insert Whether it is an insert.
	 * @param offerReply Whether it is an offer reply.
	 * @param local Whether it is local. If it is local we use both maps. If
	 * it is not we expect the latter to be null.
	 * @param canFail
	 */
	private<T extends UIDTag> void innerUnlock(HashMap<Long, T> overallMap, HashMap<Long, T> localMap, T tag, Long uid, boolean ssk, boolean insert, boolean offerReply, boolean local, boolean canFail) {
		synchronized(overallMap) {
			if(logMINOR) Logger.minor(this, "Unlocking "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+overallMap.size(), new Exception("debug"));
			if(overallMap.get(uid) != tag) {
				if(canFail) {
					if(logMINOR) Logger.minor(this, "Can fail and did fail: removing "+tag+" got "+overallMap.get(uid)+" for "+uid);
				} else {
					Logger.error(this, "Removing "+tag+" for "+uid+" returned "+overallMap.get(uid));
				}
			} else
				overallMap.remove(uid);
			if(logMINOR) Logger.minor(this, "Unlocked "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+overallMap.size());
			if(local) {
				if(localMap.get(uid) != tag) {
					if(canFail) {
						if(logMINOR) Logger.minor(this, "Can fail and did fail (local): removing "+tag+" got "+localMap.get(uid)+" for "+uid);
					} else {
						Logger.error(this, "Removing "+tag+" for "+uid+" returned (local) "+localMap.get(uid));
					}
				} else
					localMap.remove(uid);
				if(logMINOR) Logger.minor(this, "Unlocked (local) "+uid+" ssk="+ssk+" insert="+insert+" offerReply="+offerReply+" local="+local+" size="+localMap.size());
				
			} else {
				assert(localMap == null);
			}
		}
	}

	public static class CountedRequests {
		private int total;
		private int expectedTransfersOut;
		private int expectedTransfersIn;
		public int total() {
			return total;
		}
		public int expectedTransfersOut() {
			return expectedTransfersOut;
		}
		public int expectedTransfersIn() {
			return expectedTransfersIn;
		}
	}

	/** Count all requests running globally which match particular parameters.
	 * @param local If true, only include requests which originated locally.
	 * @param ssk If true, count SSK requests, if false, count CHK requests.
	 * @param insert If true, count inserts, otherwise count requests.
	 * @param offer If true, count offer replies (takes precedence over insert).
	 * @param realTimeFlag If true, count real-time requests, if false, count bulk requests. 
	 * @param transfersPerInsert Assume that any insert will cause this many outgoing transfers. 
	 * This is not predictable, so we use an average.
	 * @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local 
	 * (that is, count imaginary onward transfers etc depending on the request type).
	 * @param counter Transfer counts for all requests will be added to this counter object.
	 * @param counterSourceRestarted Transfer counts for requests whose source restarted (and so 
	 * are counted as local) will be added to this counter object. */
	public void countRequests(boolean local, boolean ssk, boolean insert, boolean offer, boolean realTimeFlag, int transfersPerInsert, boolean ignoreLocalVsRemote, CountedRequests counter, CountedRequests counterSourceRestarted) {
		HashMap<Long, ? extends UIDTag> map = getTracker(local, ssk, insert, offer, realTimeFlag);
		// Map is locked by the non-local version, although we're counting from the local version.
		HashMap<Long, ? extends UIDTag> mapLock = map;
		if(local)
			mapLock = getTracker(false, ssk, insert, offer, realTimeFlag);
		synchronized(mapLock) {
			int count = 0;
			int transfersOut = 0;
			int transfersIn = 0;
			int countSR = 0;
			int transfersOutSR = 0;
			int transfersInSR = 0;
			for(Map.Entry<Long, ? extends UIDTag> entry : map.entrySet()) {
				UIDTag tag = entry.getValue();
				// The overall running* map can include local. But the local map can't include non-local.
				if((!local) && tag.wasLocal) continue;
				int out = tag.expectedTransfersOut(ignoreLocalVsRemote, transfersPerInsert, true);
				int in = tag.expectedTransfersIn(ignoreLocalVsRemote, transfersPerInsert, true);
				count++;
				transfersOut += out;
				transfersIn += in;
				if(counterSourceRestarted != null && tag.countAsSourceRestarted()) {
					countSR++;
					transfersOutSR += out;
					transfersInSR += in;
				}
				if(logDEBUG) Logger.debug(this, "UID "+entry.getKey()+" : out "+transfersOut+" in "+transfersIn);
			}
			counter.total += count;
			counter.expectedTransfersIn += transfersIn;
			counter.expectedTransfersOut += transfersOut;
			if(counterSourceRestarted != null) {
				counterSourceRestarted.total += countSR;
				counterSourceRestarted.expectedTransfersIn += transfersInSR;
				counterSourceRestarted.expectedTransfersOut += transfersOutSR;
			}
		}
	}

	/**
	 * Count requests routed to a peer, or accepted from a peer, that match the specified criteria.
	 * PERFORMANCE: There is a map for all requests of a given type (local, ssk, etc). However this
	 * is not divided up by node. FIXME ideally we would countRequests for all PeerNode's
	 * simultaneously when we need data on more than one. FIXME it would be even better if we could
	 * just store the status on the PeerNode's, but the memory usage might be an issue and 
	 * synchronization would likely be problematic.
	 * @param source The peer the requests were accepted from or routed to.
	 * @param requestsToNode If true, count requests sent to the node and currently 
	 * running. If false, count requests originated by the node.
	 * @param local If true, only include requests which originated locally. 
	 * @param ssk If true, count SSK requests, if false, count CHK requests. 
	 * @param insert If true, count inserts, otherwise count requests.
	 * @param offer If true, count offer replies (takes precedence over insert).
	 * @param realTimeFlag If true, count real-time requests, if false, count bulk requests.
	 * @param transfersPerInsert Assume that any insert will cause this many outgoing transfers. 
	 * This is not predictable, so we use an average.
	 * @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local 
	 * (that is, count imaginary onward transfers etc depending on the request type).
	 * @param counter Transfer counts for all requests will be added to this counter object.
	 * @param counterSR Transfer counts for requests whose source restarted (and so 
	 * are counted as local) will be added to this counter object. */
	public void countRequests(PeerNode source, boolean requestsToNode, boolean local, boolean ssk, boolean insert, boolean offer, boolean realTimeFlag, int transfersPerInsert, boolean ignoreLocalVsRemote, CountedRequests counter, CountedRequests counterSR) {
		HashMap<Long, ? extends UIDTag> map = getTracker(local, ssk, insert, offer, realTimeFlag);
		// Map is locked by the non-local version, although we're counting from the local version.
		HashMap<Long, ? extends UIDTag> mapLock = map;
		if(local)
			mapLock = getTracker(false, ssk, insert, offer, realTimeFlag);
		synchronized(mapLock) {
			int count = 0;
			int transfersOut = 0;
			int transfersIn = 0;
			int countSR = 0;
			int transfersOutSR = 0;
			int transfersInSR = 0;
			if(!requestsToNode) {
				// If a request is adopted by us as a result of a timeout, it can be in the
				// remote map despite having source == null. However, if a request is in the
				// local map it will always have source == null.
				if(source != null && local) return;
				for(Map.Entry<Long, ? extends UIDTag> entry : map.entrySet()) {
					UIDTag tag = entry.getValue();
					// The overall running* map can include local. But the local map can't include non-local.
					if((!local) && tag.wasLocal) continue;
					if(tag.getSource() == source) {
						int out = tag.expectedTransfersOut(ignoreLocalVsRemote, transfersPerInsert, true);
						int in = tag.expectedTransfersIn(ignoreLocalVsRemote, transfersPerInsert, true);
						count++;
						transfersOut += out;
						transfersIn += in;
						if(counterSR != null && tag.countAsSourceRestarted()) {
							countSR++;
							transfersOutSR += out;
							transfersInSR += in;
						}
						if(logMINOR) Logger.minor(this, "Counting "+tag+" from "+entry.getKey()+" from "+source+" count now "+count+" out now "+transfersOut+" in now "+transfersIn);
					} else if(logDEBUG) Logger.debug(this, "Not counting "+entry.getKey());
				}
				if(logMINOR) Logger.minor(this, "Returning count: "+count+" in: "+transfersIn+" out: "+transfersOut);
				counter.total += count;
				counter.expectedTransfersIn += transfersIn;
				counter.expectedTransfersOut += transfersOut;
				if(counterSR != null) {
					counterSR.total += countSR;
					counterSR.expectedTransfersIn += transfersInSR;
					counterSR.expectedTransfersOut += transfersOutSR;
				}
			} else {
				// hasSourceRestarted is irrelevant for requests *to* a node.
				// FIXME improve efficiency!
				for(Map.Entry<Long, ? extends UIDTag> entry : map.entrySet()) {
					UIDTag tag = entry.getValue();
					// The overall running* map can include local. But the local map can't include non-local.
					if((!local) && tag.wasLocal) continue;
					// Ordinary requests can be routed to an offered key.
					// So we *DO NOT* care whether it's an ordinary routed relayed request or a GetOfferedKey, if we are counting outgoing requests.
					if(tag.currentlyFetchingOfferedKeyFrom(source)) {
						if(logMINOR) Logger.minor(this, "Counting "+tag+" to "+entry.getKey());
						transfersOut += tag.expectedTransfersOut(ignoreLocalVsRemote, transfersPerInsert, false);
						transfersIn += tag.expectedTransfersIn(ignoreLocalVsRemote, transfersPerInsert, false);
						count++;
					} else if(tag.currentlyRoutingTo(source)) {
						if(logMINOR) Logger.minor(this, "Counting "+tag+" to "+entry.getKey());
						transfersOut += tag.expectedTransfersOut(ignoreLocalVsRemote, transfersPerInsert, false);
						transfersIn += tag.expectedTransfersIn(ignoreLocalVsRemote, transfersPerInsert, false);
						count++;
					} else if(logDEBUG) Logger.debug(this, "Not counting "+entry.getKey());
				}
				if(logMINOR) Logger.minor(this, "Counted for "+(local?"local":"remote")+" "+(ssk?"ssk":"chk")+" "+(insert?"insert":"request")+" "+(offer?"offer":"")+" : "+count+" of "+map.size()+" for "+source);
				counter.total += count;
				counter.expectedTransfersIn += transfersIn;
				counter.expectedTransfersOut += transfersOut;
			}
		}
	}
	
	/**
	 * Count all requests, by the peer which originated the request.
	 * @param source The peer the requests were accepted from or routed to.
	 * @param local If true, only include requests which originated locally. 
	 * @param ssk If true, count SSK requests, if false, count CHK requests. 
	 * @param insert If true, count inserts, otherwise count requests.
	 * @param offer If true, count offer replies (takes precedence over insert).
	 * @param realTimeFlag If true, count real-time requests, if false, count bulk requests.
	 * @param transfersPerInsert Assume that any insert will cause this many outgoing transfers. 
	 * This is not predictable, so we use an average.
	 * @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local 
	 * (that is, count imaginary onward transfers etc depending on the request type).
	 * @param counterMap Map from PeerNode to CountedRequests counters. We will use "null" for 
	 * various cases: local requests, requested that have been adopted because their originator
	 * restarted, requests where the originator PeerNode has been removed from the routing table
	 * etc. */
	public void countAllRequestsByIncomingPeer(boolean requestsToNode, boolean local, boolean ssk, boolean insert, boolean offer, boolean realTimeFlag, int transfersPerInsert, boolean ignoreLocalVsRemote, Map<PeerNode, CountedRequests> counterMap) {
		HashMap<Long, ? extends UIDTag> map = getTracker(local, ssk, insert, offer, realTimeFlag);
		// Map is locked by the non-local version, although we're counting from the local version.
		HashMap<Long, ? extends UIDTag> mapLock = map;
		if(local)
			mapLock = getTracker(false, ssk, insert, offer, realTimeFlag);
		synchronized(mapLock) {
			if(!requestsToNode) {
				// If a request is adopted by us as a result of a timeout, it can be in the
				// remote map despite having source == null. However, if a request is in the
				// local map it will always have source == null.
				for(Map.Entry<Long, ? extends UIDTag> entry : map.entrySet()) {
					UIDTag tag = entry.getValue();
					// The overall running* map can include local. But the local map can't include non-local.
					if((!local) && tag.wasLocal) continue;
					PeerNode source = tag.getSource(); // Can be null in various cases
					CountedRequests counter = counterMap.get(source);
					if(counter == null) {
						counter = new CountedRequests();
						counterMap.put(source, counter);
					}
					int out = tag.expectedTransfersOut(ignoreLocalVsRemote, transfersPerInsert, true);
					int in = tag.expectedTransfersIn(ignoreLocalVsRemote, transfersPerInsert, true);
					counter.total++;
					counter.expectedTransfersIn += in;
					counter.expectedTransfersOut += out;
				}
			}
		}
	}
	
	public class WaitingForSlots {
		int local;
		int remote;
	}
	
	/**
	 * @return [0] is the number of local requests waiting for slots, [1] is the
	 * number of remote requests waiting for slots.
	 */
	public WaitingForSlots countRequestsWaitingForSlots() {
		WaitingForSlots slots = new WaitingForSlots();
		countRequestsWaitingForSlots(runningSSKGetUIDsRT, slots);
		countRequestsWaitingForSlots(runningCHKGetUIDsRT, slots);
		countRequestsWaitingForSlots(runningSSKPutUIDsRT, slots);
		countRequestsWaitingForSlots(runningCHKPutUIDsRT, slots);
		countRequestsWaitingForSlots(runningSSKOfferReplyUIDsRT, slots);
		countRequestsWaitingForSlots(runningCHKOfferReplyUIDsRT, slots);
		countRequestsWaitingForSlots(runningSSKGetUIDsBulk, slots);
		countRequestsWaitingForSlots(runningCHKGetUIDsBulk, slots);
		countRequestsWaitingForSlots(runningSSKPutUIDsBulk, slots);
		countRequestsWaitingForSlots(runningCHKPutUIDsBulk, slots);
		return slots;
	}
	
	private void countRequestsWaitingForSlots(HashMap<Long, ? extends UIDTag> runningUIDs, WaitingForSlots slots) {
		// FIXME use a counter, but that means make sure it always removes it when something bad happens.
		
		synchronized(runningUIDs) {
			for(UIDTag tag : runningUIDs.values()) {
				if(!tag.isWaitingForSlot()) continue;
				if(tag.isLocal())
					slots.local++;
				else
					slots.remote++;
			}
		}
	}

	void reassignTagToSelf(UIDTag tag) {
		// The tag remains remote, but we flag it as adopted.
		tag.reassignToSelf();
	}

	private HashMap<Long, ? extends UIDTag> getTracker(boolean local, boolean ssk,
			boolean insert, boolean offer, boolean realTimeFlag) {
		if(offer)
			return getOfferTracker(ssk, realTimeFlag);
		else if(insert)
			return getInsertTracker(ssk, local, realTimeFlag);
		else
			return getRequestTracker(ssk, local, realTimeFlag);
	}


	private HashMap<Long, RequestTag> getRequestTracker(boolean ssk, boolean local, boolean realTimeFlag) {
		if(realTimeFlag) {
			if(ssk) {
				return local ? runningLocalSSKGetUIDsRT : runningSSKGetUIDsRT;
			} else {
				return local ? runningLocalCHKGetUIDsRT : runningCHKGetUIDsRT;
			}
		} else {
			if(ssk) {
				return local ? runningLocalSSKGetUIDsBulk : runningSSKGetUIDsBulk;
			} else {
				return local ? runningLocalCHKGetUIDsBulk : runningCHKGetUIDsBulk;
			}
		}
	}

	private HashMap<Long, InsertTag> getInsertTracker(boolean ssk, boolean local, boolean realTimeFlag) {
		if(realTimeFlag) {
			if(ssk) {
				return local ? runningLocalSSKPutUIDsRT : runningSSKPutUIDsRT;
			} else {
				return local ? runningLocalCHKPutUIDsRT : runningCHKPutUIDsRT;
			}
		} else {
			if(ssk) {
				return local ? runningLocalSSKPutUIDsBulk : runningSSKPutUIDsBulk;
			} else {
				return local ? runningLocalCHKPutUIDsBulk : runningCHKPutUIDsBulk;
			}
		}
	}

	private HashMap<Long, OfferReplyTag> getOfferTracker(boolean ssk, boolean realTimeFlag) {
		if(realTimeFlag)
			return ssk ? runningSSKOfferReplyUIDsRT : runningCHKOfferReplyUIDsRT;
		else
			return ssk ? runningSSKOfferReplyUIDsBulk : runningCHKOfferReplyUIDsBulk;
	}

	// Must include bulk inserts so fairly long.
	// 21 minutes is enough for a fatal timeout.
	static final long TIMEOUT = MINUTES.toMillis(21);

	void startDeadUIDChecker() {
		ticker.queueTimedJob(deadUIDChecker, TIMEOUT);
	}

	private Runnable deadUIDChecker = new Runnable() {
		@Override
		public void run() {
			try {
				checkUIDs(runningSSKGetUIDsRT);
				checkUIDs(runningCHKGetUIDsRT);
				checkUIDs(runningSSKPutUIDsRT);
				checkUIDs(runningCHKPutUIDsRT);
				checkUIDs(runningSSKOfferReplyUIDsRT);
				checkUIDs(runningCHKOfferReplyUIDsRT);
				checkUIDs(runningSSKGetUIDsBulk);
				checkUIDs(runningCHKGetUIDsBulk);
				checkUIDs(runningSSKPutUIDsBulk);
				checkUIDs(runningCHKPutUIDsBulk);
				checkUIDs(runningSSKOfferReplyUIDsBulk);
				checkUIDs(runningCHKOfferReplyUIDsBulk);
			} finally {
				ticker.queueTimedJob(this, SECONDS.toMillis(60));
			}
		}

		private void checkUIDs(HashMap<Long, ? extends UIDTag> map) {
			Long[] uids;
			UIDTag[] tags;
			synchronized(map) {
				uids = map.keySet().toArray(new Long[map.size()]);
				tags = map.values().toArray(new UIDTag[map.size()]);
			}
			long now = System.currentTimeMillis();
			for(int i=0;i<uids.length;i++) {
				tags[i].maybeLogStillPresent(now, uids[i]);
			}
		}
	};
	

	public void onRestartOrDisconnect(PeerNode pn) {
		onRestartOrDisconnect(pn, runningSSKGetUIDsRT);
		onRestartOrDisconnect(pn, runningCHKGetUIDsRT);
		onRestartOrDisconnect(pn, runningSSKPutUIDsRT);
		onRestartOrDisconnect(pn, runningCHKPutUIDsRT);
		onRestartOrDisconnect(pn, runningSSKOfferReplyUIDsRT);
		onRestartOrDisconnect(pn, runningCHKOfferReplyUIDsRT);
		onRestartOrDisconnect(pn, runningSSKGetUIDsBulk);
		onRestartOrDisconnect(pn, runningCHKGetUIDsBulk);
		onRestartOrDisconnect(pn, runningSSKPutUIDsBulk);
		onRestartOrDisconnect(pn, runningCHKPutUIDsBulk);
		onRestartOrDisconnect(pn, runningSSKOfferReplyUIDsBulk);
		onRestartOrDisconnect(pn, runningCHKOfferReplyUIDsBulk);
	}

	private void onRestartOrDisconnect(PeerNode pn,
			HashMap<Long, ? extends UIDTag> uids) {
		synchronized(uids) {
			for(UIDTag tag : uids.values()) {
				if(tag.isSource(pn))
					tag.onRestartOrDisconnectSource();
			}
		}
	}
	
	public int getNumSSKRequests() {
		int total = 0;
		// running* include all requests, local and remote.
		synchronized(runningSSKGetUIDsBulk) {
			total += runningSSKGetUIDsBulk.size();
		}
		synchronized(runningSSKGetUIDsRT) {
			total += runningSSKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumCHKRequests() {
		int total = 0;
		synchronized(runningCHKGetUIDsBulk) {
			total += runningCHKGetUIDsBulk.size();
		}
		synchronized(runningCHKGetUIDsRT) {
			total += runningCHKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumSSKInserts() {
		int total = 0;
		synchronized(runningSSKPutUIDsBulk) {
			total += runningSSKPutUIDsBulk.size();
		}
		synchronized(runningSSKPutUIDsRT) {
			total += runningSSKPutUIDsRT.size();
		}
		return total;
	}

	public int getNumCHKInserts() {
		int total = 0;
		synchronized(runningCHKPutUIDsBulk) {
			total += runningCHKPutUIDsBulk.size();
		}
		synchronized(runningCHKPutUIDsRT) {
			total += runningCHKPutUIDsRT.size();
		}
		return total;
	}

	public int getNumLocalSSKRequests() {
		int total = 0;
		synchronized(runningSSKGetUIDsBulk) {
			total += runningLocalSSKGetUIDsBulk.size();
		}
		synchronized(runningSSKGetUIDsRT) {
			total += runningLocalSSKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumLocalCHKRequests() {
		int total = 0;
		synchronized(runningCHKGetUIDsBulk) {
			total += runningLocalCHKGetUIDsBulk.size();
		}
		synchronized(runningCHKGetUIDsRT) {
			total += runningLocalCHKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumRemoteCHKRequests() {
		int total = 0;
		synchronized(runningCHKGetUIDsBulk) {
			total += runningCHKGetUIDsBulk.size();
			total -= runningLocalCHKGetUIDsBulk.size();
		}
		synchronized(runningCHKGetUIDsRT) {
			total += runningCHKGetUIDsRT.size();
			total -= runningLocalCHKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumRemoteSSKRequests() {
		int total = 0;
		synchronized(runningSSKGetUIDsBulk) {
			total += runningSSKGetUIDsBulk.size();
			total -= runningLocalSSKGetUIDsBulk.size();
		}
		synchronized(runningSSKGetUIDsRT) {
			total += runningSSKGetUIDsRT.size();
			total -= runningLocalSSKGetUIDsRT.size();
		}
		return total;
	}

	public int getNumLocalCHKInserts() {
		int total = 0;
		synchronized(runningCHKPutUIDsBulk) {
			total += runningLocalCHKPutUIDsBulk.size();
		}
		synchronized(runningCHKPutUIDsRT) {
			total += runningLocalCHKPutUIDsRT.size();
		}
		return total;
	}

	public int getNumLocalSSKInserts() {
		int total = 0;
		synchronized(runningSSKPutUIDsBulk) {
			total += runningLocalSSKPutUIDsBulk.size();
		}
		synchronized(runningSSKPutUIDsRT) {
			total += runningLocalSSKPutUIDsRT.size();
		}
		return total;
	}

	public int getNumRemoteCHKInserts() {
		int total = 0;
		synchronized(runningCHKPutUIDsBulk) {
			total += runningCHKPutUIDsBulk.size() - runningLocalCHKPutUIDsBulk.size();
		}
		synchronized(runningCHKPutUIDsRT) {
			total += runningCHKPutUIDsRT.size() - runningLocalCHKPutUIDsRT.size();
		}
		return total;
	}

	public int getNumRemoteSSKInserts() {
		int total = 0;
		synchronized(runningSSKPutUIDsRT) {
			total += runningSSKPutUIDsRT.size() - runningLocalSSKPutUIDsRT.size();
		}
		synchronized(runningSSKPutUIDsBulk) {
			total += runningSSKPutUIDsBulk.size() - runningLocalSSKPutUIDsBulk.size();
		}
		return total;
	}

	public int getNumSSKOfferReplies() {
		int total = 0;
		synchronized(runningSSKOfferReplyUIDsRT) {
			total += runningSSKOfferReplyUIDsRT.size();
		}
		synchronized(runningSSKOfferReplyUIDsBulk) {
			total += runningSSKOfferReplyUIDsBulk.size();
		}
		return total;
	}

	public int getNumCHKOfferReplies() {
		int total = 0;
		synchronized(runningCHKOfferReplyUIDsRT) {
			total += runningCHKOfferReplyUIDsRT.size();
		}
		synchronized(runningCHKOfferReplyUIDsBulk) {
			total += runningCHKOfferReplyUIDsBulk.size();
		}
		return total;
	}

	public int getNumSSKOfferReplies(boolean realTimeFlag) {
		return realTimeFlag ? runningSSKOfferReplyUIDsRT.size() : runningSSKOfferReplyUIDsBulk.size();
	}

	public int getNumCHKOfferReplies(boolean realTimeFlag) {
		return realTimeFlag ? runningCHKOfferReplyUIDsRT.size() : runningCHKOfferReplyUIDsBulk.size();
	}

	public void addRunningUIDs(List<Long> list) {
		addRunningUIDs(runningSSKGetUIDsRT, list);
		addRunningUIDs(runningCHKGetUIDsRT, list);
		addRunningUIDs(runningSSKPutUIDsRT, list);
		addRunningUIDs(runningCHKPutUIDsRT, list);
		addRunningUIDs(runningSSKOfferReplyUIDsRT, list);
		addRunningUIDs(runningCHKOfferReplyUIDsRT, list);
		addRunningUIDs(runningSSKGetUIDsBulk, list);
		addRunningUIDs(runningCHKGetUIDsBulk, list);
		addRunningUIDs(runningSSKPutUIDsBulk, list);
		addRunningUIDs(runningCHKPutUIDsBulk, list);
		addRunningUIDs(runningSSKOfferReplyUIDsBulk, list);
		addRunningUIDs(runningCHKOfferReplyUIDsBulk, list);
	}
	
	private void addRunningUIDs(HashMap<Long, ? extends UIDTag> runningUIDs, List<Long> list) {
		synchronized(runningUIDs) {
			list.addAll(runningUIDs.keySet());
		}
	}

	public int getTotalRunningUIDsAlt() {
		return this.runningCHKGetUIDsRT.size() + this.runningCHKPutUIDsRT.size() + this.runningSSKGetUIDsRT.size() +
		this.runningSSKPutUIDsRT.size() + this.runningSSKOfferReplyUIDsRT.size() + this.runningCHKOfferReplyUIDsRT.size() +
		this.runningCHKGetUIDsBulk.size() + this.runningCHKPutUIDsBulk.size() + this.runningSSKGetUIDsBulk.size() +
		this.runningSSKPutUIDsBulk.size() + this.runningSSKOfferReplyUIDsBulk.size() + this.runningCHKOfferReplyUIDsBulk.size();
	}

	private ArrayList<Long> completedBuffer = new ArrayList<Long>();

	// Every this many slots, we tell all the PeerMessageQueue's to remove the old Items for the ID's in question.
	// This prevents memory DoS amongst other things.
	static final int COMPLETED_THRESHOLD = 128;

	/**
	 * A request completed (regardless of success).
	 */
	void completed(long id) {
		Long[] list;
		synchronized (completedBuffer) {
			completedBuffer.add(id);
			if(completedBuffer.size() < COMPLETED_THRESHOLD) return;
			list = completedBuffer.toArray(new Long[completedBuffer.size()]);
			completedBuffer.clear();
		}
		for(PeerNode pn : peers.myPeers()) {
			if(!pn.isRoutingCompatible()) continue;
			pn.removeUIDsFromMessageQueues(list);
		}
	}

	public RequestSender getTransferringRequestSenderByKey(NodeCHK key, boolean realTimeFlag) {
		HashMap<NodeCHK, RequestSender> transferringRequestSenders =
			realTimeFlag ? transferringRequestSendersRT : transferringRequestSendersBulk;
		synchronized(transferringRequestSenders) {
			return transferringRequestSenders.get(key);
		}
	}
	
	/**
	 * Add a transferring RequestSender to our HashMap.
	 * Should only be called by UIDTag.
	 */
	public void addTransferringSender(NodeCHK key, RequestSender sender) {
		HashMap<NodeCHK, RequestSender> transferringRequestSenders =
			sender.realTimeFlag ? transferringRequestSendersRT : transferringRequestSendersBulk;
		synchronized(transferringRequestSenders) {
			transferringRequestSenders.put(key, sender);
		}
	}

	/** Should only be called by RequestTag. */
	void addTransferringRequestHandler(long id) {
		synchronized(transferringRequestHandlers) {
			transferringRequestHandlers.add(id);
		}
	}

	/** Should only be called by RequestTag. */
	void removeTransferringRequestHandler(long id) {
		synchronized(transferringRequestHandlers) {
			transferringRequestHandlers.remove(id);
		}
	}

	/**
	 * Remove a sender from the set of currently transferring senders.
	 */
	public void removeTransferringSender(NodeCHK key, RequestSender sender) {
		HashMap<NodeCHK, RequestSender> transferringRequestSenders =
			sender.realTimeFlag ? transferringRequestSendersRT : transferringRequestSendersBulk;
		synchronized(transferringRequestSenders) {
//			RequestSender rs = (RequestSender) transferringRequestSenders.remove(key);
//			if(rs != sender) {
//				Logger.error(this, "Removed "+rs+" should be "+sender+" for "+key+" in removeTransferringSender");
//			}

			// Since there is no request coalescing, we only remove it if it matches,
			// and don't complain if it doesn't.
			if(transferringRequestSenders.get(key) == sender)
				transferringRequestSenders.remove(key);
		}
	}

	public int getNumTransferringRequestSenders() {
		int total = 0;
		synchronized(transferringRequestSendersRT) {
			total += transferringRequestSendersRT.size();
		}
		synchronized(transferringRequestSendersBulk) {
			total += transferringRequestSendersBulk.size();
		}
		return total;
	}

	public int getNumTransferringRequestHandlers() {
		synchronized(transferringRequestHandlers) {
			return transferringRequestHandlers.size();
		}
	}


}
