/* 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.simulator;

import java.io.File;

import freenet.crypt.DummyRandomSource;
import freenet.crypt.RandomSource;
import freenet.node.LocationManager;
import freenet.node.Node;
import freenet.node.NodeStarter;
import freenet.support.Executor;
import freenet.support.Logger;
import freenet.support.PooledExecutor;
import freenet.support.Logger.LogLevel;
import freenet.support.io.FileUtil;
import freenet.support.math.BootstrappingDecayingRunningAverage;
import freenet.support.math.RunningAverage;
import freenet.support.math.SimpleRunningAverage;

/**
 * @author amphibian
 * 
 * Create a mesh of nodes and let them sort out their locations.
 * 
 * Then run some node-to-node searches.
 */
public class RealNodeRoutingTest extends RealNodeTest {

	static final int NUMBER_OF_NODES = 100;
	static final int DEGREE = 10;
	static final short MAX_HTL = (short) 5;
	static final boolean START_WITH_IDEAL_LOCATIONS = true;
	static final boolean FORCE_NEIGHBOUR_CONNECTIONS = true;
	static final int MAX_PINGS = 2000;
	static final boolean ENABLE_SWAPPING = false;
	static final boolean ENABLE_SWAP_QUEUEING = false;
	static final boolean ENABLE_FOAF = true;
	
	public static int DARKNET_PORT_BASE = RealNodeRequestInsertTest.DARKNET_PORT_END;
	public static final int DARKNET_PORT_END = DARKNET_PORT_BASE + NUMBER_OF_NODES;

	public static void main(String[] args) throws Exception {
		System.out.println("Routing test using real nodes:");
		System.out.println();
		String dir = "realNodeRequestInsertTest";
		File wd = new File(dir);
		if(!FileUtil.removeAll(wd)) {
			System.err.println("Mass delete failed, test may not be accurate.");
			System.exit(EXIT_CANNOT_DELETE_OLD_DATA);
		}
		wd.mkdir();
		//NOTE: globalTestInit returns in ignored random source
		NodeStarter.globalTestInit(dir, false, LogLevel.ERROR, "", true);
		// Make the network reproducible so we can easily compare different routing options by specifying a seed.
		DummyRandomSource random = new DummyRandomSource(3142);
		//DiffieHellman.init(random);
		Node[] nodes = new Node[NUMBER_OF_NODES];
		Logger.normal(RealNodeRoutingTest.class, "Creating nodes...");
		Executor executor = new PooledExecutor();
		for(int i = 0; i < NUMBER_OF_NODES; i++) {
			System.err.println("Creating node " + i);
			nodes[i] = NodeStarter.createTestNode(DARKNET_PORT_BASE + i, 0, dir, true, MAX_HTL, 0 /* no dropped packets */, random, executor, 500 * NUMBER_OF_NODES, 65536, true, ENABLE_SWAPPING, false, false, false, ENABLE_SWAP_QUEUEING, true, 0, ENABLE_FOAF, false, true, false, null);
			Logger.normal(RealNodeRoutingTest.class, "Created node " + i);
		}
		Logger.normal(RealNodeRoutingTest.class, "Created " + NUMBER_OF_NODES + " nodes");
		// Now link them up
		makeKleinbergNetwork(nodes, START_WITH_IDEAL_LOCATIONS, DEGREE, FORCE_NEIGHBOUR_CONNECTIONS, random);

		Logger.normal(RealNodeRoutingTest.class, "Added random links");

		for(int i = 0; i < NUMBER_OF_NODES; i++) {
			System.err.println("Starting node " + i);
			nodes[i].start(false);
		}

		waitForAllConnected(nodes);

		// Make the choice of nodes to ping to and from deterministic too.
		// There is timing noise because of all the nodes, but the network
		// and the choice of nodes to start and finish are deterministic, so
		// the overall result should be more or less deterministic.
		waitForPingAverage(0.98, nodes, new DummyRandomSource(3143), MAX_PINGS, 5000);
		System.exit(0);
	}

