/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.io.exports;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.print.PageFormat;
import java.awt.print.Printable;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

import java.util.Calendar;
import java.util.NoSuchElementException;

import javax.print.Doc;
import javax.print.DocFlavor;
import javax.print.DocFlavor.BYTE_ARRAY;
import javax.print.DocFlavor.SERVICE_FORMATTED;
import javax.print.DocPrintJob;
import javax.print.PrintException;
import javax.print.SimpleDoc;
import javax.print.StreamPrintService;
import javax.print.StreamPrintServiceFactory;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.OrientationRequested;
import javax.print.event.PrintJobAdapter;
import javax.print.event.PrintJobEvent;

import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;

import net.sf.jaxodraw.gui.JaxoDialogs;
import net.sf.jaxodraw.io.JaxoPreview;
import net.sf.jaxodraw.plugin.JaxoPluginExecutionException;
import net.sf.jaxodraw.util.JaxoInfo;
import net.sf.jaxodraw.util.JaxoLanguage;
import net.sf.jaxodraw.util.JaxoLog;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/** The class that is responsible for exporting to postscript output: portrait,
 * landscape or EPS are possible options. Asks for an export file name.
 * @since 2.0
 */
public class JaxoExportPS extends JaxoExport implements Printable {
    /**
     * The export modes supported by this JaxoExport.
     */
    public enum Format { PS, EPS }

    // private
    private Format mode;
    private final Rectangle psbBox = new Rectangle();
    private boolean landscape;
    private JPanel panel;
    private JLabel orientationLabel;
    private JComboBox orientation;

    /** The unique plugin id. */
    private static final String PLUGIN_ID = JaxoExportPS.class.getName();
    private static final String TMP_PS_FILENAME = ".temp.ps";
    private static final String TMP_EPS_FILENAME = ".temp.eps";
    private static final String PS_FIRST_LINE = "%!PS-Adobe-3.0";
    private static final String EPS_FIRST_LINE = "%!PS-Adobe-3.0 EPSF-3.0";
    private static final String CREATOR_LINE =
        "%%Creator: " + JaxoInfo.USER_NAME + " with " + JaxoInfo.VERSION;
    private static final String SETPAGEDEVICE = "setpagedevice";

    /** Constructor.
     * @param psmode PS_PS or PS_EPS.
     */
    public JaxoExportPS(final Format psmode) {
        super();
        setMode(psmode);
    }

    /** Returns the landscape mode.
     * @return True for landscape, false for portrait..
     */
    public final boolean isLandscape() {
        return landscape;
    }

    /** Sets the landscape mode.
     * @param value True for landscape, false for portrait.
     */
    public void setLandscape(final boolean value) {
        landscape = value;
    }

    /** Sets the export mode.
     * @param value The new export mode (PS_XYZ constant).
     */
    public final void setMode(final Format value) {
        mode = value;
    }

    /** Returns the current export mode.
     * @return The current export mode.
     */
    public final Format getMode() {
        return mode;
    }

    /** {@inheritDoc} */
    public final String getFormatName() {
        return JaxoLanguage.translate("JaxoExportPS.formatName."
            + getFileExtension());
    }

    /** {@inheritDoc} */
    public final String getFileExtension() {
        return (getMode().equals(Format.EPS)) ? "eps" : "ps";
    }

    /** {@inheritDoc} */
    public final String getFileExtensionDescription() {
        return JaxoLanguage.translate("JaxoExportPS.fileDescription."
            + getFileExtension());
    }

    /** {@inheritDoc} */
    public String description() {
        return JaxoLanguage.translate("JaxoExportPS.description."
            + getFileExtension());
    }

    /** {@inheritDoc} */
    public String pluginId() {
        return PLUGIN_ID;
    }

    /** {@inheritDoc} */
    public String getShortName() {
        return "ps-export";
    }

    /** {@inheritDoc} */
    protected void exportTo(final String fileName)
        throws JaxoPluginExecutionException {
        try {
            if (getMode().equals(Format.EPS)) {
                doExportEPS(fileName);
            } else {
                doExportPS(fileName);
            }
        } catch (IOException ie) {
            throw new JaxoPluginExecutionException(
                errorDialogMessage(fileName), ie, this);
        }
    }

