/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: FOPGVTGlyphVector.java 1831346 2018-05-10 14:30:38Z matthias $ */

package org.apache.fop.svg.font;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
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 java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.List;

import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTGlyphMetrics;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.font.GVTLineMetrics;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;

import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontMetrics;
import org.apache.fop.fonts.GlyphMapping;
import org.apache.fop.fonts.TextFragment;
import org.apache.fop.traits.MinOptMax;

public class FOPGVTGlyphVector implements GVTGlyphVector {

    protected final TextFragment text;

    protected final FOPGVTFont font;

    private final int fontSize;

    private final FontMetrics fontMetrics;

    private final FontRenderContext frc;

    protected int[] glyphs;

    protected List associations;

    protected int[][] gposAdjustments;

    protected float[] positions;

    protected Rectangle2D[] boundingBoxes;

    protected GeneralPath outline;

    protected AffineTransform[] glyphTransforms;

    protected boolean[] glyphVisibilities;

    protected Rectangle2D logicalBounds;

    FOPGVTGlyphVector(FOPGVTFont font, final CharacterIterator iter, FontRenderContext frc) {
        this.text = new SVGTextFragment(iter);
        this.font = font;
        Font f = font.getFont();
        this.fontSize = f.getFontSize();
        this.fontMetrics = f.getFontMetrics();
        this.frc = frc;
    }

    public void performDefaultLayout() {
        Font f = font.getFont();
        MinOptMax letterSpaceIPD = MinOptMax.ZERO;
        MinOptMax[] letterSpaceAdjustments = new MinOptMax[text.getEndIndex()];
        boolean retainControls = false;
        GlyphMapping mapping = GlyphMapping.doGlyphMapping(text, text.getBeginIndex(), text.getEndIndex(),
            f, letterSpaceIPD, letterSpaceAdjustments, '\0', '\0',
            false, text.getBidiLevel(), true, true, retainControls);
        CharacterIterator glyphAsCharIter =
            mapping.mapping != null ? new StringCharacterIterator(mapping.mapping) : text.getIterator();
        this.glyphs = buildGlyphs(f, glyphAsCharIter);
        this.associations = mapping.associations;
        this.gposAdjustments = mapping.gposAdjustments;
        if (text.getBeginIndex() > 0) {
            int arrlen = text.getEndIndex() - text.getBeginIndex();
            MinOptMax[] letterSpaceAdjustmentsNew = new MinOptMax[arrlen];
            System.arraycopy(letterSpaceAdjustments, text.getBeginIndex(), letterSpaceAdjustmentsNew,
                    0, arrlen);
            letterSpaceAdjustments = letterSpaceAdjustmentsNew;
        }
        this.positions = buildGlyphPositions(glyphAsCharIter, mapping.gposAdjustments, letterSpaceAdjustments);
        this.glyphVisibilities = new boolean[this.glyphs.length];
        Arrays.fill(glyphVisibilities, true);
        this.glyphTransforms = new AffineTransform[this.glyphs.length];
    }

    private static class SVGTextFragment implements TextFragment {

        private final CharacterIterator charIter;

        private String script;

        private String language;

        private int level = -1;

