/*

 ============================================================================
                   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.text;

import java.awt.font.FontRenderContext;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;

import org.apache.batik.gvt.font.AWTGVTFont;
import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.font.GVTLineMetrics;

public class GlyphIterator {
    public static final AttributedCharacterIterator.Attribute PREFORMATTED 
        = GVTAttributedCharacterIterator.TextAttribute.PREFORMATTED;

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

    public static final AttributedCharacterIterator.Attribute 
        TEXT_COMPOUND_DELIMITER 
        = GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER;
    public static final 
        AttributedCharacterIterator.Attribute GVT_FONT 
        = GVTAttributedCharacterIterator.TextAttribute.GVT_FONT;

    public static final char SOFT_HYPHEN       = 0x00AD;
    public static final char ZERO_WIDTH_SPACE  = 0x200B;
    public static final char ZERO_WIDTH_JOINER = 0x200D;

    // Glyph index of current glyph
    int   idx         = -1;
    // Glyph index of last 'printing' glyph.
    int   chIdx       = -1;
    int   lineIdx     = -1;

    // The ACI index of current glyph.
    int   aciIdx      = -1;
    // The number of characters in ACI for current Glyph.
    int   charCount   = -1;

    // The total advance for line including last non-space glyph
    float adv         =  0;
    // The total advance for line including spaces at end of line.
    float adj         =  0;
    // The runLimit for current font
    int     runLimit   = 0;

    // The runLimit for current line element (need a line break at end).
    int     lineBreakRunLimit = 0;
    int     lineBreakCount    = 0;


    GVTFont font       = null;
    int     fontStart  = 0;
    float   maxAscent  = 0;
    float   maxDescent = 0;
    float   maxFontSize = 0;

    float width = 0;
    // The current char (from ACI)
    char ch = 0;
    // The number of glyphs in gv.
    int numGlyphs = 0;
    // The AttributedCharacterIterator.
    AttributedCharacterIterator aci;
    // The GVTGlyphVector for this Text chunk.
    GVTGlyphVector gv;
    // The GlyphPositions for this GlyphVector (Shared)
    float [] gp;
    // The font render context for this GylphVector.
    FontRenderContext frc;

    // The Indexes of new leftShift amounts (soft-hyphens)
    int   [] leftShiftIdx = null;
    // The amount of new leftShifts (soft-hyphen adv)
    float [] leftShiftAmt = null;
    // The current left shift (inherited from previous line).
    int leftShift = 0;

    Point2D gvBase = null;

    
    public GlyphIterator(AttributedCharacterIterator aci,
                         GVTGlyphVector gv) {
        this.aci       = aci;
        this.gv        = gv;
 
        this.idx       = 0;
        this.chIdx     = 0;
        this.lineIdx   = 0;
        this.aciIdx    = aci.getBeginIndex();
        this.charCount = gv.getCharacterCount(idx, idx);
        this.ch        = aci.first();
        this.frc       = gv.getFontRenderContext();
        
        this.font = (GVTFont)aci.getAttribute(GVT_FONT);
        if (font == null) {
            font = new AWTGVTFont(aci.getAttributes());
        }
        fontStart = aciIdx;
        this.maxFontSize = -Float.MAX_VALUE;
        this.maxAscent   = -Float.MAX_VALUE;
        this.maxDescent  = -Float.MAX_VALUE;

        // Figure out where the font size might change again...
        this.runLimit  = aci.getRunLimit(TEXT_COMPOUND_DELIMITER);
        
        this.lineBreakRunLimit = aci.getRunLimit(FLOW_LINE_BREAK);
        Object o = aci.getAttribute(FLOW_LINE_BREAK);
        this.lineBreakCount = (o == null)?0:1;


        this.numGlyphs   = gv.getNumGlyphs();
        this.gp          = gv.getGlyphPositions(0, this.numGlyphs+1, null);
        this.gvBase      = new Point2D.Float(gp[0], gp[1]);
        this.adv = getCharWidth();
        this.adj = getCharAdvance();
    }

    public GlyphIterator(GlyphIterator gi) {
        gi.copy(this);
    }

    public GlyphIterator copy() {
        return new GlyphIterator(this);
    }

    public GlyphIterator copy(GlyphIterator gi) {
        if (gi == null)
            return new GlyphIterator(this);

        gi.idx        = this.idx;
        gi.chIdx      = this.chIdx;
        gi.aciIdx     = this.aciIdx;
        gi.charCount  = this.charCount;
        gi.adv        = this.adv;
        gi.adj        = this.adj;
        gi.runLimit   = this.runLimit;
        gi.ch         = this.ch;
        gi.numGlyphs  = this.numGlyphs;
        gi.gp         = this.gp;
        gi.gvBase     = this.gvBase;

        gi.lineBreakRunLimit = this.lineBreakRunLimit;
        gi.lineBreakCount    = this.lineBreakCount;

        gi.frc         = this.frc;
        gi.font        = this.font;
        gi.fontStart   = this.fontStart;
        gi.maxAscent   = this.maxAscent;
        gi.maxDescent  = this.maxDescent;
        gi.maxFontSize = this.maxFontSize;

        gi.leftShift    = this.leftShift;
        gi.leftShiftIdx = this.leftShiftIdx;
        gi.leftShiftAmt = this.leftShiftAmt;
        return gi;
    }

    /**
     * @return  The index into glyph vector for current character.
     */
    public int getGlyphIndex() { return idx; }

    /**
     * @return the current character.
     */
    public char getChar() { return ch; }

    /**
     * @return The index into Attributed Character iterator for
     * current character.  
     */
    public int getACIIndex() { return aciIdx; }

    /**
     * @return The current advance for the line, this is the 'visual width'
     * of the current line.
     */
    public float getAdv() { return adv; }

    /**
     * @return The origin of the glyph vector (the point all glyphs are
     * layed out with respect to).
     */
    public Point2D getOrigin() { return gvBase; }

    /**
     * @return The current adjustment for the line.  This is the ammount
     * that needs to be subracted from the following line to get it back
     * to the start of the next line.
     */
    public float getAdj() { return adj; }

    public float getMaxFontSize()  {
        if (aciIdx >= fontStart) {
            int newFS = aciIdx + charCount;
            updateLineMetrics(newFS);
            fontStart = newFS;
        }
        return maxFontSize; 
    }

    public float getMaxAscent()  { 
        if (aciIdx >= fontStart) {
            int newFS = aciIdx + charCount;
            updateLineMetrics(newFS);
            fontStart = newFS;
        }
        return maxAscent; 
    }

    public float getMaxDescent() { 
        if (aciIdx >= fontStart) {
            int newFS = aciIdx + charCount;
            updateLineMetrics(newFS);
            fontStart = newFS;
        }
        return maxDescent; 
    }

    public boolean isLastChar() {
        return (idx == (numGlyphs-1));
    }

    public boolean done() {
        return (idx >= numGlyphs);
    }

    public boolean isBreakChar() {
        switch (ch) {
        case GlyphIterator.ZERO_WIDTH_SPACE:  return true;            
        case GlyphIterator.ZERO_WIDTH_JOINER: return false;            
        case GlyphIterator.SOFT_HYPHEN:       return true;
        case ' ': case '\t':                  return true;
        default:                              return false;
        }
    }

    protected boolean isPrinting(char tstCH) {
        switch (ch) {
        case GlyphIterator.ZERO_WIDTH_SPACE:  return false;            
        case GlyphIterator.ZERO_WIDTH_JOINER: return false;            
        case GlyphIterator.SOFT_HYPHEN:       return true;
        case ' ': case '\t':                  return false;
        default:                              return true;
        }
    }        
       
    public int getLineBreaks() {
        int ret = 0;
        if (aciIdx+charCount >= lineBreakRunLimit) {
            // Next char is outside this line element so break after
            // the current char.
            ret = lineBreakCount;

            // Update the lineBreakRunLimit, this is a bit tricky since
            // The attribute doesn't change until the next glyph...
            aci.setIndex(aciIdx+charCount);
            lineBreakRunLimit = aci.getRunLimit(FLOW_LINE_BREAK);
            aci.setIndex(aciIdx);  // Restore location...

            Object o = aci.getAttribute(FLOW_LINE_BREAK);
            lineBreakCount = (o == null)?0:1;
        }
        return ret;
    }

    /**
     * Move iterator to the next char.
     */
    public void nextChar() {
        if ((ch == SOFT_HYPHEN)      || 
            (ch == ZERO_WIDTH_SPACE) ||
            (ch == ZERO_WIDTH_JOINER)) {
            // Special handling for soft hyphens and zero-width spaces
            gv.setGlyphVisible(idx, false);
            float chAdv = getCharAdvance();
            adj -= chAdv;
            addLeftShift(idx, chAdv);
        }

        aciIdx += charCount;
        ch = aci.setIndex(aciIdx);
        idx++;
        charCount = gv.getCharacterCount(idx,idx);
        if (idx == numGlyphs) return;

        if (aciIdx >= runLimit) {
            updateLineMetrics(aciIdx);
            runLimit = aci.getRunLimit(TEXT_COMPOUND_DELIMITER);
            font     = (GVTFont)aci.getAttribute(GVT_FONT);
            if (font == null) {
                font = new AWTGVTFont(aci.getAttributes());
            }
            fontStart = aciIdx;
        }

        float chAdv = getCharAdvance();
        adj += chAdv;
        if (isPrinting()) {
            chIdx = idx;
            float chW   = getCharWidth();
            adv = adj-(chAdv-chW);
        }
    }

    protected void addLeftShift(int idx, float chAdv) {
        if (leftShiftIdx == null) {
            leftShiftIdx = new int[1];
            leftShiftIdx[0] = idx;
            leftShiftAmt = new float[1];
            leftShiftAmt[0] = chAdv;
        } else {
            int [] newLeftShiftIdx = new int[leftShiftIdx.length+1];
            for (int i=0; i<leftShiftIdx.length; i++)
                newLeftShiftIdx[i] = leftShiftIdx[i];
            newLeftShiftIdx[leftShiftIdx.length] = idx;
            leftShiftIdx = newLeftShiftIdx;

            float [] newLeftShiftAmt = new float[leftShiftAmt.length+1];
            for (int i=0; i<leftShiftAmt.length; i++)
                newLeftShiftAmt[i] = leftShiftAmt[i];
            newLeftShiftAmt[leftShiftAmt.length] = chAdv;
            leftShiftAmt = newLeftShiftAmt;
        }
    }

    protected void updateLineMetrics(int end) {
        GVTLineMetrics glm = font.getLineMetrics
            (aci, fontStart, end, frc);
        float ascent  = glm.getAscent();
        float descent = glm.getDescent();
        float fontSz  = font.getSize();
        if (ascent >  maxAscent)   maxAscent   = ascent;
        if (descent > maxDescent)  maxDescent  = descent;
        if (fontSz >  maxFontSize) maxFontSize = fontSz;
    }

    public LineInfo newLine(Point2D.Float loc, 
                            float lineWidth, 
                            boolean partial,
                            Point2D.Float verticalAlignOffset) {
        if (ch == SOFT_HYPHEN) {
            gv.setGlyphVisible(idx, true);
        }

        int lsi = 0;
        int nextLSI;
        if (leftShiftIdx != null) nextLSI = leftShiftIdx[lsi];
        else                      nextLSI = idx+1;
        for (int ci=lineIdx; ci<=idx; ci++) {
            if (ci == nextLSI) {
                leftShift += leftShiftAmt[lsi++];
                if (lsi < leftShiftIdx.length)
                    nextLSI = leftShiftIdx[lsi];
            }
            gv.setGlyphPosition(ci, new Point2D.Float(gp[2*ci]-leftShift,
                                                      gp[2*ci+1]));
        }
        leftShiftIdx = null;
        leftShiftAmt = null;

        float lineInfoChW;
        int   hideIdx;
        // System.out.println("ChIdx: " + chIdx);
        if ((chIdx != 0) || (isPrinting())) {
            lineInfoChW = getCharWidth(chIdx);
            hideIdx     = chIdx+1;
        } else {
            lineInfoChW = 0;
            hideIdx     = 0;
        }

        int   lineInfoIdx = idx+1;
        float lineInfoAdv = adv;
        float lineInfoAdj = adj;
        while (!done()) {
          adv=0;
          adj=0;

          if ((ch == ZERO_WIDTH_SPACE) ||
              (ch == ZERO_WIDTH_JOINER))
            gv.setGlyphVisible(idx, false);

          ch = 0;  // Disable soft-hyphen/ZWS advance adjustment.
          nextChar();

          if (isPrinting()) break;

          lineInfoIdx = idx+1;
          lineInfoAdj += adj;
        }

        // hide trailing spaces if any
        for (int i = hideIdx; i<lineInfoIdx; i++) {
            gv.setGlyphVisible(i, false);
        }

        maxAscent   = -Float.MAX_VALUE;
        maxDescent  = -Float.MAX_VALUE;
        maxFontSize = -Float.MAX_VALUE;
        LineInfo ret = new LineInfo(loc, aci, gv, lineIdx, lineInfoIdx, 
                            lineInfoAdj, lineInfoAdv, lineInfoChW, 
                                    lineWidth, partial, verticalAlignOffset);
        lineIdx = idx;

        return ret;
    }

    public boolean isPrinting() {
        if (aci.getAttribute(PREFORMATTED) == Boolean.TRUE)
            return true;
        return isPrinting(ch);
    }

    /**
     * Get the advance associated with the current glyph
     */
    public float getCharAdvance() {
        return getCharAdvance(idx);
    }

    /**
     * Get the visual advance associated with the current glyph.
     * This is the distance from the location of the glyph to
     * the rightmost part of the glyph.
     */
    public float getCharWidth() {
        return getCharWidth(idx);
    }

    /**
     * Get the advance associated with any glyph
     */
    protected float getCharAdvance(int gvIdx) {
        return gp[2*gvIdx+2] - gp[2*gvIdx];
    }

    /**
     * Get the visual advance associated with the current glyph.
     * This is the distance from the location of the glyph to
     * the rightmost part of the glyph.
     */
    protected float getCharWidth(int gvIdx) {
        Rectangle2D lcBound = gv.getGlyphVisualBounds(gvIdx).getBounds2D();
        Point2D     lcLoc   = gv.getGlyphPosition(gvIdx);
        return (float)(lcBound.getX()+lcBound.getWidth()- lcLoc.getX());
    }
}
