/*
 * Dijjer - A Peer to Peer HTTP Cache
 * Copyright (C) 2004,2005 Change.Tv, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package freenet.io.xfer;

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

import freenet.io.comm.AsyncMessageFilterCallback;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.DisconnectedException;
import freenet.io.comm.Message;
import freenet.io.comm.MessageCore;
import freenet.io.comm.MessageFilter;
import freenet.io.comm.NotConnectedException;
import freenet.io.comm.PeerContext;
import freenet.io.comm.RetrievalException;
import freenet.io.comm.SlowAsyncMessageFilterCallback;
import freenet.node.PeerNode;
import freenet.node.SyncSendWaitedTooLongException;
import freenet.support.BitArray;
import freenet.support.Buffer;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.Ticker;
import freenet.support.TimeUtil;
import freenet.support.io.NativeThread;
import freenet.support.math.MedianMeanRunningAverage;

/**
 * IMPORTANT: The receiver can cancel the incoming transfer. This may or may not, 
 * depending on the caller, result in the PRB being cancelled, and thus propagate back to
 * the originator.
 * 
 * This allows for a weak DoS, in that a node can start a request and then cancel it, 
 * having wasted a certain amount of upstream bandwidth on transferring data, especially
 * if upstream has lots of bandwidth and the attacker has limited bandwidth in the victim
 * -> attacker direction. However this behaviour can be detected fairly easily.
 * 
 * If we allow receiver cancels and don't propagate, a more serious DoS is possible. If we
 * don't allow receiver cancels, we have to get rid of turtles, and massively tighten up
 * transfer timeouts.
 * 
 * However, if we do that, we have to consider that a node might be able to connect, max
 * out the bandwidth with transfers, and then disconnect, avoiding the need to spend
 * bandwidth on receiving all the data; and then reconnect, after it's confident that the
 * transfers to it will have been cancelled. Or not reconnect at all, on opennet - just 
 * use a different identity. Downstream bandwidth is very cheap for small-scale attackers,
 * but if this is a usable force multiplier it could still be a good DoS if we went that
 * way.
 * 
 * But if we did get rid of receiver cancels, it *would* mean we could get rid of a lot of
 * code - e.g. the ReceiverAbortHandler, which in some cases (e.g RequestHandler) is 
 * complex and involves complex security tradeoffs. It would also make transfers 
 * significantly more reliable.
 * 
 * @author ian
 */
public class BlockReceiver implements AsyncMessageFilterCallback {

	private static volatile boolean logMINOR;

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

	public interface BlockReceiverTimeoutHandler {
		
		/** After a block times out, we call this callback. Once it returns, we cancel the
		 * PRB and wait for a cancel message or the second timeout. Hence, if the problem
		 * is on the node sending the data, we will get the first timeout then the second
		 * (fatal) timeout. But if the problem is upstream, we will only get the first
		 * timeout. 
		 * 
		 * Simple requests will need to implement this and transfer ownership of
		 * the request to this node, because the source node will end the request as soon
		 * as it sees the transfer cancel resulting from the PRB being cancelled; 
		 * assigning the UID to ourselves keeps it consistent, and thus avoids severe load
		 * management problems (resulting in e.g. constantly sending requests to a node 
		 * which are then rejected because we think we have capacity when we don't). */
		void onFirstTimeout();
		
		/** After the first timeout, we wait for either a cancel message (sendAborted 
		 * here), or the second timeout. If we get the second timeout, the problem was
		 * caused by the node we are receiving the data from, rather than upstream. In
		 * which case, we may need to take severe action against the node responsible, 
		 * because we do not know whether or not it thinks the transfer is still running.
		 * If it is still running and yet we cancel it, we will think that there is 
		 * capacity for more requests on the node when there isn't, resulting in load 
		 * management problems as above. */
		void onFatalTimeout(PeerContext source);
	}

