/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.draw2d;

import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;

import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;

/**
 * A figure that can display text and/or an image.
 */
public class Label extends Figure implements PositionConstants {

	private static String ELLIPSIS = "..."; //$NON-NLS-1$

	private Image icon;
	private String text = "";//$NON-NLS-1$
	private String subStringText;
	private Dimension textSize;
	private Dimension subStringTextSize;
	private Dimension iconSize = new Dimension(0, 0);
	private Point iconLocation;
	private Point textLocation;
	private int textAlignment = CENTER;
	private int iconAlignment = CENTER;
	private int labelAlignment = CENTER;
	private int textPlacement = EAST;
	private int iconTextGap = 3;

	/**
	 * Construct an empty Label.
	 * 
	 * @since 2.0
	 */
	public Label() {
	}

	/**
	 * Construct a Label with passed String as its text.
	 * 
	 * @param s
	 *            the label text
	 * @since 2.0
	 */
	public Label(String s) {
		setText(s);
	}

	/**
	 * Construct a Label with passed Image as its icon.
	 * 
	 * @param i
	 *            the label image
	 * @since 2.0
	 */
	public Label(Image i) {
		setIcon(i);
	}

	/**
	 * Construct a Label with passed String as text and passed Image as its
	 * icon.
	 * 
	 * @param s
	 *            the label text
	 * @param i
	 *            the label image
	 * @since 2.0
	 */
	public Label(String s, Image i) {
		setText(s);
		setIcon(i);
	}

	private void alignOnHeight(Point loc, Dimension size, int alignment) {
		Insets insets = getInsets();
		switch (alignment) {
		case TOP:
			loc.y = insets.top;
			break;
		case BOTTOM:
			loc.y = bounds.height - size.height - insets.bottom;
			break;
		default:
			loc.y = (bounds.height - size.height) / 2;
		}
	}

	private void alignOnWidth(Point loc, Dimension size, int alignment) {
		Insets insets = getInsets();
		switch (alignment) {
		case LEFT:
			loc.x = insets.left;
			break;
		case RIGHT:
			loc.x = bounds.width - size.width - insets.right;
			break;
		default:
			loc.x = (bounds.width - size.width) / 2;
		}
	}

	private void calculateAlignment() {
		switch (textPlacement) {
		case EAST:
		case WEST:
			alignOnHeight(textLocation, getTextSize(), textAlignment);
			alignOnHeight(iconLocation, getIconSize(), iconAlignment);
			break;
		case NORTH:
		case SOUTH:
			alignOnWidth(textLocation, getSubStringTextSize(), textAlignment);
			alignOnWidth(iconLocation, getIconSize(), iconAlignment);
			break;
		}
	}

	/**
	 * Calculates the size of the Label using the passed Dimension as the size
	 * of the Label's text.
	 * 
	 * @param txtSize
	 *            the precalculated size of the label's text
	 * @return the label's size
	 * @since 2.0
	 */
	protected Dimension calculateLabelSize(Dimension txtSize) {
		int gap = getIconTextGap();
		if (getIcon() == null || getText().equals("")) //$NON-NLS-1$
			gap = 0;
		Dimension d = new Dimension(0, 0);
		if (textPlacement == WEST || textPlacement == EAST) {
			d.width = getIconSize().width + gap + txtSize.width;
			d.height = Math.max(getIconSize().height, txtSize.height);
		} else {
			d.width = Math.max(getIconSize().width, txtSize.width);
			d.height = getIconSize().height + gap + txtSize.height;
		}
		return d;
	}

