/* 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 freenet.client.async.ClientContext;
import freenet.client.async.ClientRequestScheduler;
import freenet.config.Config;
import freenet.config.EnumerableOptionCallback;
import freenet.config.InvalidConfigValueException;
import freenet.config.SubConfig;
import freenet.crypt.RandomSource;
import freenet.keys.Key;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.SimpleFieldSet;
import freenet.support.TimeUtil;
import freenet.support.api.StringCallback;
import freenet.support.math.BootstrappingDecayingRunningAverage;

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

public class RequestStarterGroup {
	private static volatile boolean logMINOR;

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

	private final ThrottleWindowManager throttleWindowBulk;
	private final ThrottleWindowManager throttleWindowRT;
	// These are for diagnostic purposes
	private final ThrottleWindowManager throttleWindowCHK;
	private final ThrottleWindowManager throttleWindowSSK;
	private final ThrottleWindowManager throttleWindowInsert;
	private final ThrottleWindowManager throttleWindowRequest;
	final MyRequestThrottle chkRequestThrottleBulk;
	final RequestStarter chkRequestStarterBulk;
	final MyRequestThrottle chkInsertThrottleBulk;
	final RequestStarter chkInsertStarterBulk;
	final MyRequestThrottle sskRequestThrottleBulk;
	final RequestStarter sskRequestStarterBulk;
	final MyRequestThrottle sskInsertThrottleBulk;
	final RequestStarter sskInsertStarterBulk;
	
	final MyRequestThrottle chkRequestThrottleRT;
	final RequestStarter chkRequestStarterRT;
	final MyRequestThrottle chkInsertThrottleRT;
	final RequestStarter chkInsertStarterRT;
	final MyRequestThrottle sskRequestThrottleRT;
	final RequestStarter sskRequestStarterRT;
	final MyRequestThrottle sskInsertThrottleRT;
	final RequestStarter sskInsertStarterRT;

	public final ClientRequestScheduler chkFetchSchedulerBulk;
	public final ClientRequestScheduler chkPutSchedulerBulk;
	public final ClientRequestScheduler sskFetchSchedulerBulk;
	public final ClientRequestScheduler sskPutSchedulerBulk;
	public final ClientRequestScheduler chkFetchSchedulerRT;
	public final ClientRequestScheduler chkPutSchedulerRT;
	public final ClientRequestScheduler sskFetchSchedulerRT;
	public final ClientRequestScheduler sskPutSchedulerRT;

	private final NodeStats stats;
	RequestStarterGroup(Node node, NodeClientCore core, int portNumber, RandomSource random, Config config, SimpleFieldSet fs, ClientContext ctx) throws InvalidConfigValueException {
		SubConfig schedulerConfig = config.createSubConfig("node.scheduler");
		this.stats = core.nodeStats;
		
		throttleWindowBulk = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindow"), node);
		throttleWindowRT = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindowRT"), node);
		
		throttleWindowCHK = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindowCHK"), node);
		throttleWindowSSK = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindowSSK"), node);
		throttleWindowInsert = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindowInsert"), node);
		throttleWindowRequest = new ThrottleWindowManager(2.0, fs == null ? null : fs.subset("ThrottleWindowRequest"), node);
		chkRequestThrottleBulk = new MyRequestThrottle(5000, "CHK Request", fs == null ? null : fs.subset("CHKRequestThrottle"), 32768, false);
		chkRequestThrottleRT = new MyRequestThrottle(5000, "CHK Request (RT)", fs == null ? null : fs.subset("CHKRequestThrottleRT"), 32768, true);
		chkRequestStarterBulk = new RequestStarter(core, chkRequestThrottleBulk, "CHK Request starter ("+portNumber+ ')', stats.localChkFetchBytesSentAverage, stats.localChkFetchBytesReceivedAverage, false, false, false);
		chkRequestStarterRT = new RequestStarter(core, chkRequestThrottleRT, "CHK Request starter ("+portNumber+ ')', stats.localChkFetchBytesSentAverage, stats.localChkFetchBytesReceivedAverage, false, false, true);
		chkFetchSchedulerBulk = new ClientRequestScheduler(false, false, false, random, chkRequestStarterBulk, node, core, "CHKrequester", ctx);
		chkFetchSchedulerRT = new ClientRequestScheduler(false, false, true, random, chkRequestStarterRT, node, core, "CHKrequester", ctx);
		chkRequestStarterBulk.setScheduler(chkFetchSchedulerBulk);
		chkRequestStarterRT.setScheduler(chkFetchSchedulerRT);
		
		registerSchedulerConfig(schedulerConfig, "CHKrequester", chkFetchSchedulerBulk, chkFetchSchedulerRT, false, false);
		
		//insertThrottle = new ChainedRequestThrottle(10000, 2.0F, requestThrottle);
		// FIXME reenable the above
		chkInsertThrottleBulk = new MyRequestThrottle(20000, "CHK Insert", fs == null ? null : fs.subset("CHKInsertThrottle"), 32768, false);
		chkInsertThrottleRT = new MyRequestThrottle(20000, "CHK Insert (RT)", fs == null ? null : fs.subset("CHKInsertThrottleRT"), 32768, true);
		chkInsertStarterBulk = new RequestStarter(core, chkInsertThrottleBulk, "CHK Insert starter ("+portNumber+ ')', stats.localChkInsertBytesSentAverage, stats.localChkInsertBytesReceivedAverage, true, false, false);
		chkInsertStarterRT = new RequestStarter(core, chkInsertThrottleRT, "CHK Insert starter ("+portNumber+ ')', stats.localChkInsertBytesSentAverage, stats.localChkInsertBytesReceivedAverage, true, false, true);
		chkPutSchedulerBulk = new ClientRequestScheduler(true, false, false, random, chkInsertStarterBulk, node, core, "CHKinserter", ctx);
		chkPutSchedulerRT = new ClientRequestScheduler(true, false, true, random, chkInsertStarterRT, node, core, "CHKinserter", ctx);
		chkInsertStarterBulk.setScheduler(chkPutSchedulerBulk);
		chkInsertStarterRT.setScheduler(chkPutSchedulerRT);
		
		registerSchedulerConfig(schedulerConfig, "CHKinserter", chkPutSchedulerBulk, chkPutSchedulerRT, false, true);
		
		sskRequestThrottleBulk = new MyRequestThrottle(5000, "SSK Request", fs == null ? null : fs.subset("SSKRequestThrottle"), 1024, false);
		sskRequestThrottleRT = new MyRequestThrottle(5000, "SSK Request (RT)", fs == null ? null : fs.subset("SSKRequestThrottleRT"), 1024, true);
		sskRequestStarterBulk = new RequestStarter(core, sskRequestThrottleBulk, "SSK Request starter ("+portNumber+ ')', stats.localSskFetchBytesSentAverage, stats.localSskFetchBytesReceivedAverage, false, true, false);
		sskRequestStarterRT = new RequestStarter(core, sskRequestThrottleRT, "SSK Request starter ("+portNumber+ ')', stats.localSskFetchBytesSentAverage, stats.localSskFetchBytesReceivedAverage, false, true, true);
		sskFetchSchedulerBulk = new ClientRequestScheduler(false, true, false, random, sskRequestStarterBulk, node, core, "SSKrequester", ctx);
		sskFetchSchedulerRT = new ClientRequestScheduler(false, true, true, random, sskRequestStarterRT, node, core, "SSKrequester", ctx);
		sskRequestStarterBulk.setScheduler(sskFetchSchedulerBulk);
		sskRequestStarterRT.setScheduler(sskFetchSchedulerRT);
		
		registerSchedulerConfig(schedulerConfig, "SSKrequester", sskFetchSchedulerBulk, sskFetchSchedulerRT, true, false);
		
		//insertThrottle = new ChainedRequestThrottle(10000, 2.0F, requestThrottle);
		// FIXME reenable the above
		sskInsertThrottleBulk = new MyRequestThrottle(20000, "SSK Insert", fs == null ? null : fs.subset("SSKInsertThrottle"), 1024, false);
		sskInsertThrottleRT = new MyRequestThrottle(20000, "SSK Insert", fs == null ? null : fs.subset("SSKInsertThrottleRT"), 1024, true);
		sskInsertStarterBulk = new RequestStarter(core, sskInsertThrottleBulk, "SSK Insert starter ("+portNumber+ ')', stats.localSskInsertBytesSentAverage, stats.localSskFetchBytesReceivedAverage, true, true, false);
		sskInsertStarterRT = new RequestStarter(core, sskInsertThrottleRT, "SSK Insert starter ("+portNumber+ ')', stats.localSskInsertBytesSentAverage, stats.localSskFetchBytesReceivedAverage, true, true, true);
		sskPutSchedulerBulk = new ClientRequestScheduler(true, true, false, random, sskInsertStarterBulk, node, core, "SSKinserter", ctx);
		sskPutSchedulerRT = new ClientRequestScheduler(true, true, true, random, sskInsertStarterRT, node, core, "SSKinserter", ctx);
		sskInsertStarterBulk.setScheduler(sskPutSchedulerBulk);
		sskInsertStarterRT.setScheduler(sskPutSchedulerRT);
		
		registerSchedulerConfig(schedulerConfig, "SSKinserter", sskPutSchedulerBulk, sskPutSchedulerRT, true, true);
		
		schedulerConfig.finishedInitialization();
	}
	
	private void registerSchedulerConfig(SubConfig schedulerConfig,
			String name, ClientRequestScheduler csBulk,
			ClientRequestScheduler csRT, boolean forSSKs, boolean forInserts) throws InvalidConfigValueException {
		PrioritySchedulerCallback callback = new PrioritySchedulerCallback();
		schedulerConfig.register(name+"_priority_policy", ClientRequestScheduler.PRIORITY_SOFT, name.hashCode(), true, false,
				"RequestStarterGroup.scheduler"+(forSSKs?"SSK" : "CHK")+(forInserts?"Inserts":"Requests"),
				"RequestStarterGroup.schedulerLong",
				callback);
		callback.init(csRT, csBulk, schedulerConfig.getString(name+"_priority_policy"));
	}

	public void start() {
		chkRequestStarterRT.start();
		chkInsertStarterRT.start();
		sskRequestStarterRT.start();
		sskInsertStarterRT.start();
		chkRequestStarterBulk.start();
		chkInsertStarterBulk.start();
		sskRequestStarterBulk.start();
		sskInsertStarterBulk.start();
	}
	
	public class MyRequestThrottle implements BaseRequestThrottle {
		private final BootstrappingDecayingRunningAverage roundTripTime;
		/** Data size for purposes of getRate() */
		private final int size;
		private final boolean realTime;

		public MyRequestThrottle(int rtt, String string, SimpleFieldSet fs, int size, boolean realTime) {
			roundTripTime = new BootstrappingDecayingRunningAverage(rtt, 10, MINUTES.toMillis(5), 10, fs == null ? null : fs.subset("RoundTripTime"));
			this.size = size;
			this.realTime = realTime;
		}

		@Override
		public synchronized long getDelay() {
			double rtt = roundTripTime.currentValue();
			double winSizeForMinPacketDelay = rtt / MIN_DELAY;
			double _simulatedWindowSize = getThrottleWindow().currentValue(realTime);
			if (_simulatedWindowSize > winSizeForMinPacketDelay) {
				_simulatedWindowSize = winSizeForMinPacketDelay;
			}
			if (_simulatedWindowSize < 1.0) {
				_simulatedWindowSize = 1.0F;
			}
			// return (long) (_roundTripTime / _simulatedWindowSize);
			return Math.max(MIN_DELAY, Math.min((long) (rtt / _simulatedWindowSize), MAX_DELAY));
		}

		private ThrottleWindowManager getThrottleWindow() {
			return RequestStarterGroup.this.getThrottleWindow(realTime);
		}

		public synchronized void successfulCompletion(long rtt) {
			roundTripTime.report(Math.max(rtt, 10));
			if(logMINOR)
				Logger.minor(this, "Reported successful completion: "+rtt+" on "+this+" avg "+roundTripTime.currentValue());
		}
		
		@Override
		public String toString() {
			return "rtt: "+roundTripTime.currentValue()+" _s="+getThrottleWindow().currentValue(realTime)+" RT="+realTime;
		}

		public SimpleFieldSet exportFieldSet() {
			SimpleFieldSet fs = new SimpleFieldSet(false);
			fs.put("RoundTripTime", roundTripTime.exportFieldSet(false));
			return fs;
		}

		public double getRTT() {
			return roundTripTime.currentValue();
		}

		public long getRate() {
			return (long) ((1000.0 / getDelay()) * size);
		}
	}

	public static class PrioritySchedulerCallback extends StringCallback implements EnumerableOptionCallback {
		ClientRequestScheduler csRT;
		ClientRequestScheduler csBulk;
		private final String[] possibleValues = new String[]{ ClientRequestScheduler.PRIORITY_HARD, ClientRequestScheduler.PRIORITY_SOFT };
		
		public void init(ClientRequestScheduler csRT, ClientRequestScheduler csBulk, String config) throws InvalidConfigValueException{
			this.csRT = csRT;
			this.csBulk = csBulk;
			set(config);
		}
		
		@Override
		public String get(){
			if(csBulk != null)
				return csBulk.getChoosenPriorityScheduler();
			else
				return ClientRequestScheduler.PRIORITY_SOFT;
		}
		
		@Override
		public void set(String val) throws InvalidConfigValueException{
			String value;
			if(val == null || val.equalsIgnoreCase(get())) return;
			if(val.equalsIgnoreCase(ClientRequestScheduler.PRIORITY_HARD)){
				value = ClientRequestScheduler.PRIORITY_HARD;
			}else if(val.equalsIgnoreCase(ClientRequestScheduler.PRIORITY_SOFT)){
				value = ClientRequestScheduler.PRIORITY_SOFT;
			}else{
				throw new InvalidConfigValueException("Invalid priority scheme");
			}
			csBulk.setPriorityScheduler(value);
			csRT.setPriorityScheduler(value);
		}
		
		@Override
		public String[] getPossibleValues() {
			return possibleValues;
		}
	}

	public ThrottleWindowManager getThrottleWindow(boolean realTime) {
		if(realTime) return throttleWindowRT;
		else return throttleWindowBulk;
	}

	public void requestCompleted(boolean isSSK, boolean isInsert, Key key, boolean realTime) {
		getThrottleWindow(realTime).requestCompleted();
		(isSSK ? throttleWindowSSK : throttleWindowCHK).requestCompleted();
		(isInsert ? throttleWindowInsert : throttleWindowRequest).requestCompleted();
		stats.reportOutgoingRequestLocation(key.toNormalizedDouble());
	}
	
	public void rejectedOverload(boolean isSSK, boolean isInsert, boolean realTime) {
		getThrottleWindow(realTime).rejectedOverload();
		(isSSK ? throttleWindowSSK : throttleWindowCHK).rejectedOverload();
		(isInsert ? throttleWindowInsert : throttleWindowRequest).rejectedOverload();
	}
	
	/**
	 * Persist the throttle data to a SimpleFieldSet.
	 */
	SimpleFieldSet persistToFieldSet() {
		SimpleFieldSet fs = new SimpleFieldSet(false);
		fs.put("ThrottleWindow", throttleWindowBulk.exportFieldSet(false));
		fs.put("ThrottleWindowRT", throttleWindowRT.exportFieldSet(false));
		fs.put("ThrottleWindowCHK", throttleWindowCHK.exportFieldSet(false));
		fs.put("ThrottleWindowSSK", throttleWindowCHK.exportFieldSet(false));
		fs.put("CHKRequestThrottle", chkRequestThrottleBulk.exportFieldSet());
		fs.put("SSKRequestThrottle", sskRequestThrottleBulk.exportFieldSet());
		fs.put("CHKInsertThrottle", chkInsertThrottleBulk.exportFieldSet());
		fs.put("SSKInsertThrottle", sskInsertThrottleBulk.exportFieldSet());
		fs.put("CHKRequestThrottleRT", chkRequestThrottleRT.exportFieldSet());
		fs.put("SSKRequestThrottleRT", sskRequestThrottleRT.exportFieldSet());
		fs.put("CHKInsertThrottleRT", chkInsertThrottleRT.exportFieldSet());
		fs.put("SSKInsertThrottleRT", sskInsertThrottleRT.exportFieldSet());
		return fs;
	}
	
	public double getWindow(boolean realTime) {
		return getThrottleWindow(realTime).currentValue(realTime);
	}

	public double getRTT(boolean isSSK, boolean isInsert, boolean realTime) {
		return getThrottle(isSSK, isInsert, realTime).getRTT();
	}

	public double getDelay(boolean isSSK, boolean isInsert, boolean realTime) {
		return getThrottle(isSSK, isInsert, realTime).getDelay();
	}
	
	MyRequestThrottle getThrottle(boolean isSSK, boolean isInsert, boolean realTime) {
		if(realTime) {
			if(isSSK) {
				if(isInsert) return sskInsertThrottleRT;
				else return sskRequestThrottleRT;
			} else {
				if(isInsert) return chkInsertThrottleRT;
				else return chkRequestThrottleRT;
			}
		} else {
			if(isSSK) {
				if(isInsert) return sskInsertThrottleBulk;
				else return sskRequestThrottleBulk;
			} else {
				if(isInsert) return chkInsertThrottleBulk;
				else return chkRequestThrottleBulk;
			}
		}
	}

	public String statsPageLine(boolean isSSK, boolean isInsert, boolean realTime) {
		StringBuilder sb = new StringBuilder(100);
		sb.append(isSSK ? "SSK" : "CHK");
		sb.append(' ');
		sb.append(isInsert ? "Insert" : "Request");
		sb.append(' ');
		sb.append(realTime ? "RealTime" : "Bulk");
		sb.append(" RTT=");
		MyRequestThrottle throttle = getThrottle(isSSK, isInsert, realTime);
		sb.append(TimeUtil.formatTime((long)throttle.getRTT(), 2, true));
		sb.append(" delay=");
		sb.append(TimeUtil.formatTime(throttle.getDelay(), 2, true));
		sb.append(" bw=");
		sb.append(throttle.getRate());
		sb.append("B/sec");
		return sb.toString();
	}

	public String diagnosticThrottlesLine(boolean mode) {
		StringBuilder sb = new StringBuilder();
		if(mode) {
			sb.append("Request window: ");
			sb.append(throttleWindowRequest.toString());
			sb.append(", Insert window: ");
			sb.append(throttleWindowInsert.toString());
		} else {
			sb.append("CHK window: ");
			sb.append(throttleWindowCHK.toString());
			sb.append(", SSK window: ");
			sb.append(throttleWindowSSK.toString());
		}
		return sb.toString();
	}

	public double getRealWindow(boolean realTime) {
		return getThrottleWindow(realTime).realCurrentValue();
	}

	public long countQueuedRequests() {
		return chkFetchSchedulerBulk.countQueuedRequests() +
			sskFetchSchedulerBulk.countQueuedRequests() +
			chkPutSchedulerBulk.countQueuedRequests() +
			sskPutSchedulerBulk.countQueuedRequests() +
			chkFetchSchedulerRT.countQueuedRequests() +
			sskFetchSchedulerRT.countQueuedRequests() +
			chkPutSchedulerRT.countQueuedRequests() +
			sskPutSchedulerRT.countQueuedRequests();
	}

	public ClientRequestScheduler getScheduler(boolean ssk, boolean insert,
			boolean realTime) {
		if(realTime) {
			if(insert) {
				return ssk ? sskPutSchedulerRT : chkPutSchedulerRT;
			} else {
				return ssk ? sskFetchSchedulerRT : chkFetchSchedulerRT;
			}
		} else {
			if(insert) {
				return ssk ? sskPutSchedulerBulk : chkPutSchedulerBulk;
			} else {
				return ssk ? sskFetchSchedulerBulk : chkFetchSchedulerBulk;
			}
		}
	}

    public void setGlobalSalt(byte[] salt) {
        chkFetchSchedulerBulk.startCore(salt);
        sskFetchSchedulerBulk.startCore(salt);
        chkPutSchedulerBulk.startCore(salt);
        sskPutSchedulerBulk.startCore(salt);
        chkFetchSchedulerRT.startCore(salt);
        sskFetchSchedulerRT.startCore(salt);
        chkPutSchedulerRT.startCore(salt);
        sskPutSchedulerRT.startCore(salt);
    }

}
