/*
 * $Id$
 *
 * Copyright (c) 1996, 2009, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.interview.wizard;

import com.sun.interview.Interview;
import com.sun.interview.Question;
import com.sun.interview.WizPrint;
import com.sun.javatest.tool.jthelp.HelpBroker;
import com.sun.javatest.tool.jthelp.HelpSet;
import com.sun.javatest.tool.jthelp.JTHelpBroker;
import com.sun.javatest.util.PropertyUtils;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSplitPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.filechooser.FileFilter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;

/**
 * A wizard to present an {@link Interview interview} consisting of
 * a series of {@link Question questions}.
 *
 * <p>The tool can be started as an application itself,
 * by using the {@link #main main}
 * method. This requires that the class name of the interview
 * be supplied as the first argument; the class itself must be on
 * the tool's class path. This technique allows any interview
 * to be run by this tool.
 * <p>An alternative technique is to provide a small default main method
 * inside each interview, which creates an instance of the interview
 * and starts up a tool such as this one to run the interview.
 * <pre>
 *    import javasoft.sqe.wizard.Interview;
 *    import javasoft.sqe.wizard.swing.Wizard;
 *
 *    public class Demo extends Interview {
 *        public static void main(String[] args) {
 *          Demo d = new Demo();
 *          Wizard w = new Wizard(d);
 *          w.showInFrame(true);
 *        }
 *    }
 * </pre>
 */
public class Wizard extends JComponent {
    /**
     * Action command for the okListener for {@link #showInDialog}.
     */
    public static final String OK = "OK";
    private static final KeyStroke enterKey = KeyStroke.getKeyStroke("ENTER");
    private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle();
    private static final String[][] fileMenuData = {
            {"new", "performNew"},
            {"open", "performOpen"},
            {"save", "performSave"},
            {"saveAs", "performSaveAs"},
            null,
            {"exportLog", "performExportLog"}
    };
    private static final String[][] helpMenuData = {
            {"help", "performHelp", "F1"}
    };
    private static final String[][] searchMenuData = {
            {"find", "performFind", "control F"},
            {"findNext", "performFindNext", "F3"},
    };
    private final FileFilter jtiFilter = new ExtensionFileFilter(".jti");
    private final FileFilter htmlFilter =
            new ExtensionFileFilter(".htm", ".html");
    private ActionListener performer = e -> perform(e.getActionCommand());
    private Interview interview;
    private Exporter[] exporters;
    private String title;
    private JMenuBar menuBar;
    private JMenu fileMenu;
    //private JPanel main;
    private JComponent main;
    private JPanel body;
    private PathPanel pathPanel;
    private QuestionPanel questionPanel;
    private InfoPanel infoPanel;
    private JToolBar buttonPanel;
    private JButton cancelBtn;
    private JButton backBtn;
    private JButton nextBtn;
    private JButton okBtn;
    private JToggleButton infoBtn;
    private Window window;
    private ActionListener okListener;
    private SearchDialog searchDialog;
    private boolean initialInfoVisible = true;
    private Listener listener = new Listener();
    // help for Help menu and context sensitive help (F1)
    private HelpSet helpHelpSet;
    private HelpBroker helpHelpBroker;
    private String helpHelpPrefix;
    private JMenu helpMenu;
    private File currFile;
    private File defaultFile;
    private boolean exitOnClose;

    /**
     * Create a wizard to present an interview.
     *
     * @param i The interview to be presented.
     */
    public Wizard(Interview i) {
        this(i, (Exporter[]) null);
    }

    /**
     * Create a wizard to present an interview.
     *
     * @param i The interview to be presented.
     * @param e An array of exporters to which the interview can be exported.
     */
    public Wizard(Interview i, Exporter... e) {
        interview = i;
        exporters = e;
    }

