/**
 * 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.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;

import javax.swing.JComponent;

import com.pump.swing.JGradientSlider;
import com.pump.swing.MultiThumbSlider;

/**
 * The UI for the GradientSlider class.
 * 
 * There are a few properties you can use to customize the UI of a
 * GradientSlider. You can set these for each slider by calling: <BR>
 * <code>slider.putClientProperty(key,value);</code>
 * <P>
 * Or you can set these globally by calling: <BR>
 * <code>UIManager.put(key,value);</code>
 * <P>
 * The available properties are:
 * <P>
 * <TABLE summary="GradientSliderUI Client Properties" BORDER="1" CELLPADDING=5>
 * <TR>
 * <TD>Property Name</TD>
 * <TD>Default Value</TD>
 * <TD>Description</td>
 * </TR>
 * <TR>
 * <TD>GradientSlider.useBevel</TD>
 * <TD>"false"</TD>
 * <TD>If this is <code>true</code>, then this slider will be painted in a
 * rectangle with a bevel effect around the borders. If this is
 * <code>false</code>, then this slider will be painted in a rounded rectangle.</td>
 * </TR>
 * <TR>
 * <TD>GradientSlider.showTranslucency</TD>
 * <TD>"true"</TD>
 * <TD>If this is <code>true</code>, then the slider will reflect the opacity of
 * the colors in the gradient, and paint a checkered background underneath the
 * colors to indicate opacity. If this is <code>false</code>, then this slider
 * will always paint with completely opaque colors, although the actual colors
 * may be translucent.</td>
 * </TR>
 * <TR>
 * <TD>GradientSlider.colorPickerIncludesOpacity</TD>
 * <TD>"true"</TD>
 * <TD>This is used when the user double-clicks a color and a JColorPicker
 * dialog is invoked. (So this value may not have any meaning if you override
 * <code>GradientSlider.doDoubleClick()</code>.) This controls whether the
 * opacity/alpha controls are available in that dialog. This does <i>not</i>
 * control whether translucent colors can be used in this slider: translucent
 * colors are always allowed, if the user can enter them.</TD>
 * </TR>
 * <TR>
 * <TD>MultiThumbSlider.indicateComponent</TD>
 * <TD>"true"</TD>
 * <TD>If this is <code>true</code>, then the thumbs will only paint on this
 * component when the mouse is inside this slider <i>or</i> when this slider as
 * the keyboard focus.</td>
 * </TR>
 * <TR>
 * <TD>MultiThumbSlider.indicateThumb</TD>
 * <TD>"true"</TD>
 * <TD>If this is <code>true</code>, then the thumb the mouse is over will
 * gently fade into a slightly different color.</td>
 * </TR>
 * </TABLE>
 *
 */
public class GradientSliderUI extends MultiThumbSliderUI<Color> {
	int TRIANGLE_SIZE = 8;
	/**
	 * The width of this image is the absolute widest the track will ever
	 * become.
	 */
	BufferedImage img = new BufferedImage(1000, 1, BufferedImage.TYPE_INT_ARGB);
	/** A temporary array used for the buffered image */
	int[] array = new int[img.getWidth()];

	public GradientSliderUI(JGradientSlider slider) {
		super(slider);
	}

	@Override
	protected int getPreferredComponentDepth() {
		return 2 * TRIANGLE_SIZE;
	}

	protected void calculateImage() {
		float[] f = slider.getThumbPositions();
		Color[] c = slider.getValues();

		/**
		 * make sure we DO have a value at 0 and 1:
		 */
		if (f[0] != 0) {
			float[] f2 = new float[f.length + 1];
			System.arraycopy(f, 0, f2, 1, f.length);
			Color[] c2 = new Color[c.length + 1];
			System.arraycopy(c, 0, c2, 1, f.length);
			f = f2;
			c = c2;
			f[0] = 0;
			c[0] = c[1];
		}
		if (f[f.length - 1] != 1) {
			float[] f2 = new float[f.length + 1];
			System.arraycopy(f, 0, f2, 0, f.length);
			Color[] c2 = new Color[c.length + 1];
			System.arraycopy(c, 0, c2, 0, f.length);
			f = f2;
			c = c2;
			f[f.length - 1] = 1;
			c[c.length - 1] = c[c.length - 2];
		}

		/** Now, finally paint */
		int[] argb = new int[c.length];
		for (int a = 0; a < argb.length; a++) {
			argb[a] = ((c[a].getAlpha() & 0xff) << 24)
					+ ((c[a].getRed() & 0xff) << 16)
					+ ((c[a].getGreen() & 0xff) << 8)
					+ ((c[a].getBlue() & 0xff) << 0);
		}
		int max;
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			max = trackRect.width;
		} else {
			max = trackRect.height;
		}
		if (max <= 0)
			return;