	private void calculateLocations() {
		textLocation = new Point();
		iconLocation = new Point();

		calculatePlacement();
		calculateAlignment();
		Dimension offset = getSize().getDifference(getPreferredSize());
		offset.width += getTextSize().width - getSubStringTextSize().width;
		switch (labelAlignment) {
		case CENTER:
			offset.scale(0.5f);
			break;
		case LEFT:
			offset.scale(0.0f);
			break;
		case RIGHT:
			offset.scale(1.0f);
			break;
		case TOP:
			offset.height = 0;
			offset.scale(0.5f);
			break;
		case BOTTOM:
			offset.height = offset.height * 2;
			offset.scale(0.5f);
			break;
		default:
			offset.scale(0.5f);
			break;
		}

		switch (textPlacement) {
		case EAST:
		case WEST:
			offset.height = 0;
			break;
		case NORTH:
		case SOUTH:
			offset.width = 0;
			break;
		}

		textLocation.translate(offset);
		iconLocation.translate(offset);
	}

	private void calculatePlacement() {
		int gap = getIconTextGap();
		if (icon == null || text.equals("")) //$NON-NLS-1$
			gap = 0;
		Insets insets = getInsets();

		switch (textPlacement) {
		case EAST:
			iconLocation.x = insets.left;
			textLocation.x = getIconSize().width + gap + insets.left;
			break;
		case WEST:
			textLocation.x = insets.left;
			iconLocation.x = getSubStringTextSize().width + gap + insets.left;
			break;
		case NORTH:
			textLocation.y = insets.top;
			iconLocation.y = getTextSize().height + gap + insets.top;
			break;
		case SOUTH:
			textLocation.y = getIconSize().height + gap + insets.top;
			iconLocation.y = insets.top;
		}
	}

	/**
	 * Calculates the size of the Label's text size. The text size calculated
	 * takes into consideration if the Label's text is currently truncated. If
	 * text size without considering current truncation is desired, use
	 * {@link #calculateTextSize()}.
	 * 
	 * @return the size of the label's text, taking into account truncation
	 * @since 2.0
	 */
	protected Dimension calculateSubStringTextSize() {
		return getTextUtilities().getTextExtents(getSubStringText(), getFont());
	}

	/**
	 * Calculates and returns the size of the Label's text. Note that this
	 * Dimension is calculated using the Label's full text, regardless of
	 * whether or not its text is currently truncated. If text size considering
	 * current truncation is desired, use {@link #calculateSubStringTextSize()}.
	 * 
	 * @return the size of the label's text, ignoring truncation
	 * @since 2.0
	 */
	protected Dimension calculateTextSize() {
		return getTextUtilities().getTextExtents(getText(), getFont());
	}

	private void clearLocations() {
		iconLocation = textLocation = null;
	}

	/**
	 * Returns the Label's icon.
	 * 
	 * @return the label icon
	 * @since 2.0
	 */
	public Image getIcon() {
		return icon;
	}

	/**
	 * Returns the current alignment of the Label's icon. The default is
	 * {@link PositionConstants#CENTER}.
	 * 
	 * @return the icon alignment
	 * @since 2.0
	 */
	public int getIconAlignment() {
		return iconAlignment;
	}

	/**
	 * Returns the bounds of the Label's icon.
	 * 
	 * @return the icon's bounds
	 * @since 2.0
	 */
	public Rectangle getIconBounds() {
		Rectangle bounds = getBounds();
		return new Rectangle(bounds.getLocation().translate(getIconLocation()),
				getIconSize());
	}

	/**
	 * Returns the location of the Label's icon relative to the Label.
	 * 
	 * @return the icon's location
	 * @since 2.0
	 */
	protected Point getIconLocation() {
		if (iconLocation == null)
			calculateLocations();
		return iconLocation;
	}

	/**
	 * Returns the gap in pixels between the Label's icon and its text.
	 * 
	 * @return the gap
	 * @since 2.0
	 */
	public int getIconTextGap() {
		return iconTextGap;
	}