    /**
     * A minimal main program to invoke the wizard on a specified interview.
     *
     * @param args Only one argument is accepted: the name of a class which is
     *             a subtype of {@link Interview}.
     */
    public static void main(String... args) {
        try {
            UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());

            Class<? extends Interview> ic =
                    Class.forName(args[0], true, ClassLoader.getSystemClassLoader()).asSubclass(Interview.class);
            Interview i = ic.getDeclaredConstructor().newInstance();
            Wizard w = new Wizard(i);
            w.showInFrame(true);
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Open a file and load it into the interview for this wizard.
     * This does not affect the name of the current file.
     *
     * @param f The file to be loaded.
     * @throws IOException     if any problems occur while reading the file.
     * @throws Interview.Fault if the checksum is missing or incorrect in the file
     * @see Interview#load
     * @see #setFile
     */
    public void open(File f) throws Interview.Fault, IOException {
        try (InputStream in = new BufferedInputStream(new FileInputStream(f))) {

            Map<String, String> stringProps = PropertyUtils.load(in);
            interview.load(stringProps);
            interview.setEdited(false);
            String info = stringProps.get("INFO");
            if (info == null) {
                info = "true";
            }
            initialInfoVisible = info.equals("true");
        }
    }

    /**
     * Save the current responses to the interview's questions in a file..
     *
     * @param f The file in which to save the responses.
     * @throws IOException if any problems occur while reading the file.
     * @see Interview#save
     */
    public void save(File f) throws IOException {
        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
            Properties p = new Properties();
            if (infoPanel != null) {
                p.put("INFO", String.valueOf(infoPanel.isShowing()));
            }
            interview.save(PropertyUtils.convertToStringProps(p));
            interview.setEdited(false);
            p.save(out, "Wizard data file: " + interview.getTitle());
        }
    }

    /**
     * Get the name of the current file associated with this interview.
     *
     * @return the file for the interview
     * @see #setFile
     */
    public File getFile() {
        return currFile;
    }

    /**
     * Set the name of the current file associated with this interview.
     * The file may be used as a default in open/save operations.
     *
     * @param f The file to be associated with this interview.
     * @see #getFile
     * @see #setDefaultFile
     */
    public void setFile(File f) {
        currFile = new File(f.getAbsolutePath());
        if (window != null) {
            updateTitle(window);
        }
    }

    /**
     * Set the name of a default file associated with this interview.
     * The default file is used for the name of the current value
     * if the user performs a File>New operation. In addition, if the
     * default file is set, and the current file matches the default file,
     * it will not be shown in the title bar.
     *
     * @param f The default file to be associated with this interview.
     */
    public void setDefaultFile(File f) {
        defaultFile = f;
        if (window != null) {
            updateTitle(window);
        }
    }

    /**
     * Set the help broker in which context sensitive help and default menu help
     * is displayed. If not set, a default help broker will be created.
     *
     * @param helpBroker The help broker to use for context sensitive and menu help.
     */
    public void setHelpBroker(HelpBroker helpBroker) {
        helpHelpBroker = helpBroker;
    }

    /**
     * Set the help set to be used for context sensitive help and the default menu help.
     * If not set, the interview's help set will be used.
     *
     * @param helpSet The help set to use for context sensitive and menu help.
     */
    public void setHelpSet(HelpSet helpSet) {
        helpHelpSet = helpSet;
    }

    /**
     * Set the prefix string for the help IDs for context sensitive help and default menu help.
     * If not set, the default is "wizard.".
     *
     * @param helpPrefix A prefix to be used for all context sentive help and menu entries.
     */
    public void setHelpSetPrefix(String helpPrefix) {
        helpHelpPrefix = helpPrefix;
    }

    /**
     * Set the help menu to be used on the wizard. If not set, the default is a menu
     * containing a single "Help" entry.
     *
     * @param helpMenu The help menu to be used.
     */
    public void setHelpMenu(JMenu helpMenu) {
        this.helpMenu = helpMenu;
    }

