/*
 * @(#)LabelView.java	1.38 98/04/09
 * 
 * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of Sun
 * Microsystems, Inc. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Sun.
 * 
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
 * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
 * THIS SOFTWARE OR ITS DERIVATIVES.
 * 
 */
package xv;

import com.sun.java.swing.text.AttributeSet;
import com.sun.java.swing.text.BadLocationException;
import com.sun.java.swing.text.Document;
import com.sun.java.swing.text.Element;
import com.sun.java.swing.text.Segment;
//import com.sun.java.swing.text.StateInvariantError;
import com.sun.java.swing.text.StyleConstants;
import com.sun.java.swing.text.StyledDocument;
import com.sun.java.swing.text.TabableView;
import com.sun.java.swing.text.TabExpander;
import com.sun.java.swing.text.Utilities;
import com.sun.java.swing.text.View;
import com.sun.java.swing.text.ViewFactory;

import java.awt.*;
import com.sun.java.swing.event.*;

/**
 * A xv_LabelView is a styled chunk of text that represents a view
 * mapped over an element in the text model.  The view supports breaking
 * for the purpose of formatting.   The fragments produced
 * by breaking share the view that has primary responsibility
 * for the element (i.e. they are nested classes and carry only 
 * a small amount of state of their own) so they can share its 
 * resources.
 * <p>
 * This view is generally responsible for displaying character
 * level attributes in some way.  Since this view represents 
 * text that may have tabs embedded in it, it implements the
 * <code>TabableView</code> interface.  Tabs will only be
 * expanded if this view is embedded in a container that does
 * tab expansion.  ParagraphView is an example of a container
 * that does tab expansion.
 *
 * @author Timothy Prinzing
 * @version 1.38 04/09/98
 */
public class xv_LabelView extends View implements TabableView
{
    static boolean  UseRedFlag = true;  //TEMP
    /**
     * Constructs a new view wrapped on an element.
     *
     * @param elem the element
     */
    public xv_LabelView(Element elem) {
	super(elem);
	text = new Segment();
    }

    /**
     * Load the text buffer with the given range
     * of text.  This is used by the fragments 
     * broken off of this view as well as this 
     * view itself.
     */
    final void loadText(int p0, int p1) {
	try {
	    Document doc = getDocument();
	    doc.getText(p0, p1 - p0, text);
	    if (rightToLeft) {
		// PENDING(Java2D) use real rendering with shaping
		char[] reversed = new char[p1 - p0];
		int i = 0;
		for (int offs = text.offset + text.count - 1; offs >= text.offset; offs--) {
		    reversed[i++] = text.array[offs];
		}
		text.array = reversed;
		text.offset = 0;
	    }
	}
    catch (BadLocationException bl)
    {
//	    throw new StateInvariantError("xv_LabelView: Stale view: " + bl);
	}
    }

    /**
     * Paint the given range.  This is used by the
     * fragments broken off of this view as well as this
     * view itself.
     */
    final void paintText(Graphics g, Shape a, int p0, int p1, boolean stripWhitespace) {
	Rectangle alloc = a.getBounds();
	sync();
	loadText(p0, p1);
	int y = alloc.y + alloc.height - metrics.getDescent();
	g.setFont(font);
	g.setColor(fg);
	Utilities.drawTabbedText(text, alloc.x, y, g, expander, p0);
	if (underline) {
	    if (stripWhitespace) {
		while ((text.count > 0) && (Character.isWhitespace(text.array[text.count-1]))) {
		    alloc.width -= metrics.charWidth(text.array[text.count-1]);
		    text.count -= 1;
		}
	    }
	    y += 1;
	    g.drawLine(alloc.x, y, alloc.x + alloc.width, y);
	}
    }

