/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  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.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.exec;

import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.File;
import java.util.Arrays;
import java.util.Map;

import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.sun.interview.Interview;
import com.sun.interview.Question;
import com.sun.javatest.InterviewParameters;
import com.sun.javatest.InitialUrlFilter;
import com.sun.javatest.Keywords;
import com.sun.javatest.KeywordsFilter;
import com.sun.javatest.Parameters.ExcludeListParameters;
import com.sun.javatest.Parameters.MutableExcludeListParameters;
import com.sun.javatest.Status;
import com.sun.javatest.StatusFilter;
import com.sun.javatest.TestFilter;
import com.sun.javatest.TestDescription;
import com.sun.javatest.TestResultTable;
import com.sun.javatest.TestSuite;
import com.sun.javatest.ObservableTestFilter;
import com.sun.javatest.ObservableTestFilter.Observer;
import com.sun.javatest.util.DynamicArray;
import com.sun.javatest.util.StringArray;
import com.sun.javatest.tool.TestTreeSelectionPane;
import com.sun.javatest.tool.UIFactory;
import java.util.HashSet;

/**
 * This filter allows the user to configure the filter using
 * the filtering attributes normally found in the parameter
 * section of an interview:
 * <ul>
 * <li>Initial URLs
 * <li>Status
 * <li>Exclude List
 * <li>Keywords
 * </ul>
 *
 *<p>
 * The settings for this panel are global, so any changes made affect all
 * exec tool instances.
 */
class BasicCustomTestFilter extends ConfigurableTestFilter {
    // UIFactory parameter assume this class stays in the exec package
    BasicCustomTestFilter(String name, ExecModel e, UIFactory uif) {
        super(name, e);
        this.uif = uif;
        init(null);
    }

    BasicCustomTestFilter(Map map, ExecModel e, UIFactory uif) {
        super(map, e);
        this.uif = uif;
        init(map);
    }

    BasicCustomTestFilter(ExecModel e, UIFactory uif) {
        super(uif.getI18NString("basicTf.namePrefix"), e);
        this.uif = uif;

        init(null);
    }

    ConfigurableTestFilter cloneInstance() {
        return new BasicCustomTestFilter(uif.getI18NString("basicTf.namePrefix") +
                                         instanceCount, execModel, uif);
    }

    // override superclass methods
    // observers must be static data since all exec tool instances
    // must be notified of changes
    public void addObserver(Observer o) {
        obs = (Observer[])DynamicArray.append(obs, o);
    }

    public void removeObserver(Observer o) {
        obs = (Observer[])DynamicArray.remove(obs, o);
    }

    protected void notifyUpdated(ObservableTestFilter filter) {
        // obs will be null if called indirectly via load(map) from the
        // superclass constructor, because super(...) is defined to be
        // called before instance variables are initialized.
        // (Hence the danger of overriding methods called from a superclass
        // constructor.)  11/12/02
        if (obs == null)
            return;

        for (int i = 0; i < obs.length; i++)
            obs[i].filterUpdated(filter);
    }

    boolean load(Map map) {
        boolean result = super.load(map);
        activeSettings = new SettingsSnapshot(map);
        putSettings(activeSettings);
        activateSettings(activeSettings);

        notifyUpdated(this);

        return result;
    }

    boolean save(Map map) {
        boolean result = super.save(map);
        activeSettings.save(map);

        return result;
    }

    synchronized JComponent getEditorPane() {
        if (editorPane == null) {
            editorPane = uif.createTabbedPane("basicTf.tabs", createTabPanels());
            editorPane.setTabPlacement(SwingConstants.TOP);

            try {
                if (activeSettings == null)
                    activeSettings = grabSettings();
                else
                    putSettings(activeSettings);
            }
            catch (IllegalStateException e) {
                throw new IllegalStateException("Illegal state of BCTF GUI on startup.");
            }   // catch

            editorPane.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
        }

        return editorPane;
    }

