// Copyright 2000-2007, FreeHEP
package org.freehep.graphicsio.pdf;

import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.OutputStream;

import org.freehep.graphicsio.ImageConstants;
import org.freehep.util.io.ASCII85OutputStream;
import org.freehep.util.io.ASCIIHexOutputStream;
import org.freehep.util.io.CountedByteOutputStream;
import org.freehep.util.io.FinishableOutputStream;
import org.freehep.util.io.FlateOutputStream;

/**
 * This class allows you to write/print into a PDFStream. Several methods are
 * available to specify the content of a page, image. This class performs some
 * error checking, while writing the stream.
 * <p>
 * The stream allows to write dictionary entries. The /Length entry is written
 * automatically, referencing an object which will also be written just after
 * the stream is closed and the length is calculated.
 * <p>
 * 
 * @author Mark Donszelmann
 * @version $Id: freehep-graphicsio-pdf/src/main/java/org/freehep/graphicsio/pdf/PDFStream.java 344e3292515f 2007/06/08 22:23:13 duns $
 */
public class PDFStream extends PDFDictionary implements PDFConstants {

    private String name;

    private PDFObject object;

    private boolean dictionaryOpen;

    private OutputStream[] stream;

    private CountedByteOutputStream byteCountStream;

    private String[] encode;

    PDFStream(PDF pdf, PDFByteWriter writer, String name, PDFObject parent,
            String[] encode) throws IOException {
        super(pdf, writer);
        this.name = name;
        object = parent;
        if (object == null)
            System.err
                    .println("PDFWriter: 'PDFStream' cannot have a null parent");
        // first write the dictionary
        dictionaryOpen = true;
        this.encode = encode;
    }

    /**
     * Starts the stream, writes out the filters using the preset encoding, and
     * encodes the stream.
     */
    private void startStream() throws IOException {
        startStream(encode);
    }

    /**
     * Starts the stream, writes out the filters using the given encoding, and
     * encodes the stream.
     */
    private void startStream(String[] encode) throws IOException {
        if (dictionaryOpen) {
            PDFName[] filters = decodeFilters(encode);
            if (filters != null)
                entry("Filter", filters);

            super.close();
            dictionaryOpen = false;
            out.printPlain("stream\n");

            byteCountStream = new CountedByteOutputStream(out);
            stream = openFilters(byteCountStream, encode);
        }
    }

    private void write(int b) throws IOException {
        startStream();
        stream[0].write(b);
    }

    private void write(byte[] b) throws IOException {
        for (int i = 0; i < b.length; i++) {
            write((int) b[i]);
        }
    }

    private static PDFName[] decodeFilters(String[] encode) {
        PDFName[] filters = null;
        if ((encode != null) && (encode.length != 0)) {
            filters = new PDFName[encode.length];
            for (int i = 0; i < filters.length; i++) {
                filters[i] = new PDFName(encode[encode.length - i - 1]
                        + "Decode");
            }
        }
        return filters;
    }

    // open new stream using Standard Filters (see table 3.5)
    // stream[0] is the one to write to, the last one is s
    private static OutputStream[] openFilters(OutputStream s, String[] filters) {
        OutputStream[] os;
        if ((filters != null) && (filters.length != 0)) {
            os = new OutputStream[filters.length + 1];
            os[os.length - 1] = s;
            for (int i = os.length - 2; i >= 0; i--) {
                if (filters[i].equals("ASCIIHex")) {
                    os[i] = new ASCIIHexOutputStream(os[i + 1]);
                } else if (filters[i].equals("ASCII85")) {
                    os[i] = new ASCII85OutputStream(os[i + 1]);
                } else if (filters[i].equals("Flate")) {
                    os[i] = new FlateOutputStream(os[i + 1]);
                } else if (filters[i].equals("DCT")) {
                    os[i] = os[i + 1];
                } else {
                    System.err.println("PDFWriter: unknown stream format: "
                            + filters[i]);
                }
            }
        } else {
            os = new OutputStream[1];
            os[0] = s;
        }
        return os;
    }

    // stream[0] is the first one to finish, the last one is not finished
    private static void closeFilters(OutputStream[] s) throws IOException {
        for (int i = 0; i < s.length - 1; i++) {
            s[i].flush();
            if (s[i] instanceof FinishableOutputStream) {
                ((FinishableOutputStream) s[i]).finish();
            }
        }
        s[s.length - 1].flush();
    }

    private void write(String s) throws IOException {
        byte[] b = s.getBytes("ISO-8859-1");
        for (int i = 0; i < b.length; i++) {
            write(b[i]);
        }
    }

