/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.gvt.font;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;

import org.apache.batik.gvt.text.ArabicTextHandler;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
import org.apache.batik.gvt.text.TextPaintInfo;

/**
 * This is a wrapper class for a java.awt.font.GlyphVector instance.
 *
 * @author <a href="mailto:bella.robinson@cmis.csiro.au">Bella Robinson</a>
 * @version $Id: AWTGVTGlyphVector.java,v 1.30 2003/09/30 00:30:54 deweese Exp $
 */
public class AWTGVTGlyphVector implements GVTGlyphVector {

    public static final AttributedCharacterIterator.Attribute PAINT_INFO 
        = GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO;

    private GlyphVector awtGlyphVector;
    private AWTGVTFont gvtFont;
    private CharacterIterator ci;

    // This contains the glyphPostions after doing a performDefaultLayout
    private Point2D      [] defaultGlyphPositions;
    private Point2D.Float[] glyphPositions;

    // need to keep track of the glyphTransforms since GlyphVector doesn't
    // seem to
    private AffineTransform[] glyphTransforms;

    // these are for caching the glyph outlines
    private Shape[] glyphOutlines;
    private Shape[] glyphVisualBounds;
    private Shape[] glyphLogicalBounds;
    private boolean[] glyphVisible;
    private GVTGlyphMetrics [] glyphMetrics;
    private GeneralPath outline;
    private Rectangle2D visualBounds;
    private Rectangle2D logicalBounds;
    private Rectangle2D bounds2D;
    private float scaleFactor;
    private float ascent;
    private float descent;
    private TextPaintInfo cacheTPI;

    /**
     * Creates and new AWTGVTGlyphVector from the specified GlyphVector and
     * AWTGVTFont objects.
     *
     * @param glyphVector The glyph vector that this one will be based upon.
     * @param font The font that is creating this glyph vector.
     * @param scaleFactor The scale factor to apply to the glyph vector.
     * IMPORTANT: This is only required because the GlyphVector class doesn't
     * handle font sizes less than 1 correctly. By using the scale factor we
     * can use a GlyphVector created by a larger font and then scale it down to
     * the correct size.
     * @param ci The character string that this glyph vector represents.
     */
    public AWTGVTGlyphVector(GlyphVector glyphVector,
                             AWTGVTFont font,
                             float scaleFactor,
                             CharacterIterator ci) {

        this.awtGlyphVector = glyphVector;
        this.gvtFont = font;
        this.scaleFactor = scaleFactor;
        this.ci = ci;

        GVTLineMetrics lineMetrics = gvtFont.getLineMetrics
            ("By", awtGlyphVector.getFontRenderContext());

        ascent  = lineMetrics.getAscent();
        descent = lineMetrics.getDescent();

        outline       = null;
        visualBounds  = null;
        logicalBounds = null;
        bounds2D      = null;
        int numGlyphs = glyphVector.getNumGlyphs();
        glyphPositions     = new Point2D.Float  [numGlyphs+1];
        glyphTransforms    = new AffineTransform[numGlyphs];
        glyphOutlines      = new Shape          [numGlyphs];
        glyphVisualBounds  = new Shape          [numGlyphs];
        glyphLogicalBounds = new Shape          [numGlyphs];
        glyphVisible       = new boolean        [numGlyphs];
        glyphMetrics       = new GVTGlyphMetrics[numGlyphs];

        for (int i = 0; i < numGlyphs; i++) {
            glyphVisible[i] = true;
        }
    }

    /**
     * Returns the GVTFont associated with this GVTGlyphVector.
     */
    public GVTFont getFont() {
        return gvtFont;
    }

    /**
     * Returns the FontRenderContext associated with this GlyphVector.
     */
    public FontRenderContext getFontRenderContext() {
        return awtGlyphVector.getFontRenderContext();
    }

    /**
     * Returns the glyphcode of the specified glyph.
     */
    public int getGlyphCode(int glyphIndex) {
        return awtGlyphVector.getGlyphCode(glyphIndex);
    }