	/**
	 * @see IFigure#getMinimumSize(int, int)
	 */
	public Dimension getMinimumSize(int w, int h) {
		if (minSize != null)
			return minSize;
		minSize = new Dimension();
		if (getLayoutManager() != null)
			minSize.setSize(getLayoutManager().getMinimumSize(this, w, h));

		Dimension labelSize = calculateLabelSize(getTextUtilities()
				.getTextExtents(getTruncationString(), getFont())
				.intersect(
						getTextUtilities().getTextExtents(getText(), getFont())));
		Insets insets = getInsets();
		labelSize.expand(insets.getWidth(), insets.getHeight());
		minSize.union(labelSize);
		return minSize;
	}

	/**
	 * @see IFigure#getPreferredSize(int, int)
	 */
	public Dimension getPreferredSize(int wHint, int hHint) {
		if (prefSize == null) {
			prefSize = calculateLabelSize(getTextSize());
			Insets insets = getInsets();
			prefSize.expand(insets.getWidth(), insets.getHeight());
			if (getLayoutManager() != null)
				prefSize.union(getLayoutManager().getPreferredSize(this, wHint,
						hHint));
		}
		if (wHint >= 0 && wHint < prefSize.width) {
			Dimension minSize = getMinimumSize(wHint, hHint);
			Dimension result = prefSize.getCopy();
			result.width = Math.min(result.width, wHint);
			result.width = Math.max(minSize.width, result.width);
			return result;
		}
		return prefSize;
	}

	/**
	 * Calculates the amount of the Label's current text will fit in the Label,
	 * including an elipsis "..." if truncation is required.
	 * 
	 * @return the substring
	 * @since 2.0
	 */
	public String getSubStringText() {
		if (subStringText != null)
			return subStringText;

		subStringText = text;
		int widthShrink = getPreferredSize().width - getSize().width;
		if (widthShrink <= 0)
			return subStringText;

		Dimension effectiveSize = getTextSize().getExpanded(-widthShrink, 0);
		Font currentFont = getFont();
		int dotsWidth = getTextUtilities().getTextExtents(
				getTruncationString(), currentFont).width;

		if (effectiveSize.width < dotsWidth)
			effectiveSize.width = dotsWidth;

		int subStringLength = getTextUtilities().getLargestSubstringConfinedTo(
				text, currentFont, effectiveSize.width - dotsWidth);
		subStringText = new String(text.substring(0, subStringLength)
				+ getTruncationString());
		return subStringText;
	}

	/**
	 * Returns the size of the Label's current text. If the text is currently
	 * truncated, the truncated text with its ellipsis is used to calculate the
	 * size.
	 * 
	 * @return the size of this label's text, taking into account truncation
	 * @since 2.0
	 */
	protected Dimension getSubStringTextSize() {
		if (subStringTextSize == null)
			subStringTextSize = calculateSubStringTextSize();
		return subStringTextSize;
	}

	/**
	 * Returns the text of the label. Note that this is the complete text of the
	 * label, regardless of whether it is currently being truncated. Call
	 * {@link #getSubStringText()} to return the label's current text contents
	 * with truncation considered.
	 * 
	 * @return the complete text of this label
	 * @since 2.0
	 */
	public String getText() {
		return text;
	}

	/**
	 * Returns the current alignment of the Label's text. The default text
	 * alignment is {@link PositionConstants#CENTER}.
	 * 
	 * @return the text alignment
	 */
	public int getTextAlignment() {
		return textAlignment;
	}

	/**
	 * Returns the bounds of the label's text. Note that the bounds are
	 * calculated using the label's complete text regardless of whether the
	 * label's text is currently truncated.
	 * 
	 * @return the bounds of this label's complete text
	 * @since 2.0
	 */
	public Rectangle getTextBounds() {
		Rectangle bounds = getBounds();
		return new Rectangle(bounds.getLocation().translate(getTextLocation()),
				textSize);
	}

	/**
	 * Returns the location of the label's text relative to the label.
	 * 
	 * @return the text location
	 * @since 2.0
	 */
	protected Point getTextLocation() {
		if (textLocation != null)
			return textLocation;
		calculateLocations();
		return textLocation;
	}

