/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://github.com/mickleness/pumpernickel/raw/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.plaf;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.event.MouseInputAdapter;

import com.pump.geom.CurvedPolyline;
import com.pump.swing.ShapeCreationPanel;
import com.pump.swing.ShapeCreationPanel.DataModel;
import com.pump.swing.ShapeCreationPanel.DataModelListener;
import com.pump.swing.ShapeCreationPanel.Selection;

/**
 * A {@link ShapeCreationUI} to create a series of {@link CurvedPolyline}
 * shapes.
 * 
 * @see com.pump.geom.CurvedPolyline
 */
public class CurvedPolylineCreationUI extends ShapeCreationUI {

	/** Maps to an array of CurvedPolylines for editing. */
	protected static final String SHAPES_MIRROR = CurvedPolylineCreationUI.class
			.getName() + ".shapes-mirror";

	private static Set<Thread> changingDataModel = new HashSet<Thread>();

	PropertyChangeListener myPropertyListener = new PropertyChangeListener() {
		public void propertyChange(PropertyChangeEvent evt) {
			ShapeCreationPanel scp = (ShapeCreationPanel) evt.getSource();
			if (ShapeCreationPanel.MODE_KEY.equals(evt.getPropertyName())) {
				if (ShapeCreationPanel.MODE_CREATE.equals(evt.getNewValue())) {
					scp.getSelectionModel().select(-1, -1, null);
				} else if (ShapeCreationPanel.DATA_MODEL_KEY.equals(evt
						.getPropertyName())) {
					((DataModel) evt.getOldValue())
							.removeListener(myDataModelListener);
					invalidateMirror(scp);
					((DataModel) evt.getNewValue())
							.addListener(myDataModelListener);
				}
			}
			;
		}
	};

