package freenet.support;

import freenet.support.Logger.LogLevel;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

/**
 * Token bucket. Can be used for e.g. bandwidth limiting.
 * Tokens are added once per tick.
 */
@Deprecated public class TokenBucket {

	private static boolean logMINOR;
	static {
		LoggerHook.registerClass(TokenBucket.class);
	}
	protected long current;
	protected long max;
	protected long timeLastTick;
	protected long nanosPerTick;
	
	/**
	 * Create a token bucket.
	 * @param max The maximum size of the bucket, in tokens.
	 * @param nanosPerTick The number of nanoseconds between ticks.
	 */
	public TokenBucket(long max, long nanosPerTick, long initialValue) {
		this.max = max;
		this.current = initialValue;
		if(current > max) {
			Logger.error(this, "initial value ("+current+") > max ("+max+") in "+this, new Exception("error"));
			current = max;
		}
		this.nanosPerTick = nanosPerTick;
		long now = System.currentTimeMillis();
		this.timeLastTick = NANOSECONDS.convert(now, MILLISECONDS);
		if(nanosPerTick <= 0) throw new IllegalArgumentException();
		if(max <= 0) throw new IllegalArgumentException();
	}
	
	/**
	 * Either grab a bunch of tokens, or don't. Never block.
	 * @param tokens The number of tokens to grab.
	 * @return True if we could acquire the tokens.
	 */
	public synchronized boolean instantGrab(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't grab negative tokens: "+tokens);
		if(logMINOR)
			Logger.minor(this, "instant grab: "+tokens+" current="+current+" max="+max);
		addTokens();
		if(logMINOR)
			Logger.minor(this, "instant grab: "+tokens+" current="+current+" max="+max);
		if(current >= tokens) {
			current -= tokens;
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Try to grab some tokens; if there aren't enough, grab all of them. Never block.
	 * @param tokens The number of tokens to grab.
	 * @return The number of tokens grabbed.
	 */
	public synchronized long partialInstantGrab(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't grab negative tokens: "+tokens);
		if(logMINOR)
			Logger.minor(this, "instant grab: "+tokens+" current="+current+" max="+max);
		addTokens();
		if(logMINOR)
			Logger.minor(this, "instant grab: "+tokens+" current="+current+" max="+max);
		if(current >= tokens) {
			current -= tokens;
			return tokens;
		} else {
			tokens = current;
			current = 0;
			return tokens;
		}
	}
	
	/**
	 * Remove tokens, without blocking, even if it causes the balance to go negative.
	 * @param tokens The number of tokens to remove.
	 */
	public synchronized void forceGrab(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't grab negative tokens: "+tokens);
		if(logMINOR) Logger.minor(this, "forceGrab("+tokens+")");
		addTokens();
		current -= tokens;
		if(logMINOR) Logger.minor(this, "Removed tokens, balance now "+current);
	}
	
	public synchronized long count() {
		return current;
	}
	
	/**
	 * Get the current number of available tokens.
	 */
	public synchronized long getCount() {
		addTokens();
		return current;
	}

	protected long offset() {
		return 0;
	}
	
	public synchronized void blockingGrab(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't grab negative tokens: "+tokens);
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
		if(logMINOR) Logger.minor(this, "Blocking grab: "+tokens);
		if(tokens < max)
			innerBlockingGrab(tokens);
		else {
			for(int i=0;i<tokens;i+=max) {
				innerBlockingGrab(Math.min(tokens, max));
			}
		}
	}
	
	/**
	 * Grab a bunch of tokens. Block if necessary.
	 * @param tokens The number of tokens to grab.
	 */
	public synchronized void innerBlockingGrab(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't grab negative tokens: "+tokens);
		if(logMINOR) Logger.minor(this, "Inner blocking grab: "+tokens);
		addTokens();
		if(logMINOR) Logger.minor(this, "current="+current);
		
		current -= tokens;
		
		if(current >= 0) {
			if(logMINOR) Logger.minor(this, "Got tokens instantly, current="+current);
			return;
		} else {
			if(logMINOR) Logger.minor(this, "Blocking grab removed tokens, current="+current+" - will have to wait because negative...");
		}
		
		long minDelayNS = nanosPerTick * (-current);
		long minDelayMS = MILLISECONDS.convert(minDelayNS + MILLISECONDS.toNanos(1) - 1, NANOSECONDS);
		long now = System.currentTimeMillis();
		long wakeAt = now + minDelayMS;
		
		if(logMINOR) Logger.minor(this, "Waking in "+minDelayMS+" millis");
		
		while(true) {
			now = System.currentTimeMillis();
			int delay = (int) Math.min(Integer.MAX_VALUE, wakeAt - now);
			if(delay <= 0) break;
			if(logMINOR) Logger.minor(this, "Waiting "+delay+"ms");
			try {
				wait(delay);
			} catch (InterruptedException e) {
				// Go around the loop again.
			}
		}
		if(logMINOR) Logger.minor(this, "Blocking grab finished: current="+current);
	}

	public synchronized void recycle(long tokens) {
		if(tokens < 0) throw new IllegalArgumentException("Can't recycle negative tokens: "+tokens);
		current += tokens;
		if(current > max) current = max;
	}
	
	/**
	 * Change the number of nanos per tick.
	 * @param nanosPerTick The new number of nanos per tick.
	 */
	public synchronized void changeNanosPerTick(long nanosPerTick) {
		if(nanosPerTick <= 0) throw new IllegalArgumentException();
		// Synchronize up first, using the old nanosPerTick.
		addTokens();
		this.nanosPerTick = nanosPerTick;
		if(nanosPerTick < this.nanosPerTick)
			notifyAll();
	}

	public synchronized void changeBucketSize(long newMax) {
		if(newMax <= 0) throw new IllegalArgumentException();
		max = newMax;
		addTokens();
	}
	
	public synchronized void changeNanosAndBucketSize(long nanosPerTick, long newMax) {
		if(nanosPerTick <= 0) throw new IllegalArgumentException();
		if(newMax <= 0) throw new IllegalArgumentException();
		// Synchronize up first, using the old nanosPerTick.
		addTokensNoClip();
		if(nanosPerTick < this.nanosPerTick)
			notifyAll();
		this.nanosPerTick = nanosPerTick;
		this.max = newMax;
		if(current > max) current = max;
	}
	
	public synchronized void addTokens() {
		addTokensNoClip();
		if(current > max) current = max;
		if(logMINOR)
			Logger.minor(this, "addTokens: Clipped, current="+current);
	}
	
	/**
	 * Update the number of tokens according to elapsed time.
	 */
	public synchronized void addTokensNoClip() {
		long add = tokensToAdd();
		current += add;
		timeLastTick += add * nanosPerTick;
		if(logMINOR)
			Logger.minor(this, "addTokensNoClip: Added "+add+" tokens, current="+current);
		// Deliberately do not clip to size at this point; caller must do this, but it is usually beneficial for the caller to do so.
	}
	
	synchronized long tokensToAdd() {
		long nowNS = NANOSECONDS.convert(System.currentTimeMillis(), MILLISECONDS);
		if(timeLastTick > nowNS) {
			System.err.println("CLOCK SKEW DETECTED! CLOCK WENT BACKWARDS BY AT LEAST "+TimeUtil.formatTime(MILLISECONDS.convert(timeLastTick - nowNS, NANOSECONDS), 2, true));
			System.err.println("FREENET WILL BREAK SEVERELY IF THIS KEEPS HAPPENING!");
			Logger.error(this, "CLOCK SKEW DETECTED! CLOCK WENT BACKWARDS BY AT LEAST "+TimeUtil.formatTime(MILLISECONDS.convert(timeLastTick - nowNS, NANOSECONDS), 2, true));
			timeLastTick = nowNS;
			return 0;
		}
		long nextTick = timeLastTick + nanosPerTick;
		if(nextTick > nowNS) {
			return 0;
		}
		if(nextTick + nanosPerTick > nowNS) {
			return 1;
		}
		return (nowNS - nextTick) / nanosPerTick;
	}
	
	public synchronized long getNanosPerTick() {
		return nanosPerTick;
	}
}
