package templates;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import jebl.evolution.graphs.Node;
import jebl.evolution.io.ImportException;
import jebl.evolution.io.NexusImporter;
import jebl.evolution.io.TreeImporter;
import jebl.evolution.trees.RootedTree;
import processing.core.PApplet;
import readers.SliceHeightsReader;
import structure.Coordinates;
import structure.TimeLine;
import utils.ThreadLocalSpreadDate;
import utils.Utils;
import contouring.ContourMaker;
import contouring.ContourPath;
import contouring.ContourWithSynder;

@SuppressWarnings("serial")
public class TimeSlicerToProcessing extends PApplet {

	private int analysisType;
	public final static int FIRST_ANALYSIS = 1;
	public final static int SECOND_ANALYSIS = 2;

	// how many millisecond one day holds
	private final int DayInMillis = 86400000;
	// how many days one year holds
	private static final int DaysInYear = 365;

	// Concurrency stuff
	private ConcurrentMap<Double, List<Coordinates>> slicesMap;
	private RootedTree currentTree;

	private RootedTree tree;
	private int numberOfIntervals;
	private double[] sliceHeights;

	private double minPolygonRedMapping;
	private double minPolygonGreenMapping;
	private double minPolygonBlueMapping;
	private double minPolygonOpacityMapping;

	private double maxPolygonRedMapping;
	private double maxPolygonGreenMapping;
	private double maxPolygonBlueMapping;
	private double maxPolygonOpacityMapping;

	private double minBranchRedMapping;
	private double minBranchGreenMapping;
	private double minBranchBlueMapping;
	private double minBranchOpacityMapping;

	private double maxBranchRedMapping;
	private double maxBranchGreenMapping;
	private double maxBranchBlueMapping;
	private double maxBranchOpacityMapping;

	private double branchWidth;
	private TreeImporter treesImporter;
	private TreeImporter treeImporter;
	private double treeRootHeight;
	private String precisionString;
	private String coordinatesName;
	private String longitudeName;
	private String latitudeName;
	private String rateString;
	private boolean useTrueNoise;
	private String mrsdString;
	private ThreadLocalSpreadDate mrsd;
	private double timescaler;
	private TimeLine timeLine;
	private double startTime;
	private double endTime;
	private double burnIn;
	private double hpd;
	private int gridSize;

	private MapBackground mapBackground;
	private float minX, maxX;
	private float minY, maxY;

	public TimeSlicerToProcessing() {
	}// END: constructor

	public void setAnalysisType(int analysisType) {
		this.analysisType = analysisType;
	}

	public void setCustomSliceHeightsPath(String path) {
		sliceHeights = new SliceHeightsReader(path).getSliceHeights();
	}

	public void setTimescaler(double timescaler) {
		this.timescaler = timescaler;
	}

	public void setHPD(double hpd) {
		this.hpd = hpd;
	}

	public void setGridSize(int gridSize) {
		this.gridSize = gridSize;
	}

	public void setTreePath(String path) throws FileNotFoundException {
		treeImporter = new NexusImporter(new FileReader(path));
	}

	public void setTreesPath(String path) throws FileNotFoundException {
		treesImporter = new NexusImporter(new FileReader(path));
	}

	public void setMrsdString(String mrsd) {
		mrsdString = mrsd;
	}

	public void setCoordinatesName(String name) {
		coordinatesName = name;
		// this is for coordinate attribute names
		latitudeName = (coordinatesName + 1);
		longitudeName = (coordinatesName + 2);
	}

	public void setRateAttributeName(String name) {
		rateString = name;
	}

	public void setPrecisionAttributeName(String name) {
		precisionString = name;
	}

	public void setNumberOfIntervals(int number) {
		numberOfIntervals = number;
	}

	public void setBurnIn(double burnIn) {
		this.burnIn = burnIn;
	}

	public void setUseTrueNoise(boolean useTrueNoise) {
		this.useTrueNoise = useTrueNoise;
	}

	public void setMinPolygonRedMapping(double min) {
		minPolygonRedMapping = min;
	}

	public void setMinPolygonGreenMapping(double min) {
		minPolygonGreenMapping = min;
	}

	public void setMinPolygonBlueMapping(double min) {
		minPolygonBlueMapping = min;
	}

	public void setMinPolygonOpacityMapping(double min) {
		minPolygonOpacityMapping = min;
	}

	public void setMaxPolygonRedMapping(double max) {
		maxPolygonRedMapping = max;
	}

	public void setMaxPolygonGreenMapping(double max) {
		maxPolygonGreenMapping = max;
	}

	public void setMaxPolygonBlueMapping(double max) {
		maxPolygonBlueMapping = max;
	}

	public void setMaxPolygonOpacityMapping(double max) {
		maxPolygonOpacityMapping = max;
	}

	public void setMinBranchRedMapping(double min) {
		minBranchRedMapping = min;
	}

	public void setMinBranchGreenMapping(double min) {
		minBranchGreenMapping = min;
	}

	public void setMinBranchBlueMapping(double min) {
		minBranchBlueMapping = min;
	}

