/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 461261
 *     Loris Securo <lorissek@gmail.com> - Bug 472668
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.painter.cell;

import java.util.Map;
import java.util.WeakHashMap;

import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.layer.cell.CellDisplayConversionUtils;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.style.TextDecorationEnum;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;

/**
 * Abstract TextPainter the contains general methods for drawing text into a
 * cell. Can handle word wrapping and/or word cutting and/or automatic
 * calculation and resizing of the cell height or width if the text does not fit
 * into the cell.
 */
public abstract class AbstractTextPainter extends BackgroundPainter {

    public static final String EMPTY = ""; //$NON-NLS-1$
    public static final String DOT = "..."; //$NON-NLS-1$

    /**
     * The regular expression to find predefined new lines in the text to show.
     * Is used for word wrapping to preserve user defined new lines. To be
     * platform independent \n and \r and the combination of both are used to
     * find user defined new lines.
     */
    public static final String NEW_LINE_REGEX = "\\n\\r|\\r\\n|\\n|\\r"; //$NON-NLS-1$

    public static final String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$

    /**
     * @since 1.5
     */
    protected boolean wordWrapping = false;
    protected boolean wrapText;
    protected final boolean paintBg;
    protected boolean paintFg = true;
    protected int spacing = 5;
    /**
     * @since 1.5
     */
    protected int lineSpacing = 0;
    // can only grow but will not calculate the minimal length
    protected boolean calculateByTextLength;
    protected boolean calculateByTextHeight;
    private boolean underline;
    private boolean strikethrough;

    private boolean trimText = true;

    private Color originalBackground;
    private Color originalForeground;
    private Font originalFont;

    private static Map<String, Integer> temporaryMap = new WeakHashMap<>();
    private static Map<org.eclipse.swt.graphics.Font, FontData[]> fontDataCache = new WeakHashMap<>();

    public AbstractTextPainter() {
        this(false, true);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg) {
        this(wrapText, paintBg, 0);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     * @param spacing
     *            The space between text and cell border
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing) {
        this(wrapText, paintBg, spacing, false);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     * @param calculate
     *            tells the text painter to calculate the cell borders regarding
     *            the content
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg,
            boolean calculate) {
        this(wrapText, paintBg, 0, calculate);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     * @param calculateByTextLength
     *            tells the text painter to calculate the cell border by
     *            containing text length. For horizontal text rendering, this
     *            means the width of the cell is calculated by content, for
     *            vertical text rendering the height is calculated
     * @param calculateByTextHeight
     *            tells the text painter to calculate the cell border by
     *            containing text height. For horizontal text rendering, this
     *            means the height of the cell is calculated by content, for
     *            vertical text rendering the width is calculated
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg,
            boolean calculateByTextLength, boolean calculateByTextHeight) {
        this(wrapText, paintBg, 0, calculateByTextLength, calculateByTextHeight);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     * @param spacing
     *            The space between text and cell border
     * @param calculate
     *            tells the text painter to calculate the cell borders regarding
     *            the content
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing,
            boolean calculate) {
        this(wrapText, paintBg, spacing, calculate, calculate);
    }

