/*
 * @(#)GradientSlider.java
 *
 * $Date: 2015-01-04 18:13:42 -0800 (Sun, 04 Jan 2015) $
 *
 * Copyright (c) 2011 by Jeremy Wood.
 * All rights reserved.
 *
 * The copyright of this software is owned by Jeremy Wood. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Jeremy Wood. For details see accompanying license terms.
 * 
 * This software is probably, but not necessarily, discussed here:
 * https://javagraphics.java.net/
 * 
 * That site should also contain the most recent official version
 * of this software.  (See the SVN repository for more details.)
 */
package com.bric.swing;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;

import javax.swing.JColorChooser;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.ComponentUI;

import com.bric.plaf.MultiThumbSliderUI;

/** This component lets the user manipulate the colors in a gradient.
 * A <code>GradientSlider</code> can contain any number of thumbs.  The
 * slider itself represents a range of values from zero to one, so the thumbs
 * must always be within this range.  Each thumb maps to a specific <code>Color</code>.
 * <P>There are some specific properties you can set to customize the look-and-feel
 * of this slider in the default {@link com.bric.plaf.GradientSliderUI} class.
 * <P>The UI for each slider is loaded from the UIManager property: "GradientSliderUI".
 * By default this is "com.bric.plaf.GradientSliderUI".
 * <p>The {@link GradientSliderDemoHelper} creates this sample graphic:
 * <br><img src="https://javagraphics.java.net/blurbs/GradientSlider.png" alt="Screenshot of the GradientSlider">
 * 
 * @see com.bric.swing.GradientSliderDemo
 *
 */
public class GradientSlider extends MultiThumbSlider<Color> {

	private static final long serialVersionUID = 1L;
	
	static {
		if(UIManager.getString("GradientSliderUI")==null)
			UIManager.put("GradientSliderUI", "com.bric.plaf.GradientSliderUI");
	}

	/** Create a horizontal <code>GradientSlider</code> that
	 * represents a gradient from white to black.
	 */
	public GradientSlider() {
		this(HORIZONTAL);
	}
	
	/** Create a <code>GradientSlider</code> that represents a
	 * gradient form white to black.
	 * @param orientation HORIZONTAL or VERTICAL
	 */
	public GradientSlider(int orientation) {
		this(orientation, new float[] {0f,1f},new Color[] {Color.white, Color.black});
	}
	
	/** Create a new <code>GradientSlider</code>.
	 * 
	 * @param orientation HORIZONTAL or VERTICAL
	 * @param thumbPositions the initial positions of each thumb
	 * @param values the initial colors at each position
	 * @throws IllegalArgumentException if the number of elements in
	 * <code>thumbPositions</code> does not equal the number of elements
	 * in <code>values</code>.
	 * 
	 */
	public GradientSlider(int orientation,float[] thumbPositions,Color[] values) {
		super(orientation, thumbPositions, values);
	}

	/** @return the Color at the specified position.
	 */
	@Override
	public Color createValueForInsertion(float pos) {
		for(int a = 0; a<thumbPositions.length-1; a++) {
			if(thumbPositions[a]<=pos && pos<=thumbPositions[a+1]) {
				float v = (pos-thumbPositions[a])/(thumbPositions[a+1]-thumbPositions[a]);
				return tween((Color)values[a],(Color)values[a+1],v);
			}
		}
		if(pos<thumbPositions[0]) {
			return values[0];
		}
		if(pos>thumbPositions[thumbPositions.length-1]) {
			return values[values.length-1];
		}
		return null;
	}
	
	private static Color tween( Color c1, Color c2, float p) {
		if(p==0)
			return c1;
		if(p==1)
			return c2;
		
		return new Color(
				(int)(c1.getRed()*(1-p)+c2.getRed()*(p)),
				(int)(c1.getGreen()*(1-p)+c2.getGreen()*(p)),
				(int)(c1.getBlue()*(1-p)+c2.getBlue()*(p)),
				(int)(c1.getAlpha()*(1-p)+c2.getAlpha()*(p))
		);
	}

	/** This invokes a <code>ColorPicker</code> dialog to edit
	 * the thumb at the selected index.
	 * 
	 */
	@Override
	public boolean doDoubleClick(int x,int y) {
		int i = getSelectedThumb();
		if(i!=-1) {
			showColorPicker();
			//showJColorChooser();
			SwingUtilities.invokeLater(new SelectThumbRunnable(i));
			return true;
		} else {
			return false;
		}
	}
	
	class SelectThumbRunnable implements Runnable {
		int index;
		public SelectThumbRunnable(int i) {
			index = i;
		}
		public void run() {
			setSelectedThumb(index);
		}
	}
	
	/** The popup for contextual menus. */
	JPopupMenu popup;
	
	private JPopupMenu createPopup() {
		return new ColorPickerPopup();
	}
	
	abstract class AbstractPopup extends JPopupMenu {
		private static final long serialVersionUID = 1L;

		int lastSelectedThumb;
		
