/*
 * 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: PDFDocumentGraphics2D.java 1760942 2016-09-15 14:37:18Z ssteiner $ */

package org.apache.fop.svg;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;

import org.apache.xmlgraphics.image.GraphicsConstants;

import org.apache.fop.Version;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontSetup;
import org.apache.fop.pdf.PDFAnnotList;
import org.apache.fop.pdf.PDFColorHandler;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFFilterList;
import org.apache.fop.pdf.PDFNumber;
import org.apache.fop.pdf.PDFPage;
import org.apache.fop.pdf.PDFPaintingState;
import org.apache.fop.pdf.PDFReference;
import org.apache.fop.pdf.PDFResources;
import org.apache.fop.pdf.PDFStream;

/**
 * This class is a wrapper for the {@link PDFGraphics2D} that
 * is used to create a full document around the PDF rendering from
 * {@link PDFGraphics2D}.
 *
 * @see org.apache.fop.svg.PDFGraphics2D
 */
public class PDFDocumentGraphics2D extends PDFGraphics2D {

    private final PDFContext pdfContext;

    private int width;
    private int height;

    //for SVG scaling
    private float svgWidth;
    private float svgHeight;

    /** Normal PDF resolution (72dpi) */
    public static final int NORMAL_PDF_RESOLUTION = 72;
    /** Default device resolution (300dpi is a resonable quality for most purposes) */
    public static final int DEFAULT_NATIVE_DPI = GraphicsConstants.DEFAULT_SAMPLE_DPI;

    /**
     * The device resolution may be different from the normal target resolution. See
     * http://issues.apache.org/bugzilla/show_bug.cgi?id=37305
     */
    private float deviceDPI = DEFAULT_NATIVE_DPI;

    /** Initial clipping area, used to restore to original setting
     * when a new page is started. */
    protected Shape initialClip;

    /**
     * Initial transformation matrix, used to restore to original
     * setting when a new page is started.
     */
    protected AffineTransform initialTransform;

    /**
     * Create a new PDFDocumentGraphics2D.
     * This is used to create a new pdf document, the height,
     * width and output stream can be setup later.
     * For use by the transcoder which needs font information
     * for the bridge before the document size is known.
     * The resulting document is written to the stream after rendering.
     *
     * @param textAsShapes set this to true so that text will be rendered
     * using curves and not the font.
     */
    public PDFDocumentGraphics2D(boolean textAsShapes) {
        super(textAsShapes);

        this.pdfDoc = new PDFDocument("Apache FOP Version " + Version.getVersion()
                + ": PDFDocumentGraphics2D");
        this.pdfContext = new PDFContext();
        this.colorHandler = new PDFColorHandler(this.pdfDoc.getResources());
    }

    /**
     * Create a new PDFDocumentGraphics2D.
     * This is used to create a new pdf document of the given height
     * and width.
     * The resulting document is written to the stream after rendering.
     *
     * @param textAsShapes set this to true so that text will be rendered
     * using curves and not the font.
     * @param stream the stream that the final document should be written to.
     * @param width the width of the document (in points)
     * @param height the height of the document (in points)
     * @throws IOException an io exception if there is a problem
     *         writing to the output stream
     */
    public PDFDocumentGraphics2D(boolean textAsShapes, OutputStream stream,
                                 int width, int height) throws IOException {
        this(textAsShapes);
        setupDocument(stream, width, height);
    }

    /**
     * Create a new PDFDocumentGraphics2D.
     * This is used to create a new pdf document.
     * For use by the transcoder which needs font information
     * for the bridge before the document size is known.
     * The resulting document is written to the stream after rendering.
     * This constructor is Avalon-style.
     */
    public PDFDocumentGraphics2D() {
        this(false);
    }

    /**
     * Setup the document.
     * @param stream the output stream to write the document
     * @param width the width of the page
     * @param height the height of the page
     * @throws IOException an io exception if there is a problem
     *         writing to the output stream
     */
    public void setupDocument(OutputStream stream, int width, int height) throws IOException {
        this.width = width;
        this.height = height;

        pdfDoc.outputHeader(stream);
        setOutputStream(stream);
    }

