package com.aelitis.azureus.core.neuronal;

public class NeuralSpeedLimiter {
	
	//in bytes / sec
	long maxDlSpeed;
	long maxUlSpeed;
	long ulSpeed;
	long dlSpeed;
	
	//in ms
	long minLatency;
	long maxLatency;
	long latency;
	
	NeuralNetwork neuralNetwork;
	
	private boolean dirty;
	
	private double currentULTarget = 0.6;
	
	double trainingSet[][] = new double[][] {
			//dl speed,	ul speed,	latency,	no_down_limit,	down_limit,	no_up_limit,	up_limit
			
			//no latency => no limits
			{0.0,		0.0,		0.0,		0.9,			0.9,		0.9,			0.9},
			{0.0,		1.0,		0.0,		0.9,			0.9,		0.9,			0.9},
			{1.0,		0.0,		0.0,		0.9,			0.9,		0.9,			0.9},
			{1.0,		1.0,		0.0,		0.9,			0.9,		0.9,			0.9},
			
			//no download, high upload, mid-high latency => limit upload
			{0.0,		1.0,		1.0,		0.9,			0.9,		0.1,			0.1},
			{0.0,		1.0,		0.3,		0.9,			0.9,		0.1,			0.1},
			
			//no download, high upload, low-mid latency => some limit upload
			{0.0,		1.0,		0.1,		0.9,			0.9,		0.1,			0.8},
			{0.0,		1.0,		0.2,		0.9,			0.9,		0.1,			0.6},
			
			//no upload, high download, mid-high latency => limit download
			{1.0,		0.0,		1.0,		0.1,			0.5,		0.9,			0.9},
			{1.0,		0.0,		0.3,		0.1,			0.6,		0.9,			0.9},
			
			//no upload, high download, low-mid latency => some limit download
			{1.0,		0.0,		0.1,		0.1,			0.9,		0.9,			0.9},
			{1.0,		0.0,		0.2,		0.1,			0.8,		0.9,			0.9},
	};
	
	public NeuralSpeedLimiter() {
		neuralNetwork = new NeuralNetwork(3,4,4);
		neuralNetwork.setLearningRate(0.05);
		neuralNetwork.setMomentum(true, 0.9);
		neuralNetwork.setActivationFunction(new LogisticActivationFunction());
		train();
		dirty = false;
	}

	private void train() {
		//System.out.println(neuralNetwork);
		double error = 1.0;
		int c = 0;
		while(error > 0.002 && c < 200000) {
			
			error = 0;
			for(int i = 0 ; i <trainingSet.length ; i++) {
				neuralNetwork.setInput(0, trainingSet[i][0]);
				neuralNetwork.setInput(1, trainingSet[i][1]);
				neuralNetwork.setInput(2, trainingSet[i][2]);
				
				neuralNetwork.setDesiredOutput(0, trainingSet[i][3]);
				neuralNetwork.setDesiredOutput(1, trainingSet[i][4]);
				neuralNetwork.setDesiredOutput(2, trainingSet[i][5]);
				neuralNetwork.setDesiredOutput(3, trainingSet[i][6]);
				
				neuralNetwork.feedForward();
				error += neuralNetwork.calculateError();
				neuralNetwork.backPropagate();
				
			}
			
			error /= trainingSet.length;
			
			c++;
		}
	}

	private void resetInput() {
		try {
			if(ulSpeed > maxUlSpeed) maxUlSpeed = ulSpeed;
			if(dlSpeed > maxDlSpeed) maxDlSpeed = dlSpeed;
			if(latency > maxLatency) maxLatency = latency;
			if(latency < minLatency) minLatency = latency;
			
			double downloadFactor = (double)dlSpeed / maxDlSpeed;
			double uploadFactor = (double)ulSpeed / maxUlSpeed;
			double latencyFactor = ((double)latency - (double)minLatency) / maxLatency;
			
			neuralNetwork.setInput(0, downloadFactor);
			neuralNetwork.setInput(1, uploadFactor);
			neuralNetwork.setInput(2, latencyFactor);
			
			dirty = true;
		} catch(Throwable t) {
			//Ignore
		}
	}
	