    private void doExportPS(final String fileName) throws IOException {
        final File tempFile = new File(JaxoPreview.TMP_DIR, TMP_PS_FILENAME);
        tempFile.deleteOnExit();

        writePS(tempFile);

        BufferedReader inbr = null;
        BufferedWriter outbr = null;
        try {
            inbr = new BufferedReader(new FileReader(tempFile));
            outbr = new BufferedWriter(new FileWriter(fileName));

            final Calendar rightNow = Calendar.getInstance();

            final String forthLine = "%%Title: " + getGraph().getDescription();
            final String fifthLine =
                "%%CreationDate: " + rightNow.get(Calendar.DAY_OF_MONTH) + "/"
                + (rightNow.get(Calendar.MONTH) + 1) + "/"
                + rightNow.get(Calendar.YEAR);

            String str;
            boolean isFirstLine = true;

            while ((str = inbr.readLine()) != null) {
                if (isFirstLine) {
                    outbr.write(PS_FIRST_LINE, 0, PS_FIRST_LINE.length());
                    outbr.newLine();
                    outbr.write(CREATOR_LINE, 0, CREATOR_LINE.length());
                    outbr.newLine();
                    outbr.write(forthLine, 0, forthLine.length());
                    outbr.newLine();
                    outbr.write(fifthLine, 0, fifthLine.length());
                    outbr.newLine();
                    isFirstLine = false;
                } else {
                    outbr.write(str, 0, str.length());
                    outbr.newLine();
                }
            }
        } finally { // make sure the streams are closed
            close(inbr);
            close(outbr);
        }
    }

    private void doExportEPS(final String fileName) throws IOException {
        final File tempFile = new File(JaxoPreview.TMP_DIR, TMP_EPS_FILENAME);
        tempFile.deleteOnExit();

        writePS(tempFile);

        BufferedReader inbr = null;
        BufferedWriter outbr = null;
        try {
            inbr = new BufferedReader(new FileReader(tempFile));
            outbr = new BufferedWriter(new FileWriter(fileName));

            final Rectangle bb = psbBox.getBounds();

            final Calendar rightNow = Calendar.getInstance();

            final String secondLine =
                "%%BoundingBox: "
                + Math.round((float) Math.floor(bb.getMinX())) + " "
                + Math.round((float) Math.floor(bb.getMinY())) + " "
                + Math.round((float) Math.ceil(bb.getMaxX())) + " "
                + Math.round((float) Math.ceil(bb.getMaxY()));
            final String forthLine = "%%Title: " + getGraph().getDescription();
            final String fifthLine =
                "%%CreationDate: " + rightNow.get(Calendar.DAY_OF_MONTH) + "/"
                + (rightNow.get(Calendar.MONTH) + 1) + "/"
                + rightNow.get(Calendar.YEAR);

            String str;
            boolean isFirstLine = true;

            while ((str = inbr.readLine()) != null) {
                if (isFirstLine) {
                    outbr.write(EPS_FIRST_LINE, 0, EPS_FIRST_LINE.length());
                    outbr.newLine();
                    outbr.write(secondLine, 0, secondLine.length());
                    outbr.newLine();
                    outbr.write(CREATOR_LINE, 0, CREATOR_LINE.length());
                    outbr.newLine();
                    outbr.write(forthLine, 0, forthLine.length());
                    outbr.newLine();
                    outbr.write(fifthLine, 0, fifthLine.length());
                    outbr.newLine();
                    isFirstLine = false;
                } else {
                    /*
                        check whether the line contains
                        the postscript command "setpagedevice",
                        which is not allowed in EPS

                        other forbidden commands:
                        banddevice      exitserver    quit
                        clear           framedevice   renderbands
                        cleardictstack  grestoreall   setglobal
                        copypage        initclip      setpagedevice
                        erasepage       initgraphics  setshared
                        initmatrix      startjob
                    */
                    if (str.indexOf(SETPAGEDEVICE) != -1) {
                        str = str.replaceAll(SETPAGEDEVICE, "pop");
                    }
                    outbr.write(str, 0, str.length());
                    outbr.newLine();
                }
            }
        } finally {
            close(inbr);
            close(outbr);
        }
    }

