/*
 * Copyright (c) 2005-2007 Laf-Widget Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Laf-Widget Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package org.jvnet.lafwidget.scroll;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Map;

import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;

import org.jvnet.lafwidget.LafWidgetRepository;
import org.jvnet.lafwidget.LafWidgetUtilities;
import org.jvnet.lafwidget.preview.PreviewPainter;
import org.jvnet.lafwidget.utils.FadeTracker;
import org.jvnet.lafwidget.utils.FadeTracker.FadeKind;

/**
 * ScrollPaneSelector is a little utility class that provides a means to quickly
 * scroll both vertically and horizontally on a single mouse click, by dragging
 * a selection rectangle over a "thumbnail" of the scrollPane's viewport view.
 * <p>
 * Once the selector is installed on a given JScrollPane instance, a little
 * button appears as soon as at least one of its scroll bars is made visible.
 * <p>
 * Contributed by the original author under BSD license. Also appears in the <a
 * href="https://jdnc-incubator.dev.java.net">JDNC Incubator</a>.
 * 
 * @author weebib (Pierre LE LANNIC)
 * @author Kirill Grouchnikov (animations).
 */
public class ScrollPaneSelector extends JComponent {
	// static final fields
	private static final double MAX_SIZE = 200;
	// private static final Icon LAUNCH_SELECTOR_ICON = new Icon() {
	// public void paintIcon(Component c, Graphics g, int x, int y) {
	// Color tmpColor = g.getColor();
	// g.setColor(Color.BLACK);
	// g.drawRect(2, 2, 10, 10);
	// g.drawRect(4, 5, 6, 4);
	// g.setColor(tmpColor);
	// }
	//
	// public int getIconWidth() {
	// return 15;
	// }
	//
	// public int getIconHeight() {
	// return 15;
	// }
	// };
	private static Map theInstalledScrollPaneSelectors = new HashMap();
	private static final String COMPONENT_ORIENTATION = "componentOrientation";

	private static FadeKind SCROLL_FADE_KIND = new FadeKind(
			"substancelaf.scrollSelector.scrollVisible");
	private static FadeKind SCROLL_HIDE_KIND = new FadeKind(
			"substancelaf.scrollSelector.scrollHide");

	// member fields
	private LayoutManager theFormerLayoutManager;
	private JScrollPane theScrollPane;
	private JComponent theComponent;
	private JPopupMenu thePopupMenu;
	private boolean toRestoreOriginal;
	private JButton theButton;
	private BufferedImage theImage;
	private Rectangle theStartRectangle;
	private Rectangle theRectangle;
	private Point theStartPoint;
	private Point thePrevPoint;
	private double theScale;
	private PropertyChangeListener propertyChangeListener;
	private ContainerAdapter theViewPortViewListener;

//	static {
//		FadeConfigurationManager configManager = FadeConfigurationManager
//				.getInstance();
//		configManager.allowFades(SCROLL_FADE_KIND);
//		configManager.allowFades(SCROLL_HIDE_KIND);
//	}

	/**
	 * Installs a ScrollPaneSelector to the given JScrollPane instance.
	 * 
	 * @param aScrollPane
	 */
	public synchronized static void installScrollPaneSelector(
			JScrollPane aScrollPane) {
		if (aScrollPane == null)
			return;
		PreviewPainter pPainter = LafWidgetUtilities
				.getComponentPreviewPainter(aScrollPane);
		if (pPainter == null)
			return;
		if (theInstalledScrollPaneSelectors.containsKey(aScrollPane))
			return;
		ScrollPaneSelector scrollPaneSelector = new ScrollPaneSelector();
		scrollPaneSelector.installOnScrollPane(aScrollPane);
		theInstalledScrollPaneSelectors.put(aScrollPane, scrollPaneSelector);
	}

