package freenet.node;

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

import java.lang.ref.WeakReference;
import java.util.HashSet;

import freenet.support.Logger;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger.LogLevel;

/**
 * Base class for tags representing a running request. These store enough information
 * to detect whether they are finished; if they are still in the list, this normally
 * represents a bug.
 * @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
 */
public abstract class UIDTag {
	
    private static volatile boolean logMINOR;
    
    static {
    	Logger.registerLogThresholdCallback(new LogThresholdCallback(){
    		@Override
    		public void shouldUpdate(){
    			logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    		}
    	});
    }
    
	final long createdTime;
	final boolean wasLocal;
	private final WeakReference<PeerNode> sourceRef;
	final boolean realTimeFlag;
	protected final RequestTracker tracker;
	protected boolean accepted;
	protected boolean sourceRestarted;
	private boolean slowDown;
	
	/** Nodes we have routed to at some point */
	private HashSet<PeerNode> routedTo = null;
	/** Nodes we are currently talking to i.e. which have not yet removed our UID from the
	 * list of active requests. */
	private HashSet<PeerNode> currentlyRoutingTo = null;
	/** Node we are currently doing an offered-key-fetch from */
	private HashSet<PeerNode> fetchingOfferedKeyFrom = null;
	/** We are waiting for two stage timeouts from these nodes. If the handler is unlocked
	 * and there are still nodes in the above two, we will log an error; but if those nodes
	 * are here too, we will reassignTagToSelf and not log an error. */
	private HashSet<PeerNode> handlingTimeouts = null;
	protected boolean notRoutedOnwards;
	final long uid;
	
	protected boolean unlockedHandler;
	protected boolean noRecordUnlock;
	private boolean hasUnlocked;
	
	private boolean waitingForSlot;
	
	UIDTag(PeerNode source, boolean realTimeFlag, long uid, Node node) {
		createdTime = System.currentTimeMillis();
		this.sourceRef = source == null ? null : source.myRef;
		wasLocal = source == null;
		this.realTimeFlag = realTimeFlag;
		this.tracker = node.getTracker();
		this.uid = uid;
		if(logMINOR)
			Logger.minor(this, "Created "+this);
		if(wasLocal) accepted = true; // FIXME remove, but it's always true at the moment.
	}

	public abstract void logStillPresent(Long uid);

	long age() {
		return System.currentTimeMillis() - createdTime;
	}
	
	/** Notify that we are routing to, or fetching an offered key from, a 
	 * specific node. This should be called before we send the actual request
	 * message, to avoid us thinking we have more outgoing capacity than we
	 * actually have on a specific peer.
	 * @param peer The peer we are routing to.
	 * @param offeredKey If true, we are fetching an offered key, if false we
	 * are routing a normal request. Fetching an offered key is quite distinct,
	 * notably it has much shorter timeouts.
	 * @return True if we were already routing to (or fetching an offered key 
	 * from, depending on offeredKey) the peer.
	 */
	public synchronized boolean addRoutedTo(PeerNode peer, boolean offeredKey) {
		if(logMINOR)
			Logger.minor(this, "Routing to "+peer+" on "+this+(offeredKey ? " (offered)" : ""), new Exception("debug"));
		if(routedTo == null) routedTo = new HashSet<PeerNode>();
		routedTo.add(peer);
		if(offeredKey) {
			if(fetchingOfferedKeyFrom == null) fetchingOfferedKeyFrom = new HashSet<PeerNode>();
			return fetchingOfferedKeyFrom.add(peer);
		} else {
			if(currentlyRoutingTo == null) currentlyRoutingTo = new HashSet<PeerNode>();
			return currentlyRoutingTo.add(peer);
		}
	}

	public synchronized boolean hasRoutedTo(PeerNode peer) {
		if(routedTo == null) return false;
		return routedTo.contains(peer);
	}

	public synchronized boolean currentlyRoutingTo(PeerNode peer) {
		if(currentlyRoutingTo == null) return false;
		return currentlyRoutingTo.contains(peer);
	}
	
	// Note that these don't actually get removed until the request is finished, unless there is a disconnection or similar. But that
	// is generally not a problem as they complete quickly and successfully mostly.
	// The alternative would be to remove when the transfer is finished, but that does not
	// guarantee the UID has been freed; we can only safely (for load management purposes)
	// remove once we have an acknowledgement which is sent after the UID is removed.
	