    /** The actual printing routine required by the Printable interface.
     * @param g The specified graphics context.
     * @param pf The specified page format.
     * @param pageIndex The specified page index.
     * @return Either Printable.PAGE_EXISTS or Printable.NO_SUCH_PAGE.
     */
    public final int print(final Graphics g, final PageFormat pf, final int pageIndex) {
        double transX = pf.getImageableX();
        double transY = pf.getImageableY();
        final double paperHeight = pf.getHeight();

        Rectangle graphBB = getGraph().getBounds();

        if (graphBB == null) {
            graphBB = new Rectangle(1, 1); // to avoid division by zero below
        }

        final double scaleX = pf.getImageableWidth() / graphBB.getWidth();
        final double scaleY = pf.getImageableHeight() / graphBB.getHeight();

        if (pageIndex == 0) {
            final JaxoGraphics2D g2 = new JaxoGraphics2D((Graphics2D) g);

            g2.setPrinting(true);

            final double right = transX + pf.getImageableWidth() - graphBB.getX() - graphBB.getWidth();
            final double bottom = transY + pf.getImageableHeight() - graphBB.getY() - graphBB.getHeight();

            double scale = scaleX;

            if (scaleY < scaleX) {
                scale = scaleY;
            }

            if (scale > 1.) {
                scale = 1.d;
            }

            g2.translate(transX, transY); // origin of printable area

            if (scale < 1.d) {
                // case 1: graph is bigger than printable area: scale it and shift to origin
                g2.scale(scale, scale);
                g2.translate(-graphBB.getX(), -graphBB.getY());
            } else {
                // case 2: graph size fits printable area but lies (partly) outside: just shift it back in
                if (right < 0.d) {
                    g2.translate(right - transX, 0.d);
                    transX = right;
                }

                if (bottom < 0.d) {
                    g2.translate(0.d, bottom - transY);
                    transY = bottom;
                }
            }

            if (getMode().equals(Format.EPS)) {
                setEPSBoundingBox(transX, transY, scale, paperHeight, graphBB);
            }

            getGraph().paint(g2);

            return Printable.PAGE_EXISTS;
        } else {
            return Printable.NO_SUCH_PAGE;
        }
    }

    private void setEPSBoundingBox(final double transX, final double transY, final double scale, final double paperHeight, final Rectangle bBox) {

        double shiftX = bBox.getX();
        double shiftY = bBox.getY();

        if (scale < 1.d) {
            shiftX = 0.d;
            shiftY = 0.d;
        }

        if (landscape) {
            psbBox.setFrameFromDiagonal((shiftY * scale) + transX,
                (shiftX * scale) + transX,
                ((shiftY + bBox.height) * scale) + transX,
                ((shiftX + bBox.width) * scale) + transX);
        } else {
            psbBox.setFrameFromDiagonal((shiftX * scale) + transX,
                paperHeight - (((shiftY + bBox.height) * scale) + transY),
                ((shiftX + bBox.width) * scale) + transX,
                paperHeight - ((shiftY * scale) + transY));
        }
    }

    /** The class that is responsible for exporting to postscript output:
     * portrait, landscape or EPS are possible options.
     * The export file name is given by fileName.
     * @param file The export file.
     */
    protected final void writePS(final File file) {
        final DocFlavor flavor = SERVICE_FORMATTED.PRINTABLE;
        final String psMimeType = BYTE_ARRAY.POSTSCRIPT.getMimeType();
        final StreamPrintServiceFactory[] factories =
            StreamPrintServiceFactory.lookupStreamPrintServiceFactories(flavor,
                psMimeType);

        if (factories.length == 0) {
            setFailure(new NoSuchElementException("StreamPrintServiceFactory"));
            showErrorDialog(JaxoLanguage.translate("No_suitable_factories_found!"));
        } else {
            FileOutputStream fos = null;
            try {
                // Create a file for the exported postscript
                fos = new FileOutputStream(file);

                // Create a Stream printer for Postscript
                final StreamPrintService sps = factories[0].getPrintService(fos);

                // Create and call a Print Job
                final DocPrintJob job = sps.createPrintJob();

                final PrintRequestAttributeSet aset =
                    new HashPrintRequestAttributeSet();

                aset.add(landscape ? OrientationRequested.LANDSCAPE
                                   : OrientationRequested.PORTRAIT);

                final Doc doc = new SimpleDoc(this, flavor, null);
                final PrintJobWatcher pjDone = new PrintJobWatcher(job);
                job.print(doc, aset);
                pjDone.waitForDone();

            } catch (PrintException pe) {
                setFailure(pe);
                showErrorDialog(errorDialogMessage(file.getName()));
            } catch (IOException ie) {
                setFailure(ie);
                showErrorDialog(errorDialogMessage(file.getName()));
            } finally {
                close(fos);
            }
        }
    }

