/*
 * @(#)MultiLineTextPaintable.java    2.6.0   7 June 2007
 *
 * Copyright 2007
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.*;

import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.awt.font.*;

/**
 * <p>Class <code>MultiLineTextPaintable</code> provides the
 * basic functionality of <code>TextPaintable</code> for text
 * that contains newlines.</p>
 * 
 * <p>The class <code>TextPaintable</code> accepts text with
 * newlines but does not treat them properly.  The newlines
 * are simply ignored and all text is placed on one line.
 * There are deep technical reasons for this.  The Java tools
 * that we use to render the text and later to test for mouse
 * hits is fundamentally a single-line technology.  Since we
 * have no other way to test for mouse hits, we must rely on
 * this Java technology in <code>TextPaintable</code>.</p>
 * 
 * <p>In this class, we solve the problem of multi-line text
 * by breaking the text into an array of single-line strings,
 * using each line to create a <code>TextPaintable</code>,
 * and then collecting these <code>TextPaintable</code>
 * objects into an internal <code>PaintableSequence</code>.
 * Here is a summary of the parameter options:</p>
 * 
 * <ul>
 *   <li>The following data may be provided in the constructors
 *       or set subsequent to construction:</li>
 *       <ul>
 *         <li>The text string to display on multiple lines</li>
 *         <li>The font</li>
 *         <li>The fill paint for the text</li>
 *         <li>The anchor locator strategy for all lines</li>
 *         <li>The anchor x and y position for the first line</li>
 *       </ul>
 *   <li>The text bounds strategy for each internal text paintable
 *       is LOOSE and cannot be modified</li>
 *   <li>All generic properties of a <code>Paintable</code> can be
 *       queried or modified as usual</li>
 * </ul>
 * 
 * <p>Depending on the constructor called, the parameters may
 * be supplied or left to be the following defaults:</p>
 * 
 * <ul>
 *   <li><code>string: ""</code></li>
 *   <li><code>font: TextPaintable.getDefaultFont()</code></li>
 *   <li><code>fillpaint: Color.black</code></li>
 *   <li><code>locator: TextAnchor.LEFT_ASCENTLINE</code></li>
 *   <li><code>anchorX: 0</code></li>
 *   <li><code>anchorY: 0</code></li>
 * </ul>
 * 
 * <p>Of course, when the string is empty, nothing will be painted.</p>
 * 
 * @author  Richard Rasala
 * @version 2.6.0
 * @since   2.6.0
 * @see     TextPaintable
 */