	public synchronized boolean currentlyFetchingOfferedKeyFrom(PeerNode peer) {
		if(fetchingOfferedKeyFrom == null) return false;
		return fetchingOfferedKeyFrom.contains(peer);
	}

	/** Notify that we are no longer fetching an offered key from a specific 
	 * node. Must be called only when we are sure the next node doesn't think
	 * we are routing to it any more. See removeRoutingTo() explanation for more
	 * detail. When we are not routing to any nodes, and not fetching from, and
	 * the handler has also been unlocked, the UID is unlocked.
	 * @param next The node we are no longer fetching an offered key from.
	 */
	public void removeFetchingOfferedKeyFrom(PeerNode next) {
		boolean noRecordUnlock;
		synchronized(this) {
			if(fetchingOfferedKeyFrom == null) return;
			fetchingOfferedKeyFrom.remove(next);
			if(handlingTimeouts != null) {
				handlingTimeouts.remove(next);
			}
			if(!mustUnlock()) return;
			noRecordUnlock = this.noRecordUnlock;
		}
		if(logMINOR) Logger.minor(this, "Unlocking "+this);
		innerUnlock(noRecordUnlock);
	}
	
	/** Notify that we are no longer routing to a specific node. When we are
	 * not routing to (or fetching offered keys from) any nodes, and the handler
	 * side has also been unlocked, the whole tag is unlocked (note that this 
	 * is only relevant to incoming requests; outgoing requests only care about
	 * what we are routing to). We should not call this method until we are 
	 * reasonably sure that the node in question no longer thinks we are 
	 * routing to it. Whereas we unlock the handler as soon as possible, 
	 * without waiting for acknowledgement of our completion notice. Be 
	 * cautious (late) in what you send, and generous (early) in what 
	 * you accept! This avoids problems with the previous node thinking we've
	 * finished when we haven't, or us thinking the next node has finished when
	 * it hasn't.
	 * @param next The node we are no longer routing to.
	 */
	public void removeRoutingTo(PeerNode next) {
		if(logMINOR) {
			Logger.minor(this, "No longer routing to "+next+" on "+this, new Exception("debug"));
		}
		boolean noRecordUnlock;
		synchronized(this) {
			if(currentlyRoutingTo == null) {
				return;
			}
			if(!currentlyRoutingTo.remove(next)) {
				if(logMINOR) {
					Logger.minor(this, "Removing wrong node or removing twice? on "+this+" : "+next, new Exception("debug"));
				}
			}
			if(handlingTimeouts != null) {
				handlingTimeouts.remove(next);
			}
			if(!mustUnlock()) return;
			noRecordUnlock = this.noRecordUnlock;
		}
		if(logMINOR) {
			Logger.minor(this, "Unlocking "+this);
		}
		innerUnlock(noRecordUnlock);
	}
	
	protected void innerUnlock(boolean noRecordUnlock) {
		tracker.unlockUID(this, false, noRecordUnlock);
	}

	public void postUnlock() {
		PeerNode[] peers;
		synchronized(this) {
			if(routedTo != null)
				peers = routedTo.toArray(new PeerNode[routedTo.size()]);
			else
				peers = null;
		}
		if(peers != null)
			for(PeerNode p : peers)
				p.postUnlock(this);
	}

	/** Add up the expected transfers in.
	 * @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local.
	 * @param outwardTransfersPerInsert Expected number of outward transfers for an insert.
	 * @param forAccept If true, we are deciding whether to accept a request.
	 * If false, we are deciding whether to SEND a request. We need to be more
	 * careful for the latter than the former, to avoid unnecessary rejections 
	 * and mandatory backoffs.
	 */
	public abstract int expectedTransfersIn(boolean ignoreLocalVsRemote, int outwardTransfersPerInsert, boolean forAccept);
	
	/** Add up the expected transfers out.
	 * @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local.
	 * @param outwardTransfersPerInsert Expected number of outward transfers for an insert.
	 * @param forAccept If true, we are deciding whether to accept a request.
	 * If false, we are deciding whether to SEND a request. We need to be more
	 * careful for the latter than the former, to avoid unnecessary rejections 
	 * and mandatory backoffs.
	 */
	public abstract int expectedTransfersOut(boolean ignoreLocalVsRemote, int outwardTransfersPerInsert, boolean forAccept);
	
	public synchronized void setNotRoutedOnwards() {
		this.notRoutedOnwards = true;
	}

	private boolean reassigned;
	