    void close() throws IOException {
        closeFilters(stream);
        stream = null;
        out.printPlain("\nendstream");
        out.println();
        object.close();
        
        if (gStates > 0) {
            System.err.println("PDFStream: unbalanced saves()/restores(), too many saves: "+gStates);
        }
    }

    String getName() {
        return name;
    }

    public int getLength() {
        return byteCountStream.getCount();
    }

    public void print(String s) throws IOException {
        write(s);
    }

    public void println(String s) throws IOException {
        write(s);
        write(EOL);
    }

    public void comment(String comment) throws IOException {
        println("% " + comment);
    }

    // ==========================================================================
    // PDFStream Operators according to Table 4.1
    // ==========================================================================

    //
    // Graphics State operators (see Table 4.7)
    //
    private int gStates = 0;

    public void save() throws IOException {
        println("q");
        gStates++;
    }

    public void restore() throws IOException {
        if (gStates <= 0) {
            System.err.println("PDFStream: unbalanced saves()/restores(), too many restores");
        }
        gStates--;
        println("Q");
    }

    public void matrix(AffineTransform xform) throws IOException {
        matrix(xform.getScaleX(), xform.getShearY(), xform.getShearX(), xform
                .getScaleY(), xform.getTranslateX(), xform.getTranslateY());
    }

    public void matrix(double m00, double m10, double m01, double m11,
            double m02, double m12) throws IOException {
        println(PDFUtil.fixedPrecision(m00) + " " + PDFUtil.fixedPrecision(m10)
                + " " + PDFUtil.fixedPrecision(m01) + " "
                + PDFUtil.fixedPrecision(m11) + " "
                + PDFUtil.fixedPrecision(m02) + " "
                + PDFUtil.fixedPrecision(m12) + " cm");
    }

    public void width(double width) throws IOException {
        println(PDFUtil.fixedPrecision(width) + " w");
    }

    public void cap(int capStyle) throws IOException {
        println(capStyle + " J");
    }

    public void join(int joinStyle) throws IOException {
        println(joinStyle + " j");
    }

    public void mitterLimit(double limit) throws IOException {
        println(PDFUtil.fixedPrecision(limit) + " M");
    }

    public void dash(int[] dash, double phase) throws IOException {
        print("[");
        for (int i = 0; i < dash.length; i++) {
            print(" " + PDFUtil.fixedPrecision(dash[i]));
        }
        println("] " + PDFUtil.fixedPrecision(phase) + " d");
    }

    public void dash(float[] dash, double phase) throws IOException {
        print("[");
        for (int i = 0; i < dash.length; i++) {
            print(" " + PDFUtil.fixedPrecision(dash[i]));
        }
        println("] " + PDFUtil.fixedPrecision(phase) + " d");
    }

    public void flatness(double flatness) throws IOException {
        println(PDFUtil.fixedPrecision(flatness) + " i");
    }

    public void state(PDFName stateDictionary) throws IOException {
        println(stateDictionary + " gs");
    }

    //
    // Path Construction operators (see Table 4.9)
    //
    public void cubic(double x1, double y1, double x2, double y2, double x3,
            double y3) throws IOException {
        println(PDFUtil.fixedPrecision(x1) + " " + PDFUtil.fixedPrecision(y1)
                + " " + PDFUtil.fixedPrecision(x2) + " "
                + PDFUtil.fixedPrecision(y2) + " " + PDFUtil.fixedPrecision(x3)
                + " " + PDFUtil.fixedPrecision(y3) + " c");
    }

    public void cubicV(double x2, double y2, double x3, double y3)
            throws IOException {
        println(PDFUtil.fixedPrecision(x2) + " " + PDFUtil.fixedPrecision(y2)
                + " " + PDFUtil.fixedPrecision(x3) + " "
                + PDFUtil.fixedPrecision(y3) + " v");
    }

    public void cubicY(double x1, double y1, double x3, double y3)
            throws IOException {
        println(PDFUtil.fixedPrecision(x1) + " " + PDFUtil.fixedPrecision(y1)
                + " " + PDFUtil.fixedPrecision(x3) + " "
                + PDFUtil.fixedPrecision(y3) + " y");
    }

    public void move(double x, double y) throws IOException {
        println(PDFUtil.fixedPrecision(x) + " " + PDFUtil.fixedPrecision(y)
                + " m");
    }

    public void line(double x, double y) throws IOException {
        println(PDFUtil.fixedPrecision(x) + " " + PDFUtil.fixedPrecision(y)
                + " l");
    }

    public void closePath() throws IOException {
        println("h");
    }

