/* 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 java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

import freenet.crypt.DummyRandomSource;
import freenet.crypt.RandomSource;
import freenet.io.comm.PeerParseException;
import freenet.io.comm.ReferenceSignatureVerificationException;
import freenet.keys.CHKEncodeException;
import freenet.keys.ClientCHKBlock;
import freenet.keys.ClientKSK;
import freenet.keys.ClientKey;
import freenet.keys.ClientKeyBlock;
import freenet.keys.FreenetURI;
import freenet.keys.InsertableClientSSK;
import freenet.keys.KeyDecodeException;
import freenet.keys.SSKEncodeException;
import freenet.node.FSParseException;
import freenet.node.LowLevelGetException;
import freenet.node.Node;
import freenet.node.NodeInitException;
import freenet.node.NodeStarter;
import freenet.support.Executor;
import freenet.support.Logger;
import freenet.support.PooledExecutor;
import freenet.support.Logger.LogLevel;
import freenet.support.LoggerHook.InvalidThresholdException;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.compress.InvalidCompressionCodecException;
import freenet.support.io.ArrayBucket;
import freenet.support.io.FileUtil;
import freenet.support.math.RunningAverage;
import freenet.support.math.SimpleRunningAverage;

/**
 * @author amphibian
 */
public class RealNodeRequestInsertTest extends RealNodeRoutingTest {

    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 boolean ENABLE_SWAPPING = false;
    static final boolean ENABLE_ULPRS = false;
    static final boolean ENABLE_PER_NODE_FAILURE_TABLES = false;
    static final boolean ENABLE_SWAP_QUEUEING = false;
    static final boolean ENABLE_PACKET_COALESCING = true;
    static final boolean ENABLE_FOAF = true;
    static final boolean FORK_ON_CACHEABLE = false;
    static final boolean DISABLE_PROBABILISTIC_HTLS = true;
    // Set to true to cache everything. This depends on security level.
    static final boolean USE_SLASHDOT_CACHE = false;
    static final boolean REAL_TIME_FLAG = false;
    
    static final int TARGET_SUCCESSES = 20;
    //static final int NUMBER_OF_NODES = 50;
    //static final short MAX_HTL = 10;

    // FIXME: HACK: High bwlimit makes the "other" requests not affect the test requests.
    // Real solution is to get rid of the "other" requests!!
    static final int BWLIMIT = 1000*1024;
    
    //public static final int DARKNET_PORT_BASE = RealNodePingTest.DARKNET_PORT2+1;
    public static final int DARKNET_PORT_BASE = 10000;
    public static final int DARKNET_PORT_END = DARKNET_PORT_BASE + NUMBER_OF_NODES;
    
	public static void main(String[] args) throws FSParseException, PeerParseException, CHKEncodeException, InvalidThresholdException, NodeInitException, ReferenceSignatureVerificationException, InterruptedException {
        String name = "realNodeRequestInsertTest";
        File wd = new File(name);
        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(name, false, LogLevel.ERROR, "freenet.node.Location:normal,freenet.node.simulator.RealNode:minor,freenet.node.Insert:MINOR,freenet.node.Request:MINOR,freenet.node.Node:MINOR");
        //NodeStarter.globalTestInit(name, false, LogLevel.ERROR, "freenet.node.Location:MINOR,freenet.io.comm:MINOR,freenet.node.NodeDispatcher:MINOR,freenet.node.simulator:MINOR,freenet.node.PeerManager:MINOR,freenet.node.RequestSender:MINOR");
        //NodeStarter.globalTestInit(name, false, LogLevel.ERROR, "freenet.node.FNP:MINOR,freenet.node.Packet:MINOR,freenet.io.comm:MINOR,freenet.node.PeerNode:MINOR,freenet.node.DarknetPeerNode:MINOR");
        NodeStarter.globalTestInit(name, false, LogLevel.ERROR, "", true);
        System.out.println("Insert/retrieve test");
        System.out.println();
        DummyRandomSource random = new DummyRandomSource(3142);
        DummyRandomSource topologyRandom = new DummyRandomSource(3143);
        //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++) {
            nodes[i] = 
            	NodeStarter.createTestNode(DARKNET_PORT_BASE+i, 0, name, DISABLE_PROBABILISTIC_HTLS, MAX_HTL, 20 /* 5% */, random, executor, 500*NUMBER_OF_NODES, 256*1024, true, ENABLE_SWAPPING, false, ENABLE_ULPRS, ENABLE_PER_NODE_FAILURE_TABLES, ENABLE_SWAP_QUEUEING, ENABLE_PACKET_COALESCING, BWLIMIT, ENABLE_FOAF, false, true, USE_SLASHDOT_CACHE, null);
            Logger.normal(RealNodeRoutingTest.class, "Created node "+i);
        }
        
