/* =====================================================================
 * JFreePDF : a fast, light-weight PDF library for the Java(tm) platform
 * =====================================================================
 *
 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
 *
 * https://github.com/jfree/orsonpdf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * runtime license is available to JFree sponsors:
 *
 * https://github.com/sponsors/jfree
 *
 */

package org.jfree.pdf;

import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Image;
import java.awt.MultipleGradientPaint;
import java.awt.RadialGradientPaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jfree.pdf.internal.Pattern.ShadingPattern;
import org.jfree.pdf.dictionary.Dictionary;
import org.jfree.pdf.dictionary.GraphicsStateDictionary;
import org.jfree.pdf.filter.FlateFilter;
import org.jfree.pdf.function.ExponentialInterpolationFunction;
import org.jfree.pdf.function.Function;
import org.jfree.pdf.function.StitchingFunction;
import org.jfree.pdf.internal.PDFFont;
import org.jfree.pdf.internal.Pages;
import org.jfree.pdf.internal.PDFObject;
import org.jfree.pdf.internal.Pattern;
import org.jfree.pdf.shading.AxialShading;
import org.jfree.pdf.shading.RadialShading;
import org.jfree.pdf.shading.Shading;
import org.jfree.pdf.stream.GraphicsStream;
import org.jfree.pdf.stream.PDFImage;
import org.jfree.pdf.stream.PDFSoftMaskImage;
import org.jfree.pdf.util.Args;
import org.jfree.pdf.util.GradientPaintKey;
import org.jfree.pdf.util.RadialGradientPaintKey;

/**
 * Represents a page in a {@link PDFDocument}.  Our objective is to be able
 * to write to the page using the {@link PDFGraphics2D} class (see the
 * {@link #getGraphics2D()} method).
 */
public class Page extends PDFObject {
    
    /** The pages of the document. */
    private Pages parent;
 
    /** The page bounds. */
    private Rectangle2D bounds;
    
    /** The page contents. */
    private GraphicsStream contents;
    
    /** The Graphics2D for writing to the page contents. */
    private PDFGraphics2D graphics2d;
    
    /**
     * The list of font (names) used on the page.  We let the parent take
     * care of tracking the font objects.
     */
    private List<String> fontsOnPage;
    
    /**
     * A map between gradient paints and the names used to define the
     * associated pattern in the page resources.
     */
    private Map<GradientPaintKey, String> gradientPaintsOnPage;
    
    private Map<RadialGradientPaintKey, String> radialGradientPaintsOnPage;
    
    /** The pattern dictionary for this page. */
    private Dictionary patterns;
    
    /** The ExtGState dictionary for the page. */
    private Dictionary graphicsStates;
    
    /** 
     * The transform between Page and Java2D coordinates, used in Shading 
     * patterns. 
     */
    private AffineTransform j2DTransform;

    private Dictionary xObjects = new Dictionary();

    /**
     * Creates a new page.
     * 
     * @param number  the PDF object number.
     * @param generation  the PDF object generation number.
     * @param parent  the parent (manages the pages in the {@code PDFDocument}).
     * @param bounds  the page bounds ({@code null} not permitted).
     */
    Page(int number, int generation, Pages parent, Rectangle2D bounds) {
        this(number, generation, parent, bounds, true);
    }
    
    /**
     * Creates a new page.
     * 
     * @param number  the PDF object number.
     * @param generation  the PDF object generation number.
     * @param parent  the parent (manages the pages in the {@code PDFDocument}).
     * @param bounds  the page bounds ({@code null} not permitted).
     * @param filter  a flag that controls whether or not the graphics stream
     *     for the page has a FlateFilter applied.
     * 
     * @since 1.4
     */
    Page(int number, int generation, Pages parent, Rectangle2D bounds, 
            boolean filter) {

        super(number, generation);
        Args.nullNotPermitted(bounds, "bounds");
        this.parent = parent;
        this.bounds = (Rectangle2D) bounds.clone();
        this.fontsOnPage = new ArrayList<>();
        int n = this.parent.getDocument().getNextNumber();
        this.contents = new GraphicsStream(n, this);
        if (filter) {
            this.contents.addFilter(new FlateFilter());
        }
        this.gradientPaintsOnPage = new HashMap<>();
        this.radialGradientPaintsOnPage = new HashMap<>();
        this.patterns = new Dictionary();
        this.graphicsStates = new Dictionary();
        
        this.j2DTransform = AffineTransform.getTranslateInstance(0.0, 
                bounds.getHeight());
        this.j2DTransform.concatenate(AffineTransform.getScaleInstance(1.0, 
                -1.0));        
    }

    /**
     * Returns a new rectangle containing the bounds for this page (as supplied
     * to the constructor).
     * 
     * @return The page bounds. 
     */
    public Rectangle2D getBounds() {
        return (Rectangle2D) this.bounds.clone();
    }
    
