// Copyright 2001 freehep
package org.freehep.graphicsio.pdf;

import java.awt.GradientPaint;
import java.awt.Paint;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

/**
 * Delay <tt>Paint</tt> objects (gradient/texture, not color) for writing
 * pattern/shading/function dictionaries to the pdf file when the pageStream is
 * complete.<br>
 * TODO: - reuse pattern dictionaries if possible - cyclic function not working
 * yet (ps calculation)
 * 
 * @author Simon Fischer
 * @version $Id: freehep-graphicsio-pdf/src/main/java/org/freehep/graphicsio/pdf/PDFPaintDelayQueue.java 2fa79ac3a135 2007/01/09 18:18:57 duns $
 */
public class PDFPaintDelayQueue {

    private static int currentNumber = 0;

    private class Entry {
        private Paint paint;

        private String name;

        private AffineTransform trafo;

        private String writeAs;

        private boolean written;

        private Entry(Paint paint, AffineTransform trafo, String writeAs) {
            this.paint = paint;
            this.trafo = trafo;
            this.writeAs = writeAs;
            this.name = "Paint" + (currentNumber++);
            this.written = false;
        }
    }

    private List<Entry> paintList;

    private PDFWriter pdf;

//    private PDFImageDelayQueue imageDelayQueue;

    private AffineTransform pageMatrix;

    /** Don't forget to call <tt>setPageMatrix()</tt>. */
    public PDFPaintDelayQueue(PDFWriter pdf, PDFImageDelayQueue imageDelayQueue) {
        this.pdf = pdf;
        this.paintList = new LinkedList<Entry>();
//        this.imageDelayQueue = imageDelayQueue;
        this.pageMatrix = new AffineTransform();
    }

    /**
     * Call this method in order to inform this class about the transformation
     * that is necessary to map the pattern's coordinate space to the default
     * coordinate system of the parent's content stream (in our case the
     * flipping of the page).
     */
    public void setPageMatrix(AffineTransform t) {
        pageMatrix = new AffineTransform(t);
    }

    public PDFName delayPaint(Paint paint, AffineTransform transform,
            String writeAs) {
        Entry e = new Entry(paint, transform, writeAs);
        paintList.add(e);
        return pdf.name(e.name);
    }

    /** Creates a stream for every delayed image. */
    public void processAll() throws IOException {
        ListIterator<Entry> i = paintList.listIterator();
        while (i.hasNext()) {
            Entry e = i.next();

            if (!e.written) {
                e.written = true;
                if (e.paint instanceof GradientPaint) {
                    addGradientPaint(e);
                } else if (e.paint instanceof TexturePaint) {
                    addTexturePaint(e);
                } else {
                    System.err.println("PDFWriter: Paint of class '"
                            + e.paint.getClass() + "' not supported.");
                    // FIXME, we could write a color here, to keep the file
                    // valid.
                }
            }
        }
    }

    /**
     * Adds all names to the dictionary which should be the value of the
     * resources dicionrary's /Pattern entry.
     */
    public int addPatterns() throws IOException {
        if (paintList.size() > 0) {
            PDFDictionary patterns = pdf.openDictionary("Pattern");
            ListIterator<Entry> i = paintList.listIterator();
            while (i.hasNext()) {
                Entry e = i.next();
                patterns.entry(e.name, pdf.ref(e.name));
            }
            pdf.close(patterns);
        }
        return paintList.size();
    }

    private void addGradientPaint(Entry e) throws IOException {
        GradientPaint gp = (GradientPaint) e.paint;

        // open the pattern dictionary
        PDFDictionary pattern = pdf.openDictionary(e.name);
        pattern.entry("Type", pdf.name("Pattern"));
        pattern.entry("PatternType", 2);
        setMatrix(pattern, e, 0, 0);

        // the shading subdictionary
        PDFDictionary shading = pattern.openDictionary("Shading");
        shading.entry("ShadingType", 2);
        shading.entry("ColorSpace", pdf.name("DeviceRGB"));
        Point2D p1 = gp.getPoint1();
        Point2D p2 = gp.getPoint2();
        shading.entry("Coords", new double[] { p1.getX(), p1.getY(), p2.getX(),
                p2.getY() });
        double[] domain = new double[] { 0, 1 };
        shading.entry("Domain", domain);

        // the function subdictionary (necessarily by reference, because
        // the type 4 function is a stream)
        String functionRef = e.name + "Function";
        shading.entry("Function", pdf.ref(functionRef));
        shading.entry("Extend", new boolean[] { true, true });

        pattern.close(shading);

        pdf.close(pattern);

        // get color components and generate the functions
        float[] col0 = new float[3];
        gp.getColor1().getRGBColorComponents(col0);
        double c0[] = new double[] { col0[0], col0[1], col0[2] };
        float[] col1 = new float[3];
        gp.getColor2().getRGBColorComponents(col1);
        double c1[] = new double[] { col1[0], col1[1], col1[2] };
        if (gp.isCyclic()) {
            // FIXME: cyclic function only eveluated between 0 and 1 for short
            // range, not repetitive.
            // addCyclicFunction(functionRef, c0, c1, domain);
            addLinearFunction(functionRef, c0, c1, domain);
        } else {
            addLinearFunction(functionRef, c0, c1, domain);
        }
    }