public class MultiLineTextPaintable
    extends PaintableSequenceComposite
{
    /** Bound property name for set string. */
    public static final String SET_STRING          = TextPaintable.SET_STRING;
        
    /** Bound property name for set font. */
    public static final String SET_FONT            = TextPaintable.SET_FONT;
        
    /** Bound property name for set fill paint. */
    public static final String SET_FILL_PAINT      = TextPaintable.SET_FILL_PAINT;
    
    /** Bound property name for set anchor locator. */
    public static final String SET_ANCHOR_LOCATOR  = TextPaintable.SET_ANCHOR_LOCATOR;
        
    /** Bound property name for set anchor position. */
    public static final String SET_ANCHOR_POSITION = TextPaintable.SET_ANCHOR_POSITION;
    
    
    /** The text string. */
    private String string = "";
    
    /** The font. */
    private Font font = TextPaintable.getDefaultFont();
    
    /** The fill paint. */
    private Paint fillpaint = Color.black;
    
    /** The text bounds strategy. */
    private TextBounds.Strategy strategy = TextBounds.LOOSE;
    
    /** The text anchor locator. */
    private TextAnchor.Locator locator = TextAnchor.LEFT_ASCENTLINE;
    
    /** The x anchor position. */
    private float anchorX = 0;
    
    /** The y anchor position. */
    private float anchorY = 0;
    
    
    /**
     * <p>The default constructor.</p>
     * 
     * <p>The following defaults are set:</p>
     * 
     * <ul>
     *   <li><code>string: ""</code></li>
     *   <li><code>font: TextPaintable.getDefaultFont()</code></li>
     *   <li><code>fillpaint: Color.black</code></li>
     *   <li><code>locator: TextAnchor.LEFT_ASCENTLINE</code></li>
     *   <li><code>anchorX: 0</code></li>
     *   <li><code>anchorY: 0</code></li>
     * </ul>
     * 
     * <p>All properties may be changed after construction.</p>
     * 
     * <p>With the default settings, the paintable will display nothing
     * since its text string is empty.</p>
     */
    public MultiLineTextPaintable()
    {
        this(null, null, null, null, 0, 0);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string.</p>
     *
     * @param string    the string to display on multiple lines
     */
    public MultiLineTextPaintable(String string)
    {
        this(string, null, null, null, 0, 0);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        float              anchorX,
        float              anchorY)
    {
        this(string, null, null, null, anchorX, anchorY);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, anchor locator, and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        this(string, null, null, locator, anchorX, anchorY);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string and font.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font)
    {
        this(string, font, null, null, 0, 0);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, font, and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font,
        float              anchorX,
        float              anchorY)
    {
        this(string, font, null, null, anchorX, anchorY);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, font, anchor locator, and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        this(string, font, null, locator, anchorX, anchorY);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, font, fill paint.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font,
        Paint              fillpaint)
    {
        this(string, font, fillpaint, null, 0, 0);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, font, fill paint, and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font,
        Paint              fillpaint,
        float              anchorX,
        float              anchorY)
    {
        this(string, font, fillpaint, null, anchorX, anchorY);
    }

    
    /**
     * <p>The constructor that creates a <code>MultiLineTextPaintable</code>
     * with the given string, font, fill paint, bounds strategy,
     * anchor locator, and anchor position.</p>
     *
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    public MultiLineTextPaintable(
        String             string,
        Font               font,
        Paint              fillpaint,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        initMLTP(string, font, fillpaint, locator, anchorX, anchorY);
    }
    
    
    /**
     * <p>The helper method to carry out the work of the constructors.</p>
     * 
     * @param string    the string to display on multiple lines
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate for the first line
     * @param anchorY   the anchor y-coordinate for the first line
     */
    protected void initMLTP(
        String             string,
        Font               font,
        Paint              fillpaint,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        if (string != null)
            this.string = string;
        
        if (font != null)
            this.font = font;
        
        if (fillpaint != null)
            this.fillpaint = fillpaint;
        
        if (locator != null)
            this.locator = locator;
        
        this.anchorX = anchorX;
        this.anchorY = anchorY;
        
        makeMLTP();
    }
    
    
    /**
     * <p>The helper method to build the <code>MultiLineTextPaintable</code>
     * once the settings are established.</p>
     */
    protected void makeMLTP() {
        // get and clear the internal paintable sequence
        PaintableSequence sequence = getPaintableSequence();
        sequence.clearSequence();
        
        // get the individual lines of text
        String[] lines = getLines();
        int count = lines.length;
        
        // make a dummy TextPaintable to get the line height
        TextPaintable dummy =
            new TextPaintable
                ("0", font, fillpaint, strategy, locator, 0, 0);
        
        float lineheight = dummy.getLeadingLineY() - dummy.getAscentLineY();
        
        // make the text paintables for the sequence
        float x = anchorX;
        float y = anchorY;
        
        for (int i = 0; i < count; i++) {
            TextPaintable paintable =
                new TextPaintable
                    (lines[i], font, fillpaint, strategy, locator, x, y);
            
            sequence.appendPaintable(paintable);
            
            y += lineheight;
        }
    }
    
    
    /**
     * <p>Sets the string for this <code>MultiLineTextPaintable</code>.</p>
     *
     * <p>If the given string is <code>null</code>, it is set to
     * the zero length string.</p>
     *
     * <p>Does nothing if the string is equal to the current string.</p>
     * 
     * <p>Fires property change: SET_STRING.</p>
     *
     * @param string the string to display on multiple lines
     */
    public void setString(String string) {
        if (string == null)
            string = "";
        
        if (string.equals(this.string))
            return;
        
        this.string = string;
        
        makeMLTP();
        
        firePropertyChange(SET_STRING, null, null);
    }
    
    
    /**
     * Returns the string for this <code>MultiLineTextPaintable</code>.
     *
     * @return the string to display on multiple lines
     */
    public String getString() {
        return string;
    }
    
    
    /**
     * Returns the string for this <code>MultiLineTextPaintable</code>
     * split into individual lines based on the newline character.
     */
    public String[] getLines() {
        return Strings.exactSplitList(string, '\n', false);
    }
    
    
    /**
     * <p>Sets the font for this <code>MultiLineTextPaintable</code>.</p>
     *
     * <p>If the given font is <code>null</code> then it is set to
     * <code>TextPaintable.getDefaultFont()</code>.</p>
     *
     * <p>Does nothing if the font is equal to the current font.</p>
     *
     * <p>Fires property change: SET_FONT.</p>
     *
     * @param font the font to use to display the string
     */
    public void setFont(Font font) {
        if (font == null)
            font = TextPaintable.getDefaultFont();
        
        if (font.equals(this.font))
            return;
        
        removeAndAddForwardingListener(this.font, font);
        
        this.font = font;
        
        PaintableSequence sequence = getPaintableSequence();
        int length = sequence.length();
        
        TextPaintable dummy =
            new TextPaintable
                ("0", font, fillpaint, strategy, locator, 0, 0);
        
        float lineheight = dummy.getLeadingLineY() - dummy.getAscentLineY();
        
        float x = anchorX;
        float y = anchorY;
        
        for (int i = 0; i < length; i++) {
            TextPaintable paintable =
                (TextPaintable) sequence.getPaintable(i);
            
            paintable.setFont(font);
            paintable.setAnchorPosition(x, y);
            
            y += lineheight;
        }
        
        firePropertyChange(SET_FONT, null, null);
    }
    
    
    /**
     * Returns the font for this <code>MultiLineTextPaintable</code>.
     *
     * @return the font to use to display the string
     */
    public Font getFont() {
        return font;
    }
    
    
    /**
     * <p>Sets the fill paint for this <code>MultiLineTextPaintable</code>.</p>
     *
     * <p>If the given paint is <code>null</code>, it is set to
     * <code>Color.black</code>.</p>
     *
     * <p>Fires property change: SET_FILL_PAINT.</p>
     *
     * @param fillpaint the paint to use to fill the string
     */
    public void setFillPaint(Paint fillpaint) {
        if (fillpaint == null)
            fillpaint = Color.black;
        
        if (fillpaint.equals(this.fillpaint))
            return;
        
        removeAndAddForwardingListener(this.fillpaint, fillpaint);
        
        this.fillpaint = fillpaint;
        
        PaintableSequence sequence = getPaintableSequence();
        int length = sequence.length();
        
        for (int i = 0; i < length; i++) {
            TextPaintable paintable =
                (TextPaintable) sequence.getPaintable(i);
            
            paintable.setFillPaint(fillpaint);
        }
        
        firePropertyChange(SET_FILL_PAINT, null, null);
    }
    
    
    /**
     * Returns the fill paint for this <code>MultiLineTextPaintable</code>.
     *
     * @return the paint to use to fill the string
     */
    public Paint getFillPaint() {
        return fillpaint;
    }
    
    
    /**
     * <p>Sets the <code>TextAnchor.Locator</code> for this
     * <code>MultiLineTextPaintable</code>.</p>
     *
     * <p>The following constants are provided in the class
     * <code>TextAnchor</code> as possible options for the
     * <code>TextAnchor.Locator</code> parameter:</p>
     *
     * <ul>
     *   <li><code>TextAnchor.LEFT_BASELINE</code></li>
     *   <li><code>TextAnchor.RIGHT_BASELINE</code></li>
     *   <li><code>TextAnchor.CENTER_BASELINE</code></li>
     *   <li><code>TextAnchor.LEFT_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.CENTER_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.LEFT_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.CENTER_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.LEFT_LEADINGLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_LEADINGLINE</code></li>
     *   <li><code>TextAnchor.CENTER_LEADINGLINE</code></li>
     * </ul>
     *
     * <p>If the given locator is <code>null</code>, it is set to
     * <code>TextAnchor.LEFT_ASCENTLINE</code>.</p>
     *
     * <p>Fires property change: SET_ANCHOR_LOCATOR.</p>
     *
     * @param locator the locator for the anchor position
     */
    public void setAnchorLocator(TextAnchor.Locator locator) {
        if (locator == null)
            locator = TextAnchor.LEFT_ASCENTLINE;
        
        if (locator.equals(this.locator))
            return;
        
        this.locator = locator;
        
        PaintableSequence sequence = getPaintableSequence();
        int length = sequence.length();
        
        for (int i = 0; i < length; i++) {
            TextPaintable paintable =
                (TextPaintable) sequence.getPaintable(i);
            
            paintable.setAnchorLocator(locator);
        }
        
        firePropertyChange(SET_ANCHOR_LOCATOR, null, null);
    }
    
    
    /**
     * <p>Returns the <code>TextAnchor.Locator</code> for this
     * <code>MultiLineTextPaintable</code>.</p>
     *
     * @return the locator for the anchor position
     */
    public TextAnchor.Locator getAnchorLocator() {
        return locator;
    }
    
    
    /**
     * <p>Sets the anchor position for the first line of
     * this <code>MultiLineTextPaintable</code>.</p>
     * 
     * <p>The anchor position for each subsequent line is
     * then adjusted appropriately.</p>
     *
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param anchorX the anchor x-coordinate
     * @param anchorY the anchor y-coordinate
     */
    public void setAnchorPosition(float anchorX, float anchorY) {
        float dx = anchorX - this.anchorX;
        float dy = anchorY - this.anchorY;
        
        shiftAnchorPosition(dx, dy);
    }
    
    
    /**
     * <p>Sets the anchor position for the first line of
     * this <code>MultiLineTextPaintable</code>
     * using an array <code>float[2]</code>.</p>
     *
     * <p>The anchor position for each subsequent line is
     * then adjusted appropriately.</p>
     *
     * <p>A <code>null</code> parameter is ignored.</p>
     *
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param position the anchor position
     */
    public void setAnchorPosition(float[] position) {
        if ((position == null) || (position.length != 2))
            return;
        
        setAnchorPosition(position[0], position[1]);
    }
    
    
    /**
     * <p>Shift the anchor position for each line of this
     * <code>MultiLineTextPaintable</code>
     * by the given dx and dy.</p>
     * 
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param dx the shift in the anchor x position
     * @param dy the shift in the anchor y position
     */
    public void shiftAnchorPosition(float dx, float dy) {
        if ((dx == 0) && (dy == 0))
            return;
        
        anchorX += dx;
        anchorY += dy;
        
        PaintableSequence sequence = getPaintableSequence();
        int length = sequence.length();
        
        for (int i = 0; i < length; i++) {
            TextPaintable paintable =
                (TextPaintable) sequence.getPaintable(i);
            
            paintable.shiftAnchorPosition(dx, dy);
        }
        
        addPreMutation (TransformFactory.translate(-dx, -dy));
        addPostMutation(TransformFactory.translate( dx,  dy));
        
        firePropertyChange(SET_ANCHOR_POSITION, null, null);
    }
    
    
    /**
     * Returns a copy of the anchor position for the first line
     * this <code>MultiLineTextPaintable</code>
     * as an array <code>float[2]</code>.
     *
     * @return the anchor position
     */
    public float[] getAnchorPosition() {
        return new float[] { anchorX, anchorY };
    }
    
    
    /**
     * <p>Returns a copy of the center of the paint region affected
     * by the <code>originalPaint</code> method.</p>
     *
     * <p>If the user has set a default original position then that
     * is returned.  Otherwise, we define the original center to be
     * the anchor position.  This will usually not be the geometric
     * center.</p>
     *
     * @return a copy of the center of the original paint region
     */
    public XPoint2D getOriginalCenter() {
        XPoint2D center = getDefaultOriginalCenter();
        
        if (center != null)
            return center;
        
        return new XPoint2D(anchorX, anchorY);
    }
    

}

