/*
 * Copyright (c) 2005-2010 Flamingo 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 Flamingo 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.pushingpixels.flamingo.internal.utils;

import java.awt.*;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.util.List;

import javax.swing.*;
import javax.swing.plaf.*;

import org.pushingpixels.flamingo.api.common.AbstractCommandButton;
import org.pushingpixels.flamingo.api.common.JCommandButton;
import org.pushingpixels.flamingo.api.common.icon.ResizableIcon;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager;
import org.pushingpixels.flamingo.api.ribbon.*;
import org.pushingpixels.flamingo.api.ribbon.resize.IconRibbonBandResizePolicy;
import org.pushingpixels.flamingo.api.ribbon.resize.RibbonBandResizePolicy;
import org.pushingpixels.flamingo.internal.ui.ribbon.AbstractBandControlPanel;
import org.pushingpixels.flamingo.internal.ui.ribbon.JRibbonTaskToggleButton;
import org.pushingpixels.flamingo.internal.ui.ribbon.appmenu.JRibbonApplicationMenuButton;

/**
 * Helper utilities for Flamingo project. This class is for internal use only.
 * 
 * @author Kirill Grouchnikov
 */
public class FlamingoUtilities {
	/**
	 * Gets the component font.
	 * 
	 * @param comp
	 *            Component.
	 * @param keys
	 *            {@link UIManager} keys.
	 * @return If the component is not <code>null</code>, its font is returned.
	 *         Otherwise the first entry in {@link UIManager} which is a
	 *         {@link Font} is returned.
	 */
	public static FontUIResource getFont(Component comp, String... keys) {
		if (comp != null) {
			Font compFont = comp.getFont();
			if ((compFont != null) && !(compFont instanceof UIResource)) {
				return new FontUIResource(compFont);
			}
		}
		for (String key : keys) {
			Font font = UIManager.getFont(key);
			if (font != null) {
				if (font instanceof UIResource)
					return (FontUIResource) font;
				else
					return new FontUIResource(font);
			}
		}
		return null;
	}

	/**
	 * Gets the color based on the specified {@link UIManager} keys.
	 * 
	 * @param defaultColor
	 *            Default color to return if none of the {@link UIManager} keys
	 *            are present.
	 * @param keys
	 *            {@link UIManager} keys.
	 * @return The first entry in {@link UIManager} which is a color. If none,
	 *         then the default color is returned.
	 */
	public static Color getColor(Color defaultColor, String... keys) {
		for (String key : keys) {
			Color color = UIManager.getColor(key);
			if (color != null)
				return color;
		}
		return new ColorUIResource(defaultColor);
	}

	/**
	 * Returns a ribbon band expand icon.
	 * 
	 * @return Ribbon band expand icon.
	 */
	public static ResizableIcon getRibbonBandExpandIcon(
			AbstractRibbonBand ribbonBand) {
		boolean ltr = ribbonBand.getComponentOrientation().isLeftToRight();
		return new ArrowResizableIcon(9, ltr ? SwingConstants.EAST
				: SwingConstants.WEST);
	}

	/**
	 * Returns a popup action icon for the specific command button.
	 */
	public static ResizableIcon getCommandButtonPopupActionIcon(
			JCommandButton commandButton) {
		JCommandButton.CommandButtonPopupOrientationKind popupOrientationKind = ((JCommandButton) commandButton)
				.getPopupOrientationKind();
		switch (popupOrientationKind) {
		case DOWNWARD:
			return new ArrowResizableIcon.CommandButtonPopupIcon(9,
					SwingConstants.SOUTH);
		case SIDEWARD:
			return new ArrowResizableIcon.CommandButtonPopupIcon(
					9,
					commandButton.getComponentOrientation().isLeftToRight() ? SwingConstants.EAST
							: SwingConstants.WEST);
		}
		return null;
	}

