/*
 * 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$ */

package org.apache.fop.render.awt.viewer;

// Originally contributed by:
// Juergen Verwohlt: Juergen.Verwohlt@jCatalog.com,
// Rainer Steinkuhle: Rainer.Steinkuhle@jCatalog.com,
// Stanislav Gorkhover: Stanislav.Gorkhover@jCatalog.com

// Java
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.InputMap;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.render.awt.AWTRenderer;

/**
 * AWT Viewer main window.
 * Surrounds a PreviewPanel with a bunch of pretty buttons and controls.
 */
public class PreviewDialog extends JFrame implements StatusListener {

    /** The Translator for localization */
    protected Translator translator;
    /** The AWT renderer */
    protected AWTRenderer renderer;
    /** The FOUserAgent associated with this window */
    protected FOUserAgent foUserAgent;
    /** The originally configured target resolution */
    protected float configuredTargetResolution;
    /**
     * Renderable instance that can be used to reload and re-render a document after
     * modifications.
     */
    protected Renderable renderable;

    /** The JCombobox to rescale the rendered page view */
    private JComboBox scale;

    /** The JLabel for the process status bar */
    private JLabel processStatus;

    /** The JLabel information status bar */
    private JLabel infoStatus;

    /** The main display area */
    private PreviewPanel previewPanel;

    /** Formats the text in the scale combobox. */
    private DecimalFormat percentFormat = new DecimalFormat("###0.0#",
                                            new DecimalFormatSymbols(Locale.ENGLISH));

    /**
     * Creates a new PreviewDialog that uses the given renderer.
     * @param foUserAgent the user agent
     * @param renderable the Renderable instance that is used to reload/re-render a document
     *                   after modifications.
     */
    public PreviewDialog(
        FOUserAgent foUserAgent, Renderable renderable) {
        renderer = (AWTRenderer) foUserAgent.getRendererOverride();
        this.foUserAgent = foUserAgent;
        this.configuredTargetResolution = this.foUserAgent.getTargetResolution();
        this.renderable = renderable;
        translator = new Translator();

        //Commands aka Actions
        Command printAction = new Command(translator.getString("Menu.Print"), "Print") {
            public void doit() {
                startPrinterJob(true);
            }
        };
        Command firstPageAction = new Command(translator.getString("Menu.First.page"),
                                      "firstpg") {
            public void doit() {
                goToFirstPage();
            }
        };
        Command previousPageAction = new Command(translator.getString("Menu.Prev.page"),
                                         "prevpg") {
            public void doit() {
                goToPreviousPage();
            }
        };
        Command nextPageAction = new Command(translator.getString("Menu.Next.page"), "nextpg") {
            public void doit() {
                goToNextPage();
            }

        };
        Command lastPageAction = new Command(translator.getString("Menu.Last.page"), "lastpg") {
            public void doit() {
                goToLastPage();
            }
        };
        Command reloadAction = new Command(translator.getString("Menu.Reload"), "reload") {
            public void doit() {
                previewPanel.reload();
            }
        };
        Command debugAction = new Command(translator.getString("Menu.Debug"), "debug") {
            // TODO use Translator
            public void doit() {
                previewPanel.debug();
            }
        };
        Command aboutAction = new Command(translator.getString("Menu.About"), "fopLogo") {
            public void doit() {
                startHelpAbout();
            }
        };

        setTitle("FOP: AWT-" + translator.getString("Title.Preview"));
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);

        //Sets size to be 61%x90% of the screen size
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        // Needed due to bug in Sun's JVM 1.5 (6429775)
        pack();
        //Rather frivolous size - fits A4 page width in 1024x768 screen on my desktop
        setSize(screen.width * 61 / 100, screen.height * 9 / 10);

        //Page view stuff
        previewPanel = new PreviewPanel(foUserAgent, renderable, renderer);
        getContentPane().add(previewPanel, BorderLayout.CENTER);
        previewPanel.addPageChangeListener(new PageChangeListener() {
            public void pageChanged(PageChangeEvent pce) {
              new ShowInfo().run();
            }
        });