    /**
     * Synchronize the view's cached values with the model.
     * This causes the font, metrics, color, etc to be 
     * recached if the cache has been invalidated.
     */
    final void sync()
    {
	    if (font == null)
        {
    	    Element  e = getElement();
    	    Document d = getDocument();
    	    if (d instanceof StyledDocument)
            {
        		StyledDocument doc = (StyledDocument) d;
        		AttributeSet attr = e.getAttributes();
        		font = doc.getFont(attr);
        		fg = doc.getForeground(attr);
        		underline = StyleConstants.isUnderline(attr);
/*                if (UseRedFlag)
                {
                    fg = Color.red;  //TEMP
                    UseRedFlag = false;
                    underline = false;
                }
                else
                {
                    fg = Color.green;  //TEMP
                    UseRedFlag = true;
                    underline = true;
                }
*/
        		metrics = Toolkit.getDefaultToolkit().getFontMetrics(font);
    	    }
            else
            {
//  		    throw new StateInvariantError("xv_LabelView needs StyledDocument");
    	    }
    	}
    }

    /**
     * Determines the preferred span for this view along an
     * axis. This is shared by the broken views.
     *
     * @param axis may be either X_AXIS or Y_AXIS
     * @param x the location to calculate the span from.
     * @returns  the span the view would like to be rendered into.
     *           Typically the view is told to render into the span
     *           that is returned, although there is no guarantee.  
     *           The parent may choose to resize or break the view.
     */
    final float getPreferredSpan(int axis, int p0, int p1, int x) {
	sync();
	switch (axis) {
	case View.X_AXIS:
	    loadText(p0, p1);
	    int width = Utilities.getTabbedTextWidth(text, metrics, x, expander, p0);
	    return Math.max(width, 1);
	case View.Y_AXIS:
	    return metrics.getHeight();
	default:
	    throw new IllegalArgumentException("Invalid axis: " + axis);
	}
    }

    /**
     * Provides a mapping from the document model coordinate space
     * to the coordinate space of the view mapped to it.
     * This is shared by the broken views.
     *
     * @param pos the position to convert
     * @param a the allocated region to render into
     * @return the bounding box of the given position
     * @exception BadLocationException  if the given position does not represent a
     *   valid location in the associated document
     * @see View#modelToView
     */
    Shape modelToView(int pos, Shape a, int p0, int p1) throws BadLocationException {
	Rectangle alloc = a.getBounds();
	if ((pos >= p0) && (pos <= p1)) {
	    // determine range to the left of the position
	    loadText(p0, pos);
	    sync();
	    int width = Utilities.getTabbedTextWidth(text, metrics, alloc.x, expander, p0);
	    if (rightToLeft) {
		// PENDING(Java2D) use real rendering with shaping
		return new Rectangle(alloc.x + alloc.width - width, 
				     alloc.y, 0, metrics.getHeight());
	    }
	    return new Rectangle(alloc.x + width, alloc.y, 0, metrics.getHeight());
	}
	throw new BadLocationException("modelToView - can't convert", p1);
    }

    /**
     * Provides a mapping from the view coordinate space to the logical
     * coordinate space of the model.
     *
     * @param x the X coordinate
     * @param y the Y coordinate
     * @param a the allocated region to render into
     * @return the location within the model that best represents the
     *  given point of view
     * @see View#viewToModel
     */
    int viewToModel(float x, float y, Shape a, int p0, int p1) {
	Rectangle alloc = a.getBounds();
	sync();
	loadText(p0, p1);
	int offs = Utilities.getTabbedTextOffset(text, metrics, 
						 alloc.x, (int) x, expander, p0);
	if (rightToLeft) {
	    // PENDING(Java2D) use real rendering with shaping
	    return p1 - offs;
	}
	return p0 + offs;
    }