    /**
     * Returns the {@code PDFObject} that represents the page content.
     * 
     * @return The {@code PDFObject} that represents the page content.
     */
    public PDFObject getContents() {
        return this.contents;
    }
    
    /**
     * Returns the {@link PDFGraphics2D} instance for drawing to the page.
     * 
     * @return The {@code PDFGraphics2D} instance for drawing to the page.
     */
    public PDFGraphics2D getGraphics2D() {
        if (this.graphics2d == null) {
            this.graphics2d = new PDFGraphics2D(this.contents, 
                    (int) this.bounds.getWidth(), 
                    (int) this.bounds.getHeight());
        }
        return this.graphics2d;
    }

    /**
     * Finds the font reference corresponding to the given Java2D font, 
     * creating a new one if there isn't one already.
     * 
     * @param font  the AWT font.
     * 
     * @return The font reference.
     */
    public String findOrCreateFontReference(Font font) {
        String ref = this.parent.findOrCreateFontReference(font);
        if (!this.fontsOnPage.contains(ref)) {
            this.fontsOnPage.add(ref);
        }
        return ref;
    }
    
    private Dictionary createFontDictionary() {
        Dictionary d = new Dictionary();
        for (String name : this.fontsOnPage) {
            PDFFont f = this.parent.getFont(name);
            d.put(name, f.getReference());
        }
        return d;
    }
    
    /**
     * Returns the name of the pattern for the specified {@code GradientPaint}, 
     * reusing an existing pattern if possible, otherwise creating a new 
     * pattern if necessary.
     * 
     * @param gp  the gradient ({@code null} not permitted).
     * 
     * @return The pattern name. 
     */
    public String findOrCreatePattern(GradientPaint gp) {
        GradientPaintKey key = new GradientPaintKey(gp);
        String patternName = this.gradientPaintsOnPage.get(key);
        if (patternName == null) {
            PDFDocument doc = this.parent.getDocument();
            Function f = new ExponentialInterpolationFunction(
                    doc.getNextNumber(), 
                    gp.getColor1().getRGBColorComponents(null), 
                    gp.getColor2().getRGBColorComponents(null));
            doc.addObject(f);
            double[] coords = new double[4];
            coords[0] = gp.getPoint1().getX();
            coords[1] = gp.getPoint1().getY();
            coords[2] = gp.getPoint2().getX();
            coords[3] = gp.getPoint2().getY();
            Shading s = new AxialShading(doc.getNextNumber(), coords, f);
            doc.addObject(s);
            Pattern p = new ShadingPattern(doc.getNextNumber(), s, 
                    this.j2DTransform);
            doc.addObject(p);
            patternName = "/P" + (this.patterns.size() + 1);
            this.patterns.put(patternName, p);
            this.gradientPaintsOnPage.put(key, patternName);
        }
        return patternName; 
    }
    
    /**
     * Returns the name of the pattern for the specified 
     * {@code RadialGradientPaint}, reusing an existing pattern if 
     * possible, otherwise creating a new pattern if necessary.
     * 
     * @param gp  the gradient ({@code null} not permitted).
     * 
     * @return The pattern name. 
     */
    public String findOrCreatePattern(RadialGradientPaint gp) {
        RadialGradientPaintKey key = new RadialGradientPaintKey(gp);
        String patternName = this.radialGradientPaintsOnPage.get(key);
        if (patternName == null) {
            PDFDocument doc = this.parent.getDocument();
            Function f = createFunctionForMultipleGradient(gp);
            doc.addObject(f);
            double[] coords = new double[6];
            coords[0] = gp.getFocusPoint().getX();
            coords[1] = gp.getFocusPoint().getY();
            coords[2] = 0.0;
            coords[3] = gp.getCenterPoint().getX();
            coords[4] = gp.getCenterPoint().getY();
            coords[5] = gp.getRadius();
            Shading s = new RadialShading(doc.getNextNumber(), coords, f);
            doc.addObject(s);
            Pattern p = new ShadingPattern(doc.getNextNumber(), s, 
                    this.j2DTransform);
            doc.addObject(p);
            patternName = "/P" + (this.patterns.size() + 1);
            this.patterns.put(patternName, p);
            this.radialGradientPaintsOnPage.put(key, patternName);
        }
        return patternName; 
    }
    
