package net.bull.javamelody.swing.util;

import java.awt.AWTException;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Panel;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Window;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JApplet;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JInternalFrame;
import javax.swing.JRootPane;
import javax.swing.JWindow;
import javax.swing.LookAndFeel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingUtilities;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;

/**
 * The JGoodies Looks implementation of <code>PopupFactory</code>.
 * Adds a drop shadow border to all popups except ComboBox popups.
 *
 * @author JGoodies Karsten Lentzsch
 * @author Andrej Golovnin
 * @author Karsten Lentzsch
 * @version 1.2
 *
 * @see java.awt.AWTPermission
 * @see java.awt.Robot
 * @see javax.swing.Popup
 * @see LookAndFeel#initialize
 * @see LookAndFeel#uninitialize
 */
public final class ShadowPopupFactory extends PopupFactory {

	/**
	 * In the case of heavy weight popups, snapshots of the screen background
	 * will be stored as client properties of the popup contents' parent.
	 * These snapshots will be used by the popup border to simulate the drop
	 * shadow effect. The two following constants define the names of
	 * these client properties.
	 */
	static final String PROP_HORIZONTAL_BACKGROUND = "hShadowBg";
	static final String PROP_VERTICAL_BACKGROUND = "vShadowBg";

	/**
	 * The 'scratch pad' objects used to calculate dirty regions of
	 * the screen snapshots.
	 */
	static final Point POINT = new Point();
	static final Rectangle RECT = new Rectangle();

	/**
	 * The PopupFactory used before this PopupFactory has been installed
	 * in <code>#install</code>. Used to restored the original state
	 * in <code>#uninstall</code>.
	 */
	private final PopupFactory storedFactory;

	// Instance Creation ******************************************************

	private ShadowPopupFactory(PopupFactory storedFactory) {
		super();
		this.storedFactory = storedFactory;
	}

	// API ********************************************************************

	/**
	 * Installs the ShadowPopupFactory as the shared popup factory
	 * on non-Mac platforms. Also stores the previously set factory,
	 * so that it can be restored in <code>#uninstall</code>.<p>
	 *
	 * In some Mac Java environments the popup factory throws
	 * a NullPointerException when we call <code>#getPopup</code>.<p>
	 *
	 * The Mac case shows that we may have problems replacing
	 * non PopupFactory instances. Therefore we should consider
	 * replacing only instances of PopupFactory.
	 *
	 * @see #uninstall()
	 */
	public static void install() {
		final String os = System.getProperty("os.name");
		final boolean macintosh = os != null && os.indexOf("Mac") != -1;
		if (macintosh) {
			return;
		}

		final PopupFactory factory = PopupFactory.getSharedInstance();
		if (factory instanceof ShadowPopupFactory) {
			return;
		}

		PopupFactory.setSharedInstance(new ShadowPopupFactory(factory));
	}

	/**
	 * Uninstalls the ShadowPopupFactory and restores the original
	 * popup factory as the new shared popup factory.
	 *
	 * @see #install()
	 */
	public static void uninstall() {
		final PopupFactory factory = PopupFactory.getSharedInstance();
		if (!(factory instanceof ShadowPopupFactory)) {
			return;
		}

		final PopupFactory stored = ((ShadowPopupFactory) factory).storedFactory;
		PopupFactory.setSharedInstance(stored);
	}

	/**
	 * Creates a <code>Popup</code> for the Component <code>owner</code>
	 * containing the Component <code>contents</code>. In addition to
	 * the superclass behavior, we try to return a Popup that has a drop shadow,
	 * if popup drop shadows are active - as returned by
	 * <code>Options#isPopupDropShadowActive</code>.<p>
	 *
	 * <code>owner</code> is used to determine which <code>Window</code> the new
	 * <code>Popup</code> will parent the <code>Component</code> the
	 * <code>Popup</code> creates to. A null <code>owner</code> implies there
	 * is no valid parent. <code>x</code> and
	 * <code>y</code> specify the preferred initial location to place
	 * the <code>Popup</code> at. Based on screen size, or other paramaters,
	 * the <code>Popup</code> may not display at <code>x</code> and
	 * <code>y</code>.<p>
	 *
	 * We invoke the super <code>#getPopup</code>, not the one in the
	 * stored factory, because the popup type is set in this instance,
	 * not in the stored one.
	 *
	 * @param owner    Component mouse coordinates are relative to, may be null
	 * @param contents Contents of the Popup
	 * @param x        Initial x screen coordinate
	 * @param y        Initial y screen coordinate
	 * @return Popup containing Contents
	 */
	@Override
	public Popup getPopup(Component owner, Component contents, int x, int y) {
		final Popup popup = super.getPopup(owner, contents, x, y);
		return ShadowPopup.getInstance(owner, contents, x, y, popup);
	}

