/*
 * 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: PSTextPainter.java 1357883 2012-07-05 20:29:53Z gadams $ */

package org.apache.fop.render.ps;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.util.Iterator;
import java.util.List;

import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.text.TextPaintInfo;
import org.apache.batik.gvt.text.TextSpanLayout;

import org.apache.xmlgraphics.java2d.ps.PSGraphics2D;
import org.apache.xmlgraphics.ps.PSGenerator;

import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontMetrics;
import org.apache.fop.fonts.LazyFont;
import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.svg.NativeTextPainter;
import org.apache.fop.util.CharUtilities;
import org.apache.fop.util.HexEncoder;

/**
 * Renders the attributed character iterator of a text node.
 * This class draws the text directly using PostScript text operators so
 * the text is not drawn using shapes which makes the PS files larger.
 * <p>
 * The text runs are split into smaller text runs that can be bundles in single
 * calls of the xshow, yshow or xyshow operators. For outline text, the charpath
 * operator is used.
 */
public class PSTextPainter extends NativeTextPainter {

    private static final boolean DEBUG = false;

    private FontResourceCache fontResources;

    private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform();

    /**
     * Create a new PS text painter with the given font information.
     * @param fontInfo the font collection
     */
    public PSTextPainter(FontInfo fontInfo) {
        super(fontInfo);
        this.fontResources = new FontResourceCache(fontInfo);
    }

    /** {@inheritDoc} */
    protected boolean isSupported(Graphics2D g2d) {
        return g2d instanceof PSGraphics2D;
    }

    /** {@inheritDoc} */
    protected void paintTextRun(TextRun textRun, Graphics2D g2d) throws IOException {
        AttributedCharacterIterator runaci = textRun.getACI();
        runaci.first();

        TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO);
        if (tpi == null || !tpi.visible) {
            return;
        }
        if ((tpi != null) && (tpi.composite != null)) {
            g2d.setComposite(tpi.composite);
        }

        //------------------------------------
        TextSpanLayout layout = textRun.getLayout();
        logTextRun(runaci, layout);
        CharSequence chars = collectCharacters(runaci);
        runaci.first(); //Reset ACI

        final PSGraphics2D ps = (PSGraphics2D)g2d;
        final PSGenerator gen = ps.getPSGenerator();
        ps.preparePainting();

        if (DEBUG) {
            log.debug("Text: " + chars);
            gen.commentln("%Text: " + chars);
        }

        GeneralPath debugShapes = null;
        if (DEBUG) {
            debugShapes = new GeneralPath();
        }

        TextUtil textUtil = new TextUtil(gen);
        textUtil.setupFonts(runaci);
        if (!textUtil.hasFonts()) {
            //Draw using Java2D when no native fonts are available
            textRun.getLayout().draw(g2d);
            return;
        }

        gen.saveGraphicsState();
        gen.concatMatrix(g2d.getTransform());
        Shape imclip = g2d.getClip();
        clip(ps, imclip);

        gen.writeln("BT"); //beginTextObject()