	/*
	 * RECEIPT_TIMEOUT must be less than 60 seconds because BlockTransmitter times out after not
	 * hearing from us in 60 seconds. Without contact from the transmitter, we will try sending
	 * at most MAX_CONSECUTIVE_MISSING_PACKET_REPORTS every RECEIPT_TIMEOUT to recover.
	 */
	public final long RECEIPT_TIMEOUT;
	public static final long RECEIPT_TIMEOUT_REALTIME = SECONDS.toMillis(10);
	public static final long RECEIPT_TIMEOUT_BULK = SECONDS.toMillis(30);
	// TODO: This should be proportional to the calculated round-trip-time, not a constant
	public final long MAX_ROUND_TRIP_TIME;
	public static final int MAX_CONSECUTIVE_MISSING_PACKET_REPORTS = 4;
	public static final int MAX_SEND_INTERVAL = 500;
	public static final long CLEANUP_TIMEOUT = SECONDS.toMillis(5);
	// After 15 seconds, the receive is overdue and will cause backoff.
	public static final long TOO_LONG_TIMEOUT = SECONDS.toMillis(15);
	/** sendAborted is not sent at the realtime/bulk priority. Most of the two
	 * stage timeout stuff uses 60 seconds, it's a good number. */
	public static final long ACK_TRANSFER_FAILED_TIMEOUT = SECONDS.toMillis(60);
	PartiallyReceivedBlock _prb;
	PeerContext _sender;
	long _uid;
	MessageCore _usm;
	ByteCounter _ctr;
	Ticker _ticker;
	boolean sentAborted;
	private MessageFilter discardFilter;
	private long discardEndTime;
	private boolean senderAborted;
	private final boolean _realTime;
//	private final boolean _doTooLong;
	private final BlockReceiverTimeoutHandler _timeoutHandler;
	private final boolean completeAfterAckedAllReceived;

	/**
	 * @param usm
	 * @param sender
	 * @param uid
	 * @param prb
	 * @param ctr
	 * @param ticker
	 * @param doTooLong
	 * @param realTime
	 * @param timeoutHandler
	 * @param completeAfterAckedAllReceived If true, we need to call completion
	 * only after we have received an ack to the allReceived message. Generally,
	 * handlers want to complete early (=false), so the slot is freed up and can 
	 * be reused by the other side; senders want to complete late, so they don't 
	 * end up reusing the slot before the handler has completed (=true).
	 */
	public BlockReceiver(MessageCore usm, PeerContext sender, long uid, PartiallyReceivedBlock prb, ByteCounter ctr, Ticker ticker, boolean doTooLong, boolean realTime, BlockReceiverTimeoutHandler timeoutHandler, boolean completeAfterAckedAllReceived) {
		BlockReceiverTimeoutHandler nullTimeoutHandler = new BlockReceiverTimeoutHandler() {

			@Override
			public void onFirstTimeout() {
				// Do nothing
			}

			@Override
			public void onFatalTimeout(PeerContext source) {
				// Do nothing
			}

		};
		_timeoutHandler = timeoutHandler == null ? nullTimeoutHandler : timeoutHandler;
		_sender = sender;
		_prb = prb;
		_uid = uid;
		_usm = usm;
		_ctr = ctr;
		_ticker = ticker;
		_realTime = realTime;
		this.completeAfterAckedAllReceived = completeAfterAckedAllReceived;
		RECEIPT_TIMEOUT = _realTime ? RECEIPT_TIMEOUT_REALTIME : RECEIPT_TIMEOUT_BULK;
		MAX_ROUND_TRIP_TIME = RECEIPT_TIMEOUT;
//		_doTooLong = doTooLong;
	}

	private void sendAborted(int reason, String desc) throws NotConnectedException {
		synchronized(this) {
			if(sentAborted) return;
			sentAborted = true;
		}
		_usm.send(_sender, DMT.createSendAborted(_uid, reason, desc), _ctr);
	}
	
	public interface BlockReceiverCompletion {

		public void blockReceived(byte[] buf);

		public void blockReceiveFailed(RetrievalException e);

	}
	
	private BlockReceiverCompletion callback;
	
	private long startTime;
	
	// If false, don't check for duplicate messages from the sender.
	// Turn off if e.g. we know that the PRB is already partially received when we start the transfer.
	// This prevents malicious or broken nodes from trickling transfers forever by sending the same packets over and over.
	static final boolean CHECK_DUPES = true;
	
	private boolean gotAllSent;
	