	/**
	 * Does all the magic for getting popups with drop shadows.
	 * It adds the drop shadow border to the Popup,
	 * in <code>#show</code> it snapshots the screen background as needed,
	 * and in <code>#hide</code> it cleans up all changes made before.
	 *
	 * @author Andrej Golovnin
	 * @version 1.4
	 */
	public static final class ShadowPopup extends Popup {

		/**
		 * Max number of items to store in the cache.
		 */
		private static final int MAX_CACHE_SIZE = 5;

		/**
		 * The cache to use for ShadowPopups.
		 */
		private static List<ShadowPopup> cache;

		/**
		 * The singleton instance used to draw all borders.
		 */
		private static final Border SHADOW_BORDER = ShadowPopupBorder.getInstance();

		/**
		 * The size of the drop shadow.
		 */
		private static final int SHADOW_SIZE = 5;

		/**
		 * The component mouse coordinates are relative to, may be null.
		 */
		private Component owner;

		/**
		 * The contents of the popup.
		 */
		private Component contents;

		/**
		 * The desired x and y location of the popup.
		 */
		private int x, y;

		/**
		 * The real popup. The #show() and #hide() methods will delegate
		 * all calls to these popup.
		 */
		private Popup popup;

		/**
		 * The border of the contents' parent replaced by SHADOW_BORDER.
		 */
		private Border oldBorder;

		/**
		 * The old value of the opaque property of the contents' parent.
		 */
		private boolean oldOpaque;

		/**
		 * The heavy weight container of the popup contents, may be null.
		 */
		private Container heavyWeightContainer;

		// Returns a previously used <code>ShadowPopup</code>, or a new one
		// if none of the popups have been recycled.
		static Popup getInstance(Component owner, Component contents, int x, int y,
				Popup delegate) {
			final ShadowPopup result;
			synchronized (ShadowPopup.class) {
				if (cache == null) {
					cache = new ArrayList<>(MAX_CACHE_SIZE);
				}
				if (!cache.isEmpty()) {
					result = cache.remove(0);
				} else {
					result = new ShadowPopup();
				}
			}
			result.reset(owner, contents, x, y, delegate);
			return result;
		}

		//Recycles the ShadowPopup.
		private static void recycle(ShadowPopup popup) {
			synchronized (ShadowPopup.class) {
				if (cache.size() < MAX_CACHE_SIZE) {
					cache.add(popup);
				}
			}
		}

		/**
		 * Hides and disposes of the <code>Popup</code>. Once a <code>Popup</code>
		 * has been disposed you should no longer invoke methods on it. A
		 * <code>dispose</code>d <code>Popup</code> may be reclaimed and later used
		 * based on the <code>PopupFactory</code>. As such, if you invoke methods
		 * on a <code>disposed</code> <code>Popup</code>, indeterminate
		 * behavior will result.<p>
		 *
		 * In addition to the superclass behavior, we reset the stored
		 * horizontal and vertical drop shadows - if any.
		 */
		@Override
		public void hide() {
			if (contents == null) {
				return;
			}

			final JComponent parent = (JComponent) contents.getParent();
			popup.hide();
			if (parent != null && parent.getBorder() == SHADOW_BORDER) {
				parent.setBorder(oldBorder);
				parent.setOpaque(oldOpaque);
				oldBorder = null;
				if (heavyWeightContainer != null) {
					parent.putClientProperty(PROP_HORIZONTAL_BACKGROUND, null);
					parent.putClientProperty(PROP_VERTICAL_BACKGROUND, null);
					heavyWeightContainer = null;
				}
			}
			owner = null;
			contents = null;
			popup = null;
			recycle(this);
		}

		/**
		 * Makes the <code>Popup</code> visible. If the popup has a
		 * heavy-weight container, we try to snapshot the background.
		 * If the <code>Popup</code> is currently visible, it remains visible.
		 */
		@Override
		public void show() {
			if (heavyWeightContainer != null) {
				snapshot();
			}
			popup.show();
		}