    public void rectangle(double x, double y, double width, double height)
            throws IOException {
        println(PDFUtil.fixedPrecision(x) + " " + PDFUtil.fixedPrecision(y)
                + " " + PDFUtil.fixedPrecision(width) + " "
                + PDFUtil.fixedPrecision(height) + " re");
    }

    //
    // Path Painting operators (see Table 4.10)
    //
    public void stroke() throws IOException {
        println("S");
    }

    public void closeAndStroke() throws IOException {
        println("s");
    }

    public void fill() throws IOException {
        println("f");
    }

    public void fillEvenOdd() throws IOException {
        println("f*");
    }

    public void fillAndStroke() throws IOException {
        println("B");
    }

    public void fillEvenOddAndStroke() throws IOException {
        println("B*");
    }

    public void closeFillAndStroke() throws IOException {
        println("b");
    }

    public void closeFillEvenOddAndStroke() throws IOException {
        println("b*");
    }

    public void endPath() throws IOException {
        println("n");
    }

    //
    // Clipping Path operators (see Table 4.11)
    //
    public void clip() throws IOException {
        println("W");
    }

    public void clipEvenOdd() throws IOException {
        println("W*");
    }

    //
    // Text Object operators (see Table 5.4)
    //
    private boolean textOpen = false;

    public void beginText() throws IOException {
        if (textOpen)
            System.err.println("PDFStream: nested beginText() not allowed.");
        println("BT");
        textOpen = true;
    }

    public void endText() throws IOException {
        if (!textOpen)
            System.err
                    .println("PDFStream: unbalanced use of beginText()/endText().");
        println("ET");
        textOpen = false;
    }

    //
    // Text State operators (see Table 5.2)
    //
    public void charSpace(double charSpace) throws IOException {
        println(PDFUtil.fixedPrecision(charSpace) + " Tc");
    }

    public void wordSpace(double wordSpace) throws IOException {
        println(PDFUtil.fixedPrecision(wordSpace) + " Tw");
    }

    public void scale(double scale) throws IOException {
        println(PDFUtil.fixedPrecision(scale) + " Tz");
    }

    public void leading(double leading) throws IOException {
        println(PDFUtil.fixedPrecision(leading) + " TL");
    }

    private boolean fontWasSet = false;

    public void font(PDFName fontName, double size) throws IOException {
        println(fontName + " " + PDFUtil.fixedPrecision(size) + " Tf");
        fontWasSet = true;
    }

    public void rendering(int mode) throws IOException {
        println(mode + " Tr");
    }

    public void rise(double rise) throws IOException {
        println(PDFUtil.fixedPrecision(rise) + " Ts");
    }

    //
    // Text Positioning operators (see Table 5.5)
    //
    public void text(double x, double y) throws IOException {
        println(PDFUtil.fixedPrecision(x) + " " + PDFUtil.fixedPrecision(y)
                + " Td");
    }

    public void textLeading(double x, double y) throws IOException {
        println(PDFUtil.fixedPrecision(x) + " " + PDFUtil.fixedPrecision(y)
                + " TD");
    }

    public void textMatrix(double a, double b, double c, double d, double e,
            double f) throws IOException {
        println(PDFUtil.fixedPrecision(a) + " " + PDFUtil.fixedPrecision(b)
                + " " + PDFUtil.fixedPrecision(c) + " "
                + PDFUtil.fixedPrecision(d) + " " + PDFUtil.fixedPrecision(e)
                + " " + PDFUtil.fixedPrecision(f) + " Tm");
    }

    public void textLine() throws IOException {
        println("T*");
    }

    //
    // Text Showing operators (see Table 5.6)
    //
    public void show(String text) throws IOException {
        if (!fontWasSet)
            System.err
                    .println("PDFStream: cannot use Text Showing operator before font is set.");
        if (!textOpen)
            System.err
                    .println("PDFStream: Text Showing operator only allowed inside Text section.");
        println("(" + PDFUtil.escape(text) + ") Tj");
    }

    public void showLine(String text) throws IOException {
        if (!fontWasSet)
            System.err
                    .println("PDFStream: cannot use Text Showing operator before font is set.");
        if (!textOpen)
            System.err
                    .println("PDFStream: Text Showing operator only allowed inside Text section.");
        println("(" + PDFUtil.escape(text) + ") '");
    }

    public void showLine(double wordSpace, double charSpace, String text)
            throws IOException {
        if (!fontWasSet)
            System.err
                    .println("PDFStream: cannot use Text Showing operator before font is set.");
        if (!textOpen)
            System.err
                    .println("PDFStream: Text Showing operator only allowed inside Text section.");
        println(PDFUtil.fixedPrecision(wordSpace) + " "
                + PDFUtil.fixedPrecision(charSpace) + " ("
                + PDFUtil.escape(text) + ") \"");
    }

