package ui.view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.LinkedList;
import java.awt.Point;

import javax.swing.JPanel;

import classes.HolonElement;
import ui.controller.Control;
import ui.model.Model;
import classes.HolonSwitch;

import java.awt.Cursor;

/**
 * This Class represents a Graph where the User can model the behavior of
 * elements and switches over time.
 * 
 * @author Gruppe14
 */
public class UnitGraph extends JPanel implements MouseListener, MouseMotionListener, ComponentListener {

	private static final long serialVersionUID = 1L;
	private float maximum = 0;

	// Information shown when a Point is Dragged
	private String dragInformation = "";

	// Points
	private Point recSize = new Point(8, 8); // Point Size
	private Graphics2D g2;
	private CubicCurve2D c = new CubicCurve2D.Double();
	private CubicCurve2D cr = new CubicCurve2D.Double();
	private CubicCurve2D cl = new CubicCurve2D.Double();
	private LinkedList<Point> pointList;
	// Scale for the Graph
	private double scaleX;
	private double scaleY;

	private float[] arrayOfFloats = null;
	private boolean[] arrayOfBooleans = null;

	private double width = -1;
	private double height = -1;

	private boolean isElement = false;
	private boolean isSwitch = false;

	private ArrayList<HolonElement> tempElements = new ArrayList<>();
	private Model model;
	private Control controller;
	private Line2D.Double line = null;
	GeneralPath graphCurve = new GeneralPath();

	private boolean pointDrag = false;
	private boolean init = true;
	private Point tempP = null;
	private double x = 0, y = 0;
	private int x1, x2, y1, y2, ctrlx1, ctrly1, ctrlx2, ctrly2;

	/**
	 * Constructor.
	 * 
	 * @param model
	 *            the Model
	 * @param control
	 *            the Controller
	 */
	public UnitGraph(final Model model, Control control) {
		setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
		this.controller = control;
		this.model = model;

		this.pointList = new LinkedList<>();
		this.setBackground(Color.WHITE);

		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.addComponentListener(this);
	}

	/**
	 * Paints all Components on the Canvas.
	 * 
	 * @param g
	 *            Graphics
	 * 
	 */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		g2 = (Graphics2D) g;
		RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHints(rh);
		g2.setStroke(new BasicStroke(0));

		graphCurve.reset();

		// Draw the Vertical Lines
		g2.setColor(Color.BLACK);
		for (int i = 0; i <= this.getWidth(); i += 10) {
			g2.drawLine(i, 0, i, this.getHeight());
		}

		for (int i = 0; i <= this.getHeight(); i += 5) {
			g2.drawLine(0, i, this.getWidth(), i);
		}

		if (isElement) {
			// array fillen
			fillArrayofValue();

			if (arrayOfFloats != null) {
				// Draw the Lines
				g2.setStroke(new BasicStroke(2));
				g2.setColor(Color.BLACK);
				for (int i = 0; i < pointList.size() - 1; i++) {
					c = buildCurve(pointList.get(i), pointList.get(i + 1));
					graphCurve.append(c, true);
				}
				g2.draw(graphCurve);

				// Draw the Points
				g2.setColor(Color.BLUE);
				for (int i = 0; i < pointList.size() - 0; i++) {
					g2.fillOval((int) (pointList.get(i).getX() * scaleX - recSize.getX() / 2),
							(int) (pointList.get(i).getY() * scaleY - recSize.getY() / 2), (int) recSize.getX(),
							(int) recSize.getY());
				}

				// Iteration Value
				if (arrayOfFloats != null) {
					g2.drawString("" + arrayOfFloats[model.getCurIteration()],
							(model.getCurIteration()) * this.getWidth() / (model.getIterations() - 1) + 2,
							this.getHeight() - 10);
				}
			}
			// drag Information
			if (tempP != null) {
				dragInformation = "" + convertToValueY(getYValueAt((int) tempP.getX()));
				g2.drawString(dragInformation, (int) (tempP.getX() * scaleX) + 10, (int) (tempP.getY() * scaleY) + 10);
			}

			/*
			 * // Actual Iteration Point Visualization g2.setColor(Color.RED);
			 * if (arrayOfValue != null) { for (int i = 0; i <
			 * arrayOfValue.length; i++) { g2.fillOval((int) (i * width /
			 * (model.getIterations() - 1) * scaleX - recSize.getX() / 2), (int)
			 * (convertToCanvasY((int) arrayOfValue[i]) * scaleY -
			 * recSize.getY() / 2), (int) recSize.getX(), (int) recSize.getY());
			 * } }
			 */
		} else if (isSwitch) {
			if (arrayOfBooleans != null) {
				// array fillen
				fillArrayofBooleans();

				// Draw the Lines
				g2.setStroke(new BasicStroke(2));
				g2.setColor(Color.BLACK);
				for (int i = 0; i < pointList.size() - 1; i++) {
					line = new Line2D.Double(pointList.get(i).getX() * scaleX, pointList.get(i).getY() * scaleY,
							pointList.get(i + 1).getX() * scaleX, pointList.get(i + 1).getY() * scaleY);
					graphCurve.append(line, true);
				}
				g2.draw(graphCurve);

				/*
				 * // Draw the Points g2.setColor(Color.BLUE); for (int i = 0; i
				 * < pointList.size() - 0; i++) { g2.fillOval((int)
				 * (pointList.get(i).getX() * scaleX - recSize.getX() / 2),
				 * (int) (pointList.get(i).getY() * scaleY - recSize.getY() /
				 * 2), (int) recSize.getX(), (int) recSize.getY()); }
				 */

				// Iteration Value
				if (arrayOfBooleans != null) {
					g2.drawString("" + arrayOfBooleans[model.getCurIteration()],
							(model.getCurIteration()) * this.getWidth() / (model.getIterations() - 1) + 2,
							this.getHeight() - 10);
				}
			}
			if (tempP != null) {
				try {
					int i;
					for (i = 0; (i * this.getWidth() / (model.getIterations() - 1) < getMousePosition().getX()); i++) {

					}
					dragInformation = "" + i;
					g2.drawString(dragInformation, (int) (getMousePosition().getX()) + 10,
							(int) (getMousePosition().getY()) + 10);
				} catch (Exception e) {
					// TODO: handle exception
				}
			}
		}

