package freenet.node.simulator;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;

import freenet.client.ClientMetadata;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchWaiter;
import freenet.client.HighLevelSimpleClient;
import freenet.client.InsertBlock;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.crypt.RandomSource;
import freenet.keys.FreenetURI;
import freenet.node.Node;
import freenet.node.NodeStarter;
import freenet.node.RequestClient;
import freenet.node.RequestClientBuilder;
import freenet.node.Version;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.PooledExecutor;
import freenet.support.api.RandomAccessBucket;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;

/** 
 * Insert 32x single blocks. Pull them individually, with 0 retries, after 2^n-1 
 * days, for n in 0...8.
 * @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
 */
public class LongTermManySingleBlocksTest extends LongTermTest {
	
	public static class InsertBatch {

		private final HighLevelSimpleClient client;
		private int runningInserts;
		private ArrayList<BatchInsert> inserts = new ArrayList<BatchInsert>();
		
		public InsertBatch(HighLevelSimpleClient client) {
			this.client = client;
			// TODO Auto-generated constructor stub
		}

		public void startInsert(InsertBlock block) {
			BatchInsert bi = new BatchInsert(block);
			synchronized(this) {
				inserts.add(bi);
			}
			bi.start();
		}
		
		class BatchInsert implements Runnable {

			private final InsertBlock block;
			//private boolean finished = false;
			//private boolean started = false;
			private long insertTime;
			private InsertException failed;
			private FreenetURI uri;
			
			public BatchInsert(InsertBlock block) {
				this.block = block;
			}

			public void start() {
				Thread t = new Thread(this);
				t.setDaemon(true);
				t.start();
			}

			@Override
			public void run() {
				synchronized(InsertBatch.this) {
					runningInserts++;
					//started = true;
					System.out.println("Starting insert: running "+runningInserts);
				}
				long t1 = 0, t2 = 0;
				FreenetURI thisURI = null;
				InsertException f = null;
				try {
					t1 = System.currentTimeMillis();
					thisURI = client.insert(block, false, null);
					t2 = System.currentTimeMillis();
				} catch (InsertException e) {
					f = e;
				} finally {
					synchronized(InsertBatch.this) {
						runningInserts--;
						System.out.println("Completed insert: running "+runningInserts);
						//finished = true;
						if(thisURI != null) {
							uri = thisURI;
							insertTime = t2 - t1;
						} else {
							if(f != null)
								failed = f;
							else
								f = new InsertException(InsertExceptionMode.INTERNAL_ERROR);
						}
							
						InsertBatch.this.notifyAll();
					}
				}
			}
			
		}

		public synchronized void waitUntilFinished() {
			while(true) {
				if(runningInserts == 0) return;
				try {
					wait();
				} catch (InterruptedException e) {
					// Ignore
				}
			}
		}

		public synchronized FreenetURI[] getURIs() {
			FreenetURI[] uris = new FreenetURI[inserts.size()];
			for(int i=0;i<uris.length;i++)
				uris[i] = inserts.get(i).uri;
			return uris;
		}

		public synchronized long[] getTimes() {
			long[] times = new long[inserts.size()];
			for(int i=0;i<times.length;i++)
				times[i] = inserts.get(i).insertTime;
			return times;
		}

		public InsertException[] getErrors() {
			InsertException[] errors = new InsertException[inserts.size()];
			for(int i=0;i<errors.length;i++)
				errors[i] = inserts.get(i).failed;
			return errors;
		}

	}

	private static final int TEST_SIZE = 32 * 1024;

	private static final int DARKNET_PORT1 = 9010;
	private static final int OPENNET_PORT1 = 9011;
	
	private static final int MAX_N = 8;
	
	private static final int INSERTED_BLOCKS = 32;

	public static void main(String[] args) {
		if (args.length < 1 || args.length > 2) {
			System.err.println("Usage: java freenet.node.simulator.LongTermPushPullTest <unique identifier>");
			System.exit(1);
		}
		String uid = args[0];
		
		List<String> csvLine = new ArrayList<String>();
		System.out.println("DATE:" + dateFormat.format(today.getTime()));
		csvLine.add(dateFormat.format(today.getTime()));

		System.out.println("Version:" + Version.buildNumber());
		csvLine.add(String.valueOf(Version.buildNumber()));

		int exitCode = 0;
		Node node = null;
		Node node2 = null;
		FileInputStream fis = null;
		File file = new File("many-single-blocks-test-"+uid + ".csv");
		long t1, t2;
		
		try {
			
			// INSERT STUFF
			
			final File dir = new File("longterm-mhk-test-" + uid);
			FileUtil.removeAll(dir);
			RandomSource random = NodeStarter.globalTestInit(dir.getPath(), false, LogLevel.ERROR, "", false);
			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);
			}

			final File innerDir = new File(dir, Integer.toString(DARKNET_PORT1));
			innerDir.mkdir();
			fis = new FileInputStream(seednodes);
			FileUtil.writeTo(fis, new File(innerDir, "seednodes.fref"));
			fis.close();

