/* 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 java.util.HashSet;

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.PeerParseException;
import freenet.io.comm.ReferenceSignatureVerificationException;
import freenet.node.OpennetManager.ConnectionType;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;

public class AnnounceSender implements PrioRunnable, ByteCounter {
	private static volatile boolean logMINOR;

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


	// Constants
	static final int ACCEPTED_TIMEOUT = 10000;
	static final int ANNOUNCE_TIMEOUT = 120000; // longer than a regular request as have to transfer noderefs hop by hop etc
	static final int END_TIMEOUT = 30000; // After received the completion message, wait 30 seconds for any late reordered replies

	private final PeerNode source;
	private final long uid;
	private final OpennetManager om;
	private final Node node;
	private final long xferUID;
	private final int noderefLength;
	private final int paddedLength;
	private byte[] noderefBuf;
	private short htl;
	private final double target;
	private final AnnouncementCallback cb;
	private final PeerNode onlyNode;
	private int forwardedRefs;

	public AnnounceSender(double target, short htl, long uid, PeerNode source, OpennetManager om, Node node, long xferUID, int noderefLength, int paddedLength, AnnouncementCallback cb) {
		this.source = source;
		this.uid = uid;
		this.om = om;
		this.node = node;
		this.onlyNode = null;
		this.htl = htl;
		this.xferUID = xferUID;
		this.paddedLength = paddedLength;
		this.noderefLength = noderefLength;
		this.target = target;
		this.cb = cb;
	}

	public AnnounceSender(double target, OpennetManager om, Node node, AnnouncementCallback cb, PeerNode onlyNode) {
		source = null;
		this.uid = node.getRandom().nextLong();
		// Prevent it being routed back to us.
		node.getTracker().completed(uid);
		this.om = om;
		this.node = node;
		this.htl = node.maxHTL();
		this.target = target;
		this.cb = cb;
		this.onlyNode = onlyNode;
		noderefBuf = om.getCrypto().myCompressedFullRef();
		this.xferUID = 0;
		this.paddedLength = 0;
		this.noderefLength = 0;
	}

	@Override
	public void run() {
		try {
			realRun();
			node.getNodeStats().reportAnnounceForwarded(forwardedRefs, source);
		} catch (Throwable t) {
			Logger.error(this, "Caught "+t+" announcing "+uid+" from "+source, t);
		} finally {
			if(source != null) {
				source.completedAnnounce(uid);
			}
			node.getTracker().completed(uid);
			if(cb != null)
				cb.completed();
			node.getNodeStats().endAnnouncement(uid);
		}
	}

	private void realRun() {
		boolean hasForwarded = false;
		if(source != null) {
			try {
				source.sendAsync(DMT.createFNPAccepted(uid), null, this);
			} catch (NotConnectedException e) {
				return;
			}
			if(!transferNoderef()) return;
		}

		// Now route it.

		HashSet<PeerNode> nodesRoutedTo = new HashSet<PeerNode>();
		PeerNode next = null;
		while(true) {
			if(logMINOR) Logger.minor(this, "htl="+htl);
			/*
			 * 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.
			if(onlyNode == null)
				htl = node.decrementHTL(hasForwarded ? next : source, htl);

			if(htl == 0) {
				// No more nodes.
				complete();
				return;
			}

			if(!node.isOpennetEnabled()) {
				complete();
				return;
			}

			if(onlyNode == null) {
				// Route it
				next = node.getPeers().closerPeer(source, nodesRoutedTo, target, true, node.isAdvancedModeEnabled(), -1,
				        null, null, htl, 0, source == null, false, false);
			} else {
				next = onlyNode;
				if(nodesRoutedTo.contains(onlyNode)) {
					rnf(onlyNode);
					return;
				}
			}

			if(next == null) {
				// Backtrack
				rnf(next);
				return;
			}
			if(logMINOR) Logger.minor(this, "Routing request to "+next);
			if(onlyNode == null)
				next.reportRoutedTo(target, source == null, false, source, nodesRoutedTo, htl);
			nodesRoutedTo.add(next);

			long xferUID = sendTo(next);
			if(xferUID == -1) continue;

			hasForwarded = true;

			Message msg = null;

			while(true) {

				/**
				 * What are we waiting for?
				 * FNPAccepted - continue
				 * FNPRejectedLoop - go to another node
				 * FNPRejectedOverload - go to another node
				 */

				MessageFilter mfAccepted = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ACCEPTED_TIMEOUT).setType(DMT.FNPAccepted);
				MessageFilter mfRejectedLoop = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ACCEPTED_TIMEOUT).setType(DMT.FNPRejectedLoop);
				MessageFilter mfRejectedOverload = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ACCEPTED_TIMEOUT).setType(DMT.FNPRejectedOverload);
				MessageFilter mfOpennetDisabled = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ACCEPTED_TIMEOUT).setType(DMT.FNPOpennetDisabled);

				// The order of these filters is performance critical. The last or-filter is checked first.
				// So the last filter in the or-"chain" must be the filter which matches most frequently.
				MessageFilter mf = mfRejectedOverload.or(mfRejectedLoop.or(mfOpennetDisabled.or(mfAccepted)));

				try {
					msg = node.getUSM().waitFor(mf, this);
					if(logMINOR) Logger.minor(this, "first part got "+msg);
				} catch (DisconnectedException e) {
					Logger.normal(this, "Disconnected from "+next+" while waiting for Accepted on "+uid);
					break;
				}

				if(msg == null) {
					if(logMINOR) Logger.minor(this, "Timeout waiting for Accepted");
					// Try next node
					msg = null;
					break;
				}

				if(msg.getSpec() == DMT.FNPRejectedLoop) {
					if(logMINOR) Logger.minor(this, "Rejected loop");
					// Find another node to route to
					msg = null;
					break;
				}

				if(msg.getSpec() == DMT.FNPRejectedOverload) {
					if(logMINOR) Logger.minor(this, "Rejected: overload");
					// Give up on this one, try another
					msg = null;
					break;
				}

				if(msg.getSpec() == DMT.FNPOpennetDisabled) {
					if(logMINOR) Logger.minor(this, "Opennet disabled");
					msg = null;
					break;
				}

				if(msg.getSpec() != DMT.FNPAccepted) {
					Logger.error(this, "Unrecognized message: "+msg);
					continue;
				}

				break;
			}

			if((msg == null) || (msg.getSpec() != DMT.FNPAccepted)) {
				// Try another node
				continue;
			}

			if(logMINOR) Logger.minor(this, "Got Accepted");
			
			if(cb != null)
				cb.acceptedSomewhere();

			// Send the rest

			try {
				sendRest(next, xferUID);
			} catch (NotConnectedException e1) {
				if(logMINOR)
					Logger.minor(this, "Not connected while sending noderef on "+next);
				continue;
			}

			// Otherwise, must be Accepted

			// So wait...

			while(true) {

				MessageFilter mfAnnounceCompleted = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPOpennetAnnounceCompleted);
				MessageFilter mfRouteNotFound = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPRouteNotFound);
				MessageFilter mfRejectedOverload = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPRejectedOverload);
				MessageFilter mfAnnounceReply = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPOpennetAnnounceReply);
				MessageFilter mfOpennetDisabled = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPOpennetDisabled);
				MessageFilter mfNotWanted = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPOpennetAnnounceNodeNotWanted);
				MessageFilter mfOpennetNoderefRejected = MessageFilter.create().setSource(next).setField(DMT.UID, uid).setTimeout(ANNOUNCE_TIMEOUT).setType(DMT.FNPOpennetNoderefRejected);
				MessageFilter mf = mfAnnounceCompleted.or(mfRouteNotFound.or(mfRejectedOverload.or(mfAnnounceReply.or(mfOpennetDisabled.or(mfNotWanted.or(mfOpennetNoderefRejected))))));

				try {
					msg = node.getUSM().waitFor(mf, this);
				} catch (DisconnectedException e) {
					Logger.normal(this, "Disconnected from "+next+" while waiting for announcement");
					break;
				}

				if(logMINOR) Logger.minor(this, "second part got "+msg);

				if(msg == null) {
					// Fatal timeout, must be terminal (IS_LOCAL==true)
					timedOut(next);
					return;
				}

				if(msg.getSpec() == DMT.FNPOpennetNoderefRejected) {
					int reason = msg.getInt(DMT.REJECT_CODE);
					Logger.normal(this, "Announce rejected by "+next+" : "+DMT.getOpennetRejectedCode(reason));
					msg = null;
					break;
				}

				if(msg.getSpec() == DMT.FNPOpennetAnnounceCompleted) {
					// Send the completion on immediately. We don't want to accumulate 30 seconds per hop!
					complete();
					mfAnnounceReply.setTimeout(END_TIMEOUT).setTimeoutRelativeToCreation(true);
					mfNotWanted.setTimeout(END_TIMEOUT).setTimeoutRelativeToCreation(true);
					mfAnnounceReply.clearOr();
					mfNotWanted.clearOr();
					mf = mfAnnounceReply.or(mfNotWanted);
					while(true)  {
						try {
							msg = node.getUSM().waitFor(mf, this);
						} catch (DisconnectedException e) {
							return;
						}
						if(msg == null) return;
						if(msg.getSpec() == DMT.FNPOpennetAnnounceReply) {
							validateForwardReply(msg, next);
							continue;
						}
						if(msg.getSpec() == DMT.FNPOpennetAnnounceNodeNotWanted) {
							if(cb != null)
								cb.nodeNotWanted();
							if(source != null) {
								try {
									sendNotWanted();
								} catch (NotConnectedException e) {
									Logger.warning(this, "Lost connection to source (announce completed)");
									return;
								}
							}
							continue;
						}
					}
				}

				if(msg.getSpec() == DMT.FNPRouteNotFound) {
					// Backtrack within available hops
					short newHtl = msg.getShort(DMT.HTL);
					if(newHtl < 0) newHtl = 0;
					if(newHtl < htl) htl = newHtl;
					break;
				}

				if(msg.getSpec() == DMT.FNPRejectedOverload) {
					// Give up on this one, try another
					break;
				}

				if(msg.getSpec() == DMT.FNPOpennetDisabled) {
					Logger.minor(this, "Opennet disabled");
					msg = null;
					break;
				}

				if(msg.getSpec() == DMT.FNPOpennetAnnounceReply) {
					validateForwardReply(msg, next);
					continue; // There may be more
				}

				if(msg.getSpec() == DMT.FNPOpennetAnnounceNodeNotWanted) {
					if(cb != null)
						cb.nodeNotWanted();
					if(source != null) {
						try {
							sendNotWanted();
						} catch (NotConnectedException e) {
							Logger.warning(this, "Lost connection to source (announce not wanted)");
							return;
						}
					}
					continue; // This message is propagated, they will send a Completed or RNF
				}

				Logger.error(this, "Unexpected message: "+msg);
			}
		}
	}
	
	private int waitingForTransfers = 0;

	/**
	 * Validate a reply, and relay it back to the source.
	 * @param msg2 The AnnouncementReply message.
	 * @return True unless we lost the connection to our request source.
	 */
	private void validateForwardReply(Message msg, final PeerNode next) {
		final long xferUID = msg.getLong(DMT.TRANSFER_UID);
		final int noderefLength = msg.getInt(DMT.NODEREF_LENGTH);
		final int paddedLength = msg.getInt(DMT.PADDED_LENGTH);
		synchronized(this) {
			waitingForTransfers++;
		}
		Runnable r = new Runnable() {

			@Override
			public void run() {
				try {
					byte[] noderefBuf = OpennetManager.innerWaitForOpennetNoderef(xferUID, paddedLength, noderefLength, next, false, uid, true, AnnounceSender.this, node);
					if(noderefBuf == null) {
						return; // Don't relay
					}
					SimpleFieldSet fs = OpennetManager.validateNoderef(noderefBuf, 0, noderefLength, next, false);
					if(fs == null) {
						if(cb != null) cb.bogusNoderef("invalid noderef");
						return; // Don't relay
					}
					if(source != null) {
						// Now relay it
						try {
							forwardedRefs++;
							om.sendAnnouncementReply(uid, source, noderefBuf, AnnounceSender.this);
							if(cb != null) {
								cb.relayedNoderef();
							}
						} catch (NotConnectedException e) {
							// Hmmm...!
							return;
						}
					} else {
						// Add it
						try {
							OpennetPeerNode pn = node.addNewOpennetNode(fs, ConnectionType.ANNOUNCE);
							if(cb != null) {
								if(pn != null)
									cb.addedNode(pn);
								else
									cb.nodeNotAdded();
							}
						} catch (FSParseException e) {
							Logger.normal(this, "Failed to parse reply: "+e, e);
							if(cb != null) cb.bogusNoderef("parse failed: "+e);
						} catch (PeerParseException e) {
							Logger.normal(this, "Failed to parse reply: "+e, e);
							if(cb != null) cb.bogusNoderef("parse failed: "+e);
						} catch (ReferenceSignatureVerificationException e) {
							Logger.normal(this, "Failed to parse reply: "+e, e);
							if(cb != null) cb.bogusNoderef("parse failed: "+e);
						}
					}
					return;
				} finally {
					synchronized(AnnounceSender.this) {
						waitingForTransfers--;
						AnnounceSender.this.notifyAll();
					}
				}
			}
			
		};
		try {
			node.getExecutor().execute(r);
		} catch (Throwable t) {
			synchronized(this) {
				waitingForTransfers--;
			}
		}
	}

	/**
	 * Send an AnnouncementRequest.
	 * @param next The node to send the announcement to.
	 * @return True if the announcement was successfully sent.
	 */
	private long sendTo(PeerNode next) {
		try {
			return om.startSendAnnouncementRequest(uid, next, noderefBuf, this, target, htl);
		} catch (NotConnectedException e) {
			if(logMINOR) Logger.minor(this, "Disconnected");
			return -1;
		}
	}

	/**
	 * Send an AnnouncementRequest.
	 * @param next The node to send the announcement to.
	 * @return True if the announcement was successfully sent.
	 * @throws NotConnectedException
	 */
	private void sendRest(PeerNode next, long xferUID) throws NotConnectedException {
		om.finishSentAnnouncementRequest(next, noderefBuf, this, xferUID);
	}

	private void timedOut(PeerNode next) {
		Message msg = DMT.createFNPRejectedOverload(uid, true, false, false);
		if(source != null) {
			try {
				source.sendAsync(msg, null, this);
			} catch (NotConnectedException e) {
				// Ok
			}
		}
		if(cb != null) cb.nodeFailed(next, "timed out");
	}

        private synchronized void waitForRunningTransfers() {
           while(waitingForTransfers > 0) {
              try {
                 wait();
              } catch (InterruptedException e) {
                 // Ignore.
              }
           }
        }

	private void rnf(PeerNode next) {
		waitForRunningTransfers();
		Message msg = DMT.createFNPRouteNotFound(uid, htl);
		if(source != null) {
			try {
				source.sendAsync(msg, null, this);
			} catch (NotConnectedException e) {
				// Ok
			}
		}
		if(cb != null) {
			if(next != null) cb.nodeFailed(next, "route not found");
			else cb.noMoreNodes();
		}
	}

	private void complete() {
		waitForRunningTransfers();
		Message msg = DMT.createFNPOpennetAnnounceCompleted(uid);
		if(source != null) {
			try {
				source.sendAsync(msg, null, this);
			} catch (NotConnectedException e) {
				// Oh well.
			}
		}
	}

	/**
	 * @return True unless the noderef is bogus.
	 */
	private boolean transferNoderef() {
		noderefBuf = OpennetManager.innerWaitForOpennetNoderef(xferUID, paddedLength, noderefLength, source, false, uid, true, this, node);
		if(noderefBuf == null) {
			return false;
		}
		SimpleFieldSet fs = OpennetManager.validateNoderef(noderefBuf, 0, noderefLength, source, false);
		if(fs == null) {
			OpennetManager.rejectRef(uid, source, DMT.NODEREF_REJECTED_INVALID, this);
			return false;
		}
		// If we want it, add it and send it.
		try {
			// Allow reconnection - sometimes one side has the ref and the other side doesn't.
			if(om.addNewOpennetNode(fs, ConnectionType.ANNOUNCE, true) != null) {
				sendOurRef(source, om.getCrypto().myCompressedFullRef());
			} else {
				if(logMINOR)
					Logger.minor(this, "Don't need the node");
				sendNotWanted();
				// Okay, just route it.
			}
		} catch (FSParseException e) {
			Logger.warning(this, "Rejecting noderef: "+e, e);
			OpennetManager.rejectRef(uid, source, DMT.NODEREF_REJECTED_INVALID, this);
			return false;
		} catch (PeerParseException e) {
			Logger.warning(this, "Rejecting noderef: "+e, e);
			OpennetManager.rejectRef(uid, source, DMT.NODEREF_REJECTED_INVALID, this);
			return false;
		} catch (ReferenceSignatureVerificationException e) {
			Logger.warning(this, "Rejecting noderef: "+e, e);
			OpennetManager.rejectRef(uid, source, DMT.NODEREF_REJECTED_INVALID, this);
			return false;
		} catch (NotConnectedException e) {
			Logger.normal(this, "Could not receive noderef, disconnected");
			return false;
		}
		return true;
	}

	private void sendNotWanted() throws NotConnectedException {
		Message msg = DMT.createFNPOpennetAnnounceNodeNotWanted(uid);
		source.sendAsync(msg, null, this);
	}

	private void sendOurRef(PeerNode next, byte[] ref) throws NotConnectedException {
		om.sendAnnouncementReply(uid, next, ref, this);
	}

	@Override
	public void sentBytes(int x) {
		node.getNodeStats().announceByteCounter.sentBytes(x);
	}

	@Override
	public void receivedBytes(int x) {
		node.getNodeStats().announceByteCounter.receivedBytes(x);
	}

	@Override
	public void sentPayload(int x) {
		node.getNodeStats().announceByteCounter.sentPayload(x);
		// Doesn't count.
	}

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

}