        SVGTextFragment(CharacterIterator charIter) {
            this.charIter = charIter;
            if (charIter instanceof AttributedCharacterIterator) {
                AttributedCharacterIterator aci = (AttributedCharacterIterator) charIter;
                aci.first();
                this.script = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.SCRIPT);
                this.language = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.LANGUAGE);
                Integer level = (Integer) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.BIDI_LEVEL);
                if (level != null) {
                    this.level = level;
                }
            }
        }

        public CharacterIterator getIterator() {
            return charIter;
        }

        public int getBeginIndex() {
            return charIter.getBeginIndex();
        }

        public int getEndIndex() {
            return charIter.getEndIndex();
        }

        // TODO - [GA] the following appears to be broken because it ignores
        // sttart and end index arguments
        public CharSequence subSequence(int startIndex, int endIndex) {
            StringBuilder sb = new StringBuilder();
            for (char c = charIter.first(); c != CharacterIterator.DONE; c = charIter.next()) {
                sb.append(c);
            }
            return sb.toString();
        }

        public String getScript() {
            if (script != null) {
                return script;
            } else {
                return "auto";
            }
        }

        public String getLanguage() {
            if (language != null) {
                return language;
            } else {
                return "none";
            }
        }

        public int getBidiLevel() {
            return level;
        }

        public char charAt(int index) {
            return charIter.setIndex(index - charIter.getBeginIndex());
        }
    }

    private int[] buildGlyphs(Font font, final CharacterIterator glyphAsCharIter) {
        int[] glyphs = new int[glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex()];
        int index = 0;
        for (char c = glyphAsCharIter.first();  c != CharacterIterator.DONE; c = glyphAsCharIter.next()) {
            glyphs[index] = font.mapChar(c);
            index++;
        }
        return glyphs;
    }

    private static final int[] PA_ZERO = new int[4];

    /**
     * Build glyph position array.
     * @param glyphAsCharIter iterator for mapped glyphs as char codes (not glyph codes)
     * @param dp optionally null glyph position adjustments array
     * @param lsa optionally null letter space adjustments array
     * @return array of floats that denote [X,Y] position pairs for each glyph including
     * including an implied subsequent glyph; i.e., returned array contains one more pair
     * than the numbers of glyphs, where the position denoted by this last pair represents
     * the position after the last glyph has incurred advancement
     */
    private float[] buildGlyphPositions(final CharacterIterator glyphAsCharIter, int[][] dp, MinOptMax[] lsa) {
        int numGlyphs = glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex();
        float[] positions = new float[2 * (numGlyphs + 1)];
        float xc = 0f;
        float yc = 0f;
        if (dp != null) {
            for (int i = 0; i < numGlyphs + 1; ++i) {
                int[] pa = ((i >= dp.length) || (dp[i] == null)) ? PA_ZERO : dp[i];
                float xo = xc + ((float) pa[0]) / 1000f;
                float yo = yc - ((float) pa[1]) / 1000f;
                float xa = getGlyphWidth(i) + ((float) pa[2]) / 1000f;
                float ya = ((float) pa[3]) / 1000f;
                int k = 2 * i;
                positions[k + 0] = xo;
                positions[k + 1] = yo;
                xc += xa;
                yc += ya;
            }
        } else if (lsa != null) {
            for (int i = 0; i < numGlyphs + 1; ++i) {
                MinOptMax sa = (((i + 1) >= lsa.length) || (lsa[i + 1] == null)) ? MinOptMax.ZERO : lsa[i + 1];
                float xo = xc;
                float yo = yc;
                float xa = getGlyphWidth(i) + sa.getOpt() / 1000f;
                float ya = 0;
                int k = 2 * i;
                positions[k + 0] = xo;
                positions[k + 1] = yo;
                xc += xa;
                yc += ya;
            }
        }
        return positions;
    }

    private float getGlyphWidth(int index) {
        if (index < glyphs.length) {
            return fontMetrics.getWidth(glyphs[index], fontSize) / 1000000f;
        } else {
            return 0f;
        }
    }

    public GVTFont getFont() {
        return font;
    }

    public FontRenderContext getFontRenderContext() {
        return frc;
    }

    public void setGlyphCode(int glyphIndex, int glyphCode) {
        glyphs[glyphIndex] = glyphCode;
    }

    public int getGlyphCode(int glyphIndex) {
        return glyphs[glyphIndex];
    }

    public int[] getGlyphCodes(int beginGlyphIndex, int numEntries,
            int[] codeReturn) {
        if (codeReturn == null) {
            codeReturn = new int[numEntries];
        }
        System.arraycopy(glyphs, beginGlyphIndex, codeReturn, 0, numEntries);
        return codeReturn;
    }

    public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException();
    }

    public Shape getGlyphLogicalBounds(int glyphIndex) {
        GVTGlyphMetrics metrics = getGlyphMetrics(glyphIndex);
        Point2D pos = getGlyphPosition(glyphIndex);
        GVTLineMetrics fontMetrics = font.getLineMetrics(0);
        Rectangle2D bounds = new Rectangle2D.Float(0, -fontMetrics.getDescent(), metrics.getHorizontalAdvance(),
                fontMetrics.getAscent() + fontMetrics.getDescent());
        AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY());
        AffineTransform transf = getGlyphTransform(glyphIndex);
        if (transf != null) {
            t.concatenate(transf);
        }
        t.scale(1, -1); // Translate from glyph coordinate system to user
        return t.createTransformedShape(bounds);
    }

    public GVTGlyphMetrics getGlyphMetrics(int glyphIndex) {
        Rectangle2D bbox = getBoundingBoxes()[glyphIndex];
        return new GVTGlyphMetrics(positions[2 * (glyphIndex + 1)] - positions[2 * glyphIndex],
                (fontMetrics.getAscender(fontSize) - fontMetrics.getDescender(fontSize)) / 1000000f,
                bbox, GlyphMetrics.STANDARD);
    }

    public Shape getGlyphOutline(int glyphIndex) {
        Shape glyphBox = getBoundingBoxes()[glyphIndex];
        AffineTransform tr = AffineTransform.getTranslateInstance(positions[glyphIndex * 2],
                positions[glyphIndex * 2 + 1]);
        AffineTransform glyphTransform = getGlyphTransform(glyphIndex);
        if (glyphTransform != null) {
            tr.concatenate(glyphTransform);
        }
        return tr.createTransformedShape(glyphBox);
    }

    public Rectangle2D getGlyphCellBounds(int glyphIndex) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException();
    }

    public int[][] getGlyphPositionAdjustments() {
        return gposAdjustments;
    }

    public Point2D getGlyphPosition(int glyphIndex) {
        int positionIndex = glyphIndex * 2;
        return new Point2D.Float(positions[positionIndex], positions[positionIndex + 1]);
    }

    public float[] getGlyphPositions(int beginGlyphIndex, int numEntries, float[] positionReturn) {
        if (positionReturn == null) {
            positionReturn = new float[numEntries * 2];
        }
        System.arraycopy(positions, beginGlyphIndex * 2, positionReturn, 0, numEntries * 2);
        return positionReturn;
    }

    public AffineTransform getGlyphTransform(int glyphIndex) {
        return glyphTransforms[glyphIndex];
    }

    public Shape getGlyphVisualBounds(int glyphIndex) {
        Rectangle2D bbox = getBoundingBoxes()[glyphIndex];
        Point2D pos = getGlyphPosition(glyphIndex);
        AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY());
        AffineTransform transf = getGlyphTransform(glyphIndex);
        if (transf != null) {
            t.concatenate(transf);
        }
        return t.createTransformedShape(bbox);
    }

    public Rectangle2D getLogicalBounds() {
        if (logicalBounds == null) {
            GeneralPath logicalBoundsPath = new GeneralPath();
            for (int i = 0; i < getNumGlyphs(); i++) {
                Shape glyphLogicalBounds = getGlyphLogicalBounds(i);
                logicalBoundsPath.append(glyphLogicalBounds, false);
            }
            logicalBounds = logicalBoundsPath.getBounds2D();
        }
        return logicalBounds;
    }

    public int getNumGlyphs() {
        return glyphs.length;
    }

    public Shape getOutline() {
        if (outline == null) {
            outline = new GeneralPath();
            for (int i = 0; i < glyphs.length; i++) {
                outline.append(getGlyphOutline(i), false);
            }
        }
        return outline;
    }

    public Shape getOutline(float x, float y) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException();
    }

    public Rectangle2D getGeometricBounds() {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException();
    }

    public Rectangle2D getBounds2D(AttributedCharacterIterator aci) {
        return getOutline().getBounds2D();
    }

    public void setGlyphPosition(int glyphIndex, Point2D newPos) {
        int idx = glyphIndex * 2;
        positions[idx] = (float) newPos.getX();
        positions[idx + 1] = (float) newPos.getY();
    }

    public void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
        glyphTransforms[glyphIndex] = newTX;
    }

    public void setGlyphVisible(int glyphIndex, boolean visible) {
        glyphVisibilities[glyphIndex] = visible;
    }

    public boolean isGlyphVisible(int glyphIndex) {
        return glyphVisibilities[glyphIndex];
    }

    public int getCharacterCount(int startGlyphIndex, int endGlyphIndex) {
        // TODO Not that simple if complex scripts are involved
        return endGlyphIndex - startGlyphIndex + 1;
    }

    public boolean isReversed() {
        return false;
    }

    public void maybeReverse(boolean mirror) {
    }

    public void draw(Graphics2D graphics2d, AttributedCharacterIterator aci) {
        // NOP
    }

    private Rectangle2D[] getBoundingBoxes() {
        if (boundingBoxes == null) {
            buildBoundingBoxes();
        }
        return boundingBoxes;
    }

    private void buildBoundingBoxes() {
        boundingBoxes = new Rectangle2D[glyphs.length];
        for (int i = 0; i < glyphs.length; i++) {
            Rectangle bbox = fontMetrics.getBoundingBox(glyphs[i], fontSize);
            boundingBoxes[i] = new Rectangle2D.Float(bbox.x / 1000000f, -(bbox.y + bbox.height) / 1000000f,
                    bbox.width / 1000000f, bbox.height / 1000000f);
        }
    }

}