		/**
		 * Reinitializes this ShadowPopup using the given parameters.
		 *
		 * @param newOwner component mouse coordinates are relative to, may be null
		 * @param newContents the contents of the popup
		 * @param newX the desired x location of the popup
		 * @param newY the desired y location of the popup
		 * @param newPopup the popup to wrap
		 */
		private void reset(Component newOwner, Component newContents, int newX, int newY,
				Popup newPopup) {
			this.owner = newOwner;
			this.contents = newContents;
			this.popup = newPopup;
			this.x = newX;
			this.y = newY;
			if (newOwner instanceof JComboBox) {
				return;
			}
			// Do not install the shadow border when the contents
			// has a preferred size less than or equal to 0.
			// We can't use the size, because it is(0, 0) for new popups.
			final Dimension contentsPrefSize = newContents.getPreferredSize();
			if (contentsPrefSize.width <= 0 || contentsPrefSize.height <= 0) {
				return;
			}
			for (Container p = newContents.getParent(); p != null; p = p.getParent()) {
				if (p instanceof JWindow || p instanceof Panel) {
					// Workaround for the gray rect problem.
					p.setBackground(newContents.getBackground());
					heavyWeightContainer = p;
					break;
				}
			}
			final JComponent parent = (JComponent) newContents.getParent();
			oldOpaque = parent.isOpaque();
			oldBorder = parent.getBorder();
			parent.setOpaque(false);
			parent.setBorder(SHADOW_BORDER);
			// Pack it because we have changed the border.
			if (heavyWeightContainer != null) {
				heavyWeightContainer.setSize(heavyWeightContainer.getPreferredSize());
			} else {
				parent.setSize(parent.getPreferredSize());
			}
		}

		/**
		 * Snapshots the background. The snapshots are stored as client
		 * properties of the contents' parent. The next time the border is drawn,
		 * this background will be used.<p>
		 *
		 * Uses a robot on the default screen device to capture the screen
		 * region under the drop shadow. Does <em>not</em> use the window's
		 * device, because that may be an outdated device (due to popup reuse)
		 * and the robot's origin seems to be adjusted with the default screen
		 * device.
		 *
		 * @return boolean
		 * @see #show()
		 */
		private boolean snapshot() {
			try {
				final Dimension size = heavyWeightContainer.getPreferredSize();
				final int width = size.width;
				final int height = size.height;

				// Avoid unnecessary and illegal screen captures
				// for degenerated popups.
				if (width <= 0 || height <= SHADOW_SIZE) {
					return false;
				}

				final Robot robot = new Robot(); // uses the default screen device

				RECT.setBounds(x, y + height - SHADOW_SIZE, width, SHADOW_SIZE);
				final BufferedImage hShadowBg = robot.createScreenCapture(RECT);

				RECT.setBounds(x + width - SHADOW_SIZE, y, SHADOW_SIZE, height - SHADOW_SIZE);
				final BufferedImage vShadowBg = robot.createScreenCapture(RECT);

				final JComponent parent = (JComponent) contents.getParent();
				parent.putClientProperty(PROP_HORIZONTAL_BACKGROUND, hShadowBg);
				parent.putClientProperty(PROP_VERTICAL_BACKGROUND, vShadowBg);

				final Container layeredPane = getLayeredPane();
				if (layeredPane == null) {
					// This could happen if owner is null.
					return false;
				}

				POINT.x = x;
				POINT.y = y;
				SwingUtilities.convertPointFromScreen(POINT, layeredPane);

				// If needed paint dirty region of the horizontal snapshot.
				RECT.x = POINT.x;
				RECT.y = POINT.y + height - SHADOW_SIZE;
				RECT.width = width;
				RECT.height = SHADOW_SIZE;

				paintShadow(hShadowBg, layeredPane);

				// If needed paint dirty region of the vertical snapshot.
				RECT.x = POINT.x + width - SHADOW_SIZE;
				RECT.y = POINT.y;
				RECT.width = SHADOW_SIZE;
				RECT.height = height - SHADOW_SIZE;

				paintShadow(vShadowBg, layeredPane);
			} catch (final AWTException e) {
				return true;
			} catch (final SecurityException e) {
				return true;
			}
			return false;
		}

		/**
		 * If needed paint dirty region of the snapshot
		 *
		 * @param shadowBg
		 *           BufferedImage
		 * @param layeredPane
		 *           Container
		 */
		private void paintShadow(final BufferedImage shadowBg, final Container layeredPane) {
			final int layeredPaneWidth = layeredPane.getWidth();
			final int layeredPaneHeight = layeredPane.getHeight();

			if (RECT.x + RECT.width > layeredPaneWidth) {
				RECT.width = layeredPaneWidth - RECT.x;
			}
			if (RECT.y + RECT.height > layeredPaneHeight) {
				RECT.height = layeredPaneHeight - RECT.y;
			}
			if (!RECT.isEmpty()) {
				final Graphics g = shadowBg.createGraphics();
				g.translate(-RECT.x, -RECT.y);
				g.setClip(RECT);
				if (layeredPane instanceof JComponent) {
					final JComponent c = (JComponent) layeredPane;
					final boolean doubleBuffered = c.isDoubleBuffered();
					c.setDoubleBuffered(false);
					c.paintAll(g);
					c.setDoubleBuffered(doubleBuffered);
				} else {
					layeredPane.paintAll(g);
				}
				g.dispose();
			}
		}

