/*******************************************************************************
 * Copyright (c) 2000, 2008 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;
}
}