    /**
     * Show the wizard in a frame centered on the screen.
     *
     * @param exitOnClose Set to true if the JVM should be exited when the frame is closed.
     */
    public void showInFrame(final boolean exitOnClose) {
        if (window != null && !(window instanceof JFrame)) {
            throw new IllegalStateException();
        }

        if (!EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(() -> showInFrame(exitOnClose));
            return;
        }

        initGUI();
        okBtn.setVisible(false);
        cancelBtn.setVisible(false);

        JFrame f = new JFrame();
        initMenuBar(f);
        updateTitle(f);
        f.setName("interview.wizard");
        f.setJMenuBar(menuBar);
        f.setContentPane(main);
        f.pack();

        f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                if (interview.isEdited() && !okToContinue()) {
                    return;
                }
                e.getWindow().dispose();
            }

            @Override
            public void windowClosed(WindowEvent e) {
                if (exitOnClose) {
                    System.exit(0);
                }
            }
        });

        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension size = f.getSize();
        f.setLocation(screenSize.width / 2 - size.width / 2, screenSize.height / 2 - size.height / 2);
        f.show();

        window = f;
    }

    /**
     * Show the wizard in a dialog.
     *
     * @param parent     The parent frame for this dialog.
     * @param okListener A listener to e notified when the dialog is dismissed.
     */
    public void showInDialog(final Frame parent, final ActionListener okListener) {
        if (window != null && !(window instanceof JDialog)) {
            throw new IllegalStateException();
        }

        if (!EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(() -> showInDialog(parent, okListener));
            return;
        }

        this.okListener = okListener;

        initGUI();
        okBtn.setVisible(true);
        okBtn.setEnabled(interview.isFinishable());
        cancelBtn.setVisible(true);

        JDialog d = new JDialog(parent);
        initMenuBar(d);
        updateTitle(d);
        d.setJMenuBar(menuBar);
        d.setContentPane(main);
        d.pack();

        d.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        d.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                if (!interview.isEdited() || okToContinue()) {
                    e.getWindow().dispose();
                }
            }

            @Override
            public void windowClosed(WindowEvent e) {
            }
        });

        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension size = d.getSize();
        d.setLocation(screenSize.width / 2 - size.width / 2, screenSize.height / 2 - size.height / 2);
        d.show();

        window = d;
    }

    /**
     * Check if this object is being displayed on the screen.
     *
     * @return true if the wizard is currently being displayed,
     * and false otherwise.
     */
    @Override
    public boolean isShowing() {
        return window != null && window.isShowing();
    }

    /**
     * Ensure that this object is showing in front of all other windows
     * on the screen. If the object is not currently visible, the call
     * has no effect.
     */
    public void toFront() {
        if (window != null) {
            window.toFront();
        }
    }

    /**
     * Initialize the frame's GUI components
     */
    private void initGUI() {

        title = interview.getTitle();
        if (title == null || title.isEmpty()) {
            title = i18n.getString("wizard.defaultTitle");
        }

        //main = new JPanel(new BorderLayout());
        setLayout(new BorderLayout());
        main = this;

        questionPanel = new QuestionPanel(interview);
        questionPanel.setBorder(BorderFactory.createLoweredBevelBorder());
        pathPanel = new PathPanel(questionPanel, interview);

        if (interview.getHelpSet() != null) {
            infoPanel = new InfoPanel(interview);
        }

        buttonPanel = new JToolBar();
        buttonPanel.setFloatable(false);
        //buttonPanel.setBorder(BorderFactory.createRaisedBevelBorder());

        buttonPanel.add(Box.createHorizontalGlue());
        backBtn = createButton("back", "performBack", performer);
        buttonPanel.add(backBtn);
        nextBtn = createButton("next", "performNext", performer);
        buttonPanel.add(nextBtn);
        buttonPanel.addSeparator();
        okBtn = createButton("ok", "performOk", performer);
        buttonPanel.add(okBtn);
        cancelBtn = createButton("cancel", "performCancel", performer);
        buttonPanel.add(cancelBtn);
        if (infoPanel != null) {
            buttonPanel.addSeparator();
            infoBtn = createToggle("info", "performInfo", performer);
            infoBtn.setSelected(initialInfoVisible);
            buttonPanel.add(infoBtn);
        }
        buttonPanel.addAncestorListener(new Listener());

        body = new JPanel(new BorderLayout());
        body.add(pathPanel, BorderLayout.WEST);
        body.add(questionPanel, BorderLayout.CENTER);
        body.add(buttonPanel, BorderLayout.SOUTH);

        body.registerKeyboardAction(performer, "performFindNext", KeyStroke.getKeyStroke("F3"),
                JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        if (helpHelpPrefix == null) {
            helpHelpPrefix = "wizard.";
        }

        if (helpHelpSet == null && infoPanel != null) {
            helpHelpSet = infoPanel.getHelpSet();
        }

        if (helpHelpBroker == null && helpHelpSet != null) {
            helpHelpBroker = new JTHelpBroker();
        }

        if (helpHelpBroker != null && helpHelpSet != null) {
            helpHelpBroker.enableHelpKey(main, helpHelpPrefix + "window.csh");
        }
        if (infoPanel == null) {
            main.add(body);
        } else {
            update(infoBtn.isSelected());
        }
    }

    private void initMenuBar(Window w) {
        menuBar = new JMenuBar();

        fileMenu = createMenu("file", fileMenuData, performer);
        if (w instanceof JFrame) {
            fileMenu.addSeparator();
            fileMenu.add(createMenuItem("file", "exit", "performExit", performer));
        } else {
            fileMenu.addSeparator();
            fileMenu.add(createMenuItem("file", "close", "performCancel", performer));
        }


        if (exporters != null) {
            // replace the default "export log" item with a full export submenu
            for (int i = 0; i < fileMenu.getItemCount(); i++) {
                JMenuItem mi = fileMenu.getItem(i);
                if (mi != null && mi.getActionCommand().equals("performExportLog")) {
                    fileMenu.remove(i);
                    JMenu exportMenu = new ExportMenu(exporters);
                    exportMenu.add(createMenuItem("export", "log", "performExportLog", performer));
                    fileMenu.insert(exportMenu, i);
                    break;
                }
            }
        }
        menuBar.add(fileMenu);

        JMenu searchMenu = createMenu("search", searchMenuData, performer);
        menuBar.add(searchMenu);

        if (helpHelpBroker != null) {
            if (helpMenu == null) {
                helpMenu = createMenu("help", helpMenuData, performer);
            }
            menuBar.add(helpMenu);
        }
    }

    private void update(boolean showInfoPanel) {
        Dimension bodySize = body.getSize();
        if (bodySize.width == 0) {
            bodySize = body.getPreferredSize();
        }

        Dimension infoSize = infoPanel.getSize();
        if (infoSize.width == 0) {
            infoSize = infoPanel.getPreferredSize();
        }
        // need to capture the next value before we remove everything from main
        boolean infoPanelIsShowing = infoPanel.isShowing();

        main.removeAll();

        if (showInfoPanel) {
            // body-help
            JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, body, infoPanel);
            sp.setDividerLocation(bodySize.width + 2);
            main.add(sp);
            infoPanel.setCurrentID(interview.getCurrentQuestion());
        } else {
            // body
            main.add(body);
        }

        if (window != null) {
            int divWidth = new JSplitPane().getDividerSize();
            Dimension winSize = window.getSize();
            int newWidth = winSize.width;
            if (showInfoPanel != infoPanelIsShowing) {
                newWidth += (showInfoPanel ? +1 : -1) * (infoSize.width + divWidth + 4);
            }
            window.setSize(newWidth, winSize.height);
        }

        if (infoBtn.isSelected() != showInfoPanel) {
            infoBtn.setSelected(showInfoPanel);
        }
    }

    private void updateTitle(Window w) {
        String t;
        if (currFile == null
                || (defaultFile != null && currFile.equals(defaultFile))) {
            t = title;
        } else {
            t = i18n.getString("wizard.titleAndFile", title, currFile.getPath());
        }
        if (w instanceof JFrame) {
            ((JFrame) w).setTitle(t);
        } else {
            ((JDialog) w).setTitle(t);
        }
    }

    /**
     * Invoke a performXXX method via reflection
     *
     * @param s The name of the method to be invoked.
     */
    private void perform(String s) {
        try {
            Method m = Wizard.class.getDeclaredMethod(s);
            m.invoke(Wizard.this);
        } catch (IllegalAccessException ex) {
            System.err.println(s);
            ex.printStackTrace();
        } catch (InvocationTargetException ex) {
            System.err.println(s);
            ex.getTargetException().printStackTrace();
        } catch (NoSuchMethodException ex) {
            System.err.println(s);
        }
    }

    /**
     * Handle the "back" action
     */
    private void performBack() {
        try {
            questionPanel.saveCurrentResponse();
            interview.prev();
        } catch (Interview.Fault e) {
            // exception normally means no more questions
            // e.printStackTrace();
        } catch (RuntimeException e) {
            // typically NumberFormatError
            // SEE ALSO QuestionPanel.showInetAddressQuestion
            // which wants to throw Interview.Fault from
            // the value saver, but can't
            questionPanel.getToolkit().beep();
        }
    }

    /**
     * Handle the "cancel" action
     */
    private void performCancel() {
        questionPanel.saveCurrentResponse();
        if (interview.isEdited() && !okToContinue()) {
            return;
        }
        window.dispose();
    }

    /**
     * Handle the "exit" action
     */
    private void performExit() {
        questionPanel.saveCurrentResponse();
        if (interview.isEdited() && !okToContinue()) {
            return;
        }
        // setVisible(false);
        System.exit(0); // uugh
    }

    /**
     * Handle the "exportLog" action
     */
    private void performExportLog() {
        questionPanel.saveCurrentResponse();
        JFileChooser chooser = new JFileChooser();
        if (currFile != null) {
            //  setCurrentDirectory required
            chooser.setCurrentDirectory(new File(currFile.getParent()));
            int dot = currFile.getName().lastIndexOf('.');
            if (dot != -1) {
                File f = new File(currFile.getName().substring(0, dot) + ".html");
                chooser.setSelectedFile(f);
            }
        } else {
            chooser.setCurrentDirectory(getUserDir());
        }
        chooser.setFileFilter(htmlFilter);
        //chooser.addChoosableFileFilter(txtFilter);
        int action = chooser.showDialog(main, i18n.getString("wizard.exportLog"));
        if (action != JFileChooser.APPROVE_OPTION) {
            return;
        }

        File f = ensureExtn(chooser.getSelectedFile(), ".html");
        if (f.exists() && !okToOverwrite(f)) {
            return;
        }
        try {
            Writer out = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8);
            WizPrint w = new WizPrint(interview, interview.getPath());
            w.setShowResponses(true);
            w.write(out);
        } catch (FileNotFoundException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
                    i18n.getString("wizard.fileNotFound.title"),
                    JOptionPane.ERROR_MESSAGE);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.badFile.txt", e.getMessage()),
                    i18n.getString("wizard.badFile.title"),
                    JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Handle the "find" action
     */
    private void performFind() {
        if (searchDialog == null) {
            searchDialog = SearchDialog.create(window, interview, helpHelpBroker, helpHelpPrefix);
        }
        searchDialog.setVisible(true);
    }

    /**
     * Handle the "find" action
     */
    private void performFindNext() {
        if (searchDialog == null) {
            searchDialog = SearchDialog.create(window, interview, helpHelpBroker, helpHelpPrefix);
        }
        searchDialog.find();
    }

    /**
     * Handle the "help" action
     */
    private void performHelp() {
        helpHelpBroker.displayCurrentID(helpHelpPrefix + "intro.csh");
    }

    /**
     * Handle the "info" action
     */
    private void performInfo() {
        boolean infoOn = infoBtn.isSelected();
        if (infoPanel.isShowing() != infoOn) {
            update(infoOn);
            window.validate();
        }
    }

    /**
     * Handle the "new" action
     */
    private void performNew() {
        questionPanel.saveCurrentResponse();
        if (interview.isEdited() && !okToContinue()) {
            return;
        }
        interview.clear();
        interview.setEdited(false);
        setFile(defaultFile);
    }

    /**
     * Handle the "next" action
     */
    private void performNext() {
        try {
            questionPanel.saveCurrentResponse();
            interview.next();
        } catch (Interview.Fault | RuntimeException e) {
            // exception normally means no more questions
            // e.printStackTrace();
            questionPanel.getToolkit().beep();
        }
    }

    /**
     * Handle the "ok" action
     */
    private void performOk() {
        try {
            questionPanel.saveCurrentResponse();
            window.dispose();
            okListener.actionPerformed(new ActionEvent(this,
                    ActionEvent.ACTION_PERFORMED,
                    OK));
        } catch (RuntimeException e) {
            // typically NumberFormatError
            questionPanel.getToolkit().beep();
        }
    }

    /**
     * Handle the "open" action
     */
    private void performOpen() {
        questionPanel.saveCurrentResponse();
        if (interview.isEdited() && !okToContinue()) {
            return;
        }

        JFileChooser chooser = new JFileChooser();
        // set current directory from file or user.dir
        if (currFile != null) {
            // setCurrentDirectory required
            chooser.setCurrentDirectory(new File(currFile.getParent()));
            chooser.setSelectedFile(new File(currFile.getName()));
        } else {
            chooser.setCurrentDirectory(getUserDir());
        }
        chooser.setFileFilter(jtiFilter);
        int action = chooser.showOpenDialog(main);
        if (action != JFileChooser.APPROVE_OPTION) {
            return;
        }
        File f = ensureExtn(chooser.getSelectedFile(), ".jti");
        try {
            open(f);
            setFile(f);
        } catch (Interview.Fault e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.badInterview.txt", e.getMessage()),
                    i18n.getString("wizard.badInterview.title"),
                    JOptionPane.ERROR_MESSAGE);
        } catch (FileNotFoundException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
                    i18n.getString("wizard.fileNotFound.title"),
                    JOptionPane.ERROR_MESSAGE);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.badFile.txt", e.getMessage()),
                    i18n.getString("wizard.badFile.title"),
                    JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Handle the "save" action
     */
    private void performSave() {
        questionPanel.saveCurrentResponse();
        // save with current file
        if (currFile == null) {
            performSaveAs();
        } else {
            performSaveInternal(currFile);
        }
    }

    /**
     * Handle the "save as" action
     */
    private void performSaveAs() {
        questionPanel.saveCurrentResponse();
        JFileChooser chooser = new JFileChooser();
        if (currFile != null) {
            // setCurrentDirectory required
            chooser.setCurrentDirectory(new File(currFile.getParent()));
            chooser.setSelectedFile(new File(currFile.getName()));
        } else {
            chooser.setCurrentDirectory(getUserDir());
        }
        chooser.setFileFilter(jtiFilter);
        int action = chooser.showSaveDialog(main);
        if (action != JFileChooser.APPROVE_OPTION) {
            return;
        }
        File f = ensureExtn(chooser.getSelectedFile(), ".jti");
        if (f.exists() && !okToOverwrite(f)) {
            return;
        }
        performSaveInternal(f);
    }

    /**
     * Internal common routine for the save/saveAs actions
     */
    private void performSaveInternal(File f) {
        try {
            save(f);
            setFile(f);
        } catch (FileNotFoundException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
                    i18n.getString("wizard.fileNotFound.title"),
                    JOptionPane.ERROR_MESSAGE);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(main,
                    i18n.getString("wizard.badFile.txt", e.getMessage()),
                    i18n.getString("wizard.badFile.title"),
                    JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Get the user's current directory
     */
    private File getUserDir() {
        return new File(System.getProperty("user.dir"));
    }

    private JButton createButton(String uiKey, String actionCommand, ActionListener l) {
        JButton b = new JButton(createIcon(uiKey));
        b.setToolTipText(i18n.getString("wizard." + uiKey + ".tip"));
        b.setActionCommand(actionCommand);
        b.addActionListener(l);
        b.registerKeyboardAction(l, actionCommand, enterKey, JComponent.WHEN_FOCUSED);
        return b;
    }

    private Icon createIcon(String uiKey) {
        String iconResource = i18n.getString("wizard." + uiKey + ".icon");
        URL url = getClass().getResource(iconResource);
        return url == null ? null : new ImageIcon(url);
    }

    /**
     * Create a menu according to an array of data
     *
     * @param uiKey the title for the menu
     * @param menuData the data for the menu; one element per menu item; an element can be
     * one of
     * <dl>
     * <dt> null
     * <dd> a separator
     * <dt> an array of two strings
     * <dd> a menu item, whose name is the first string, and whose action is the second
     * </dl>
     */
    private JMenu createMenu(String uiKey, String[][] menuData, ActionListener l) {
        JMenu m = new JMenu(i18n.getString("wizard." + uiKey + ".menu"));
        m.setName("wizard." + uiKey);
        m.setMnemonic(i18n.getString("wizard." + uiKey + ".mne").charAt(0));
        for (String[] data : menuData) {
            if (data == null) {
                m.addSeparator();
            } else {
                JMenuItem mi = createMenuItem(uiKey, data[0], data[1], l);
                if (data.length > 2) {
                    KeyStroke accel = KeyStroke.getKeyStroke(data[2]);
                    mi.setAccelerator(accel);
                }
                m.add(mi);
            }
        }
        return m;
    }

    private JMenuItem createMenuItem(String uiKey, String name, String actionCommand, ActionListener l) {
        JMenuItem item = new JMenuItem(i18n.getString("wizard." + uiKey + "." + name + ".mit"));
        item.setName(name);
        item.setMnemonic(i18n.getString("wizard." + uiKey + "." + name + ".mne").charAt(0));
        item.setActionCommand(actionCommand);
        item.addActionListener(l);
        return item;
    }

    private JToggleButton createToggle(String uiKey, String actionCommand, ActionListener l) {
        JToggleButton b = new JToggleButton(createIcon(uiKey)) {
            @Override
            public Insets getInsets() {
                return nextBtn == null ? super.getInsets() : nextBtn.getInsets(); // !!
            }
        };
        b.setToolTipText(i18n.getString("wizard." + uiKey + ".tip"));
        b.setActionCommand(actionCommand);
        b.addActionListener(l);
        b.registerKeyboardAction(l, actionCommand, enterKey, JComponent.WHEN_FOCUSED);
        return b;
    }

    private File ensureExtn(File f, String extn) {
        if (f.getName().endsWith(extn)) {
            return f;
        } else {
            return new File(f.getPath() + extn);
        }
    }

    private boolean okToContinue() {
        int response =
                JOptionPane.showConfirmDialog(main,
                        i18n.getString("wizard.unsavedAnswers.txt"),
                        i18n.getString("wizard.unsavedAnswers.title"),
                        JOptionPane.YES_NO_OPTION);
        return response == JOptionPane.YES_OPTION;
    }

    private boolean okToOverwrite(File f) {
        int response =
                JOptionPane.showConfirmDialog(main,
                        i18n.getString("wizard.overwrite.txt", f),
                        i18n.getString("wizard.overwrite.title"),
                        JOptionPane.YES_NO_OPTION);
        return response == JOptionPane.YES_OPTION;
    }

    private static class ExtensionFileFilter extends FileFilter {
        private String[] extns;
        private String description;


        ExtensionFileFilter(String extn) {
            this.extns = new String[]{extn};
        }

        ExtensionFileFilter(String... extns) {
            this.extns = extns;
        }

        ExtensionFileFilter(String[] extns, String description) {
            this.extns = extns;
            this.description = description;
        }

        @Override
        public boolean accept(File f) {
            if (f.isDirectory()) {
                return true;
            }
            for (String extn : extns) {
                if (f.getName().endsWith(extn)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String getDescription() {
            if (description == null) {
                StringBuilder sb = new StringBuilder("wizard.extn");
                if (extns.length == 0) {
                    sb.append(".allFiles");
                } else {
                    for (String extn : extns) {
                        sb.append(extn);
                    }
                }
                description = i18n.getString(sb.toString());
            }
            return description;
        }
    }

    private class ExportMenu extends JMenu implements ActionListener, PopupMenuListener {
        ExportMenu(Exporter... exporters) {
            super(i18n.getString("wizard.export.menu"));
            setName("export");
            setMnemonic(i18n.getString("wizard.export.mne").charAt(0));
            for (Exporter exporter : exporters) {
                JMenuItem mi = new JMenuItem(exporter.getName());
                mi.putClientProperty("exporter", exporter);
                mi.setActionCommand("performGenericExport");
                mi.addActionListener(this);
                add(mi);
            }
            getPopupMenu().addPopupMenuListener(this);
        }

        @Override
        public void actionPerformed(ActionEvent ev) {
            questionPanel.saveCurrentResponse();
            JMenuItem mi = (JMenuItem) ev.getSource();
            Exporter e = (Exporter) mi.getClientProperty("exporter");
            export(e);
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent ev) {
        }

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
            JPopupMenu m = (JPopupMenu) ev.getSource();
            for (int i = 0; i < m.getComponentCount(); i++) {
                JMenuItem mi = (JMenuItem) m.getComponent(i);
                if (mi != null) {
                    Exporter e = (Exporter) mi.getClientProperty("exporter");
                    if (e != null) {
                        mi.setEnabled(e.isExportable());
                    }
                }
            }
        }

        private void export(Exporter e) {
            JFileChooser exportChooser = new JFileChooser();
            if (currFile != null) {
                // setCurrentDirectory required
                exportChooser.setCurrentDirectory(new File(currFile.getParent()));
                String[] extns = e.getFileExtensions();
                int dot = currFile.getName().lastIndexOf('.');
                if (dot != -1 && extns != null && extns.length > 0) {
                    File f = new File(currFile.getName().substring(0, dot) + extns[0]);
                    exportChooser.setSelectedFile(f);
                }
            } else {
                exportChooser.setCurrentDirectory(getUserDir());
            }
            exportChooser.setApproveButtonText(i18n.getString("wizard.exportChooser.export"));
            String[] extns = e.getFileExtensions();
            String desc = e.getFileDescription();
            exportChooser.setFileFilter(new ExtensionFileFilter(extns, desc));
            int action = exportChooser.showSaveDialog(main);
            if (action != JFileChooser.APPROVE_OPTION) {
                return;
            }
            File f = ensureExtn(exportChooser.getSelectedFile(), extns[0]);
            if (f.exists() && !okToOverwrite(f)) {
                return;
            }
            e.export(f);

        }
    }

    private class Listener implements AncestorListener, Interview.Observer {
        // ---------- from AncestorListener -----------

        @Override
        public void ancestorAdded(AncestorEvent e) {
            interview.addObserver(this);
            pathUpdated();
            currentQuestionChanged(interview.getCurrentQuestion());
        }

        @Override
        public void ancestorMoved(AncestorEvent e) {
        }

        @Override
        public void ancestorRemoved(AncestorEvent e) {
            interview.removeObserver(this);
        }

        //----- from Interview.Observer -----------

        @Override
        public void pathUpdated() {
            okBtn.setEnabled(interview.isFinishable());
        }

        @Override
        public void currentQuestionChanged(Question q) {
            backBtn.setEnabled(!interview.isFirst(q));
            nextBtn.setEnabled(!interview.isLast(q));
        }
    }
}