    /**
     * @param wrapText
     *            split text over multiple lines
     * @param paintBg
     *            skips painting the background if is FALSE
     * @param spacing
     *            The space between text and cell border
     * @param calculateByTextLength
     *            tells the text painter to calculate the cell border by
     *            containing text length. For horizontal text rendering, this
     *            means the width of the cell is calculated by content, for
     *            vertical text rendering the height is calculated
     * @param calculateByTextHeight
     *            tells the text painter to calculate the cell border by
     *            containing text height. For horizontal text rendering, this
     *            means the height of the cell is calculated by content, for
     *            vertical text rendering the width is calculated
     */
    public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing,
            boolean calculateByTextLength, boolean calculateByTextHeight) {
        this.wrapText = wrapText;
        this.paintBg = paintBg;
        this.spacing = spacing;
        this.calculateByTextLength = calculateByTextLength;
        this.calculateByTextHeight = calculateByTextHeight;
    }

    /**
     * Convert the data value of the cell using the {@link IDisplayConverter}
     * from the {@link IConfigRegistry}
     *
     * @param cell
     *            The cell whose data value should be converted.
     * @param configRegistry
     *            The {@link IConfigRegistry} to retrieve the converter.
     * @return The data value converted to a String.
     */
    protected String convertDataType(ILayerCell cell, IConfigRegistry configRegistry) {
        return CellDisplayConversionUtils.convertDataType(cell, configRegistry);
    }

    /**
     * Setup the GC by the values defined in the given cell style.
     *
     * @param gc
     *            The {@link GC} that is used for rendering.
     * @param cellStyle
     *            The {@link IStyle} to retrieve the styling options.
     */
    public void setupGCFromConfig(GC gc, IStyle cellStyle) {
        Color fg = cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR);
        Color bg = cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR);
        Font font = cellStyle.getAttributeValue(CellStyleAttributes.FONT);

        // remember previous settings
        this.originalFont = gc.getFont();
        this.originalForeground = gc.getForeground();
        this.originalBackground = gc.getBackground();

        gc.setAntialias(GUIHelper.DEFAULT_ANTIALIAS);
        gc.setTextAntialias(GUIHelper.DEFAULT_TEXT_ANTIALIAS);
        gc.setFont(font);
        gc.setForeground(fg != null ? fg : GUIHelper.COLOR_LIST_FOREGROUND);
        gc.setBackground(bg != null ? bg : GUIHelper.COLOR_LIST_BACKGROUND);
    }

    /**
     * Reset the GC to the original values.
     *
     * @param gc
     *            The {@link GC} that is used for rendering.
     *
     * @since 1.4
     */
    public void resetGC(GC gc) {
        gc.setFont(this.originalFont);
        gc.setForeground(this.originalForeground);
        gc.setBackground(this.originalBackground);
    }

    /**
     * Checks if there is a underline text decoration configured within the
     * given cell style.
     *
     * @param cellStyle
     *            The cell style of the current cell to check for the text
     *            decoration.
     * @return <code>true</code> if there is a underline text decoration
     *         configured, <code>false</code> otherwise.
     */
    protected boolean renderUnderlined(IStyle cellStyle) {
        TextDecorationEnum decoration = cellStyle.getAttributeValue(CellStyleAttributes.TEXT_DECORATION);
        if (decoration != null) {
            return (decoration.equals(TextDecorationEnum.UNDERLINE)
                    || decoration.equals(TextDecorationEnum.UNDERLINE_STRIKETHROUGH));
        }
        return this.underline;
    }

    /**
     * Checks if there is a strikethrough text decoration configured within the
     * given cell style.
     *
     * @param cellStyle
     *            The cell style of the current cell to check for the text
     *            decoration.
     * @return <code>true</code> if there is a strikethrough text decoration
     *         configured, <code>false</code> otherwise.
     */
    protected boolean renderStrikethrough(IStyle cellStyle) {
        TextDecorationEnum decoration = cellStyle.getAttributeValue(CellStyleAttributes.TEXT_DECORATION);
        if (decoration != null) {
            return (decoration.equals(TextDecorationEnum.STRIKETHROUGH)
                    || decoration.equals(TextDecorationEnum.UNDERLINE_STRIKETHROUGH));
        }
        return this.strikethrough;
    }

    /**
     * Scans for new line characters and counts the number of lines for the
     * given text.
     *
     * @param text
     *            the text to scan
     * @return the number of lines for the given text
     */
    protected int getNumberOfNewLines(String text) {
        String[] lines = text.split(NEW_LINE_REGEX);
        return lines.length;
    }

    /**
     * Calculates the length of a given text by using the GC. To minimize the
     * count of calculations, the calculation result will be stored within a
     * Map, so the next time the length of the same text is asked for, the
     * result is only returned by cache and is not calculated again.
     *
     * @param gc
     *            the current GC
     * @param text
     *            the text to get the length for
     * @return the length of the text
     */
    protected int getLengthFromCache(GC gc, String text) {
        String originalString = text;
        StringBuilder buffer = new StringBuilder();
        buffer.append(text);
        if (gc.getFont() != null) {
            FontData[] datas = fontDataCache.get(gc.getFont());
            if (datas == null) {
                datas = gc.getFont().getFontData();
                fontDataCache.put(gc.getFont(), datas);
            }
            if (datas != null && datas.length > 0) {
                buffer.append(datas[0].getName());
                buffer.append(","); //$NON-NLS-1$
                buffer.append(datas[0].getHeight());
                buffer.append(","); //$NON-NLS-1$
                buffer.append(datas[0].getStyle());
            }
        }
        text = buffer.toString();
        Integer width = temporaryMap.computeIfAbsent(text, t -> Integer.valueOf(gc.textExtent(originalString).x));

        return width.intValue();
    }

    /**
     * Computes dependent on the configuration of the TextPainter the text to
     * display. If word wrapping is enabled new lines are inserted if the
     * available space is not enough. If calculation of available space is
     * enabled, the space is automatically widened for the text to display, and
     * if no calculation is enabled the text is cut and modified to end with
     * "..." to fit into the available space
     *
     * @param cell
     *            the current cell to paint
     * @param gc
     *            the current GC
     * @param availableLength
     *            the available space for the text to display
     * @param text
     *            the text that should be modified for display
     * @return the modified text
     */
    protected String getTextToDisplay(ILayerCell cell, GC gc, int availableLength, String text) {
        StringBuilder output = new StringBuilder();

        if (isTrimText()) {
            text = text.trim();
        }

        // take the whole width of the text
        int textLength = getLengthFromCache(gc, text);

        if (this.wordWrapping || (!this.calculateByTextLength && this.wrapText)) {
            String[] lines = text.split(NEW_LINE_REGEX);
            for (String line : lines) {
                if (output.length() > 0) {
                    output.append(LINE_SEPARATOR);
                }

                String[] words = line.split("\\s"); //$NON-NLS-1$

                // concat the words with spaces and newlines
                String computedText = ""; //$NON-NLS-1$
                for (String word : words) {
                    computedText = computeTextToDisplay(computedText, word, gc, availableLength);
                }

                output.append(computedText);
            }

        } else if (this.calculateByTextLength && this.wrapText) {
            if (availableLength < textLength) {
                // calculate length by finding the longest word in text
                textLength = (availableLength - (2 * this.spacing));

                String[] lines = text.split(NEW_LINE_REGEX);
                for (String line : lines) {
                    if (output.length() > 0) {
                        output.append(LINE_SEPARATOR);
                    }

                    String[] words = line.split("\\s"); //$NON-NLS-1$
                    for (String word : words) {
                        textLength = Math.max(textLength, getLengthFromCache(gc, word));
                    }

                    // concat the words with spaces and newlines to be always
                    // smaller then available
                    String computedText = ""; //$NON-NLS-1$
                    for (String word : words) {
                        computedText = computeTextToDisplay(computedText, word, gc, textLength);
                    }
                    output.append(computedText);
                }
            } else {
                output.append(text);
            }

            setNewMinLength(cell, textLength + calculatePadding(cell, availableLength));
        } else if (this.calculateByTextLength && !this.wrapText) {
            output.append(modifyTextToDisplay(text, gc, textLength));

            // add padding and spacing to textLength because they are needed for
            // correct sizing
            // padding can occur on using decorators like the
            // BeveledBorderDecorator or the PaddingDecorator
            setNewMinLength(cell, textLength + calculatePadding(cell, availableLength) + (2 * this.spacing));
        } else if (!this.calculateByTextLength && !this.wrapText) {
            output.append(modifyTextToDisplay(text, gc, availableLength + (2 * this.spacing)));
        }

        return output.toString();
    }

    /**
     * This method gets only called if text wrapping is enabled. Concatenates
     * the two given words by taking the availableSpace into account. If
     * concatenating those two words with a space as delimiter does fit into the
     * available space the return value is exactly this. Else instead of a space
     * there will be a new line character used as delimiter.
     *
     * @param one
     *            the first word or the whole text before the next word
     * @param two
     *            the next word to add to the first parameter
     * @param gc
     *            the current GC
     * @param availableSpace
     *            the available space
     * @return the concatenated String of the first two parameters
     */
    private String computeTextToDisplay(String one, String two, GC gc, int availableSpace) {
        String result = one;
        // if one is empty or one ends with newline just add two
        if (one == null || one.length() == 0 || one.endsWith(LINE_SEPARATOR)) {
            result += modifyTextToDisplay(two, gc, availableSpace);
        } else {
            // if one contains a newline
            if (one.indexOf(LINE_SEPARATOR) != -1) {
                // get the end of the last part after the last newline
                one = one.substring(one.lastIndexOf(LINE_SEPARATOR) + LINE_SEPARATOR.length());
            }

            if (getLengthFromCache(gc, one) == availableSpace
                    || getLengthFromCache(gc, one + " " + two) >= availableSpace) { //$NON-NLS-1$
                result += LINE_SEPARATOR;
                result += modifyTextToDisplay(two, gc, availableSpace);
            } else {
                result += ' ';
                result += two;
            }
        }
        return result;
    }

    /**
     * Checks if the given text is bigger than the available space. If not the
     * given text is simply returned without modification. If the text does not
     * fit into the available space, it will be modified by cutting and adding
     * three dots.
     *
     * @param text
     *            the text to compute
     * @param gc
     *            the current GC
     * @param availableLength
     *            the available space
     * @return the modified text if it is bigger than the available space or the
     *         text as it was given if it fits into the available space
     */
    private String modifyTextToDisplay(String text, GC gc, int availableLength) {
        // length of the text on GC taking new lines into account
        // this means the textLength is the value of the longest line
        int textLength = getLengthFromCache(gc, text);
        if (textLength > availableLength) {
            // as looking at the text length without taking new lines into
            // account we have to look at every line itself
            StringBuilder result = new StringBuilder();
            String[] lines = text.split(NEW_LINE_REGEX);
            for (String line : lines) {
                if (result.length() > 0) {
                    result.append(LINE_SEPARATOR);
                }

                // now modify every line if it is longer than the available
                // space this way every line will get ... if it doesn't fit
                int lineLength = getLengthFromCache(gc, line);
                if (lineLength > availableLength) {
                    if (!this.wordWrapping) {
                        int numExtraChars = 0;

                        int newStringLength = line.length();
                        String trialLabelText = line + DOT;
                        int newTextExtent = getLengthFromCache(gc, trialLabelText);

                        while (newTextExtent > availableLength + 1 && newStringLength > 0) {
                            double avgWidthPerChar = (double) newTextExtent / trialLabelText.length();
                            numExtraChars += 1 + (int) ((newTextExtent - availableLength) / avgWidthPerChar);

                            newStringLength = line.length() - numExtraChars;
                            if (newStringLength > 0) {
                                trialLabelText = line.substring(0, newStringLength) + DOT;
                                newTextExtent = getLengthFromCache(gc, trialLabelText);
                            }
                        }

                        if (numExtraChars > line.length()) {
                            numExtraChars = line.length();
                        }

                        // now we have gone too short, lets add chars one at a
                        // time to exceed the width...
                        String testString = line;
                        for (int i = 0; i < line.length(); i++) {
                            testString = line.substring(0, line.length() + i - numExtraChars) + DOT;
                            textLength = getLengthFromCache(gc, testString);

                            if (textLength >= availableLength) {

                                // now roll back one as this was the first
                                // number that exceeded
                                if (line.length() + i - numExtraChars < 1) {
                                    line = EMPTY;
                                } else {
                                    line = line.substring(0, line.length() + i - numExtraChars - 1) + DOT;
                                }
                                break;
                            }
                        }
                    } else {
                        StringBuilder output = new StringBuilder();
                        StringBuilder wrap = new StringBuilder();
                        for (char c : line.toCharArray()) {
                            wrap.append(c);
                            int length = getLengthFromCache(gc, wrap.toString());
                            if (length >= availableLength) {
                                output.append(wrap.substring(0, wrap.length() - 1)).append(LINE_SEPARATOR);
                                wrap = new StringBuilder();
                                wrap.append(c);
                            }
                        }
                        line = output.append(wrap).toString();
                    }
                }
                result.append(line);
            }

            return result.toString();
        }
        return text;
    }

    /**
     * This method gets only called if automatic length calculation is enabled.
     * Calculate the new cell width/height by using the given content length and
     * the difference from current cell width/height to available length. If the
     * calculated cell is greater than the current set contentLength, update the
     * contentLength and execute a corresponding resize command.
     *
     * @param cell
     *            the current cell that is painted
     * @param contentLength
     *            the length of the content
     */
    protected abstract void setNewMinLength(ILayerCell cell, int contentLength);

    /**
     * This method is used to determine the padding from the cell to the
     * available length. A padding can occur for example by using a
     * BeveledBorderDecorator or PaddingDecorator. This TextPainter is called
     * with the available space rectangle which is calculated by the wrapping
     * painters and decorators by subtracting paddings. As this TextPainter does
     * not know his wrapping painters and decorators the existing padding needs
     * to be calculated for automatic resizing. Abstract because a horizontal
     * TextPainter uses the width while a VerticalTextPainter uses the height of
     * the cell and the Rectangle.
     *
     * @param cell
     *            the current cell which should be resized
     * @param availableLength
     *            the length value that is available and was given into
     *            paintCell() as Rectangle argument
     * @return the padding between the current cell length - availableLength
     */
    protected abstract int calculatePadding(ILayerCell cell, int availableLength);

    /**
     * Set if the text should be rendered underlined or not.
     *
     * @param underline
     *            <code>true</code> if the text should be printed underlined,
     *            <code>false</code> if not
     */
    public void setUnderline(boolean underline) {
        this.underline = underline;
    }

    /**
     * Set if the text should be rendered strikethrough or not.
     *
     * @param strikethrough
     *            <code>true</code> if the text should be printed strikethrough,
     *            <code>false</code> if not
     */
    public void setStrikethrough(boolean strikethrough) {
        this.strikethrough = strikethrough;
    }

    /**
     * @return <code>true</code> if this text painter is calculating the cell
     *         dimensions by containing text length. For horizontal text
     *         rendering, this means the <b>width</b> of the cell is calculated
     *         by content, for vertical text rendering the <b>height</b> is
     *         calculated.
     */
    public boolean isCalculateByTextLength() {
        return this.calculateByTextLength;
    }

    /**
     * Configure whether the text painter should calculate the cell dimensions
     * by containing text length. For horizontal text rendering, this means the
     * <b>width</b> of the cell is calculated by content, for vertical text
     * rendering the <b>height</b> is calculated.
     *
     * @param calculateByTextLength
     *            <code>true</code> to calculate and modify the cell dimension
     *            according to the text length, <code>false</code> to not
     *            modifying the cell dimensions.
     */
    public void setCalculateByTextLength(boolean calculateByTextLength) {
        this.calculateByTextLength = calculateByTextLength;
    }

    /**
     * @return <code>true</code> if this text painter is calculating the cell
     *         dimensions by containing text height. For horizontal text
     *         rendering, this means the <b>height</b> of the cell is calculated
     *         by content, for vertical text rendering the <b>width</b> is
     *         calculated.
     */
    public boolean isCalculateByTextHeight() {
        return this.calculateByTextHeight;
    }

    /**
     * Configure whether the text painter should calculate the cell dimensions
     * by containing text height. For horizontal text rendering, this means the
     * <b>height</b> of the cell is calculated by content, for vertical text
     * rendering the <b>width</b> is calculated.
     *
     * @param calculateByTextHeight
     *            <code>true</code> to calculate and modify the cell dimension
     *            according to the text height, <code>false</code> to not
     *            modifying the cell dimensions.
     */
    public void setCalculateByTextHeight(boolean calculateByTextHeight) {
        this.calculateByTextHeight = calculateByTextHeight;
    }

    /**
     * @return <code>true</code> if the text to display should be trimmed,
     *         <code>false</code> if not. Default is <code>true</code>
     *
     * @since 1.3
     */
    public boolean isTrimText() {
        return this.trimText;
    }

    /**
     * @param trimText
     *            <code>true</code> if the text to display should be trimmed,
     *            <code>false</code> if not.
     *
     * @since 1.3
     */
    public void setTrimText(boolean trimText) {
        this.trimText = trimText;
    }

    /**
     *
     * @return <code>true</code> if the text will be wrapped, <code>false</code>
     *         if not.
     *
     * @since 1.4
     */
    public boolean isWrapText() {
        return this.wrapText;
    }

    /**
     *
     * @param wrapText
     *            <code>true</code> if the text should be wrapped,
     *            <code>false</code> if not.
     *
     * @since 1.4
     */
    public void setWrapText(boolean wrapText) {
        this.wrapText = wrapText;
    }

    /**
     * Render a decoration to the text, e.g. underline and/or strikethrough
     * lines.
     *
     * @param cellStyle
     *            The {@link IStyle} that contains the styling information for
     *            rendering.
     * @param gc
     *            the {@link GC} used to paint
     * @param x
     *            start x of the text
     * @param y
     *            start y of the text
     * @param length
     *            length of the text
     * @param fontHeight
     *            The height of the current font
     *
     * @since 1.4
     */
    protected void paintDecoration(IStyle cellStyle, GC gc, int x, int y, int length, int fontHeight) {
        boolean ul = renderUnderlined(cellStyle);
        boolean st = renderStrikethrough(cellStyle);

        if ((ul || st) && length > 0) {
            // check and draw underline and strikethrough separately
            // so it is possible to combine both
            if (ul) {
                // y = start y of text + font height - half of the font
                // descent so the underline is between baseline and bottom
                int underlineY = y + fontHeight - (gc.getFontMetrics().getDescent() / 2);
                gc.drawLine(x, underlineY, x + length, underlineY);
            }

            if (st) {
                // y = start y of text + half of font height + ascent
                // this way lower case characters are also strikethrough
                int strikeY = y + (fontHeight / 2) + (gc.getFontMetrics().getLeading() / 2);
                gc.drawLine(x, strikeY, x + length, strikeY);
            }
        }
    }

    /**
     * Return whether word wrapping is enabled or not.
     * <p>
     * Word wrapping is the wrapping behavior similar to spreadsheet
     * applications where words are wrapped if there is not enough space. Text
     * wrapping on the other hand only wraps whole words.
     * </p>
     * <p>
     * Enabling this feature could result in slow rendering performance. It is
     * therefore disabled by default.
     * </p>
     * <p>
     * <b>Note:</b> If word wrapping is enabled, features like automatic size
     * calculation by text length and text wrapping are ignored.
     * </p>
     *
     * @return <code>true</code> if word wrapping is enabled, <code>false</code>
     *         if not.
     *
     * @since 1.5
     */
    public boolean isWordWrapping() {
        return this.wordWrapping;
    }

    /**
     * Configure whether word wrapping should be enabled or not.
     * <p>
     * Word wrapping is the wrapping behavior similar to spreadsheet
     * applications where words are wrapped if there is not enough space. Text
     * wrapping on the other hand only wraps whole words.
     * </p>
     * <p>
     * Enabling this feature could result in slow rendering performance. It is
     * therefore disabled by default.
     * </p>
     * <p>
     * <b>Note:</b> If word wrapping is enabled, features like automatic size
     * calculation by text length and text wrapping are ignored.
     * </p>
     *
     * @param wordWrapping
     *            <code>true</code> to enable word wrapping, <code>false</code>
     *            to disable it.
     *
     * @since 1.5
     */
    public void setWordWrapping(boolean wordWrapping) {
        this.wordWrapping = wordWrapping;
    }

    /**
     * Return the number of pixels that are added between lines. Default is 0,
     * which means that the line height is defined by the font height only.
     *
     * @return The number of pixels that are added between lines
     *
     * @since 1.5
     */
    public int getLineSpacing() {
        return this.lineSpacing;
    }

    /**
     * Specify the number of pixels that should be added between lines. Default
     * is 0, which means that the line height is defined by the font height
     * only.
     *
     * @param spacing
     *            The number of pixels that should be added between lines
     *
     * @since 1.5
     */
    public void setLineSpacing(int spacing) {
        this.lineSpacing = spacing;
    }
}
