/*
* Copyright (c) 2005, Graph Builder
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Graph Builder nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import javax.swing.*;
import java.awt.event.*;
import java.awt.geom.*;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.RenderingHints;

import com.graphbuilder.curve.*;
import com.graphbuilder.geom.*;

public class ControlPathPanel extends JPanel implements MouseListener, MouseMotionListener {

	private ControlPath cp = new ControlPath();
	private PointFactory pointFactory = new PointFactory();

	private int controlPointSize = 10;
	private Color controlPointColor = Color.red;
	private Color controlLineColor = Color.blue;
	private Color insertPointColor = Color.red;
	private Color closestPointColor = Color.black;
	private Color pointNumberColor = Color.black;

	private int grabDistanceThreshold = 10;
	private boolean grabbing = false;

	private int closestPointIndex1 = -1;
	private int closestPointIndex2 = -1;
	private int closestPointIndex = -1;

	private double mouseX = 0;
	private double mouseY = 0;
	private double deltaX = 0;
	private double deltaY = 0;

	private boolean showControlPoints = true;
	private boolean showControlLine = true;
	private boolean showPointNumbers = false;
	private boolean showPointLocations = false;
	private boolean antialiasCurves = false;
	private boolean showCurves = true;

	private ShapeMultiPath multiPath = new ShapeMultiPath();

	public ControlPathPanel() {
		super();
		addMouseListener(this);
		addMouseMotionListener(this);
		setBackground(Color.white);

		//cp.addCurve(new BezierCurve(cp, new GroupIterator("0:n-1", 0)));
		cp.addCurve(new BSpline(cp, new GroupIterator("0:n-1", 0)));
	}

	public void setShowControlLine(boolean b) {
		showControlLine = b;
	}

	public void setShowControlPoints(boolean b) {
		showControlPoints = b;
	}

	public boolean getShowControlLine() {
		return showControlLine;
	}

	public boolean getShowControlPoints() {
		return showControlPoints;
	}

	public boolean getAntialiasCurves() {
		return antialiasCurves;
	}

	public void setAntialiasCurves(boolean b) {
		antialiasCurves = b;
	}

	public boolean getShowPointNumbers() {
		return showPointNumbers;
	}

	public void setShowPointNumbers(boolean b) {
		showPointNumbers = b;
	}

	public ControlPath getControlPath() {
		return cp;
	}

	public void setControlPath(ControlPath cp) {
		this.cp = cp;
	}

	public double getFlatness() {
		return multiPath.getFlatness();
	}

	public void setFlatness(double f) {
		multiPath.setFlatness(f);
	}

	public void setShowPointLocations(boolean b) {
		showPointLocations = b;
	}

	public boolean getShowPointLocations() {
		return showPointLocations;
	}

	public void setShowCurves(boolean b) {
		showCurves = b;
	}

	public boolean getShowCurves() {
		return showCurves;
	}

	public void mousePressed(MouseEvent evt) {
		double x = evt.getX();
		double y = evt.getY();

		if (evt.getClickCount() == 2) {
			int numPoints = cp.numPoints();

			if (closestPointIndex >= 0) {
				cp.removePoint(closestPointIndex);
				closestPointIndex = -1;
				closestPointIndex1 = -1;
				closestPointIndex2 = -1;
			}
			else {
				Point p = pointFactory.createPoint(x, y);

				if (cp.numPoints() == 0) {
					closestPointIndex = cp.numPoints();
					cp.addPoint(p);
				}
				else {
					if (closestPointIndex2 < 0) {
						if (closestPointIndex1 == 0) {
							cp.insertPoint(p, 0);
							closestPointIndex = 0;
						}
						else if (closestPointIndex1 > 0) {
							closestPointIndex = cp.numPoints();
							cp.addPoint(p);
						}
					}
					else {
						cp.insertPoint(p, closestPointIndex1);
						closestPointIndex = closestPointIndex1;
					}
				}
			}

			if (cp.numPoints() != numPoints) {
				for (int i = 0; i < cp.numCurves(); i++) {
					Curve c = cp.getCurve(i);
					GroupIterator gi = c.getGroupIterator();
					c.setGroupIterator(new GroupIterator(gi.getControlString(), cp.numPoints()));
				}
			}
		}

		if (closestPointIndex >= 0) {
			double[] loc = cp.getPoint(closestPointIndex).getLocation();
			deltaX = loc[0] - x;
			deltaY = loc[1] - y;
			grabbing = true;
		}

		repaint();
	}

	public void mouseReleased(MouseEvent evt) {
		grabbing = false;
	}

	public void mouseClicked(MouseEvent evt) {}
	public void mouseEntered(MouseEvent evt) {}
	public void mouseExited(MouseEvent evt) {}

	public void mouseDragged(MouseEvent evt) {
		closestPointIndex1 = -1;
		closestPointIndex2 = -1;

		if (grabbing) {
			double x = evt.getX();
			double y = evt.getY();

			double[] loc = cp.getPoint(closestPointIndex).getLocation();
			loc[0] = x + deltaX;
			loc[1] = y + deltaY;
		}

		repaint();
	}

	public void mouseMoved(MouseEvent evt) {
		mouseX = evt.getX();
		mouseY = evt.getY();

		findClosestLineSegment(mouseX, mouseY);
		findClosestPoint(mouseX, mouseY);
		repaint();
	}

	private void findClosestPoint(double x, double y) {
		double dist = Double.MAX_VALUE;

		for (int i = 0; i < cp.numPoints(); i++) {
			double[] loc = cp.getPoint(i).getLocation();
			double dx = x - loc[0];
			double dy = y - loc[1];
			double d = dx * dx + dy * dy;
			if (d < dist) {
				dist = d;
				closestPointIndex = i;
			}
		}

		if (Math.sqrt(dist) > controlPointSize / 2 + grabDistanceThreshold) {
			closestPointIndex = -1;
		}
	}

	private void findClosestLineSegment(double x, double y) {
		if (cp.numPoints() == 0) {
			closestPointIndex1 = -1;
			closestPointIndex2 = -1;
			return;
		}
		if (cp.numPoints() == 1) {
			closestPointIndex1 = 0;
			closestPointIndex2 = -1;
			return;
		}

		int index = 0;
		double dist = Double.MAX_VALUE;
		double angle = Math.PI;

		for (int i = 1; i < cp.numPoints(); i++) {
			Point p1 = cp.getPoint(i-1);
			Point p2 = cp.getPoint(i);
			double[] loc1 = p1.getLocation();
			double[] loc2 = p2.getLocation();
			double d = Geom.ptSegDistSq(loc1[0], loc1[1], loc2[0], loc2[1], x, y, null);

			if (d < dist) {
				dist = d;
				index = i;
			}
			else if (d == dist && index + 1 == i) {
				double[] loc3 = cp.getPoint(i-2).getLocation();

				double ox = loc1[0];
				double oy = loc1[1];

				double a = Geom.getAngle(ox, oy, x, y);
				double b = Geom.getAngle(ox, oy, loc3[0], loc3[1]);

				double dab = a - b;
				if (dab < 0) dab = -dab;
				if (dab > Math.PI) dab = 2 * Math.PI - dab;

				double c = Geom.getAngle(ox, oy, loc2[0], loc2[1]);
				double dac = a - c;
				if (dac < 0) dac = -dac;
				if (dac > Math.PI) dac = 2 * Math.PI - dac;

				if (dac < dab)
					index = i;
			}
		}

		boolean done = false;

		if (index == 1) {
			double[] loc1 = cp.getPoint(0).getLocation();
			double[] loc2 = cp.getPoint(1).getLocation();
			double a = Geom.getAngle(loc1[0], loc1[1], x, y);
			double b = Geom.getAngle(loc1[0], loc1[1], loc2[0], loc2[1]);

			double dab = a - b;
			if (dab < 0) dab = -dab;
			if (dab > Math.PI) dab = 2 * Math.PI - dab;

			if (dab > Math.PI / 2) {
				closestPointIndex1 = 0;
				closestPointIndex2 = -1;
				done = true;
			}
		}

		if (index == cp.numPoints() - 1) {
			double[] loc1 = cp.getPoint(index).getLocation();
			double[] loc2 = cp.getPoint(index-1).getLocation();
			double a = Geom.getAngle(loc1[0], loc1[1], x, y);
			double b = Geom.getAngle(loc1[0], loc1[1], loc2[0], loc2[1]);

			double dab = a - b;
			if (dab < 0) dab = -dab;
			if (dab > Math.PI) dab = 2 * Math.PI - dab;

			if (dab > Math.PI / 2) {
				closestPointIndex1 = index;
				closestPointIndex2 = -1;
				done = true;
			}
		}

		if (!done) {
			closestPointIndex1 = index;
			closestPointIndex2 = index - 1;
		}
	}

	public void paint(Graphics _g) {
		super.paint(_g);

		if (cp.numPoints() == 0) return;

		Graphics2D g = (Graphics2D) _g;

		if (showCurves) {
			//long time = System.currentTimeMillis();
			//for (int x = 0; x < 100; x++) {
			multiPath.setNumPoints(0);
			for (int i = 0; i < cp.numCurves(); i++)
				cp.getCurve(i).appendTo(multiPath);
			//}
			//long dt = System.currentTimeMillis() - time;
			//System.out.println(dt);

			if (antialiasCurves)
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

			g.draw(multiPath);
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		}

		if (showControlLine) {
			Line2D.Double line = new Line2D.Double();
			g.setColor(controlLineColor);
			Point lastPoint = cp.getPoint(0);

			for (int i = 1; i < cp.numPoints(); i++) {
				Point p = cp.getPoint(i);
				double[] loc = p.getLocation();
				double[] old = lastPoint.getLocation();
				line.setLine(old[0], old[1], loc[0], loc[1]);
				g.draw(line);
				lastPoint = p;
			}

			g.setColor(insertPointColor);
			if (closestPointIndex1 >= 0) {
				double[] loc = cp.getPoint(closestPointIndex1).getLocation();
				line.setLine(mouseX, mouseY, loc[0], loc[1]);
				g.draw(line);
			}

			if (closestPointIndex2 >= 0) {
				double[] loc = cp.getPoint(closestPointIndex2).getLocation();
				line.setLine(mouseX, mouseY, loc[0], loc[1]);
				g.draw(line);
			}
		}


		if (showControlPoints) {
			Rectangle2D.Double rect = new Rectangle2D.Double();
			g.setColor(controlPointColor);

			for (int i = 0; i < cp.numPoints(); i++) {
				Point p = cp.getPoint(i);
				double[] loc = p.getLocation();
				rect.setRect(loc[0] - controlPointSize / 2, loc[1] - controlPointSize / 2, controlPointSize, controlPointSize);
				g.fill(rect);
			}

			if (closestPointIndex >= 0) {
				g.setColor(closestPointColor);
				double[] loc = cp.getPoint(closestPointIndex).getLocation();
				rect.setRect(loc[0] - controlPointSize / 2, loc[1] - controlPointSize / 2, controlPointSize, controlPointSize);
				g.fill(rect);
			}

			if (showPointNumbers || showPointLocations) {
				g.setColor(pointNumberColor);

				for (int i = 0; i < cp.numPoints(); i++) {
					double[] loc = cp.getPoint(i).getLocation();
					String s = "";
					if (showPointNumbers) {
						s = String.valueOf(i) + " ";
					}
					if (showPointLocations) {
						s += "(" + ((int) loc[0]) + ", " + ((int) loc[1]) + ")";
					}
					g.drawString(s, (int) loc[0], (int) loc[1] - controlPointSize);
				}
			}
		}
	}
}