	/** Get the effective source node (e.g. for load management). This is null if the tag 
	 * was reassigned to us. */
	public synchronized PeerNode getSource() {
		if(reassigned) return null;
		if(wasLocal) return null;
		return sourceRef.get();
	}

	/** Reassign the tag to us rather than its original sender. */
	public synchronized void reassignToSelf() {
		if(wasLocal) return;
		reassigned = true;
	}
	
	/** Was the request originated locally? This returns the original answer: It is not
	 * affected by reassigning to self. */
	public boolean wasLocal() {
		return wasLocal;
	}
	
	/** Is the request local now? I.e. was it either originated locally or reassigned to
	 * self? */
	public boolean isLocal() {
		if(wasLocal) return true;
		synchronized(this) {
			return reassigned;
		}
	}

	public abstract boolean isSSK();

	public abstract boolean isInsert();

	public abstract boolean isOfferReply();
	
	/** Caller must call innerUnlock(noRecordUnlock) immediately if this returns true. 
	 * Hence derived versions should call mustUnlock() only after they have checked their
	 * own unlock blockers. */
	protected synchronized boolean mustUnlock() {
		if(hasUnlocked) return false;
		if(!unlockedHandler) return false;
		if(currentlyRoutingTo != null && !currentlyRoutingTo.isEmpty()) {
			if(!(reassigned || wasLocal || sourceRestarted || timedOutButContinued)) {
				boolean expected = false;
				if(handlingTimeouts != null) {
					expected = true;
					for(PeerNode pn : currentlyRoutingTo) {
						if(handlingTimeouts.contains(pn)) {
							if(logMINOR) Logger.debug(this, "Still waiting for "+pn.shortToString()+" but expected because handling timeout in unlockHandler - will reassign to self to resolve timeouts");
							break;
						}
						expected = false;
					}
				}
				if(!expected) {
					if(handlingTimeouts != null)
						Logger.normal(this, "Unlocked handler but still routing to "+currentlyRoutingTo+" - expected because have timed out so a fork might have succeeded and we might be waiting for the original");
					else
						Logger.error(this, "Unlocked handler but still routing to "+currentlyRoutingTo+" yet not reassigned on "+this, new Exception("debug"));
				} else
					reassignToSelf();
			}
			return false;
		}
		if(fetchingOfferedKeyFrom != null && !fetchingOfferedKeyFrom.isEmpty()) {
			if(!(reassigned || wasLocal)) {
				boolean expected = false;
				if(handlingTimeouts != null) {
					expected = true;
					for(PeerNode pn : fetchingOfferedKeyFrom) {
						if(handlingTimeouts.contains(pn)) {
							if(logMINOR) Logger.debug(this, "Still waiting for "+pn.shortToString()+" but expected because handling timeout in unlockHandler - will reassign to self to resolve timeouts");
							break;
						}
						expected = false;
					}
				}
				if(!expected)
					// Fork succeeds can't happen for fetch-offered-keys.
					Logger.error(this, "Unlocked handler but still fetching offered keys from "+fetchingOfferedKeyFrom+" yet not reassigned on "+this, new Exception("debug"));
				else
					reassignToSelf();
			}
			return false;
		}
		hasUnlocked = true;
		return true;
	}
	
	/** Unlock the handler. That is, the incoming request has finished. This 
	 * method should be called before the acknowledgement that the request has
	 * finished is sent downstream. Therefore, we will never be waiting for an
	 * acknowledgement from downstream in order to release the slot it is using,
	 * during which time it might think we are rejecting wrongly.
	 * 
	 * Once both the incoming and outgoing requests are unlocked, the whole tag
	 * is unlocked.
	 */
	public void unlockHandler(boolean noRecord) {
		boolean canUnlock;
		synchronized(this) {
			if(unlockedHandler) return;
			noRecordUnlock = noRecord;
			unlockedHandler = true;
			canUnlock = mustUnlock();
		}
		if(canUnlock)
			innerUnlock(noRecordUnlock);
		else {
			Logger.normal(this, "Cannot unlock yet in unlockHandler, still sending requests");
		}
	}

	public void unlockHandler() {
		unlockHandler(false);
	}

