/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 * 
 * http://izpack.org/
 * http://izpack.codehaus.org/
 * 
 * Copyright 1997,2002 Elmar Grom
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.izforge.izpack.util;

import javax.swing.*;
import java.awt.*;
import java.util.Vector;

/*---------------------------------------------------------------------------*/
/**
 * <BR>
 * <code>MultiLineLabel</code> may be used in place of javax.swing.JLabel. <BR>
 * <BR>
 * This class implements a component that is capable of displaying multiple lines of text. Line
 * breaks are inserted automatically whenever a line of text extends beyond the predefined maximum
 * line length. Line breaks will only be inserted between words, except where a single word is
 * longer than the maximum line length. Line breaks may be forced at any location in the text by
 * inserting a newline (\n). White space that is not valuable (i.e. is placed at the beginning of a
 * new line or at the very beginning or end of the text) is removed. <br>
 * <br>
 * <b>Note:</b> you can set the maximum width of the label either through one of the constructors
 * or you can call <code>setMaxWidth()</code> explicitly. If this is not set,
 * <code>MultiLineLabel</code> will derive its width from the parent component.
 *
 * @author Elmar Grom
 * @version 1.0 / 04-13-02
 */
/*---------------------------------------------------------------------------*
 * Reviving some old code here that was written before there was swing.
 * The original was written to work with awt. I had to do some masaging to
 * make it a JComponent and I hope it behaves like a reasonably good mannered
 * swing component.
 *---------------------------------------------------------------------------*/
public class MultiLineLabel extends JComponent
{

    /**
     *
     */
    private static final long serialVersionUID = 4051045255031894837L;

    public static final int LEFT = 0; // alignment constants

    public static final int CENTER = 1;

    public static final int RIGHT = 2;

    public static final int DEFAULT_MARGIN = 10;

    public static final int DEFAULT_ALIGN = LEFT;

    public static final int LEAST_ALLOWED = 200; // default setting for

    // maxAllowed

    private static final int FOUND = 0; // constants for string search.

    private static final int NOT_FOUND = 1;

    private static final int NOT_DONE = 0;

    private static final int DONE = 1;

    private static final char[] WHITE_SPACE = {' ', '\n', '\t'};

    private static final char[] SPACES = {' ', '\t'};

    private static final char NEW_LINE = '\n';

    protected Vector<String> line = new Vector<String>();// text lines to display

    protected String labelText; // text lines to display

    protected int numLines; // the number of lines

    protected int marginHeight; // top and bottom margins

    protected int marginWidth; // left and right margins

    protected int lineHeight; // total height of the font

    protected int lineAscent; // font height above the baseline

    protected int lineDescent; // font hight below the baseline

    protected int[] lineWidth; // width of each line

    protected int maxWidth; // width of the widest line

    private int maxAllowed = LEAST_ALLOWED; // max width allowed to use

    private boolean maxAllowedSet = false; // signals if the max allowed width

    // has been explicitly set

    protected int alignment = LEFT; // default text alignment