    String commitEditorSettings() {
        SettingsSnapshot nowSettings = null;

        try {
            nowSettings = grabSettings();
        }
        catch (IllegalStateException e) {
            // indicates that the GUI settings are not valid
            return e.getMessage();
        }   // catch

        if (!activeSettings.equals(nowSettings)) {
            return activateSettings(nowSettings);
        }
        else {
            return null;
        }
    }

    void resetEditorSettings() {
        // set GUI to match live settings
        putSettings(activeSettings);
    }

    boolean isEditorChanged() {
        SettingsSnapshot nowSettings = null;

        try {
            nowSettings = grabSettings();
        }
        catch (IllegalStateException e) {
            // indicates that the GUI settings are not valid
            return true;
        }   // catch
        if (activeSettings.equals(nowSettings))
            return false;
        else
            return true;
    }

    // TestFilter interface
    public boolean accepts(TestDescription td)
                throws TestFilter.Fault {
        return accepts(td, null);
    }

    public boolean accepts(TestDescription td, TestFilter.Observer o)
                throws TestFilter.Fault {
        for (int i = 0; i < activeFilters.length; i++) {
            if (activeFilters[i] != null &&
                !activeFilters[i].accepts(td)) {
                if (o != null)
                    o.rejected(td, activeFilters[i]);
                else { }

                return false;
            }   // outer if
        }   // for

        return true;
    }

    public String getBaseName() {
        return NAME;
    }

    public String getName() {
        return instanceName;
    }

    public String getReason() {
        return REASON;
    }

    public String getDescription() {
        return DESCRIPTION;
    }

    // ----- PRIVATE -----

    private void init(Map map) {
        if (NAME == null)
            NAME = uif.getI18NString("basicTf.name");

        if (REASON == null)
            REASON = uif.getI18NString("basicTf.reason");

        if (DESCRIPTION == null)
            DESCRIPTION = uif.getI18NString("basicTf.description");

        // assignment done in load() -- 11/12/02
        // activeFilters = new TestFilter[NUM_FILTERS];

        if (map != null)
            activeSettings = new SettingsSnapshot(map);
        else
            activeSettings = new SettingsSnapshot();

        instanceCount++;

        // not using this from superclass
        observers = null;
        activateSettings(activeSettings);
    }

    /**
     * Make the given settings the active ones.
     * Except under exceptional conditions, the filters are recreated and
     * an update message is sent.
     */
    private String activateSettings(SettingsSnapshot nowSettings) {
        KeywordsFilter newKeyFilter = null;
        InitialUrlFilter newUrlFilter = null;
        TestFilter newJtxFilter = null;
        StatusFilter newStatusFilter = null;
        TestFilter newTsfFilter = null;

        InterviewParameters ip = execModel.getInterviewParameters();
        TestSuite ts = execModel.getTestSuite();
        updateInterviewObserver(ip);

        // recreate filters
        if (nowSettings.urlsEnabled) {
            // converting to strings to avoid any confusion
            // files vs urls
            if (nowSettings.initialUrls != null) {
               newUrlFilter = new InitialUrlFilter(nowSettings.initialUrls);
            }

        }

        if (nowSettings.keywordsEnabled) {
            try {
                String[] validKeywords = ts.getKeywords();
                HashSet validKeywordsSet;
                if (validKeywords == null)
                    validKeywordsSet = null;
                else
                    validKeywordsSet = new HashSet(Arrays.asList(validKeywords));

                Keywords kw = Keywords.create(kwModeToType(nowSettings.keyChoice),
                                    nowSettings.keyString, validKeywordsSet);
                newKeyFilter = new KeywordsFilter(kw);
            }
            catch (Keywords.Fault f) {
                return f.getMessage();
            }

        }

        if (nowSettings.statusEnabled) {
            // this filter won't work without a TRT
            TestResultTable trt = execModel.getActiveTestResultTable();
            if (trt != null) {
                newStatusFilter = new StatusFilter(
                                nowSettings.statusFields,
                                trt);
            }
            else { }
        }
        else { }

        if (nowSettings.jtxEnabled) {
            // we only support copying exclude list from interview
            // right now
            if (ip != null) {
                // may set var. to null, but that's okay
                newJtxFilter = ip.getExcludeListFilter();
            }
        }

        if (nowSettings.tsfEnabled) {
            // we only support copying exclude list from interview
            // right now
            if (ip != null && ts != null) {
                // may set filter to null, but that's okay
                newTsfFilter = ts.createTestFilter(ip.getEnv());
            }
        }

        // can abort anytime before this
        // commit now
        keyFilter = newKeyFilter;
        urlFilter = newUrlFilter;
        jtxFilter = newJtxFilter;
        statusFilter = newStatusFilter;
        tsfFilter = newTsfFilter;

        // initialize the array if not yet done so
        if (activeFilters == null)
            activeFilters = new TestFilter[NUM_FILTERS];

        activeFilters[0] = keyFilter;
        activeFilters[1] = urlFilter;
        activeFilters[2] = jtxFilter;
        activeFilters[3] = statusFilter;
        activeFilters[4] = tsfFilter;

        activeSettings = nowSettings;
        updateExcludeInfo();

        // notify observers
        notifyUpdated(this);

        return null;
    }