	/**
	 * Returns the current placement of the label's text relative to its icon.
	 * The default text placement is {@link PositionConstants#EAST}.
	 * 
	 * @return the text placement
	 * @since 2.0
	 */
	public int getTextPlacement() {
		return textPlacement;
	}

	/**
	 * Returns the size of the label's complete text. Note that the text used to
	 * make this calculation is the label's full text, regardless of whether the
	 * label's text is currently being truncated and is displaying an ellipsis.
	 * If the size considering current truncation is desired, call
	 * {@link #getSubStringTextSize()}.
	 * 
	 * @return the size of this label's complete text
	 * @since 2.0
	 */
	protected Dimension getTextSize() {
		if (textSize == null)
			textSize = calculateTextSize();
		return textSize;
	}

	/**
	 * @see IFigure#invalidate()
	 */
	public void invalidate() {
		prefSize = null;
		minSize = null;
		clearLocations();
		textSize = null;
		subStringTextSize = null;
		subStringText = null;
		super.invalidate();
	}

	/**
	 * Returns <code>true</code> if the label's text is currently truncated and
	 * is displaying an ellipsis, <code>false</code> otherwise.
	 * 
	 * @return <code>true</code> if the label's text is truncated
	 * @since 2.0
	 */
	public boolean isTextTruncated() {
		return !getSubStringText().equals(getText());
	}

	/**
	 * @see Figure#paintFigure(Graphics)
	 */
	protected void paintFigure(Graphics graphics) {
		if (isOpaque())
			super.paintFigure(graphics);
		Rectangle bounds = getBounds();
		graphics.translate(bounds.x, bounds.y);
		if (icon != null)
			graphics.drawImage(icon, getIconLocation());
		if (!isEnabled()) {
			graphics.translate(1, 1);
			graphics.setForegroundColor(ColorConstants.buttonLightest);
			graphics.drawText(getSubStringText(), getTextLocation());
			graphics.translate(-1, -1);
			graphics.setForegroundColor(ColorConstants.buttonDarker);
		}
		graphics.drawText(getSubStringText(), getTextLocation());
		graphics.translate(-bounds.x, -bounds.y);
	}

	/**
	 * Sets the label's icon to the passed image.
	 * 
	 * @param image
	 *            the new label image
	 * @since 2.0
	 */
	public void setIcon(Image image) {
		if (icon == image)
			return;
		icon = image;
		// Call repaint, in case the image dimensions are the same.
		repaint();
		if (icon == null)
			setIconDimension(new Dimension());
		else
			setIconDimension(new Dimension(image));
	}

	/**
	 * This method sets the alignment of the icon within the bounds of the
	 * label. If the label is larger than the icon, then the icon will be
	 * aligned according to this alignment. Valid values are:
	 * <UL>
	 * <LI><EM>{@link PositionConstants#CENTER}</EM>
	 * <LI>{@link PositionConstants#TOP}
	 * <LI>{@link PositionConstants#BOTTOM}
	 * <LI>{@link PositionConstants#LEFT}
	 * <LI>{@link PositionConstants#RIGHT}
	 * </UL>
	 * 
	 * @param align
	 *            the icon alignment
	 * @since 2.0
	 */
	public void setIconAlignment(int align) {
		if (iconAlignment == align)
			return;
		iconAlignment = align;
		clearLocations();
		repaint();
	}

	/**
	 * Sets the label's icon size to the passed Dimension.
	 * 
	 * @param d
	 *            the new icon size
	 * @deprecated the icon is automatically displayed at 1:1
	 * @since 2.0
	 */
	public void setIconDimension(Dimension d) {
		if (d.equals(getIconSize()))
			return;
		iconSize = d;
		revalidate();
	}