    /**
     * Returns an array of glyphcodes for the specified glyphs.
     */
    public int[] getGlyphCodes(int beginGlyphIndex, int numEntries,
                               int[] codeReturn) {
        return awtGlyphVector.getGlyphCodes(beginGlyphIndex, numEntries,
                                            codeReturn);
    }

    /**
     * Returns the justification information for the glyph at the specified
     * index into this GlyphVector.
     */
    public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) {
        return awtGlyphVector.getGlyphJustificationInfo(glyphIndex);
    }

    /**
     * Returns a tight bounds on the GylphVector including stroking.
     */
    public Rectangle2D getBounds2D(AttributedCharacterIterator aci) {
        aci.first();
        TextPaintInfo tpi = (TextPaintInfo)aci.getAttribute(PAINT_INFO);
        if ((bounds2D != null) &&
            TextPaintInfo.equivilent(tpi, cacheTPI))
            return bounds2D;

        if (tpi == null)
            return null;

        cacheTPI = new TextPaintInfo(tpi);
        Shape outline = null;
        if (tpi.fillPaint != null) {
            outline = getOutline();
            bounds2D = outline.getBounds2D();
        }
        
        // check if we need to include the 
        // outline of this glyph
        Stroke stroke = tpi.strokeStroke;
        Paint  paint  = tpi.strokePaint;
        if ((stroke != null) && (paint != null)) {
            if (outline == null)
                outline = getOutline();
            Rectangle2D strokeBounds 
                = stroke.createStrokedShape(outline).getBounds2D();
            if (bounds2D == null)
                bounds2D = strokeBounds;
            else 
                bounds2D = bounds2D.createUnion(strokeBounds);
        }
        if (bounds2D == null)
            return null;

        if ((bounds2D.getWidth()  == 0) ||
            (bounds2D.getHeight() == 0))
            bounds2D = null;

        return bounds2D;
    }

    /**
     *  Returns the logical bounds of this GlyphVector.
     * This is a bound useful for hit detection and highlighting.
     */
    public Rectangle2D getLogicalBounds() {
        if (logicalBounds == null) {
            // This fills in logicalBounds...
            computeGlyphLogicalBounds();
        }
        return logicalBounds;
    }

    /**
     *  Returns the logical bounds of the specified glyph within this
     *  GlyphVector.
     */
    public Shape getGlyphLogicalBounds(int glyphIndex) {
        if (glyphLogicalBounds[glyphIndex] == null &&
            glyphVisible[glyphIndex]) {

            computeGlyphLogicalBounds();
        }
        return glyphLogicalBounds[glyphIndex];
    }

    /**
     * Calculates the logical bounds for each glyph. The logical
     * bounds are what is used for highlighting the glyphs when
     * selected.
     */
    private void computeGlyphLogicalBounds() {

        Shape[] tempLogicalBounds = new Shape[getNumGlyphs()];
        boolean[] rotated  = new boolean[getNumGlyphs()];

        double maxWidth = -1;
        double maxHeight = -1;

        for (int i = 0; i < getNumGlyphs(); i++) {

            if (!glyphVisible[i]) {
                // the glyph is not drawn
                tempLogicalBounds[i] = null;
                continue;
            }

            AffineTransform glyphTransform = getGlyphTransform(i);
            GVTGlyphMetrics glyphMetrics   = getGlyphMetrics(i);

            float glyphX      = 0;
            float glyphY      = -ascent/scaleFactor;
            float glyphWidth  = (glyphMetrics.getHorizontalAdvance()/
                                 scaleFactor);
            float glyphHeight = (glyphMetrics.getVerticalAdvance()/
                                 scaleFactor);

            Rectangle2D glyphBounds = new Rectangle2D.Double(glyphX,
                                                             glyphY,
                                                             glyphWidth,
                                                             glyphHeight);

            if (glyphBounds.isEmpty()) {
                if (i > 0) {
                    // can't tell if rotated or not, make it the same as
                    // the previous glyph
                    rotated [i] = rotated [i-1];
                } else {
                    rotated [i] = true;
                }
            } else {
                // get three corner points so we can determine
                // whether the glyph is rotated
                Point2D p1 = new Point2D.Double(glyphBounds.getMinX(),
                                                glyphBounds.getMinY());
                Point2D p2 = new Point2D.Double(glyphBounds.getMaxX(),
                                                glyphBounds.getMinY());
                Point2D p3 = new Point2D.Double(glyphBounds.getMinX(),
                                                glyphBounds.getMaxY());

                AffineTransform tr = AffineTransform.getTranslateInstance
                    (getGlyphPosition(i).getX(),
                     getGlyphPosition(i).getY());

                if (glyphTransform != null)
                    tr.concatenate(glyphTransform);
                tr.scale(scaleFactor, scaleFactor);

                tempLogicalBounds[i] = tr.createTransformedShape(glyphBounds);

                Point2D tp1 = new Point2D.Double();
                Point2D tp2 = new Point2D.Double();
                Point2D tp3 = new Point2D.Double();
                tr.transform(p1, tp1);
                tr.transform(p2, tp2);
                tr.transform(p3, tp3);
                double tdx12 = tp1.getX()-tp2.getX();
                double tdx13 = tp1.getX()-tp3.getX();
                double tdy12 = tp1.getY()-tp2.getY();
                double tdy13 = tp1.getY()-tp3.getY();

                if (((Math.abs(tdx12) < 0.001) && (Math.abs(tdy13) < 0.001)) ||
                    ((Math.abs(tdx13) < 0.001) && (Math.abs(tdy12) < 0.001))) {
                    // If either of these are zero then it is axially aligned
                    rotated[i] = false;
                } else {
                    rotated [i] = true;
                }

                Rectangle2D rectBounds;
                rectBounds = tempLogicalBounds[i].getBounds2D();
                if (rectBounds.getWidth() > maxWidth)
                    maxWidth = rectBounds.getWidth();
                if (rectBounds.getHeight() > maxHeight)
                    maxHeight = rectBounds.getHeight();
            }
        }

        // if appropriate, join adjacent glyph logical bounds
        GeneralPath logicalBoundsPath = new GeneralPath();
        for (int i = 0; i < getNumGlyphs(); i++) {
            if (tempLogicalBounds[i] != null) {
                logicalBoundsPath.append(tempLogicalBounds[i], false);
            }
        }

        logicalBounds = logicalBoundsPath.getBounds2D();

        if (logicalBounds.getHeight() < maxHeight*1.5) {
            // make all glyphs tops and bottoms the same as the full bounds
            for (int i = 0; i < getNumGlyphs(); i++) {
                // first make sure that the glyph logical bounds are
                // not rotated
                if (rotated[i]) continue;
                if (tempLogicalBounds[i] == null) continue;

                Rectangle2D glyphBounds = tempLogicalBounds[i].getBounds2D();

                double x = glyphBounds.getMinX();
                double width = glyphBounds.getWidth();

                if ((i < getNumGlyphs()-1) &&
                    (tempLogicalBounds[i+1] != null)) {
                    // make this glyph extend to the start of the next one
                    Rectangle2D nextGlyphBounds =
                        tempLogicalBounds[i+1].getBounds2D();

                    if (nextGlyphBounds.getX() > x) {
                        // going left to right (this is pretty hoky)
                        width = nextGlyphBounds.getX() - x;
                    } else {
                        double newGlyphX = (nextGlyphBounds.getX() +
                                            nextGlyphBounds.getWidth());
                        width += (x - newGlyphX);
                        x = newGlyphX;
                    }
                }

                tempLogicalBounds[i] = new Rectangle2D.Double
                    (x, logicalBounds.getMinY(),
                     width, logicalBounds.getHeight());
            }
        } else if (logicalBounds.getWidth() < maxWidth*1.5) {
            // make all glyphs left and right edges the same as the full bounds
            for (int i = 0; i < getNumGlyphs(); i++) {
                // first make sure that the glyph logical bounds are
                // not rotated
                if (rotated[i]) continue;
                if (tempLogicalBounds[i] == null) continue;

                Rectangle2D glyphBounds = tempLogicalBounds[i].getBounds2D();
                double      y           = glyphBounds.getMinY();
                double      height      = glyphBounds.getHeight();

                if ((i < getNumGlyphs()-1) &&
                    (tempLogicalBounds[i+1] != null)) {
                    // make this glyph extend to the start of the next one
                    Rectangle2D nextGlyphBounds =
                        tempLogicalBounds[i+1].getBounds2D();
                    if (nextGlyphBounds.getY() > y) { // going top to bottom
                        height = nextGlyphBounds.getY() - y;
                    } else {
                        double newGlyphY = (nextGlyphBounds.getY() +
                                            nextGlyphBounds.getHeight());
                        height += (y - newGlyphY);
                        y = newGlyphY;
                    }
                }

                tempLogicalBounds[i] = new Rectangle2D.Double
                    (logicalBounds.getMinX(),  y,
                     logicalBounds.getWidth(), height);
            }
        }

        for (int i = 0; i < getNumGlyphs(); i++) {
            glyphLogicalBounds[i] = tempLogicalBounds[i];
        }
    }

    /**
     * Returns the metrics of the glyph at the specified index into this
     * GVTGlyphVector.
     */
    public GVTGlyphMetrics getGlyphMetrics(int glyphIndex) {
        if (glyphMetrics[glyphIndex] == null) {
/*
            GlyphMetrics gm = awtGlyphVector.getGlyphMetrics(glyphIndex);
            Rectangle2D gmB = gm.getBounds2D();
*/
            // -- start glyph cache code --
            Point2D glyphPos = defaultGlyphPositions[glyphIndex];
            char c = ci.setIndex(ci.getBeginIndex()+glyphIndex);
            ci.setIndex(ci.getBeginIndex());
            AWTGlyphGeometryCache.Value v = AWTGVTFont.getGlyphGeometry
                (gvtFont, c, awtGlyphVector, glyphIndex, glyphPos);
            Rectangle2D gmB = v.getBounds2D();
           // -- end glyph cache code --

            Rectangle2D bounds = new Rectangle2D.Double
                (gmB.getX()     * scaleFactor, gmB.getY()      * scaleFactor,
                 gmB.getWidth() * scaleFactor, gmB.getHeight() * scaleFactor);

            // defaultGlyphPositions has one more entry than glyphs
            // the last entry stores the total advance for the
            // glyphVector.
            float adv = (float)(defaultGlyphPositions[glyphIndex+1].getX()-
                                defaultGlyphPositions[glyphIndex]  .getX());
            glyphMetrics[glyphIndex] =  new GVTGlyphMetrics
                (adv*scaleFactor, (ascent+descent),
                 bounds, GlyphMetrics.STANDARD);
        }
        return glyphMetrics[glyphIndex];
    }

    /**
     * Returns a Shape whose interior corresponds to the visual representation
     * of the specified glyph within this GlyphVector.
     */
    public Shape getGlyphOutline(int glyphIndex) {
        if (glyphOutlines[glyphIndex] == null) {
/*
            Shape glyphOutline = awtGlyphVector.getGlyphOutline(glyphIndex);
*/
            // -- start glyph cache code --
            Point2D glyphPos = defaultGlyphPositions[glyphIndex];
            char c = ci.setIndex(ci.getBeginIndex()+glyphIndex);
            ci.setIndex(ci.getBeginIndex());
            AWTGlyphGeometryCache.Value v = AWTGVTFont.getGlyphGeometry
                (gvtFont, c, awtGlyphVector, glyphIndex, glyphPos);
            Shape glyphOutline = v.getOutline();
           // -- end glyph cache code --

            AffineTransform tr = AffineTransform.getTranslateInstance
                (getGlyphPosition(glyphIndex).getX(),
                 getGlyphPosition(glyphIndex).getY());

            AffineTransform glyphTransform = getGlyphTransform(glyphIndex);

            if (glyphTransform != null) {
                tr.concatenate(glyphTransform);
            }
            //
            // <!> HACK
            //
            // GlyphVector.getGlyphOutline behavior changes between 1.3 and 1.4
            //
            // I've looked at this problem a bit more and the incorrect glyph
            // positioning in Batik is definitely due to the change in
            // behavior of GlyphVector.getGlyphOutline(glyphIndex). It used to
            // return the outline of the glyph at position 0,0 which meant
            // that we had to translate it to the actual glyph position before
            // drawing it. Now, it returns the outline which has already been
            // positioned.
            //
            // -- Bella
            //
/*
            if (outlinesPositioned()) {
                Point2D glyphPos = defaultGlyphPositions[glyphIndex];
                tr.translate(-glyphPos.getX(), -glyphPos.getY());
            }
*/
            tr.scale(scaleFactor, scaleFactor);
            glyphOutlines[glyphIndex]=tr.createTransformedShape(glyphOutline);
        }

        return glyphOutlines[glyphIndex];
    }

    // This is true if GlyphVector.getGlyphOutline returns glyph outlines
    // that are positioned (if it is false the outlines are always at 0,0).
    private static final boolean outlinesPositioned;
    // This is true if Graphics2D.drawGlyphVector works for the
    // current JDK/OS combination.
    private static final boolean drawGlyphVectorWorks;
    // This is true if Graphics2D.drawGlyphVector will correctly
    // render Glyph Vectors with per glyph transforms.
    private static final boolean glyphVectorTransformWorks;

    static {
        String s = System.getProperty("java.specification.version");
        if ("1.4".compareTo(s) <= 0) {
            outlinesPositioned = true;
            drawGlyphVectorWorks = true;
            glyphVectorTransformWorks = true;
        } else if ("Mac OS X".equals(System.getProperty("os.name"))) {
            outlinesPositioned = true;
            drawGlyphVectorWorks = false;
            glyphVectorTransformWorks = false;
        } else {
            outlinesPositioned = false;
            drawGlyphVectorWorks = true;
            glyphVectorTransformWorks = false;
        }
    }

    // Returns true if GlyphVector.getGlyphOutlines returns glyph outlines
    // that are positioned (otherwise they are always at 0,0).
    static boolean outlinesPositioned() {
        return outlinesPositioned;
    }

    /**
     * Returns the position of the specified glyph within this GlyphVector.
     */
    public Point2D getGlyphPosition(int glyphIndex) {
        return glyphPositions[glyphIndex];
    }

    /**
     * Returns an array of glyph positions for the specified glyphs
     */
    public float[] getGlyphPositions(int beginGlyphIndex,
                                     int numEntries,
                                     float[] positionReturn) {

        if (positionReturn == null) {
            positionReturn = new float[numEntries*2];
        }

        for (int i = beginGlyphIndex; i < (beginGlyphIndex+numEntries); i++) {
            Point2D glyphPos = getGlyphPosition(i);
            positionReturn[(i-beginGlyphIndex)*2] = (float)glyphPos.getX();
            positionReturn[(i-beginGlyphIndex)*2 + 1] = (float)glyphPos.getY();
        }

        return positionReturn;
    }

    /**
     * Gets the transform of the specified glyph within this GlyphVector.
     */
    public AffineTransform getGlyphTransform(int glyphIndex) {
        return glyphTransforms[glyphIndex];
    }

    /**
     * Returns the visual bounds of the specified glyph within the GlyphVector.
     */
    public Shape getGlyphVisualBounds(int glyphIndex) {
        if (glyphVisualBounds[glyphIndex] == null) {
/*
            Shape glyphOutline = awtGlyphVector.getGlyphOutline(glyphIndex);
            Rectangle2D glyphBounds = glyphOutline.getBounds2D();
*/
            // -- start glyph cache code --
            Point2D glyphPos = defaultGlyphPositions[glyphIndex];
            char c = ci.setIndex(ci.getBeginIndex()+glyphIndex);
            ci.setIndex(ci.getBeginIndex());
            AWTGlyphGeometryCache.Value v = AWTGVTFont.getGlyphGeometry
                (gvtFont, c, awtGlyphVector, glyphIndex, glyphPos);
            Rectangle2D glyphBounds = v.getOutlineBounds2D();
           // -- end glyph cache code --

            AffineTransform tr = AffineTransform.getTranslateInstance
                (getGlyphPosition(glyphIndex).getX(),
                 getGlyphPosition(glyphIndex).getY());

            AffineTransform glyphTransform = getGlyphTransform(glyphIndex);
            if (glyphTransform != null) {
                tr.concatenate(glyphTransform);
            }
            tr.scale(scaleFactor, scaleFactor);
            glyphVisualBounds[glyphIndex] =
                tr.createTransformedShape(glyphBounds);
        }

        return glyphVisualBounds[glyphIndex];
    }

    /**
     * Returns the number of glyphs in this GlyphVector.
     */
    public int getNumGlyphs() {
        return awtGlyphVector.getNumGlyphs();
    }

    /**
     * Returns a Shape whose interior corresponds to the visual representation
     * of this GlyphVector.
     */
    public Shape getOutline() {
        if (outline != null) 
            return outline;
        
        outline = new GeneralPath();
        for (int i = 0; i < getNumGlyphs(); i++) {
            if (glyphVisible[i]) {
                Shape glyphOutline = getGlyphOutline(i);
                outline.append(glyphOutline, false);
            }
        }
        return outline;
    }

    /**
     * Returns a Shape whose interior corresponds to the visual representation
     * of this GlyphVector, offset to x, y.
     */
    public Shape getOutline(float x, float y) {
        Shape outline = getOutline();
        AffineTransform tr = AffineTransform.getTranslateInstance(x,y);
        outline = tr.createTransformedShape(outline);
        return outline;
    }

    /**
     * Returns the visual bounds of this GlyphVector The visual bounds is the
     * tightest rectangle enclosing all non-background pixels in the rendered
     * representation of this GlyphVector.
     */
    public Rectangle2D getGeometricBounds() {
        if (visualBounds == null) {
            Shape outline = getOutline();
            visualBounds = outline.getBounds2D();
        }
        return visualBounds;
    }

    /**
     * Assigns default positions to each glyph in this GlyphVector.
     */
    public void performDefaultLayout() {
        if (defaultGlyphPositions == null) {
            awtGlyphVector.performDefaultLayout();
            defaultGlyphPositions = new Point2D.Float[getNumGlyphs()+1];
            for (int i = 0; i <= getNumGlyphs(); i++)
                defaultGlyphPositions[i] = awtGlyphVector.getGlyphPosition(i);
        }

        outline       = null;
        visualBounds  = null;
        logicalBounds = null;
        bounds2D      = null;
        float shiftLeft = 0;
        int i=0;
        for (; i < getNumGlyphs(); i++) {
            glyphTransforms   [i] = null;
            glyphVisualBounds [i] = null;
            glyphLogicalBounds[i] = null;
            glyphOutlines     [i] = null;
            glyphMetrics      [i] = null;
            Point2D glyphPos = defaultGlyphPositions[i];
            float x = (float)((glyphPos.getX() * scaleFactor)-shiftLeft);
            float y = (float) (glyphPos.getY() * scaleFactor);
            if (glyphPositions[i] == null) {
                glyphPositions[i] = new Point2D.Float(x,y);
            } else {
                glyphPositions[i].x = x;
                glyphPositions[i].y = y;
            }

            // if c is a transparent arabic char then need to shift the
            // following glyphs left so that the current glyph is overwritten
            char c = ci.setIndex(i + ci.getBeginIndex());
            if (ArabicTextHandler.arabicCharTransparent(c)) {
                shiftLeft += getGlyphMetrics(i).getHorizontalAdvance();
            }
        }

        // Need glyph pos for point after last char...
        Point2D glyphPos = defaultGlyphPositions[i];
        glyphPositions[i] = new Point2D.Float
                ((float)((glyphPos.getX() * scaleFactor)-shiftLeft),
                 (float) (glyphPos.getY() * scaleFactor));
    }

    /**
     * Sets the position of the specified glyph within this GlyphVector.
     */
    public void setGlyphPosition(int glyphIndex, Point2D newPos) {
        glyphPositions[glyphIndex].x = (float)newPos.getX();
        glyphPositions[glyphIndex].y = (float)newPos.getY();
        outline       = null;
        visualBounds  = null;
        logicalBounds = null;
        bounds2D      = null;

        if (glyphIndex != getNumGlyphs()) {
            glyphVisualBounds [glyphIndex] = null;
            glyphLogicalBounds[glyphIndex] = null;
            glyphOutlines     [glyphIndex] = null;
            glyphMetrics      [glyphIndex] = null;
        }
    }

    /**
     * Sets the transform of the specified glyph within this GlyphVector.
     */
    public void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
        glyphTransforms[glyphIndex] = newTX;
        outline       = null;
        visualBounds  = null;
        logicalBounds = null;
        bounds2D      = null;

        glyphVisualBounds [glyphIndex] = null;
        glyphLogicalBounds[glyphIndex] = null;
        glyphOutlines     [glyphIndex] = null;
        glyphMetrics      [glyphIndex] = null;
    }

    /**
     * Tells the glyph vector whether or not to draw the specified glyph.
     */
    public void setGlyphVisible(int glyphIndex, boolean visible) {
        if (visible == glyphVisible[glyphIndex]) 
            return;
        glyphVisible[glyphIndex] = visible;
        outline       = null;
        visualBounds  = null;
        logicalBounds = null;
        bounds2D      = null;

        glyphVisualBounds [glyphIndex] = null;
        glyphLogicalBounds[glyphIndex] = null;
        glyphOutlines     [glyphIndex] = null;
        glyphMetrics      [glyphIndex] = null;
    }

    /**
     * Returns true if specified glyph will be rendered.
     */
    public boolean isGlyphVisible(int glyphIndex) {
        return glyphVisible[glyphIndex];
    }

    /**
     * Returns the number of chars represented by the glyphs within the
     * specified range.
     *
     * @param startGlyphIndex The index of the first glyph in the range.
     * @param endGlyphIndex The index of the last glyph in the range.
     * @return The number of chars.
     */
    public int getCharacterCount(int startGlyphIndex, int endGlyphIndex) {
        if (startGlyphIndex < 0) {
            startGlyphIndex = 0;
        }
        if (endGlyphIndex >= getNumGlyphs()) {
            endGlyphIndex = getNumGlyphs()-1;
        }
        int charCount = 0;
        int start = startGlyphIndex+ci.getBeginIndex();
        int end   = endGlyphIndex+ci.getBeginIndex();

        for (char c = ci.setIndex(start); ci.getIndex() <= end; c=ci.next()) {
            if (ArabicTextHandler.isLigature(c)) {
                charCount += ArabicTextHandler.getNumChars(c);
            } else {
                charCount++;
            }
        }

        return charCount;
    }

    /**
     * Draws this glyph vector.
     */
    public void draw(Graphics2D graphics2D,
                     AttributedCharacterIterator aci) {
        int numGlyphs = getNumGlyphs();

        aci.first();
        TextPaintInfo tpi = (TextPaintInfo)aci.getAttribute
            (GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO);
        if (tpi == null) return;
        Paint  fillPaint   = tpi.fillPaint;
        Stroke stroke      = tpi.strokeStroke;
        Paint  strokePaint = tpi.strokePaint;

        if ((fillPaint == null) && ((strokePaint == null) ||
                                    (stroke == null)))
            return;

        boolean useHinting = drawGlyphVectorWorks;
        if (useHinting && (stroke != null) && (strokePaint != null))
            // Can't stroke with drawGlyphVector.
            useHinting = false;

        if (useHinting && 
            (fillPaint != null) && !(fillPaint instanceof Color))
            // The coordinate system is different for drawGlyphVector.
            // So complex paints aren't positioned properly.
            useHinting = false;

        if (useHinting) {
            Object v1 = graphics2D.getRenderingHint
                (RenderingHints.KEY_TEXT_ANTIALIASING);
            Object v2 = graphics2D.getRenderingHint
                (RenderingHints.KEY_STROKE_CONTROL);
            // text-rendering = geometricPrecision so fill shapes.
            if ((v1 == RenderingHints.VALUE_TEXT_ANTIALIAS_ON) &&
                (v2 == RenderingHints.VALUE_STROKE_PURE))
                useHinting = false;
        }

        final int typeGRot   = AffineTransform.TYPE_GENERAL_ROTATION;
        final int typeGTrans = AffineTransform.TYPE_GENERAL_TRANSFORM;

        if (useHinting) {
            // Check if usr->dev transform has general rotation,
            // or shear..
            AffineTransform at = graphics2D.getTransform();
            int type = at.getType();
            if (((type & typeGTrans) != 0) || ((type & typeGRot)  != 0))
                useHinting = false;
        }
            
        if (useHinting) {
            for (int i=0; i<numGlyphs; i++) {
                if (!glyphVisible[i]) {
                    useHinting = false;
                    break;
                }
                AffineTransform at = glyphTransforms[i];
                if (at != null) {
                    int type = at.getType();
                    if ((type & ~AffineTransform.TYPE_TRANSLATION) == 0) {
                        // Just translation
                    } else if (glyphVectorTransformWorks &&
                               ((type & typeGTrans) == 0) &&
                               ((type & typeGRot)   == 0)) {
                        // It's a simple 90Deg rotate, and we can put
                        // it into the GlyphVector.
                    } else {
                        // we can't (or it doesn't make sense
                        // to use the GlyphVector.
                        useHinting = false;
                        break;
                    }
                }
            }
        }

        if (useHinting) {
            double sf = scaleFactor;
            double [] mat = new double[6];
            for (int i=0; i< numGlyphs; i++) {
                Point2D         pos = glyphPositions[i];
                double x = pos.getX();
                double y = pos.getY();
                AffineTransform at = glyphTransforms[i];
                if (at != null) {
                    // Scale the translate portion of matrix,
                    // and add it into the position.
                    at.getMatrix(mat);
                    x += mat[4];
                    y += mat[5];
                    if ((mat[0] != 1) || (mat[1] != 0) ||
                        (mat[2] != 0) || (mat[3] != 1)) {
                        // More than just translation.
                        mat[4] = 0; mat[5] = 0;
                        at = new AffineTransform(mat);
                    } else {
                        at = null;
                    }
                }
                pos = new Point2D.Double(x/sf, y/sf);
                awtGlyphVector.setGlyphPosition(i, pos);
                awtGlyphVector.setGlyphTransform(i, at);
            }
            graphics2D.scale(sf, sf);
            graphics2D.setPaint(fillPaint);
            graphics2D.drawGlyphVector(awtGlyphVector, 0, 0);
            graphics2D.scale(1/sf, 1/sf);

            for (int i=0; i< numGlyphs; i++) {
                Point2D         pos = defaultGlyphPositions[i];
                awtGlyphVector.setGlyphPosition(i, pos);
                awtGlyphVector.setGlyphTransform(i, null);
            }

        } else {
            Shape outline = getOutline();

            // check if we need to fill this glyph
            if (fillPaint != null) {
                graphics2D.setPaint(fillPaint);
                graphics2D.fill(outline);
            }

            // check if we need to draw the outline of this glyph
            if (stroke != null && strokePaint != null) {
                graphics2D.setStroke(stroke);
                graphics2D.setPaint(strokePaint);
                graphics2D.draw(outline);
            }
        }
    }
}