        // Now link them up
        makeKleinbergNetwork(nodes, START_WITH_IDEAL_LOCATIONS, DEGREE, FORCE_NEIGHBOUR_CONNECTIONS, topologyRandom);

        Logger.normal(RealNodeRoutingTest.class, "Added random links");
        
        for(int i=0;i<NUMBER_OF_NODES;i++) {
            nodes[i].start(false);
            System.err.println("Started node "+i+"/"+nodes.length);
        }
        
        waitForAllConnected(nodes);
        
        waitForPingAverage(0.5, nodes, new DummyRandomSource(3143), MAX_PINGS, 1000);
        
        random = new DummyRandomSource(3144);
        
        System.out.println();
        System.out.println("Ping average > 95%, lets do some inserts/requests");
        System.out.println();
        
        RealNodeRequestInsertTest tester = new RealNodeRequestInsertTest(nodes, random, TARGET_SUCCESSES);
        
        while(true) {
            try {
    			waitForAllConnected(nodes);
    			int status = tester.insertRequestTest();
    			if(status == -1) continue;
    			System.exit(status);
            } catch (Throwable t) {
                Logger.error(RealNodeRequestInsertTest.class, "Caught "+t, t);
            }
        }
    }

    public RealNodeRequestInsertTest(Node[] nodes, DummyRandomSource random, int targetSuccesses) {
    	this.nodes = nodes;
    	this.random = random;
    	this.targetSuccesses = targetSuccesses;
	}

    private final Node[] nodes;
    private final RandomSource random;
    private int requestNumber = 0;
    private RunningAverage requestsAvg = new SimpleRunningAverage(100, 0.0);
    private String baseString = System.currentTimeMillis() + " ";
	private int insertAttempts = 0;
	private int fetchSuccesses = 0;
	private final int targetSuccesses;

	/**
	 * @param nodes
	 * @param random
	 * @return -1 to continue or an exit code (0 or positive for an error).
	 * @throws CHKEncodeException
	 * @throws InvalidCompressionCodecException
	 * @throws SSKEncodeException
	 * @throws IOException
	 * @throws KeyDecodeException
	 */
	int insertRequestTest() throws CHKEncodeException, InvalidCompressionCodecException, SSKEncodeException, IOException, KeyDecodeException {
		
        requestNumber++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e1) {
        }
        String dataString = baseString + requestNumber;
        // Pick random node to insert to
        int node1 = random.nextInt(NUMBER_OF_NODES);
        Node randomNode = nodes[node1];
        //Logger.error(RealNodeRequestInsertTest.class,"Inserting: \""+dataString+"\" to "+node1);
        
        //boolean isSSK = requestNumber % 2 == 1;
        boolean isSSK = true;
        
        FreenetURI testKey;
        ClientKey insertKey;
        ClientKey fetchKey;
        ClientKeyBlock block;
        
        byte[] buf = dataString.getBytes(StandardCharsets.UTF_8);
        if(isSSK) {
        	testKey = new FreenetURI("KSK", dataString);
        	
        	insertKey = InsertableClientSSK.create(testKey);
        	fetchKey = ClientKSK.create(testKey);

        	block = ((InsertableClientSSK)insertKey).encode(new ArrayBucket(buf), false, false, (short)-1, buf.length, random, COMPRESSOR_TYPE.DEFAULT_COMPRESSORDESCRIPTOR);
        } else {
        	block = ClientCHKBlock.encode(buf, false, false, (short)-1, buf.length, COMPRESSOR_TYPE.DEFAULT_COMPRESSORDESCRIPTOR);
        	insertKey = fetchKey = block.getClientKey();
        	testKey = insertKey.getURI();
        }
        
        System.err.println();
        System.err.println("Created random test key "+testKey+" = "+fetchKey.getNodeKey(false));
        System.err.println();
        
        byte[] data = dataString.getBytes(StandardCharsets.UTF_8);
        Logger.minor(RealNodeRequestInsertTest.class, "Decoded: "+new String(block.memoryDecode(), StandardCharsets.UTF_8));
        Logger.normal(RealNodeRequestInsertTest.class,"Insert Key: "+insertKey.getURI());
        Logger.normal(RealNodeRequestInsertTest.class,"Fetch Key: "+fetchKey.getURI());
		try {
			insertAttempts++;
			randomNode.getClientCore().realPut(block.getBlock(), false, FORK_ON_CACHEABLE, false, false, REAL_TIME_FLAG);
			Logger.error(RealNodeRequestInsertTest.class, "Inserted to "+node1);
		} catch (freenet.node.LowLevelPutException putEx) {
			Logger.error(RealNodeRequestInsertTest.class, "Insert failed: "+ putEx);
			System.err.println("Insert failed: "+ putEx);
			return EXIT_INSERT_FAILED;
		}
        // Pick random node to request from
        int node2;
        do {
            node2 = random.nextInt(NUMBER_OF_NODES);
        } while(node2 == node1);
        Node fetchNode = nodes[node2];
        try {
        	block = fetchNode.getClientCore().realGetKey(fetchKey, false, false, false, REAL_TIME_FLAG);
        } catch (LowLevelGetException e) {
        	block = null;
        }
        if(block == null) {
			int percentSuccess=100*fetchSuccesses/insertAttempts;
            Logger.error(RealNodeRequestInsertTest.class, "Fetch #"+requestNumber+" FAILED ("+percentSuccess+"%); from "+node2);
            System.err.println("Fetch #"+requestNumber+" FAILED ("+percentSuccess+"%); from "+node2);
            requestsAvg.report(0.0);
        } else {
            byte[] results = block.memoryDecode();
            requestsAvg.report(1.0);
            if(Arrays.equals(results, data)) {
				fetchSuccesses++;
				int percentSuccess=100*fetchSuccesses/insertAttempts;
                Logger.error(RealNodeRequestInsertTest.class, "Fetch #"+requestNumber+" from node "+node2+" succeeded ("+percentSuccess+"%): "+new String(results));
                System.err.println("Fetch #"+requestNumber+" succeeded ("+percentSuccess+"%): \""+new String(results)+'\"');
                if(fetchSuccesses == targetSuccesses) {
                	System.err.println("Succeeded, "+targetSuccesses+" successful fetches");
                	return 0;
                }
            } else {
                Logger.error(RealNodeRequestInsertTest.class, "Returned invalid data!: "+new String(results));
                System.err.println("Returned invalid data!: "+new String(results));
                return EXIT_BAD_DATA;
            }
        }
        StringBuilder load = new StringBuilder("Running UIDs for nodes: ");
        int totalRunningUIDsAlt = 0;
        List<Long> runningUIDsList = new ArrayList<Long>();
        for(int i=0;i<nodes.length;i++) {
        	load.append(i);
        	load.append(':');
        	nodes[i].getTracker().addRunningUIDs(runningUIDsList);
        	int runningUIDsAlt = nodes[i].getTracker().getTotalRunningUIDsAlt();
        	totalRunningUIDsAlt += runningUIDsAlt;
        	load.append(totalRunningUIDsAlt);
        	if(i != nodes.length-1)
        		load.append(' ');
        }
        System.err.println(load.toString());
        if(totalRunningUIDsAlt != 0)
        	System.err.println("Still running UIDs (alt): "+totalRunningUIDsAlt);
        if(!runningUIDsList.isEmpty()) {
        	System.err.println("List of running UIDs: "+Arrays.toString(runningUIDsList.toArray()));
        }
        return -1;
	}
}
