package templates;

import gui.InteractiveTableModel;

import java.io.FileReader;
import java.io.IOException;
import java.util.List;
import java.util.Random;

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 processing.core.PFont;
import structure.Coordinates;
import utils.Utils;

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

	public long time;

	private TreeImporter importer;
	private RootedTree tree;
	private InteractiveTableModel table;
	private String stateAttName;
	private int numberOfIntervals;
	private double rootHeight;

	private MapBackground mapBackground;

	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 minPolygonRedMapping;
	private double minPolygonGreenMapping;
	private double minPolygonBlueMapping;
	private double minPolygonOpacityMapping;

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

	private double branchWidth;
	private double polygonsRadiusMultiplier;
	private Random generator;

	// min/max longitude
	private float minX, maxX;
	// min/max latitude
	private float minY, maxY;

	public DiscreteTreeToProcessing() {

	}// END: DiscreteTreeToProcessing

	public void setStateAttName(String name) {
		stateAttName = name;
	}

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

	public void setTreePath(String path) throws IOException, ImportException {
		importer = new NexusImporter(new FileReader(path));
	}

	public void setTable(InteractiveTableModel tableModel) {
		table = tableModel;
	}

	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 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 setPolygonsRadiusMultiplier(double multiplier) {
		polygonsRadiusMultiplier = multiplier;
	}

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

	public void setup() {

		try {

			minX = -180;
			maxX = 180;

			minY = -90;
			maxY = 90;

			generator = new Random();

			// will improve font rendering speed with default renderer
			hint(ENABLE_NATIVE_FONTS);
			PFont plotFont = createFont("Monaco", 12);
			textFont(plotFont);

			tree = (RootedTree) importer.importNextTree();
			// this is for time calculations
			rootHeight = tree.getHeight(tree.getRootNode());

			mapBackground = new MapBackground(this);

		} catch (IOException e) {
			e.printStackTrace();

		} catch (ImportException e) {
			e.printStackTrace();
		}

	}// END: setup

	public void draw() {

		// start timing
		time = -System.currentTimeMillis();

		noLoop();
		smooth();
		mapBackground.drawMapBackground();
		drawCircles();
		drawPlaces();
		drawBranches();
		drawPlacesLabels();

		// stop timing
		time += System.currentTimeMillis();

	}// END:draw

	// //////////////
	// ---PLACES---//
	// //////////////
	private void drawPlaces() {

		float radius = 7;
		// White
		fill(255, 255, 255);
		noStroke();

		for (int row = 0; row < table.getRowCount(); row++) {

			Float longitude = Float.valueOf(String.valueOf(table.getValueAt(
					row, 2)));

			Float latitude = Float.valueOf(String.valueOf(table.getValueAt(row,
					1)));

			// Equirectangular projection:
			float X = map(longitude, minX, maxX, 0, width);
			float Y = map(latitude, maxY, minY, 0, height);

			ellipse(X, Y, radius, radius);

		}
	}// END DrawPlaces

	private void drawPlacesLabels() {

		textSize(7);

		// Black labels
		fill(0, 0, 0);

		for (int row = 0; row < table.getRowCount(); row++) {

			String name = String.valueOf(table.getValueAt(row, 0));

			Float longitude = Float.valueOf(String.valueOf(table.getValueAt(
					row, 2)));

			Float latitude = Float.valueOf(String.valueOf(table.getValueAt(row,
					1)));

			float X = map(longitude, minX, maxX, 0, width);
			float Y = map(latitude, maxY, minY, 0, height);

			text(name, X, Y);
		}
	}// END: DrawPlacesLabels

	// ////////////////
	// ---BRANCHES---//
	// ////////////////

	private void drawBranches() {

		strokeWeight((float) branchWidth);

		double treeHeightMax = Utils.getTreeHeightMax(tree);

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

				String state = getRandomState((String) node
						.getAttribute(stateAttName), true);

				Node parentNode = tree.getParent(node);

				String parentState = getRandomState((String) parentNode
						.getAttribute(stateAttName), false);

				if (state != null && parentState != null) {

					if (!state.toLowerCase().equals(parentState.toLowerCase())) {

						float longitude = Utils.matchStateCoordinate(table,
								state, 2);
						float latitude = Utils.matchStateCoordinate(table,
								state, 1);

						float parentLongitude = Utils.matchStateCoordinate(
								table, parentState, 2);
						float parentLatitude = Utils.matchStateCoordinate(
								table, parentState, 1);

						float x0 = map(parentLongitude, minX, maxX, 0, width);
						float y0 = map(parentLatitude, maxY, minY, 0, height);

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

						/**
						 * Color mapping
						 * */
						double nodeHeight = tree.getHeight(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(x0, y0, x1, y1);

					}// END: state and parent state equality check
				}// END: null check
			}// END: root check
		}// END: nodes loop
	}// END: DrawBranches

	// ///////////////
	// ---CIRCLES---//
	// ///////////////

	private void drawCircles() {

		double[][] numberOfLineages = CountLineagesHoldingState(
				numberOfIntervals, rootHeight);
		double lineagesCountMax = Utils.get2DArrayMax(numberOfLineages);

		for (int i = 0; i < (numberOfIntervals - 1); i++) {
			for (int j = 0; j < (table.getRowCount()); j++) {

				if (numberOfLineages[i][j + 1] > 0) {

					/**
					 * Color mapping
					 * */
					int red = (int) Utils.map(numberOfLineages[i][j + 1], 0,
							lineagesCountMax, minPolygonRedMapping,
							maxPolygonRedMapping);

					int green = (int) Utils.map(numberOfLineages[i][j + 1], 0,
							lineagesCountMax, minPolygonGreenMapping,
							maxPolygonGreenMapping);

					int blue = (int) Utils.map(numberOfLineages[i][j + 1], 0,
							lineagesCountMax, minPolygonBlueMapping,
							maxPolygonBlueMapping);

					/**
					 * Opacity mapping
					 * 
					 * Larger the values more opaque the colors
					 * */
					int alpha = (int) Utils.map(numberOfLineages[i][j + 1], 0,
							lineagesCountMax, maxPolygonOpacityMapping,
							minPolygonOpacityMapping);

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

					double radius = Math.round(100 * Math
							.sqrt(numberOfLineages[i][j + 1]))
							* polygonsRadiusMultiplier;

					Double longitude = Double.valueOf(String.valueOf(table
							.getValueAt(j, 1)));

					Double latitude = Double.valueOf(String.valueOf(table
							.getValueAt(j, 2)));

					List<Coordinates> coordinates = Utils.generateCircle(
							latitude, // centerLat
							longitude, // centerLong
							radius, // radius
							36); // numPoints

					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: if numberOfLineages

			}// END: table rows loop
		}// END: numberOfIntervals loop

	}// END: drawCircles

	private double[][] CountLineagesHoldingState(int numberOfIntervals,
			double rootHeight) {

		double delta = rootHeight / numberOfIntervals;
		double[][] numberOfLineages = new double[(numberOfIntervals - 1)][table
				.getRowCount() + 1];

		for (int i = 0; i < (numberOfIntervals - 1); i++) {
			numberOfLineages[i][0] = rootHeight - ((i + 1) * delta);
		}

		for (int i = 0; i < (numberOfIntervals - 1); i++) {
			for (int j = 0; j < table.getRowCount(); j++) {

				int numberOfLineagesOfState = 0;

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

						String state = getRandomState((String) node
								.getAttribute(stateAttName), false);

						Node parentNode = tree.getParent(node);

						String parentState = getRandomState((String) parentNode
								.getAttribute(stateAttName), false);

						if ((tree.getHeight(node) <= numberOfLineages[i][0])
								&& (tree.getHeight(parentNode) > numberOfLineages[i][0])) {

							String name = String
									.valueOf(table.getValueAt(j, 0));

							if ((state.toLowerCase().equals(parentState
									.toLowerCase()))
									&& (parentState.toLowerCase().equals(name
											.toLowerCase()))) {

								numberOfLineagesOfState++;

							}
						}
					}
				}// END: node loop

				numberOfLineages[i][j + 1] = numberOfLineagesOfState;

			}// END: col loop
		}// END: row loop

		return numberOfLineages;
	}// END: CountLineagesHoldingState

	private String getRandomState(String state, boolean verbose) {

		generator.setSeed(time);

		if (!state.contains("+")) {
			return state;

		} else {// this breaks ties

			if (verbose)
				System.out.println("Found combined " + stateAttName
						+ " attribute: " + state);

			state = Utils.pickRand(state.split("\\+"), generator);

			if (verbose)
				System.out.println("Randomly picking: " + state);

			return state;
		}

	}// END: getRandomState

}// END: PlotOnMap class