    /**
     * Attach observer to the given interview.
     * Automatically detaches from other interviews being observered.
     * @param ip The interview to observe.  For convienience, if this param
     *        is null, any current observers will be detached.
     */
    private void updateInterviewObserver(InterviewParameters ip) {
        if (ip == null && intObs != null)
            intObs.getInterview().removeObserver(intObs);
        else if (intObs == null || intObs.getInterview() != ip) {
            if (intObs != null)
                intObs.getInterview().removeObserver(intObs);

            if (ip != null) {
                intObs = new InterviewObserver(ip);
                ip.addObserver(intObs);
            }
        }
    }

    private void updateExcludeInfo() {
        // check to see if GUI is init-ed
        if (jtxMode == null)
            return;

        InterviewParameters ip = execModel.getInterviewParameters();

        if (ip == null)     // nothing to do
            return;

        boolean isUnknown = true;
        ExcludeListParameters elp = ip.getExcludeListParameters();

        if (ip != null) {
            elp = ip.getExcludeListParameters();
            if (elp instanceof MutableExcludeListParameters) {
                MutableExcludeListParameters melp = (MutableExcludeListParameters)elp;
                int mode = melp.getExcludeMode();
                switch (mode) {
                case MutableExcludeListParameters.NO_EXCLUDE_LIST:
                    jtxMode.setText(uif.getI18NString("basicTf.exclude.mode.none"));
                    setExcludeFiles(null);
                    isUnknown = false;
                    break;
                case MutableExcludeListParameters.INITIAL_EXCLUDE_LIST:
                    jtxMode.setText(uif.getI18NString("basicTf.exclude.mode.initial"));
                    setExcludeFiles(melp.getExcludeFiles());
                    isUnknown = false;
                    break;
                case MutableExcludeListParameters.LATEST_EXCLUDE_LIST:
                    jtxMode.setText(uif.getI18NString("basicTf.exclude.mode.latest"));
                    setExcludeFiles(null);
                    isUnknown = false;
                    break;
                case MutableExcludeListParameters.CUSTOM_EXCLUDE_LIST:
                    jtxMode.setText(uif.getI18NString("basicTf.exclude.mode.custom"));
                    setExcludeFiles(melp.getCustomExcludeFiles());
                    isUnknown = false;
                    break;
                }   // switch

            }
        }

        // done here to avoid duplicating code above
        if (isUnknown) {
            jtxMode.setText(uif.getI18NString("basicTf.exclude.mode.unknown"));
            jtxFiles.removeAllElements();
        }
    }

    /**
     * Adds exclude list files to the list.
     * If null or zero length, the list is cleared.
     * @param files The files to add.  Null ok.
     */
    private void setExcludeFiles(File[] files) {
        jtxFiles.removeAllElements();

        if (files == null || files.length == 0)
            return;
        else
            for (int i = 0; i < files.length; i++)
                jtxFiles.addElement(files[i].getPath());
    }