        AffineTransform localTransform = new AffineTransform();
        Point2D prevPos = null;
        GVTGlyphVector gv = layout.getGlyphVector();
        PSTextRun psRun = new PSTextRun(); //Used to split a text run into smaller runs
        for (int index = 0, c = gv.getNumGlyphs(); index < c; index++) {
            char ch = chars.charAt(index);
            boolean visibleChar = gv.isGlyphVisible(index)
                || (CharUtilities.isAnySpace(ch) && !CharUtilities.isZeroWidthSpace(ch));
            logCharacter(ch, layout, index, visibleChar);
            if (!visibleChar) {
                continue;
            }
            Point2D glyphPos = gv.getGlyphPosition(index);

            AffineTransform glyphTransform = gv.getGlyphTransform(index);
            if (log.isTraceEnabled()) {
                log.trace("pos " + glyphPos + ", transform " + glyphTransform);
            }
            if (DEBUG) {
                Shape sh = gv.getGlyphLogicalBounds(index);
                if (sh == null) {
                    sh = new Ellipse2D.Double(glyphPos.getX(), glyphPos.getY(), 2, 2);
                }
                debugShapes.append(sh, false);
            }

            //Exact position of the glyph
            localTransform.setToIdentity();
            localTransform.translate(glyphPos.getX(), glyphPos.getY());
            if (glyphTransform != null) {
                localTransform.concatenate(glyphTransform);
            }
            localTransform.scale(1, -1);

            boolean flushCurrentRun = false;
            //Try to optimize by combining characters using the same font and on the same line.
            if (glyphTransform != null) {
                //Happens for text-on-a-path
                flushCurrentRun = true;
            }
            if (psRun.getRunLength() >= 128) {
                //Don't let a run get too long
                flushCurrentRun = true;
            }

            //Note the position of the glyph relative to the previous one
            Point2D relPos;
            if (prevPos == null) {
                relPos = new Point2D.Double(0, 0);
            } else {
                relPos = new Point2D.Double(
                        glyphPos.getX() - prevPos.getX(),
                        glyphPos.getY() - prevPos.getY());
            }
            if (psRun.vertChanges == 0
                    && psRun.getHorizRunLength() > 2
                    && relPos.getY() != 0) {
                //new line
                flushCurrentRun = true;
            }

            //Select the actual character to paint
            char paintChar = (CharUtilities.isAnySpace(ch) ? ' ' : ch);

            //Select (sub)font for character
            Font f = textUtil.selectFontForChar(paintChar);
            char mapped = f.mapChar(ch);
            boolean fontChanging = textUtil.isFontChanging(f, mapped);
            if (fontChanging) {
                flushCurrentRun = true;
            }

            if (flushCurrentRun) {
                //Paint the current run and reset for the next run
                psRun.paint(ps, textUtil, tpi);
                psRun.reset();
            }

            //Track current run
            psRun.addCharacter(paintChar, relPos);
            psRun.noteStartingTransformation(localTransform);

            //Change font if necessary
            if (fontChanging) {
                textUtil.setCurrentFont(f, mapped);
            }

            //Update last position
            prevPos = glyphPos;
        }
        psRun.paint(ps, textUtil, tpi);
        gen.writeln("ET"); //endTextObject()
        gen.restoreGraphicsState();