	public void setMinBranchOpacityMapping(double min) {
		minBranchOpacityMapping = min;
	}

	public void setMaxBranchRedMapping(double max) {
		maxBranchRedMapping = max;
	}

	public void setMaxBranchGreenMapping(double max) {
		maxBranchGreenMapping = max;
	}

	public void setMaxBranchBlueMapping(double max) {
		maxBranchBlueMapping = max;
	}

	public void setMaxBranchOpacityMapping(double max) {
		maxBranchOpacityMapping = max;
	}

	public void setBranchWidth(double width) {
		branchWidth = width;
	}

	public void setup() {

		minX = -180;
		maxX = 180;

		minY = -90;
		maxY = 90;

		mapBackground = new MapBackground(this);

	}// END:setup

	public void draw() {

		noLoop();
		smooth();
		mapBackground.drawMapBackground();
			System.out.println("Drawing polygons...");
			drawPolygons();

		switch (analysisType) {
		case 1:
			System.out.println("Drawing branches...");
			drawBranches();
			break;
		case 2:
			break;
		}

	}// END:draw

	private void drawPolygons() throws OutOfMemoryError {

		System.out.println("Iterating through Map...");

		Set<Double> hostKeys = slicesMap.keySet();
		Iterator<Double> iterator = hostKeys.iterator();

		int polygonsStyleId = 1;
		while (iterator.hasNext()) {

			System.out.println("Key " + polygonsStyleId + "...");

			Double sliceTime = iterator.next();
			drawPolygon(sliceTime);

			polygonsStyleId++;
			// slicesMap.remove(sliceTime);
		}

	}// END: drawPolygons

	private void drawPolygon(Double sliceTime) throws OutOfMemoryError {

		/**
		 * Color and Opacity mapping
		 * */
		int red = (int) Utils.map(sliceTime, startTime, endTime,
				minPolygonRedMapping, maxPolygonRedMapping);

		int green = (int) Utils.map(sliceTime, startTime, endTime,
				minPolygonGreenMapping, maxPolygonGreenMapping);

		int blue = (int) Utils.map(sliceTime, startTime, endTime,
				minPolygonBlueMapping, maxPolygonBlueMapping);

		int alpha = (int) Utils.map(sliceTime, startTime, endTime,
				maxPolygonOpacityMapping, minPolygonOpacityMapping);

		stroke(red, green, blue, alpha);
		fill(red, green, blue, alpha);

		List<Coordinates> list = slicesMap.get(sliceTime);

		double[] x = new double[list.size()];
		double[] y = new double[list.size()];

		for (int i = 0; i < list.size(); i++) {

			x[i] = list.get(i).getLatitude();
			y[i] = list.get(i).getLongitude();

		}

		ContourMaker contourMaker = new ContourWithSynder(x, y, gridSize);
		ContourPath[] paths = contourMaker.getContourPaths(hpd);

		for (ContourPath path : paths) {

			double[] latitude = path.getAllX();
			double[] longitude = path.getAllY();

			List<Coordinates> coordinates = new ArrayList<Coordinates>();

			for (int i = 0; i < latitude.length; i++) {

				coordinates
						.add(new Coordinates(longitude[i], latitude[i], 0.0));
			}

			beginShape();

			for (int row = 0; row < coordinates.size() - 1; row++) {

				double X = Utils.map(coordinates.get(row).getLongitude(), minX,
						maxX, 0, width);
				double Y = Utils.map(coordinates.get(row).getLatitude(), maxY,
						minY, 0, height);

				double XEND = Utils.map(
						coordinates.get(row + 1).getLongitude(), minX, maxX, 0,
						width);
				double YEND = Utils.map(
						(coordinates.get(row + 1).getLatitude()), maxY, minY,
						0, height);

				vertex((float) X, (float) Y);
				vertex((float) XEND, (float) YEND);

			}// END: coordinates loop
			endShape(CLOSE);

		}// END: paths loop

		slicesMap.remove(sliceTime);

	}// END: drawPolygon()

	private void drawBranches() {

		double treeHeightMax = Utils.getTreeHeightMax(tree);
		strokeWeight((float) branchWidth);

		for (Node node : tree.getNodes()) {
			if (!tree.isRoot(node)) {

				Double longitude = (Double) node.getAttribute(longitudeName);
				Double latitude = (Double) node.getAttribute(latitudeName);

				Node parentNode = tree.getParent(node);

				Double parentLongitude = (Double) parentNode
						.getAttribute(longitudeName);
				Double parentLatitude = (Double) parentNode
						.getAttribute(latitudeName);

				// Equirectangular projection:
				double x0 = Utils.map(parentLongitude, minX, maxX, 0, width);
				double y0 = Utils.map(parentLatitude, maxY, minY, 0, height);

				double x1 = Utils.map(longitude, minX, maxX, 0, width);
				double y1 = Utils.map(latitude, maxY, minY, 0, height);

				/**
				 * Color mapping
				 * */
				double nodeHeight = Utils.getNodeHeight(tree, node);

				int red = (int) Utils.map(nodeHeight, 0, treeHeightMax,
						minBranchRedMapping, maxBranchRedMapping);

				int green = (int) Utils.map(nodeHeight, 0, treeHeightMax,
						minBranchGreenMapping, maxBranchGreenMapping);

				int blue = (int) Utils.map(nodeHeight, 0, treeHeightMax,
						minBranchBlueMapping, maxBranchBlueMapping);

				int alpha = (int) Utils.map(nodeHeight, 0, treeHeightMax,
						maxBranchOpacityMapping, minBranchOpacityMapping);

				stroke(red, green, blue, alpha);
				line((float) x0, (float) y0, (float) x1, (float) y1);
			}
		}// END: nodes loop
	}// END: DrawBranches