	private AsyncMessageFilterCallback notificationWaiter = new SlowAsyncMessageFilterCallback() {

		@Override
		public void onMatched(Message m1) {
            if(logMINOR)
            	Logger.minor(this, "Received "+m1);
            if ((m1 != null) && m1.getSpec().equals(DMT.sendAborted)) {
				String desc=m1.getString(DMT.DESCRIPTION);
				if (!desc.contains("Upstream"))
					desc="Upstream transmit error: "+desc;
				_prb.abort(m1.getInt(DMT.REASON), desc, false);
				synchronized(BlockReceiver.this) {
					senderAborted = true;
				}
				complete(m1.getInt(DMT.REASON), desc);
				return;
			}
            boolean truncateTimeout = false;
			if ((m1 != null) && (m1.getSpec().equals(DMT.packetTransmit))) {
				// packetTransmit received
				int packetNo = m1.getInt(DMT.PACKET_NO);
				BitArray sent = (BitArray) m1.getObject(DMT.SENT);
				Buffer data = (Buffer) m1.getObject(DMT.DATA);
				int missing = 0;
				try {
					synchronized(BlockReceiver.this) {
						if(completed) return;
					}
					if(CHECK_DUPES && _prb.isReceived(packetNo)) {
						// Transmitter sent the same packet twice?!?!?
						Logger.error(this, "Already received the packet - DoS??? on "+this+" uid "+_uid+" from "+_sender);
						// Does not extend timeouts.
						truncateTimeout = true;
					} else {
						_prb.addPacket(packetNo, data);
						if(logMINOR) {
							synchronized(BlockReceiver.this) {
								long interval = System.currentTimeMillis() - timeStartedWaiting;
								Logger.minor(this, "Packet interval: "+interval+" = "+TimeUtil.formatTime(interval, 2, true)+" from "+_sender);
							}
						}
						// Check that we have what the sender thinks we have
						for (int x = 0; x < sent.getSize(); x++) {
							if (sent.bitAt(x) && !_prb.isReceived(x)) {
								missing++;
							}
						}
						if(logMINOR && missing != 0) 
							Logger.minor(this, "Packets which the sender says it has sent but we have not received: "+missing);
					}
				} catch (AbortedException e) {
					// We didn't cause it?!
					Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e, e);
					complete(RetrievalException.UNKNOWN, "Aborted?");
					return;
				}
			} else if (m1 != null && m1.getSpec().equals(DMT.allSent)) {
				synchronized(BlockReceiver.this) {
					if(completed) return;
					if(gotAllSent)
						// Multiple allSent's don't extend the timeouts.
						truncateTimeout = true;
					gotAllSent = true;
				}
			}
			try {
				if(_prb.allReceived()) {
					try {
						Message m = DMT.createAllReceived(_uid);
						if(completeAfterAckedAllReceived) {
							try {
								// FIXME layer violation
								// FIXME make asynchronous
								((PeerNode)_sender).sendSync(m, _ctr, _realTime);
							} catch (SyncSendWaitedTooLongException e) {
								// Complete anyway.
							}
						} else {
							_usm.send(_sender, m, _ctr);
						}
						discardEndTime=System.currentTimeMillis()+CLEANUP_TIMEOUT;
						discardFilter=relevantMessages(CLEANUP_TIMEOUT);
						maybeResetDiscardFilter();
					} catch (NotConnectedException e1) {
						// Ignore, we've got it.
						if(logMINOR) Logger.minor(this, "Got data but can't send allReceived to "+_sender+" as is disconnected");
					}
					long endTime = System.currentTimeMillis();
					long transferTime = (endTime - startTime);
					if(logMINOR) {
						synchronized(avgTimeTaken) {
							avgTimeTaken.report(transferTime);
							Logger.minor(this, "Block transfer took "+transferTime+"ms - average is "+avgTimeTaken);
						}
					}
					complete(_prb.getBlock());
					return;
				}
			} catch (AbortedException e1) {
				// We didn't cause it?!
				Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e1, e1);
				complete(RetrievalException.UNKNOWN, "Aborted?");
				return;
			}
			try {
				// Even if timeout <= 0, we still add the filter, because we want to receive any messages that are already buffered before we timeout.
				waitNotification(truncateTimeout);
			} catch (DisconnectedException e) {
				onDisconnect(null);
				return;
			}
		}
		
		@Override
		public boolean shouldTimeout() {
			return completed;
		}
		
		@Override
		public void onTimeout() {
			synchronized(this) {
				if(completed) return;
			}
			try {
				if(_prb.allReceived()) return;
				_prb.abort(RetrievalException.SENDER_DIED, "Sender unresponsive to resend requests", false);
				complete(RetrievalException.SENDER_DIED,
						"Sender unresponsive to resend requests");
				
				_timeoutHandler.onFirstTimeout();
				// If upstream caused the problem, then sender will itself timeout
				// and will tell us. So wait for a timeout.
				// It is important for load management that the two sides agree on the number of transfers happening.
				// Therefore we need to not complete until the other side has acknowledged that the transfer has been cancelled.
				MessageFilter mfSendAborted = MessageFilter.create().setTimeout(ACK_TRANSFER_FAILED_TIMEOUT).setType(DMT.sendAborted).setField(DMT.UID, _uid).setSource(_sender);
				try {
					_usm.addAsyncFilter(mfSendAborted, new SlowAsyncMessageFilterCallback() {

						@Override
						public void onMatched(Message m) {
							// Ok.
							if(logMINOR) Logger.minor(this, "Transfer cancel acknowledged");
						}

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

						@Override
						public void onTimeout() {
							Logger.error(this, "Other side did not acknowlege transfer failure on "+BlockReceiver.this);
							_timeoutHandler.onFatalTimeout(_sender);
						}

						@Override
						public void onDisconnect(PeerContext ctx) {
							// Ok.
						}

						@Override
						public void onRestarted(PeerContext ctx) {
							// Ok.
						}

						@Override
						public int getPriority() {
							return NativeThread.NORM_PRIORITY;
						}
						
					}, _ctr);
				} catch (DisconnectedException e) {
					// Ignore
				}
				
				return;
			} catch (AbortedException e) {
				// We didn't cause it?!
				Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e, e);
				complete(RetrievalException.UNKNOWN, "Aborted?");
				return;
			}
		}
		
		@Override
		public void onDisconnect(PeerContext ctx) {
			complete(RetrievalException.SENDER_DISCONNECTED, RetrievalException.getErrString(RetrievalException.SENDER_DISCONNECTED));
		}
		
		@Override
		public void onRestarted(PeerContext ctx) {
			complete(RetrievalException.SENDER_DISCONNECTED, RetrievalException.getErrString(RetrievalException.SENDER_DISCONNECTED));
		}

		@Override
		public int getPriority() {
			return NativeThread.NORM_PRIORITY;
		}
		
	};
	
	private boolean completed;
	
	private void complete(int reason, String description) {
		synchronized(this) {
			if(completed) {
				if(logMINOR) Logger.minor(this, "Already completed");
				return;
			}
			completed = true;
		}
		if(logMINOR)
			Logger.minor(this, "Transfer failed: ("+(_realTime?"realtime":"bulk")+") "+reason+" : "+description+" on "+_uid+" from "+_sender);
		_prb.removeListener(myListener);
		byte[] block = _prb.abort(reason, description, false);
		if(block == null) {
			// Expected behaviour.
			// Send the abort whether we have received one or not.
			// If we are cancelling due to failing to turtle, we need to tell the sender
			// this otherwise he will keep sending, wasting a lot of bandwidth on packets
			// that we will ignore. If we are cancelling because the sender has told us 
			// to, we need to acknowledge that.
			try {
				sendAborted(_prb._abortReason, _prb._abortDescription);
			} catch (NotConnectedException e) {
				// Ignore at this point.
			}
			callback.blockReceiveFailed(new RetrievalException(reason, description));
		} else {
			Logger.error(this, "Succeeded in complete("+reason+","+description+") on "+this, new Exception("error"));
			callback.blockReceived(block);
		}
		decRunningBlockReceives();
	}

	private void complete(byte[] ret) {
		synchronized(this) {
			if(completed) {
				if(logMINOR) Logger.minor(this, "Already completed");
				return;
			}
			completed = true;
		}
		_prb.removeListener(myListener);
		callback.blockReceived(ret);
		decRunningBlockReceives();
	}

	private long timeStartedWaiting = -1;
	
	private void waitNotification(boolean truncateTimeout) throws DisconnectedException {
		long timeout;
		long now = System.currentTimeMillis();
		synchronized(this) {
			if(truncateTimeout) {
				timeout = (int)Math.min(timeStartedWaiting + RECEIPT_TIMEOUT - now, RECEIPT_TIMEOUT);
			} else {
				timeStartedWaiting = now;
				timeout = RECEIPT_TIMEOUT;
			}
		}
		_usm.addAsyncFilter(relevantMessages(timeout), notificationWaiter, _ctr);
	}

	private MessageFilter relevantMessages(long timeout) {
		MessageFilter mfPacketTransmit = MessageFilter.create().setTimeout(timeout).setType(DMT.packetTransmit).setField(DMT.UID, _uid).setSource(_sender);
		MessageFilter mfAllSent = MessageFilter.create().setTimeout(timeout).setType(DMT.allSent).setField(DMT.UID, _uid).setSource(_sender);
		MessageFilter mfSendAborted = MessageFilter.create().setTimeout(timeout).setType(DMT.sendAborted).setField(DMT.UID, _uid).setSource(_sender);
		return mfSendAborted.or(mfAllSent.or(mfPacketTransmit));
	}

	PartiallyReceivedBlock.PacketReceivedListener myListener;
	
	public void receive(BlockReceiverCompletion callback) {
		startTime = System.currentTimeMillis();
		this.callback = callback;
		synchronized(_prb) {
			try {
				_prb.addListener(myListener = new PartiallyReceivedBlock.PacketReceivedListener() {

					@Override
					public void packetReceived(int packetNo) {
						// Ignore
					}

					@Override
					public void receiveAborted(int reason, String description) {
						complete(reason, description);
					}
				});
			} catch (AbortedException e) {
				try {
					callback.blockReceived(_prb.getBlock());
					return;
				} catch (AbortedException e1) {
					e = e1;
				}
				callback.blockReceiveFailed(new RetrievalException(_prb._abortReason, _prb._abortDescription));
				return;
			}
		}
		incRunningBlockReceives();
		try {
			waitNotification(false);
		} catch (DisconnectedException e) {
			RetrievalException retrievalException = new RetrievalException(RetrievalException.SENDER_DISCONNECTED);
			_prb.abort(retrievalException.getReason(), retrievalException.toString(), true /* kind of, it shouldn't count towards the stats anyway */);
			callback.blockReceiveFailed(retrievalException);
			decRunningBlockReceives();
		} catch(RuntimeException e) {
			decRunningBlockReceives();
			throw e;
		} catch (Error e) {
			decRunningBlockReceives();
			throw e;
		}
	}
	
	private static MedianMeanRunningAverage avgTimeTaken = new MedianMeanRunningAverage();
	
	private void maybeResetDiscardFilter() {
		long timeleft=discardEndTime-System.currentTimeMillis();
		if (timeleft>0) {
			try {
				discardFilter.setTimeout((int)timeleft);
				_usm.addAsyncFilter(discardFilter, this, _ctr);
			} catch (DisconnectedException e) {
				//ignore
			}
		}
	}
	
	/**
	 * Used to discard leftover messages, usually just packetTransmit and allSent.
	 * allSent, is quite common, as the receive() routine usually quits immediately on receiving all packets.
	 * packetTransmit is less common, when receive() requested what it thought was a missing packet, only reordered.
	 */
	@Override
	public void onMatched(Message m) {
		if (logMINOR)
			Logger.minor(this, "discarding message post-receive: "+m);
		maybeResetDiscardFilter();												   
	}
	
	@Override
	public boolean shouldTimeout() {
		return false;
	}
	
	@Override
	public void onTimeout() {
		//ignore
	}

	@Override
	public void onDisconnect(PeerContext ctx) {
		// Ignore
	}

	@Override
	public void onRestarted(PeerContext ctx) {
		// Ignore
	}

	public synchronized boolean senderAborted() {
		return senderAborted;
	}
	
	static int runningBlockReceives = 0;
	
	private void incRunningBlockReceives() {
		if(logMINOR) Logger.minor(this, "Starting block receive "+_uid);
		synchronized(BlockReceiver.class) {
			runningBlockReceives++;
			if(logMINOR) Logger.minor(BlockTransmitter.class, "Started a block receive, running: "+runningBlockReceives);
		}
	}
	
	private void decRunningBlockReceives() {
		if(logMINOR) Logger.minor(this, "Stopping block receive "+_uid);
		synchronized(BlockReceiver.class) {
			runningBlockReceives--;
			if(logMINOR) Logger.minor(BlockTransmitter.class, "Finished a block receive, running: "+runningBlockReceives);
		}
	}

	public synchronized static int getRunningReceives() {
		return runningBlockReceives;
	}
	
	@Override
	public String toString() {
		return super.toString()+":"+_uid+":"+_sender.shortToString();
	}
	
}