	/**
	 * Sets the gap in pixels between the label's icon and text to the passed
	 * value. The default is 4.
	 * 
	 * @param gap
	 *            the gap
	 * @since 2.0
	 */
	public void setIconTextGap(int gap) {
		if (iconTextGap == gap)
			return;
		iconTextGap = gap;
		repaint();
		revalidate();
	}

	/**
	 * Sets the alignment of the label (icon and text) within the figure. If
	 * this figure's bounds are larger than the size needed to display the
	 * label, the label will be aligned accordingly. Valid values are:
	 * <UL>
	 * <LI><EM>{@link PositionConstants#CENTER}</EM>
	 * <LI>{@link PositionConstants#TOP}
	 * <LI>{@link PositionConstants#BOTTOM}
	 * <LI>{@link PositionConstants#LEFT}
	 * <LI>{@link PositionConstants#RIGHT}
	 * </UL>
	 * 
	 * @param align
	 *            label alignment
	 */
	public void setLabelAlignment(int align) {
		if (labelAlignment == align)
			return;
		labelAlignment = align;
		clearLocations();
		repaint();
	}

	/**
	 * Sets the label's text.
	 * 
	 * @param s
	 *            the new label text
	 * @since 2.0
	 */
	public void setText(String s) {
		// "text" will never be null.
		if (s == null)
			s = "";//$NON-NLS-1$
		if (text.equals(s))
			return;
		text = s;
		revalidate();
		repaint();
	}

	/**
	 * Sets the alignment of the text relative to the icon within the label. The
	 * text alignment must be orthogonal to the text placement. For example, if
	 * the placement is EAST, then the text can be aligned using TOP, CENTER, or
	 * BOTTOM. Valid values are:
	 * <UL>
	 * <LI><EM>{@link PositionConstants#CENTER}</EM>
	 * <LI>{@link PositionConstants#TOP}
	 * <LI>{@link PositionConstants#BOTTOM}
	 * <LI>{@link PositionConstants#LEFT}
	 * <LI>{@link PositionConstants#RIGHT}
	 * </UL>
	 * 
	 * @see #setLabelAlignment(int)
	 * @param align
	 *            the text alignment
	 * @since 2.0
	 */
	public void setTextAlignment(int align) {
		if (textAlignment == align)
			return;
		textAlignment = align;
		clearLocations();
		repaint();
	}

	/**
	 * Sets the placement of the text relative to the icon within the label.
	 * Valid values are:
	 * <UL>
	 * <LI><EM>{@link PositionConstants#EAST}</EM>
	 * <LI>{@link PositionConstants#NORTH}
	 * <LI>{@link PositionConstants#SOUTH}
	 * <LI>{@link PositionConstants#WEST}
	 * </UL>
	 * 
	 * @param where
	 *            the text placement
	 * @since 2.0
	 */
	public void setTextPlacement(int where) {
		if (textPlacement == where)
			return;
		textPlacement = where;
		revalidate();
		repaint();
	}

	/**
	 * Gets the <code>TextUtilities</code> instance to be used in measurement
	 * calculations.
	 * 
	 * @return a <code>TextUtilities</code> instance
	 * @since 3.4
	 */
	public TextUtilities getTextUtilities() {
		return TextUtilities.INSTANCE;
	}

	/**
	 * Gets the string that will be appended to the text when the label is
	 * truncated. By default, this returns an ellipsis.
	 * 
	 * @return the string to append to the text when truncated
	 * @since 3.4
	 */
	protected String getTruncationString() {
		return ELLIPSIS;
	}

	/**
	 * Gets the icon size
	 * 
	 * @return the icon size
	 * @since 3.4
	 */
	protected Dimension getIconSize() {
		return iconSize;
	}

	/**
	 * Returns the alignment of the entire label (icon and text). The default
	 * label alignment is {@link PositionConstants#CENTER}.
	 * 
	 * @return the label alignment
	 * @since 3.5
	 */
	public int getLabelAlignment() {
		return labelAlignment;
	}
}