    int getBreakWeight(int axis, float pos, float len, int p0, int p1) {
	if (axis == View.X_AXIS) {
	    sync();
	    loadText(p0, p1);
	    int index = Utilities.getTabbedTextOffset(text, metrics, 
						      (int)pos, (int)(pos+len), 
						      expander, p0);
	    if (index == 0) {
		// break is at the start offset
		return BadBreakWeight;
	    }
	    for (int i = text.offset + Math.min(index, text.count - 1); 
		 i >= text.offset; i--) {

		char ch = text.array[i];
		if (Character.isWhitespace(ch)) {
		    // found whitespace
		    return ExcellentBreakWeight;
		}
	    }
	    // no whitespace
	    return GoodBreakWeight;
	}
	return super.getBreakWeight(axis, pos, len);
    }

    // --- TabableView methods --------------------------------------

    /**
     * Determines the desired span when using the given 
     * tab expansion implementation.  
     *
     * @param x the position the view would be located
     *  at for the purpose of tab expansion >= 0.
     * @param e how to expand the tabs when encountered.
     * @return the desired span >= 0
     * @see TabableView#getTabbedSpan
     */
    public float getTabbedSpan(float x, TabExpander e) {
	expander = e;
	this.x = (int) x;
	return getPreferredSpan(X_AXIS, getStartOffset(), getEndOffset(), this.x);
    }
    
    /**
     * Determines the span along the same axis as tab 
     * expansion for a portion of the view.  This is
     * intended for use by the TabExpander for cases
     * where the tab expansion involves aligning the
     * portion of text that doesn't have whitespace 
     * relative to the tab stop.  There is therefore
     * an assumption that the range given does not
     * contain tabs.
     * <p>
     * This method can be called while servicing the
     * getTabbedSpan or getPreferredSize.  It has to
     * arrange for its own text buffer to make the
     * measurements.
     *
     * @param p0 the starting document offset >= 0
     * @param p1 the ending document offset >= p0
     * @return the span >= 0
     */
    public float getPartialSpan(int p0, int p1) {
	// PENDING should probably use a static buffer since there 
	// should be only one thread accessing it.
	sync();
	int width = 0;
	try {
	    Segment s = new Segment();
	    getDocument().getText(p0, p1 - p0, s);
	    width = Utilities.getTabbedTextWidth(s, metrics, x, expander, p0);
	} catch (BadLocationException bl) {
	}
	return width;
    }

    // --- View methods ---------------------------------------------

    /**
     * Renders a portion of a text style run.
     *
     * @param g the rendering surface to use
     * @param a the allocated region to render into
     */
    public void paint(Graphics g, Shape a) {
	paintText(g, a, getStartOffset(), getEndOffset(), false);
    }

    /**
     * Determines the preferred span for this view along an
     * axis. 
     *
     * @param axis may be either View.X_AXIS or View.Y_AXIS
     * @returns  the span the view would like to be rendered into >= 0.
     *           Typically the view is told to render into the span
     *           that is returned, although there is no guarantee.  
     *           The parent may choose to resize or break the view.
     */
    public float getPreferredSpan(int axis) {
	return getPreferredSpan(axis, getStartOffset(), getEndOffset(), this.x);
    }

    /**
     * Determines the desired alignment for this view along an
     * axis.  For the label, the alignment is along the font
     * baseline for the y axis, and the superclasses alignment
     * along the x axis.
     *
     * @param axis may be either View.X_AXIS or View.Y_AXIS
     * @returns the desired alignment.  This should be a value
     *   between 0.0 and 1.0 inclusive, where 0 indicates alignment at the
     *   origin and 1.0 indicates alignment to the full span
     *   away from the origin.  An alignment of 0.5 would be the
     *   center of the view.
     */
    public float getAlignment(int axis) {
	if (axis == View.Y_AXIS) {
	    float h = metrics.getHeight();
	    float d = metrics.getDescent();
	    float align = (h - d) / h;
	    return align;
	} 
	return super.getAlignment(axis);
    }


    /**
     * Provides a mapping from the document model coordinate space
     * to the coordinate space of the view mapped to it.
     *
     * @param pos the position to convert >= 0
     * @param a the allocated region to render into
     * @return the bounding box of the given position
     * @exception BadLocationException  if the given position does not represent a
     *   valid location in the associated document
     * @see View#modelToView
     */
    public Shape modelToView(int pos, Shape a) throws BadLocationException {
	return modelToView(pos, a, getStartOffset(), getEndOffset());
    }