    /** {@inheritDoc} */
    public void preview(final JaxoPreview p, final boolean sameWindow) {
        if (p.canShowPostscript()) {
            final String previewFileName =
                getMode().equals(Format.EPS) ? "Jaxo_tmp.eps" : "Jaxo_tmp.ps";

            final File previewFile = new File(JaxoPreview.TMP_DIR, previewFileName);
            final boolean previewFileExists = previewFile.exists();
            previewFile.deleteOnExit();

            try {
                exportTo(previewFile.getAbsolutePath());
            } catch (JaxoPluginExecutionException e) {
                setFailure(e); // this logs in debug mode
                if (e.getMessage() != null) {
                    showErrorDialog(e.getMessage());
                }
                return;
            }

            final boolean newWindow = !(sameWindow && previewFileExists);

            // p.setTitle(.) -- postscript is still always external
            if (newWindow) {
                p.showPostscript(previewFile);
            }
        } else {
            p.showMessageForPostscriptViewer();

        }
    }

    /** {@inheritDoc} */
    public void commitConfiguration() {
        // by default: do nothing.
    }

    /** {@inheritDoc}
     * @return A panel to configure the postscript orientation.
     */
    public JComponent getConfigurationPanel() {
        if (panel == null) {
            panel = new JPanel(new GridBagLayout(), false);

            final GridBagConstraints labels = new GridBagConstraints();
            final GridBagConstraints components = new GridBagConstraints();

            labels.anchor = GridBagConstraints.LINE_END;
            labels.gridx = 0;
            labels.gridy = 0;
            components.anchor = GridBagConstraints.LINE_START;
            components.gridx = 1;
            components.gridy = 0;
            components.weightx = 1;

            orientationLabel =
                new JLabel(JaxoLanguage.label("JaxoExportPS.orientation"));
            panel.add(orientationLabel, labels);

            this.orientation =
                new JComboBox(new Object[]{
                        JaxoLanguage.translate(
                            "JaxoExportPS.orientation.portrait"),
                        JaxoLanguage.translate(
                            "JaxoExportPS.orientation.landscape")
                    });

            orientation.addActionListener(new ActionListener() {
                    public void actionPerformed(final ActionEvent e) {
                        landscape = orientation.getSelectedIndex() == 1;
                    }
                });

            panel.add(orientation, components);

            labels.gridy++;
            components.gridy++;

        }

        return panel;
    }

    /** {@inheritDoc} */
    public final void updateLanguage() {
        if (orientationLabel != null) {
            orientationLabel.setText(JaxoLanguage.label(
                    "JaxoExportPS.orientation"));
            this.orientation =
                new JComboBox(new Object[]{
                        JaxoLanguage.translate(
                            "JaxoExportPS.orientation.portrait"),
                        JaxoLanguage.translate(
                            "JaxoExportPS.orientation.landscape")
                    });
        }
    }

    private class PrintJobWatcher {
        // true if it is safe to close the print job's input stream
        private boolean done;

        PrintJobWatcher(final DocPrintJob job) {
            // Add a listener to the print job
            job.addPrintJobListener(new PrintJobAdapter() {
                    @Override
                    public final void printJobCanceled(final PrintJobEvent pje) {
                        allDone();
                    }

                    @Override
                    public final void printJobCompleted(final PrintJobEvent pje) {
                        allDone();
                    }

                    @Override
                    public final void printJobFailed(final PrintJobEvent pje) {
                        allDone();
                    }

                    @Override
                    public final void printJobNoMoreEvents(final PrintJobEvent pje) {
                        allDone();
                    }

                    void allDone() {
                        synchronized (PrintJobWatcher.this) {
                            done = true;
                            PrintJobWatcher.this.notifyAll();
                        }
                    }
                });
        }

        public final void waitForDone() {
            synchronized (this) {
                try {
                    while (!done) {
                        wait();
                    }
                } catch (InterruptedException e) {
                    setFailure(e);
                    JaxoLog.debug(e);
                    JaxoDialogs.showI18NErrorDialog(null, "Print_job_aborted!");
                }
            }
        }
    }
}