	/**
	 * Removes the ScrollPaneSelector from the given JScrollPane.
	 * 
	 * @param aScrollPane
	 */
	public synchronized static void uninstallScrollPaneSelector(
			JScrollPane aScrollPane) {
		if (aScrollPane == null)
			return;
		ScrollPaneSelector scrollPaneSelector = (ScrollPaneSelector) theInstalledScrollPaneSelectors
				.get(aScrollPane);
		if (scrollPaneSelector == null)
			return;
		scrollPaneSelector.uninstallFromScrollPane();
		theInstalledScrollPaneSelectors.remove(aScrollPane);
	}

	// -- Constructor ------
	private ScrollPaneSelector() {
		setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
		theScrollPane = null;
		theImage = null;
		theStartRectangle = null;
		theRectangle = null;
		theStartPoint = null;
		theScale = 0.0;
		theButton = new JButton();
		LafWidgetRepository.getRepository().getLafSupport().markButtonAsFlat(
				theButton);
		theButton.setFocusable(false);
		theButton.setFocusPainted(false);

		MouseInputListener mil = new MouseInputAdapter() {
			public void mousePressed(MouseEvent e) {
				Point p = e.getPoint();
				SwingUtilities.convertPointToScreen(p, theButton);
				display(p);
			}

			public void mouseReleased(MouseEvent e) {
				if (!thePopupMenu.isVisible())
					return;
				// if (theStartPoint != null) {
				// Point newPoint = e.getPoint();
				// SwingUtilities.convertPointToScreen(newPoint, (Component) e
				// .getSource());
				// int deltaX = (int) ((newPoint.x - theStartPoint.x) /
				// theScale);
				// int deltaY = (int) ((newPoint.y - theStartPoint.y) /
				// theScale);
				// scroll(deltaX, deltaY);
				// }
				toRestoreOriginal = false;

				// final Component topLevel =
				// thePopupMenu.getTopLevelAncestor();
				// final Point startCorner = topLevel.getLocationOnScreen();
				// final Dimension startDim = topLevel.getSize();
				// final int centerX = startCorner.x + startDim.width / 2;
				// final int centerY = startCorner.y + startDim.height / 2;
				// System.out.println(startCorner.x + ":" + startCorner.y + ":"
				// + startDim.width + ":" + startDim.height);
				thePopupMenu.setVisible(false);
				// FadeTracker.getInstance().trackFadeOut(SCROLL_HIDE_KIND,
				// thePopupMenu, false,
				// new FadeTracker.FadeTrackerCallback() {
				// public void fadeEnded(FadeKind fadeKind) {
				// thePopupMenu.setVisible(false);
				// }
				//
				// public void fadePerformed(FadeKind fadeKind,
				// float fadeCycle10) {
				// int newWidth = (int) (startDim.width
				// * fadeCycle10 / 10.0);
				// int newHeight = (int) (startDim.height
				// * fadeCycle10 / 10.0);
				// int newX = startCorner.x;//centerX - newWidth / 2;
				// int newY = startCorner.y;//centerY - newHeight / 2;
				// // thePopupMenu.setBounds(newX, newY, newWidth,
				// // newHeight);
				// // topLevel.setLocation(newX, newY);
				// topLevel.setSize(newWidth, newHeight);
				// // System.out.println(newX + ":" + newY + ":"
				// // + newWidth + ":" + newHeight);
				// }
				// });
				// theStartPoint = null;
				// thePrevPoint = null;
				theStartRectangle = theRectangle;
			}

			public void mouseDragged(MouseEvent e) {
				if (theStartPoint == null)
					return;
				Point newPoint = e.getPoint();
				SwingUtilities.convertPointToScreen(newPoint, (Component) e
						.getSource());

				Rectangle popupScreenRect = new Rectangle(thePopupMenu
						.getLocationOnScreen(), thePopupMenu.getSize());
				if (!popupScreenRect.contains(newPoint))
					return;

				// if (theStartPoint != null) {
				// Point newPoint = e.getPoint();
				// SwingUtilities.convertPointToScreen(newPoint, (Component) e
				// .getSource());
				int deltaX = (int) ((newPoint.x - thePrevPoint.x) / theScale);
				int deltaY = (int) ((newPoint.y - thePrevPoint.y) / theScale);
				scroll(deltaX, deltaY, false);

				// final Rectangle oldRectangle = theComponent.getVisibleRect();
				// final Rectangle newRectangle = new Rectangle(oldRectangle.x +
				// deltaX,
				// oldRectangle.y + deltaY, oldRectangle.width,
				// oldRectangle.height);
				// syncRectangle();
				// moveRectangle(newPoint.x - theStartPoint.x, newPoint.y
				// - theStartPoint.y);

				// System.out.println(theStartPoint + ":" + newPoint);
				thePrevPoint = newPoint;
				// }
			}
		};
		theButton.addMouseListener(mil);
		theButton.addMouseMotionListener(mil);
		setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
		thePopupMenu = new JPopupMenu();
		thePopupMenu.setLayout(new BorderLayout());
		thePopupMenu.add(this, BorderLayout.CENTER);
		propertyChangeListener = new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if (theScrollPane == null)
					return;
				if ("componentOrientation".equals(evt.getPropertyName())) {
					theScrollPane.setCorner(JScrollPane.LOWER_LEADING_CORNER,
							null);
					theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER,
							theButton);
				}
			}
		};
		theViewPortViewListener = new ContainerAdapter() {
			public void componentAdded(ContainerEvent e) {
				if (thePopupMenu.isVisible())
					thePopupMenu.setVisible(false);
				Component comp = theScrollPane.getViewport().getView();
				theComponent = (comp instanceof JComponent) ? (JComponent) comp
						: null;
			}
		};
		thePopupMenu.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if ("visible".equals(evt.getPropertyName())) {
					if (!thePopupMenu.isVisible()) {
						setCursor(Cursor
								.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
						if (toRestoreOriginal) {
							int deltaX = (int) ((thePrevPoint.x - theStartPoint.x) / theScale);
							int deltaY = (int) ((thePrevPoint.y - theStartPoint.y) / theScale);
							scroll(-deltaX, -deltaY, true);
						}
					}
				}
			}
		});
	}

	// -- JComponent overriden methods ------
	public Dimension getPreferredSize() {
		if (theImage == null || theRectangle == null)
			return new Dimension();
		Insets insets = getInsets();
		return new Dimension(theImage.getWidth(null) + insets.left
				+ insets.right, theImage.getHeight(null) + insets.top
				+ insets.bottom);
	}

	protected void paintComponent(Graphics g1D) {
		if (theImage == null || theRectangle == null)
			return;
		Graphics2D g = (Graphics2D) g1D.create();

		if (FadeTracker.getInstance().isTracked(thePopupMenu, SCROLL_HIDE_KIND)) {
			float fade10 = FadeTracker.getInstance().getFade10(thePopupMenu,
					SCROLL_HIDE_KIND);
			g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
					fade10 / 10.0f));
		}

		Insets insets = getInsets();
		int xOffset = insets.left;
		int yOffset = insets.top;
		int availableWidth = getWidth() - insets.left - insets.right;
		int availableHeight = getHeight() - insets.top - insets.bottom;
		g.drawImage(theImage, xOffset, yOffset, null);

		Color tmpColor = g.getColor();
		Area area = new Area(new Rectangle(xOffset, yOffset, availableWidth,
				availableHeight));
		area.subtract(new Area(theRectangle));
		g.setColor(new Color(200, 200, 200, 128));
		g.fill(area);
		g.setColor(Color.BLACK);
		g.draw(theRectangle);
		g.setColor(tmpColor);

		g.dispose();
	}

	// -- Private methods ------
	private void installOnScrollPane(JScrollPane aScrollPane) {
		if (theScrollPane != null)
			uninstallFromScrollPane();
		theScrollPane = aScrollPane;
		theFormerLayoutManager = theScrollPane.getLayout();
		theScrollPane.setLayout(new TweakedScrollPaneLayout());
		theScrollPane.firePropertyChange("layoutManager", false, true);
		theScrollPane.addPropertyChangeListener(COMPONENT_ORIENTATION,
				propertyChangeListener);
		theScrollPane.getViewport().addContainerListener(
				theViewPortViewListener);
		theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER, theButton);
		Component comp = theScrollPane.getViewport().getView();
		theComponent = (comp instanceof JComponent) ? (JComponent) comp : null;

		this.theButton.setIcon(LafWidgetRepository.getRepository()
				.getLafSupport().getSearchIcon(UIManager.getInt("ScrollBar.width") - 3,
						theScrollPane.getComponentOrientation()));

		theScrollPane.doLayout();
	}

	private void uninstallFromScrollPane() {
		if (theScrollPane == null)
			return;
		if (thePopupMenu.isVisible())
			thePopupMenu.setVisible(false);
		theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER, null);
		theScrollPane.removePropertyChangeListener(COMPONENT_ORIENTATION,
				propertyChangeListener);
		theScrollPane.getViewport().removeContainerListener(
				theViewPortViewListener);
		theScrollPane.setLayout(theFormerLayoutManager);
		theScrollPane.firePropertyChange("layoutManager", true, false);
		theScrollPane = null;
	}

	private void display(Point aPointOnScreen) {
		if (theComponent == null)
			return;

		PreviewPainter previewPainter = LafWidgetUtilities
				.getComponentPreviewPainter(theScrollPane);
		if (!previewPainter.hasPreview(theComponent.getParent(), theComponent,
				0))
			return;

		// if (previewPainter == null) {
		// double compWidth = theComponent.getWidth();
		// double compHeight = theComponent.getHeight();
		// double scaleX = MAX_SIZE / compWidth;
		// double scaleY = MAX_SIZE / compHeight;
		// theScale = Math.min(scaleX, scaleY);
		// theImage = new BufferedImage(
		// (int) (theComponent.getWidth() * theScale),
		// (int) (theComponent.getHeight() * theScale),
		// BufferedImage.TYPE_INT_RGB);
		//
		// Graphics2D g = theImage.createGraphics();
		// g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
		// RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		// g.scale(theScale, theScale);
		// theComponent.paint(g);
		// g.dispose();
		// } else {
		Dimension pDimension = previewPainter.getPreviewWindowDimension(
				theComponent.getParent(), theComponent, 0);
		double compWidth = theComponent.getWidth();
		double compHeight = theComponent.getHeight();
		double scaleX = pDimension.getWidth() / compWidth;
		double scaleY = pDimension.getHeight() / compHeight;
		theScale = Math.min(scaleX, scaleY);
		theImage = new BufferedImage(
				(int) (theComponent.getWidth() * theScale), (int) (theComponent
						.getHeight() * theScale), BufferedImage.TYPE_INT_RGB);

		Graphics2D g = theImage.createGraphics();
		previewPainter.previewComponent(null, theComponent, 0, g, 0, 0,
				theImage.getWidth(), theImage.getHeight());
		g.dispose();
		// }

		theStartRectangle = theComponent.getVisibleRect();
		Insets insets = getInsets();
		theStartRectangle.x = (int) (theScale * theStartRectangle.x + insets.left);
		theStartRectangle.y = (int) (theScale * theStartRectangle.y + insets.right);
		theStartRectangle.width *= theScale;
		theStartRectangle.height *= theScale;
		theRectangle = theStartRectangle;

		Dimension pref = thePopupMenu.getPreferredSize();
		Point buttonLocation = theButton.getLocationOnScreen();
		Point popupLocation = new Point(
				(theButton.getWidth() - pref.width) / 2,
				(theButton.getHeight() - pref.height) / 2);
		Point centerPoint = new Point(buttonLocation.x + popupLocation.x
				+ theRectangle.x + theRectangle.width / 2, buttonLocation.y
				+ popupLocation.y + theRectangle.y + theRectangle.height / 2);
		try {
			// Attempt to move the mouse pointer to the center of the selector's
			// rectangle.
			new Robot().mouseMove(centerPoint.x, centerPoint.y);
			theStartPoint = centerPoint;
		} catch (Exception e) {
			// Since we cannot move the cursor, we'll move the popup instead.
			theStartPoint = aPointOnScreen;
			popupLocation.x += theStartPoint.x - centerPoint.x;
			popupLocation.y += theStartPoint.y - centerPoint.y;
		}
		thePrevPoint = new Point(theStartPoint);
		toRestoreOriginal = true;
		thePopupMenu.show(theButton, popupLocation.x, popupLocation.y);
	}

	private void moveRectangle(int aDeltaX, int aDeltaY) {
		if (theStartRectangle == null)
			return;

		Insets insets = getInsets();
		Rectangle newRect = new Rectangle(theStartRectangle);
		newRect.x += aDeltaX;
		newRect.y += aDeltaY;
		newRect.x = Math.min(Math.max(newRect.x, insets.left), getWidth()
				- insets.right - newRect.width);
		newRect.y = Math.min(Math.max(newRect.y, insets.right), getHeight()
				- insets.bottom - newRect.height);
		Rectangle clip = new Rectangle();
		Rectangle.union(theRectangle, newRect, clip);
		clip.grow(2, 2);
		theRectangle = newRect;
		paintImmediately(clip);
	}

	private void syncRectangle() {
		JViewport viewport = this.theScrollPane.getViewport();
		Rectangle viewRect = viewport.getViewRect();

		Insets insets = getInsets();
		Rectangle newRect = new Rectangle();
		newRect.x = (int) (theScale * viewRect.x + insets.left);
		newRect.y = (int) (theScale * viewRect.y + insets.top);
		newRect.width = (int) (viewRect.width * theScale);
		newRect.height = (int) (viewRect.height * theScale);

		Rectangle clip = new Rectangle();
		Rectangle.union(theRectangle, newRect, clip);
		clip.grow(2, 2);
		theRectangle = newRect;

		// System.out.println(viewRect + "-->" + theRectangle);
		paintImmediately(clip);
	}

	private void scroll(final int aDeltaX, final int aDeltaY, boolean toAnimate) {
		if (theComponent == null)
			return;
		final Rectangle oldRectangle = theComponent.getVisibleRect();
		final Rectangle newRectangle = new Rectangle(oldRectangle.x + aDeltaX,
				oldRectangle.y + aDeltaY, oldRectangle.width,
				oldRectangle.height);

		// Animate scrolling
		if (toAnimate) {
			FadeTracker.getInstance().trackFadeIn(
					ScrollPaneSelector.SCROLL_FADE_KIND, theComponent, false,
					new FadeTracker.FadeTrackerCallback() {
						public void fadeEnded(FadeKind fadeKind) {
							theComponent.scrollRectToVisible(newRectangle);
							syncRectangle();
						}

						public void fadePerformed(FadeKind fadeKind,
								float fadeCycle10) {
							int x = (int) (oldRectangle.x + fadeCycle10
									* aDeltaX / 10.0);
							int y = (int) (oldRectangle.y + fadeCycle10
									* aDeltaY / 10.0);
							theComponent
									.scrollRectToVisible(new Rectangle(x, y,
											oldRectangle.width,
											oldRectangle.height));
							syncRectangle();
							// System.out.println(fadeCycle10 + ":" +
							// oldRectangle
							// + "-->" + newRectangle + ":"
							// + theComponent.getVisibleRect());
						}
					});
		} else {
			theComponent.scrollRectToVisible(newRectangle);
			syncRectangle();
		}
	}
}