	/**
	 * Creates a thumbnail of the specified width.
	 * 
	 * @param image
	 *            The original image.
	 * @param requestedThumbWidth
	 *            The width of the resulting thumbnail.
	 * @return Thumbnail of the specified width.
	 * @author Romain Guy
	 */
	public static BufferedImage createThumbnail(BufferedImage image,
			int requestedThumbWidth) {
		float ratio = (float) image.getWidth() / (float) image.getHeight();
		int width = image.getWidth();
		BufferedImage thumb = image;

		do {
			width /= 2;
			if (width < requestedThumbWidth) {
				width = requestedThumbWidth;
			}

			BufferedImage temp = new BufferedImage(width,
					(int) (width / ratio), BufferedImage.TYPE_INT_ARGB);
			Graphics2D g2 = temp.createGraphics();
			g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
					RenderingHints.VALUE_INTERPOLATION_BILINEAR);
			g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null);
			g2.dispose();

			thumb = temp;
		} while (width != requestedThumbWidth);

		return thumb;
	}

	/**
	 * Returns the outline of the ribbon border.
	 * 
	 * @param startX
	 *            The starting X of the ribbon area.
	 * @param endX
	 *            The ending X of the ribbon area.
	 * @param startSelectedX
	 *            The starting X of the toggle tab button of the selected task.
	 * @param endSelectedX
	 *            The ending X of the toggle tab button of the selected task.
	 * @param topY
	 *            The top Y of the ribbon area.
	 * @param bandTopY
	 *            The top Y of the ribbon band area.
	 * @param bottomY
	 *            The bottom Y of the ribbon area.
	 * @param radius
	 *            Corner radius.
	 * @return The outline of the ribbon border.
	 */
	public static GeneralPath getRibbonBorderOutline(int startX, int endX,
			int startSelectedX, int endSelectedX, int topY, int bandTopY,
			int bottomY, float radius) {
		int height = bottomY - topY;
		GeneralPath result = new GeneralPath();
		float radius3 = (float) (radius / (1.5 * Math.pow(height, 0.5)));

		// start in the top left corner at the end of the curve
		result.moveTo(startX + radius, bandTopY);

		// move to the bottom start of the selected tab and curve up
		result.lineTo(startSelectedX - radius, bandTopY);
		// result.quadTo(startSelectedX - radius3, bandTopY - radius3,
		// startSelectedX, bandTopY - radius);

		// move to the top start of the selected tab and curve right
		// result.lineTo(startSelectedX, topY + radius);
		// result.quadTo(startSelectedX + radius3, topY + radius3,
		// startSelectedX
		// + radius, topY);

		// move to the top end of the selected tab and curve down
		// result.lineTo(endSelectedX - radius - 1, topY);
		// result.quadTo(endSelectedX + radius3 - 1, topY + radius3,
		// endSelectedX - 1, topY + radius);

		// move to the bottom end of the selected tab and curve right
		// result.lineTo(endSelectedX - 1, bandTopY - radius);
		// result.quadTo(endSelectedX + radius3 - 1, bandTopY - radius3,
		// endSelectedX + radius - 1, bandTopY);
		result.moveTo(endSelectedX + radius - 1, bandTopY);

		// move to the top right corner and curve down
		result.lineTo(endX - radius - 1, bandTopY);
		result.quadTo(endX - radius3 - 1, bandTopY + radius3, endX - 1,
				bandTopY + radius);

		// move to the bottom right corner and curve left
		result.lineTo(endX - 1, bottomY - radius - 1);
		result.quadTo(endX - radius3 - 1, bottomY - 1 - radius3, endX - radius
				- 1, bottomY - 1);

		// move to the bottom left corner and curve up
		result.lineTo(startX + radius, bottomY - 1);
		result.quadTo(startX + radius3, bottomY - 1 - radius3, startX, bottomY
				- radius - 1);

		// move to the top left corner and curve right
		result.lineTo(startX, bandTopY + radius);
		result.quadTo(startX + radius3, bandTopY + radius3, startX + radius,
				bandTopY);

		return result;
	}

	/**
	 * Returns the clip area of a task toggle button in ribbon component.
	 * 
	 * @param width
	 *            Toggle tab button width.
	 * @param height
	 *            Toggle tab button height.
	 * @param radius
	 *            Toggle tab button corner radius.
	 * @return Clip area of a toggle tab button in ribbon component.
	 */
	public static GeneralPath getRibbonTaskToggleButtonOutline(int width,
			int height, float radius) {
		GeneralPath result = new GeneralPath();
		float radius3 = (float) (radius / (1.5 * Math.pow(height, 0.5)));

		// start at the bottom left
		result.moveTo(0, height);

		// move to the top start and curve right
		result.lineTo(0, radius);
		result.quadTo(radius3, radius3, radius, 0);

		// move to the top end and curve down
		result.lineTo(width - radius - 1, 0);
		result.quadTo(width + radius3 - 1, radius3, width - 1, radius);

		// move to the bottom right end
		result.lineTo(width - 1, height);

		// move to the bottom left end
		result.lineTo(0, height);

		return result;
	}

	/**
	 * Returns the outline of in-ribbon gallery.
	 * 
	 * @param startX
	 *            Start X of the in-ribbon gallery.
	 * @param endX
	 *            End X of the in-ribbon gallery.
	 * @param topY
	 *            Top Y of the in-ribbon gallery.
	 * @param bottomY
	 *            Bottom Y of the in-ribbon gallery.
	 * @param radius
	 *            Corner radius.
	 * @return The outline of in-ribbon gallery.
	 */
	public static GeneralPath getRibbonGalleryOutline(int startX, int endX,
			int topY, int bottomY, float radius) {

		int height = bottomY - topY;
		GeneralPath result = new GeneralPath();
		float radius3 = (float) (radius / (1.5 * Math.pow(height, 0.5)));

		// start in the top left corner at the end of the curve
		result.moveTo(startX + radius, topY);

		// move to the top right corner and curve down
		result.lineTo(endX - radius - 1, topY);
		result.quadTo(endX - radius3 - 1, topY + radius3, endX - 1, topY
				+ radius);

		// move to the bottom right corner and curve left
		result.lineTo(endX - 1, bottomY - radius - 1);
		result.quadTo(endX - radius3 - 1, bottomY - 1 - radius3, endX - radius
				- 1, bottomY - 1);

		// move to the bottom left corner and curve up
		result.lineTo(startX + radius, bottomY - 1);
		result.quadTo(startX + radius3, bottomY - 1 - radius3, startX, bottomY
				- radius - 1);

		// move to the top left corner and curve right
		result.lineTo(startX, topY + radius);
		result.quadTo(startX + radius3, topY + radius3, startX + radius, topY);

		return result;
	}

	/**
	 * Clips string based on specified font metrics and available width (in
	 * pixels). Returns the clipped string, which contains the beginning and the
	 * end of the input string separated by ellipses (...) in case the string is
	 * too long to fit into the specified width, and the origianl string
	 * otherwise.
	 * 
	 * @param metrics
	 *            Font metrics.
	 * @param availableWidth
	 *            Available width in pixels.
	 * @param fullText
	 *            String to clip.
	 * @return The clipped string, which contains the beginning and the end of
	 *         the input string separated by ellipses (...) in case the string
	 *         is too long to fit into the specified width, and the origianl
	 *         string otherwise.
	 */
	public static String clipString(FontMetrics metrics, int availableWidth,
			String fullText) {

		if (metrics.stringWidth(fullText) <= availableWidth)
			return fullText;

		String ellipses = "...";
		int ellipsesWidth = metrics.stringWidth(ellipses);
		if (ellipsesWidth > availableWidth)
			return "";

		String starter = "";

		int w = fullText.length();
		String prevText = "";
		for (int i = 0; i < w; i++) {
			String newStarter = starter + fullText.charAt(i);
			String newText = newStarter + ellipses;
			if (metrics.stringWidth(newText) <= availableWidth) {
				starter = newStarter;
				prevText = newText;
				continue;
			}
			return prevText;
		}
		return fullText;
	}

	/**
	 * Retrieves transparent image of specified dimension.
	 * 
	 * @param width
	 *            Image width.
	 * @param height
	 *            Image height.
	 * @return Transparent image of specified dimension.
	 */
	public static BufferedImage getBlankImage(int width, int height) {
		GraphicsEnvironment e = GraphicsEnvironment
				.getLocalGraphicsEnvironment();
		GraphicsDevice d = e.getDefaultScreenDevice();
		GraphicsConfiguration c = d.getDefaultConfiguration();
		BufferedImage compatibleImage = c.createCompatibleImage(width, height,
				Transparency.TRANSLUCENT);
		return compatibleImage;
	}

	/**
	 * Returns the alpha version of the specified color.
	 * 
	 * @param color
	 *            Original color.
	 * @param alpha
	 *            Alpha channel value.
	 * @return Alpha version of the specified color.
	 */
	public static Color getAlphaColor(Color color, int alpha) {
		return new Color(color.getRed(), color.getGreen(), color.getBlue(),
				alpha);
	}

	public static int getHLayoutGap(AbstractCommandButton commandButton) {
		Font font = commandButton.getFont();
		if (font == null)
			font = UIManager.getFont("Button.font");
		return (int) Math.ceil(commandButton.getHGapScaleFactor()
				* (font.getSize() - 4) / 4);
	}

	public static int getVLayoutGap(AbstractCommandButton commandButton) {
		Font font = commandButton.getFont();
		if (font == null)
			font = UIManager.getFont("Button.font");
		return (int) Math.ceil(commandButton.getVGapScaleFactor()
				* (font.getSize() - 4) / 4);
	}

	public static boolean hasPopupAction(AbstractCommandButton commandButton) {
		if (commandButton instanceof JCommandButton) {
			JCommandButton jcb = (JCommandButton) commandButton;
			return jcb.getCommandButtonKind().hasPopup();
		}
		return false;
	}

	public static void updateRibbonFrameIconImages(JRibbonFrame ribbonFrame) {
		JRibbonApplicationMenuButton appMenuButton = getApplicationMenuButton(ribbonFrame);
		if (appMenuButton == null) {
			return;
		}

		ResizableIcon appIcon = ribbonFrame.getApplicationIcon();
		if (appIcon != null) {
			appMenuButton.setIcon(appIcon);
		}
	}

	public static JRibbonApplicationMenuButton getApplicationMenuButton(
			Component comp) {
		if (comp instanceof JRibbonApplicationMenuButton)
			return (JRibbonApplicationMenuButton) comp;
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++) {
				JRibbonApplicationMenuButton result = getApplicationMenuButton(cont
						.getComponent(i));
				if ((result != null) && result.isVisible())
					return result;
			}
		}
		return null;
	}

	public static void renderSurface(Graphics g, Container c, Rectangle rect,
			boolean toSimulateRollover, boolean hasTopBorder,
			boolean hasBottomBorder) {
		CellRendererPane buttonRendererPane = new CellRendererPane();
		JButton rendererButton = new JButton("");
		rendererButton.getModel().setRollover(toSimulateRollover);

		buttonRendererPane.setBounds(rect.x, rect.y, rect.width, rect.height);
		Graphics2D g2d = (Graphics2D) g.create();
		g2d.clipRect(rect.x, rect.y, rect.width, rect.height);
		buttonRendererPane.paintComponent(g2d, rendererButton, c, rect.x
				- rect.width / 2, rect.y - rect.height / 2, 2 * rect.width,
				2 * rect.height, true);

		g2d.setColor(FlamingoUtilities.getBorderColor());
		if (hasTopBorder) {
			g2d.drawLine(rect.x, rect.y, rect.x + rect.width - 1, rect.y);
		}
		if (hasBottomBorder) {
			g2d.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width
					- 1, rect.y + rect.height - 1);
		}
		g2d.dispose();
	}

	/**
	 * Returns lighter version of the specified color.
	 * 
	 * @param color
	 *            Color.
	 * @param diff
	 *            Difference factor (values closer to 1.0 will produce results
	 *            closer to white color).
	 * @return Lighter version of the specified color.
	 */
	public static Color getLighterColor(Color color, double diff) {
		int r = color.getRed() + (int) (diff * (255 - color.getRed()));
		int g = color.getGreen() + (int) (diff * (255 - color.getGreen()));
		int b = color.getBlue() + (int) (diff * (255 - color.getBlue()));
		return new Color(r, g, b);
	}

	public static Color getBorderColor() {
		return FlamingoUtilities.getColor(Color.gray,
				"TextField.inactiveForeground", "Button.disabledText",
				"ComboBox.disabledForeground");
	}

	public static boolean isShowingMinimizedRibbonInPopup(JRibbon ribbon) {
		List<PopupPanelManager.PopupInfo> popups = PopupPanelManager
				.defaultManager().getShownPath();
		if (popups.size() == 0)
			return false;

		for (PopupPanelManager.PopupInfo popup : popups) {
			JComponent originator = popup.getPopupOriginator();
			if (originator instanceof JRibbonTaskToggleButton) {
				return (ribbon == SwingUtilities.getAncestorOfClass(
						JRibbon.class, originator));
			}
		}
		return false;
	}

	public static boolean isShowingMinimizedRibbonInPopup(
			JRibbonTaskToggleButton taskToggleButton) {
		List<PopupPanelManager.PopupInfo> popups = PopupPanelManager
				.defaultManager().getShownPath();
		if (popups.size() == 0)
			return false;

		for (PopupPanelManager.PopupInfo popup : popups) {
			JComponent originator = popup.getPopupOriginator();
			if (originator == taskToggleButton)
				return true;
		}
		return false;
	}

	public static void checkResizePoliciesConsistency(
			AbstractRibbonBand ribbonBand) {
		Insets ins = ribbonBand.getInsets();
		AbstractBandControlPanel controlPanel = ribbonBand.getControlPanel();
		if (controlPanel == null)
			return;
		int height = controlPanel.getPreferredSize().height
				+ ribbonBand.getUI().getBandTitleHeight() + ins.top
				+ ins.bottom;
		List<RibbonBandResizePolicy> resizePolicies = ribbonBand
				.getResizePolicies();
		checkResizePoliciesConsistencyBase(ribbonBand);
		for (int i = 0; i < (resizePolicies.size() - 1); i++) {
			RibbonBandResizePolicy policy1 = resizePolicies.get(i);
			RibbonBandResizePolicy policy2 = resizePolicies.get(i + 1);
			int width1 = policy1.getPreferredWidth(height, 4);
			int width2 = policy2.getPreferredWidth(height, 4);
			if (width1 < width2) {
				// create the trace message
				StringBuilder builder = new StringBuilder();
				builder.append("Inconsistent preferred widths\n");
				builder.append("Ribbon band '" + ribbonBand.getTitle()
						+ "' has the following resize policies\n");
				for (int j = 0; j < resizePolicies.size(); j++) {
					RibbonBandResizePolicy policy = resizePolicies.get(j);
					int width = policy.getPreferredWidth(height, 4);
					builder.append("\t" + policy.getClass().getName()
							+ " with preferred width " + width + "\n");
				}
				builder.append(policy1.getClass().getName()
						+ " with pref width " + width1
						+ " is followed by resize policy "
						+ policy2.getClass().getName()
						+ " with larger pref width\n");

				throw new IllegalStateException(builder.toString());
			}
		}
	}

	public static void checkResizePoliciesConsistencyBase(
			AbstractRibbonBand ribbonBand) {
		List<RibbonBandResizePolicy> resizePolicies = ribbonBand
				.getResizePolicies();
		if (resizePolicies.size() == 0) {
			throw new IllegalStateException("Resize policy list is empty");
		}
		for (int i = 0; i < resizePolicies.size(); i++) {
			RibbonBandResizePolicy policy = resizePolicies.get(i);
			boolean isIcon = policy instanceof IconRibbonBandResizePolicy;
			if (isIcon && (i < (resizePolicies.size() - 1))) {
				throw new IllegalStateException(
						"Icon resize policy must be the last in the list");
			}
		}
	}
}
