/**
 * 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://raw.githubusercontent.com/mickleness/pumpernickel/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.diagram.plaf;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.plaf.ComponentUI;

import com.pump.diagram.Box;
import com.pump.diagram.BoxContainer;
import com.pump.diagram.Connector;
import com.pump.diagram.Relationship;
import com.pump.diagram.swing.BoxContainerPanel;
import com.pump.geom.ShapeBounds;
import com.pump.geom.TransformUtils;
import com.pump.plaf.UIEffect;
import com.pump.swing.ContextualMenuHelper;
import com.pump.util.ObservableProperties;
import com.pump.util.ObservableProperties.Key;
import com.pump.util.ObservableProperties.NonNullBoundsChecker;

public class BoxContainerPanelUI extends ComponentUI {

	protected static final String KEY_PLAF_CONTEXT = BoxContainerPanelUI.class
			.getName() + "#plafContext";

	public static final String KEY_TARGET_HANDLE_OPACITY = BoxContainerPanelUI.class
			.getName() + "#handleTargetOpacity";
	public static final String KEY_REAL_HANDLE_OPACITY = BoxContainerPanelUI.class
			.getName() + "#handleRealOpacity";

	public static ComponentUI createUI(JComponent c) {
		return new RightAngleBoxContainerPanelUI();
	}

	static final float handleRadius = 5;

	static class ScalingHandles {
		static class Handle extends AbstractHandle {
			int position;

			GeneralPath shape = null;
			boolean priority;
			String name;

			Handle(int position, boolean priority, String name) {
				this.position = position;
				this.priority = priority;
				this.name = name;
			}

			public Cursor getCursor() {
				switch (position) {
				case SwingConstants.NORTH:
					return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
				case SwingConstants.SOUTH:
					return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
				case SwingConstants.EAST:
					return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
				case SwingConstants.WEST:
					return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
				case SwingConstants.NORTH_EAST:
					return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
				case SwingConstants.SOUTH_EAST:
					return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
				case SwingConstants.NORTH_WEST:
					return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
				case SwingConstants.SOUTH_WEST:
					return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
				}
				return Cursor.getDefaultCursor();
			}

			@Override
			public String toString() {
				return name;
			}

			@Override
			public Shape getShape() {
				return shape;
			}

			public void defineShape(Rectangle r) {
				shape = new GeneralPath();
				int k1 = 15;
				int k2 = 8;
				switch (position) {
				case SwingConstants.NORTH_WEST:
					shape.moveTo(r.x, r.y);
					shape.lineTo(r.x + k1, r.y);
					shape.lineTo(r.x + k1, r.y - k2);
					shape.lineTo(r.x - k2, r.y - k2);
					shape.lineTo(r.x - k2, r.y + k1);
					shape.lineTo(r.x, r.y + k1);
					shape.closePath();
					break;
				case SwingConstants.NORTH_EAST:
					shape.moveTo(r.x + r.width, r.y);
					shape.lineTo(r.x + r.width - k1, r.y);
					shape.lineTo(r.x + r.width - k1, r.y - k2);
					shape.lineTo(r.x + r.width + k2, r.y - k2);
					shape.lineTo(r.x + r.width + k2, r.y + k1);
					shape.lineTo(r.x + r.width, r.y + k1);
					shape.closePath();
					break;
				case SwingConstants.SOUTH_WEST:
					shape.moveTo(r.x, r.y + r.height);
					shape.lineTo(r.x + k1, r.y + r.height);
					shape.lineTo(r.x + k1, r.y + r.height + k2);
					shape.lineTo(r.x - k2, r.y + r.height + k2);
					shape.lineTo(r.x - k2, r.y + r.height - k1);
					shape.lineTo(r.x, r.y + r.height - k1);
					shape.closePath();
					break;
				case SwingConstants.SOUTH_EAST:
					shape.moveTo(r.x + r.width, r.y + r.height);
					shape.lineTo(r.x + r.width - k1, r.y + r.height);
					shape.lineTo(r.x + r.width - k1, r.y + r.height + k2);
					shape.lineTo(r.x + r.width + k2, r.y + r.height + k2);
					shape.lineTo(r.x + r.width + k2, r.y + r.height - k1);
					shape.lineTo(r.x + r.width, r.y + r.height - k1);
					shape.closePath();
					break;
				case SwingConstants.NORTH:
					shape.moveTo(r.x + r.width / 2 - k1 / 2, r.y);
					shape.lineTo(r.x + r.width / 2 + k1 / 2, r.y);
					shape.lineTo(r.x + r.width / 2 + k1 / 2, r.y - k2);
					shape.lineTo(r.x + r.width / 2 - k1 / 2, r.y - k2);
					break;
				case SwingConstants.SOUTH:
					shape.moveTo(r.x + r.width / 2 - k1 / 2, r.y + r.height);
					shape.lineTo(r.x + r.width / 2 + k1 / 2, r.y + r.height);
					shape.lineTo(r.x + r.width / 2 + k1 / 2, r.y + r.height
							+ k2);
					shape.lineTo(r.x + r.width / 2 - k1 / 2, r.y + r.height
							+ k2);
					break;
				case SwingConstants.EAST:
					shape.moveTo(r.x, r.y + r.height / 2 - k1 / 2);
					shape.lineTo(r.x, r.y + r.height / 2 + k1 / 2);
					shape.lineTo(r.x - k2, r.y + r.height / 2 + k1 / 2);
					shape.lineTo(r.x - k2, r.y + r.height / 2 - k1 / 2);
					break;
				case SwingConstants.WEST:
					shape.moveTo(r.x + r.width, r.y + r.height / 2 - k1 / 2);
					shape.lineTo(r.x + r.width, r.y + r.height / 2 + k1 / 2);
					shape.lineTo(r.x + r.width + k2, r.y + r.height / 2 + k1
							/ 2);
					shape.lineTo(r.x + r.width + k2, r.y + r.height / 2 - k1
							/ 2);
					break;
				default:
					throw new RuntimeException("unexpected position: "
							+ position);
				}
			}

			public void paint(BoxContainerPanel bcp, Graphics2D g) {
				g = (Graphics2D) g.create();
				g.setColor(new Color(0, 0, 0, (int) (200 * bcp.getUI()
						.getHandleOpacity(bcp, this))));
				g.fill(shape);
				g.dispose();
			}

			Map<Box, Rectangle> originalBoxBounds;
			Rectangle originalBounds;
			Rectangle baseRect;

			@Override
			public void prepareForDrag(BoxContainerPanel bcp) {
				originalBoxBounds = new HashMap<>();
				for (Box box : bcp.getSelectionModel().get()) {
					originalBoxBounds.put(box, new Rectangle(box.getBounds()));
				}
				originalBounds = getBoxBounds(bcp.getSelectionModel().get());
				switch (position) {
				case SwingConstants.NORTH:
					baseRect = new Rectangle(originalBounds.x, originalBounds.y
							+ originalBounds.height, originalBounds.width, 0);
					break;
				case SwingConstants.SOUTH:
					baseRect = new Rectangle(originalBounds.x,
							originalBounds.y, originalBounds.width, 0);
					break;
				case SwingConstants.WEST:
					baseRect = new Rectangle(originalBounds.x,
							originalBounds.y, 0, originalBounds.height);
					break;
				case SwingConstants.EAST:
					baseRect = new Rectangle(originalBounds.x
							+ originalBounds.width, originalBounds.y, 0,
							originalBounds.height);
					break;
				case SwingConstants.NORTH_EAST:
					baseRect = new Rectangle(originalBounds.x, originalBounds.y
							+ originalBounds.height, 0, 0);
					break;
				case SwingConstants.NORTH_WEST:
					baseRect = new Rectangle(originalBounds.x
							+ originalBounds.width, originalBounds.y
							+ originalBounds.height, 0, 0);
					break;
				case SwingConstants.SOUTH_EAST:
					baseRect = new Rectangle(originalBounds.x,
							originalBounds.y, 0, 0);
					break;
				case SwingConstants.SOUTH_WEST:
					baseRect = new Rectangle(originalBounds.x
							+ originalBounds.width, originalBounds.y, 0, 0);
					break;
				}
			}

			@Override
			public void drag(BoxContainerPanel bcp, MouseEvent e) {
				Rectangle newSelectionBounds = new Rectangle(baseRect);
				newSelectionBounds.add(e.getPoint());
				AffineTransform tx = TransformUtils.createAffineTransform(
						originalBounds, newSelectionBounds);

				for (Box box : bcp.getSelectionModel().get()) {
					Rectangle bounds = new Rectangle(originalBoxBounds.get(box));
					bounds = tx.createTransformedShape(bounds).getBounds();
					box.setBounds(bounds);
				}
			}

		}

		Handle topLeft = new Handle(SwingConstants.NORTH_WEST, true,
				"northwest");
		Handle top = new Handle(SwingConstants.NORTH, false, "north");
		Handle topRight = new Handle(SwingConstants.NORTH_EAST, true,
				"northeast");
		Handle left = new Handle(SwingConstants.WEST, false, "west");
		Handle right = new Handle(SwingConstants.EAST, false, "east");
		Handle bottomLeft = new Handle(SwingConstants.SOUTH_WEST, true,
				"southwest");
		Handle bottom = new Handle(SwingConstants.SOUTH, false, "south");
		Handle bottomRight = new Handle(SwingConstants.SOUTH_EAST, true,
				"southeast");
		Handle[] handles = new Handle[] { topLeft, top, topRight, left, right,
				bottomLeft, bottom, bottomRight };

		public ScalingHandles() {

		}

		private Rectangle grow(Rectangle r) {
			r.x -= 5;
			r.y -= 5;
			r.width += 10;
			r.height += 10;
			return r;
		}

		public void selectionChanged(Collection<Box> newSelection) {
			Rectangle r = getBoxBounds(newSelection);
			for (Handle handle : handles) {
				handle.defineShape(r);
			}
			// if the handles overlap because the space is confined, then drop
			// the smaller handles:
			for (Handle a : handles) {
				for (Handle b : handles) {
					if (a.priority && (!b.priority)) {
						Rectangle r1 = grow(a.shape.getBounds());
						Rectangle r2 = grow(b.shape.getBounds());
						if (r1.intersects(r2)) {
							b.shape = new GeneralPath();
						}
					}
				}
			}
		}

		public void paint(BoxContainerPanel bcp, Graphics2D g) {
			for (Handle handle : handles) {
				Graphics2D g2 = (Graphics2D) g.create();
				g2.setComposite(AlphaComposite.getInstance(
						AlphaComposite.SRC_OVER,
						bcp.getUI().getHandleOpacity(bcp, handle)));
				handle.paint(bcp, g2);
				g2.dispose();
			}
		}

		public static Rectangle getBoxBounds(Collection<Box> boxes) {
			Rectangle r = null;
			for (Box box : boxes) {
				if (r == null) {
					r = new Rectangle(box.getBounds());
				} else {
					r.add(box.getBounds());
				}
			}
			return r;
		}

	}

	public static class ConnectorHandle extends AbstractHandle {
		Shape shape;
		Connector connector;

		public ConnectorHandle(Connector connector, Point center, Shape shape) {
			if (shape == null)
				throw new NullPointerException();
			this.connector = connector;
			this.shape = shape;
			setCenter(center);
		}

		public Cursor getCursor() {
			return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
		}

		@Override
		public Shape getShape() {
			if (centerDirty) {
				Rectangle2D r = ShapeBounds.getBounds(shape);
				Point center = getCenter();
				double dx = center.x - r.getCenterX();
				double dy = center.y - r.getCenterY();
				AffineTransform tx = AffineTransform.getTranslateInstance(dx,
						dy);
				shape = tx.createTransformedShape(shape);
				centerDirty = false;
			}
			return shape;
		}

		public void setCenter(Point p) {
			properties.set(KEY_CENTER, p);
			centerDirty = true;
		}

		public Point getCenter() {
			return new Point(properties.get(KEY_CENTER));
		}

		@Override
		public void prepareForDrag(BoxContainerPanel bcp) {
			// nothing to do here
		}

		@Override
		public void drag(BoxContainerPanel bcp, MouseEvent e) {
			setCenter(e == null ? null : e.getPoint());
		}
	}

	protected abstract static class AbstractHandle {
		public static final Key<Point> KEY_CENTER = new Key<>("center",
				Point.class, new NonNullBoundsChecker());
		ObservableProperties properties = new ObservableProperties();

		transient boolean centerDirty = true;

		public AbstractHandle() {
		}

		public abstract Shape getShape();

		public abstract Cursor getCursor();

		public <T> void set(Key<T> key, T value) {
			properties.set(key, value);
		}

		public <T> T get(Key<T> key) {
			return properties.get(key);
		}

		public void addPropertyChangeListener(
				PropertyChangeListener propertyChangeListener) {
			properties.addListener(propertyChangeListener);
		}

		public void removePropertyChangeListener(
				PropertyChangeListener propertyChangeListener) {
			properties.removeListener(propertyChangeListener);
		}

		public abstract void prepareForDrag(BoxContainerPanel bcp);

		public abstract void drag(BoxContainerPanel bcp, MouseEvent e);
	}

	protected class PlafContext {
		public final BoxContainerPanel bcp;
		Map<String, Object> properties = new HashMap<>();
		Rectangle selectionRectangle = null;
		ScalingHandles handles = null;
		List<JMenuItem> contextMenuItems = new ArrayList<>();

		JMenuItem deleteConnectorItem = new JMenuItem("Delete");
		AbstractHandle clickedHandle;
		KeyListener keyListener = new KeyAdapter() {

			@Override
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
					nudge(1, 0);
				} else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
					nudge(-1, 0);
				} else if (e.getKeyCode() == KeyEvent.VK_UP) {
					nudge(0, -1);
				} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
					nudge(0, 1);
				}
			}

			private void nudge(int dx, int dy) {
				for (Box box : bcp.getSelectionModel().get()) {
					Rectangle r = box.getBounds();
					r.x += dx;
					r.y += dy;
					box.setBounds(r);
				}
			}
		};

		MouseInputAdapter mouseListener = new MouseInputAdapter() {
			Box clickedBox;
			Point clickedLoc;
			Point lastLoc;

			@Override
			public void mousePressed(MouseEvent e) {
				clickedLoc = e.getPoint();
				clickedBox = getBox(e, null);
				lastLoc = e.getPoint();
				bcp.requestFocus();

				if (clickedBox == null) {
					clickedHandle = getHandle(PlafContext.this, e);
					if (clickedHandle != null) {
						if (clickedHandle instanceof ConnectorHandle)
							contextMenuItems.add(deleteConnectorItem);
						clickedHandle.prepareForDrag(bcp);
					} else {
						clickedHandle = null;
						bcp.getSelectionModel().set(new ArrayList<Box>(0));
					}
				} else {
					if (e.isShiftDown()) {
						if (bcp.getSelectionModel().contains(clickedBox)) {
							bcp.getSelectionModel().remove(clickedBox);
						} else {
							bcp.getSelectionModel().add(clickedBox);
						}
					} else {
						bcp.getSelectionModel().set(
								Collections.singleton(clickedBox));
					}
				}
			}

			@Override
			public void mouseMoved(MouseEvent e) {
				Cursor cursor = getCursor(e);
				if (cursor == null) {
					cursor = Cursor.getDefaultCursor();
				}
				bcp.setCursor(cursor);
				bcp.putClientProperty(KEY_TARGET_HANDLE_OPACITY, 1f);
			}

			private Cursor getCursor(MouseEvent e) {
				AbstractHandle handle = getHandle(PlafContext.this, e);
				if (handle != null)
					return handle.getCursor();
				return null;
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				boolean dirty = false;
				clickedLoc = null;
				clickedBox = null;
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						contextMenuItems.remove(deleteConnectorItem);
					}
				});
				if (selectionRectangle != null) {
					dirty = true;
				}
				setSelectionRectangle(null);
				if (dirty)
					bcp.repaint();
			}

			@Override
			public void mouseExited(MouseEvent e) {
				if (clickedLoc == null) {
					bcp.setCursor(Cursor.getDefaultCursor());
					bcp.putClientProperty(KEY_TARGET_HANDLE_OPACITY, 0f);
				}
			}

			@Override
			public void mouseDragged(MouseEvent e) {
				boolean dirty = false;
				if (lastLoc != null && clickedLoc != null) {
					int dx = e.getX() - lastLoc.x;
					int dy = e.getY() - lastLoc.y;
					if (clickedBox != null) {
						Rectangle r = clickedBox.getBounds();
						r.x += dx;
						r.y += dy;
						clickedBox.setBounds(r);
						dirty = true;
					} else if (clickedHandle != null) {
						clickedHandle.drag(bcp, e);
						dirty = true;
					} else {
						Rectangle r = new Rectangle(clickedLoc.x, clickedLoc.y,
								0, 0);
						r.add(e.getPoint());
						setSelectionRectangle(r);
						dirty = true;
					}
				}
				lastLoc = e.getPoint();
				if (dirty) {
					bcp.repaint();
					bcp.getUI().refreshConnectors(bcp);
				}
			}

			protected Box getBox(MouseEvent e, Box preferredBox) {
				BoxContainer bc = bcp.getBoxContainer();

				if (preferredBox != null
						&& preferredBox.getBounds().contains(e.getPoint())) {
					return preferredBox;
				}

				for (int a = bc.getBoxes().size() - 1; a >= 0; a--) {
					Box box = bc.getBoxes().get(a);
					if (box.getBounds().contains(e.getPoint())) {
						return box;
					}
				}
				return null;
			}
		};

		protected void setSelectionRectangle(Rectangle r) {
			if (selectionRectangle != null) {
				bcp.repaint(selectionRectangle.x - 1, selectionRectangle.y - 1,
						selectionRectangle.width + 2,
						selectionRectangle.height + 2);
			}
			if (r != null) {
				bcp.repaint(r.x - 1, r.y - 1, r.width + 2, r.height + 2);
			}
			selectionRectangle = r;

			Set<Box> newSelection = new HashSet<>();
			for (int a = 0; a < bcp.getBoxContainer().getBoxes().size(); a++) {
				Box box = bcp.getBoxContainer().getBoxes().get(a);
				Rectangle boxBounds = box.getBounds();
				if (r != null && boxBounds.intersects(r)) {
					newSelection.add(box);
				}
			}
			if (r != null)
				bcp.getSelectionModel().set(newSelection);
		}

		protected void refreshScalingHandles() {
			Collection<Box> selection = bcp.getSelectionModel().get();
			if (selection.size() > 0) {
				handles = new ScalingHandles();
				handles.selectionChanged(selection);
			} else {
				handles = null;
			}
		}

		PropertyChangeListener refreshListener = new PropertyChangeListener() {
			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				if (Box.KEY_BOUNDS.matches(evt)
						|| Connector.KEY_BOX1.matches(evt)
						|| Connector.KEY_BOX2.matches(evt)
						|| Connector.KEY_CONTROL_POINT.matches(evt)
						|| Connector.KEY_RELATIONSHIP.matches(evt)) {
					refreshConnectors(bcp);
				}
			}
		};

		ChangeListener boxContainerListener = new ChangeListener() {

			Set<Box> allBoxes = new HashSet<>();
			Set<Connector> allConnectors = new HashSet<>();

			@Override
			public void stateChanged(ChangeEvent e) {
				BoxContainer bc = bcp.getBoxContainer();
				boolean dirty = false;
				if (bc != null) {
					if (e.getSource() == bc.getConnectors()) {
						for (Connector connector : bc.getConnectors()) {
							if (allConnectors.add(connector)) {
								connector
										.addPropertyChangeListener(refreshListener);
								dirty = true;
							}
						}
						Iterator<Connector> myConnectorIter = allConnectors
								.iterator();
						while (myConnectorIter.hasNext()) {
							Connector myC = myConnectorIter.next();
							if (!bc.getConnectors().contains(myC)) {
								myC.removePropertyChangeListener(refreshListener);
								myConnectorIter.remove();
								dirty = true;
							}
						}
					} else if (e.getSource() == bc.getBoxes()) {
						for (Box box : bc.getBoxes()) {
							if (allBoxes.add(box)) {
								dirty = true;
								box.addPropertyChangeListener(refreshListener);
								bindBox(box);
							}
						}
						Iterator<Box> myBoxIter = allBoxes.iterator();
						while (myBoxIter.hasNext()) {
							Box myBox = myBoxIter.next();
							if (!bc.getBoxes().contains(myBox)) {
								JComponent jc = myBox.getComponent();
								if (jc != null) {
									bcp.remove(jc);
								}
								myBox.removePropertyChangeListener(refreshListener);
								myBoxIter.remove();
								dirty = true;
							}
						}
						SwingUtilities.invokeLater(new Runnable() {
							public void run() {
								bcp.getSelectionModel().set();
							}
						});
					}
				} else {
					for (Connector connector : allConnectors) {
						connector.removePropertyChangeListener(refreshListener);
						dirty = true;
					}
					allConnectors.clear();

					for (Box box : allBoxes) {
						box.removePropertyChangeListener(refreshListener);
						dirty = true;
					}
					allBoxes.clear();
				}
				if (dirty)
					refreshConnectors(bcp);
			}

			protected void bindBox(final Box box) {
				JComponent jc = box.getComponent();
				if (jc != null) {
					bindBox(box, jc);
				}
				box.addPropertyChangeListener(new PropertyChangeListener() {
					@Override
					public void propertyChange(PropertyChangeEvent evt) {
						JComponent jc = box.getComponent();
						if (jc != null && Box.KEY_COMPONENT.matches(evt)) {
							bindBox(box, jc);
						}
					}
				});
			}

			protected void bindBox(final Box box, final JComponent jc) {
				bcp.add(jc);
				box.addPropertyChangeListener(new PropertyChangeListener() {
					@Override
					public void propertyChange(PropertyChangeEvent evt) {
						if (Box.KEY_BOUNDS.matches(evt)) {
							bcp.getLayout().layoutContainer(bcp);
							bcp.revalidate();
							refreshScalingHandles();
						}
					}
				});
				bcp.revalidate();
			}
		};

		/**
		 * This is the master listener that is notified when a BoxContainerPanel
		 * changes the BoxContainer it should be displaying. This is relatively
		 * rare.
		 */
		PropertyChangeListener componentBoxContainerListener = new PropertyChangeListener() {
			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				BoxContainer c1 = (BoxContainer) evt.getOldValue();
				BoxContainer c2 = (BoxContainer) evt.getNewValue();
				if (c1 != null) {
					c1.getBoxes().removeChangeListener(boxContainerListener);
					c1.getConnectors().removeChangeListener(
							boxContainerListener);
				}
				if (c2 != null) {
					c2.getBoxes().addChangeListener(boxContainerListener, true);
					c2.getConnectors().addChangeListener(boxContainerListener,
							true);
				}
			}
		};

		protected PlafContext(BoxContainerPanel bcp) {
			this.bcp = bcp;
			UIEffect.installTweenEffect(bcp, KEY_TARGET_HANDLE_OPACITY,
					KEY_REAL_HANDLE_OPACITY, .05f, 20);
			this.bcp.getSelectionModel().addChangeListener(
					new ChangeListener() {
						public void stateChanged(ChangeEvent e) {
							refreshScalingHandles();
							PlafContext.this.bcp.repaint();
						}
					});

			new ContextualMenuHelper(bcp) {
				@Override
				protected void showPopup(Component c, int x, int y) {
					if (contextMenuItems.size() == 0)
						return;
					getPopupMenu().removeAll();
					for (JMenuItem i : contextMenuItems) {
						getPopupMenu().add(i);
					}
					super.showPopup(c, x, y);
				}
			};

			deleteConnectorItem.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					((ConnectorHandle) clickedHandle).connector
							.setControlPoint(null);
				}
			});

			bcp.setLayout(new LayoutManager() {

				@Override
				public void addLayoutComponent(String name, Component comp) {
				}

				@Override
				public void removeLayoutComponent(Component comp) {
				}

				@Override
				public Dimension preferredLayoutSize(Container parent) {
					return null;
				}

				@Override
				public Dimension minimumLayoutSize(Container parent) {
					return null;
				}

				@Override
				public void layoutContainer(Container parent) {
					BoxContainer bc = PlafContext.this.bcp.getBoxContainer();
					for (int a = 0; a < bc.getBoxes().size(); a++) {
						Box box = bc.getBoxes().get(a);
						JComponent jc = box.getComponent();
						if (jc != null) {
							Rectangle r = box.getBounds();
							jc.setBounds(r);
						}
					}
				}

			});
		}

		public void installUI() {
			bcp.addMouseListener(mouseListener);
			bcp.addMouseMotionListener(mouseListener);
			bcp.addKeyListener(keyListener);
			bcp.addPropertyChangeListener(BoxContainerPanel.KEY_BOX_CONTAINER,
					componentBoxContainerListener);
			refreshConnectors(bcp);
			BoxContainer bc = bcp.getBoxContainer();
			if (bc != null) {
				bc.getBoxes().addChangeListener(boxContainerListener, true);
				bc.getConnectors()
						.addChangeListener(boxContainerListener, true);
			}
		}

		public void uninstallUI() {
			bcp.removeMouseListener(mouseListener);
			bcp.removeMouseMotionListener(mouseListener);
			bcp.removeKeyListener(keyListener);
			bcp.removePropertyChangeListener(
					BoxContainerPanel.KEY_BOX_CONTAINER,
					componentBoxContainerListener);
			BoxContainer bc = bcp.getBoxContainer();
			if (bc != null) {
				bc.getBoxes().removeChangeListener(boxContainerListener);
				bc.getConnectors().removeChangeListener(boxContainerListener);
			}
		}

		public <T> void set(Key<T> key, T value) {
			properties.put(key.getKeyName(), value);
		}

		public void set(String keyName, Object value) {
			properties.put(keyName, value);
		}

		public Object get(String keyName) {
			return properties.get(keyName);
		}

		public <T> T get(Key<T> key) {
			return (T) properties.get(key.getKeyName());
		}
	}

	protected PlafContext getContext(BoxContainerPanel bcp) {
		PlafContext plafContext = (PlafContext) bcp
				.getClientProperty(KEY_PLAF_CONTEXT);
		if (plafContext == null) {
			plafContext = new PlafContext(bcp);
			bcp.putClientProperty(KEY_PLAF_CONTEXT, plafContext);
		}
		return plafContext;
	}

	protected float getHandleOpacity(BoxContainerPanel bcp,
			AbstractHandle handle) {
		Number n = (Number) bcp.getClientProperty(KEY_REAL_HANDLE_OPACITY);
		if (n == null)
			return 1;
		return n.floatValue();
	}

	/**
	 * Outside classes should never need to call this; this is made public
	 * mostly for debugging.
	 */
	public void refreshConnectors(BoxContainerPanel bcp) {
		bcp.repaint();
	}

	@Override
	public void installUI(JComponent c) {
		getContext((BoxContainerPanel) c).installUI();
	}

	@Override
	public void uninstallUI(JComponent c) {
		getContext((BoxContainerPanel) c).uninstallUI();
	}

	@Override
	public Dimension getPreferredSize(JComponent c) {
		return new Dimension(800, 600);
	}

	@Override
	public Dimension getMinimumSize(JComponent c) {
		return new Dimension(200, 200);
	}

	@Override
	public void paint(Graphics g0, JComponent c) {
		Graphics2D g = (Graphics2D) g0.create();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		try {
			BoxContainerPanel bcp = (BoxContainerPanel) c;
			BoxContainer bc = bcp.getBoxContainer();

			PlafContext context = getContext(bcp);
			if (context.selectionRectangle != null) {
				g.setColor(new Color(0, 0, 0, 50));
				g.fill(context.selectionRectangle);
			}

			if (bc != null) {
				for (Connector connector : bc.getConnectors()) {
					paintConnector(g, bcp, connector);
				}

				if (context.handles != null) {
					context.handles.paint(bcp, g);
				}

				paintScalingHandles(bcp);

				for (Box box : bc.getBoxes()) {
					paintBox(g, box);
				}

				List<Connector> allConnectors = bc.getConnectors();
				List<Connector> dashedConnectors = new ArrayList<>();
				List<Connector> solidConnectors = new ArrayList<>();
				for (Connector connector : allConnectors) {
					if (connector.getControlPoint(true) == null) {
						dashedConnectors.add(connector);
					} else {
						solidConnectors.add(connector);
					}
				}
				paintHandles(g, bcp, dashedConnectors, bc.getBoxes(), true);
				paintHandles(g, bcp, solidConnectors, bc.getBoxes(), false);
			}
		} finally {
			g.dispose();
		}
	}

	protected void paintScalingHandles(BoxContainerPanel bcp) {
		PlafContext context = getContext(bcp);
		Collection<Box> selectedBoxes = context.bcp.getSelectionModel().get();
		Rectangle selectionBounds = null;
		for (Box box : selectedBoxes) {
			if (selectionBounds == null) {
				selectionBounds = new Rectangle(box.getBounds());
			} else {
				selectionBounds.add(box.getBounds());
			}
		}

	}

	protected AbstractHandle getHandle(PlafContext context, MouseEvent e) {
		Point p = e.getPoint();

		for (final Connector connector : context.bcp.getBoxContainer()
				.getConnectors()) {
			Point controlPoint = connector.getControlPoint(false);
			Ellipse2D handleShape = new Ellipse2D.Float(controlPoint.x
					- handleRadius, controlPoint.y - handleRadius,
					2 * handleRadius, 2 * handleRadius);
			if (handleShape.contains(e.getPoint())) {
				AbstractHandle handle = new ConnectorHandle(connector,
						controlPoint, handleShape);
				handle.addPropertyChangeListener(new PropertyChangeListener() {
					@Override
					public void propertyChange(PropertyChangeEvent evt) {
						if (AbstractHandle.KEY_CENTER.matches(evt)) {
							connector.setControlPoint((Point) evt.getNewValue());
						}
					}
				});
				return handle;
			}
		}

		if (context.handles != null) {
			for (int a = 0; a < context.handles.handles.length; a++) {
				if (context.handles.handles[a].shape.contains(p)) {
					return context.handles.handles[a];
				}
			}
		}
		return null;
	}

	protected void paintBox(Graphics2D g, Box box) {
		if (box.getComponent() != null) {
			return;
		}
		Rectangle r = box.getBounds();
		g.setColor(Color.lightGray);
		g.setPaint(new GradientPaint(0, r.y, new Color(0xffffff), 0, r.y
				+ r.height, new Color(0xdddddd)));
		g.fill(r);
		g.setColor(Color.darkGray);
		g.draw(r);

		g.setColor(Color.black);
		String boxName = box.toString();
		Rectangle2D bounds = g.getFont().getStringBounds(boxName,
				g.getFontRenderContext());
		if (bounds.getWidth() + 10 > r.width) {
			r.setBounds(r.x, r.y, (int) (Math.ceil(bounds.getWidth()) + 10.5),
					r.height);
		}
		g.drawString(boxName, r.x + 5, r.y + 15);
	}

	protected void paintConnector(Graphics2D g, BoxContainerPanel bcp,
			Connector connector) {
		Rectangle r1 = connector.getBox1().getBounds();
		Rectangle r2 = connector.getBox2().getBounds();
		g.setColor(Color.black);
		g.drawLine(r1.x + r1.width / 2, r1.y + r1.height / 2, r2.x + r2.width
				/ 2, r2.y + r2.height / 2);
	}

	protected void paintHandles(Graphics2D g, BoxContainerPanel bcp,
			List<Connector> connectors, List<Box> boxes, boolean dashStroke) {
		g = (Graphics2D) g.create();
		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
				getHandleOpacity(bcp, null)));
		for (Connector connector : connectors) {
			Point cp = connector.getControlPoint(false);
			paintEllipseHandle(g, cp, dashStroke);
		}
		g.dispose();
	}

	protected void paintEllipseHandle(Graphics2D g, Point p, boolean dashed) {
		g.setColor(new Color(0, 0, 0, 100));
		if (dashed) {
			g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
					RenderingHints.VALUE_STROKE_PURE);
			g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
					BasicStroke.JOIN_BEVEL, 5, new float[] { 2f, 2f }, 0));
		} else {
			g.setStroke(new BasicStroke(1));
		}
		Ellipse2D handle = new Ellipse2D.Float(p.x - 4, p.y - 4, 8, 8);
		g.draw(handle);
	}

	public void addConnectorDecoration(GeneralPath path,
			Relationship relationship, Point target, Point source) {
		relationship.appendDecoration(path, target, source);
	}

}