	public void analyzeTrees() throws IOException, ImportException,
			ParseException {

		mrsd = new ThreadLocalSpreadDate(mrsdString);

		switch (analysisType) {
		case 1:
			tree = (RootedTree) treeImporter.importNextTree();
			treeRootHeight = Utils.getNodeHeight(tree, tree.getRootNode());
			sliceHeights = generateTreeSliceHeights(treeRootHeight,
					numberOfIntervals);
			timeLine = generateTreeTimeLine(tree);
			break;
		case 2:
			timeLine = generateCustomTimeLine(sliceHeights);
			break;
		}

		//sort them in ascending numerical order
		Arrays.sort(sliceHeights);
		
		System.out.println("Using as slice times: ");
		Utils.printArray(sliceHeights);
		System.out.println();
		
			startTime = timeLine.getStartTime();
			endTime = timeLine.getEndTime();

			// This is for slice times
			slicesMap = new ConcurrentHashMap<Double, List<Coordinates>>();

			// Executor for threads
			int NTHREDS = Runtime.getRuntime().availableProcessors();
			ExecutorService executor = Executors.newFixedThreadPool(NTHREDS * 2);

			int treesAssumed = 10000;
			int treesRead = 0;
			
			System.out.println("Analyzing trees (bar assumes 10,000 trees)");
			System.out.println("0                   25                  50                  75                 100");
			System.out.println("|---------------------|---------------------|---------------------|---------------------|");
//			System.out.println("0              25             50             75            100");
//			System.out.println("|--------------|--------------|--------------|--------------|");
			
			int stepSize = treesAssumed / 60;
			if (stepSize < 1) {
				stepSize = 1;
			}

			// This is for collecting coordinates into polygons
			slicesMap = new ConcurrentHashMap<Double, List<Coordinates>>();

			int totalTrees = 0;
			while (treesImporter.hasTree()) {

				currentTree = (RootedTree) treesImporter.importNextTree();

				if (totalTrees >= burnIn) {

					executor.submit(new AnalyzeTree(currentTree, //
							precisionString,//
							coordinatesName, //
							rateString, //
							sliceHeights, //
							timescaler,//
							mrsd, //
							slicesMap, //
							useTrueNoise//
							));

					// new AnalyzeTree(currentTree,
					// precisionString, coordinatesName, rateString,
					// sliceHeights, timescaler, mrsd, slicesMap,
					// useTrueNoise).run();

					treesRead += 1;

				}// END: if burn-in

				if (totalTrees > 0 && totalTrees % stepSize == 0) {
					System.out.print("*");
					System.out.flush();
				}

				totalTrees++;

			}// END: while has trees

			if ((totalTrees - burnIn) <= 0.0) {
				throw new RuntimeException("Burnt too many trees!");
			} else {
				System.out.println("\nAnalyzed " + treesRead
						+ " trees with burn-in of " + burnIn + " for the total of "
						+ totalTrees + " trees");
			}

			// Wait until all threads are finished
			executor.shutdown();
			while (!executor.isTerminated()) {
			}

	}// END: AnalyzeTrees

	private TimeLine generateTreeTimeLine(RootedTree tree) {

		// This is a general time span for all of the trees
		double treeRootHeight = Utils.getNodeHeight(tree, tree.getRootNode());
		double startTime = mrsd.getTime()
				- (treeRootHeight * DayInMillis * DaysInYear * timescaler);
		double endTime = mrsd.getTime();
		TimeLine timeLine = new TimeLine(startTime, endTime, numberOfIntervals);

		return timeLine;
	}// END: generateTreeTimeLine

	private double[] generateTreeSliceHeights(double treeRootHeight,
			int numberOfIntervals) {

		double[] timeSlices = new double[numberOfIntervals];

		for (int i = 0; i < numberOfIntervals; i++) {

			timeSlices[i] = treeRootHeight
					- (treeRootHeight / (double) numberOfIntervals)
					* ((double) i);
		}

		return timeSlices;
	}// END: generateTimeSlices

	private TimeLine generateCustomTimeLine(double[] timeSlices) {

		// This is a general time span for all of the trees
		int numberOfSlices = timeSlices.length;
		double firstSlice = timeSlices[0];
		
		double startTime = mrsd.getTime() - (firstSlice * DayInMillis * DaysInYear * timescaler);
		double endTime = mrsd.getTime();

		return new TimeLine(startTime, endTime, numberOfSlices);
	}// END: generateCustomTimeLine

}// END: TimeScalerToProcessing class