			// Create one node
			node = NodeStarter.createTestNode(DARKNET_PORT1, OPENNET_PORT1, dir.getPath(), false, Node.DEFAULT_MAX_HTL,
			        0, random, new PooledExecutor(), 1000, 4 * 1024 * 1024, true, true, true, true, true, true, true,
			        12 * 1024, true, true, false, false, null);
			Logger.getChain().setThreshold(LogLevel.ERROR);

			// Start it
			node.start(true);
			t1 = System.currentTimeMillis();
			if (!TestUtil.waitForNodes(node)) {
				exitCode = EXIT_FAILED_TARGET;
				return;
			}
				
			t2 = System.currentTimeMillis();
			System.out.println("SEED-TIME:" + (t2 - t1));
			csvLine.add(String.valueOf(t2 - t1));

			HighLevelSimpleClient client = node.getClientCore().makeClient((short) 0, false, false);

			int successes = 0;
			
			long startInsertsTime = System.currentTimeMillis();
			
			InsertBatch batch = new InsertBatch(client);
			
			// Inserts are sloooooow so do them in parallel.
			
			for(int i=0;i<INSERTED_BLOCKS;i++) {
				
				System.err.println("Inserting block "+i);
				
				RandomAccessBucket single = randomData(node);
				
				InsertBlock block = new InsertBlock(single, new ClientMetadata(), FreenetURI.EMPTY_CHK_URI);
				
				batch.startInsert(block);
				
			}
			
			batch.waitUntilFinished();
			FreenetURI[] uris = batch.getURIs();
			long[] times = batch.getTimes();
			InsertException[] errors = batch.getErrors();
			
			for(int i=0;i<INSERTED_BLOCKS;i++) {
				if(uris[i] != null) {
					csvLine.add(String.valueOf(times[i]));
					csvLine.add(uris[i].toASCIIString());
					System.out.println("Pushed block "+i+" : "+uris[i]+" in "+times[i]);
					successes++;
				} else {
					csvLine.add(InsertException.getShortMessage(errors[i].getMode()));
					csvLine.add("N/A");
					System.out.println("Failed to push block "+i+" : "+errors[i]);
				}
			}
			
			long endInsertsTime = System.currentTimeMillis();
			
			System.err.println("Succeeded inserts: "+successes+" of "+INSERTED_BLOCKS+" in "+(endInsertsTime-startInsertsTime)+"ms");
			
			FetchContext fctx = client.getFetchContext();
			fctx.maxNonSplitfileRetries = 0;
			fctx.maxSplitfileBlockRetries = 0;
			RequestClient requestContext = new RequestClientBuilder().build();

			// PARSE FILE AND FETCH OLD STUFF IF APPROPRIATE
			