    public void show(Object[] array) throws IOException {
        print("[");
        for (int i = 0; i < array.length; i++) {
            Object object = array[i];
            if (object instanceof String) {
                print(" (" + PDFUtil.escape(object.toString()) + ")");
            } else if (object instanceof Integer) {
                print(" " + ((Integer) object).intValue());
            } else if (object instanceof Double) {
                print(" " + ((Double) object).doubleValue());
            } else {
                System.err
                        .println("PDFStream: input array of operator TJ may only contain objects of type 'String', 'Integer' or 'Double'");
            }
        }
        println("] TJ");
    }

    //
    // Type 3 Font operators (see Table 5.10)
    //
    public void glyph(double wx, double wy) throws IOException {
        println(PDFUtil.fixedPrecision(wx) + " " + PDFUtil.fixedPrecision(wy)
                + " d0");
    }

    public void glyph(double wx, double wy, double llx, double lly, double urx,
            double ury) throws IOException {
        println(PDFUtil.fixedPrecision(wx) + " " + PDFUtil.fixedPrecision(wy)
                + " " + PDFUtil.fixedPrecision(llx) + " "
                + PDFUtil.fixedPrecision(lly) + " "
                + PDFUtil.fixedPrecision(urx) + " "
                + PDFUtil.fixedPrecision(ury) + " d1");
    }

    //
    // Color operators (see Table 4.21)
    //
    public void colorSpace(PDFName colorSpace) throws IOException {
        println(colorSpace + " cs");
    }

    public void colorSpaceStroke(PDFName colorSpace) throws IOException {
        println(colorSpace + " CS");
    }

    public void colorSpace(double[] color) throws IOException {
        for (int i = 0; i < color.length; i++) {
            print(" " + color[i]);
        }
        println(" scn");
    }

    public void colorSpaceStroke(double[] color) throws IOException {
        for (int i = 0; i < color.length; i++) {
            print(" " + color[i]);
        }
        println(" SCN");
    }

    public void colorSpace(double[] color, PDFName name) throws IOException {
        if (color != null) {
            for (int i = 0; i < color.length; i++) {
                print(PDFUtil.fixedPrecision(color[i]) + " ");
            }
        }
        println(name + " scn");
    }

    public void colorSpaceStroke(double[] color, PDFName name)
            throws IOException {
        if (color != null) {
            for (int i = 0; i < color.length; i++) {
                print(PDFUtil.fixedPrecision(color[i]) + " ");
            }
        }
        println(name + " SCN");
    }

    public void colorSpace(double g) throws IOException {
        println(PDFUtil.fixedPrecision(g) + " g");
    }

    public void colorSpaceStroke(double g) throws IOException {
        println(PDFUtil.fixedPrecision(g) + " G");
    }

    public void colorSpace(double r, double g, double b) throws IOException {
        println(PDFUtil.fixedPrecision(r) + " " + PDFUtil.fixedPrecision(g)
                + " " + PDFUtil.fixedPrecision(b) + " rg");
    }

    public void colorSpaceStroke(double r, double g, double b)
            throws IOException {
        println(PDFUtil.fixedPrecision(r) + " " + PDFUtil.fixedPrecision(g)
                + " " + PDFUtil.fixedPrecision(b) + " RG");
    }

    public void colorSpace(double c, double m, double y, double k)
            throws IOException {
        println(PDFUtil.fixedPrecision(c) + " " + PDFUtil.fixedPrecision(m)
                + " " + PDFUtil.fixedPrecision(y) + " "
                + PDFUtil.fixedPrecision(k) + " k");
    }

    public void colorSpaceStroke(double c, double m, double y, double k)
            throws IOException {
        println(PDFUtil.fixedPrecision(c) + " " + PDFUtil.fixedPrecision(m)
                + " " + PDFUtil.fixedPrecision(y) + " "
                + PDFUtil.fixedPrecision(k) + " K");
    }

    //
    // Shading Pattern operator (see Table 4.24)
    //
    public void shade(PDFName name) throws IOException {
        println(name + " sh");
    }