    /**
     * Setup a default FontInfo instance if none has been setup before.
     */
    public void setupDefaultFontInfo() {
        if (fontInfo == null) {
            //Default minimal fonts
            FontInfo fontInfo = new FontInfo();
            boolean base14Kerning = false;
            FontSetup.setup(fontInfo, base14Kerning);
            setFontInfo(fontInfo);
        }
    }

    /**
     * Set the device resolution for rendering.  Will take effect at the
     * start of the next page.
     * @param deviceDPI the device resolution (in dpi)
     */
    public void setDeviceDPI(float deviceDPI) {
        this.deviceDPI = deviceDPI;
    }

    /**
     * @return the device resolution (in dpi) for rendering.
     */
    public float getDeviceDPI() {
        return deviceDPI;
    }

    /**
     * Sets the font info for this PDF document.
     * @param fontInfo the font info object with all the fonts
     */
    public void setFontInfo(FontInfo fontInfo) {
        this.fontInfo = fontInfo;
    }

    /**
     * Get the font info for this pdf document.
     * @return the font information
     */
    public FontInfo getFontInfo() {
        return fontInfo;
    }

    /**
     * Get the pdf document created by this class.
     * @return the pdf document
     */
    public PDFDocument getPDFDocument() {
        return this.pdfDoc;
    }

    /**
     * Return the PDFContext for this instance.
     * @return the PDFContext
     */
    public PDFContext getPDFContext() {
        return this.pdfContext;
    }

    /**
     * Set the dimensions of the svg document that will be drawn.
     * This is useful if the dimensions of the svg document are different
     * from the pdf document that is to be created.
     * The result is scaled so that the svg fits correctly inside the
     * pdf document.
     * @param w the width of the page
     * @param h the height of the page
     */
    public void setSVGDimension(float w, float h) {
        this.svgWidth = w;
        this.svgHeight = h;
    }

    /**
     * Set the background of the pdf document.
     * This is used to set the background for the pdf document
     * Rather than leaving it as the default white.
     * @param col the background colour to fill
     */
    public void setBackgroundColor(Color col) {
        StringBuffer sb = new StringBuffer();
        sb.append("q\n");
        this.colorHandler.establishColor(sb, col, true);

        sb.append("0 0 ").append(width).append(" ").append(height).append(" re\n");

        sb.append("f\n");
        sb.append("Q\n");
        currentStream.write(sb.toString());
    }

    /**
     * Is called to prepare the PDFDocumentGraphics2D for the next page to be painted. Basically,
     * this closes the current page. A new page is prepared as soon as painting starts.
     */
    public void nextPage() {
        closePage();
    }

    /**
     * Is called to prepare the PDFDocumentGraphics2D for the next page to be painted. Basically,
     * this closes the current page. A new page is prepared as soon as painting starts.
     * This method allows to start the new page (and following pages) with a different page size.
     * @param width the width of the new page (in points)
     * @param height the height of the new page (in points)
     */
    public void nextPage(int width, int height) {
        this.width = width;
        this.height = height;
        nextPage();
    }

    /**
     * Closes the current page and adds it to the PDF file.
     */
    protected void closePage() {
        if (!pdfContext.isPagePending()) {
            return; //ignore
        }
        currentStream.write("Q\n");
        //Finish page
        PDFStream pdfStream = this.pdfDoc.getFactory().makeStream(
                PDFFilterList.CONTENT_FILTER, false);
        pdfStream.add(getString());
        this.pdfDoc.registerObject(pdfStream);
        pdfContext.getCurrentPage().setContents(new PDFReference(pdfStream));
        PDFAnnotList annots = pdfContext.getCurrentPage().getAnnotations();
        if (annots != null) {
            this.pdfDoc.addObject(annots);
        }
        this.pdfDoc.addObject(pdfContext.getCurrentPage());
        pdfContext.clearCurrentPage();
    }

    /** {@inheritDoc} */
    @Override
    protected void preparePainting() {
        if (pdfContext.isPagePending()) {
            return;
        }
        //Setup default font info if no more font configuration has been done by the user.
        if (!this.textAsShapes && getFontInfo() == null) {
            setupDefaultFontInfo();
        }
        try {
            startPage();
        } catch (IOException ioe) {
            handleIOException(ioe);
        }
    }

