/**
 * 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.plaf;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Array;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;

import com.pump.geom.ShapeBounds;
import com.pump.swing.MultiThumbSlider;
import com.pump.swing.MultiThumbSlider.Collision;

/**
 * This is the abstract UI for <code>MultiThumbSliders</code>
 * 
 * 
 */
public abstract class MultiThumbSliderUI<T> extends ComponentUI implements
		MouseListener, MouseMotionListener {

	/**
	 * The Swing client property associated with a Thumb.
	 * 
	 * @see Thumb
	 */
	public static final String THUMB_SHAPE_PROPERTY = MultiThumbSliderUI.class
			.getName() + ".thumbShape";

	PropertyChangeListener thumbShapeListener = new PropertyChangeListener() {

		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			slider.repaint();
		}

	};

	/**
	 * A thumb shape.
	 */
	public static enum Thumb {
		Circle() {
			@Override
			public Shape getShape(float width, float height, boolean leftEdge,
					boolean rightEdge, boolean sharpEdgesHint) {
				Ellipse2D e = new Ellipse2D.Float(-width / 2f, -height / 2f,
						width, height);
				return e;
			}
		},
		Triangle() {
			@Override
			public Shape getShape(float width, float height, boolean leftEdge,
					boolean rightEdge, boolean sharpEdgesHint) {
				float k = width / 2;
				GeneralPath p = new GeneralPath();
				float r = 5;

				if ((leftEdge) && (!rightEdge)) {
					k = k * 2;
					p.moveTo(0, height / 2);
					p.lineTo(-k, height / 2 - k);
					p.lineTo(-k, -height / 2 + r);
					p.curveTo(-k, -height / 2, -k, -height / 2, -k + r,
							-height / 2);
					p.lineTo(0, -height / 2);
					p.closePath();
				} else if ((rightEdge) && (!leftEdge)) {
					k = k * 2;
					p.moveTo(0, -height / 2);
					p.lineTo(k - r, -height / 2);
					p.curveTo(k, -height / 2, k, -height / 2, k, -height / 2
							+ r);
					p.lineTo(k, height / 2 - k);
					p.lineTo(0, height / 2);
					p.closePath();
				} else {
					if (sharpEdgesHint) {
						p.moveTo(0, height / 2);
						p.lineTo(-k, height / 2 - k);
						p.lineTo(-k, -height / 2 + 1);
						p.lineTo(-k + 1, -height / 2);
						p.lineTo(k - 1, -height / 2);
						p.lineTo(k, -height / 2 + 1);
						p.lineTo(k, height / 2 - k);
						p.closePath();
					} else {
						p.moveTo(0, height / 2);
						p.lineTo(-k, height / 2 - k);
						p.lineTo(-k, -height / 2 + r);
						p.curveTo(-k, -height / 2, -k, -height / 2, -k + r,
								-height / 2);
						p.lineTo(k - r, -height / 2);
						p.curveTo(k, -height / 2, k, -height / 2, k, -height
								/ 2 + r);
						p.lineTo(k, height / 2 - k);
						p.closePath();
					}
				}
				return p;
			}
		},
		Rectangle() {
			@Override
			public Shape getShape(float width, float height, boolean leftEdge,
					boolean rightEdge, boolean sharpEdgesHint) {
				if ((leftEdge) && (!rightEdge)) {
					return new Rectangle2D.Float(-width, -height / 2, width,
							height);
				} else if ((rightEdge) && (!leftEdge)) {
					return new Rectangle2D.Float(0, -height / 2, width, height);
				} else {
					if (sharpEdgesHint)
						return new Rectangle2D.Float(-width / 2, -height / 2,
								width, height);

					return new RoundRectangle2D.Float(-width / 2, -height / 2,
							width, height, 4, 4);
				}
			}
		},
		Hourglass() {
			@Override
			public Shape getShape(float width, float height, boolean leftEdge,
					boolean rightEdge, boolean sharpEdgesHint) {
				GeneralPath p = new GeneralPath();
				if ((leftEdge) && (!rightEdge)) {
					float k = width;
					p.moveTo(-width, -height / 2);
					p.lineTo(0, -height / 2);
					p.lineTo(0, height / 2);
					p.lineTo(-width, height / 2);
					p.lineTo(0, height / 2 - k);
					p.lineTo(0, -height / 2 + k);
					p.closePath();
				} else if ((rightEdge) && (!leftEdge)) {
					float k = width;
					p.moveTo(width, -height / 2);
					p.lineTo(0, -height / 2);
					p.lineTo(0, height / 2);
					p.lineTo(width, height / 2);
					p.lineTo(0, height / 2 - k);
					p.lineTo(0, -height / 2 + k);
					p.closePath();
				} else {
					float k = width / 2;
					p.moveTo(-width / 2, -height / 2);
					p.lineTo(width / 2, -height / 2);
					p.lineTo(0, -height / 2 + k);
					p.lineTo(0, height / 2 - k);
					p.lineTo(width / 2, height / 2);
					p.lineTo(-width / 2, height / 2);
					p.lineTo(0, height / 2 - k);
					p.lineTo(0, -height / 2 + k);
					p.closePath();
				}
				return p;
			}
		};

		/**
		 * Create a thumb that is centered at (0,0) for a horizontally oriented
		 * slider.
		 * 
		 * @param sliderUI
		 *            the slider UI this thumb relates to.
		 * @param x
		 *            the x-coordinate where this thumb is centered.
		 * @param y
		 *            the y-coordinate where this thumb is centered.
		 * @param width
		 *            the width of the the thumb (assuming this is a horizontal
		 *            slider)
		 * @param height
		 *            the height of the the thumb (assuming this is a horizontal
		 *            slider)
		 * @param leftEdge
		 *            true if this is the left-most thumb
		 * @param rightEdge
		 *            true if this is the right-most thumb.
		 * @return the shape of this thumb.
		 */
		public Shape getShape(MultiThumbSliderUI<?> sliderUI, float x, float y,
				int width, int height, boolean leftEdge, boolean rightEdge) {

			// TODO: reinstate leftEdge and rightEdge once bug related to
			// nudging
			// adjacent thumbs is resolved.

			GeneralPath path = new GeneralPath(getShape(width, height, false,
					false, !sliderUI.getThumbAntialiasing()));
			if (sliderUI.slider.getOrientation() == SwingConstants.VERTICAL) {
				path.transform(AffineTransform.getRotateInstance(-Math.PI / 2));
			}
			path.transform(AffineTransform.getTranslateInstance(Math.round(x),
					Math.round(y)));
			return path;
		}

		/**
		 * Create a thumb that is centered at (0,0) for a horizontally oriented
		 * slider.
		 * 
		 * @param width
		 *            the width of the the thumb (assuming this is a horizontal
		 *            slider)
		 * @param height
		 *            the height of the the thumb (assuming this is a horizontal
		 *            slider)
		 * @param leftEdge
		 *            true if this is the left-most thumb
		 * @param rightEdge
		 *            true if this is the right-most thumb.
		 * @param sharpEdgesHint
		 *            if true then this may return something more polygonal with
		 *            the assumption that antialiasing is turned off. If false
		 *            then this should instead return something with bezier
		 *            curves.
		 * @return the shape of this thumb.
		 */
		public abstract Shape getShape(float width, float height,
				boolean leftEdge, boolean rightEdge, boolean sharpEdgesHint);
	}

	protected MultiThumbSlider<T> slider;

	/**
	 * The maximum width returned by <code>getMaximumSize()</code>. (or if the
	 * slider is vertical, this is the maximum height.)
	 */
	int MAX_LENGTH = 300;

	/**
	 * The minimum width returned by <code>getMinimumSize()</code>. (or if the
	 * slider is vertical, this is the minimum height.)
	 */
	int MIN_LENGTH = 50;

	/**
	 * The maximum width returned by <code>getPreferredSize()</code>. (or if the
	 * slider is vertical, this is the preferred height.)
	 */
	int PREF_LENGTH = 140;

	/**
	 * The height of a horizontal slider -- or width of a vertical slider.
	 */
	int DEPTH = 15;

	/**
	 * The pixel position of the thumbs. This may be x or y coordinates,
	 * depending on whether this slider is horizontal or vertical
	 */
	int[] thumbPositions = new int[0];

	/**
	 * A float from zero to one, indicating whether that thumb should be
	 * highlighted or not.
	 */
	protected float[] thumbIndications = new float[0];

	/**
	 * This is used by the animating thread. The field indication is updated
	 * until it equals this value.
	 */
	private float indicationGoal = 0;

	/**
	 * The overall indication of the thumbs. At one they should be opaque, at
	 * zero they should be transparent.
	 */
	float indication = 0;

	/** The rectangle the track should be painted in. */
	protected Rectangle trackRect = new Rectangle(0, 0, 0, 0);

	public MultiThumbSliderUI(MultiThumbSlider<T> slider) {
		this.slider = slider;
	}

	@Override
	public Dimension getMaximumSize(JComponent s) {
		MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
		int k = Math.max(DEPTH, getPreferredComponentDepth());
		if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			return new Dimension(MAX_LENGTH, k);
		}
		return new Dimension(k, MAX_LENGTH);
	}

	@Override
	public Dimension getMinimumSize(JComponent s) {
		MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
		int k = Math.max(DEPTH, getPreferredComponentDepth());
		if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			return new Dimension(MIN_LENGTH, k);
		}
		return new Dimension(k, MIN_LENGTH);
	}

	@Override
	public Dimension getPreferredSize(JComponent s) {
		MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
		int k = Math.max(DEPTH, getPreferredComponentDepth());
		if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			return new Dimension(PREF_LENGTH, k);
		}
		return new Dimension(k, PREF_LENGTH);
	}

	/**
	 * Return the typical height of a horizontally oriented slider, or the width
	 * of the vertically oriented slider.
	 * 
	 * @return the typical height of a horizontally oriented slider, or the
	 *         width of the vertically oriented slider.
	 */
	protected abstract int getPreferredComponentDepth();

	/**
	 * This records the positions/values of each thumb. This is used when the
	 * mouse is pressed, so as the mouse is dragged values can get replaced and
	 * rearranged freely. (Including removing and adding thumbs)
	 * 
	 */
	class State {
		T[] values;
		float[] positions;
		int selectedThumb;

		public State() {
			values = slider.getValues();
			positions = slider.getThumbPositions();
			selectedThumb = slider.getSelectedThumb(false);
		}

		public State(State s) {
			selectedThumb = s.selectedThumb;
			positions = new float[s.positions.length];
			values = createSimilarArray(s.values, s.values.length);
			System.arraycopy(s.positions, 0, positions, 0, positions.length);
			System.arraycopy(s.values, 0, values, 0, values.length);
		}

		/** Strip values outside of [0,1] */
		private void polish() {
			while (positions[0] < 0) {
				float[] f2 = new float[positions.length - 1];
				System.arraycopy(positions, 1, f2, 0, positions.length - 1);
				T[] c2 = createSimilarArray(values, values.length - 1);
				System.arraycopy(values, 1, c2, 0, positions.length - 1);
				positions = f2;
				values = c2;
				selectedThumb++;
			}
			while (positions[positions.length - 1] > 1) {
				float[] f2 = new float[positions.length - 1];
				System.arraycopy(positions, 0, f2, 0, positions.length - 1);
				T[] c2 = createSimilarArray(values, values.length - 1);
				System.arraycopy(values, 0, c2, 0, positions.length - 1);
				positions = f2;
				values = c2;
				selectedThumb--;
			}
			if (selectedThumb >= positions.length)
				selectedThumb = -1;
		}

		/** Make the slider reflect this object */
		public void install() {
			polish();

			slider.setValues(positions, values);
			slider.setSelectedThumb(selectedThumb);
		}

		/**
		 * This is a kludgy casting trick to make our arrays mesh with generics.
		 */
		private T[] createSimilarArray(T[] src, int length) {
			Class<?> componentType = src.getClass().getComponentType();
			return (T[]) Array.newInstance(componentType, length);
		}

		public void removeThumb(int index) {
			float[] f = new float[positions.length - 1];
			T[] c = createSimilarArray(values, values.length - 1);
			System.arraycopy(positions, 0, f, 0, index);
			System.arraycopy(values, 0, c, 0, index);
			System.arraycopy(positions, index + 1, f, index, f.length - index);
			System.arraycopy(values, index + 1, c, index, f.length - index);
			positions = f;
			values = c;
			selectedThumb = -1;
		}

		public boolean setPosition(int thumbIndex, float newPosition) {
			return setPosition(thumbIndex, newPosition, true);
		}

		private boolean isCrossover(int thumbIndexA, int thumbIndexB,
				float newThumbBPosition) {
			if (thumbIndexA == thumbIndexB)
				return false;
			int oldState = new Float(positions[thumbIndexA])
					.compareTo(positions[thumbIndexB]);
			int newState = new Float(positions[thumbIndexA])
					.compareTo(newThumbBPosition);
			if (newState * oldState < 0)
				return true;
			return isOverlap(thumbIndexA, thumbIndexB, newThumbBPosition);
		}

		private boolean isOverlap(int thumbIndexA, int thumbIndexB,
				float newThumbBPosition) {
			if (thumbIndexA == thumbIndexB)
				return false;
			if (!slider.isThumbOverlap()) {
				Point2D aCenter = getThumbCenter(positions[thumbIndexA]);
				Point2D bCenter = getThumbCenter(newThumbBPosition);
				Rectangle2D aBounds = ShapeBounds.getBounds(getThumbShape(
						thumbIndexA, aCenter));
				Rectangle2D bBounds = ShapeBounds.getBounds(getThumbShape(
						thumbIndexB, bCenter));
				return aBounds.intersects(bBounds) || aBounds.equals(bBounds);
			}
			return false;
		}

		private boolean setPosition(int thumbIndex, float newPosition,
				boolean revise) {
			Collision c = slider.getCollisionPolicy();
			if (Collision.JUMP_OVER_OTHER.equals(c)
					&& (!slider.isThumbOverlap())) {
				newPosition = Math.max(0, Math.min(1, newPosition));
				for (int a = 0; a < positions.length; a++) {
					if (isOverlap(a, thumbIndex, newPosition)) {
						if (revise) {
							float alternative;

							int maxWidth = Math.max(getThumbSize(a).width,
									getThumbSize(thumbIndex).width);
							float trackSize = slider.getOrientation() == SwingConstants.HORIZONTAL ? trackRect.width
									: trackRect.height;
							newPosition = Math.max(0, Math.min(1, newPosition));
							// offset is measured in pixels
							for (int offset = 0; offset < 4 * maxWidth; offset++) {
								alternative = Math
										.max(0,
												Math.min(1, newPosition
														- ((float) offset)
														/ trackSize));
								if (!isOverlap(a, thumbIndex, alternative)) {
									return setPosition(thumbIndex, alternative,
											false);
								}
								alternative = Math
										.max(0,
												Math.min(1, newPosition
														+ ((float) offset)
														/ trackSize));
								if (!isOverlap(a, thumbIndex, alternative)) {
									return setPosition(thumbIndex, alternative,
											false);
								}
							}
							return false;
						}
						return false;
					}
				}
			} else if (Collision.STOP_AGAINST.equals(c)) {
				for (int a = 0; a < positions.length; a++) {
					if (isCrossover(a, thumbIndex, newPosition)) {
						// this move would cross thumbIndex over an existing
						// thumb. This violates the collision policy:
						if (revise) {
							float alternative;

							int maxWidth = Math.max(getThumbSize(a).width,
									getThumbSize(thumbIndex).width);
							float trackSize = slider.getOrientation() == SwingConstants.HORIZONTAL ? trackRect.width
									: trackRect.height;
							// offset is measured in pixels
							for (int offset = 0; offset < 2 * maxWidth; offset++) {
								if (positions[a] > positions[thumbIndex]) {
									alternative = positions[a]
											- ((float) offset) / trackSize;
								} else {
									alternative = positions[a]
											+ ((float) offset) / trackSize;
								}
								if (!isCrossover(a, thumbIndex, alternative)) {
									return setPosition(thumbIndex, alternative,
											false);
								}
							}
							return false;
						}

						return false;
					}
				}
			} else if (Collision.NUDGE_OTHER.equals(c)) {
				if (revise) {
					final Set<Integer> processedThumbs = new HashSet<Integer>();
					processedThumbs.add(-1);

					class NudgeRequest {
						/** The index of the thumb this request wants to move. */
						final int thumbIndex;
						/** The original value of this thumb. */
						final float startingValue;
						/** The amount we're asking to change this value by. */
						final float requestedDelta;

						NudgeRequest(int thumbIndex, float startingValue,
								float requestedDelta) {
							this.thumbIndex = thumbIndex;
							this.startingValue = startingValue;
							this.requestedDelta = requestedDelta;
						}

						void process() {
							float span;
							if (slider.isThumbOverlap()) {
								span = 0;
							} else {
								span = (float) ShapeBounds.getBounds(
										getThumbShape(thumbIndex)).getWidth();
								if (slider.getOrientation() == SwingConstants.HORIZONTAL) {
									span = span / ((float) trackRect.width);
								} else {
									span = span / ((float) trackRect.height);
								}
							}
							int[] neighbors = getNeighbors(thumbIndex);
							float newPosition = startingValue + requestedDelta;
							processedThumbs.add(thumbIndex);

							if (neighbors[0] == -1 && newPosition < 0) {
								setPosition(thumbIndex, 0, false);
							} else if (neighbors[1] == -1 && newPosition > 1) {
								setPosition(thumbIndex, 1, false);
							} else if (processedThumbs.add(neighbors[0])
									&& (newPosition < positions[neighbors[0]] || Math
											.abs(positions[neighbors[0]]
													- newPosition) < span - .0001)) {
								NudgeRequest dependsOn = new NudgeRequest(
										neighbors[0], positions[neighbors[0]],
										(newPosition - span)
												- positions[neighbors[0]]);
								dependsOn.process();
								setPosition(thumbIndex,
										positions[dependsOn.thumbIndex] + span,
										false);
							} else if (processedThumbs.add(neighbors[1])
									&& (newPosition > positions[neighbors[1]] || Math
											.abs(positions[neighbors[1]]
													- newPosition) < span - .0001)) {
								NudgeRequest dependsOn = new NudgeRequest(
										neighbors[1], positions[neighbors[1]],
										(newPosition + span)
												- positions[neighbors[1]]);
								dependsOn.process();
								setPosition(thumbIndex,
										positions[dependsOn.thumbIndex] - span,
										false);
							} else {
								setPosition(thumbIndex, startingValue
										+ requestedDelta, false);
							}
						}
					}

					float originalValue = positions[thumbIndex];
					NudgeRequest rootRequest = new NudgeRequest(thumbIndex,
							positions[thumbIndex], newPosition
									- positions[thumbIndex]);
					rootRequest.process();
					return positions[thumbIndex] != originalValue;
				}

			}
			positions[thumbIndex] = newPosition;
			return true;
		}

		/**
		 * Return the left (lesser) neighbor and the right (greater) neighbor.
		 * Either index may be -1 if it is not available.
		 * 
		 * @param thumbIndex
		 *            the index of the thumb to examine.
		 * @return the left (lesser) neighbor and the right (greater) neighbor.
		 */
		int[] getNeighbors(int thumbIndex) {
			float leftNeighborDelta = 10;
			float rightNeighborDelta = 10;
			int leftNeighbor = -1;
			int rightNeighbor = -1;
			for (int a = 0; a < positions.length; a++) {
				if (a != thumbIndex) {
					if (positions[thumbIndex] < positions[a]) {
						float delta = positions[a] - positions[thumbIndex];
						if (delta < rightNeighborDelta) {
							rightNeighborDelta = delta;
							rightNeighbor = a;
						}
					} else if (positions[thumbIndex] > positions[a]) {
						float delta = positions[thumbIndex] - positions[a];
						if (delta < leftNeighborDelta) {
							leftNeighborDelta = delta;
							leftNeighbor = a;
						}
					}
				}
			}
			return new int[] { leftNeighbor, rightNeighbor };
		}
	}

	Thread animatingThread = null;

	Runnable animatingRunnable = new Runnable() {
		public void run() {
			boolean finished = false;
			while (!finished) {
				synchronized (MultiThumbSliderUI.this) {
					finished = true;
					for (int a = 0; a < thumbIndications.length; a++) {
						if (a != slider.getSelectedThumb()) {
							if (a == currentIndicatedThumb) {
								if (thumbIndications[a] < 1) {
									thumbIndications[a] = Math.min(1,
											thumbIndications[a] + .025f);
									finished = false;
								}
							} else {
								if (thumbIndications[a] > 0) {
									thumbIndications[a] = Math.max(0,
											thumbIndications[a] - .025f);
									finished = false;
								}
							}
						} else {
							// the selected thumb is painted as selected,
							// so there's no indication to animate.
							// just set the indication to whatever it should
							// be and move on. No repainting.
							if (a == currentIndicatedThumb) {
								thumbIndications[a] = 1;
							} else {
								thumbIndications[a] = 0;
							}
						}
					}
					if (indicationGoal > indication + .01f) {
						if (indication < .99f) {
							indication = Math.min(1, indication + .1f);
							finished = false;
						}
					} else if (indicationGoal < indication - .01f) {
						if (indication > .01f) {
							indication = Math.max(0, indication - .1f);
							finished = false;
						}
					}
				}
				if (!finished)
					slider.repaint();

				// rest a little bit
				long t = System.currentTimeMillis();
				while (System.currentTimeMillis() - t < 20) {
					try {
						Thread.sleep(10);
					} catch (Exception e) {
						Thread.yield();
					}
				}
			}
		}
	};

	private int currentIndicatedThumb = -1;
	protected boolean mouseInside = false;
	protected boolean mouseIsDown = false;
	private State pressedState;
	private int dx, dy;

	public void mousePressed(MouseEvent e) {
		dx = 0;
		dy = 0;

		if (slider.isEnabled() == false)
			return;

		if (e.getClickCount() >= 2) {
			if (slider.doDoubleClick(e.getX(), e.getY())) {
				e.consume();
				return;
			}
		} else if (e.isPopupTrigger()) {
			int x = e.getX();
			int y = e.getY();
			if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
				if (x < trackRect.x || x > trackRect.x + trackRect.width)
					return;
				y = trackRect.y + trackRect.height;
			} else {
				if (y < trackRect.y || y > trackRect.y + trackRect.height)
					return;
				x = trackRect.x + trackRect.width;
			}
			if (slider.doPopup(x, y)) {
				e.consume();
				return;
			}
		}
		mouseIsDown = true;
		mouseMoved(e);

		if (e.getSource() != slider) {
			throw new RuntimeException(
					"only install this UI on the GradientSlider it was constructed with");
		}
		slider.requestFocus();

		int index = getIndex(e);
		if (index != -1) {
			if (slider.getOrientation() == SwingConstants.HORIZONTAL) {
				dx = -e.getX() + thumbPositions[index];
			} else {
				dy = -e.getY() + thumbPositions[index];
			}
		}

		if (index != -1) {
			slider.setSelectedThumb(index);
			e.consume();
		} else {
			if (slider.isAutoAdding()) {
				float k;

				int v;
				if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
					v = e.getX();
				} else {
					v = e.getY();
				}

				if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
					k = ((float) (v - trackRect.x)) / ((float) trackRect.width);
					if (slider.isInverted())
						k = 1 - k;
				} else {
					k = ((float) (v - trackRect.y))
							/ ((float) trackRect.height);
					if (slider.isInverted() == false)
						k = 1 - k;
				}
				if (k > 0 && k < 1) {
					int added = slider.addThumb(k);
					slider.setSelectedThumb(added);
				}
				e.consume();
			} else {
				if (slider.getSelectedThumb() != -1) {
					slider.setSelectedThumb(-1);
					e.consume();
				}
			}
		}
		pressedState = new State();
	}

	private int getIndex(MouseEvent e) {
		int v;
		Rectangle2D shapeSum = new Rectangle2D.Double(trackRect.x, trackRect.y,
				trackRect.width, trackRect.height);
		for (int a = 0; a < slider.getThumbCount(); a++) {
			shapeSum.add(ShapeBounds.getBounds(getThumbShape(a)));
		}
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			v = e.getX();
			if (v < shapeSum.getMinX() || v > shapeSum.getMaxX()) {
				return -1; // didn't click in the track;
			}
		} else {
			v = e.getY();
			if (v < shapeSum.getMinY() || v > shapeSum.getMaxY()) {
				return -1;
			}
		}
		int min = Math.abs(v - thumbPositions[0]);
		int minIndex = 0;
		for (int a = 1; a < thumbPositions.length; a++) {
			int distance = Math.abs(v - thumbPositions[a]);
			if (distance < min) {
				min = distance;
				minIndex = a;
			} else if (distance == min) {
				// two thumbs may perfectly overlap
				if (v < thumbPositions[a]) {
					// you clicked to the left of the fulcrum, so we should side
					// with the smaller index
					if (slider.isInverted()) {
						// ... unless it's inverted:
						minIndex = a;
					}
				} else {
					if (!slider.isInverted())
						minIndex = a;
				}
			}
		}
		if (min < getThumbSize(minIndex).width / 2) {
			return minIndex;
		}
		return -1;
	}

	public void mouseEntered(MouseEvent e) {
		mouseMoved(e);
	}

	public void mouseExited(MouseEvent e) {
		setCurrentIndicatedThumb(-1);
		setMouseInside(false);
	}

	public void mouseClicked(MouseEvent e) {
	}

	public void mouseMoved(MouseEvent e) {
		if (slider.isEnabled() == false)
			return;

		int i = getIndex(e);
		setCurrentIndicatedThumb(i);
		boolean b = (e.getX() >= 0 && e.getX() < slider.getWidth()
				&& e.getY() >= 0 && e.getY() < slider.getHeight());
		if (mouseIsDown)
			b = true;
		setMouseInside(b);
	}

	protected Dimension getThumbSize(int thumbIndex) {
		return new Dimension(16, 16);
	}

	/**
	 * Create the shape used to render a specific thumb.
	 * 
	 * @param thumbIndex
	 *            the index of the thumb to render.
	 * @return the shape used to render a specific thumb.
	 * 
	 * @see #getThumbCenter(int)
	 * @see #getThumb(int)
	 */
	public Shape getThumbShape(int thumbIndex) {
		return getThumbShape(thumbIndex, null);
	}

	/**
	 * Create the shape used to render a specific thumb.
	 * 
	 * @param thumbIndex
	 *            the index of the thumb to render.
	 * @param center
	 *            an optional center to focus the thumb around. If this is null
	 *            then the current (real) center is used, but this can be
	 *            supplied manually to consider possible shapes and visual size
	 *            constraints based on the current collision policy.
	 * @return the shape used to render a specific thumb.
	 * 
	 * @see #getThumbCenter(int)
	 * @see #getThumb(int)
	 */
	public Shape getThumbShape(int thumbIndex, Point2D center) {
		Thumb thumb = getThumb(thumbIndex);
		if (center == null)
			center = getThumbCenter(thumbIndex);
		Dimension d = getThumbSize(thumbIndex);
		return thumb.getShape(this, (float) center.getX(),
				(float) center.getY(), d.width, d.height, thumbIndex == 0,
				thumbIndex == slider.getThumbCount() - 1);
	}

	/**
	 * Calculate the thumb center
	 * 
	 * @param thumbIndex
	 *            the index of the thumb to consult.
	 * @return the center of a given thumb
	 */
	public Point2D getThumbCenter(int thumbIndex) {
		float[] values = slider.getThumbPositions();
		float n = values[thumbIndex];

		return getThumbCenter(n);
	}

	/**
	 * @return true if Thumbs should be rendered with curved antialiasing. False
	 *         if a crisp pixelated appearance is expected.
	 */
	protected boolean getThumbAntialiasing() {
		return true;
	}

	/**
	 * Calculate the thumb center based on a fractional position
	 * 
	 * @param position
	 *            a value from [0,1]
	 * @return the center of a potential thumbnail for this position.
	 */
	public Point2D getThumbCenter(float position) {
		/*
		 * I'm on the fence about whether to document this as allowing null or
		 * not. Does this occur in the wild? If so: is this more an internal
		 * error than something we need to document/allow for?
		 */
		if (position < 0 || position > 1)
			return null;

		if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
			float y;
			float height = (float) trackRect.height;
			float x = (float) trackRect.getCenterX();
			if (slider.isInverted()) {
				y = (float) (position * height + trackRect.y);
			} else {
				y = (float) ((1 - position) * height + trackRect.y);
			}
			return new Point2D.Float(x, y);
		} else {
			float x;
			float width = (float) trackRect.width;
			float y = (float) trackRect.getCenterY();
			if (slider.isInverted()) {
				x = (float) ((1 - position) * width + trackRect.x);
			} else {
				x = (float) (position * width + trackRect.x);
			}
			return new Point2D.Float(x, y);
		}
	}

	/**
	 * Return the Thumb option used to render a specific thumb. The default
	 * implementation here consults the client property
	 * MultiThumbSliderUI.THUMB_SHAPE_PROPERTY, and returns Circle by default.
	 * 
	 * @param thumbIndex
	 *            the index of the thumb to render.
	 * @return the Thumb option used to render a specific thumb.
	 */
	public Thumb getThumb(int thumbIndex) {
		Thumb defaultThumb = slider.isPaintTicks() ? Thumb.Triangle
				: Thumb.Circle;
		Thumb thumb = getProperty(slider, THUMB_SHAPE_PROPERTY, defaultThumb);
		return thumb;
	}

	private void setCurrentIndicatedThumb(int i) {
		if (getProperty(slider, "MultiThumbSlider.indicateThumb", "true")
				.equals("false")) {
			// never activate a specific thumb
			i = -1;
		}
		currentIndicatedThumb = i;
		boolean finished = true;
		for (int a = 0; a < thumbIndications.length; a++) {
			if (a == currentIndicatedThumb) {
				if (thumbIndications[a] != 1) {
					finished = false;
				}
			} else {
				if (thumbIndications[a] != 0) {
					finished = false;
				}
			}
		}
		if (!finished) {
			synchronized (MultiThumbSliderUI.this) {
				if (animatingThread == null
						|| animatingThread.isAlive() == false) {
					animatingThread = new Thread(animatingRunnable);
					animatingThread.start();
				}
			}
		}
	}

	private void setMouseInside(boolean b) {
		mouseInside = b;
		updateIndication();
	}

	public void mouseDragged(MouseEvent e) {
		if (slider.isEnabled() == false)
			return;

		e.translatePoint(dx, dy);

		mouseMoved(e);
		if (pressedState != null && pressedState.selectedThumb != -1) {
			slider.setValueIsAdjusting(true);

			State newState = new State(pressedState);
			float v;
			boolean outside;
			if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
				v = ((float) (e.getX() - trackRect.x))
						/ ((float) trackRect.width);
				if (slider.isInverted())
					v = 1 - v;
				outside = (e.getY() < trackRect.y - 10)
						|| (e.getY() > trackRect.y + trackRect.height + 10);

				// don't whack the thumb off the slider if you happen to be
				// *near* the edge:
				if (e.getX() > trackRect.x - 10
						&& e.getX() < trackRect.x + trackRect.width + 10) {
					if (v < 0)
						v = 0;
					if (v > 1)
						v = 1;
				}
			} else {
				v = ((float) (e.getY() - trackRect.y))
						/ ((float) trackRect.height);
				if (slider.isInverted() == false)
					v = 1 - v;
				outside = (e.getX() < trackRect.x - 10)
						|| (e.getX() > trackRect.x + trackRect.width + 10);

				if (e.getY() > trackRect.y - 10
						&& e.getY() < trackRect.y + trackRect.height + 10) {
					if (v < 0)
						v = 0;
					if (v > 1)
						v = 1;
				}
			}
			if (newState.positions.length <= slider.getMinimumThumbnailCount()) {
				outside = false; // I don't care if you are outside: no
									// removing!
			}
			newState.setPosition(newState.selectedThumb, v);

			// because we delegate mouseReleased() to this method:
			if (outside && slider.isThumbRemovalAllowed()) {
				newState.removeThumb(newState.selectedThumb);
			}
			if (validatePositions(newState)) {
				newState.install();
			}
			e.consume();
		}
	}

	public void mouseReleased(MouseEvent e) {
		if (slider.isEnabled() == false)
			return;

		mouseIsDown = false;
		if (pressedState != null
				&& slider.getThumbCount() <= pressedState.positions.length) {
			mouseDragged(e); // go ahead and commit this final location
		}
		if (slider.isValueAdjusting()) {
			slider.setValueIsAdjusting(false);
		}
		slider.repaint();

		if (e.isPopupTrigger() && slider.doPopup(e.getX(), e.getY())) {
			// on windows popuptriggers happen on mouseRelease
			e.consume();
			return;
		}
	}

	/**
	 * This retrieves a property. If the component has this property manually
	 * set (by calling <code>component.putClientProperty()</code>), then that
	 * value will be returned. Otherwise this method refers to
	 * <code>UIManager.get()</code>. If that value is missing, this returns
	 * <code>defaultValue</code>
	 * 
	 * @param jc
	 * @param propertyName
	 *            the property name
	 * @param defaultValue
	 *            if no other value is found, this is returned
	 * @return the property value
	 */
	public static <K> K getProperty(JComponent jc, String propertyName,
			K defaultValue) {
		Object jcValue = jc.getClientProperty(propertyName);
		if (jcValue != null)
			return (K) jcValue;
		Object uiValue = UIManager.get(propertyName);
		if (uiValue != null)
			return (K) uiValue;
		return defaultValue;
	}

	/**
	 * Makes sure the thumbs are in the right order.
	 * 
	 * @param state
	 * @return true if the thumbs are valid. False if there are two thumbs with
	 *         the same value (this is not allowed)
	 */
	protected boolean validatePositions(State state) {
		float[] p = state.positions;
		Object[] c = state.values;

		/**
		 * Don't let the user position a thumb outside of [0,1] if there are
		 * only 2 colors: colors outside [0,1] are deleted, and we can't delete
		 * colors so we get less than 2.
		 */
		if (p.length <= slider.getMinimumThumbnailCount()
				|| (!slider.isThumbRemovalAllowed())) {
			/**
			 * Since the user can only manipulate 1 thumb at a time, only 1
			 * thumb should be outside the domain of [0,1]. So we *don't* have
			 * to reorganize c when we change p
			 */
			for (int a = 0; a < p.length; a++) {
				if (p[a] < 0) {
					p[a] = 0;
				} else if (p[a] > 1) {
					p[a] = 1;
				}
			}
		}

		// validate the new positions:
		boolean checkAgain = true;
		while (checkAgain) {
			checkAgain = false;
			for (int a = 0; a < p.length - 1; a++) {
				if (p[a] > p[a + 1]) {
					checkAgain = true;

					float swap1 = p[a];
					p[a] = p[a + 1];
					p[a + 1] = swap1;
					Object swap2 = c[a];
					c[a] = c[a + 1];
					c[a + 1] = swap2;

					if (a == state.selectedThumb) {
						state.selectedThumb = a + 1;
					} else if (a + 1 == state.selectedThumb) {
						state.selectedThumb = a;
					}
				}
			}
		}

		return true;
	}

	FocusListener focusListener = new FocusListener() {
		public void focusLost(FocusEvent e) {
			Component c = (Component) e.getSource();
			if (getProperty(slider, "MultiThumbSlider.indicateComponent",
					"false").toString().equals("true")) {
				slider.setSelectedThumb(-1);
			}
			updateIndication();
			c.repaint();
		}

		public void focusGained(FocusEvent e) {
			Component c = (Component) e.getSource();
			int i = slider.getSelectedThumb(false);
			if (i == -1) {
				int direction = 1;
				if (slider.getOrientation() == MultiThumbSlider.VERTICAL)
					direction *= -1;
				if (slider.isInverted())
					direction *= -1;
				slider.setSelectedThumb((direction == 1) ? 0 : slider
						.getThumbCount() - 1);
			}
			updateIndication();
			c.repaint();
		}
	};

	/**
	 * This will try to add a thumb between index1 and index2.
	 * <P>
	 * This method will not add a thumb if there is already a very small
	 * distance between these two endpoints
	 * 
	 * @param index1
	 * @param index2
	 * @return true if a new thumb was added
	 */
	protected boolean addThumb(int index1, int index2) {
		float pos1 = 0;
		float pos2 = 1;
		int min;
		int max;
		if (index1 < index2) {
			min = index1;
			max = index2;
		} else {
			min = index2;
			max = index1;
		}
		float[] positions = slider.getThumbPositions();
		if (min >= 0)
			pos1 = positions[min];
		if (max < positions.length)
			pos2 = positions[max];

		if (pos2 - pos1 < .05)
			return false;

		float newPosition = (pos1 + pos2) / 2f;
		slider.setSelectedThumb(slider.addThumb(newPosition));

		return true;
	}

	KeyListener keyListener = new KeyListener() {
		public void keyPressed(KeyEvent e) {
			if (slider.isEnabled() == false)
				return;

			if (e.getSource() != slider)
				throw new RuntimeException(
						"only install this UI on the GradientSlider it was constructed with");
			int i = slider.getSelectedThumb();
			int code = e.getKeyCode();
			int orientation = slider.getOrientation();
			if (i != -1
					&& (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_LEFT)
					&& orientation == MultiThumbSlider.HORIZONTAL
					&& e.getModifiers() == Toolkit.getDefaultToolkit()
							.getMenuShortcutKeyMask()) {
				// insert a new thumb
				int i2;
				if ((code == KeyEvent.VK_RIGHT && slider.isInverted() == false)
						|| (code == KeyEvent.VK_LEFT && slider.isInverted() == true)) {
					i2 = i + 1;
				} else {
					i2 = i - 1;
				}
				addThumb(i, i2);
				e.consume();
				return;
			} else if (i != -1
					&& (code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN)
					&& orientation == MultiThumbSlider.VERTICAL
					&& e.getModifiers() == Toolkit.getDefaultToolkit()
							.getMenuShortcutKeyMask()) {
				// insert a new thumb
				int i2;
				if ((code == KeyEvent.VK_UP && slider.isInverted() == false)
						|| (code == KeyEvent.VK_DOWN && slider.isInverted() == true)) {
					i2 = i + 1;
				} else {
					i2 = i - 1;
				}
				addThumb(i, i2);
				e.consume();
				return;
			} else if (code == KeyEvent.VK_DOWN
					&& orientation == MultiThumbSlider.HORIZONTAL && i != -1) {
				// popup up!
				int x = slider.isInverted() ? (int) (trackRect.x + trackRect.width
						* (1 - slider.getThumbPositions()[i]))
						: (int) (trackRect.x + trackRect.width
								* slider.getThumbPositions()[i]);
				int y = trackRect.y + trackRect.height;
				if (slider.doPopup(x, y)) {
					e.consume();
					return;
				}
			} else if (code == KeyEvent.VK_RIGHT
					&& orientation == MultiThumbSlider.VERTICAL && i != -1) {
				// popup up!
				int y = slider.isInverted() ? (int) (trackRect.y + trackRect.height
						* slider.getThumbPositions()[i])
						: (int) (trackRect.y + trackRect.height
								* (1 - slider.getThumbPositions()[i]));
				int x = trackRect.x + trackRect.width;
				if (slider.doPopup(x, y)) {
					e.consume();
					return;
				}
			}
			if (i != -1) {
				// move the selected thumb
				if (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_DOWN) {
					nudge(i, 1);
					e.consume();
				} else if (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_UP) {
					nudge(i, -1);
					e.consume();
				} else if (code == KeyEvent.VK_DELETE
						|| code == KeyEvent.VK_BACK_SPACE) {
					if (slider.getThumbCount() > slider
							.getMinimumThumbnailCount()
							&& slider.isThumbRemovalAllowed()) {
						slider.removeThumb(i);
						e.consume();
					}
				} else if (code == KeyEvent.VK_SPACE
						|| code == KeyEvent.VK_ENTER) {
					slider.doDoubleClick(-1, -1);
				}
			}
		}

		public void keyReleased(KeyEvent e) {
		}

		public void keyTyped(KeyEvent e) {
		}
	};

	PropertyChangeListener propertyListener = new PropertyChangeListener() {

		public void propertyChange(PropertyChangeEvent e) {
			String name = e.getPropertyName();
			if (name.equals(MultiThumbSlider.VALUES_PROPERTY)
					|| name.equals(MultiThumbSlider.ORIENTATION_PROPERTY)
					|| name.equals(MultiThumbSlider.INVERTED_PROPERTY)) {
				calculateGeometry();
				slider.repaint();
			} else if (name.equals(MultiThumbSlider.SELECTED_THUMB_PROPERTY)
					|| name.equals(MultiThumbSlider.PAINT_TICKS_PROPERTY)) {
				slider.repaint();
			} else if (name.equals("MultiThumbSlider.indicateComponent")) {
				setMouseInside(mouseInside);
				slider.repaint();
			}
		}

	};

	ComponentListener compListener = new ComponentListener() {

		public void componentHidden(ComponentEvent e) {
		}

		public void componentMoved(ComponentEvent e) {
		}

		public void componentResized(ComponentEvent e) {
			calculateGeometry();
			Component c = (Component) e.getSource();
			c.repaint();
		}

		public void componentShown(ComponentEvent e) {
		}
	};

	protected void updateIndication() {
		synchronized (MultiThumbSliderUI.this) {
			if (slider.isEnabled() && (slider.hasFocus() || mouseInside)) {
				indicationGoal = 1;
			} else {
				indicationGoal = 0;
			}

			if (getProperty(slider, "MultiThumbSlider.indicateComponent",
					"false").equals("false")) {
				// always turn on the "indication", so controls are always
				// visible
				indicationGoal = 1;
				if (slider.isVisible() == false) { // when the component isn't
													// yet initialized
					indication = 1; // initialize it to fully indicated
				}
			}

			if (indication != indicationGoal) {
				if (animatingThread == null
						|| animatingThread.isAlive() == false) {
					animatingThread = new Thread(animatingRunnable);
					animatingThread.start();
				}
			}
		}
	}

	protected synchronized void calculateGeometry() {
		trackRect = calculateTrackRect();

		float[] pos = slider.getThumbPositions();

		if (thumbPositions.length != pos.length) {
			thumbPositions = new int[pos.length];
			thumbIndications = new float[pos.length];
		}
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			for (int a = 0; a < thumbPositions.length; a++) {
				if (slider.isInverted() == false) {
					thumbPositions[a] = trackRect.x
							+ (int) (trackRect.width * pos[a]);
				} else {
					thumbPositions[a] = trackRect.x
							+ (int) (trackRect.width * (1 - pos[a]));
				}
				thumbIndications[a] = 0;
			}
		} else {
			for (int a = 0; a < thumbPositions.length; a++) {
				if (slider.isInverted()) {
					thumbPositions[a] = trackRect.y
							+ (int) (trackRect.height * pos[a]);
				} else {
					thumbPositions[a] = trackRect.y
							+ (int) (trackRect.height * (1 - pos[a]));
				}
				thumbIndications[a] = 0;
			}
		}
	}

	protected Rectangle calculateTrackRect() {
		Insets i = new Insets(5, 5, 5, 5);
		int w, h;
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			w = slider.getWidth() - i.left - i.right;
			h = Math.min(DEPTH, slider.getHeight() - i.top - i.bottom);
		} else {
			h = slider.getHeight() - i.top - i.bottom;
			w = Math.min(DEPTH, slider.getWidth() - i.left - i.right);
		}
		return new Rectangle(slider.getWidth() / 2 - w / 2, slider.getHeight()
				/ 2 - h / 2, w, h);
	}

	private void nudge(int thumbIndex, int direction) {
		float pixelFraction;
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			pixelFraction = 1f / (trackRect.width);
		} else {
			pixelFraction = 1f / (trackRect.height);
		}
		if (direction < 0)
			pixelFraction *= -1;
		if (slider.isInverted())
			pixelFraction *= -1;
		if (slider.getOrientation() == MultiThumbSlider.VERTICAL)
			pixelFraction *= -1;

		// repeat a couple of times: it's possible we'll nudge two values
		// so they're exactly equal, which will make validate() fail.
		// in that case: move the value ANOTHER nudge to the left/right
		// to really make a change. But make sure we still respect the [0,1]
		// limits.
		State state = new State();
		int a = 0;
		while (a < 10 && state.positions[thumbIndex] >= 0
				&& state.positions[thumbIndex] <= 1) {
			state.setPosition(thumbIndex, state.positions[thumbIndex]
					+ pixelFraction);
			if (validatePositions(state)) {
				state.install();
				return;
			}
			a++;
		}
	}

	@Override
	public void installUI(JComponent slider) {
		slider.addMouseListener(this);
		slider.addMouseMotionListener(this);
		slider.addFocusListener(focusListener);
		slider.addKeyListener(keyListener);
		slider.addComponentListener(compListener);
		slider.addPropertyChangeListener(propertyListener);
		slider.addPropertyChangeListener(THUMB_SHAPE_PROPERTY,
				thumbShapeListener);
		calculateGeometry();
	}

	@Override
	public void paint(Graphics g, JComponent slider2) {
		if (slider2 != slider)
			throw new RuntimeException(
					"only use this UI on the GradientSlider it was constructed with");

		Graphics2D g2 = (Graphics2D) g;
		int w = slider.getWidth();
		int h = slider.getHeight();

		if (slider.isOpaque()) {
			g.setColor(slider.getBackground());
			g.fillRect(0, 0, w, h);
		}

		if (slider2.hasFocus()) {
			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			paintFocus(g2);
		}
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_OFF);
		paintTrack(g2);
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		paintThumbs(g2);
	}

	protected abstract void paintTrack(Graphics2D g);

	protected abstract void paintFocus(Graphics2D g);

	protected abstract void paintThumbs(Graphics2D g);

	@Override
	public void uninstallUI(JComponent slider) {
		slider.removeMouseListener(this);
		slider.removeMouseMotionListener(this);
		slider.removeFocusListener(focusListener);
		slider.removeKeyListener(keyListener);
		slider.removeComponentListener(compListener);
		slider.removePropertyChangeListener(propertyListener);
		slider.removePropertyChangeListener(THUMB_SHAPE_PROPERTY,
				thumbShapeListener);
		super.uninstallUI(slider);
	}

}