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.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.HighLevelSimpleClient;
import freenet.client.InsertBlock;
import freenet.client.InsertException;
import freenet.crypt.RandomSource;
import freenet.keys.FreenetURI;
import freenet.node.Node;
import freenet.node.NodeStarter;
import freenet.node.Version;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.PooledExecutor;
import freenet.support.api.Bucket;
import freenet.support.api.RandomAccessBucket;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;

/** Simulates MHKs. Creates 4 CHKs, inserts the first one 3 times, and inserts the
 * others 1 time each. Pulls them all after 1, 3, 7, 15, 31 etc days and computes 
 * success rates for the 1 versus the 3 combined. I am convinced the 3 combined will
 * be much more successful, but evanbd isn't.
 * @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
 *
 */
public class LongTermMHKTest extends LongTermTest {
	
	private static final int TEST_SIZE = 64 * 1024;

	private static final int EXIT_DIFFERENT_URI = 262;

	private static final int DARKNET_PORT1 = 5010;
	private static final int OPENNET_PORT1 = 5011;
	
	/** Delta - the number of days we wait before fetching. */
	private static final int DELTA = 7;

	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];
		
		boolean dumpOnly = args.length == 2 && "--dump".equalsIgnoreCase(args[1]);
		
		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("mhk-test-"+uid + ".csv");
		long t1, t2;

		HighLevelSimpleClient client = null;
		
		try {
			
			// INSERT STUFF
			
			final File dir = new File("longterm-mhk-test-" + uid);
			if(!dumpOnly) {
			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));

			// Create four CHKs
			
			RandomAccessBucket single = randomData(node);
			RandomAccessBucket[] mhks = new RandomAccessBucket[3];
			
			for(int i=0;i<mhks.length;i++) mhks[i] = randomData(node);
			
			client = node.clientCore.makeClient((short) 0, false, false);

			System.err.println("Inserting single block 3 times");
			
			InsertBlock block = new InsertBlock(single, new ClientMetadata(), FreenetURI.EMPTY_CHK_URI);
			
			FreenetURI uri = null;
			
			int successes = 0;
			
			for(int i=0;i<3;i++) {
				System.err.println("Inserting single block, try #"+i);
				try {
					t1 = System.currentTimeMillis();
					FreenetURI thisURI = client.insert(block, false, null);
					if(uri != null && !thisURI.equals(uri)) {
						System.err.println("URI "+i+" is "+thisURI+" but previous is "+uri);
						System.exit(EXIT_DIFFERENT_URI);
					}
					uri = thisURI;
					t2 = System.currentTimeMillis();
					
					System.out.println("PUSH-TIME-" + i + ":" + (t2 - t1)+" for "+uri+" for single block");
					csvLine.add(String.valueOf(t2 - t1));
					csvLine.add(uri.toASCIIString());
					successes++;
				} catch (InsertException e) {
					e.printStackTrace();
					csvLine.add(InsertException.getShortMessage(e.getMode()));
					csvLine.add("N/A");
					System.out.println("INSERT FAILED: "+e+" for insert "+i+" for single block");
				}
			}
			
			if(successes == 3)
				System.err.println("All inserts succeeded for single block: "+successes);
			else if(successes != 0)
				System.err.println("Some inserts succeeded for single block: "+successes);
			else
				System.err.println("NO INSERTS SUCCEEDED FOR SINGLE BLOCK: "+successes);
			
			uri = null;
			
			// Insert 3 blocks
			
			for(int i=0;i<3;i++) {
				System.err.println("Inserting MHK #"+i);
				uri = null;
				block = new InsertBlock(mhks[i], new ClientMetadata(), FreenetURI.EMPTY_CHK_URI);
				try {
					t1 = System.currentTimeMillis();
					FreenetURI thisURI = client.insert(block, false, null);
					uri = thisURI;
					t2 = System.currentTimeMillis();
					
					System.out.println("PUSH-TIME-" + i + ":" + (t2 - t1)+" for "+uri+" for MHK #"+i);
					csvLine.add(String.valueOf(t2 - t1));
					csvLine.add(uri.toASCIIString());
					successes++;
				} catch (InsertException e) {
					e.printStackTrace();
					csvLine.add(InsertException.getShortMessage(e.getMode()));
					csvLine.add("N/A");
					System.out.println("INSERT FAILED: "+e+" for MHK #"+i);
				}
			}
			
			if(successes == 3)
				System.err.println("All inserts succeeded for MHK: "+successes);
			else if(successes != 0)
				System.err.println("Some inserts succeeded for MHK: "+successes);
			else
				System.err.println("NO INSERTS SUCCEEDED FOR MHK: "+successes);
			
			uri = null;
			}
			
			// PARSE FILE AND FETCH OLD STUFF IF APPROPRIATE
			
			boolean match = false;
			
			FreenetURI singleURI = null;
			FreenetURI[] mhkURIs = new FreenetURI[3];
			fis = new FileInputStream(file);
			BufferedReader br = new BufferedReader(new InputStreamReader(fis, ENCODING));
			String line = null;
			int linesTooShort = 0, linesBroken = 0, linesNoNumber = 0, linesNoURL = 0, linesNoFetch = 0;
			int total = 0, singleKeysSucceeded = 0, mhkSucceeded = 0;
			int totalSingleKeyFetches = 0, totalSingleKeySuccesses = 0;
			while((line = br.readLine()) != null) {
				
				singleURI = 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()));
				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);
				target.add(Calendar.DAY_OF_MONTH, -DELTA);
				calendar.set(Calendar.HOUR_OF_DAY, 0);
				calendar.set(Calendar.MINUTE, 0);
				calendar.set(Calendar.MILLISECOND, 0);
				calendar.set(Calendar.SECOND, 0);
				calendar.getTime();
				target.getTime();
				try {
					if(split.length < 3) {
						linesTooShort++;
						continue;
					}
					int seedTime = Integer.parseInt(split[2]);
					System.out.println("Seed time: "+seedTime);
					
					int token = 3;
					if(split.length < 4) {
						linesTooShort++;
						continue;
					}
					
					for(int i=0;i<3;i++) {
						int insertTime = Integer.parseInt(split[token]);
						System.out.println("Single key insert "+i+" : "+insertTime);
						token++;
						FreenetURI thisURI = new FreenetURI(split[token]);
						if(singleURI == null)
							singleURI = thisURI;
						else {
							if(!singleURI.equals(thisURI)) {
								System.err.println("URI is not the same for all 3 inserts: was "+singleURI+" but "+i+" is "+thisURI);
								linesBroken++;
								continue;
							}
						}
						token++;
					}
					System.out.println("Single key URI: "+singleURI);
					
					for(int i=0;i<3;i++) {
						int insertTime = Integer.parseInt(split[token]);
						token++;
						mhkURIs[i] = new FreenetURI(split[token]);
						token++;
						System.out.println("MHK #"+i+" URI: "+mhkURIs[i]+" insert time "+insertTime);
					}
					
				} catch (NumberFormatException e) {
					System.err.println("Failed to parse row: "+e);
					linesNoNumber++;
					continue;
				} catch (MalformedURLException e) {
					System.err.println("Failed to parse row: "+e);
					linesNoURL++;
					continue;
				}
				if(Math.abs(target.getTimeInMillis() - calendar.getTimeInMillis()) < HOURS.toMillis(12)) {
					System.out.println("Found row for target date "+dateFormat.format(target.getTime())+" : "+dateFormat.format(calendar.getTime()));
					System.out.println("Version: "+split[1]);
					match = true;
					break;
				} else if(split.length > 3+6+6) {
					int token = 3 + 6 + 6;
					int singleKeyFetchTime = -1;
					boolean singleKeySuccess = false;
					for(int i=0;i<3;i++) {
						// Fetched 3 times
						if(!singleKeySuccess) {
							try {
								singleKeyFetchTime = Integer.parseInt(split[token]);
								singleKeySuccess = true;
								System.out.println("Fetched single key on try "+i+" on "+date+" in "+singleKeyFetchTime+"ms");
							} catch (NumberFormatException e) {
								System.out.println("Failed fetch single key on "+date+" try "+i+" : "+split[token]);
								singleKeyFetchTime = -1;
							}
						} // Else will be empty.
						token++;
					}
					boolean mhkSuccess = false;
					for(int i=0;i<3;i++) {
						totalSingleKeyFetches++;
						int mhkFetchTime = -1;
						try {
							mhkFetchTime = Integer.parseInt(split[token]);
							mhkSuccess = true;
							totalSingleKeySuccesses++;
							System.out.println("Fetched MHK #"+i+" on "+date+" in "+mhkFetchTime+"ms");
						} catch (NumberFormatException e) {
							System.out.println("Failed fetch MHK #"+i+" on "+date+" : "+split[token]);
						}
						token++;
					}
					total++;
					if(singleKeySuccess)
						singleKeysSucceeded++;
					if(mhkSuccess)
						mhkSucceeded++;
				} else linesNoFetch++;
			}
			System.out.println("Lines where insert failed or no fetch: too short: "+linesTooShort+" broken: "+linesBroken+" no number: "+linesNoNumber+" no url: "+linesNoURL+" no fetch "+linesNoFetch);
			System.out.println("Total attempts where insert succeeded and fetch executed: "+total);
			System.out.println("Single keys succeeded: "+singleKeysSucceeded);
			System.out.println("MHKs succeeded: "+mhkSucceeded);
			System.out.println("Single key individual fetches: "+totalSingleKeyFetches);
			System.out.println("Single key individual fetches succeeded: "+totalSingleKeySuccesses);
			System.out.println("Success rate for individual keys (from MHK inserts): "+((double)totalSingleKeySuccesses)/((double)totalSingleKeyFetches));
			System.out.println("Success rate for the single key triple inserted: "+((double)singleKeysSucceeded)/((double)total));
			System.out.println("Success rate for the MHK (success = any of the 3 different keys worked): "+((double)mhkSucceeded)/((double)total));
			fis.close();
			fis = null;
			
			// FETCH STUFF
			
			
			if((!dumpOnly) && match) {
				
				// FETCH SINGLE URI
				
				// Fetch the first one 3 times, since the MHK is 3 fetches also.
				// Technically this is 9 fetches because we multiply by 3 fetches per high-level fetch by default.
				
				boolean fetched = false;
				for(int i=0;i<3;i++) {
					if(fetched) {
						csvLine.add("");
						continue;
					}
					try {
						t1 = System.currentTimeMillis();
						client.fetch(singleURI);
						t2 = System.currentTimeMillis();
						
						System.out.println("PULL-TIME FOR SINGLE URI:" + (t2 - t1));
						csvLine.add(String.valueOf(t2 - t1));
						fetched = true;
					} 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 SINGLE URI: "+e);
					}
				}
				
				for(int i=0;i<mhkURIs.length;i++) {
					try {
						t1 = System.currentTimeMillis();
						client.fetch(mhkURIs[i]);
						t2 = System.currentTimeMillis();
						
						System.out.println("PULL-TIME FOR MHK #"+i+":" + (t2 - t1));
						csvLine.add(String.valueOf(t2 - t1));
					} 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 MHK #"+i+": "+e);
					}
				}
			}
			
		} 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);

			if(!dumpOnly) {
				writeToStatusLog(file, csvLine);
			}
			System.exit(exitCode);
		}
	}	
	
	private static RandomAccessBucket randomData(Node node) throws IOException {
	    RandomAccessBucket data = node.clientCore.tempBucketFactory.makeBucket(TEST_SIZE);
		OutputStream os = data.getOutputStream();
		try {
		byte[] buf = new byte[4096];
		for (long written = 0; written < TEST_SIZE;) {
			node.fastWeakRandom.nextBytes(buf);
			int toWrite = (int) Math.min(TEST_SIZE - written, buf.length);
			os.write(buf, 0, toWrite);
			written += toWrite;
		}
		} finally {
		os.close();
		}
		return data;
	}

}