	// LOCKING: Synchronized because of access to currentlyRoutingTo i.e. to avoid ConcurrentModificationException.
	// UIDTag lock is always taken last anyway so this is safe.
	// Also it is only used in logging anyway.
	@Override
	public synchronized String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(super.toString());
		sb.append(":");
		sb.append(uid);
		if(unlockedHandler)
			sb.append(" (unlocked handler)");
		if(hasUnlocked)
			sb.append(" (unlocked)");
		if(noRecordUnlock)
			sb.append(" (don't record unlock)");
		if(currentlyRoutingTo != null && !currentlyRoutingTo.isEmpty()) {
			sb.append(" (routing to ");
			for(PeerNode pn : currentlyRoutingTo) {
				sb.append(pn.shortToString());
				sb.append(",");
			}
			sb.setLength(sb.length()-1);
			sb.append(")");
		}
		if(fetchingOfferedKeyFrom != null)
			sb.append(" (fetch offered keys from ").append(fetchingOfferedKeyFrom.size()).append(")");
		if(sourceRestarted)
			sb.append(" (source restarted)");
		if(timedOutButContinued)
			sb.append(" (timed out but continued)");
		return sb.toString();
	}

	/** Mark a peer as handling a timeout. Hence if when the handler is unlocked, this 
	 * peer is still marked as routing to (or fetching offered keys from), rather than 
	 * logging an error, we will reassign this tag to self, to wait for the fatal timeout.
	 * @param next
	 */
	public synchronized void handlingTimeout(PeerNode next) {
		if(handlingTimeouts == null)
			handlingTimeouts = new HashSet<PeerNode>();
		handlingTimeouts.add(next);
	}

	private long loggedStillPresent;
	private static final long LOGGED_STILL_PRESENT_INTERVAL = SECONDS.toMillis(60);

	public void maybeLogStillPresent(long now, Long uid) {
		if(now - createdTime > RequestTracker.TIMEOUT) {
			synchronized(this) {
				if(now - loggedStillPresent < LOGGED_STILL_PRESENT_INTERVAL) return;
				loggedStillPresent = now;
			}
			logStillPresent(uid);
		}
	}

	public synchronized void setAccepted() {
		accepted = true;
	}
	
	private boolean timedOutButContinued;

	/** Set when we are going to tell downstream that the request has timed out,
	 * but can't terminate it yet. We will terminate the request if we have to
	 * reroute it, and we count it towards the peer's limit, but we don't stop
	 * messages to the request source. */
	public synchronized void timedOutToHandlerButContinued() {
		timedOutButContinued = true;
	}
	
	/** The handler disconnected or restarted. */
	public synchronized void onRestartOrDisconnectSource() {
		sourceRestarted = true;
	}
	
	// The third option is reassignToSelf(). We only use that when we actually
	// want the data, and mean to continue. In that case, none of the next three
	// are appropriate.
	
	/** Should we deduct this request from the source's limit, instead of 
	 * counting it towards it? A normal request is counted towards it. A hidden
	 * request is deducted from it. This is used when the source has restarted
	 * but also in some other cases. */
	public synchronized boolean countAsSourceRestarted() {
		return sourceRestarted || timedOutButContinued;
	}
	
	/** Should we send messages to the source? */
	public synchronized boolean hasSourceReallyRestarted() {
		return sourceRestarted;
	}
	
	/** Should we stop the request as soon as is convenient? Normally this 
	 * happens when the source is restarted or disconnected. */
	public synchronized boolean shouldStop() {
		return sourceRestarted || timedOutButContinued;
	}

	public synchronized boolean isSource(PeerNode pn) {
		if(reassigned) return false;
		if(wasLocal) return false;
		if(sourceRef == null) return false;
		return sourceRef == pn.myRef;
	}
	
	public synchronized void setWaitingForSlot() {
		// FIXME use a counter on Node.
		// We'd need to ensure it ALWAYS gets unset when some wierd
		// error happens.
		if(waitingForSlot) return;
		waitingForSlot = true;
	}
	
	public synchronized void clearWaitingForSlot() {
		// FIXME use a counter on Node.
		// We'd need to ensure it ALWAYS gets unset when some wierd
		// error happens.
		// Probably we can do this just by calling clearWaitingForSlot() when unlocking???
		if(!waitingForSlot) return;
		waitingForSlot = false;
	}
	
	public synchronized boolean isWaitingForSlot() {
		return waitingForSlot;
	}

	/** Set a flag indicating the originator should slow down. Only used at the shouldRejectRequest stage. */
	synchronized void slowDown() {
		slowDown = true;
	}

	/** Query the slow-down flag. Should be checked after shouldRejectRequest. */
	synchronized boolean shouldSlowDown() {
		return slowDown;
	}
}
