package freenet.node.simulator;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

import freenet.support.math.MersenneTwister;

import freenet.client.FetchException;
import freenet.client.HighLevelSimpleClient;
import freenet.crypt.RandomSource;
import freenet.keys.FreenetURI;
import freenet.node.Node;
import freenet.node.NodeInitException;
import freenet.node.NodeStarter;
import freenet.support.PooledExecutor;
import freenet.support.SimpleFieldSet;
import freenet.support.TimeUtil;
import freenet.support.Logger.LogLevel;
import freenet.support.LoggerHook.InvalidThresholdException;
import freenet.support.io.FileUtil;
import freenet.support.io.LineReadingInputStream;

/**
 * Insert a random block of data to an established node via FCP, then
 * bootstrap a newbie node and pull it from that.
 * @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
 */
public class BootstrapPullTest {

	public static int TEST_SIZE = 1024*1024;

	public static int EXIT_NO_SEEDNODES = 257;
	public static int EXIT_FAILED_TARGET = 258;
	public static int EXIT_INSERT_FAILED = 259;
	public static int EXIT_FETCH_FAILED = 260;
	public static int EXIT_INSERTER_PROBLEM = 261;
	public static int EXIT_THREW_SOMETHING = 262;

	public static int DARKNET_PORT = 5000;
	public static int OPENNET_PORT = 5001;

	/**
	 * @param args
	 * @throws InvalidThresholdException
	 * @throws IOException
	 * @throws NodeInitException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InvalidThresholdException, IOException, NodeInitException, InterruptedException {
		Node secondNode = null;
		try {
		String ipOverride = null;
		if(args.length > 0)
			ipOverride = args[0];
        File dir = new File("bootstrap-pull-test");
        FileUtil.removeAll(dir);
        RandomSource random = NodeStarter.globalTestInit(dir.getPath(), false, LogLevel.ERROR, "", false);
        byte[] seed = new byte[64];
        random.nextBytes(seed);
        MersenneTwister fastRandom = new MersenneTwister(seed);
        File seednodes = new File("seednodes.fref");
        if(!seednodes.exists() || seednodes.length() == 0 || !seednodes.canRead()) {
        	System.err.println("Unable to read seednodes.fref, it doesn't exist, or is empty");
        	System.exit(EXIT_NO_SEEDNODES);
        }
        File secondInnerDir = new File(dir, Integer.toString(DARKNET_PORT));
        secondInnerDir.mkdir();
        FileInputStream fis = new FileInputStream(seednodes);
        FileUtil.writeTo(fis, new File(secondInnerDir, "seednodes.fref"));
        fis.close();

        // Create the test data
        System.out.println("Creating test data.");
        File dataFile = File.createTempFile("testdata", ".tmp", dir);
        OutputStream os = new FileOutputStream(dataFile);
        byte[] buf = new byte[4096];
        for(long written = 0; written < TEST_SIZE;) {
        	fastRandom.nextBytes(buf);
        	int toWrite = (int) Math.min(TEST_SIZE - written, buf.length);
        	os.write(buf, 0, toWrite);
        	written += toWrite;
        }
        os.close();

        // Insert it to the established node.
        System.out.println("Inserting test data to an established node.");
        FreenetURI uri = insertData(dataFile);

        // Bootstrap a second node.
        secondInnerDir.mkdir();
        fis = new FileInputStream(seednodes);
        FileUtil.writeTo(fis, new File(secondInnerDir, "seednodes.fref"));
        fis.close();
        PooledExecutor executor = new PooledExecutor();
        secondNode = NodeStarter.createTestNode(DARKNET_PORT, OPENNET_PORT, dir.getPath(), false, Node.DEFAULT_MAX_HTL, 0, random, executor, 1000, 5*1024*1024, true, true, true, true, true, true, true, 12*1024, false, true, false, false, ipOverride);
        secondNode.start(true);

		if (!TestUtil.waitForNodes(secondNode)) {
			secondNode.park();
			System.exit(EXIT_FAILED_TARGET);
		}

        // Fetch the data
        long startFetchTime = System.currentTimeMillis();
        HighLevelSimpleClient client = secondNode.getClientCore().makeClient((short)0, false, false);
        try {
			client.fetch(uri);
		} catch (FetchException e) {
			System.err.println("FETCH FAILED: "+e);
			e.printStackTrace();
			System.exit(EXIT_FETCH_FAILED);
			return;
		}
		long endFetchTime = System.currentTimeMillis();
		System.out.println("RESULT: Fetch took "+(endFetchTime-startFetchTime)+"ms ("+TimeUtil.formatTime(endFetchTime-startFetchTime)+") of "+uri+" .");
		secondNode.park();
		System.exit(0);
	    } catch (Throwable t) {
	    	System.err.println("CAUGHT: "+t);
	    	t.printStackTrace();
	    	try {
	    		if(secondNode != null)
	    			secondNode.park();
	    	} catch (Throwable t1) {}
	    	System.exit(EXIT_THREW_SOMETHING);
	    }
	}

	private static FreenetURI insertData(File dataFile) throws IOException {
        long startInsertTime = System.currentTimeMillis();
        InetAddress localhost = InetAddress.getByName("127.0.0.1");
        Socket sock = new Socket(localhost, 9481);
        OutputStream sockOS = sock.getOutputStream();
        InputStream sockIS = sock.getInputStream();
        System.out.println("Connected to node.");
        LineReadingInputStream lis = new LineReadingInputStream(sockIS);
        OutputStreamWriter osw = new OutputStreamWriter(sockOS, StandardCharsets.UTF_8);
        osw.write("ClientHello\nExpectedVersion=0.7\nName=BootstrapPullTest-"+System.currentTimeMillis()+"\nEnd\n");
        osw.flush();
       	String name = lis.readLine(65536, 128, true);
       	SimpleFieldSet fs = new SimpleFieldSet(lis, 65536, 128, true, false, true);
       	if(!name.equals("NodeHello")) {
       		System.err.println("No NodeHello from insertor node!");
       		System.exit(EXIT_INSERTER_PROBLEM);
       	}
       	System.out.println("Connected to "+sock);
       	osw.write("ClientPut\nIdentifier=test-insert\nURI=CHK@\nVerbosity=1023\nUploadFrom=direct\nMaxRetries=-1\nDataLength="+TEST_SIZE+"\nData\n");
       	osw.flush();
       	InputStream is = new FileInputStream(dataFile);
       	FileUtil.copy(is, sockOS, TEST_SIZE);
       	System.out.println("Sent data");
       	while(true) {
           	name = lis.readLine(65536, 128, true);
           	fs = new SimpleFieldSet(lis, 65536, 128, true, false, true);
       		System.out.println("Got FCP message: \n"+name);
       		System.out.print(fs.toOrderedString());
       		if(name.equals("ProtocolError")) {
       			System.err.println("Protocol error when inserting data.");
       			System.exit(EXIT_INSERTER_PROBLEM);
       		}
       		if(name.equals("PutFailed")) {
       			System.err.println("Insert failed");
       			System.exit(EXIT_INSERT_FAILED);
       		}
       		if(name.equals("PutSuccessful")) {
       	        long endInsertTime = System.currentTimeMillis();
       			FreenetURI uri = new FreenetURI(fs.get("URI"));
       	        System.out.println("RESULT: Insert took "+(endInsertTime-startInsertTime)+"ms ("+TimeUtil.formatTime(endInsertTime-startInsertTime)+") to "+uri+" .");
       			sockOS.close();
       			sockIS.close();
       			sock.close();
       			return uri;
       		}
       	}
	}
}