    /** Writes a type 2 (exponential) function (exponent N=1) to the pdf file. */
    private void addLinearFunction(String functionRef, double[] c0,
            double[] c1, double[] dom) throws IOException {
        PDFDictionary function = pdf.openDictionary(functionRef);
        function.entry("FunctionType", 2);
        function.entry("Domain", dom);
        function.entry("Range", new double[] { 0., 1., 0., 1., 0., 1. });
        function.entry("C0", c0);
        function.entry("C1", c1);
        function.entry("N", 1);
        pdf.close(function);
    }

    /**
     * Writes a type 4 (PostScript calculator) function that describes a
     * triangle function to the pdf file.
     */
    // NOT USED
    protected void addCyclicFunction(String functionRef, double[] c0,
            double[] c1, double[] dom) throws IOException {
        // PDFStream function = pdf.openStream(functionRef, new String[] {
        // "Flate", "ASCII85" });
        PDFStream function = pdf.openStream(functionRef);
        function.entry("FunctionType", 4);
        // function.entry("Domain", new double[] {-1000.0, 1000.0} );
        function.entry("Domain", dom);
        function.entry("Range", new double[] { 0., 1., 0., 1., 0., 1. });
        function.println("{");
        /*
         * function.println("2 mod"); function.println("1 sub");
         * function.println("abs"); function.println("1 exch sub");
         */
        // function.println("sin");
        for (int i = 0; i < 3; i++) {
            if (i < 2)
                function.println("dup");
            function.println((c1[i] - c0[i]) + " mul");
            function.println(c0[i] + " add");
            if (i < 2)
                function.println("exch");
        }

        // function.println("pop 0.8 0.0 0.0");

        function.println("}");
        pdf.close(function);
    }

    private void addTexturePaint(Entry e) throws IOException {
        TexturePaint tp = (TexturePaint) e.paint;

        // open the pattern stream that also holds the inlined picture
        PDFStream pattern = pdf.openStream(e.name, null); // image is encoded.
        pattern.entry("Type", pdf.name("Pattern"));
        pattern.entry("PatternType", 1);
        pattern.entry("PaintType", 1);
        BufferedImage image = tp.getImage();
        pattern.entry("TilingType", 1);
        double width = tp.getAnchorRect().getWidth();
        double height = tp.getAnchorRect().getHeight();
        double offsX = tp.getAnchorRect().getX();
        double offsY = tp.getAnchorRect().getY();
        pattern.entry("BBox", new double[] { 0, 0, width, height });
        pattern.entry("XStep", width);
        pattern.entry("YStep", height);
        PDFDictionary resources = pattern.openDictionary("Resources");
        resources.entry("ProcSet", new Object[] { pdf.name("PDF"),
                pdf.name("ImageC") });
        pattern.close(resources);
        setMatrix(pattern, e, offsX, offsY);

        // scale the tiling image to the correct size
        pattern.matrix(width, 0, 0, -height, 0, height);

        pattern.inlineImage(image, null, e.writeAs);
        pdf.close(pattern);
    }

    /**
     * Sets both the page (flip) matrix and the actual transformation matrix
     * plus a translation offset (which may of course be be 0).
     */
    private void setMatrix(PDFDictionary dict, Entry e, double translX,
            double translY) throws IOException {
        // concatenate the page matrix and the actual transformation matrix
        AffineTransform trafo = new AffineTransform(pageMatrix);
        trafo.concatenate(e.trafo);
        trafo.translate(translX, translY);
        double[] matrix = new double[6];
        trafo.getMatrix(matrix);
        dict.entry("Matrix", new double[] { matrix[0], matrix[1], matrix[2],
                matrix[3], matrix[4], matrix[5] });
    }
}