        if (DEBUG) {
            //Paint debug shapes
            g2d.setStroke(new BasicStroke(0));
            g2d.setColor(Color.LIGHT_GRAY);
            g2d.draw(debugShapes);
        }
    }

    private void applyColor(Paint paint, final PSGenerator gen) throws IOException {
        if (paint == null) {
            return;
        } else if (paint instanceof Color) {
            Color col = (Color)paint;
            gen.useColor(col);
        } else {
            log.warn("Paint not supported: " + paint.toString());
        }
    }

    private PSFontResource getResourceForFont(Font f, String postfix) {
        String key = (postfix != null ? f.getFontName() + '_' + postfix : f.getFontName());
        return this.fontResources.getFontResourceForFontKey(key);
    }

    private void clip(PSGraphics2D ps, Shape shape) throws IOException {
        if (shape == null) {
            return;
        }
        ps.getPSGenerator().writeln("newpath");
        PathIterator iter = shape.getPathIterator(IDENTITY_TRANSFORM);
        ps.processPathIterator(iter);
        ps.getPSGenerator().writeln("clip");
    }

    private class TextUtil {

        private PSGenerator gen;
        private Font[] fonts;
        private Font currentFont;
        private int currentEncoding = -1;

        public TextUtil(PSGenerator gen) {
            this.gen = gen;
        }

        public Font selectFontForChar(char ch) {
            for (int i = 0, c = fonts.length; i < c; i++) {
                if (fonts[i].hasChar(ch)) {
                    return fonts[i];
                }
            }
            return fonts[0]; //TODO Maybe fall back to painting with shapes
        }

        public void writeTextMatrix(AffineTransform transform) throws IOException {
            double[] matrix = new double[6];
            transform.getMatrix(matrix);
            gen.writeln(gen.formatDouble5(matrix[0]) + " "
                + gen.formatDouble5(matrix[1]) + " "
                + gen.formatDouble5(matrix[2]) + " "
                + gen.formatDouble5(matrix[3]) + " "
                + gen.formatDouble5(matrix[4]) + " "
                + gen.formatDouble5(matrix[5]) + " Tm");
        }

        public boolean isFontChanging(Font f, char mapped) {
            if (f != getCurrentFont()) {
                return true;
            }
            if (mapped / 256 != getCurrentFontEncoding()) {
                return true;
            }
            return false; //Font is the same
        }

        public void selectFont(Font f, char mapped) throws IOException {
            int encoding = mapped / 256;
            String postfix = (encoding == 0 ? null : Integer.toString(encoding));
            PSFontResource res = getResourceForFont(f, postfix);
            gen.useFont("/" + res.getName(), f.getFontSize() / 1000f);
            res.notifyResourceUsageOnPage(gen.getResourceTracker());
        }

        public Font getCurrentFont() {
            return this.currentFont;
        }

        public int getCurrentFontEncoding() {
            return this.currentEncoding;
        }

        public void setCurrentFont(Font font, int encoding) {
            this.currentFont = font;
            this.currentEncoding = encoding;
        }

        public void setCurrentFont(Font font, char mapped) {
            int encoding = mapped / 256;
            setCurrentFont(font, encoding);
        }

        public void setupFonts(AttributedCharacterIterator runaci) {
            this.fonts = findFonts(runaci);
        }

        public boolean hasFonts() {
            return (fonts != null) && (fonts.length > 0);
        }

    }

    private class PSTextRun {

        private AffineTransform textTransform;
        private List relativePositions = new java.util.LinkedList();
        private StringBuffer currentChars = new StringBuffer();
        private int horizChanges = 0;
        private int vertChanges = 0;

        public void reset() {
            textTransform = null;
            currentChars.setLength(0);
            horizChanges = 0;
            vertChanges = 0;
            relativePositions.clear();
        }

        public int getHorizRunLength() {
            if (this.vertChanges == 0
                    && getRunLength() > 0) {
                return getRunLength();
            }
            return 0;
        }

        public void addCharacter(char paintChar, Point2D relPos) {
            addRelativePosition(relPos);
            currentChars.append(paintChar);
        }

        private void addRelativePosition(Point2D relPos) {
            if (getRunLength() > 0) {
                if (relPos.getX() != 0) {
                    horizChanges++;
                }
                if (relPos.getY() != 0) {
                    vertChanges++;
                }
            }
            relativePositions.add(relPos);
        }

        public void noteStartingTransformation(AffineTransform transform) {
            if (textTransform == null) {
                this.textTransform = new AffineTransform(transform);
            }
        }

        public int getRunLength() {
            return currentChars.length();
        }

        private boolean isXShow() {
            return vertChanges == 0;
        }

        private boolean isYShow() {
            return horizChanges == 0;
        }

        public void paint(PSGraphics2D g2d, TextUtil textUtil, TextPaintInfo tpi)
                    throws IOException {
            if (getRunLength() > 0) {
                if (log.isDebugEnabled()) {
                    log.debug("Text run: " + currentChars);
                }
                textUtil.writeTextMatrix(this.textTransform);
                if (isXShow()) {
                    log.debug("Horizontal text: xshow");
                    paintXYShow(g2d, textUtil, tpi.fillPaint, true, false);
                } else if (isYShow()) {
                    log.debug("Vertical text: yshow");
                    paintXYShow(g2d, textUtil, tpi.fillPaint, false, true);
                } else {
                    log.debug("Arbitrary text: xyshow");
                    paintXYShow(g2d, textUtil, tpi.fillPaint, true, true);
                }
                boolean stroke = (tpi.strokePaint != null) && (tpi.strokeStroke != null);
                if (stroke) {
                    log.debug("Stroked glyph outlines");
                    paintStrokedGlyphs(g2d, textUtil, tpi.strokePaint, tpi.strokeStroke);
                }
            }
        }

        private void paintXYShow(PSGraphics2D g2d, TextUtil textUtil, Paint paint,
                boolean x, boolean y) throws IOException {
            PSGenerator gen = textUtil.gen;
            char firstChar = this.currentChars.charAt(0);
            //Font only has to be setup up before the first character
            Font f = textUtil.selectFontForChar(firstChar);
            char mapped = f.mapChar(firstChar);
            textUtil.selectFont(f, mapped);
            textUtil.setCurrentFont(f, mapped);
            applyColor(paint, gen);

            FontMetrics metrics = f.getFontMetrics();
            boolean multiByte = metrics instanceof MultiByteFont
                    || metrics instanceof LazyFont
                            && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont;
            StringBuffer sb = new StringBuffer();
            sb.append(multiByte ? '<' : '(');
            for (int i = 0, c = this.currentChars.length(); i < c; i++) {
                char ch = this.currentChars.charAt(i);
                mapped = f.mapChar(ch);
                if (multiByte) {
                    sb.append(HexEncoder.encode(mapped));
                } else {
                    char codepoint = (char) (mapped % 256);
                    PSGenerator.escapeChar(codepoint, sb);
                }
            }
            sb.append(multiByte ? '>' : ')');
            if (x || y) {
                sb.append("\n[");
                int idx = 0;
                Iterator iter = this.relativePositions.iterator();
                while (iter.hasNext()) {
                    Point2D pt = (Point2D)iter.next();
                    if (idx > 0) {
                        if (x) {
                            sb.append(format(gen, pt.getX()));
                        }
                        if (y) {
                            if (x) {
                                sb.append(' ');
                            }
                            sb.append(format(gen, -pt.getY()));
                        }
                        if (idx % 8 == 0) {
                            sb.append('\n');
                        } else {
                            sb.append(' ');
                        }
                    }
                    idx++;
                }
                if (x) {
                    sb.append('0');
                }
                if (y) {
                    if (x) {
                        sb.append(' ');
                    }
                    sb.append('0');
                }
                sb.append(']');
            }
            sb.append(' ');
            if (x) {
                sb.append('x');
            }
            if (y) {
                sb.append('y');
            }
            sb.append("show"); // --> xshow, yshow or xyshow
            gen.writeln(sb.toString());
        }

        private String format(PSGenerator gen, double coord) {
            if (Math.abs(coord) < 0.00001) {
                return "0";
            } else {
                return gen.formatDouble5(coord);
            }
        }

        private void paintStrokedGlyphs(PSGraphics2D g2d, TextUtil textUtil,
                Paint strokePaint, Stroke stroke) throws IOException {
            PSGenerator gen = textUtil.gen;

            applyColor(strokePaint, gen);
            PSGraphics2D.applyStroke(stroke, gen);

            Font f = null;
            Iterator iter = this.relativePositions.iterator();
            iter.next();
            Point2D pos = new Point2D.Double(0, 0);
            gen.writeln("0 0 M");
            for (int i = 0, c = this.currentChars.length(); i < c; i++) {
                char ch = this.currentChars.charAt(0);
                if (i == 0) {
                    //Font only has to be setup up before the first character
                    f = textUtil.selectFontForChar(ch);
                }
                char mapped = f.mapChar(ch);
                if (i == 0) {
                    textUtil.selectFont(f, mapped);
                    textUtil.setCurrentFont(f, mapped);
                }
                //add glyph outlines to current path
                mapped = f.mapChar(this.currentChars.charAt(i));
                FontMetrics metrics = f.getFontMetrics();
                boolean multiByte = metrics instanceof MultiByteFont
                        || metrics instanceof LazyFont
                                && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont;
                if (multiByte) {
                    gen.write('<');
                    gen.write(HexEncoder.encode(mapped));
                    gen.write('>');
                } else {
                    char codepoint = (char)(mapped % 256);
                    gen.write("(" + codepoint + ")");
                }
                gen.writeln(" false charpath");

                if (iter.hasNext()) {
                    //Position for the next character
                    Point2D pt = (Point2D)iter.next();
                    pos.setLocation(pos.getX() + pt.getX(), pos.getY() - pt.getY());
                    gen.writeln(gen.formatDouble5(pos.getX()) + " "
                            + gen.formatDouble5(pos.getY()) + " M");
                }
            }
            gen.writeln("stroke"); //paints all accumulated glyph outlines
        }

    }

}