	private void retrain(double ulTarget) {
		resetInput();
		neuralNetwork.feedForward();
		double shouldLimitDownload = neuralNetwork.getOutput(0);
		double downloadLimit = neuralNetwork.getOutput(1);
		
		double downloadFactor = (double)dlSpeed / maxDlSpeed;
		double uploadFactor = (double)ulSpeed / maxUlSpeed;
		double latencyFactor = ((double)latency - (double)minLatency) / maxLatency;
		
		
		double error = 1.0;
		int c = 0;
		//Only 100 loops at a time
		while(error > 0.002 && c < 400) {
			neuralNetwork.setInput(0, downloadFactor);
			neuralNetwork.setInput(1, uploadFactor);
			neuralNetwork.setInput(2, latencyFactor);
			
			neuralNetwork.setDesiredOutput(0, shouldLimitDownload);
			neuralNetwork.setDesiredOutput(1, downloadLimit);
			neuralNetwork.setDesiredOutput(2, 0.9);
			neuralNetwork.setDesiredOutput(3, ulTarget);
			
			neuralNetwork.feedForward();
			error = neuralNetwork.calculateError();
			neuralNetwork.backPropagate();
			
			c++;
		}
	}
	
	private void feedForward() {
		neuralNetwork.feedForward();
		dirty = false;
		
		double latencyFactor = ((double)latency - (double)minLatency) / maxLatency;
		
		if(latencyFactor >= 0.15) {
			//So, we have a high latency, let's re-train the neural network to lower upload speed in this case
			currentULTarget = currentULTarget * 0.97;
			retrain(currentULTarget);
		} else if(latencyFactor < 0.05) {
			//So, we have a low latency, let's re-train the neural network to increase upload speed in this case
			currentULTarget = currentULTarget * 1.02;
			retrain(currentULTarget);
		}
		
		/*System.out.println("input : " + (double)dlSpeed/maxDlSpeed + ", " +  (double)ulSpeed/maxUlSpeed + ", " + ((double)latency-(double)minLatency)/maxLatency);
		System.out.println("output : " + neuralNetwork.getOutput(0) + ", " + 
				neuralNetwork.getOutput(1) + ", " +
				neuralNetwork.getOutput(2) + ", " +
				neuralNetwork.getOutput(3));*/
		
	}
	
	public void setMaxDlSpeed(long maxDlSpeed) {
		if(maxDlSpeed > 0) {
			this.maxDlSpeed = maxDlSpeed;
		}
		resetInput();
	}

	public void setMaxUlSpeed(long maxUlSpeed) {
		if(maxUlSpeed > 0) {
			this.maxUlSpeed = maxUlSpeed;
		}
		resetInput();
	}

	public void setMinLatency(long minLatency) {
		if(minLatency >= 0) {
			this.minLatency = minLatency;
		}
		resetInput();
	}

	public void setUlSpeed(long ulSpeed) {
		if(ulSpeed >= 0) {
			this.ulSpeed = ulSpeed;
		}
		resetInput();
	}

	public void setDlSpeed(long dlSpeed) {
		if(dlSpeed >= 0) {
			this.dlSpeed = dlSpeed;
		}
		resetInput();
	}

	public void setLatency(long latency) {
		if(latency >= 0) {
			this.latency = latency;
		}
		resetInput();
	}

	public void setMaxLatency(long maxLatency) {
		if(maxLatency > 0) {
			this.maxLatency = maxLatency;
		}
		resetInput();
	}
	
	public boolean shouldLimitDownload() {
		if(dirty) feedForward();
		return neuralNetwork.getOutput(0) < 0.5;
	}
	
	public long getDownloadLimit() {
		if(dirty) feedForward();
		return (long) ( 1.2* maxDlSpeed * neuralNetwork.getOutput(1));
		
	}
	
	public boolean shouldLimitUpload() {
		if(dirty) feedForward();
		return neuralNetwork.getOutput(2) < 0.5;
	}
	
	public long getUploadLimit() {
		if(dirty) feedForward();
		return (long) ( 1.2* maxUlSpeed * neuralNetwork.getOutput(3));
	}
	
	public static void main(String args[]) {
		new NeuralSpeedLimiter();
	}

}