    /**
     * returns the decode-format for an image -format
     *
     * @param encode {@link ImageConstants#ZLIB} or {@link ImageConstants#JPG}
     * @return {@link #decodeFilters(String[])}
     */
    private PDFName[] getFilterName(String encode) {
        if (ImageConstants.ZLIB.equals(encode)) {
            return decodeFilters(new String[] {
                ImageConstants.ENCODING_FLATE,
                ImageConstants.ENCODING_ASCII85});
        }

        if (ImageConstants.JPG.equals(encode)) {
            return decodeFilters(new String[] {
                ImageConstants.ENCODING_DCT,
                ImageConstants.ENCODING_ASCII85});
        }

        throw new IllegalArgumentException("unknown image encoding " + encode  + " for PDFStream");
    }

    /**
     * Image convenience function (see Table 4.35). Ouputs the data of the image
     * using "DeviceRGB" colorspace, and the requested encodings
     *
     * @param image Image to write
     * @param bkg Background color, null for transparent image
     * @param encode {@link org.freehep.graphicsio.ImageConstants#ZLIB} or {@link org.freehep.graphicsio.ImageConstants#JPG}
     * @throws java.io.IOException thrown by ImageBytes
     */
    public void image(RenderedImage image, Color bkg, String encode)
            throws IOException {

        ImageBytes bytes = new ImageBytes(image, bkg, encode, ImageConstants.COLOR_MODEL_RGB);

        entry("Width", image.getWidth());
        entry("Height", image.getHeight());
        entry("ColorSpace", pdf.name("DeviceRGB"));
        entry("BitsPerComponent", 8);
        entry("Filter", getFilterName(bytes.getFormat()));
        write(bytes.getBytes());
    }

    public void imageMask(RenderedImage image, String encode)
            throws IOException {

        ImageBytes bytes = new ImageBytes(image, null, encode, ImageConstants.COLOR_MODEL_A);

        entry("Width", image.getWidth());
        entry("Height", image.getHeight());
        entry("BitsPerComponent", 8);
        entry("ColorSpace", pdf.name("DeviceGray"));
        entry("Filter", getFilterName(bytes.getFormat()));
        write(bytes.getBytes());
    }


    /**
     * Inline Image convenience function (see Table 4.39 and 4.40). Ouputs the
     * data of the image using "DeviceRGB" colorspace, and the requested
     * encoding.

     * @param image Image to write
     * @param bkg Background color, null for transparent image
     * @param encode {@link org.freehep.graphicsio.ImageConstants#ZLIB} or {@link org.freehep.graphicsio.ImageConstants#JPG}
     * @throws java.io.IOException thrown by ImageBytes
     */
    public void inlineImage(RenderedImage image, Color bkg, String encode)
            throws IOException {

        ImageBytes bytes = new ImageBytes(image, bkg, ImageConstants.JPG, ImageConstants.COLOR_MODEL_RGB);

        println("BI");
        imageInfo("Width", image.getWidth());
        imageInfo("Height", image.getHeight());
        imageInfo("ColorSpace", pdf.name("DeviceRGB"));
        imageInfo("BitsPerComponent", 8);

        imageInfo("Filter", getFilterName(bytes.getFormat()));
        print("ID\n");

        write(bytes.getBytes());

        println("\nEI");
    }

    //
    // In-line Image operators (see Table 4.38)
    //
    private void imageInfo(String key, int number) throws IOException {
        println("/" + key + " " + number);
    }

    private void imageInfo(String key, PDFName name) throws IOException {
        println("/" + key + " " + name);
    }

    private void imageInfo(String key, Object[] array) throws IOException {
        print("/" + key + " [");
        for (int i = 0; i < array.length; i++) {
            print(" " + array[i]);
        }
        println("]");
    }

    /**
     * Draws the <i>points</i> of the shape using path <i>construction</i>
     * operators. The path is neither stroked nor filled.
     * 
     * @return true if even-odd winding rule should be used, false if non-zero
     *         winding rule should be used.
     */
    public boolean drawPath(Shape s) throws IOException {
        PDFPathConstructor path = new PDFPathConstructor(this);
        return path.addPath(s);
    }

    //
    // XObject operators (see Table 4.34)
    //
    public void xObject(PDFName name) throws IOException {
        println(name + " Do");
    }

    //
    // Marked Content operators (see Table 8.5)
    //
    // FIXME: missing all

    //
    // Compatibility operators (see Table 3.19)
    //
    private boolean compatibilityOpen = false;

    public void beginCompatibility() throws IOException {
        if (compatibilityOpen)
            System.err
                    .println("PDFStream: nested use of Compatibility sections not allowed.");
        println("BX");
        compatibilityOpen = true;
    }

    public void endCompatibility() throws IOException {
        if (!compatibilityOpen)
            System.err
                    .println("PDFStream: unbalanced use of begin/endCompatibilty().");
        println("EX");
        compatibilityOpen = false;
    }

}
