package graphTheory.graphDrawer;

import graphTheory.graph.Arc;
import graphTheory.graph.Graph;
import graphTheory.latex.TikzTranslator;
import graphTheory.utils.Drawing;
import graphTheory.utils.Math2;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import javax.swing.JFrame;

/**
 * 
 * This class implements two methods to draw a graph in a frame, knowing the
 * coordinates of each nodes. And a basic method to computes those coordinates.
 * 
 * @author Watel Dimitri
 * 
 */
public class GraphDrawer extends JFrame implements MouseListener,
		MouseMotionListener {

	/**
	 * For directed arcs, default angle between the triangle sides of the arrow
	 * and the strait (or not) line of the arrow.
	 */
	private static final double BETADEFAUT = 15;

	/**
	 * Default radius of the circle nodes, and middle of the default size of the
	 * edges of the square nodes.
	 */
	private static final int RAYONDEFAUT = 10;

	/**
	 * For semi circular arcs, default angle between the strait line linking the
	 * nodes and the tangent of the arc at the original node.
	 */
	private static final Double DEFAULT_START_ANGLE = Math.PI / 6;

	/**
	 * For semi circular arcs, default distance between the strait line linking
	 * the nodes and the arc at middle distance
	 */
	private static final Double DEFAULT_MIDDLE_DISTANCE = 30.0;

	private static final long serialVersionUID = 1L;

	/**
	 * Drawed graph
	 */
	public Graph graph;

	/**
	 * If a node is clicked and dragged, this let the frame knows which node is
	 * dragged.
	 */
	private Integer clikedNode;

	/**
	 * This button launch a conversion fonction from this representation to a
	 * TIKZ representation.
	 */
	private Button convButton;

	/**
	 * This HashMap map every arc with a parameter which is displayed on the
	 * screen next to the arc (like the cost of the arc).
	 */
	@SuppressWarnings("rawtypes")
	private HashMap arcDisplayedParam;

	/**
	 * Create a drawer and draw the graph g, with no parameters associated with
	 * the arcs.
	 * 
	 * @param g
	 */
	public GraphDrawer(Graph g) {
		this(g, null);
	}

	/**
	 * Create a drawer and draw the graph g. arcDisplayedParam decide what
	 * parameter is associated with each arc.
	 * 
	 * @param g
	 * @param arcDisplayedParam
	 */
	public GraphDrawer(Graph g,
			@SuppressWarnings("rawtypes") HashMap arcDisplayedParam) {
		setLayout(new BorderLayout());

		convButton = new Button("Convert Tikz");
		this.add(convButton, BorderLayout.SOUTH);
		convButton.addMouseListener(this);

		setPreferredSize(new Dimension(800, 640));
		setSize(new Dimension(800, 640));
		setLocationRelativeTo(null);
		setBackground(Color.WHITE);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setVisible(true);
		addMouseListener(this);
		addMouseMotionListener(this);
		graph = g;

		this.arcDisplayedParam = arcDisplayedParam;

		setVerticesCoordinates(); // set the coordinates of every node before drawing them.

		repaint();
	}

	/**
	 * Set the coordinates of every node with a basic method : every nodes are
	 * uniformly placed on a circle.
	 */
	protected void setVerticesCoordinates() {
		ArrayList<Integer> h = graph.getVertices();
		int s = h.size();
		int i = 0;
		for (Integer n : h) {
			int x1 = (int) (300D + 200D * Math
					.cos(((double) (2 * i) * 3.1415926535897931D) / (double) s));
			int y1 = (int) (300D + 200D * Math
					.sin(((double) (2 * i) * 3.1415926535897931D) / (double) s));

			graph.setNodeAbscisse(n, x1);
			graph.setNodeOrdonnee(n, y1);

			i++;
		}
	}

	/**
	 * Place every nodes on the frame, knowing its coordinates.
	 * 
	 * @param g
	 */
	protected void paintVertices(Graphics g) {
		for (Integer n : graph.getVertices()) {
			paintVertice(g, n);
		}
	}

	/**
	 * Place every arcs ou edges on the frame, knowing its coordinates.
	 * 
	 * @param g
	 */
	protected void paintEdges(Graphics g) {
		ArrayList<Arc> arcs = graph.getEdges();
		/*
		 * It first checks if there is no two opposites arcs. In that case, if
		 * the arcs were drawed with strait lines, the drawer uses two opposite
		 * semi circular arcs instead.
		 */
		for (Arc a : arcs) {
			for (Arc b : arcs) {
				if (a.getInput().equals(b.getOutput())
						&& a.getOutput().equals(b.getInput())
						&& graph.isLineSymbol(a) && graph.isLineSymbol(a)) {
					graph.setSymbolCircleArc1(a, -DEFAULT_START_ANGLE);
					graph.setSymbolCircleArc1(a, -DEFAULT_START_ANGLE);
				}
			}
		}

		for (Arc arc : graph.getEdges()) {
			if (arc.isDirected())
				graph.setOutputSymbolArrow(arc, BETADEFAUT, RAYONDEFAUT);
			paintEdge(g, arc);
		}
	}

	public void paint(Graphics g) {
		g.clearRect(0, 0, getWidth(), getHeight());
		paintVertices(g);
		paintEdges(g);

	}

	/**
	 * Draws the node n on the frame, knowing its coordinates.
	 * 
	 * @param g
	 * @param n
	 */
	public void paintVertice(Graphics g, Integer n) {

		g.setColor(graph.getColor(n));

		// If the node is drawed as a circle
		if (graph.isCircleSymbol(n)) {
			Integer radius = graph.getRadius(n);
			if (radius == null)
				radius = 25;

			if (graph.isFill(n))
				g.fillOval(graph.getNodeAbscisse(n) - radius,
						graph.getNodeOrdonnee(n) - radius, 2 * radius,
						2 * radius);
			else
				g.drawOval(graph.getNodeAbscisse(n) - radius,
						graph.getNodeOrdonnee(n) - radius, 2 * radius,
						2 * radius);
		}
		// if the node is drawed as a square
		else if (graph.isSquareSymbol(n)) {
			Integer sideLength = graph.getSideLength(n);
			if (sideLength == null)
				sideLength = 50;
			//Set the four x and y coordinates of the square
			int[] xPoints = { graph.getNodeAbscisse(n) - sideLength / 2,
					graph.getNodeAbscisse(n) - sideLength / 2,
					graph.getNodeAbscisse(n) + sideLength / 2,
					graph.getNodeAbscisse(n) + sideLength / 2 },

			yPoints = { graph.getNodeOrdonnee(n) - sideLength / 2,
					graph.getNodeOrdonnee(n) + sideLength / 2,
					graph.getNodeOrdonnee(n) + sideLength / 2,
					graph.getNodeOrdonnee(n) - sideLength / 2 };
			if (graph.isFill(n))
				g.fillPolygon(xPoints, yPoints, 4);
			else
				g.drawPolygon(xPoints, yPoints, 4);
		}

		// else do nothing
		else {
		}

		// draw the string representation of n at the node coordinates 
		g.setColor(graph.getTextColor(n));
		g.drawString(String.valueOf(n), graph.getNodeAbscisse(n),
				graph.getNodeOrdonnee(n));
		g.setColor(Color.black);
	}

	/**
	 * Draws the arc on the frame, knowing the coordinates of its endings
	 * 
	 * @param g
	 * @param arc
	 */
	public void paintEdge(Graphics g, Arc arc) {

		g.setColor(graph.getColor(arc));

		int x1, x2, y1, y2;
		x1 = graph.getNodeAbscisse(arc.getInput());
		y1 = graph.getNodeOrdonnee(arc.getInput());
		x2 = graph.getNodeAbscisse(arc.getOutput());
		y2 = graph.getNodeOrdonnee(arc.getOutput());

		// a is the slope of the strait line between the endings
		double a = ((double) y2 - (double) y1) / ((double) x2 - (double) x1);

		// alpha if the angle related to the slope
		double alpha = (Math.atan(a) + (x1 > x2 ? 3.1415926535897931D : 0.0D))
				% (2.0 * Math.PI);
		if (alpha < 0)
			alpha += Math.PI * 2.0;

		// d is the distance between the endings
		double d = Math.sqrt(Math.pow(x2 - x1, 2D) + Math.pow(y2 - y1, 2D));

		/*
		 * x11, x22, y11 and y22 are the coordinated of the endings of the
		 * drawed arc (if the arc link two circle nodes, the drawed arc do not
		 * link the center of the circles but the sides). It depends on the
		 * shapes of the endings.
		 */
		int x11 = 0, x22 = 0, y11 = 0, y22 = 0;
		Integer radius, sideLength;

		// If the input is a circle
		if (graph.isCircleSymbol(arc.getInput())) {
			radius = graph.getRadius(arc.getInput());
			if (radius == null)
				radius = 25;
			x11 = (int) (Math.cos(alpha) * (double) radius + (double) x1);
			y11 = (int) (Math.sin(alpha) * (double) radius + (double) y1);
		}
		// if the input is a square
		else if (graph.isSquareSymbol(arc.getInput())) {
			sideLength = graph.getSideLength(arc.getInput());
			if (sideLength == null)
				sideLength = 50;
			if (alpha <= Math.PI / 4.0 || alpha > Math.PI * 7.0 / 4.0) {
				x11 = (int) (sideLength / 2.0 + (double) x1);
				y11 = (int) (sideLength * Math.tan(alpha) / 2.0 + (double) y1);
			} else if (alpha <= Math.PI * 3.0 / 4.0 && alpha > Math.PI / 4.0) {
				x11 = (int) (sideLength / (2.0 * Math.tan(alpha)) + (double) x1);
				y11 = (int) (sideLength / 2.0 + (double) y1);
			} else if (alpha <= Math.PI * 5.0 / 4.0
					&& alpha > Math.PI * 3.0 / 4.0) {
				x11 = (int) (-sideLength / 2.0 + (double) x1);
				y11 = (int) (-sideLength * Math.tan(alpha) / 2.0 + (double) y1);
			} else {
				x11 = (int) (-sideLength / (2.0 * Math.tan(alpha)) + (double) x1);
				y11 = (int) (-sideLength / 2.0 + (double) y1);
			}
		}

		// If the output is a circle
		if (graph.isCircleSymbol(arc.getOutput())) {
			radius = graph.getRadius(arc.getOutput());
			if (radius == null)
				radius = 25;
			x22 = (int) (Math.cos(alpha) * (d - (double) radius) + (double) x1);
			y22 = (int) (Math.sin(alpha) * (d - (double) radius) + (double) y1);
		}
		// if the output is a square
		else if (graph.isSquareSymbol(arc.getOutput())) {
			sideLength = graph.getSideLength(arc.getOutput());
			if (sideLength == null)
				sideLength = 50;
			if (alpha <= Math.PI / 4.0 || alpha > Math.PI * 7.0 / 4.0) {
				x22 = (int) (-sideLength / 2.0 + (double) x2);
				y22 = (int) (-sideLength * Math.tan(alpha) / 2.0 + (double) y2);
			} else if (alpha <= Math.PI * 3.0 / 4.0 && alpha > Math.PI / 4.0) {
				x22 = (int) (-sideLength / (2.0 * Math.tan(alpha)) + (double) x2);
				y22 = (int) (-sideLength / 2.0 + (double) y2);
			} else if (alpha <= Math.PI * 5.0 / 4.0
					&& alpha > Math.PI * 3.0 / 4.0) {
				x22 = (int) (sideLength / 2.0 + (double) x2);
				y22 = (int) (sideLength * Math.tan(alpha) / 2.0 + (double) y2);
			} else {
				x22 = (int) (sideLength / (2.0 * Math.tan(alpha)) + (double) x2);
				y22 = (int) (sideLength / 2.0 + (double) y2);
			}
		}

		Double startAngle = null, middleDistance = null;

		// If the arc is a strait line
		if (graph.isLineSymbol(arc)) {
			g.drawLine(x11, y11, x22, y22);
		}

		/*
		 * For semi circular arcs defined by angles between the strait line
		 * linking the nodes and the tangent of the arc at the endings.
		 */
		else if (graph.isCircleStartAngleSymbol(arc)) {
			startAngle = graph.getStartAngle(arc);
			if (startAngle == null)
				startAngle = DEFAULT_START_ANGLE;
			Drawing.drawArc1(g, x11, y11, x22, y22, startAngle); // Special helper to draw semi circular arcs
			middleDistance = Drawing.fromStartAngleToMiddleDistance(x11, y11,
					x22, y22, startAngle);
		}

		/*
		 * For semi circular arcs defined by the distance between the strait
		 * line linking the nodes and the arc at middle distance
		 */
		else if (graph.isCircleMiddleDistanceSymbol(arc)) {
			middleDistance = graph.getMiddleDistance(arc);
			if (middleDistance == null)
				middleDistance = DEFAULT_MIDDLE_DISTANCE;
			Drawing.drawArc2(g, x11, y11, x22, y22, middleDistance); // Special helper to draw semi circular arcs
			startAngle = Drawing.fromMiddleDistanceToStartAngle(x11, y11, x22,
					y22, middleDistance);
		}

		// If the arc is directed, draw the ending triangle.
		if (arc.isDirected()) {
			if (graph.isArrowOutputSymbol(arc)) {
				Integer rayon = graph.getSideDistance(arc);
				Double beta = graph.getBeta(arc);
				if (rayon == null)
					rayon = RAYONDEFAUT;
				if (beta == null)
					beta = BETADEFAUT;
				if (graph.isCircleStartAngleSymbol(arc)
						|| graph.isCircleMiddleDistanceSymbol(arc)) {
					alpha += startAngle;
				}
				int x33 = (int) (Math.cos(alpha + (double) beta)
						* (double) rayon + (double) x22);
				int x44 = (int) (Math.cos(alpha - (double) beta)
						* (double) rayon + (double) x22);
				int y33 = (int) (Math.sin(alpha + (double) beta)
						* (double) rayon + (double) y22);
				int y44 = (int) (Math.sin(alpha - (double) beta)
						* (double) rayon + (double) y22);
				g.drawLine(x33, y33, x22, y22);
				g.drawLine(x44, y44, x22, y22);
			}
		}

		// If a parameter is associated with the arc, draw it.
		String param;
		double dist = Math2.dist(x11, y11, x22, y22, 2.0);
		double p1p2a = (x22 - x11) / (dist);
		double p1p2b = (y22 - y11) / (dist);
		double textDist = 10;
		if (middleDistance != null)
			textDist += middleDistance;
		int xt = (int) (dist / 2 * p1p2a + textDist * p1p2b + x11);
		int yt = (int) (dist / 2 * p1p2b - textDist * p1p2a + y11);
		if (arcDisplayedParam != null) {
			Object p = arcDisplayedParam.get(arc);
			if (p != null) {
				param = String.valueOf(p);
				g.drawString(param, xt, yt);
			}
		}
		g.setColor(Color.black);
	}

	/**
	 * If one click at coordinates (x,y) to select a node, returns that node
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	private Integer getClikedVertice(int x, int y) {
		Iterator<Integer> it = graph.getVerticesIterator();
		Integer n, m = null;
		boolean b;
		Integer r;
		while (it.hasNext()) {
			n = it.next();
			b = graph.isCircleSymbol(n);
			r = graph.getRadius(n);

			if (b) {
				if (Math2.dist(graph.getNodeAbscisse(n),
						graph.getNodeOrdonnee(n), x, y, 2.0) < r)
					m = n;
			} else {
				if (Math2.dist(graph.getNodeAbscisse(n),
						graph.getNodeOrdonnee(n), x, y,
						Double.POSITIVE_INFINITY) < r)
					m = n;
			}
		}
		return m;
	}

	public void mousePressed(MouseEvent me) {
		if (me.getSource() == convButton) {
			System.out.println(TikzTranslator.translateGraph(graph));
		} else {
			int x = me.getX();
			int y = me.getY();

			Integer n = getClikedVertice(x, y);
			if (n != null)
				clikedNode = n;

			repaint();
		}
	}

	public void mouseReleased(MouseEvent me) {
	}

	public void mouseEntered(MouseEvent me) {
	}

	public void mouseExited(MouseEvent me) {
	}

	public void mouseMoved(MouseEvent me) {

	}

	@Override
	public void mouseDragged(MouseEvent me) {
		int x = me.getX();
		int y = me.getY();
		if (clikedNode != null) {
			graph.setNodeCoordinates(clikedNode, x, y);
		}
		repaint();
	}

	@Override
	public void mouseClicked(MouseEvent me) {

	}
}