    /**
     * Called to prepare a new page
     * @throws IOException if starting the new page fails due to I/O errors.
     */
    protected void startPage() throws IOException {
        if (pdfContext.isPagePending()) {
            throw new IllegalStateException("Close page first before starting another");
        }
        //Start page
        paintingState = new PDFPaintingState();
        if (this.initialTransform == null) {
            //Save initial transformation matrix
            this.initialTransform = getTransform();
            this.initialClip = getClip();
        } else {
            //Reset transformation matrix
            setTransform(this.initialTransform);
            setClip(this.initialClip);
        }

        currentFontName = "";
        currentFontSize = 0;

        if (currentStream == null) {
            currentStream = new StringWriter();
        }

        PDFResources pdfResources = this.pdfDoc.getResources();
        PDFPage page = this.pdfDoc.getFactory().makePage(pdfResources,
                width, height);
        resourceContext = page;
        pdfContext.setCurrentPage(page);
        pageRef = page.makeReference();

        currentStream.write("q\n");
        AffineTransform at = new AffineTransform(1.0, 0.0, 0.0, -1.0,
                                                 0.0, height);
        currentStream.write("1 0 0 -1 0 " + height + " cm\n");
        if (svgWidth != 0) {
            double scaleX = width / svgWidth;
            double scaleY = height / svgHeight;
            at.scale(scaleX, scaleY);
            currentStream.write("" + PDFNumber.doubleOut(scaleX) + " 0 0 "
                                + PDFNumber.doubleOut(scaleY) + " 0 0 cm\n");
        }
        if (deviceDPI != NORMAL_PDF_RESOLUTION) {
            double s = NORMAL_PDF_RESOLUTION / deviceDPI;
            at.scale(s, s);
            currentStream.write("" + PDFNumber.doubleOut(s) + " 0 0 "
                                + PDFNumber.doubleOut(s) + " 0 0 cm\n");

            scale(1 / s, 1 / s);
        }
        // Remember the transform we installed.
        paintingState.concatenate(at);

        pdfContext.increasePageCount();
    }


    /**
     * The rendering process has finished.
     * This should be called after the rendering has completed as there is
     * no other indication it is complete.
     * This will then write the results to the output stream.
     * @throws IOException an io exception if there is a problem
     *         writing to the output stream
     */
    public void finish() throws IOException {
        // restorePDFState();

        closePage();
        if (fontInfo != null) {
            pdfDoc.getResources().addFonts(pdfDoc, fontInfo);
        }
        this.pdfDoc.output(outputStream);
        pdfDoc.outputTrailer(outputStream);

        outputStream.flush();
    }

    /**
     * This constructor supports the create method
     * @param g the pdf document graphics to make a copy of
     */
    public PDFDocumentGraphics2D(PDFDocumentGraphics2D g) {
        super(g);
        this.pdfContext = g.pdfContext;
        this.width = g.width;
        this.height = g.height;
        this.svgWidth = g.svgWidth;
        this.svgHeight = g.svgHeight;
    }

    /**
     * Creates a new <code>Graphics</code> object that is
     * a copy of this <code>Graphics</code> object.
     * @return     a new graphics context that is a copy of
     * this graphics context.
     */
    @Override
    public Graphics create() {
        preparePainting();
        return new PDFDocumentGraphics2D(this);
    }

    /**
     * Draw a string to the pdf document.
     * This either draws the string directly or if drawing text as
     * shapes it converts the string into shapes and draws that.
     * @param s the string to draw
     * @param x the x position
     * @param y the y position
     */
    @Override
    public void drawString(String s, float x, float y) {
        if (super.textAsShapes) {
            Font font = super.getFont();
            FontRenderContext frc = super.getFontRenderContext();
            GlyphVector gv = font.createGlyphVector(frc, s);
            Shape glyphOutline = gv.getOutline(x, y);
            super.fill(glyphOutline);
        } else {
            super.drawString(s, x, y);
        }
    }

}