    /*-------------------------------------------------------------------*/
    /**
     * Constructor
     *
     * @param text       the text to be displayed
     * @param horMargin  the horizontal margin for the label
     * @param vertMargin the vertical margin for the label
     * @param maxWidth   the maximum allowed width of the text
     * @param justify    the text alignment for the label
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    public MultiLineLabel(String text, int horMargin, int vertMargin, int maxWidth, int justify)
    {
        this.labelText = text;
        this.marginWidth = horMargin;
        this.marginHeight = vertMargin;
        this.maxAllowed = maxWidth;
        this.maxAllowedSet = true;
        this.alignment = justify;
    }

    /*-------------------------------------------------------------------*/
    /**
     * Constructor using default max-width and alignment.
     *
     * @param label        the text to be displayed
     * @param marginWidth  the horizontal margin for the label
     * @param marginHeight the vertical margin for the label
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    public MultiLineLabel(String label, int marginWidth, int marginHeight)
    {
        this.labelText = label;
        this.marginWidth = marginWidth;
        this.marginHeight = marginHeight;
    }

    /*-------------------------------------------------------------------*/
    /**
     * Constructor using default max-width, and margin.
     *
     * @param label     the text to be displayed
     * @param alignment the text alignment for the label
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    public MultiLineLabel(String label, int alignment)
    {
        this.labelText = label;
        this.alignment = alignment;
    }

    /*-------------------------------------------------------------------*/
    /**
     * Constructor using default max-width, alignment, and margin.
     *
     * @param label the text to be displayed
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    public MultiLineLabel(String label)
    {
        this.labelText = label;
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method searches the target string for occurences of any of the characters in the source
     * string. The return value is the position of the first hit. Based on the mode parameter the
     * hit position is either the position where any of the source characters first was found or the
     * first position where none of the source characters where found.
     *
     * @param target the text to be searched
     * @param start  the start position for the search
     * @param source the list of characters to be searched for
     * @param mode   the search mode FOUND = reports first found NOT_FOUND = reports first not found
     * @return position of the first occurence
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    int getPosition(String target, int start, char[] source, int mode)
    {
        int status;
        int position;
        int scan;
        int targetEnd;
        int sourceLength;
        char temp;

        targetEnd = (target.length() - 1);
        sourceLength = source.length;
        position = start;

        if (mode == FOUND)
        {
            status = NOT_DONE;
            while (status != DONE)
            {
                position++;
                if (!(position < targetEnd)) // end of string reached, the
                // next
                { // statement would cause a runtime error
                    return (targetEnd);
                }
                temp = target.charAt(position);
                for (scan = 0; scan < sourceLength; scan++) // walk through the
                // source
                { // string and compare each char
                    if (source[scan] == temp)
                    {
                        status = DONE;
                    }
                }
            }
            return (position);
        }
        else if (mode == NOT_FOUND)
        {
            status = NOT_DONE;
            while (status != DONE)
            {
                position++;
                if (!(position < targetEnd)) // end of string reached, the
                // next
                { // statement would cause a runtime error
                    return (targetEnd);
                }
                temp = target.charAt(position);
                status = DONE;
                for (scan = 0; scan < sourceLength; scan++) // walk through the
                // source
                { // string and compare each char
                    if (source[scan] == temp)
                    {
                        status = NOT_DONE;
                    }
                }
            }
            return (position);
        }
        return (0);
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method scans the input string until the max allowed width is reached. The return value
     * indicates the position just before this happens.
     *
     * @param word word to break
     * @return position character position just before the string is too long
     */
    /*-------------------------------------------------------------------*
     * <detailed description / implementation details if applicable>
     *-------------------------------------------------------------------*/
    int breakWord(String word, FontMetrics fm)
    {
        int width;
        int currentPos;
        int endPos;

        width = 0;
        currentPos = 0;
        endPos = word.length() - 1;

        // make sure we don't end up with a negative position
        if (endPos <= 0)
        {
            return (currentPos);
        }
        // seek the position where the word first is longer than allowed
        while ((width < maxAllowed) && (currentPos < endPos))
        {
            currentPos++;
            width = fm.stringWidth(labelText.substring(0, currentPos));
        }
        // adjust to get the chatacter just before (this should make it a bit
        // shorter than allowed!)
        if (currentPos != endPos)
        {
            currentPos--;
        }
        return (currentPos);
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method breaks the label text up into multiple lines of text. Line breaks are established
     * based on the maximum available space. A new line is started whenever a line break is
     * encountered, even if the permissible length is not yet reached. Words are broken only if a
     * single word happens to be longer than one line.
     */
    /*-------------------------------------------------------------------*/
    private void divideLabel()
    {
        int width;
        int startPos;
        int currentPos;
        int lastPos;
        int endPos;

        line.clear();
        FontMetrics fm = this.getFontMetrics(this.getFont());

        startPos = 0;
        currentPos = startPos;
        lastPos = currentPos;
        endPos = (labelText.length() - 1);

        while (currentPos < endPos)
        {
            width = 0;
            // ----------------------------------------------------------------
            // find the first substring that occupies more than the granted
            // space.
            // Break at the end of the string or a line break
            // ----------------------------------------------------------------
            while ((width < maxAllowed) && (currentPos < endPos)
                    && (labelText.charAt(currentPos) != NEW_LINE))
            {
                lastPos = currentPos;
                currentPos = getPosition(labelText, currentPos, WHITE_SPACE, FOUND);
                width = fm.stringWidth(labelText.substring(startPos, currentPos));
            }
            // ----------------------------------------------------------------
            // if we have a line break we want to copy everything up to
            // currentPos
            // ----------------------------------------------------------------
            if (labelText.charAt(currentPos) == NEW_LINE)
            {
                lastPos = currentPos;
            }
            // ----------------------------------------------------------------
            // if we are at the end of the string we want to copy everything up
            // to
            // the last character. Since there seems to be a problem to get the
            // last
            // character if the substring definition ends at the very last
            // character
            // we have to call a different substring function than normal.
            // ----------------------------------------------------------------
            if (currentPos == endPos && width <= maxAllowed)
            {
                lastPos = currentPos;
                String s = labelText.substring(startPos);
                line.addElement(s);
            }
            // ----------------------------------------------------------------
            // in all other cases copy the substring that we have found to fit
            // and
            // add it as a new line of text to the line vector.
            // ----------------------------------------------------------------
            else
            {
                // ------------------------------------------------------------
                // make sure it's not a single word. If so we must break it at
                // the
                // proper location.
                // ------------------------------------------------------------
                if (lastPos == startPos)
                {
                    lastPos = startPos + breakWord(labelText.substring(startPos, currentPos), fm);
                }
                String s = labelText.substring(startPos, lastPos);
                line.addElement(s);
            }

            // ----------------------------------------------------------------
            // seek for the end of the white space to cut out any unnecessary
            // spaces
            // and tabs and set the new start condition.
            // ----------------------------------------------------------------
            startPos = getPosition(labelText, lastPos, SPACES, NOT_FOUND);
            currentPos = startPos;
        }

        numLines = line.size();
        lineWidth = new int[numLines];
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method finds the font size, each line width and the widest line.
     */
    /*-------------------------------------------------------------------*/
    protected void measure()
    {
        if (!maxAllowedSet)
        {
            maxAllowed = getParent().getSize().width;
        }

        // return if width is too small
        if (maxAllowed < (20))
        {
            return;
        }

        FontMetrics fm = this.getFontMetrics(this.getFont());

        // return if no font metrics available
        if (fm == null)
        {
            return;
        }

        divideLabel();

        this.lineHeight = fm.getHeight();
        this.lineDescent = fm.getDescent();
        this.maxWidth = 0;

        for (int i = 0; i < numLines; i++)
        {
            this.lineWidth[i] = fm.stringWidth(this.line.elementAt(i));
            if (this.lineWidth[i] > this.maxWidth)
            {
                this.maxWidth = this.lineWidth[i];
            }
        }
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method draws the label.
     *
     * @param graphics the device context
     */
    /*-------------------------------------------------------------------*/
    public void paint(Graphics graphics)
    {
        int x;
        int y;

        measure();
        Dimension d = this.getSize();

        y = lineAscent + (d.height - (numLines * lineHeight)) / 2;

        for (int i = 0; i < numLines; i++)
        {
            y += lineHeight;
            switch (alignment)
            {
                case LEFT:
                    x = marginWidth;
                    break;
                case CENTER:
                    x = (d.width - lineWidth[i]) / 2;
                    break;
                case RIGHT:
                    x = d.width - marginWidth - lineWidth[i];
                    break;
                default:
                    x = (d.width - lineWidth[i]) / 2;
            }
            graphics.drawString(line.elementAt(i), x, y);
        }
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the label text
     *
     * @param labelText the text to be displayed
     */
    /*-------------------------------------------------------------------*/
    public void setText(String labelText)
    {
        this.labelText = labelText;
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the font that should be used to draw the label
     *
     * @param font font to be used within the label
     */
    /*-------------------------------------------------------------------*/
    public void setFont(Font font)
    {
        super.setFont(font);
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the color in which the text should be drawn
     *
     * @param color the text color
     */
    /*-------------------------------------------------------------------*/
    public void setColor(Color color)
    {
        super.setForeground(color);
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the text alignment for the label
     *
     * @param alignment the alignment, possible values are LEFT, CENTER, RIGHT
     */
    /*-------------------------------------------------------------------*/
    public void setJustify(int alignment)
    {
        this.alignment = alignment;
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the max allowed line width
     *
     * @param width the max allowed line width in pixels
     */
    /*-------------------------------------------------------------------*/
    public void setMaxWidth(int width)
    {
        this.maxAllowed = width;
        this.maxAllowedSet = true;
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the horizontal margin
     *
     * @param margin the margin to the left and to the right of the label
     */
    /*-------------------------------------------------------------------*/
    public void setMarginWidth(int margin)
    {
        this.marginWidth = margin;
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to set the vertical margin for the label
     *
     * @param margin the margin on the top and bottom of the label
     */
    /*-------------------------------------------------------------------*/
    public void setMarginHeight(int margin)
    {
        this.marginHeight = margin;
        repaint();
    }

    /*-------------------------------------------------------------------*/
    /**
     * Moves and resizes this component. The new location of the top-left corner is specified by
     * <code>x</code> and <code>y</code>, and the new size is specified by <code>width</code>
     * and <code>height</code>.
     *
     * @param x      The new x-coordinate of this component.
     * @param y      The new y-coordinate of this component.
     * @param width  The new width of this component.
     * @param height The new height of this component.
     */
    /*-------------------------------------------------------------------*/
    public void setBounds(int x, int y, int width, int height)
    {
        super.setBounds(x, y, width, height);
        this.maxAllowed = width;
        this.maxAllowedSet = true;
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to retrieve the text alignment for the label
     *
     * @return alignment the text alignment currently in use for the label
     */
    /*-------------------------------------------------------------------*/
    public int getAlignment()
    {
        return (this.alignment);
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to retrieve the horizontal margin for the label
     *
     * @return marginWidth the margin currently in use to the left and right of the label
     */
    /*-------------------------------------------------------------------*/
    public int getMarginWidth()
    {
        return (this.marginWidth);
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method may be used to retrieve the vertical margin for the label
     *
     * @return marginHeight the margin currently in use on the top and bottom of the label
     */
    /*-------------------------------------------------------------------*/
    public int getMarginHeight()
    {
        return (this.marginHeight);
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method is typically used by the layout manager, it reports the necessary space to
     * display the label comfortably.
     */
    /*-------------------------------------------------------------------*/
    public Dimension getPreferredSize()
    {
        measure();
        return (new Dimension(maxAllowed, (numLines * (lineHeight + lineAscent + lineDescent))
                + (2 * marginHeight)));
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method is typically used by the layout manager, it reports the absolute minimum space
     * required to display the entire label.
     */
    /*-------------------------------------------------------------------*/
    public Dimension getMinimumSize()
    {
        measure();
        return (new Dimension(maxAllowed, (numLines * (lineHeight + lineAscent + lineDescent))
                + (2 * marginHeight)));
    }

    /*-------------------------------------------------------------------*/
    /**
     * This method is called by the system after this object is first created.
     */
    /*-------------------------------------------------------------------*/
    public void addNotify()
    {
        super.addNotify(); // invoke the superclass
    }
}
/*---------------------------------------------------------------------------*/