	static void waitForPingAverage(double accuracy, Node[] nodes, RandomSource random, int maxTests, int sleepTime) throws InterruptedException {
		int totalHopsTaken = 0;
		int cycleNumber = 0;
		int lastSwaps = 0;
		int lastNoSwaps = 0;
		int failures = 0;
		int successes = 0;
		RunningAverage avg = new SimpleRunningAverage(100, 0.0);
		RunningAverage avg2 = new BootstrappingDecayingRunningAverage(0.0, 0.0, 1.0, 100, null);
		int pings = 0;
		for(int total = 0; total < maxTests; total++) {
			cycleNumber++;
			try {
				Thread.sleep(sleepTime);
			} catch(InterruptedException e) {
				// Ignore
			}
			for(int i = 0; i < nodes.length; i++) {
				System.err.println("Cycle " + cycleNumber + " node " + i + ": " + nodes[i].getLocation());
			}
			int newSwaps = LocationManager.swaps;
			int totalStarted = LocationManager.startedSwaps;
			int noSwaps = LocationManager.noSwaps;
			System.err.println("Swaps: " + (newSwaps - lastSwaps));
			System.err.println("\nTotal swaps: Started*2: " + totalStarted * 2 + ", succeeded: " + newSwaps + ", last minute failures: " + noSwaps +
				", ratio " + (double) noSwaps / (double) newSwaps + ", early failures: " + ((totalStarted * 2) - (noSwaps + newSwaps)));
			System.err.println("This cycle ratio: " + ((double) (noSwaps - lastNoSwaps)) / ((double) (newSwaps - lastSwaps)));
			lastNoSwaps = noSwaps;
			System.err.println("Swaps rejected (already locked): " + LocationManager.swapsRejectedAlreadyLocked);
			System.err.println("Swaps rejected (nowhere to go): " + LocationManager.swapsRejectedNowhereToGo);
			System.err.println("Swaps rejected (rate limit): " + LocationManager.swapsRejectedRateLimit);
			System.err.println("Swaps rejected (recognized ID):" + LocationManager.swapsRejectedRecognizedID);
			System.err.println("Swaps failed:" + LocationManager.noSwaps);
			System.err.println("Swaps succeeded:" + LocationManager.swaps);

			double totalSwapInterval = 0.0;
			double totalSwapTime = 0.0;
			for(int i = 0; i < nodes.length; i++) {
				totalSwapInterval += nodes[i].lm.getSendSwapInterval();
				totalSwapTime += nodes[i].lm.getAverageSwapTime();
			}
			System.err.println("Average swap time: " + (totalSwapTime / nodes.length));
			System.err.println("Average swap sender interval: " + (totalSwapInterval / nodes.length));

			waitForAllConnected(nodes);

			lastSwaps = newSwaps;
			// Do some (routed) test-pings
			for(int i = 0; i < 10; i++) {
				try {
					Thread.sleep(sleepTime);
				} catch(InterruptedException e1) {
				}
				try {
					Node randomNode = nodes[random.nextInt(nodes.length)];
					Node randomNode2 = randomNode;
					while(randomNode2 == randomNode) {
						randomNode2 = nodes[random.nextInt(nodes.length)];
					}
					double loc2 = randomNode2.getLocation();
					Logger.normal(RealNodeRoutingTest.class, "Pinging " + randomNode2.getDarknetPortNumber() + " @ " + loc2 + " from " + randomNode.getDarknetPortNumber() + " @ " + randomNode.getLocation());
					
					int hopsTaken = randomNode.routedPing(loc2, randomNode2.getDarknetPubKeyHash());
					pings++;
					if(hopsTaken < 0) {
						failures++;
						avg.report(0.0);
						avg2.report(0.0);
						double ratio = (double) successes / ((double) (failures + successes));
						System.err.println("Routed ping " + pings + " FAILED from " + randomNode.getDarknetPortNumber() + " to " + randomNode2.getDarknetPortNumber() + " (long:" + ratio + ", short:" + avg.currentValue() + ", vague:" + avg2.currentValue() + ')');
					} else {
						totalHopsTaken += hopsTaken;
						successes++;
						avg.report(1.0);
						avg2.report(1.0);
						double ratio = (double) successes / ((double) (failures + successes));
						System.err.println("Routed ping " + pings + " success: " + hopsTaken + ' ' + randomNode.getDarknetPortNumber() + " to " + randomNode2.getDarknetPortNumber() + " (long:" + ratio + ", short:" + avg.currentValue() + ", vague:" + avg2.currentValue() + ')');
					}
				} catch(Throwable t) {
					Logger.error(RealNodeRoutingTest.class, "Caught " + t, t);
				}
			}
			System.err.println("Average path length for successful requests: "+totalHopsTaken/successes);
			if(pings > 10 && avg.currentValue() > accuracy && ((double) successes / ((double) (failures + successes)) > accuracy)) {
				System.err.println();
				System.err.println("Reached " + (accuracy * 100) + "% accuracy.");
				System.err.println();
				System.err.println("Network size: " + nodes.length);
				System.err.println("Maximum HTL: " + MAX_HTL);
				System.err.println("Average path length for successful requests: "+totalHopsTaken/successes);
				System.err.println("Total started swaps: " + LocationManager.startedSwaps);
				System.err.println("Total rejected swaps (already locked): " + LocationManager.swapsRejectedAlreadyLocked);
				System.err.println("Total swaps rejected (nowhere to go): " + LocationManager.swapsRejectedNowhereToGo);
				System.err.println("Total swaps rejected (rate limit): " + LocationManager.swapsRejectedRateLimit);
				System.err.println("Total swaps rejected (recognized ID):" + LocationManager.swapsRejectedRecognizedID);
				System.err.println("Total swaps failed:" + LocationManager.noSwaps);
				System.err.println("Total swaps succeeded:" + LocationManager.swaps);
				return;
			}
		}
		System.exit(EXIT_PING_TARGET_NOT_REACHED);
	}
}