		/**
		 * @return the top level layered pane which contains the owner.
		 */
		private Container getLayeredPane() {
			// The code below is copied from PopupFactory#LightWeightPopup#show()
			Container parent = null;
			if (owner != null) {
				parent = owner instanceof Container ? (Container) owner : owner.getParent();
			}
			// Try to find a JLayeredPane and Window to add
			for (Container p = parent; p != null; p = p.getParent()) {
				if (p instanceof JRootPane) {
					if (p.getParent() instanceof JInternalFrame) {
						continue;
					}
					parent = ((JRootPane) p).getLayeredPane();
					// Continue, so that if there is a higher JRootPane, we'll
					// pick it up.
				} else if (p instanceof Window) {
					if (parent == null) {
						parent = p;
					}
					break;
				} else if (p instanceof JApplet) {
					// Painting code stops at Applets, we don't want
					// to add to a Component above an Applet otherwise
					// you'll never see it painted.
					break;
				}
			}
			return parent;
		}
	}

	/**
	 * A border with a drop shadow intended to be used as the outer border
	 * of popups. Can paint the screen background if used with heavy-weight
	 * popup windows.
	 *
	 * @author Stefan Matthias Aust
	 * @author Karsten Lentzsch
	 * @author Andrej Golovnin
	 * @version 1.2
	 */
	public static class ShadowPopupBorder extends AbstractBorder {

		/**
		 *
		 */
		private static final long serialVersionUID = -8512231832213353638L;

		/**
		 * The drop shadow needs 5 pixels at the bottom and the right hand side.
		 */
		private static final int SHADOW_SIZE = 5;

		/**
		 * The singleton instance used to draw all borders.
		 */
		private static final ShadowPopupBorder INSTANCE = new ShadowPopupBorder();

		/**
		 * The drop shadow is created from a PNG image with 8 bit alpha channel.
		 */
		@SuppressWarnings("all")
		private static final Image SHADOW = new ImageIcon(
				ShadowPopupBorder.class.getResource("/icons/shadow.png")).getImage();

		// Instance Creation *****************************************************

		/**
		 * Returns the singleton instance used to draw all borders.
		 * @return ShadowPopupBorder
		 */
		public static ShadowPopupBorder getInstance() {
			return INSTANCE;
		}

		/**
		 * Paints the border for the specified component with the specified position and size.
		 * @param c the component for which this border is being painted
		 * @param g the paint graphics
		 * @param x the x position of the painted border
		 * @param y the y position of the painted border
		 * @param width the width of the painted border
		 * @param height the height of the painted border
		 */
		@Override
		public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
			// fake drop shadow effect in case of heavy weight popups
			final JComponent popup = (JComponent) c;
			final Image hShadowBg = (Image) popup.getClientProperty(PROP_HORIZONTAL_BACKGROUND);
			if (hShadowBg != null) {
				g.drawImage(hShadowBg, x, y + height - 5, c);
			}
			final Image vShadowBg = (Image) popup.getClientProperty(PROP_VERTICAL_BACKGROUND);
			if (vShadowBg != null) {
				g.drawImage(vShadowBg, x + width - 5, y, c);
			}

			// draw drop shadow
			g.drawImage(SHADOW, x + 5, y + height - 5, x + 10, y + height, 0, 6, 5, 11, null, c);
			g.drawImage(SHADOW, x + 10, y + height - 5, x + width - 5, y + height, 5, 6, 6, 11,
					null, c);
			g.drawImage(SHADOW, x + width - 5, y + 5, x + width, y + 10, 6, 0, 11, 5, null, c);
			g.drawImage(SHADOW, x + width - 5, y + 10, x + width, y + height - 5, 6, 5, 11, 6, null,
					c);
			g.drawImage(SHADOW, x + width - 5, y + height - 5, x + width, y + height, 6, 6, 11, 11,
					null, c);
		}

		/**
		 * Returns the insets of the border.
		 * @param c nothing
		 * @return Insets
		 */
		@Override
		public Insets getBorderInsets(Component c) {
			return new Insets(0, 0, SHADOW_SIZE, SHADOW_SIZE);
		}

		/**
		 * Reinitializes the insets parameter with this Border's current Insets.
		 * @param c the component for which this border insets value applies
		 * @param insets the object to be reinitialized
		 * @return the <code>insets</code> object
		 */
		@Override
		public Insets getBorderInsets(Component c, Insets insets) {
			insets.left = 0;
			insets.top = 0;
			insets.right = SHADOW_SIZE;
			insets.bottom = SHADOW_SIZE;
			return insets;
		}
	}
}