		boolean alwaysOpaque = getProperty(slider,
				"GradientSlider.showTranslucency", "true").equals("false");

		float fraction;
		int i1 = 0;
		int i2 = 1;
		int a1 = (argb[0] >> 24) & 0xff;
		int r1 = (argb[0] & 0x00ff0000) >> 16;
		int g1 = (argb[0] & 0x0000ff00) >> 8;
		int b1 = (argb[0] & 0x000000ff) >> 0;
		int a2 = (argb[1] >> 24) & 0xff;
		int r2 = (argb[1] & 0x00ff0000) >> 16;
		int g2 = (argb[1] & 0x0000ff00) >> 8;
		int b2 = (argb[1] & 0x000000ff) >> 0;
		for (int z = 0; z < max; z++) {
			fraction = ((float) z) / ((float) (max - 1));
			if (fraction < 1 && fraction >= f[i2]) {
				while (fraction < 1 && fraction >= f[i2]) {
					i1++;
					i2++;
				}

				a1 = (argb[i1] >> 24) & 0xff;
				r1 = (argb[i1] & 0x00ff0000) >> 16;
				g1 = (argb[i1] & 0x0000ff00) >> 8;
				b1 = (argb[i1] & 0x000000ff) >> 0;
				a2 = (argb[i2] >> 24) & 0xff;
				r2 = (argb[i2] & 0x00ff0000) >> 16;
				g2 = (argb[i2] & 0x0000ff00) >> 8;
				b2 = (argb[i2] & 0x000000ff) >> 0;
			}
			float colorFraction = (fraction - f[i1]) / (f[i2] - f[i1]);
			if (colorFraction > 1)
				colorFraction = 1;
			if (colorFraction < 0)
				colorFraction = 0;
			if (alwaysOpaque) {
				a1 = 255;
				a2 = 255;
			}
			array[z] = (((int) (a1 * (1 - colorFraction) + a2 * colorFraction)) << 24)
					+ (((int) (r1 * (1 - colorFraction) + r2 * colorFraction)) << 16)
					+ (((int) (g1 * (1 - colorFraction) + g2 * colorFraction)) << 8)
					+ (((int) (b1 * (1 - colorFraction) + b2 * colorFraction)));
		}
		img.getRaster().setDataElements(0, 0, max, 1, array);
	}

	@Override
	public Dimension getMinimumSize(JComponent s) {
		Dimension d = super.getMinimumSize(s);
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			d.height += 2;
		} else {
			d.width += 2;
		}
		return d;
	}

	@Override
	public Dimension getPreferredSize(JComponent s) {
		Dimension d = super.getPreferredSize(s);
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			d.height += 2;
		} else {
			d.width += 2;
		}
		return d;
	}

	@Override
	protected Rectangle calculateTrackRect() {
		int w = slider.getWidth();
		int h = slider.getHeight();

		Rectangle r = new Rectangle();

		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			r.x = TRIANGLE_SIZE;
			r.y = 3;
			r.height = h - TRIANGLE_SIZE - r.y;
			r.width = w - 2 * TRIANGLE_SIZE;
			if (r.width > img.getWidth()) {
				r.width = img.getWidth();
				r.x = (w - 2 * TRIANGLE_SIZE) / 2 - r.width / 2;
			}
			if (r.height > 2 * DEPTH) {
				r.height = 2 * DEPTH;
				r.y = (h - TRIANGLE_SIZE) / 2 - r.height / 2;
			}
		} else {
			r.x = 3;
			r.y = TRIANGLE_SIZE;
			r.width = w - TRIANGLE_SIZE - r.x;
			r.height = h - 2 * TRIANGLE_SIZE;
			if (r.height > img.getWidth()) {
				r.height = img.getWidth();
				r.y = (h - 2 * TRIANGLE_SIZE) / 2 - r.height / 2;
			}
			if (r.width > 2 * DEPTH) {
				r.width = 2 * DEPTH;
				r.x = (w - TRIANGLE_SIZE) / 2 - r.width / 2;
			}
		}
		return r;
	}

	@Override
	protected void calculateGeometry() {
		super.calculateGeometry();
		calculateImage();
	}

	static TexturePaint checkerPaint;

	private static void createCheckerPaint() {
		int k = 4;
		BufferedImage bi = new BufferedImage(2 * k, 2 * k,
				BufferedImage.TYPE_INT_RGB);
		Graphics2D g = bi.createGraphics();
		g.setColor(Color.white);
		g.fillRect(0, 0, 2 * k, 2 * k);
		g.setColor(Color.lightGray);
		g.fillRect(0, 0, k, k);
		g.fillRect(k, k, k, k);
		checkerPaint = new TexturePaint(bi, new Rectangle(0, 0, bi.getWidth(),
				bi.getHeight()));
	}

	/**
	 * The "frame" includes the trackRect and possible some extra padding. For
	 * example, the frame might be the rounded rectangle enclosing the track (if
	 * rounded rectangles are turned on)
	 * 
	 * @return
	 */
	private Shape getFrame() {

		if (getProperty(slider, "GradientSlider.useBevel", "false").equals(
				"true")) {
			return trackRect;
		}

		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			int curve = Math.min(TRIANGLE_SIZE - 2, trackRect.height / 2);
			return new RoundRectangle2D.Float(trackRect.x - curve, trackRect.y,
					trackRect.width + 2 * curve, trackRect.height, curve * 2,
					curve * 2);
		} else {
			int curve = Math.min(TRIANGLE_SIZE - 2, trackRect.width / 2);
			return new RoundRectangle2D.Float(trackRect.x, trackRect.y - curve,
					trackRect.width, trackRect.height + 2 * curve, curve * 2,
					curve * 2);
		}
	}

	@Override
	protected void paintTrack(Graphics2D g) {
		Composite oldComposite = g.getComposite();
		float alpha = slider.isEnabled() ? 1 : .5f;
		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
				alpha));

		Shape frame = getFrame();

		boolean alwaysOpaque = getProperty(slider,
				"GradientSlider.showTranslucency", "true").equals("false");

		if (alwaysOpaque == false) {
			if (checkerPaint == null)
				createCheckerPaint();
			g.setPaint(checkerPaint);
			g.fill(frame);
		}

		TexturePaint tp = new TexturePaint(img, new Rectangle(trackRect.x, 0,
				img.getWidth(), 1));
		g.setPaint(tp);
		AffineTransform oldTransform = null;

		oldTransform = g.getTransform();
		AffineTransform transform = new AffineTransform();
		if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
			if (slider.isInverted()) {
				transform.rotate(Math.PI / 2, trackRect.x, trackRect.y);
			} else {
				transform.rotate(-Math.PI / 2, trackRect.x, trackRect.y
						+ trackRect.height);
			}
		} else {
			if (slider.isInverted()) {
				// flip horizontal:
				double x1 = trackRect.x;
				double x2 = trackRect.x + trackRect.width;
				// m00*x1+m02 = x2
				// m00*x2+m02 = x1
				double m00 = (x2 - x1) / (x1 - x2);
				double m02 = x1 - m00 * x2;
				transform.setTransform(m00, 0, 0, 1, m02, 0);
			} else {
				// no transform necessary
			}
		}

		g.transform(transform);

		try {
			g.fill(transform.createInverse().createTransformedShape(trackRect));
		} catch (NoninvertibleTransformException e) {
			// this won't happen; unless a width/height
			// is zero somewhere, in which case we have nothing to paint anyway.
		}
		if (oldTransform != null) {
			g.setTransform(oldTransform);
		}
		if (getProperty(slider, "GradientSlider.useBevel", "false").equals(
				"true")) {
			PlafPaintUtils.drawBevel(g, trackRect);
		} else {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			Shape oldClip = g.getClip();
			int first, last;
			Color[] colors = slider.getValues();
			float[] f = slider.getThumbPositions();
			if ((slider.isInverted() == false && slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
					|| (slider.isInverted() == true && slider.getOrientation() == MultiThumbSlider.VERTICAL)) {
				first = 0;
				last = colors.length - 1;
				while (f[first] < 0) {
					first++;
				}
				while (f[last] > 1) {
					last--;
				}
			} else {
				last = 0;
				first = colors.length - 1;
				while (f[last] < 0) {
					last++;
				}
				while (f[first] > 1) {
					first--;
				}
			}
			if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
				g.clip(frame);
				g.setColor(colors[first]);
				g.fillRect(0, 0, trackRect.x, slider.getHeight());
				g.setColor(colors[last]);
				g.fillRect(trackRect.x + trackRect.width, 0, slider.getWidth()
						- (trackRect.x + trackRect.width), slider.getHeight());
			} else {
				g.clip(frame);
				g.setColor(colors[first]);
				g.fillRect(0, 0, slider.getWidth(), trackRect.y);
				g.setColor(colors[last]);
				g.fillRect(0, trackRect.y + trackRect.height,
						slider.getWidth(), slider.getHeight()
								- (trackRect.y + trackRect.height));
			}
			g.setStroke(new BasicStroke(1));
			g.setClip(oldClip);
			g.setColor(new Color(0, 0, 0, 130));
			g.draw(frame);

			g.setColor(new Color(0, 0, 0, 130));
		}

		if (slider.isPaintTicks()) {
			paintTick(g, .25f, 2);
			paintTick(g, .5f, 2);
			paintTick(g, .75f, 2);
			paintTick(g, 0f, 2);
			paintTick(g, 1f, 2);
		}
		g.setComposite(oldComposite);
	}

	protected void paintTick(Graphics2D g, float f, int d) {
		if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
			int x = (int) (trackRect.x + trackRect.width * f + .5f);
			int y = trackRect.y + trackRect.height;
			g.drawLine(x, y, x, y + d);
			y = trackRect.y;
			g.drawLine(x, y, x, y - d);
		} else {
			int y = (int) (trackRect.y + trackRect.height * f + .5f);
			int x = trackRect.x + trackRect.width;
			g.drawLine(x, y, x + d, y);
			x = trackRect.x;
			g.drawLine(x, y, x - d, y);
		}
	}

	@Override
	protected void paintFocus(Graphics2D g) {

	}

	static GeneralPath hTriangle = null;
	static GeneralPath vTriangle = null;

	@Override
	protected void paintThumbs(Graphics2D g) {
		if (slider.isEnabled() == false)
			return;

		if (hTriangle == null) {
			hTriangle = new GeneralPath();
			hTriangle.moveTo(0, 0);
			hTriangle.lineTo(TRIANGLE_SIZE, TRIANGLE_SIZE);
			hTriangle.lineTo(-TRIANGLE_SIZE, TRIANGLE_SIZE);
			hTriangle.lineTo(0, 0);
			hTriangle.closePath();
			vTriangle = new GeneralPath();
			vTriangle.moveTo(0, 0);
			vTriangle.lineTo(TRIANGLE_SIZE, TRIANGLE_SIZE);
			vTriangle.lineTo(TRIANGLE_SIZE, -TRIANGLE_SIZE);
			vTriangle.lineTo(0, 0);
			vTriangle.closePath();
		}

		AffineTransform t = new AffineTransform();
		int dx = trackRect.x + trackRect.width;
		int dy = trackRect.y + trackRect.height;
		dy -= trackRect.height / 6;
		dx -= trackRect.width / 6;
		int selected = slider.getSelectedThumb(false);
		float[] f = slider.getThumbPositions();
		int orientation = slider.getOrientation();
		Shape shape;

		Composite oldComposite = g.getComposite();

		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
				indication));

		for (int a = 0; a < thumbPositions.length; a++) {
			if (f[a] >= 0 && f[a] <= 1 && a != selected) {
				if (orientation == MultiThumbSlider.HORIZONTAL) {
					dx = thumbPositions[a];
					shape = hTriangle;
				} else {
					dy = thumbPositions[a];
					shape = vTriangle;
				}
				t.setToTranslation(dx, dy);
				g.transform(t);

				float brightness = Math.max(0, thumbIndications[a] * .6f);

				g.setColor(new Color((int) (255 * brightness),
						(int) (255 * brightness), (int) (255 * brightness)));
				g.fill(shape);
				g.translate(-.5f, -.5f);

				g.setColor(new Color(255, 255, 255));

				g.draw(shape);
				g.translate(.5f, .5f);

				t.setToTranslation(-dx, -dy);
				g.transform(t);
			}
		}

		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
				indication));

		if (selected != -1 && f[selected] >= 0 && f[selected] <= 1) {
			if (orientation == MultiThumbSlider.HORIZONTAL) {
				dx = thumbPositions[selected];
				shape = hTriangle;
			} else {
				dy = thumbPositions[selected];
				shape = vTriangle;
			}
			t.setToTranslation(dx, dy);
			g.transform(t);

			g.setColor(new Color(255, 255, 255));
			g.fill(shape);
			g.translate(-.5f, -.5f);

			g.setColor(new Color(0, 0, 0));

			g.draw(shape);
			g.translate(.5f, .5f);

			t.setToTranslation(-dx, -dy);
			g.transform(t);
		}

		g.setComposite(oldComposite);
	}
}