	MouseInputAdapter mouseListener = new MouseInputAdapter() {
		int lastUntransformedX, lastUntransformedY;

		@Override
		public void mousePressed(MouseEvent evt) {
			ShapeCreationPanel scp = (ShapeCreationPanel) evt.getComponent();
			scp.requestFocus();
			lastUntransformedX = evt.getX();
			lastUntransformedY = evt.getY();
			try {
				AffineTransform tx = scp.getTransform().createInverse();
				Point2D mouseLoc = new Point2D.Float(evt.getX(), evt.getY());
				Point2D abstractLoc = tx.transform(mouseLoc, null);
				boolean isCreating = ShapeCreationPanel.MODE_CREATE.equals(scp
						.getMode());
				if (isCreating) {
					if (evt.getClickCount() > 1) {
						scp.setMode(ShapeCreationPanel.MODE_DEFAULT);
						return;
					}

					int shapeIndex = scp.getSelectionModel().getSelection()
							.getShapeIndex();
					if (shapeIndex == -1) {
						CurvedPolyline newShape = new CurvedPolyline();
						newShape.addPoint((float) abstractLoc.getX(),
								(float) abstractLoc.getY());
						shapeIndex = scp.getDataModel().addShape(newShape);
						scp.getSelectionModel().select(shapeIndex, 0, null);
					} else {
						CurvedPolyline currentShape = getMirror(scp)[shapeIndex];
						changingDataModel.add(Thread.currentThread());
						try {
							currentShape.setPoint(
									currentShape.getPointCount() - 1,
									abstractLoc.getX(), abstractLoc.getY());
							scp.getDataModel().setShape(shapeIndex,
									currentShape);
							scp.getSelectionModel().select(shapeIndex,
									currentShape.getPointCount() - 1, null);
						} finally {
							changingDataModel.remove(Thread.currentThread());
						}
					}
				} else {
					Selection newSelection = getSelection(evt);
					scp.getSelectionModel().select(newSelection);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		@Override
		public void mouseReleased(MouseEvent e) {
		}

		@Override
		public void mouseDragged(MouseEvent evt) {
			ShapeCreationPanel scp = (ShapeCreationPanel) evt.getComponent();
			try {
				AffineTransform tx = scp.getTransform().createInverse();
				Point2D mouseLoc = new Point2D.Float(evt.getX(), evt.getY());
				Point2D abstractLoc = tx.transform(mouseLoc, null);

				boolean isCreating = ShapeCreationPanel.MODE_CREATE.equals(scp
						.getMode());
				if (!isCreating) {
					Selection selection = scp.getSelectionModel()
							.getSelection();
					if (selection.getShapeIndex() != -1) {
						if (selection.getNodeIndex() != -1) {
							CurvedPolyline shape = getMirror(scp)[selection
									.getShapeIndex()];
							shape.setPoint(selection.getNodeIndex(),
									abstractLoc.getX(), abstractLoc.getY());
							scp.getDataModel().setShape(
									selection.getShapeIndex(), shape);
						} else {
							float dx = evt.getX() - lastUntransformedX;
							float dy = evt.getY() - lastUntransformedY;
							nudge(scp, dx, dy);
						}
						return;
					}
				}
				mouseMoved(evt);
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				lastUntransformedX = evt.getX();
				lastUntransformedY = evt.getY();
			}
		}

		@Override
		public void mouseMoved(MouseEvent evt) {
			ShapeCreationPanel scp = (ShapeCreationPanel) evt.getComponent();
			try {
				AffineTransform tx = scp.getTransform().createInverse();
				Point2D mouseLoc = new Point2D.Float(evt.getX(), evt.getY());
				Point2D abstractLoc = tx.transform(mouseLoc, null);
				boolean isCreating = ShapeCreationPanel.MODE_CREATE.equals(scp
						.getMode());
				int shapeIndex = scp.getSelectionModel().getSelection()
						.getShapeIndex();
				if (isCreating && shapeIndex >= 0) {
					CurvedPolyline currentShape = getMirror(scp)[shapeIndex];

					int nodeIndex = scp.getSelectionModel().getSelection()
							.getNodeIndex();

					changingDataModel.add(Thread.currentThread());
					try {
						if (nodeIndex == currentShape.getPointCount() - 1) {
							currentShape.addPoint(abstractLoc.getX(),
									abstractLoc.getY());
						} else {
							currentShape.setPoint(
									currentShape.getPointCount() - 1,
									abstractLoc.getX(), abstractLoc.getY());
						}

						scp.getDataModel().setShape(shapeIndex, currentShape);
					} finally {
						changingDataModel.remove(Thread.currentThread());
					}
				} else if (!isCreating) {
					Selection newIndication = getSelection(evt);
					scp.getSelectionModel().indicate(newIndication);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		private Selection getSelection(MouseEvent evt)
				throws NoninvertibleTransformException {
			ShapeCreationPanel scp = (ShapeCreationPanel) evt.getComponent();
			AffineTransform tx = scp.getTransform();
			double r = scp.getHandleSize() / 2;
			Point2D mouseLoc = new Point2D.Float(evt.getX(), evt.getY());
			CurvedPolyline[] shapes = getMirror(scp);
			for (int shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
				if (scp.getHandlesActive().supports(scp, shapeIndex)) {
					for (int b = shapes[shapeIndex].getPointCount() - 1; b >= 0; b--) {
						Point2D p = tx.transform(
								shapes[shapeIndex].getPoint(b), null);
						Ellipse2D e = new Ellipse2D.Double(p.getX() - r,
								p.getY() - r, 2 * r, 2 * r);
						if (e.contains(mouseLoc))
							return new Selection(shapeIndex, b, null);
					}
				}
			}
			return getSelectedShape(scp, evt.getPoint());
		}
	};

	DataModelListener myDataModelListener = new DataModelListener() {

		public void shapeAdded(ShapeCreationPanel shapePanel, int shapeIndex,
				Shape shape) {
			invalidateMirror(shapePanel);
		}

		public void shapeRemoved(ShapeCreationPanel shapePanel, int shapeIndex,
				Shape shape) {
			invalidateMirror(shapePanel);
		}

		public void shapeChanged(ShapeCreationPanel shapePanel, int shapeIndex,
				Shape shape) {
			invalidateMirror(shapePanel);
		}

	};

	protected void invalidateMirror(ShapeCreationPanel scp) {
		if (!changingDataModel.contains(Thread.currentThread())) {
			scp.putClientProperty(SHAPES_MIRROR, null);
		}
	}

	@Override
	protected Shape validate(Shape shape) {
		if (shape instanceof CurvedPolyline)
			return shape;
		float[] coords = new float[6];
		CurvedPolyline copy = new CurvedPolyline();
		PathIterator iter = shape.getPathIterator(null);
		while (!iter.isDone()) {
			int k = iter.currentSegment(coords);
			if (k == PathIterator.SEG_MOVETO || k == PathIterator.SEG_LINETO) {
				copy.addPoint(coords[0], coords[1]);
			} else if (k == PathIterator.SEG_QUADTO) {
				copy.addPoint(coords[2], coords[3]);
			} else if (k == PathIterator.SEG_CUBICTO) {
				copy.addPoint(coords[4], coords[5]);
			}
			iter.next();
		}
		return copy;
	}

	protected CurvedPolyline[] getMirror(ShapeCreationPanel scp) {
		CurvedPolyline[] paths = (CurvedPolyline[]) scp
				.getClientProperty(SHAPES_MIRROR);
		if (paths == null) {
			List<CurvedPolyline> mirrorList = new ArrayList<CurvedPolyline>();
			Shape[] dataModel = scp.getDataModel().getShapes();
			for (int a = 0; a < dataModel.length; a++) {
				mirrorList.add((CurvedPolyline) validate(dataModel[a]));
			}
			paths = mirrorList.toArray(new CurvedPolyline[mirrorList.size()]);
			scp.putClientProperty(SHAPES_MIRROR, paths);
		}
		return paths;
	}

	@Override
	public void installUI(JComponent c) {
		super.installUI(c);

		ShapeCreationPanel scp = (ShapeCreationPanel) c;
		scp.getDataModel().addListener(myDataModelListener);
		scp.addPropertyChangeListener(myPropertyListener);
		scp.addMouseListener(mouseListener);
		scp.addMouseMotionListener(mouseListener);
		invalidateMirror(scp);
	}

	@Override
	public void uninstallUI(JComponent c) {
		super.uninstallUI(c);

		ShapeCreationPanel scp = (ShapeCreationPanel) c;
		scp.getDataModel().removeListener(myDataModelListener);
		scp.removePropertyChangeListener(myPropertyListener);
		scp.removeMouseListener(mouseListener);
		scp.removeMouseMotionListener(mouseListener);
	}

	@Override
	protected void paintControls(Graphics2D g0, ShapeCreationPanel scp) {
		AffineTransform tx = scp.getTransform();
		Graphics2D g = (Graphics2D) g0.create();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
				RenderingHints.VALUE_STROKE_PURE);
		g.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND,
				BasicStroke.JOIN_ROUND));
		float r = (float) (scp.getHandleSize()) / 2f;
		Selection selection = scp.getSelectionModel().getSelection();
		Selection indication = scp.getSelectionModel().getIndication();
		Shape[] shapes = scp.getDataModel().getShapes();
		float[] coords = new float[6];
		Ellipse2D ellipse = new Ellipse2D.Float();
		for (int shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
			if (scp.getHandlesActive().supports(scp, shapeIndex)) {
				PathIterator iter = shapes[shapeIndex].getPathIterator(tx);
				int nodeCtr = -1;
				while (!iter.isDone()) {
					int k = iter.currentSegment(coords);
					float x, y;
					if (k == PathIterator.SEG_MOVETO
							|| k == PathIterator.SEG_LINETO) {
						x = coords[0];
						y = coords[1];
						nodeCtr++;
					} else if (k == PathIterator.SEG_QUADTO) {
						x = coords[2];
						y = coords[3];
						nodeCtr++;
					} else if (k == PathIterator.SEG_CUBICTO) {
						x = coords[4];
						y = coords[5];
						nodeCtr++;
					} else if (k == PathIterator.SEG_CLOSE) {
						x = -1;
						y = -1;
					} else {
						throw new RuntimeException("unexpected segment type: "
								+ k);
					}
					g.setColor(Color.white);
					if (selection != null
							&& selection.getShapeIndex() == shapeIndex
							&& selection.getNodeIndex() == nodeCtr) {
						g.setColor(Color.darkGray);
					} else if (indication != null
							&& indication.getShapeIndex() == shapeIndex
							&& indication.getNodeIndex() == nodeCtr) {
						g.setColor(Color.gray);
					}
					ellipse.setFrame(x - r, y - r, 2 * r, 2 * r);
					g.fill(ellipse);
					g.setColor(Color.black);
					g.draw(ellipse);

					iter.next();
				}
			}
		}
		g.dispose();
	}

}