    private Function createFunctionForMultipleGradient(
            MultipleGradientPaint mgp) {
        PDFDocument doc = this.parent.getDocument();

        if (mgp.getColors().length == 2) {
            Function f = new ExponentialInterpolationFunction(
                    doc.getNextNumber(),
                    mgp.getColors()[0].getRGBColorComponents(null), 
                    mgp.getColors()[1].getRGBColorComponents(null));
            return f;
        } else {
            int count = mgp.getColors().length - 1;
            Function[] functions = new Function[count];
            float[] fbounds = new float[count - 1];
            float[] encode = new float[count * 2];
            for (int i = 0; i < count; i++) {
                // create a linear function for each pair of colors
                functions[i] = new ExponentialInterpolationFunction(
                    doc.getNextNumber(),
                    mgp.getColors()[i].getRGBColorComponents(null), 
                    mgp.getColors()[i + 1].getRGBColorComponents(null));
                doc.addObject(functions[i]);
                if (i < count - 1) {
                    fbounds[i] = mgp.getFractions()[i + 1];
                }
                encode[i * 2] = 0;
                encode[i * 2 + 1] = 1;
            }
            return new StitchingFunction(doc.getNextNumber(), functions, 
                    fbounds, encode);
        }
    }
    
    private Map<Integer, String> alphaDictionaries = new HashMap<>();
    
    /**
     * Returns the name of the Graphics State Dictionary that can be used
     * for the specified alpha value - if there is no existing dictionary
     * then a new one is created.
     * 
     * @param alpha  the alpha value in the range 0 to 255.
     * 
     * @return The graphics state dictionary reference. 
     */
    public String findOrCreateGSDictionary(int alpha) {
        Integer key = alpha;
        float alphaValue = alpha / 255f;
        String name = this.alphaDictionaries.get(key);
        if (name == null) {
            PDFDocument pdfDoc = this.parent.getDocument();
            GraphicsStateDictionary gsd = new GraphicsStateDictionary(
                    pdfDoc.getNextNumber());
            gsd.setNonStrokeAlpha(alphaValue);
            gsd.setStrokeAlpha(alphaValue);
            pdfDoc.addObject(gsd);
            name = "/GS" + (this.graphicsStates.size() + 1);
            this.graphicsStates.put(name, gsd);
            this.alphaDictionaries.put(key, name);
        }
        return name;
    }

    /**
     * Adds a soft mask image to the page.  This is called from the 
     * {@link #addImage(java.awt.Image, boolean)} method to support image transparency.
     * 
     * @param img  the image ({@code null} not permitted).
     * 
     * @return The soft mask image reference.
     */
    String addSoftMaskImage(Image img) {
        Args.nullNotPermitted(img, "img");
        PDFDocument pdfDoc = this.parent.getDocument();
        PDFSoftMaskImage softMaskImage = new PDFSoftMaskImage(
                pdfDoc.getNextNumber(), img);
        softMaskImage.addFilter(new FlateFilter());
        pdfDoc.addObject(softMaskImage);
        String reference = "/Image" + this.xObjects.size();
        this.xObjects.put(reference, softMaskImage);
        return softMaskImage.getReference();
    }
    
    /**
     * Adds an image to the page.This creates the required PDF object, 
     * as well as adding a reference in the {@code xObjects} resources.  
     * You should not call this method directly, it exists for the use of the
     * {@link PDFGraphics2D#drawImage(java.awt.Image, int, int, int, int, java.awt.image.ImageObserver)} 
     * method.
     * 
     * @param img  the image ({@code null} not permitted).
     * @param addSoftMaskImage  add as a soft mask image?
     * 
     * @return The image reference name.
     */
    public String addImage(Image img, boolean addSoftMaskImage) {
        Args.nullNotPermitted(img, "img");
        PDFDocument pdfDoc = this.parent.getDocument();
        String softMaskImageRef = null;
        if (addSoftMaskImage) {
            softMaskImageRef = addSoftMaskImage(img);
        }
        PDFImage image = new PDFImage(pdfDoc.getNextNumber(), img, 
                softMaskImageRef);
        image.addFilter(new FlateFilter());
        pdfDoc.addObject(image);
        String reference = "/Image" + this.xObjects.size();
        this.xObjects.put(reference, image);
        return reference;
    }
    
    @Override
    public byte[] getObjectBytes() {
        return createDictionary().toPDFBytes();
    }

    private Dictionary createDictionary() {
        Dictionary dictionary = new Dictionary("/Page");
        dictionary.put("/Parent", this.parent);
        dictionary.put("/MediaBox", this.bounds);
        dictionary.put("/Contents", this.contents);
        Dictionary resources = new Dictionary();
        resources.put("/ProcSet", "[/PDF /Text /ImageB /ImageC /ImageI]");
        if (!this.xObjects.isEmpty()) {
            resources.put("/XObject", this.xObjects);
        }
        if (!this.fontsOnPage.isEmpty()) {
            resources.put("/Font", createFontDictionary());
        }
        if (!this.patterns.isEmpty()) {
            resources.put("/Pattern", this.patterns);
        }
        if (!this.graphicsStates.isEmpty()) {
            resources.put("/ExtGState", this.graphicsStates);
        }        
        dictionary.put("/Resources", resources);
        return dictionary;
    }

}