			FreenetURI[] mhkURIs = new FreenetURI[3];
			fis = new FileInputStream(file);
			BufferedReader br = new BufferedReader(new InputStreamReader(fis, ENCODING));
			String line = null;
			GregorianCalendar target = (GregorianCalendar) today.clone();
			target.set(Calendar.HOUR_OF_DAY, 0);
			target.set(Calendar.MINUTE, 0);
			target.set(Calendar.MILLISECOND, 0);
			target.set(Calendar.SECOND, 0);
			GregorianCalendar[] targets = new GregorianCalendar[MAX_N+1];
			for(int i=0;i<targets.length;i++) {
				targets[i] = ((GregorianCalendar)target.clone());
				targets[i].add(Calendar.DAY_OF_MONTH, -((1<<i)-1));
				targets[i].getTime();
			}
			int[] totalFetchesByDelta = new int[MAX_N+1];
			int[] totalSuccessfulFetchesByDelta = new int[MAX_N+1];
			long[] totalFetchTimeByDelta = new long[MAX_N+1];
			
loopOverLines:
			while((line = br.readLine()) != null) {
				
				for(int i=0;i<mhkURIs.length;i++) mhkURIs[i] = null;
				//System.out.println("LINE: "+line);
				String[] split = line.split("!");
				Date date = dateFormat.parse(split[0]);
				GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
				calendar.setTime(date);
				System.out.println("Date: "+dateFormat.format(calendar.getTime()));
				calendar.set(Calendar.HOUR_OF_DAY, 0);
				calendar.set(Calendar.MINUTE, 0);
				calendar.set(Calendar.MILLISECOND, 0);
				calendar.set(Calendar.SECOND, 0);
				calendar.getTime();
				FreenetURI[] insertedURIs = new FreenetURI[INSERTED_BLOCKS];
				int[] insertTimes = new int[INSERTED_BLOCKS];
				if(split.length < 3) continue;
				int seedTime = Integer.parseInt(split[2]);
				System.out.println("Seed time: "+seedTime);
				if(split.length < 4) continue;
				
				int token = 3;
				
				if(split.length < token + INSERTED_BLOCKS * 2) continue;
			
				for(int i=0;i<INSERTED_BLOCKS;i++) {
					try {
						insertTimes[i] = Integer.parseInt(split[token]);
					} catch (NumberFormatException e) {
						insertTimes[i] = -1;
					}
					token++;
					try {
						insertedURIs[i] = new FreenetURI(split[token]);
					} catch (MalformedURLException e) {
						insertedURIs[i] = null;
					}
					token++;
					System.out.println("Key insert "+i+" : "+insertedURIs[i]+" in "+insertTimes[i]);
				}
				for(int i=0;i<targets.length;i++) {
					if(Math.abs(targets[i].getTimeInMillis() - calendar.getTimeInMillis()) < HOURS.toMillis(12)) {
						System.out.println("Found row for target date "+((1<<i)-1)+" days ago.");
						System.out.println("Version: "+split[1]);
						csvLine.add(Integer.toString(i));
						int pulled = 0;
						int inserted = 0;
						for(int j=0;j<INSERTED_BLOCKS;j++) {
							if(insertedURIs[j] == null) {
								csvLine.add("INSERT FAILED");
								continue;
							}
							inserted++;
							try {
								t1 = System.currentTimeMillis();
								FetchWaiter fw = new FetchWaiter(requestContext);
								client.fetch(insertedURIs[j], 32768, fw, fctx);
								fw.waitForCompletion();
								t2 = System.currentTimeMillis();
								
								System.out.println("PULL-TIME FOR BLOCK "+j+": " + (t2 - t1));
								csvLine.add(String.valueOf(t2 - t1));
								pulled++;
							} catch (FetchException e) {
								if (e.getMode() != FetchExceptionMode.ALL_DATA_NOT_FOUND
										&& e.getMode() != FetchExceptionMode.DATA_NOT_FOUND)
									e.printStackTrace();
								csvLine.add(FetchException.getShortMessage(e.getMode()));
								System.err.println("FAILED PULL FOR BLOCK "+j+": "+e);
							}
						}
						System.out.println("Pulled "+pulled+" blocks of "+inserted+" from "+((1<<i)-1)+" days ago.");
					}
				}
				
				while(split.length > token + INSERTED_BLOCKS) {
					int delta;
					try {
						delta = Integer.parseInt(split[token]);
					} catch (NumberFormatException e) {
						System.err.println("Unable to parse token "+token+" = \""+token+"\"");
						System.err.println("This is supposed to be a delta");
						System.err.println("Skipping the rest of the line for date "+dateFormat.format(calendar.getTime()));
						continue loopOverLines;
					}
					System.out.println("Delta: "+((1<<delta)-1)+" days");
					token++;
					int totalFetchTime = 0;
					int totalSuccesses = 0;
					int totalFetches = 0;
					for(int i=0;i<INSERTED_BLOCKS;i++) {
						if(split[token].isEmpty())
							continue;
						int mhkFetchTime = -1;
						totalFetches++;
						try {
							mhkFetchTime = Integer.parseInt(split[token]);
							System.out.println("Fetched block #"+i+" on "+date+" in "+mhkFetchTime+"ms");
							totalSuccesses++;
							totalFetchTime += mhkFetchTime;
						} catch (NumberFormatException e) {
							System.out.println("Failed block #"+i+" on "+date+" : "+split[token]);
						}
						token++;
					}
					totalFetchesByDelta[delta] += totalFetches;
					totalSuccessfulFetchesByDelta[delta] += totalSuccesses;
					totalFetchTimeByDelta[delta] += totalFetchTime;
					System.err.println("Succeeded: "+totalSuccesses+" of "+totalFetches+" average "+((double)totalFetchTime)/((double)totalSuccesses)+"ms for delta "+delta+" on "+dateFormat.format(date));
				}
			}
			
			System.out.println();
			System.out.println();
			
			for(int i=0;i<MAX_N+1;i++) {
				System.out.println("DELTA: "+i+" days: Total fetches: "+totalFetchesByDelta[i]+" total successes "+totalSuccessfulFetchesByDelta[i]+" = "+((totalSuccessfulFetchesByDelta[i]*100.0)/totalFetchesByDelta[i])+"% in "+(totalFetchTimeByDelta[i]*1.0)/totalSuccessfulFetchesByDelta[i]+"ms");
			}
			
			fis.close();
			fis = null;
			
		} catch (Throwable t) {
			t.printStackTrace();
			exitCode = EXIT_THREW_SOMETHING;
		} finally {
			try {
				if (node != null)
					node.park();
			} catch (Throwable tt) {
			}
			try {
				if (node2 != null)
					node2.park();
			} catch (Throwable tt) {
			}
			Closer.close(fis);
			writeToStatusLog(file, csvLine);

			System.out.println("Exiting with status "+exitCode);
			System.exit(exitCode);
		}
	}	
	
	private static RandomAccessBucket randomData(Node node) throws IOException {
	    RandomAccessBucket data = node.getClientCore().getTempBucketFactory().makeBucket(TEST_SIZE);
		OutputStream os = data.getOutputStream();
		try {
		byte[] buf = new byte[4096];
		for (long written = 0; written < TEST_SIZE;) {
			node.getFastWeakRandom().nextBytes(buf);
			int toWrite = (int) Math.min(TEST_SIZE - written, buf.length);
			os.write(buf, 0, toWrite);
			written += toWrite;
		}
		} finally {
		os.close();
		}
		return data;
	}

}