		PopupMenuListener popupMenuListener = new PopupMenuListener() {
			public void popupMenuCanceled(PopupMenuEvent e) {
				setValueIsAdjusting(false);
				SwingUtilities.invokeLater(new SelectThumbRunnable(lastSelectedThumb));
			}

			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
				setValueIsAdjusting(false);
				SwingUtilities.invokeLater(new SelectThumbRunnable(lastSelectedThumb));
			}

			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
				setValueIsAdjusting(true);
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						getFocusableComponent().requestFocus();
					}
				});
			}
		};
		
		public AbstractPopup() {
			addPopupMenuListener(popupMenuListener);
		}
		
		public abstract Component getFocusableComponent();

		@Override
		public void show(Component c,int x,int y) {
			Color[] colors = getValues();
			lastSelectedThumb = getSelectedThumb();
			if(lastSelectedThumb!=-1) {
				setColor(colors[lastSelectedThumb]);
				super.show(c,x,y);
			}
		}
		
		public abstract void setColor(Color c);
	}
	
	class ColorPickerPopup extends AbstractPopup {
		private static final long serialVersionUID = 1L;
		
		ColorPicker mini;
		KeyListener commitListener = new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if(e.getKeyCode()==KeyEvent.VK_SPACE || e.getKeyCode()==KeyEvent.VK_ENTER) {
					ColorPickerPopup.this.setVisible(false);
				}
			}
		};
		
		public ColorPickerPopup() {
			super();
			boolean includeOpacity = MultiThumbSliderUI.getProperty(GradientSlider.this,"GradientSlider.includeOpacity","true").equals("true");
			
			mini = new ColorPicker(false,includeOpacity);
			mini.setMode(ColorPicker.HUE);
			mini.setPreferredSize(new Dimension(220,200));
			PropertyChangeListener p = new PropertyChangeListener() {
				public void propertyChange(PropertyChangeEvent evt) {
					ColorPicker p = (ColorPicker)evt.getSource();
					Color[] colors = getValues();
					colors[lastSelectedThumb] = p.getColor();
					setValues(getThumbPositions(),colors);
				}
			};
			mini.addPropertyChangeListener(ColorPicker.SELECTED_COLOR_PROPERTY, p);
			mini.addPropertyChangeListener(ColorPicker.OPACITY_PROPERTY, p);
			for(int a = 0; a<mini.getComponentCount(); a++) {
				Component c = mini.getComponent(a);
				c.addKeyListener(commitListener);
			}
			add(mini);
		}

		@Override
		public Component getFocusableComponent() {
			return mini.getColorPanel();
		}
		
		@Override
		public void setColor(Color c) {
			mini.setRGB(c.getRed(), c.getGreen(), c.getBlue());
			mini.setOpacity(c.getAlpha());
		}
	}
	
	/** This shows a mini ColorPicker panel to let the user
	 * change the selected color.
	 */
	@Override
	public boolean doPopup(int x,int y) {
		if(popup==null) {
			popup = createPopup();
		}
		popup.show(this, x, y);
		return true;
	}
	

	private Frame getFrame() {
		Window w = SwingUtilities.getWindowAncestor(this);
		if(w instanceof Frame) return ((Frame)w);
		return null;
	}
	
	private boolean showColorPicker() {
		Color[] colors = getValues();
		int i = getSelectedThumb();

		Frame frame = getFrame();
		
		boolean includeOpacity = MultiThumbSliderUI.getProperty(this,"GradientSlider.colorPickerIncludesOpacity","true").equals("true");
		colors[i] = ColorPicker.showDialog(frame, colors[i], includeOpacity);
		if(colors[i]!=null)
			setValues(getThumbPositions(), colors);
		return true;
	}
	
	/** TODO: If developers don't want to bundle the ColorPicker with their programs,
	 * they can use this method instead of <code>showColorPicker()</code>.
	 */
	@SuppressWarnings("unused")
	private void showJColorChooser() {
		Color[] colors = getValues();
		int i = getSelectedThumb();
		if(i>=0 && i<colors.length) {
			colors[i] = JColorChooser.showDialog(this, "Choose a Color", colors[i]);
			if(colors[i]!=null)
				setValues(getThumbPositions(), colors);
		}
	}

    @Override
	public void updateUI() {
    	String name = UIManager.getString("GradientSliderUI");
    	if(name==null) name = "com.bric.plaf.GradientSliderUI";
    	try {
	    	Class<?> c = Class.forName(name);
	    	Constructor<?>[] constructors = c.getConstructors();
	    	for(int a = 0; a<constructors.length; a++) {
	    		Class<?>[] types = constructors[a].getParameterTypes();
	    		if(types.length==1 && types[0].equals(GradientSlider.class)) {
	    			ComponentUI ui = (ComponentUI)constructors[a].newInstance(new Object[] {this});
	    			setUI(ui);
	    			return;
	    		}
	    	}
    	} catch(ClassNotFoundException e) {
    		throw new RuntimeException("The class \""+name+"\" could not be found.");
    	} catch(Throwable t) {
    		RuntimeException e = new RuntimeException("The class \""+name+"\" could not be constructed.");
    		e.initCause(t);
    		throw e;
    	}
    }

	public void setMinimumThumbnailCount(int i) {
		if(i<2) throw new IllegalArgumentException("The GradientSlider must have at least two thumbnails ("+i+")");
		putClientProperty(THUMB_MINIMUM_PROPERTY, i);
	}
	
	public int getMinimumThumbnailCount() {
		Integer i = (Integer)getClientProperty(THUMB_MINIMUM_PROPERTY);
		if(i==null) return 2;
		return i;
	}
}
