/* 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 static java.util.concurrent.TimeUnit.SECONDS;

import java.util.ArrayList;
import java.util.List;

import freenet.io.comm.AsyncMessageCallback;
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.io.comm.SlowAsyncMessageFilterCallback;
import freenet.io.xfer.AbortedException;
import freenet.io.xfer.BlockTransmitter;
import freenet.io.xfer.BlockTransmitter.BlockTransmitterCompletion;
import freenet.io.xfer.PartiallyReceivedBlock;
import freenet.keys.CHKBlock;
import freenet.keys.CHKVerifyException;
import freenet.keys.NodeCHK;
import freenet.support.Logger;
import freenet.support.io.NativeThread;

public final class CHKInsertSender extends BaseSender implements PrioRunnable, AnyInsertSender, ByteCounter {
	
	private class BackgroundTransfer implements PrioRunnable, SlowAsyncMessageFilterCallback {
		private final long uid;
		/** Node we are waiting for response from */
		final PeerNode pn;
		/** We may be sending data to that node */
		BlockTransmitter bt;
		/** Have we received notice of the downstream success
		 * or failure of dependant transfers from that node?
		 * Includes timing out. */
		boolean receivedCompletionNotice;
		/** Set when we fatally timeout, or when we get a completion other than a timeout. */
		boolean finishedWaiting;

		/** Was the notification of successful transfer? */
		boolean completionSucceeded;
		
		/** Have we completed the immediate transfer? */
		boolean completedTransfer;
		/** Did it succeed? */
		//boolean transferSucceeded;
		
		/** Do we have the InsertReply, RNF or similar completion? If not,
		 * there is no point starting to wait for a timeout. */
		boolean gotInsertReply;
		/** Have we started the first wait? We start waiting when we have 
		 * completed the transfer AND received an InsertReply, RNF or similar. */
		private boolean startedWait;
		/** Has the background transfer been terminated due to not receiving
		 * an InsertReply, or due to disconnection etc? */
		private boolean killed;
		
		private final InsertTag thisTag;
		
		BackgroundTransfer(final PeerNode pn, PartiallyReceivedBlock prb, InsertTag thisTag) {
			this.pn = pn;
			this.uid = CHKInsertSender.this.uid;
			this.thisTag = thisTag;
			bt = new BlockTransmitter(node.getUSM(), node.getTicker(), pn, uid, prb, CHKInsertSender.this, BlockTransmitter.NEVER_CASCADE,
					new BlockTransmitterCompletion() {

				@Override
				public void blockTransferFinished(boolean success) {
					if(logMINOR) Logger.minor(this, "Transfer completed: "+success+" for "+this);
					BackgroundTransfer.this.completedTransfer(success);
					// Double-check that the node is still connected. Pointless to wait otherwise.
					if (pn.isConnected() && success) {
						synchronized(backgroundTransfers) {
							if(!gotInsertReply) return;
							if(startedWait) return;
							startedWait = true;
						}
						startWait();
					} else {
						BackgroundTransfer.this.receivedNotice(false, false, false);
						pn.localRejectedOverload("TransferFailedInsert", realTimeFlag);
					}
				}
				
			}, realTimeFlag, node.getNodeStats());
		}
		
		/** Start waiting for an acknowledgement or timeout. Caller must ensure
		 * that the transfer has succeeded and we have received an RNF, InsertReply
		 * or other valid completion. The timeout is relative to that, since up
		 * to that point we could still be routing. */
		private void startWait() {
			if(logMINOR) Logger.minor(this, "Waiting for completion notification from "+this);
			//synch-version: this.receivedNotice(waitForReceivedNotification(this));
			//Add ourselves as a listener for the longterm completion message of this transfer, then gracefully exit.
			try {
				node.getUSM().addAsyncFilter(getNotificationMessageFilter(false), BackgroundTransfer.this, null);
			} catch (DisconnectedException e) {
				// Normal
				if(logMINOR)
					Logger.minor(this, "Disconnected while adding filter");
				BackgroundTransfer.this.completedTransfer(false);
				BackgroundTransfer.this.receivedNotice(false, false, true);
			}
		}
		
		void start() {
			node.getExecutor().execute(this, "CHKInsert-BackgroundTransfer for "+uid+" to "+pn.getPeer());
		}
		
		@Override
		public void run() {
			freenet.support.Logger.OSThread.logPID(this);
			try {
				this.realRun();
			} catch (Throwable t) {
				this.completedTransfer(false);
				this.receivedNotice(false, false, true);
				Logger.error(this, "Caught "+t, t);
			}
		}
		
		private void realRun() {
			bt.sendAsync();
			// REDFLAG: Load limiting:
			// No confirmation that it has finished, and it won't finish immediately on the transfer finishing.
			// So don't try to thisTag.removeRoutingTo(next), just assume it keeps running until the whole insert finishes.
		}
		
		private void completedTransfer(boolean success) {
			synchronized(backgroundTransfers) {
				//transferSucceeded = success; //FIXME Don't used
				completedTransfer = true;
				backgroundTransfers.notifyAll();
			}
			if(!success) {
				setTransferTimedOut();
			}
		}
		
		/** @param timeout Whether this completion is the result of a timeout.
		 * @return True if we should wait again, false if we have already received a notice or timed out. */
		private boolean receivedNotice(boolean success, boolean timeout, boolean kill) {
			if(logMINOR) Logger.minor(this, "Received notice: "+success+(timeout ? " (timeout)" : "")+" on "+this);
			boolean noUnlockPeer = false;
			boolean gotFatalTimeout = false;
			synchronized(backgroundTransfers) {
				if(finishedWaiting) {
					if(!(killed || kill))
						Logger.error(this, "Finished waiting already yet receivedNotice("+success+","+timeout+","+kill+")", new Exception("error"));
					return false;
				}
				if(killed) {
					// Do nothing. But do unlock.
				} else if(kill) {
					killed = true;
					finishedWaiting = true;
					receivedCompletionNotice = true;
					completionSucceeded = false;
				} else {
					if (receivedCompletionNotice) {
						// Two stage timeout.
						if(logMINOR) Logger.minor(this, "receivedNotice("+success+"), already had receivedNotice("+completionSucceeded+")");
						if(timeout) {
							// Fatal timeout.
							finishedWaiting = true;
							gotFatalTimeout = true;
						}
					} else {
						// Normal completion.
						completionSucceeded = success;
						receivedCompletionNotice = true;
						if(!timeout) // Any completion mode other than a timeout immediately sets finishedWaiting, because we won't wait any longer.
							finishedWaiting = true;
						else {
							// First timeout but not had second timeout yet.
							// Unlock downstream (below), but will wait here for the peer to fatally timeout.
							// UIDTag will automatically reassign to self when the time comes if we call handlingTimeout() here, and will avoid unnecessarily logging errors.
							// LOCKING: Note that it is safe to call the tag within the lock since we always take the UIDTag lock last.
							thisTag.handlingTimeout(pn);
							noUnlockPeer = true;
						}
					}
				}
				if(!noUnlockPeer)
					startedWait = true; // Prevent further wait's.
			}
			if((!gotFatalTimeout) && (!success)) {
				setTransferTimedOut();
			}
			if(!noUnlockPeer)
				// Downstream (away from originator), we need to stay locked on the peer until the fatal timeout / the delayed notice.
				// Upstream (towards originator), of course, we can unlockHandler() as soon as all the transfers are finished.
				// LOCKING: Do this outside the lock as pn can do heavy stuff in response (new load management).
				pn.noLongerRoutingTo(thisTag, false);
			synchronized(backgroundTransfers) {
				// Avoid "Unlocked handler but still routing to yet not reassigned".
				if(!gotFatalTimeout) {
					backgroundTransfers.notifyAll();
				}
			}
			if(timeout && gotFatalTimeout) {
				Logger.error(this, "Second timeout waiting for final ack from "+pn+" on "+this);
				pn.fatalTimeout(thisTag, false);
				return false;
			}
			return true;
		}
		
		@Override
		public void onMatched(Message m) {
			pn.successNotOverload(realTimeFlag);
			PeerNode pn = (PeerNode) m.getSource();
			// pn cannot be null, because the filters will prevent garbage collection of the nodes
			
			if(this.pn.equals(pn)) {
				boolean anyTimedOut = m.getBoolean(DMT.ANY_TIMED_OUT);
				if(anyTimedOut) {
					CHKInsertSender.this.setTransferTimedOut();
				}
				receivedNotice(!anyTimedOut, false, false);
			} else {
				Logger.error(this, "received completion notice for wrong node: "+pn+" != "+this.pn);
			}			
		}
		
		@Override
		public boolean shouldTimeout() {
			//AFIACS, this will still let the filter timeout, but not call onMatched() twice.
			return finishedWaiting;
		}
		
		private MessageFilter getNotificationMessageFilter(boolean longTimeoutAnyway) {
			return MessageFilter.create().setField(DMT.UID, uid).setType(DMT.FNPInsertTransfersCompleted).setSource(pn).setTimeout(longTimeoutAnyway ? TRANSFER_COMPLETION_ACK_TIMEOUT_BULK : transferCompletionTimeout);
		}

		@Override
		public void onTimeout() {
			/* FIXME: Cascading timeout...
			   if this times out, we don't have any time to report to the node of origin the timeout notification (anyTimedOut?).
			 */
			// NORMAL priority because it is normally caused by a transfer taking too long downstream, and that doesn't usually indicate a bug.
			Logger.normal(this, "Timed out waiting for a final ack from: "+pn+" on "+this, new Exception("debug"));
			if(receivedNotice(false, true, false)) {
				pn.localRejectedOverload("InsertTimeoutNoFinalAck", realTimeFlag);
				// First timeout. Wait for second timeout.
				try {
					node.getUSM().addAsyncFilter(getNotificationMessageFilter(true), this, CHKInsertSender.this);
				} catch (DisconnectedException e) {
					// Normal
					if(logMINOR)
						Logger.minor(this, "Disconnected while adding filter after first timeout");
					pn.noLongerRoutingTo(thisTag, false);
				}
			}
		}

		@Override
		public void onDisconnect(PeerContext ctx) {
			Logger.normal(this, "Disconnected "+ctx+" for "+this);
			receivedNotice(true, false, true); // as far as we know
			pn.noLongerRoutingTo(thisTag, false);
		}

		@Override
		public void onRestarted(PeerContext ctx) {
			Logger.normal(this, "Restarted "+ctx+" for "+this);
			receivedNotice(true, false, true);
			pn.noLongerRoutingTo(thisTag, false);
		}

		@Override
		public int getPriority() {
			return NativeThread.HIGH_PRIORITY;
		}
		
		@Override
		public String toString() {
			return super.toString()+":"+uid+":"+pn;
		}

		/** Called when we have received an InsertReply, RouteNotFound or other
		 * successful or quasi-successful completion to routing. */
		public void onCompleted() {
			synchronized(backgroundTransfers) {
				if(finishedWaiting) return;
				if(gotInsertReply) return;
				gotInsertReply = true;
				if(!completedTransfer) return;
				if(startedWait) return;
				startedWait = true;
			}
			startWait();
		}

		/** Called when we get a failure, e.g. DataInsertRejected. */
		public void kill() {
			Logger.normal(this, "Killed "+this);
			receivedNotice(false, false, true); // as far as we know
			pn.noLongerRoutingTo(thisTag, false);
		}
	}
	
	CHKInsertSender(NodeCHK myKey, long uid, InsertTag tag, byte[] headers, short htl, 
            PeerNode source, Node node, PartiallyReceivedBlock prb, boolean fromStore,
            boolean canWriteClientCache, boolean forkOnCacheable, boolean preferInsert, boolean ignoreLowBackoff, boolean realTimeFlag) {
		super(myKey, realTimeFlag, source, node, htl, uid);
        this.origUID = uid;
        this.origTag = tag;
        this.headers = headers;
        this.prb = prb;
        this.fromStore = fromStore;
        this.startTime = System.currentTimeMillis();
        this.backgroundTransfers = new ArrayList<BackgroundTransfer>();
        this.forkOnCacheable = forkOnCacheable;
        this.preferInsert = preferInsert;
        this.ignoreLowBackoff = ignoreLowBackoff;
        if(realTimeFlag) {
        	transferCompletionTimeout = TRANSFER_COMPLETION_ACK_TIMEOUT_REALTIME;
        } else {
        	transferCompletionTimeout = TRANSFER_COMPLETION_ACK_TIMEOUT_BULK;
        }
    }

	void start() {
		node.getExecutor().execute(this, "CHKInsertSender for UID "+uid+" on "+node.getDarknetPortNumber()+" at "+System.currentTimeMillis());
	}

	static boolean logMINOR;
	static boolean logDEBUG;
	
	static {
		Logger.registerClass(CHKInsertSender.class);
	}
	
    // Constants
    static final long ACCEPTED_TIMEOUT = SECONDS.toMillis(10);
    static final long TRANSFER_COMPLETION_ACK_TIMEOUT_REALTIME = MINUTES.toMillis(1);
    static final long TRANSFER_COMPLETION_ACK_TIMEOUT_BULK = MINUTES.toMillis(5);

    final long transferCompletionTimeout;
    
    // Basics
    final long origUID;
    final InsertTag origTag;
    private InsertTag forkedRequestTag;
    final byte[] headers; // received BEFORE creation => we handle Accepted elsewhere
    final PartiallyReceivedBlock prb;
    final boolean fromStore;
    private boolean receiveFailed;
    final long startTime;
    private final boolean forkOnCacheable;
    private final boolean preferInsert;
    private final boolean ignoreLowBackoff;

    
    /** List of nodes we are waiting for either a transfer completion
     * notice or a transfer completion from. Also used as a sync object for waiting for transfer completion. */
    private List<BackgroundTransfer> backgroundTransfers;
    
    /** Have all transfers completed and all nodes reported completion status? */
    private boolean allTransfersCompleted;
    
    /** Has a transfer timed out, either directly or downstream? */
    private volatile boolean transferTimedOut;
    
    private int status = -1;
    /** Still running */
    static final int NOT_FINISHED = -1;
    /** Successful insert */
    static final int SUCCESS = 0;
    /** Route not found */
    static final int ROUTE_NOT_FOUND = 1;
    /** Internal error */
    static final int INTERNAL_ERROR = 3;
    /** Timed out waiting for response */
    static final int TIMED_OUT = 4;
    /** Locally Generated a RejectedOverload */
    static final int GENERATED_REJECTED_OVERLOAD = 5;
    /** Could not get off the node at all! */
    static final int ROUTE_REALLY_NOT_FOUND = 6;
    /** Receive failed. Not used internally; only used by CHKInsertHandler. */
    static final int RECEIVE_FAILED = 7;
    
    @Override
	public String toString() {
        return super.toString()+" for "+uid;
    }
    
    @Override
    public void run() {
	    freenet.support.Logger.OSThread.logPID(this);
    	origTag.startedSender();
        try {
            routeRequests();
        } catch (Throwable t) {
            Logger.error(this, "Caught "+t, t);
        } finally {
        	// Always check: we ALWAYS set status, even if receiveFailed.
            int myStatus;
            synchronized (this) {
				myStatus = status;
			}
            if(myStatus == NOT_FINISHED)
            	finish(INTERNAL_ERROR, null);
            origTag.finishedSender();
        	if(forkedRequestTag != null)
        		forkedRequestTag.finishedSender();
        }
    }
    
	static final int MAX_HIGH_HTL_FAILURES = 5;
	
	@Override
    protected void routeRequests() {
        
        PeerNode next = null;
        // While in no-cache mode, we don't decrement HTL on a RejectedLoop or similar, but we only allow a limited number of such failures before RNFing.
        int highHTLFailureCount = 0;
        boolean starting = true;
        while(true) {
        	if(failIfReceiveFailed(null, null)) return; // don't need to set status as killed by CHKInsertHandler
            
        	if(origTag.shouldStop()) {
        		finish(SUCCESS, null);
        		return;
        	}
        	
            /*
             * If we haven't routed to any node yet, decrement according to the source.
             * If we have, decrement according to the node which just failed.
             * Because:
             * 1) If we always decrement according to source then we can be at max or min HTL
             * for a long time while we visit *every* peer node. This is BAD!
             * 2) The node which just failed can be seen as the requestor for our purposes.
             */
            // Decrement at this point so we can DNF immediately on reaching HTL 0.
            boolean canWriteStorePrev = node.canWriteDatastoreInsert(htl);
            if((!starting) && (!canWriteStorePrev)) {
            	// We always decrement on starting a sender.
            	// However, after that, if our HTL is above the no-cache threshold,
            	// we do not want to decrement the HTL for trivial rejections (e.g. RejectedLoop),
            	// because we would end up caching data too close to the originator.
            	// So allow 5 failures and then RNF.
            	if(highHTLFailureCount++ >= MAX_HIGH_HTL_FAILURES) {
            		if(logMINOR) Logger.minor(this, "Too many failures at non-cacheable HTL");
                    finish(ROUTE_NOT_FOUND, null);
                    return;
            	}
            	if(logMINOR) Logger.minor(this, "Allowing failure "+highHTLFailureCount+" htl is still "+htl);
            } else {
                htl = node.decrementHTL(hasForwarded ? next : source, htl);
                if(logMINOR) Logger.minor(this, "Decremented HTL to "+htl);
            }
            starting = false;
            boolean successNow = false;
            boolean noRequest = false;
            synchronized (this) {
            	if(htl <= 0) {
            		successNow = true;
            		// Send an InsertReply back
            		noRequest = !hasForwarded;
            	}
            }
            if(successNow) {
        		if(noRequest)
        			origTag.setNotRoutedOnwards();
        		finish(SUCCESS, null);
        		return;
            }
            
            if( node.canWriteDatastoreInsert(htl) && (!canWriteStorePrev) && forkOnCacheable && forkedRequestTag == null) {
            	// FORK! We are now cacheable, and it is quite possible that we have already gone over the ideal sink nodes,
            	// in which case if we don't fork we will miss them, and greatly reduce the insert's reachability.
            	// So we fork: Create a new UID so we can go over the previous hops again if they happen to be good places to store the data.
            	
            	// Existing transfers will keep their existing UIDs, since they copied the UID in the constructor.
            	// Both local and remote inserts can be forked here: If it has reached this HTL, it means it's already been routed to some nodes.
            	
            	uid = node.getClientCore().makeUID();
				forkedRequestTag = new InsertTag(false, InsertTag.START.REMOTE, source, realTimeFlag, uid, node);
				forkedRequestTag.reassignToSelf();
				forkedRequestTag.startedSender();
				forkedRequestTag.unlockHandler();
				forkedRequestTag.setAccepted();
            	Logger.normal(this, "FORKING CHK INSERT "+origUID+" to "+uid);
            	nodesRoutedTo.clear();
            	node.getTracker().lockUID(forkedRequestTag);
            }
            
            // Route it
            // Can backtrack, so only route to nodes closer than we are to target.
            next = node.getPeers().closerPeer(forkedRequestTag == null ? source : null, nodesRoutedTo, target, true, node.isAdvancedModeEnabled(), -1, null,
			        null, htl, ignoreLowBackoff ? Node.LOW_BACKOFF : 0, source == null, realTimeFlag, newLoadManagement);
			
            if(next == null) {
                // Backtrack
        		if(!hasForwarded)
        			origTag.setNotRoutedOnwards();
                finish(ROUTE_NOT_FOUND, null);
                return;
            }
			
            if(logMINOR) Logger.minor(this, "Routing insert to "+next);
            nodesRoutedTo.add(next);
            
            InsertTag thisTag = forkedRequestTag;
            if(forkedRequestTag == null) thisTag = origTag;
            
			if(failIfReceiveFailed(thisTag, next)) {
				// Need to tell the peer that the DataInsert is not forthcoming.
				// DataInsertRejected is overridden to work both ways.
				try {
					next.sendAsync(DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_RECEIVE_FAILED), null, this);
				} catch (NotConnectedException e) {
					// Ignore
				}
				return;
			}
			
			innerRouteRequests(next, thisTag);
			return;
		}
	}

    private void handleRejectedTimeout(Message msg, PeerNode next) {
    	// Some severe lag problem.
    	// However it is not fatal because we can be confident now that even if the DataInsert
    	// is delivered late, it will not be acted on. I.e. we are certain how many requests
    	// are running, which is what fatal timeouts are designed to deal with.
		Logger.warning(this, "Node timed out waiting for our DataInsert (" + msg + " from " + next
				+ ") after Accepted in insert - treating as fatal timeout");
		// Terminal overload
		// Try to propagate back to source
		next.localRejectedOverload("AfterInsertAcceptedRejectedTimeout", realTimeFlag);
		
		// We have always started the transfer by the time this is called, so we do NOT need to removeRoutingTo().
		finish(TIMED_OUT, next);
	}

	/** @return True if fatal i.e. we should try another node. */
	private boolean handleRejectedOverload(Message msg, PeerNode next, InsertTag thisTag) {
		// Probably non-fatal, if so, we have time left, can try next one
		if (msg.getBoolean(DMT.IS_LOCAL)) {
			next.localRejectedOverload("ForwardRejectedOverload6", realTimeFlag);
			if(logMINOR) Logger.minor(this,
					"Local RejectedOverload, moving on to next peer");
			// Give up on this one, try another
			return true;
		} else {
			forwardRejectedOverload();
		}
		return false; // Wait for any further response
	}

	private void handleRNF(Message msg, PeerNode next, InsertTag thisTag) {
		if(logMINOR) Logger.minor(this, "Rejected: RNF");
		short newHtl = msg.getShort(DMT.HTL);
		if(newHtl < 0) newHtl = 0;
		synchronized (this) {
			if (htl > newHtl)
				htl = newHtl;						
		}
		// Finished as far as this node is concerned - except for the data transfer, which will continue until it finishes.
		next.successNotOverload(realTimeFlag);
	}

	private void handleDataInsertRejected(Message msg, PeerNode next, InsertTag thisTag) {
		next.successNotOverload(realTimeFlag);
		short reason = msg
				.getShort(DMT.DATA_INSERT_REJECTED_REASON);
		if(logMINOR) Logger.minor(this, "DataInsertRejected: " + reason);
		if (reason == DMT.DATA_INSERT_REJECTED_VERIFY_FAILED) {
			if (fromStore) {
				// That's odd...
				Logger.error(this,"Verify failed on next node "
						+ next + " for DataInsert but we were sending from the store!");
			} else {
				try {
					if (!prb.allReceived())
						Logger.error(this,
								"Did not receive all packets but next node says invalid anyway!");
					else {
						// Check the data
						new CHKBlock(prb.getBlock(), headers,
								(NodeCHK) key);
						Logger.error(this,
								"Verify failed on " + next
								+ " but data was valid!");
					}
				} catch (CHKVerifyException e) {
					Logger.normal(this,
									"Verify failed because data was invalid");
				} catch (AbortedException e) {
					onReceiveFailed();
				}
			}
		} else if (reason == DMT.DATA_INSERT_REJECTED_RECEIVE_FAILED) {
			boolean recvFailed;
			synchronized(backgroundTransfers) {
				recvFailed = receiveFailed;
			}
			if (recvFailed) {
				if(logMINOR) Logger.minor(this, "Failed to receive data, so failed to send data");
			} else {
				try {
					if (prb.allReceived()) {
						// Probably caused by transient connectivity problems.
						// Only fatal timeouts warrant ERROR's because they indicate something seriously wrong that didn't result in a disconnection, and because they cause disconnections.
						Logger.warning(this, "Received all data but send failed to " + next);
					} else {
						if (prb.isAborted()) {
							Logger.normal(this, "Send failed: aborted: " + prb.getAbortReason() + ": " + prb.getAbortDescription());
						} else
							Logger.normal(this, "Send failed; have not yet received all data but not aborted: " + next);
					}
				} catch (AbortedException e) {
					onReceiveFailed();
				}
			}
		}
		Logger.error(this, "DataInsert rejected! Reason="
				+ DMT.getDataInsertRejectedReason(reason));
	}

	@Override
	protected MessageFilter makeAcceptedRejectedFilter(PeerNode next, long acceptedTimeout, UIDTag tag) {
		// Use the right UID here, in case we fork on cacheable.
		final long uid = tag.uid;
        MessageFilter mfAccepted = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(acceptedTimeout).setType(DMT.FNPAccepted);
        MessageFilter mfRejectedLoop = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(acceptedTimeout).setType(DMT.FNPRejectedLoop);
        MessageFilter mfRejectedOverload = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(acceptedTimeout).setType(DMT.FNPRejectedOverload);
        
        // mfRejectedOverload must be the last thing in the or
        // So its or pointer remains null
        // Otherwise we need to recreate it below
        mfRejectedOverload.clearOr();
        return mfAccepted.or(mfRejectedLoop.or(mfRejectedOverload));
	}

	private static final long TIMEOUT_AFTER_ACCEPTEDREJECTED_TIMEOUT = MINUTES.toMillis(1);

	@Override
	protected void handleAcceptedRejectedTimeout(final PeerNode next, final UIDTag tag) {
		// It could still be running. So the timeout is fatal to the node.
		// This is a WARNING not an ERROR because it's possible that the problem is we simply haven't been able to send the message yet, because we don't use sendSync().
		// FIXME use a callback to rule this out and log an ERROR.
		Logger.warning(this, "Timeout awaiting Accepted/Rejected "+this+" to "+next);
		// Use the right UID here, in case we fork.
		final long uid = tag.uid;
		tag.handlingTimeout(next);
		// The node didn't accept the request. So we don't need to send them the data.
		// However, we do need to wait a bit longer to try to postpone the fatalTimeout().
		// Somewhat intricate logic to try to avoid fatalTimeout() if at all possible.
		MessageFilter mf = makeAcceptedRejectedFilter(next, TIMEOUT_AFTER_ACCEPTEDREJECTED_TIMEOUT, tag);
		try {
			node.getUSM().addAsyncFilter(mf, new SlowAsyncMessageFilterCallback() {

				@Override
				public void onMatched(Message m) {
					if(m.getSpec() == DMT.FNPRejectedLoop ||
							m.getSpec() == DMT.FNPRejectedOverload) {
						// Ok.
						next.noLongerRoutingTo(tag, false);
					} else {
						assert(m.getSpec() == DMT.FNPAccepted);
						if(logMINOR)
							Logger.minor(this, "Accepted after timeout on "+CHKInsertSender.this+" - will not send DataInsert, waiting for RejectedTimeout");
						// We are not going to send the DataInsert.
						// We have moved on, and we don't want inserts to fork unnecessarily.
						// However, we need to send a DataInsertRejected, or two-stage timeout will happen.
						try {
							next.sendAsync(DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_TIMEOUT_WAITING_FOR_ACCEPTED), new AsyncMessageCallback() {

								@Override
								public void sent() {
									// Ignore.
									if(logDEBUG) Logger.debug(this, "DataInsertRejected sent after accepted timeout on "+CHKInsertSender.this);
								}

								@Override
								public void acknowledged() {
									if(logDEBUG) Logger.debug(this, "DataInsertRejected acknowledged after accepted timeout on "+CHKInsertSender.this);
									next.noLongerRoutingTo(tag, false);
								}

								@Override
								public void disconnected() {
									if(logDEBUG) Logger.debug(this, "DataInsertRejected peer disconnected after accepted timeout on "+CHKInsertSender.this);
									next.noLongerRoutingTo(tag, false);
								}

								@Override
								public void fatalError() {
									if(logDEBUG) Logger.debug(this, "DataInsertRejected fatal error after accepted timeout on "+CHKInsertSender.this);
									next.noLongerRoutingTo(tag, false);
								}
								
							}, CHKInsertSender.this);
						} catch (NotConnectedException e) {
							next.noLongerRoutingTo(tag, false);
						}
					}
				}

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

				@Override
				public void onTimeout() {
					Logger.error(this, "Fatal: No Accepted/Rejected for "+CHKInsertSender.this);
					next.fatalTimeout(tag, false);
				}

				@Override
				public void onDisconnect(PeerContext ctx) {
					next.noLongerRoutingTo(tag, false);
				}

				@Override
				public void onRestarted(PeerContext ctx) {
					next.noLongerRoutingTo(tag, false);
				}

				@Override
				public int getPriority() {
					return NativeThread.NORM_PRIORITY;
				}
				
			}, this);
		} catch (DisconnectedException e) {
			next.noLongerRoutingTo(tag, false);
		}
	}

	private BackgroundTransfer startBackgroundTransfer(PeerNode node, PartiallyReceivedBlock prb, InsertTag tag) {
		BackgroundTransfer ac = new BackgroundTransfer(node, prb, tag);
		synchronized(backgroundTransfers) {
			backgroundTransfers.add(ac);
			backgroundTransfers.notifyAll();
		}
		ac.start();
		return ac;
	}
	
	private boolean hasForwardedRejectedOverload;
    
    synchronized boolean receivedRejectedOverload() {
    	return hasForwardedRejectedOverload;
    }
    
    /** Forward RejectedOverload to the request originator.
     * DO NOT CALL if have a *local* RejectedOverload.
     */
    @Override
    protected synchronized void forwardRejectedOverload() {
    	if(hasForwardedRejectedOverload) return;
    	hasForwardedRejectedOverload = true;
   		notifyAll();
	}
	
	private void setTransferTimedOut() {
		synchronized(this) {
			if(!transferTimedOut) {
				transferTimedOut = true;
				notifyAll();
			}
		}
	}
    
    /**
     * Finish the insert process. Will set status, wait for underlings to complete, and report success
     * if appropriate.
     * @param code The status code to set. 
     * @param next The node we successfully inserted to.
     */
    private void finish(int code, PeerNode next) {
    	if(logMINOR) Logger.minor(this, "Finished: "+code+" on "+this, new Exception("debug"));
     
    	// If there is an InsertReply, it always happens before the transfer completion notice.
    	// So we do NOT need to removeRoutingTo().
    	
        synchronized(this) {
        	if(allTransfersCompleted) return; // Already called. Doesn't prevent race condition resulting in the next bit running but that's not really a problem.
        	if((code == ROUTE_NOT_FOUND) && !hasForwarded)
        		code = ROUTE_REALLY_NOT_FOUND;

        	if(status != NOT_FINISHED) {
        		if(status == RECEIVE_FAILED) {
        			if(code == SUCCESS)
        				Logger.error(this, "Request succeeded despite receive failed?! on "+this);
        		} else if(status != TIMED_OUT)
        			throw new IllegalStateException("finish() called with "+code+" when was already "+status);
        	} else {
                status = code;
        	}
        	
        	notifyAll();
        	if(logMINOR) Logger.minor(this, "Set status code: "+getStatusString()+" on "+uid);
        }
		
        boolean failedRecv = false; // receiveFailed is protected by backgroundTransfers but status by this
        // Now wait for transfers, or for downstream transfer notifications.
        // Note that even the data receive may not have completed by this point.
        boolean mustWait = false;
		synchronized(backgroundTransfers) {
			if (backgroundTransfers.isEmpty()) {
				if(logMINOR) Logger.minor(this, "No background transfers");
				failedRecv = receiveFailed;
			} else {
				mustWait = true;
			}
		}
		if(mustWait) { 
			waitForBackgroundTransferCompletions();
			synchronized(backgroundTransfers) {
				failedRecv = receiveFailed;
			}
		}
        
		synchronized(this) {
			// waitForBackgroundTransferCompletions() may have already set it.
			if(!allTransfersCompleted) {
				if(failedRecv)
					status = RECEIVE_FAILED;
				allTransfersCompleted = true;
				notifyAll();
			}
		}
        	
        if(status == SUCCESS && next != null)
        	next.onSuccess(true, false);
        
        if(logMINOR) Logger.minor(this, "Returning from finish()");
    }

    @Override
    public synchronized int getStatus() {
        return status;
    }
    
    @Override
    public synchronized short getHTL() {
        return htl;
    }
    
    public boolean failIfReceiveFailed(InsertTag tag, PeerNode next) {
    	synchronized(backgroundTransfers) {
    		if(!receiveFailed) return false;
    	}
    	if(logMINOR) Logger.minor(this, "Failing because receive failed on "+this);
    	if(tag != null && next != null) {
   			next.noLongerRoutingTo(tag, false);
    	}
    	return true;
    }

    /**
     * Called by CHKInsertHandler to notify that the receive has
     * failed.
     */
    public void onReceiveFailed() {
    	if(logMINOR) Logger.minor(this, "Receive failed on "+this);
    	synchronized(backgroundTransfers) {
    		receiveFailed = true;
    		backgroundTransfers.notifyAll();
    		// Locking is safe as UIDTag always taken last.
    		for(BackgroundTransfer t : backgroundTransfers)
    			t.thisTag.handlingTimeout(t.pn);
    	}
    	// Set status immediately.
    	// The code (e.g. waitForStatus()) relies on a status eventually being set,
    	// so we may as well set it here. The alternative is to set it in realRun()
    	// when we notice that receiveFailed = true.
    	synchronized(this) {
    		status = RECEIVE_FAILED;
    		allTransfersCompleted = true;
    		notifyAll();
    	}
    	// Do not call finish(), that can only be called on the main thread and it will block.
    }

    /**
     * @return The current status as a string
     */
    @Override
    public synchronized String getStatusString() {
        if(status == SUCCESS)
            return "SUCCESS";
        if(status == ROUTE_NOT_FOUND)
            return "ROUTE NOT FOUND";
        if(status == NOT_FINISHED)
            return "NOT FINISHED";
        if(status == INTERNAL_ERROR)
        	return "INTERNAL ERROR";
        if(status == TIMED_OUT)
        	return "TIMED OUT";
        if(status == GENERATED_REJECTED_OVERLOAD)
        	return "GENERATED REJECTED OVERLOAD";
        if(status == ROUTE_REALLY_NOT_FOUND)
        	return "ROUTE REALLY NOT FOUND";
        return "UNKNOWN STATUS CODE: "+status;
    }

	@Override
	public synchronized boolean sentRequest() {
		return hasForwarded;
	}
		
		private void waitForBackgroundTransferCompletions() {
			try {
				freenet.support.Logger.OSThread.logPID(this);
				if(logMINOR) Logger.minor(this, "Waiting for background transfer completions: "+this);
				
				// We must presently be at such a stage that no more background transfers will be added.
				
				BackgroundTransfer[] transfers;
				synchronized(backgroundTransfers) {
					transfers = new BackgroundTransfer[backgroundTransfers.size()];
					transfers = backgroundTransfers.toArray(transfers);
				}
				
				// Wait for the outgoing transfers to complete.
				if(!waitForBackgroundTransfers(transfers)) {
					setTransferTimedOut();
					return;
				}
			} finally {
				synchronized(CHKInsertSender.this) {
					allTransfersCompleted = true;
					CHKInsertSender.this.notifyAll();
				}
			}
		}

		/**
		 * Block until all transfers have reached a final-terminal state (success/failure). On success this means that a
		 * successful 'received-notification' has been received.
		 * @return True if all background transfers were successful.
		 */
		private boolean waitForBackgroundTransfers(BackgroundTransfer[] transfers) {
			long start = System.currentTimeMillis();
			// Generous deadline so we catch bugs more obviously
			long deadline = start + transferCompletionTimeout * 3;
			// MAYBE all done
			while(true) {
				if(System.currentTimeMillis() > deadline) {
					// NORMAL priority because it is normally caused by a transfer taking too long downstream, and that doesn't usually indicate a bug.
					Logger.normal(this, "Timed out waiting for background transfers! Probably caused by async filter not getting a timeout notification! DEBUG ME!");
					return false;
				}
				//If we want to be sure to exit as-soon-as the transfers are done, then we must hold the lock while we check.
				synchronized(backgroundTransfers) {
					if(receiveFailed) return false;
					
					boolean noneRouteable = true;
					boolean completedTransfers = true;
					boolean completedNotifications = true;
					boolean someFailed = false;
					for(BackgroundTransfer transfer: transfers) {
						if(!transfer.pn.isRoutable()) {
							if(logMINOR)
								Logger.minor(this, "Ignoring transfer to "+transfer.pn+" for "+this+" as not routable");
							continue;
						}
						noneRouteable = false;
						if(!transfer.completedTransfer) {
							if(logMINOR)
								Logger.minor(this, "Waiting for transfer completion to "+transfer.pn+" : "+transfer);
							//must wait
							completedTransfers = false;
							break;
						}
						if (!transfer.receivedCompletionNotice) {
							if(logMINOR)
								Logger.minor(this, "Waiting for completion notice from "+transfer.pn+" : "+transfer);
							//must wait
							completedNotifications = false;
							break;
						}
						if (!transfer.completionSucceeded)
							someFailed = true;
					}
					if(noneRouteable) return false;
					if(completedTransfers && completedNotifications) return !someFailed;
					
					if(logMINOR) Logger.minor(this, "Waiting: transfer completion=" + completedTransfers + " notification="+completedNotifications); 
					try {
						backgroundTransfers.wait(SECONDS.toMillis(100));
					} catch (InterruptedException e) {
						// Ignore
					}
				}				
			}
		}


	public synchronized boolean completed() {
		return allTransfersCompleted;
	}

	/** Block until status has been set to something other than NOT_FINISHED */
	public synchronized void waitForStatus() {
		while(status == NOT_FINISHED) {
			try {
				CHKInsertSender.this.wait(SECONDS.toMillis(100));
			} catch (InterruptedException e) {
				// Ignore
			}
		}
	}

	public boolean anyTransfersFailed() {
		return transferTimedOut;
	}

	public byte[] getPubkeyHash() {
		return headers;
	}

	public byte[] getHeaders() {
		return headers;
	}

	@Override
	public long getUID() {
		return uid;
	}

	private final Object totalBytesSync = new Object();
	private int totalBytesSent;
	
	@Override
	public void sentBytes(int x) {
		synchronized(totalBytesSync) {
			totalBytesSent += x;
		}
		node.getNodeStats().insertSentBytes(false, x);
	}
	
	public int getTotalSentBytes() {
		synchronized(totalBytesSync) {
			return totalBytesSent;
		}
	}
	
	private int totalBytesReceived;
	
	@Override
	public void receivedBytes(int x) {
		synchronized(totalBytesSync) {
			totalBytesReceived += x;
		}
		node.getNodeStats().insertReceivedBytes(false, x);
	}
	
	public int getTotalReceivedBytes() {
		synchronized(totalBytesSync) {
			return totalBytesReceived;
		}
	}

	@Override
	public void sentPayload(int x) {
		node.sentPayload(x);
		node.getNodeStats().insertSentBytes(false, -x);
	}

	public boolean failedReceive() {
		return receiveFailed;
	}

	public boolean startedSendingData() {
		synchronized(backgroundTransfers) {
			return !backgroundTransfers.isEmpty();
		}
	}

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

	public PeerNode[] getRoutedTo() {
		return this.nodesRoutedTo.toArray(new PeerNode[nodesRoutedTo.size()]);
	}

	@Override
	protected Message createDataRequest() {
        Message req;
        
        req = DMT.createFNPInsertRequest(uid, htl, key);
        if(forkOnCacheable != Node.FORK_ON_CACHEABLE_DEFAULT) {
        	req.addSubMessage(DMT.createFNPSubInsertForkControl(forkOnCacheable));
        }
        if(ignoreLowBackoff != Node.IGNORE_LOW_BACKOFF_DEFAULT) {
        	req.addSubMessage(DMT.createFNPSubInsertIgnoreLowBackoff(ignoreLowBackoff));
        }
        if(preferInsert != Node.PREFER_INSERT_DEFAULT) {
        	req.addSubMessage(DMT.createFNPSubInsertPreferInsert(preferInsert));
        }
    	req.addSubMessage(DMT.createFNPRealTimeFlag(realTimeFlag));
        
    	return req;
	}

	@Override
	protected long getAcceptedTimeout() {
		return ACCEPTED_TIMEOUT;
	}

	@Override
	protected void timedOutWhileWaiting(double load) {
		htl -= (short)Math.max(0, hopsForFatalTimeoutWaitingForPeer());
		if(htl < 0) htl = 0;
        // Backtrack, i.e. RNF.
		if(!hasForwarded)
			origTag.setNotRoutedOnwards();
        finish(ROUTE_NOT_FOUND, null);
	}

	@Override
	protected void onAccepted(PeerNode next) {
        // Send them the data.
        // Which might be the new data resulting from a collision...

        Message dataInsert;
        dataInsert = DMT.createFNPDataInsert(uid, headers);
        /** What are we waiting for now??:
         * - FNPRouteNotFound - couldn't exhaust HTL, but send us the 
         *   data anyway please
         * - FNPInsertReply - used up all HTL, yay
         * - FNPRejectOverload - propagating an overload error :(
         * - FNPRejectTimeout - we took too long to send the DataInsert
         * - FNPDataInsertRejected - the insert was invalid
         */
        
        int searchTimeout = calculateTimeout(htl);
        MessageFilter mfInsertReply = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPInsertReply);
        MessageFilter mfRejectedOverload = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRejectedOverload);
        MessageFilter mfRouteNotFound = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRouteNotFound);
        MessageFilter mfDataInsertRejected = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPDataInsertRejected);
        MessageFilter mfTimeout = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRejectedTimeout);
        
        MessageFilter mf = mfInsertReply.or(mfRouteNotFound.or(mfDataInsertRejected.or(mfTimeout.or(mfRejectedOverload))));

        InsertTag thisTag = forkedRequestTag;
        if(forkedRequestTag == null) thisTag = origTag;
        
        if(logMINOR) Logger.minor(this, "Sending DataInsert");
        try {
			next.sendSync(dataInsert, this, realTimeFlag);
		} catch (NotConnectedException e1) {
			if(logMINOR) Logger.minor(this, "Not connected sending DataInsert: "+next+" for "+uid);
			next.noLongerRoutingTo(thisTag, false);
			routeRequests();
			return;
		} catch (SyncSendWaitedTooLongException e) {
			Logger.error(this, "Unable to send "+dataInsert+" to "+next+" in a reasonable time");
			// Other side will fail. No need to do anything.
			next.noLongerRoutingTo(thisTag, false);
			routeRequests();
			return;
		}

		if(logMINOR) Logger.minor(this, "Sending data");
		final BackgroundTransfer transfer = 
			startBackgroundTransfer(next, prb, thisTag);
		
		// Once the transfer has started, we only unlock the tag after the transfer completes (successfully or not).
		
        while (true) {

        	Message msg;
        	
			if(failIfReceiveFailed(thisTag, next)) {
				// The transfer has started, it will be cancelled.
				transfer.onCompleted();
				return;
			}
			
			try {
				msg = node.getUSM().waitFor(mf, this);
			} catch (DisconnectedException e) {
				Logger.normal(this, "Disconnected from " + next
						+ " while waiting for InsertReply on " + this);
				transfer.onDisconnect(next);
				break;
			}
			if(failIfReceiveFailed(thisTag, next)) {
				// The transfer has started, it will be cancelled.
				transfer.onCompleted();
				return;
			}
			
			if (msg == null) {
				
				Logger.warning(this, "Timeout on insert "+this+" to "+next);
				
				// First timeout.
				// Could be caused by the next node, or could be caused downstream.
				next.localRejectedOverload("AfterInsertAcceptedTimeout2", realTimeFlag);
				forwardRejectedOverload();

				synchronized(this) {
					status = TIMED_OUT;
					notifyAll();
				}
				
				// Wait for the second timeout off-thread.
				// FIXME wait asynchronously.
				
				final InsertTag tag = thisTag;
				final PeerNode waitingFor = next;
				final short htl = this.htl;
				
				Runnable r = new Runnable() {

					@Override
					public void run() {
						// We do not need to unlock the tag here.
						// That will happen in the BackgroundTransfer, which has already started.
						
						// FIXME factor out
				        int searchTimeout = calculateTimeout(htl);
		                MessageFilter mfInsertReply = MessageFilter.create().setSource(waitingFor).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPInsertReply);
		                MessageFilter mfRejectedOverload = MessageFilter.create().setSource(waitingFor).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRejectedOverload);
		                MessageFilter mfRouteNotFound = MessageFilter.create().setSource(waitingFor).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRouteNotFound);
		                MessageFilter mfDataInsertRejected = MessageFilter.create().setSource(waitingFor).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPDataInsertRejected);
		                MessageFilter mfTimeout = MessageFilter.create().setSource(waitingFor).setField(DMT.UID, uid).setTimeout(searchTimeout).setType(DMT.FNPRejectedTimeout);
		                
		                MessageFilter mf = mfInsertReply.or(mfRouteNotFound.or(mfDataInsertRejected.or(mfTimeout.or(mfRejectedOverload))));

			            while (true) {
			            	
			            	Message msg;

			    			if(failIfReceiveFailed(tag, waitingFor)) {
			    				transfer.onCompleted();
			    				return;
			    			}
							
							try {
								msg = node.getUSM().waitFor(mf, CHKInsertSender.this);
							} catch (DisconnectedException e) {
								Logger.normal(this, "Disconnected from " + waitingFor
										+ " while waiting for InsertReply on " + CHKInsertSender.this);
								transfer.onDisconnect(waitingFor);
								return;
							}
							
			    			if(failIfReceiveFailed(tag, waitingFor)) {
			    				transfer.onCompleted();
			    				return;
			    			}
							
							if(msg == null) {
								// Second timeout.
								// Definitely caused by the next node, fatal.
								Logger.error(this, "Got second (local) timeout on "+CHKInsertSender.this+" from "+waitingFor);
			    				transfer.onCompleted();
								waitingFor.fatalTimeout();
								return;
							}
							
							if (msg.getSpec() == DMT.FNPRejectedTimeout) {
								// Next node timed out awaiting our DataInsert.
								// But we already sent it, so something is wrong. :(
								handleRejectedTimeout(msg, waitingFor);
								transfer.kill();
								return;
							}

							if (msg.getSpec() == DMT.FNPRejectedOverload) {
								if(handleRejectedOverload(msg, waitingFor, tag)) {
									// Already set the status, and handle... will have unlocked the next node, so no need to call finished().
									transfer.onCompleted();
									return; // Don't try another node.
								}
								else continue;
							}

							if (msg.getSpec() == DMT.FNPRouteNotFound) {
								transfer.onCompleted();
								return; // Don't try another node.
							}
							
							if (msg.getSpec() == DMT.FNPDataInsertRejected) {
								handleDataInsertRejected(msg, waitingFor, tag);
								transfer.kill();
								return; // Don't try another node.
							}
							
							if (msg.getSpec() != DMT.FNPInsertReply) {
								Logger.error(this, "Unknown reply: " + msg);
								transfer.onCompleted();
								return;
							} else {
								// Our task is complete, one node (quite deep), has accepted the insert.
								// The request will not be routed to any other nodes, this is where the data *should* be.
								// We will removeRoutingTo() after the node has sent the transfer completion notice, which never happens before the InsertReply.
								transfer.onCompleted();
								return;
							}
			            }
					}
					
				};
				
				// Wait for the timeout off-thread.
				node.getExecutor().execute(r);
				// Meanwhile, finish() to update allTransfersCompleted and hence allow the CHKInsertHandler to send the message downstream.
				// We have already set the status code, this is necessary in order to avoid race conditions.
				// However since it is set to TIMED_OUT, we are allowed to set it again.
				finish(TIMED_OUT, next);
				return;
			}

			if (msg.getSpec() == DMT.FNPRejectedTimeout) {
				// Next node timed out awaiting our DataInsert.
				// But we already sent it, so something is wrong. :(
				transfer.kill();
				handleRejectedTimeout(msg, next);
				return;
			}

			if (msg.getSpec() == DMT.FNPRejectedOverload) {
				if(handleRejectedOverload(msg, next, thisTag)) {
					// We have had an Accepted. This happens on a timeout downstream.
					// They will complete it (finish()), so we need to wait for a transfer completion.
					// FIXME it might be less confusing and therefore less likely to cause problems
					// if we had a different message sent post-accept???
					transfer.onCompleted();
					break;
				}
				else continue;
			}

			if (msg.getSpec() == DMT.FNPRouteNotFound) {
				//RNF means that the HTL was not exhausted, but that the data will still be stored.
				handleRNF(msg, next, thisTag);
				transfer.onCompleted();
				break;
			}

			//Can occur after reception of the entire chk block
			if (msg.getSpec() == DMT.FNPDataInsertRejected) {
				handleDataInsertRejected(msg, next, thisTag);
				transfer.kill();
				break;
			}
			
			if (msg.getSpec() != DMT.FNPInsertReply) {
				Logger.error(this, "Unknown reply: " + msg);
				transfer.onCompleted();
				finish(INTERNAL_ERROR, next);
				return;
			} else {
				transfer.onCompleted();
				// Our task is complete, one node (quite deep), has accepted the insert.
				// The request will not be routed to any other nodes, this is where the data *should* be.
				// We will removeRoutingTo() after the node has sent the transfer completion notice, which never happens before the InsertReply.
				finish(SUCCESS, next);
				return;
			}
		}
		routeRequests();
	}

	@Override
	protected boolean isInsert() {
		return true;
	}

	@Override
	protected PeerNode sourceForRouting() {
		if(forkedRequestTag != null) return null;
		return source;
	}
	
	@Override
	protected long ignoreLowBackoff() {
		return ignoreLowBackoff ? Node.LOW_BACKOFF : 0;
	}

}
