/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

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 = (Object)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 = (Object)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());
    }
}