    /**
     * Provides a mapping from the view coordinate space to the logical
     * coordinate space of the model.
     *
     * @param x the X coordinate >= 0
     * @param y the Y coordinate >= 0
     * @param a the allocated region to render into
     * @return the location within the model that best represents the
     *  given point of view >= 0
     * @see View#viewToModel
     */
    public int viewToModel(float x, float y, Shape a) {
	return viewToModel(x, y, a, getStartOffset(), getEndOffset());
    }

    /**
     * Gives notification from the document that attributes were changed
     * in a location that this view is responsible for.
     *
     * @param e the change information from the associated document
     * @param a the current allocation of the view
     * @param f the factory to use to rebuild if the view has children
     * @see View#changedUpdate
     */
    public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
	font = null;
    }

    /**
     * Determines how attractive a break opportunity in 
     * this view is.  This can be used for determining which
     * view is the most attractive to call <code>breakView</code>
     * on in the process of formatting.  The
     * higher the weight, the more attractive the break.  A
     * value equal to or lower than <code>View.BadBreakWeight</code>
     * should not be considered for a break.  A value greater
     * than or equal to <code>View.ForcedBreakWeight</code> should
     * be broken.
     * <p>
     * This is implemented to forward to the superclass for 
     * the Y_AXIS.  Along the X_AXIS the following values
     * may be returned.
     * <dl>
     * <dt><b>View.ExcellentBreakWeight</b>
     * <dd>if there is whitespace proceeding the desired break 
     *   location.  
     * <dt><b>View.BadBreakWeight</b>
     * <dd>if the desired break location results in a break
     *   location of the starting offset.
     * <dt><b>View.GoodBreakWeight</b>
     * <dd>if the other conditions don't occur.
     * </dl>
     * This will normally result in the behavior of breaking
     * on a whitespace location if one can be found, otherwise
     * breaking between characters.
     *
     * @param axis may be either View.X_AXIS or View.Y_AXIS
     * @param pos the potential location of the start of the 
     *   broken view >= 0.  This may be useful for calculating tab
     *   positions.
     * @param len specifies the relative length from <em>pos</em>
     *   where a potential break is desired >= 0.
     * @return the weight, which should be a value between
     *   View.ForcedBreakWeight and View.BadBreakWeight.
     * @see xv_LabelView
     * @see ParagraphView
     * @see BadBreakWeight
     * @see GoodBreakWeight
     * @see ExcellentBreakWeight
     * @see ForcedBreakWeight
     */
    public int getBreakWeight(int axis, float pos, float len) {
	return getBreakWeight(axis, pos, len, getStartOffset(), getEndOffset());
    }

    /**
     * Breaks this view on the given axis at the given length.
     * This is implemented to attempt to break on a whitespace
     * location, and returns a fragment with the whitespace at
     * the end.  If a whitespace location can't be found, the
     * nearest character is used.
     *
     * @param axis may be either View.X_AXIS or View.Y_AXIS
     * @param p0 the location in the model where the
     *  fragment should start it's representation >= 0.
     * @param pos the position along the axis that the
     *  broken view would occupy >= 0.  This may be useful for
     *  things like tab calculations.
     * @param len specifies the distance along the axis
     *  where a potential break is desired >= 0.  
     * @return the fragment of the view that represents the
     *  given span, if the view can be broken.  If the view
     *  doesn't support breaking behavior, the view itself is
     *  returned.
     * @see View#breakView
     */
    public View breakView(int axis, int p0, float pos, float len) {
	if (axis == View.X_AXIS) {
	    sync();
	    loadText(p0, getEndOffset());
	    int index = Utilities.getTabbedTextOffset(text, metrics, 
						      (int)pos, (int)(pos+len), 
						      expander, p0);
	    for (int i = text.offset + Math.min(index, text.count - 1); 
		 i >= text.offset; i--) {

		char ch = text.array[i];
		if (Character.isWhitespace(ch)) {
		    // found whitespace, break here
		    index = i - text.offset + 1;
		    break;
		}
	    }
	    int p1 = p0 + index;
	    LabelFragment frag = new LabelFragment(getElement(), p0, p1);
	    frag.x = (int) pos;
	    return frag;
	}
	return this;
    }

    /**
     * Creates a view that represents a portion of the element.
     * This is potentially useful during formatting operations
     * for taking measurements of fragments of the view.  If 
     * the view doesn't support fragmenting (the default), it 
     * should return itself.  
     * <p>
     * This view does support fragmenting.  It is implemented
     * to return a nested class that shares state in this view 
     * representing only a portion of the view.
     *
     * @param p0 the starting offset >= 0.  This should be a value
     *   greater or equal to the element starting offset and
     *   less than the element ending offset.
     * @param p1 the ending offset > p0.  This should be a value
     *   less than or equal to the elements end offset and
     *   greater than the elements starting offset.
     * @returns the view fragment, or itself if the view doesn't
     *   support breaking into fragments.
     * @see xv_LabelView
     */
    public View createFragment(int p0, int p1) {
	Element elem = getElement();
	return new LabelFragment(elem, p0, p1);
    }

    // --- variables ------------------------------------------------

    Font font;
    FontMetrics metrics;
    Color fg;
    Segment text;
    boolean underline;
    boolean rightToLeft;

    /**
     * how to expand tabs
     */
    TabExpander expander;

    /**
     * location for determining tab expansion against.
     */
    int x;

    /**
     * A label that represents only a portion of a character
     * style run element.  This carries very little state
     * of its own and depends heavily upon the outer class.
     */
    class LabelFragment extends View implements TabableView {

	/**
	 * Constructs a new view wrapped on an element.
	 *
	 * @param elem the element
	 * @param p0 the beginning of the range
	 * @param p1 the end of the range
	 */
        public LabelFragment(Element elem, int p0, int p1) {
	    super(elem);
	    offset = (short) (p0 - elem.getStartOffset());
	    length = (short) (p1 - p0);
	}

	// --- TabableView methods --------------------------------------

	/**
	 * Determines the desired span when using the given 
	 * tab expansion implementation.  
	 *
	 * @param x the position the view would be located
	 *  at for the purpose of tab expansion >= 0.
	 * @param e how to expand the tabs when encountered.
	 * @return the desired span >= 0
	 * @see TabableView#getTabbedSpan
	 */
        public float getTabbedSpan(float x, TabExpander e) {
	    xv_LabelView.this.expander = e;
	    this.x = (int) x;
	    return xv_LabelView.this.getPreferredSpan(X_AXIS, getStartOffset(), 
						   getEndOffset(), this.x);
	}
    
	/**
	 * Determine the span along the same axis as tab 
	 * expansion for a portion of the view.  This is
	 * intended for use by the TabExpander for cases
	 * where the tab expansion involves aligning the
	 * portion of text that doesn't have whitespace 
	 * relative to the tab stop.  There is therefore
	 * an assumption that the range given does not
	 * contain tabs.
	 */
        public float getPartialSpan(int p0, int p1) {
	    return xv_LabelView.this.getPartialSpan(p0, p1);
	}

	// --- View methods ----------------------------

	/**
	 * Fetches the portion of the model that this view is responsible for.
	 *
	 * @return the starting offset into the model
	 * @see View#getStartOffset
	 */
        public int getStartOffset() {
	    Element e = getElement();
	    return e.getStartOffset() + offset;
	}

	/**
	 * Fetches the portion of the model that this view is responsible for.
	 *
	 * @return the ending offset into the model
	 * @see View#getEndOffset
	 */
        public int getEndOffset() {
	    Element e = getElement();
	    return e.getStartOffset() + offset + length;
	}

	/**
	 * Renders a portion of a text style run.
	 *
	 * @param g the rendering surface to use
	 * @param a the allocated region to render into
	 */
        public void paint(Graphics g, Shape a) {
	    paintText(g, a, getStartOffset(), getEndOffset(), true);
	}

	/**
	 * Determines the preferred span for this view along an
	 * axis. 
	 *
	 * @param axis may be either X_AXIS or Y_AXIS
	 * @returns  the span the view would like to be rendered into.
	 *           Typically the view is told to render into the span
	 *           that is returned, although there is no guarantee.  
	 *           The parent may choose to resize or break the view.
	 */
        public float getPreferredSpan(int axis) {
	    return xv_LabelView.this.getPreferredSpan(axis, getStartOffset(), getEndOffset(), this.x);
	}

	/**
	 * Determines the desired alignment for this view along an
	 * axis.  For the label, the alignment is along the font
	 * baseline for the y axis, and the superclasses alignment
	 * along the x axis.
	 *
	 * @param axis may be either X_AXIS or Y_AXIS
	 * @returns the desired alignment.  This should be a value
	 *   between 0.0 and 1.0 where 0 indicates alignment at the
	 *   origin and 1.0 indicates alignment to the full span
	 *   away from the origin.  An alignment of 0.5 would be the
	 *   center of the view.
	 */
        public float getAlignment(int axis) {
	    return xv_LabelView.this.getAlignment(axis);
	}


	/**
	 * Provides a mapping from the document model coordinate space
	 * to the coordinate space of the view mapped to it.
	 *
	 * @param pos the position to convert
	 * @param a the allocated region to render into
	 * @return the bounding box of the given position
	 * @exception BadLocationException  if the given position does not represent a
	 *   valid location in the associated document
	 * @see View#modelToView
	 */
        public Shape modelToView(int pos, Shape a) throws BadLocationException {
	    return xv_LabelView.this.modelToView(pos, a, getStartOffset(), getEndOffset());
	}

	/**
	 * Provides a mapping from the view coordinate space to the logical
	 * coordinate space of the model.
	 *
	 * @param x the X coordinate
	 * @param y the Y coordinate
	 * @param a the allocated region to render into
	 * @return the location within the model that best represents the
	 *  given point of view
	 * @see View#viewToModel
	 */
        public int viewToModel(float x, float y, Shape a) {
	    return xv_LabelView.this.viewToModel(x, y, a, getStartOffset(), getEndOffset());
	}

	/**
	 * Gives notification from the document that attributes were changed
	 * in a location that this view is responsible for.
	 *
	 * @param e the change information from the associated document
	 * @param a the current allocation of the view
	 * @param f the factory to use to rebuild if the view has children
	 * @see View#changedUpdate
	 */
        public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
	    xv_LabelView.this.changedUpdate(e, a, f);
	}

	/**
	 * @see xv_LabelView#getBreakWeight
	 */
        public int getBreakWeight(int axis, float x, float len) {
	    return xv_LabelView.this.getBreakWeight(axis, x, len, 
						 getStartOffset(), getEndOffset());
	}

	/**
	 * Breaks this view on the given axis at the given length.
	 *
	 * @param axis may be either X_AXIS or Y_AXIS
	 * @param offset the location in the model where the
	 *  fragment should start it's representation.
	 * @param pos the position along the axis that the
	 *  broken view would occupy.  This may be useful for
	 *  things like tab calculations.
	 * @param len specifies the distance along the axis
	 *  where a potential break is desired.  
	 * @param a the current allocation of the view
	 * @return the fragment of the view that represents the
	 *  given span, if the view can be broken.  If the view
	 *  doesn't support breaking behavior, the view itself is
	 *  returned.
	 * @see View#breakView
	 */
        public View breakView(int axis, int offset, float pos, float len) {
	    return xv_LabelView.this.breakView(axis, offset, pos, len);
	}

	// ---- variables ---------------------------------
	short offset;
	short length;
	int x;
    }
}