    private JComponent[] createTabPanels() {
        JComponent[] items = {createTestsPanel(), createKeywordPanel(),
                              createStatusPanel(), createExcludePanel(),
                              createSpecialPanel()};

        return items;
    }

    // KEYWORD PANEL
    private JComponent createKeywordPanel() {
        JPanel p = uif.createPanel(
                    "basicTf.keywords.mainPanel", new GridBagLayout(), false);
        p.setName("keywords");

        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.fill = GridBagConstraints.HORIZONTAL;
        c.weightx = 1;

        keyBtnGrp = new ButtonGroup();
        keyAllBtn = uif.createRadioButton("basicTf.keywords.all", keyBtnGrp);
        keyAllBtn.setMnemonic(uif.getI18NString("basicTf.keywords.all.mne").charAt(0));
        p.add(keyAllBtn, c);

        keyMatchBtn = uif.createRadioButton("basicTf.keywords.match", keyBtnGrp);
        keyMatchBtn.setMnemonic(uif.getI18NString("basicTf.keywords.match.mne").charAt(0));
        keyMatchBtn.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                enableKeywordFields();
            }
        });
        c.weightx = 0;
        c.gridwidth = 1;
        p.add(keyMatchBtn, c);

        String[] kc = {ANY_OF, ALL_OF, EXPR};
        keywordsChoice = uif.createChoice("basicTf.keywords.choice", kc);
        p.add(keywordsChoice, c);

        keywordsField = uif.createInputField("basicTf.keywords.field", 20);
        keywordsField.setEditable(true);
        uif.setAccessibleInfo(keywordsField, "basicTf.keywords.field");
        c.fill = GridBagConstraints.HORIZONTAL;
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.weightx = 1;
        p.add(keywordsField, c);

        keyAllBtn.setSelected(true);
        enableKeywordFields();
        return p;
    }

    private void enableKeywordFields() {
        boolean b = keyMatchBtn.isSelected();
        keywordsChoice.setEnabled(b);
        keywordsField.setEnabled(b);
    }

    // STATUS PANEL
    private JComponent createStatusPanel() {
        JPanel p = uif.createPanel(
                "basicTf.status.mainPanel", new GridBagLayout(), false);
        p.setName("status");

        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.WEST;
        c.gridwidth = GridBagConstraints.REMAINDER;

        statusBtnGrp = new ButtonGroup();
        statusAllBtn = uif.createRadioButton("basicTf.status.all", statusBtnGrp);
        statusAllBtn.setMnemonic(uif.getI18NString("basicTf.status.all.mne").charAt(0));
        p.add(statusAllBtn, c);

        statusAnyOfBtn = uif.createRadioButton("basicTf.status.anyOf", statusBtnGrp);
        statusAnyOfBtn.setMnemonic(uif.getI18NString("basicTf.status.anyOf.mne").charAt(0));
        statusAnyOfBtn.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                enableStatusFields();
            }
        });
        c.gridwidth = 1;
        c.weightx = 0;
        p.add(statusAnyOfBtn, c);

        JPanel row = new JPanel(new GridBagLayout());
        row.setBorder(BorderFactory.createEtchedBorder());
        GridBagConstraints rc = new GridBagConstraints();
        rc.insets.left = 10;

        statusChecks[Status.PASSED] =
            uif.createCheckBox("basicTf.status.prev.passed", false);
        row.add(statusChecks[Status.PASSED], rc);

        statusChecks[Status.FAILED] =
            uif.createCheckBox("basicTf.status.prev.failed", true);
        row.add(statusChecks[Status.FAILED], rc);

        statusChecks[Status.ERROR] =
            uif.createCheckBox("basicTf.status.prev.error", true);
        row.add(statusChecks[Status.ERROR], rc);

        rc.insets.right = 10;
        statusChecks[Status.NOT_RUN] =
            uif.createCheckBox("basicTf.status.prev.notRun", true);
        row.add(statusChecks[Status.NOT_RUN], rc);
        uif.setToolTip(row, "basicTf.status.prev");

        statusAllBtn.setSelected(true);
        enableStatusFields();
        p.add(row, c);

        return p;
    }

    private void enableStatusFields() {
        boolean enable = statusAnyOfBtn.isEnabled() && statusAnyOfBtn.isSelected();
        for (int i = 0; i < statusChecks.length; i++) {
            statusChecks[i].setEnabled(enable);
        }
    }

    // INIT URLS
    private JComponent createTestsPanel() {
        final JPanel p = uif.createPanel(
                "basicTf.tests.mainPanel", new BorderLayout(), false);
        p.setName("tests");

        // more configure ...
        lastTrt = execModel.getActiveTestResultTable();
        testsField = new TestTreeSelectionPane(lastTrt);

        p.add(testsField, BorderLayout.CENTER);

        p.addComponentListener(new ComponentAdapter() {
            public void componentShown(ComponentEvent e) {
                TestResultTable nowTrt = execModel.getActiveTestResultTable();
                // replaces widget with an updated one
                // only really important if the test suite contents (structure)
                // changes
                // this code basically keeps it in sync with the main tree,
                // otherwise it might be left behind watching a temporary TRT
                if (nowTrt != lastTrt) {
                    TestTreeSelectionPane newTree = new TestTreeSelectionPane(nowTrt);
                    String[] paths = testsField.getSelection();     // save
                    p.remove(testsField);
                    testsField = newTree;
                    testsField.setSelection(paths);                 // restore
                    p.add(testsField, BorderLayout.CENTER);
                    lastTrt = nowTrt;
                }
            }
        });

        return p;
    }

    // JTX lists
    private JComponent createExcludePanel() {
        JPanel p = new JPanel(new GridBagLayout());
        p.setName("exclude");

        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.NORTHWEST;
        c.ipadx = 10;
        c.gridwidth = 1;
        c.gridx = 0;
        c.gridy= 0;
        c.fill = GridBagConstraints.NONE;

        // column 1
        c.gridwidth = 3;
        jtxCheckBox = uif.createCheckBox("basicTf.exclude", false);
        jtxCheckBox.setMnemonic(uif.getI18NString("basicTf.exclude.mne").charAt(0));
        p.add(jtxCheckBox, c);

        // create some margins
        c.gridy = 1;
        p.add(Box.createVerticalStrut(5), c);

        c.gridy = 2;
        c.gridwidth = 1;

        c.gridheight = 2;
        p.add(Box.createHorizontalStrut(8), c);

        c.gridheight = 1;
        c.gridx = 2;

        // labels
        final JLabel modeLab = uif.createLabel("basicTf.exclude.mode");
        modeLab.setDisplayedMnemonic(uif.getI18NString("basicTf.exclude.mode.mne").charAt(0));
        modeLab.setEnabled(jtxCheckBox.isSelected());
        p.add(modeLab, c);

        c.gridy = 3;
        final JLabel fileLab = uif.createLabel("basicTf.exclude.file");
        fileLab.setDisplayedMnemonic(uif.getI18NString("basicTf.exclude.file.mne").charAt(0));
        fileLab.setEnabled(jtxCheckBox.isSelected());
        p.add(fileLab, c);

        // column 2
        c.gridy = 2;
        c.gridx = 3;
        c.fill = GridBagConstraints.HORIZONTAL;
        jtxMode = uif.createOutputField("basicTf.exclude.mode", modeLab);
        jtxMode.setBorder(BorderFactory.createEmptyBorder());
        jtxMode.setEditable(false);
        jtxMode.setEnabled(jtxCheckBox.isSelected());
        uif.setAccessibleInfo(jtxMode, "basicTf.exclude.mode");
        p.add(jtxMode, c);

        c.gridy = 3;
        c.weightx = 2;
        c.weighty = 2;
        c.fill = GridBagConstraints.BOTH;
        jtxFiles = new DefaultListModel();
        jtxFileList = uif.createList("basicTf.exclude.file", jtxFiles);
        jtxFileList.setEnabled(jtxCheckBox.isSelected());
        uif.setAccessibleInfo(jtxFileList, "basicTf.exclude.file");
        fileLab.setLabelFor(jtxFileList);

        // might need to add a scroll panel here
        p.add(uif.createScrollPane(jtxFileList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
              JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), c);

        jtxCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                jtxFileList.setEnabled(jtxCheckBox.isSelected());
                jtxMode.setEnabled(jtxCheckBox.isSelected());
                modeLab.setEnabled(jtxCheckBox.isSelected());
                fileLab.setEnabled(jtxCheckBox.isSelected());
            }
        });

        return p;
    }

    // test suite specific filter
    private JComponent createSpecialPanel() {
        String k = "basicTf.tsf";
        JPanel p = uif.createPanel("basicTf.tsf", new BorderLayout(), false);
        p.setName("special");

        tsfCheckBox = uif.createCheckBox("basicTf.tsf", false);
        tsfCheckBox.setMnemonic(uif.getI18NString("basicTf.tsf.mne").charAt(0));
        p.add(tsfCheckBox, BorderLayout.CENTER);

        return p;
    }

    /**
     * Capture the settings in the current GUI.
     * @return A snapshot of the settings currently entred into the GUI.
     * @throws IllegalStateException If any part of the GUI has a state whcih
     *         does not produce a consisitent setting.  The message of this
     *         exception wil contains an internationalized message to help the
     *         user resolve the problem.
     */
    private SettingsSnapshot grabSettings() {
        SettingsSnapshot shot = new SettingsSnapshot();

        // grad top level togggle state of each type
        shot.keywordsEnabled = !keyAllBtn.isSelected();
        //shot.urlsEnabled = !allTestsBtn.isSelected();
        shot.urlsEnabled = true;        // always enabled in current impl.
        shot.statusEnabled = statusAnyOfBtn.isSelected();
        shot.jtxEnabled = jtxCheckBox.isSelected();
        shot.tsfEnabled = tsfCheckBox.isSelected();

        // grab status checkbox state
        boolean oneSelected = false;
        shot.statusFields = new boolean[statusChecks.length];
        for (int i = 0; i < statusChecks.length; i++) {
            shot.statusFields[i] = statusChecks[i].isSelected();
            oneSelected = oneSelected || shot.statusFields[i];
        }

        if (shot.statusEnabled && !oneSelected)
            throw new IllegalStateException(uif.getI18NString("basicTf.badStatus"));

        // init. urls
        shot.initialUrls = testsField.getSelection();

        // special case post processing for root selection
        // if length is one and that string is zero length, that indicates
        // a root selection
        if (shot.initialUrls != null &&
            shot.initialUrls.length == 1 &&
            shot.initialUrls[0].length() == 0)
            shot.initialUrls = null;

        // keywords
        shot.keyChoice = (String)(keywordsChoice.getSelectedItem());
        shot.keyString = keywordsField.getText();

        return shot;
    }

    /**
     * Put a set of settings into the GUI.
     */
    private void putSettings(SettingsSnapshot s) {
        // no GUI, don't do anything
        if (editorPane == null)
            return;

        keyAllBtn.setSelected(!s.keywordsEnabled);
        keyMatchBtn.setSelected(s.keywordsEnabled);

        statusAllBtn.setSelected(!s.statusEnabled);
        statusAnyOfBtn.setSelected(s.statusEnabled);

        jtxCheckBox.setSelected(s.jtxEnabled);
        tsfCheckBox.setSelected(s.tsfEnabled);

        for (int i = 0; i < statusChecks.length; i++)
            statusChecks[i].setSelected(s.statusFields[i]);

        testsField.setSelection(s.initialUrls);

        keywordsChoice.setSelectedItem(s.keyChoice);
        keywordsField.setText(s.keyString);

        updateExcludeInfo();
    }

    // Utility methods
    private static String kwModeToType(String mode) {
        if (mode == ALL_OF)
            return "all of";
        else if (mode == ANY_OF)
            return "any of";
        else
            return EXPR;
    }

    private Observer[] obs = new Observer[0];
    private static int instanceCount;
    private TestResultTable lastTrt;
    private InterviewObserver intObs;

    private static int NUM_FILTERS = 5;
    private SettingsSnapshot activeSettings;
    private TestFilter[] activeFilters;     // for convienience
    private KeywordsFilter keyFilter;
    private InitialUrlFilter urlFilter;
    private TestFilter jtxFilter;
    private StatusFilter statusFilter;
    private TestFilter tsfFilter;           // test suite filter

    private final UIFactory uif;
    private JTabbedPane editorPane;

    // keyword info
    private ButtonGroup keyBtnGrp;
    private JRadioButton keyAllBtn;
    private JRadioButton keyMatchBtn;
    private JComboBox keywordsChoice;
    private JTextField keywordsField;

    private static final String ALL_OF = "allOf";
    private static final String ANY_OF = "anyOf";
    private static final String EXPR   = "expr";

    // status info
    private ButtonGroup statusBtnGrp;
    private JRadioButton statusAllBtn;
    private JRadioButton statusAnyOfBtn;
    private JCheckBox[] statusChecks = new JCheckBox[Status.NUM_STATES];

    // tests info
    private ButtonGroup testsBtnGrp;
    //private JRadioButton allTestsBtn;
    //private JRadioButton selectTestsBtn;
    private TestTreeSelectionPane testsField;

    // checkboxes to enable exclude list from interview and test suite filter
    private JCheckBox jtxCheckBox;
    private JCheckBox tsfCheckBox;

    // jtx info fields
    private JTextField jtxMode;
    private JList jtxFileList;
    private DefaultListModel jtxFiles;

    private static String NAME, REASON, DESCRIPTION;

    /**
     * Necessary to track changes which occur in the active interview.
     */
    private class InterviewObserver implements Interview.Observer {
        InterviewObserver(InterviewParameters i) {
            interview = i;
        }

        InterviewParameters getInterview() {
            return interview;
        }

        public void currentQuestionChanged(Question q) {
            // ignore
        }

        public void pathUpdated() {
            TestFilter underTest;
            boolean needsUpdate = false;
            updateExcludeInfo();

            // determine if jtx and tsf filters were updated
            if (activeSettings.jtxEnabled) {
                // we only support copying exclude list from interview
                // right now
                InterviewParameters ip = execModel.getInterviewParameters();

                if (ip != null) {
                    // may set var. to null, but that's okay
                    underTest = ip.getExcludeListFilter();
                    if (underTest != null &&
                        (jtxFilter == null || !underTest.equals(jtxFilter)))
                        needsUpdate = true;
                    else if (jtxFilter != null)     // jtx filter now null
                        needsUpdate = true;
                }
            }

            if (needsUpdate) {
                activateSettings(activeSettings);
                return;     // rest of checking no longer needed
            }

            if (activeSettings.tsfEnabled) {
                // we only support copying exclude list from interview
                // right now
                InterviewParameters ip = execModel.getInterviewParameters();
                TestSuite ts = execModel.getTestSuite();

                if (ip != null && ts != null) {
                    underTest = ts.createTestFilter(ip.getEnv());
                    if (underTest != null)
                        if (tsfFilter == null || !underTest.equals(tsfFilter))
                            needsUpdate = true;
                        else { }
                    else if (tsfFilter != null)     // tsf filter now null
                        needsUpdate = true;
                    else { }
                }
                else { }
            }

            if (needsUpdate) {
                activateSettings(activeSettings);
                return;     // rest of checking no longer needed
            }
        }

        private InterviewParameters interview;
    }

    private class SettingsSnapshot {
        SettingsSnapshot() {
            statusFields = new boolean[Status.NUM_STATES];
            urlsEnabled = true;
            keyChoice = EXPR;
            keyString = "";
        }

        SettingsSnapshot(Map m) {
            this();
            load(m);
        }

        public boolean equals(Object settings) {
            SettingsSnapshot incoming = (SettingsSnapshot)settings;

            if (!Arrays.equals(initialUrls, incoming.initialUrls))
                return false;

            if (keywordsEnabled != incoming.keywordsEnabled)
                return false;
            else {
                if (keyChoice != incoming.keyChoice)
                    return false;

                // destination of structure is to handle null values
                if (keyString == null) {
                    if (keyString != incoming.keyString)
                        return false;
                    else { }
                }
                else {
                    if (!keyString.equals(incoming.keyString))
                        return false;
                    else { }
                }
            }   // outer else

            if (statusEnabled != incoming.statusEnabled)
                return false;
            else
                if (!Arrays.equals(statusFields, incoming.statusFields))
                    return false;

            if (jtxEnabled != incoming.jtxEnabled)
                return false;

            if (tsfEnabled != incoming.tsfEnabled)
                return false;

            return true;
        }   // equals()

        void save(Map map) {
            map.put(MAP_URL_ENABLE, booleanToInt(urlsEnabled));
            map.put(MAP_KEY_ENABLE, booleanToInt(keywordsEnabled));
            map.put(MAP_STATUS_ENABLE, booleanToInt(statusEnabled));
            map.put(MAP_JTX_ENABLE, booleanToInt(jtxEnabled));
            map.put(MAP_TSF_ENABLE, booleanToInt(tsfEnabled));

            for (int i = 0; i < statusFields.length; i++)
                map.put(MAP_STATUS_PREFIX + i, booleanToInt(statusFields[i]));

            map.put(MAP_URLS, StringArray.join(initialUrls));

            map.put(MAP_KEY_CHOICE, keyChoice);
            map.put(MAP_KEY_STRING, keyString);
        }

        void load(Map map) {
            urlsEnabled = intToBoolean((String)(map.get(MAP_URL_ENABLE)));
            keywordsEnabled = intToBoolean((String)(map.get(MAP_KEY_ENABLE)));
            statusEnabled = intToBoolean((String)(map.get(MAP_STATUS_ENABLE)));
            jtxEnabled = intToBoolean((String)(map.get(MAP_JTX_ENABLE)));
            tsfEnabled = intToBoolean((String)(map.get(MAP_TSF_ENABLE)));

            for (int i = 0; i < Status.NUM_STATES; i++) {
                statusFields[i] = intToBoolean((String)(map.get(MAP_STATUS_PREFIX + i)));
            }   // for

            initialUrls = StringArray.split((String)(map.get(MAP_URLS)));

            keyChoice = (String)(map.get(MAP_KEY_CHOICE));
            keyString = (String)(map.get(MAP_KEY_STRING));

            validate();
        }

        private void validate() {
            // conserve strings, but also sync with the actual
            // objects in the choice list for easy use
            if (keyChoice.equals(ALL_OF))
                keyChoice = ALL_OF;
            else if (keyChoice.equals(ANY_OF))
                keyChoice = ANY_OF;
            else if (keyChoice.equals(EXPR))
                keyChoice = EXPR;
            else
                keyChoice = ALL_OF;     // unknown, use default
        }

        String booleanToInt(boolean val) {
            if (val)
                return "1";
            else
                return "0";
        }

        boolean intToBoolean(String num) {
            if (num == null)
                return false;

            if (num.equals("1"))
                return true;
            else
                return false;
        }

        boolean urlsEnabled;
        boolean keywordsEnabled;
        boolean statusEnabled;
        boolean jtxEnabled;
        boolean tsfEnabled;

        boolean[] statusFields;

        String[] initialUrls;
        String keyChoice;
        String keyString;

        private static final String MAP_URL_ENABLE = "urlsEnabled";
        private static final String MAP_KEY_ENABLE = "keyEnabled";
        private static final String MAP_STATUS_ENABLE = "statusEnable";
        private static final String MAP_JTX_ENABLE = "jtxEnable";
        private static final String MAP_TSF_ENABLE = "tsfEnable";
        private static final String MAP_URLS = "urls";
        private static final String MAP_KEY_CHOICE = "keyChoice";
        private static final String MAP_KEY_STRING = "keyString";
        private static final String MAP_STATUS_PREFIX = "status";

    }

}