        // Keyboard shortcuts - pgup/pgdn
        InputMap im = previewPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap am = previewPanel.getActionMap();
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextPage");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevPage");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), "firstPage");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), "lastPage");
        previewPanel.getActionMap().put("nextPage", nextPageAction);
        previewPanel.getActionMap().put("prevPage", previousPageAction);
        previewPanel.getActionMap().put("firstPage", firstPageAction);
        previewPanel.getActionMap().put("lastPage", lastPageAction);

        //Scaling combobox
        scale = new JComboBox();
        scale.addItem(translator.getString("Menu.Fit.Window"));
        scale.addItem(translator.getString("Menu.Fit.Width"));
        scale.addItem("25%");
        scale.addItem("50%");
        scale.addItem("75%");
        scale.addItem("100%");
        scale.addItem("150%");
        scale.addItem("200%");
        scale.setMaximumSize(new Dimension(80, 24));
        scale.setPreferredSize(new Dimension(80, 24));
        scale.setSelectedItem("100%");
        scale.setEditable(true);
        scale.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                scaleActionPerformed(e);
            }
        });

        //Menu
        setJMenuBar(setupMenu());

        // Toolbar
        JToolBar toolBar = new JToolBar();
        toolBar.add(printAction);
        toolBar.add(reloadAction);
        toolBar.addSeparator();
        toolBar.add(firstPageAction);
        toolBar.add(previousPageAction);
        toolBar.add(nextPageAction);
        toolBar.add(lastPageAction);
        toolBar.addSeparator(new Dimension(20, 0));
        toolBar.add(new JLabel(translator.getString("Menu.Zoom") + " "));
        toolBar.add(scale);
        toolBar.addSeparator();
        toolBar.add(debugAction);
        toolBar.addSeparator();
        toolBar.add(aboutAction);
        getContentPane().add(toolBar, BorderLayout.NORTH);

        // Status bar
        JPanel statusBar = new JPanel();
        processStatus = new JLabel();
        processStatus.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createEtchedBorder(),
                BorderFactory.createEmptyBorder(0, 3, 0, 0)));
        infoStatus = new JLabel();
        infoStatus.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createEtchedBorder(),
                BorderFactory.createEmptyBorder(0, 3, 0, 0)));

        statusBar.setLayout(new GridBagLayout());

        processStatus.setPreferredSize(new Dimension(200, 21));
        processStatus.setMinimumSize(new Dimension(200, 21));

        infoStatus.setPreferredSize(new Dimension(100, 21));
        infoStatus.setMinimumSize(new Dimension(100, 21));
        statusBar.add(processStatus,
                      new GridBagConstraints(0, 0, 1, 0, 2.0, 0.0,
                                             GridBagConstraints.CENTER,
                                             GridBagConstraints.HORIZONTAL,
                                             new Insets(0, 0, 0, 3), 0, 0));
        statusBar.add(infoStatus,
                      new GridBagConstraints(1, 0, 1, 0, 1.0, 0.0,
                                             GridBagConstraints.CENTER,
                                             GridBagConstraints.HORIZONTAL,
                                             new Insets(0, 0, 0, 0), 0, 0));
        getContentPane().add(statusBar, BorderLayout.SOUTH);
    }

    /**
     * Creates and initialize the AWT Viewer main window.
     * @param foUserAgent the FO user agent
     * @param renderable the target for the rendering
     * @param asMainWindow true if the window shall act as the main application window.
     * @return the newly initialized preview dialog
     */
    public static PreviewDialog createPreviewDialog(FOUserAgent foUserAgent,
                Renderable renderable, boolean asMainWindow) {
        PreviewDialog frame = new PreviewDialog(foUserAgent, renderable);

        if (asMainWindow) {
            frame.addWindowListener(new WindowAdapter() {
                public void windowClosed(WindowEvent we) {
                    System.exit(0);
                }
            });
        }

        // Centers the window
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = frame.getSize();
        if (frameSize.height > screenSize.height) {
            frameSize.height = screenSize.height;
        }
        if (frameSize.width > screenSize.width) {
            frameSize.width = screenSize.width;
        }
        frame.setLocation((screenSize.width - frameSize.width) / 2,
                    (screenSize.height - frameSize.height) / 2);
        frame.setStatus(frame.translator.getString("Status.Build.FO.tree"));
        frame.setVisible(true);
        return frame;
    }

    /**
     * Creates a new PreviewDialog that uses the given renderer.
     * @param foUserAgent the user agent
     */
    public PreviewDialog(FOUserAgent foUserAgent) {
        this(foUserAgent, null);
    }

    /**
     * Creates a new menubar to be shown in this window.
     * @return the newly created menubar
     */
    private JMenuBar setupMenu() {
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu(translator.getString("Menu.File"));
        menu.setMnemonic(KeyEvent.VK_F);
        //Adds mostly the same actions, but without icons
        menu.add(new Command(translator.getString("Menu.Print"), KeyEvent.VK_P) {
            public void doit() {
                startPrinterJob(true);
            }
        });
        // inputHandler must be set to allow reloading
        if (renderable != null) {
            menu.add(new Command(translator.getString("Menu.Reload"), KeyEvent.VK_R) {
                public void doit() {
                    reload();
                }
            });
        }
        menu.addSeparator();
        menu.add(new Command(translator.getString("Menu.Exit"), KeyEvent.VK_X) {
            public void doit() {
                dispose();
            }
        });
        menuBar.add(menu);

        menu = new JMenu(translator.getString("Menu.View"));
        menu.setMnemonic(KeyEvent.VK_V);
        menu.add(new Command(translator.getString("Menu.First.page"), KeyEvent.VK_F) {
            public void doit() {
                goToFirstPage();
            }
        });
        menu.add(new Command(translator.getString("Menu.Prev.page"), KeyEvent.VK_P) {
            public void doit() {
                goToPreviousPage();
            }
        });
        menu.add(new Command(translator.getString("Menu.Next.page"), KeyEvent.VK_N) {
            public void doit() {
                goToNextPage();
            }
        });
        menu.add(new Command(translator.getString("Menu.Last.page"), KeyEvent.VK_L) {
            public void doit() {
                goToLastPage();
            }
        });
        menu.add(new Command(translator.getString("Menu.Go.to.Page"), KeyEvent.VK_G) {
            public void doit() {
                showGoToPageDialog();
            }
        });
        menu.addSeparator();
        JMenu subMenu = new JMenu(translator.getString("Menu.Zoom"));
        subMenu.setMnemonic(KeyEvent.VK_Z);
        subMenu.add(new Command("25%", 0) {
            public void doit() {
                setScale(25.0);
            }
        });
        subMenu.add(new Command("50%", 0) {
            public void doit() {
                setScale(50.0);
            }
        });
        subMenu.add(new Command("75%", 0) {
            public void doit() {
                setScale(75.0);
            }
        });
        subMenu.add(new Command("100%", 0) {
            public void doit() {
                setScale(100.0);
            }
        });
        subMenu.add(new Command("150%", 0) {
            public void doit() {
                setScale(150.0);
            }
        });
        subMenu.add(new Command("200%", 0) {
            public void doit() {
                setScale(200.0);
            }
        });
        menu.add(subMenu);
        menu.addSeparator();
        menu.add(new Command(translator.getString("Menu.Default.zoom"), KeyEvent.VK_D) {
            public void doit() {
                setScale(100.0);
            }
        });
        menu.add(new Command(translator.getString("Menu.Fit.Window"), KeyEvent.VK_F) {
            public void doit() {
                setScaleToFitWindow();
            }
        });
        menu.add(new Command(translator.getString("Menu.Fit.Width"), KeyEvent.VK_W) {
            public void doit() {
                setScaleToFitWidth();
            }
        });
        menu.addSeparator();

        ButtonGroup group = new ButtonGroup();
        JRadioButtonMenuItem single = new JRadioButtonMenuItem(
                new Command(translator.getString("Menu.Single"), KeyEvent.VK_S) {
            public void doit() {
                previewPanel.setDisplayMode(PreviewPanel.SINGLE);
            }
        });
        JRadioButtonMenuItem cont = new JRadioButtonMenuItem(
                new Command(translator.getString("Menu.Continuous"), KeyEvent.VK_C) {
            public void doit() {
                previewPanel.setDisplayMode(PreviewPanel.CONTINUOUS);
            }
        });
        JRadioButtonMenuItem facing = new JRadioButtonMenuItem(
                new Command(translator.getString("Menu.Facing"), 0) {
            public void doit() {
                previewPanel.setDisplayMode(PreviewPanel.CONT_FACING);
            }
        });
        single.setSelected(true);
        group.add(single);
        group.add(cont);
        group.add(facing);
        menu.add(single);
        menu.add(cont);
        menu.add(facing);

        menuBar.add(menu);

        menu = new JMenu(translator.getString("Menu.Help"));
        menu.setMnemonic(KeyEvent.VK_H);
        menu.add(new Command(translator.getString("Menu.About"), KeyEvent.VK_A) {
            public void doit() {
                startHelpAbout();
            }
        });
        menuBar.add(menu);
        return menuBar;
    }

    /** {@inheritDoc} */
    public void notifyRendererStopped() {
        reload();
    }

    private void reload() {
        setStatus(translator.getString("Status.Show"));
        previewPanel.reload();
    }

    /**
     * Changes the current visible page
     * @param number the page number to go to
     */
    public void goToPage(int number) {
        if (number != previewPanel.getPage()) {
            previewPanel.setPage(number);
            notifyPageRendered();
        }
    }

    /**
     * Shows the previous page.
     */
    public void goToPreviousPage() {
        int page = previewPanel.getPage();
        if (page > 0) {
            goToPage(page - 1);
        }
    }

    /**
     * Shows the next page.
     */
    public void goToNextPage() {
        int page = previewPanel.getPage();
        if (page < renderer.getNumberOfPages() - 1) {
            goToPage(page + 1);
        }
    }

    /** Shows the first page. */
    public void goToFirstPage() {
        goToPage(0);
    }

    /**
     * Shows the last page.
     */
    public void goToLastPage() {
        goToPage(renderer.getNumberOfPages() - 1);
    }

    /** Shows the About box */
    private void startHelpAbout() {
        PreviewDialogAboutBox dlg = new PreviewDialogAboutBox(this, translator);
        //Centers the box
        Dimension dlgSize = dlg.getPreferredSize();
        Dimension frmSize = getSize();
        Point loc = getLocation();
        dlg.setLocation((frmSize.width - dlgSize.width) / 2 + loc.x,
                        (frmSize.height - dlgSize.height) / 2 + loc.y);
        dlg.setVisible(true);
    }

    /**
     * Shows "go to page" dialog and then goes to the selected page
     */
    private void showGoToPageDialog() {
                int currentPage = previewPanel.getPage();
        GoToPageDialog d = new GoToPageDialog(this,
            translator.getString("Menu.Go.to.Page"), translator);
        d.setLocation((int)getLocation().getX() + 50,
                      (int)getLocation().getY() + 50);
        d.setVisible(true);
        currentPage = d.getPageNumber();
        if (currentPage < 1 || currentPage > renderer.getNumberOfPages()) {
            return;
        }
        currentPage--;
        goToPage(currentPage);
    }

    /**
     * Scales page image.
     * @param scaleFactor the scale factor
     */
    public void setScale(double scaleFactor) {
        scale.setSelectedItem(percentFormat.format(scaleFactor) + "%");
        previewPanel.setScaleFactor(scaleFactor / 100d);
    }

    /**
     * Sets the scaling so the contents fit into the window.
     */
    public void setScaleToFitWindow() {
        try {
            setScale(previewPanel.getScaleToFitWindow() * 100);
        } catch (FOPException fopEx) {
            fopEx.printStackTrace();
        }
    }

    /**
     * Sets the scaling so the contents are spread over the whole width available.
     */
    public void setScaleToFitWidth() {
        try {
            setScale(previewPanel.getScaleToFitWidth() * 100);
        } catch (FOPException fopEx) {
            fopEx.printStackTrace();
        }
    }

    private void scaleActionPerformed(ActionEvent e) {
        int index = scale.getSelectedIndex();
        if (index == 0) {
            setScaleToFitWindow();
        } else if (index == 1) {
            setScaleToFitWidth();
        } else {
            String item = (String)scale.getSelectedItem();
            setScale(Double.parseDouble(item.substring(0, item.indexOf('%'))));
        }
    }

    /**
     * Prints the document.
     * @param showDialog true if show dialog
     */
    public void startPrinterJob(boolean showDialog) {
        //Restore originally configured target resolution
        float saveResolution = foUserAgent.getTargetResolution();
        foUserAgent.setTargetResolution(this.configuredTargetResolution);

        PrinterJob pj = PrinterJob.getPrinterJob();
        pj.setPageable(renderer);
        if (!showDialog || pj.printDialog()) {
            try {
                pj.print();
            } catch (PrinterException e) {
                e.printStackTrace();
            }
        }

        foUserAgent.setTargetResolution(saveResolution);
    }

    /**
     * Sets message to be shown in the status bar in a thread safe way.
     * @param message the message
     */
    public void setStatus(String message) {
        SwingUtilities.invokeLater(new ShowStatus(message));
    }

    /** This class is used to show status in a thread safe way. */
    private class ShowStatus implements Runnable {

        /** The message to display */
        private String message;

        /**
         * Constructs ShowStatus thread
         * @param message message to display
         */
        public ShowStatus(String message) {
            this.message = message;
        }

        public void run() {
            processStatus.setText(message);
        }
    }

    /**
     * Updates the message to be shown in the info bar in a thread safe way.
     */
    public void notifyPageRendered() {
        SwingUtilities.invokeLater(new ShowInfo());
    }

    /** This class is used to show info in a thread safe way. */
    private class ShowInfo implements Runnable {

        public void run() {
            String message = translator.getString("Status.Page") + " "
                    + (previewPanel.getPage() + 1) + " "
                    + translator.getString("Status.of") + " "
                    + (renderer.getNumberOfPages());
            infoStatus.setText(message);
        }
    }

    /**
     * Opens standard Swing error dialog box and reports given exception details.
     * @param e the Exception
     */
    public void reportException(Exception e) {
        String msg = translator.getString("Exception.Occured");
        setStatus(msg);
        JOptionPane.showMessageDialog(
            getContentPane(),
            "<html><b>" + msg + ":</b><br>"
                + e.getClass().getName() + "<br>"
                + e.getMessage() + "</html>",
            translator.getString("Exception.Error"),
            JOptionPane.ERROR_MESSAGE
        );
    }
}