		// Iteration Line
		g2.setColor(Color.BLUE);
		g2.setStroke(new BasicStroke(1));
		g2.drawLine((model.getCurIteration()) * this.getWidth() / (model.getIterations() - 1), 0,
				(model.getCurIteration()) * this.getWidth() / (model.getIterations() - 1), this.getHeight());

		// algorithmus
		controller.calculateStateForTimeStep(model.getCurIteration());
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		if (isElement) {
			elementDragged(e);
		} else if (isSwitch) {
			switchDragged(e);
		}
	}

	/**
	 * When a Point of a Holon Element is dragged.
	 * 
	 * @param e
	 *            MouseEvent
	 */
	public void elementDragged(MouseEvent e) {
		if (pointDrag && tempP != null) {
			// Out of Bounds verhindern
			int i = pointList.indexOf(tempP);
			x = e.getX() / scaleX;
			y = e.getY() / scaleY;
			// y
			if (e.getY() <= 0) {
				y = 0 / scaleY;
			} else if (this.getHeight() <= e.getY()) {
				y = this.getHeight() / scaleY;
			}
			// x
			if (tempP == pointList.getFirst() || tempP == pointList.getLast() || pointList.get(i + 1).getX() <= x
					|| pointList.get(i - 1).getX() >= x) {
				x = tempP.getX();
			}
			tempP.setLocation(x, y);

			repaint();
		}
	}

	/**
	 * When a Point of a switch is dragged.
	 * 
	 * @param e
	 *            MouseEvent
	 */
	public void switchDragged(MouseEvent e) {
		if (pointDrag && tempP != null && tempP != pointList.getFirst() && tempP != pointList.getLast()) {
			int i = pointList.indexOf(tempP);
			x = e.getX() / scaleX;

			if (pointList.get(i + 1).getY() == tempP.getY()) {
				// x
				if (pointList.get(i + 1).getX() <= x + 1 || pointList.get(i - 2).getX() >= x - 1) {
					x = tempP.getX();
				}
				pointList.get(i - 1).setLocation(x, pointList.get(i - 1).getY());
			} else {
				// x
				if (pointList.get(i + 2).getX() <= x + 1 || pointList.get(i - 1).getX() >= x - 1) {
					x = tempP.getX();
				}
				pointList.get(i + 1).setLocation(x, pointList.get(i + 1).getY());
			}
			tempP.setLocation(x, tempP.getY());
			repaint();
		}
	}

	@Override
	public void mouseMoved(MouseEvent e) {
	}

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
		if (isElement) {
			elementPressed(e);
		} else if (isSwitch) {
			switchPressed(e);
		}

	}

	/**
	 * When a point of a Holon Element is pressed.
	 * 
	 * @param e
	 *            MouseEvent
	 */
	public void elementPressed(MouseEvent e) {
		boolean added = false;
		boolean deletePoint = false;

		double x = e.getX() / scaleX;
		double y = e.getY() / scaleY;

		// Click on Point
		tempP = null;
		if (pointList != null) {
			// look if a point was clicked
			for (Point p : pointList) {
				if (x >= p.getX() - recSize.getX() / 2 && y >= p.getY() - recSize.getY() / 2
						&& x <= p.getX() + recSize.getX() / 2 && y <= p.getY() * scaleY + recSize.getY() / 2) {
					if (e.getButton() == MouseEvent.BUTTON3) {
						tempP = p;
						deletePoint = true;
					} else {
						pointDrag = true;
						tempP = p;
					}
				}
			}
			// New Point
			if (!pointDrag && e.getButton() != MouseEvent.BUTTON3 && e.getX() != 0
					&& e.getX() != this.getWidth() / scaleX) {
				for (int i = 0; i < pointList.size(); i++) {
					if (x < pointList.get(i).getX() && !added) {
						if (e.getY() <= 0) {
							pointList.add(i, new Point((int) (x), (int) (0 / scaleY)));
						} else {
							pointList.add(i, new Point((int) (x), (int) y));
						}
						added = true;
						pointDrag = true;
						tempP = pointList.get(i);
					}
				}
			}
			// Delete a Point
			if (deletePoint && tempP.getX() != 0
					&& (tempP.getX() != this.getWidth() / scaleX || tempP != pointList.getLast())) {
				pointList.remove(tempP);
			}

			repaint();
		}
	}

	/**
	 * When a point of a Switch is pressed.
	 * 
	 * @param e
	 *            MouseEvent
	 */
	public void switchPressed(MouseEvent e) {
		boolean added = false;
		boolean deletePoint = false;

		double x = e.getX() / scaleX;
		e.getY();

		// Halbe Iterations Distanz
		double dist = (width / (model.getIterations() - 1)) / 2;

		// Click on Point
		tempP = null;
		if (pointList != null) {
			for (Point p : pointList) {
				if (x >= p.getX() - dist * 2 && x <= p.getX() + dist * 2) {
					if (e.getButton() == MouseEvent.BUTTON3) {
						tempP = p;
						deletePoint = true;
					} else {
						pointDrag = true;
						tempP = p;
					}
				}
			}
			// New Point
			if (!pointDrag && e.getButton() != MouseEvent.BUTTON3 && x != 0 && x != width) {
				for (int i = 0; i < pointList.size() && !added; i++) {
					if (x < pointList.get(i).getX() - dist) {
						// double p1, p2 um location der points zu bestimmen
						double p1 = pointList.get(i - 1).getX();
						double p2 = pointList.get(i).getX();
						// Punkte hinzuf�gen, je nachdem ob true oder false
						if (pointList.get(i - 1).getY() != 0 && pointList.get(i).getY() != 0) {
							pointList.add(i, new Point((int) ((x + p2) / 2 + dist), (int) height - 1));
							pointList.add(i, new Point((int) ((x + p2) / 2 + dist), 0));
							pointList.add(i, new Point((int) ((x + p1) / 2 - dist), 0));
							pointList.add(i, new Point((int) ((x + p1) / 2 - dist), (int) height - 1));
							added = true;
						} else if (pointList.get(i - 1).getY() == 0 && pointList.get(i).getY() == 0) {
							pointList.add(i, new Point((int) ((x + p2) / 2 + dist), 0));
							pointList.add(i, new Point((int) ((x + p2) / 2 + dist), (int) height - 1));
							pointList.add(i, new Point((int) ((x + p1) / 2 - dist), (int) height - 1));
							pointList.add(i, new Point((int) ((x + p1) / 2 - dist), 0));
							added = true;
						}
					}
				}
			}

			// Delete a Point
			if (deletePoint && tempP.getX() != 0
					&& (tempP.getX() != this.getWidth() / scaleX || tempP != pointList.getLast())) {
				int i = pointList.indexOf(tempP);
				if (tempP.getY() == 0) {
					pointList.remove(i);
					pointList.remove(i - 1);
					pointList.remove(i - 2);
					pointList.remove(i - 3);
				}
				if (tempP.getY() == height - 1) {
					pointList.remove(i + 2);
					pointList.remove(i + 1);
					pointList.remove(i);
					pointList.remove(i - 1);
				}
			}

			repaint();
		}
		// TODO Siwtch pressed zeugs hier hin
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		if (pointDrag) {
			pointDrag = false;
			tempP = null;
		}
		/**
		 * reset the dragInformation.
		 */
		dragInformation = "";
		repaint();
	}

	/**
	 * When the Component is Resized.
	 * 
	 * @param e
	 *            ComponentEvent
	 */
	public void componentResized(ComponentEvent e) {
		// Wenn ein anderes Element genommen wird
		if (init) {
			init = false;
			// for scale on the first initialisation
			if (width == -1 && height == -1) {
				width = this.getWidth();
				height = this.getHeight();
			}

			scaleX = this.getWidth() / width;
			scaleY = this.getHeight() / height;
		}

		// Scale
		scaleX = this.getWidth() / width;
		scaleY = this.getHeight() / height;
		repaint();
	}

	@Override
	public void componentHidden(ComponentEvent e) {
	}

	@Override
	public void componentMoved(ComponentEvent e) {

	}

	@Override
	public void componentShown(ComponentEvent e) {
	}

	/**
	 * Empty the Graph.
	 */
	public void empty() {
		pointList = null;
		tempElements = null;
		arrayOfFloats = null;
		arrayOfBooleans = null;
		isSwitch = false;
		isElement = false;
		repaint();
	}

	/**
	 * Resets the Points for the Element.
	 */
	public void reset() {
		pointList.removeAll(pointList);
		pointList.addFirst(new Point(0, 0));
		pointList.addLast(new Point((int) (this.getWidth() / scaleX), 0));
		repaint();
	}

	/**
	 * converts the number to fit the canvas.
	 * 
	 * @param d
	 *            the number to convert
	 * @return the converted number
	 */
	public double convertToCanvasY(float d) {
		return (height - (d * (height / maximum)));
	}

	/**
	 * converts the number to fit the value.
	 * 
	 * @param d
	 *            the number to convert
	 * @return the converted number
	 */
	public float convertToValueY(double d) {
		return (float) Math.round(((height - (height * (d / height))) / (height / maximum)) * 10) / 10;
	}

	/**
	 * Visualize the HolonElement on the Graph.
	 * 
	 * @param selectedElement
	 *            which should be visualized
	 */
	public void repaintWithNewElement(ArrayList<HolonElement> selectedElement) {
		arrayOfFloats = selectedElement.get(selectedElement.size() - 1).getEnergyAt();
		tempElements = selectedElement;
		pointList = selectedElement.get(selectedElement.size() - 1).getGraphPoints();
		isSwitch = false;
		isElement = true;
		maximum = selectedElement.get(selectedElement.size() - 1).getEnergy();
		// First time clicked on the Element
		if (pointList.isEmpty()) {
			pointList.addFirst(new Point(0, 0));
			pointList.addLast(new Point((int) (this.getWidth() / scaleX), 0));
		}
		repaint();
	}

	/**
	 * Visualize the Switch on the Graph.
	 * 
	 * @param s
	 *            which should be visualized
	 */
	public void repaintWithNewSwitch(HolonSwitch s) {
		arrayOfBooleans = s.getActiveAt();
		pointList = s.getGraphPoints();
		isSwitch = true;
		isElement = false;
		// First time clicked on the Element
		if (pointList.isEmpty()) {
			pointList.addFirst(new Point(0, 0));
			pointList.addLast(new Point((int) (width), 0));
		}
		repaint();
	}

	/**
	 * Build a Curve for the Graph.
	 * 
	 * @param p1
	 *            startpoint
	 * @param p2
	 *            endpoint
	 * 
	 * @return the CubicCurve2D for the Graph
	 */
	public CubicCurve2D buildCurve(Point p1, Point p2) {
		x1 = (int) p1.getX();
		y1 = (int) p1.getY();
		x2 = (int) p2.getX();
		y2 = (int) p2.getY();

		// calculate the controllpoints
		ctrlx1 = (int) p1.getX() + ((int) p2.getX() - (int) p1.getX()) / 2;
		ctrlx2 = (int) p2.getX() - ((int) p2.getX() - (int) p1.getX()) / 2;
		if (y1 < y2) {
			ctrly1 = (int) p1.getY() + ((int) p2.getY() - (int) p1.getY()) / 10;
			ctrly2 = (int) p2.getY() - ((int) p2.getY() - (int) p1.getY()) / 10;
		} else {
			ctrly1 = (int) p1.getY() - ((int) p1.getY() - (int) p2.getY()) / 10;
			ctrly2 = (int) p2.getY() + ((int) p1.getY() - (int) p2.getY()) / 10;
		}

		// set the curve
		c.setCurve(x1 * scaleX, y1 * scaleY, ctrlx1 * scaleX, ctrly1 * scaleY, ctrlx2 * scaleX, ctrly2 * scaleY,
				x2 * scaleX, y2 * scaleY);
		return c;
	}

	/**
	 * Fills the Arrays with booleans.
	 */
	public void fillArrayofBooleans() {
		for (int i = 0; i < arrayOfBooleans.length; i++) {
			int t = (int) getYValueAt2((int) (i * width / (model.getIterations() - 1)));
			if (t == 0) {
				arrayOfBooleans[i] = true;
			} else {
				arrayOfBooleans[i] = false;
			}

		}
	}

	/**
	 * Fills the Arrays of each HolonElement.
	 */
	@SuppressWarnings("unchecked")
	public void fillArrayofValue() {
		for (HolonElement he : tempElements) {
			maximum = he.getEnergy();
			he.setGraphPoints((LinkedList<Point>) pointList.clone());
			for (int i = 0; i < arrayOfFloats.length; i++) {
				he.getEnergyAt()[i] = convertToValueY(getYValueAt2((int) (i * width / (model.getIterations() - 1))));
			}
			arrayOfFloats = he.getEnergyAt();
		}
	}

	/**
	 * Get the Y Value at the x Coordination.
	 * 
	 * @param xVal
	 *            the x value for the y value
	 * @return y, the value at x
	 */
	public float getYValueAt(int xVal) {
		for (int i = 0; i < pointList.size() - 1; i++) {
			// get the Points
			if (xVal <= pointList.get(i + 1).getX()) {
				// Curve erstellen
				Line2D l1 = new Line2D.Double(pointList.get(i).getX(), pointList.get(i).getY(),
						pointList.get(i + 1).getX(), pointList.get(i + 1).getY());
				Line2D l2 = new Line2D.Double(xVal, 0, xVal, height);
				return getIntersectionPoint(l1, l2);
			}
		}
		return 0;
	}

	/**
	 * Get y value at the x Coordination via curves.
	 * 
	 * @param xVal
	 *            the x value for the y value
	 * @return y value at x
	 */
	public float getYValueAt2(int xVal) {
		for (int i = 0; i < pointList.size() - 1; i++) {
			// get the Points
			if (xVal >= pointList.get(i).getX()) {
				// Curve erstellen
				c = buildCurve(pointList.get(i), pointList.get(i + 1));
				c.subdivide(cl, cr);
				// Teil der Kurve aussuchen
				if (cl.getX1() <= xVal * scaleX && cl.getX2() > xVal * scaleX) {
					c = cl;
					// Kurve Links von "unten"
					if (pointList.get(i).getY() >= pointList.get(i + 1).getY()) {
						for (float j = (float) (height - 1); j >= 0; j -= 0.1f) {
							if (c.contains(xVal * scaleX, j * scaleY)) {
								return (float) (j);
							}
						}
					} else {// Kurve Links von "oben"
						for (float j = 0; j < height; j += 0.1f) {
							if (c.contains(xVal * scaleX, j * scaleY)) {
								return (float) (j);
							}
						}
					}
				} else {
					c = cr;
					// Kurve Links von "unten"
					if (pointList.get(i).getY() >= pointList.get(i + 1).getY()) {
						for (float j = 0; j < height; j += 0.1f) {
							if (c.contains(xVal * scaleX, j * scaleY)) {
								return (float) (j);
							}
						}
					} else {// Kurve Links von "oben"
						for (float j = (float) (height - 1); j >= 0; j -= 0.1f) {
							if (c.contains(xVal * scaleX, j * scaleY)) {
								return (float) (j);
							}
						}
					}
				}
			}
		}
		return getYValueAt(xVal);
	}

	/**
	 * Get the Intersection Point of 2 Lines.
	 * 
	 * @param l1
	 *            the first Line
	 * @param l2
	 *            the second Line
	 * 
	 * @return The Intersection Point
	 */
	public float getIntersectionPoint(Line2D l1, Line2D l2) {
		if (!l1.intersectsLine(l2)) {
			return 0;// null;
		}
		double px = l1.getX1(), py = l1.getY1(), rx = l1.getX2() - px, ry = l1.getY2() - py;
		double qx = l2.getX1(), qy = l2.getY1(), sx = l2.getX2() - qx, sy = l2.getY2() - qy;

		double det = sx * ry - sy * rx;
		if (det == 0) {
			return 0;// null;
		} else {
			double z = (sx * (qy - py) + sy * (px - qx)) / det;
			if (z < 0 || z > 1) {
				return 0;// new Point(0, 0); // intersection at end point!
			}
			return (float) (py + z * ry);// new Point((int) (px + z * rx), (int)
											// (py + z * ry));
		}
	} // end intersection line-line

}