/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2006, University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307, USA
 */

package edu.umd.cs.findbugs.gui2;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;

import edu.umd.cs.findbugs.BugAnnotation;
import edu.umd.cs.findbugs.BugCollection;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugPattern;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.FindBugsDisplayFeatures;
import edu.umd.cs.findbugs.IGuiCallback;
import edu.umd.cs.findbugs.L10N;
import edu.umd.cs.findbugs.Project;
import edu.umd.cs.findbugs.ProjectPackagePrefixes;
import edu.umd.cs.findbugs.SortedBugCollection;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.cloud.Cloud;
import edu.umd.cs.findbugs.cloud.Cloud.CloudListener;
import edu.umd.cs.findbugs.cloud.Cloud.SigninState;
import edu.umd.cs.findbugs.filter.Filter;
import edu.umd.cs.findbugs.log.ConsoleLogger;
import edu.umd.cs.findbugs.log.LogSync;
import edu.umd.cs.findbugs.log.Logger;
import edu.umd.cs.findbugs.sourceViewer.NavigableTextPane;
import edu.umd.cs.findbugs.util.Multiset;

@SuppressWarnings("serial")
/*
 * This is where it all happens... seriously... all of it... All the menus are
 * set up, all the listeners, all the frames, dockable window functionality
 * There is no one style used, no one naming convention, its all just kinda
 * here. This is another one of those classes where no one knows quite why it
 * works. <p> The MainFrame is just that, the main application window where just
 * about everything happens.
 */
public class MainFrame extends FBFrame implements LogSync {
    public static final boolean GUI2_DEBUG = SystemProperties.getBoolean("gui2.debug");

    public static final boolean MAC_OS_X = SystemProperties.getProperty("os.name").toLowerCase().startsWith("mac os x");

    private static final int SEARCH_TEXT_FIELD_SIZE = 32;

    public static final String TITLE_START_TXT = "FindBugs";

    private final static String WINDOW_MODIFIED = "windowModified";

    public static final boolean USE_WINDOWS_LAF = false;

    private static MainFrame instance;

    private final MyGuiCallback guiCallback = new MyGuiCallback();

    private BugCollection bugCollection;

    private BugAspects currentSelectedBugAspects;

    private volatile Project curProject = new Project();

    private volatile boolean newProject = false;

    private final ProjectPackagePrefixes projectPackagePrefixes = new ProjectPackagePrefixes();

    private Logger logger = new ConsoleLogger(this);

    @CheckForNull
    private File saveFile = null;

    private CloudListener userAnnotationListener = new MyCloudListener();

    private Cloud.CloudStatusListener cloudStatusListener = new MyCloudStatusListener();

    private ExecutorService backgroundExecutor = Executors.newCachedThreadPool();

    private final CountDownLatch mainFrameInitialized = new CountDownLatch(1);

    private int waitCount = 0;

    private final Object waitLock = new Object();

    private final Runnable updateStatusBarRunner = new StatusBarUpdater();

    private volatile String errorMsg = "";

    /*
     * To change this value must use setProjectChanged(boolean b). This is
     * because saveProjectItemMenu is dependent on it for when
     * saveProjectMenuItem should be enabled.
     */
    private boolean projectChanged = false;

    private final FindBugsLayoutManager guiLayout;

    private final CommentsArea comments;

    private JLabel statusBarLabel = new JLabel();

    private JTextField sourceSearchTextField = new JTextField(SEARCH_TEXT_FIELD_SIZE);

    private JButton findButton = MainFrameHelper.newButton("button.find", "First");

    private JButton findNextButton = MainFrameHelper.newButton("button.findNext", "Next");

    private JButton findPreviousButton = MainFrameHelper.newButton("button.findPrev", "Previous");

    private NavigableTextPane sourceCodeTextPane = new NavigableTextPane();

    private JPanel summaryTopPanel;

    JEditorPane summaryHtmlArea = new JEditorPane();

    private JScrollPane summaryHtmlScrollPane = new JScrollPane(summaryHtmlArea);

    private SourceCodeDisplay displayer = new SourceCodeDisplay(this);

    private ViewFilter viewFilter = new ViewFilter(this);

    private SaveType saveType = SaveType.NOT_KNOWN;

    private MainFrameLoadSaveHelper mainFrameLoadSaveHelper = new MainFrameLoadSaveHelper(this);

    final MainFrameTree mainFrameTree = new MainFrameTree(this);

    final MainFrameMenu mainFrameMenu = new MainFrameMenu(this);

    private final MainFrameComponentFactory mainFrameComponentFactory = new MainFrameComponentFactory(this);
    private final PluginUpdateDialog pluginUpdateDialog = new PluginUpdateDialog();

    public static void makeInstance(FindBugsLayoutManagerFactory factory) {
        if (instance != null)
            throw new IllegalStateException();
        instance = new MainFrame(factory);
        instance.initializeGUI();
    }

    public static MainFrame getInstance() {
        if (instance == null)
            throw new IllegalStateException();
        return instance;
    }

    private MainFrame(FindBugsLayoutManagerFactory factory) {
        guiLayout = factory.getInstance(this);
        comments = new CommentsArea(this);
        FindBugsDisplayFeatures.setAbridgedMessages(true);
        DetectorFactoryCollection.instance().addPluginUpdateListener(pluginUpdateDialog.createListener());
    }

    public void showMessageDialog(String message) {
        guiCallback.showMessageDialog(message);
    }

    public int showConfirmDialog(String message, String title, String ok, String cancel) {
        return guiCallback.showConfirmDialog(message, title, ok, cancel);
    }

    public IGuiCallback getGuiCallback() {
        return guiCallback;
    }

    public void acquireDisplayWait() {
        synchronized (waitLock) {
            waitCount++;
            if (GUI2_DEBUG) {
                System.err.println("acquiring display wait, count " + waitCount);
                Thread.dumpStack();
            }
            if (waitCount == 1)
                mainFrameTree.showCard(BugCard.WAITCARD, new Cursor(Cursor.WAIT_CURSOR), this);
        }
    }

    volatile Exception previousDecrementToZero;

    public void releaseDisplayWait() {
        synchronized (waitLock) {
            if (waitCount <= 0) {
                if (previousDecrementToZero != null)
                    throw new IllegalStateException("Can't decrease wait count; already zero", previousDecrementToZero);
                else
                    throw new IllegalStateException("Can't decrease wait count; never incremented");
            }
            waitCount--;
            if (GUI2_DEBUG) {
                System.err.println("releasing display wait, count " + waitCount);
                Thread.dumpStack();
            }
            if (waitCount == 0) {
                mainFrameTree.showCard(BugCard.TREECARD, new Cursor(Cursor.DEFAULT_CURSOR), this);
                previousDecrementToZero = new Exception("Previously decremented at");
            }
        }
    }

    public void waitUntilReady() throws InterruptedException {
        mainFrameInitialized.await();
    }

    public JTree getTree() {
        return mainFrameTree.getTree();
    }

    public BugTreeModel getBugTreeModel() {
        return mainFrameTree.getBugTreeModel();
    }

    public synchronized @Nonnull Project getProject() {
        if (curProject == null) {
            curProject = new Project();
        }
        return curProject;
    }

    public synchronized void setProject(Project p) {
        curProject = p;
    }

    /**
     * Called when something in the project is changed and the change needs to
     * be saved. This method should be called instead of using projectChanged =
     * b.
     */
    public void setProjectChanged(boolean b) {
        if (curProject == null)
            return;

        if (projectChanged == b)
            return;

        projectChanged = b;
        mainFrameMenu.setSaveMenu(this);

        getRootPane().putClientProperty(WINDOW_MODIFIED, b);

    }

    /**
     * Show an error dialog.
     */
    public void error(String message) {
        JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Write a message to stdout.
     */
    public void writeToLog(String message) {
        if (GUI2_DEBUG)
            System.out.println(message);
    }

    public int showConfirmDialog(String message, String title, int optionType) {
        return JOptionPane.showConfirmDialog(this, message, title, optionType);
    }

    public Sortables[] getAvailableSortables() {
        return mainFrameTree.getAvailableSortables();
    }

    // ============================== listeners ============================

    /*
     * This is overridden for changing the font size
     */
    @Override
    public void addNotify() {
        super.addNotify();

        float size = Driver.getFontSize();

        JMenuBar menubar = getJMenuBar();
        if (menubar != null) {
            menubar.setFont(menubar.getFont().deriveFont(size));
            for (int i = 0; i < menubar.getMenuCount(); i++) {
                for (int j = 0; j < menubar.getMenu(i).getMenuComponentCount(); j++) {
                    Component temp = menubar.getMenu(i).getMenuComponent(j);
                    temp.setFont(temp.getFont().deriveFont(size));
                }
            }
            mainFrameTree.updateFonts(size);
        }
    }

    @SwingThread
    void updateStatusBar() {
        int countFilteredBugs = BugSet.countFilteredBugs();
        String msg = "";
        if (countFilteredBugs == 1) {
            msg = "  1 " + edu.umd.cs.findbugs.L10N.getLocalString("statusbar.bug_hidden", "bug hidden (see view menu)");
        } else if (countFilteredBugs > 1) {
            msg = "  " + countFilteredBugs + " "
                    + edu.umd.cs.findbugs.L10N.getLocalString("statusbar.bugs_hidden", "bugs hidden (see view menu)");
        }
        msg = updateCloudSigninStatus(msg);
        if (errorMsg != null && errorMsg.length() > 0)
            msg = join(msg, errorMsg);

        mainFrameTree.setWaitStatusLabelText(msg); // should not be the URL
        if (msg.length() == 0)
            msg = "http://findbugs.sourceforge.net";
        statusBarLabel.setText(msg);
    }

    private String updateCloudSigninStatus(String msg) {
        if (getBugCollection() != null) {
            Cloud cloud = getBugCollection().getCloud();
            if (cloud != null) {
                String pluginMsg = cloud.getStatusMsg();
                if (pluginMsg != null && pluginMsg.length() > 1)
                    msg = join(msg, pluginMsg);
            }
        }
        return msg;
    }

    /**
     * This method is called when the application is closing. This is either by
     * the exit menuItem or by clicking on the window's system menu.
     */
    void callOnClose() {
        if (!canNavigateAway())
            return;

        if (projectChanged && !SystemProperties.getBoolean("findbugs.skipSaveChangesWarning")) {
            Object[] options = {
                    L10N.getLocalString("dlg.save_btn", "Save"),
                    L10N.getLocalString("dlg.dontsave_btn", "Don't Save"),
                    L10N.getLocalString("dlg.cancel_btn", "Cancel"),
            };
            int value = JOptionPane.showOptionDialog(this, getActionWithoutSavingMsg("closing"),
                    edu.umd.cs.findbugs.L10N.getLocalString("msg.confirm_save_txt", "Do you want to save?"),
                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);

            if (value == 2 || value == JOptionPane.CLOSED_OPTION)
                return;
            else if (value == 0) {

                if (saveFile == null) {
                    if (!mainFrameLoadSaveHelper.saveAs())
                        return;
                } else
                    mainFrameLoadSaveHelper.save();
            }
        }

        GUISaveState guiSaveState = GUISaveState.getInstance();
        guiLayout.saveState();
        guiSaveState.setFrameBounds(getBounds());
        guiSaveState.setExtendedWindowState(getExtendedState());
        guiSaveState.save();
        if (this.bugCollection != null) {
            Cloud cloud = this.bugCollection.getCloud();
            if (cloud != null)
                cloud.shutdown();
        }
        System.exit(0);
    }

    // ========================== misc junk ====================================

    JMenuItem createRecentItem(final File f, final SaveType localSaveType) {
        return mainFrameMenu.createRecentItem(f, localSaveType);
    }

    /**
     * Opens the analysis. Also clears the source and summary panes. Makes
     * comments enabled false. Sets the saveType and adds the file to the recent
     * menu.
     * 
     * @param f
     * @return whether the operation was successful
     */
    public boolean openAnalysis(File f, SaveType saveType) {
        if (!f.exists() || !f.canRead()) {
            throw new IllegalArgumentException("Can't read " + f.getPath());
        }

        mainFrameLoadSaveHelper.prepareForFileLoad(f, saveType);

        mainFrameLoadSaveHelper.loadAnalysis(f);
        return true;
    }

    public void openBugCollection(SortedBugCollection bugs) {
        acquireDisplayWait();
        try {
            mainFrameLoadSaveHelper.prepareForFileLoad(null, null);

            Project project = bugs.getProject();
            project.setGuiCallback(guiCallback);
            BugLoader.addDeadBugMatcher(bugs);
            setProjectAndBugCollectionInSwingThread(project, bugs);
        } finally {
            releaseDisplayWait();
        }

    }

    void clearBugCollection() {
        setSaveFile(null);
        setProjectAndBugCollection(null, null);
    }
    
    @SwingThread
    void setBugCollection(BugCollection bugCollection) {
        setProjectAndBugCollection(bugCollection.getProject(), bugCollection);
    }

    void setProjectAndBugCollectionInSwingThread(final Project project, final BugCollection bc) {
        setProjectAndBugCollection(project, bc);
    }

    @SwingThread
    private void setProjectAndBugCollection(@CheckForNull Project project, @CheckForNull BugCollection bugCollection) {
        if (GUI2_DEBUG) {
            if (bugCollection == null)
                System.out.println("Setting bug collection to null");
            else
                System.out.println("Setting bug collection; contains " + bugCollection.getCollection().size() + " bugs");

        }
        if (bugCollection != null && bugCollection.getProject() != project) {
            Project p2 = bugCollection.getProject();
            throw new IllegalArgumentException(String.format("project %x and bug collection %x don't match",
                    System.identityHashCode(project), System.identityHashCode(p2)));
        }
        acquireDisplayWait();
        try {
            
            if (this.bugCollection != bugCollection && this.bugCollection != null) {
                Cloud plugin = this.bugCollection.getCloud();
                if (plugin != null) {
                    plugin.removeListener(userAnnotationListener);
                    plugin.removeStatusListener(cloudStatusListener);
                    plugin.shutdown();
                }
            }
            // setRebuilding(false);
            setProject(project);
            this.bugCollection = bugCollection;
            BugLoader.addDeadBugMatcher(bugCollection);

            comments.updateBugCollection();
            displayer.clearCache();
            if (bugCollection != null) {
                Cloud plugin = bugCollection.getCloud();
                if (plugin != null) {
                    plugin.addListener(userAnnotationListener);
                    plugin.addStatusListener(cloudStatusListener);
                }
            }
            mainFrameTree.updateBugTree();
            setProjectChanged(false);
            Runnable runnable = new Runnable() {
                public void run() {
                    PreferencesFrame.getInstance().updateFilterPanel();
                    mainFrameMenu.getReconfigMenuItem().setEnabled(true);
                    comments.refresh();
                    mainFrameMenu.setViewMenu();
                    newProject();
                    clearSourcePane();
                    clearSummaryTab();

                    /*
                     * This is here due to a threading issue. It can only be
                     * called after curProject has been changed. Since this
                     * method is called by both open methods it is put here.
                     */
                    updateTitle();
                }
            };
            if (SwingUtilities.isEventDispatchThread())
                runnable.run();
            else
                SwingUtilities.invokeLater(runnable);
        } finally {
            releaseDisplayWait();
        }
    }

    void updateProjectAndBugCollection(BugCollection bugCollection) {

        if (bugCollection != null) {
            displayer.clearCache();
            BugSet bs = new BugSet(bugCollection);
            // Dont clear data, the data's correct, just get the tree off the
            // listener lists.
            BugTreeModel model = (BugTreeModel) mainFrameTree.getTree().getModel();
            model.getOffListenerList();
            model.changeSet(bs);
            // curProject=BugLoader.getLoadedProject();
            comments.updateBugCollection();
            setProjectChanged(true);
        }
        setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
    }

    @SuppressWarnings({ "SimplifiableIfStatement" })
    boolean shouldDisplayIssue(BugInstance b) {
        Project project = getProject();
        Filter suppressionFilter = project.getSuppressionFilter();
        if (null == getBugCollection() || suppressionFilter.match(b))
            return false;
        return viewFilter.show(b);
    }

    // ============================= menu actions
    // ===============================

    public void createNewProjectFromMenuItem() {
        if (!canNavigateAway())
            return;
        new NewProjectWizard();

        newProject = true;
    }

    void newProject() {
        clearSourcePane();
        if (!FindBugs.isNoAnalysis()) {
            mainFrameMenu.enableOrDisableItems(curProject, bugCollection);
        }

        if (newProject) {
            setProjectChanged(true);
            // setTitle(TITLE_START_TXT + Project.UNNAMED_PROJECT);
            saveFile = null;
            mainFrameMenu.getSaveMenuItem().setEnabled(false);
            mainFrameMenu.getReconfigMenuItem().setEnabled(true);
            newProject = false;
        }
    }

    void about() {
        AboutDialog dialog = new AboutDialog(this, logger, true);
        dialog.setSize(600, 554);
        dialog.setLocationRelativeTo(this);
        dialog.setVisible(true);
    }

    void preferences() {
        if (!canNavigateAway())
            return;
        PreferencesFrame.getInstance().setLocationRelativeTo(this);
        PreferencesFrame.getInstance().setVisible(true);
    }

    public void displayCloudReport() {
        Cloud cloud = this.bugCollection.getCloud();
        cloud.waitUntilIssueDataDownloaded();
        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        cloud.printCloudSummary(writer, getDisplayedBugs(), viewFilter.getPackagePrefixes());
        writer.close();
        String report = stringWriter.toString();
        DisplayNonmodelMessage.displayNonmodelMessage("Cloud summary", report, this, false);

    }

    void redoAnalysis() {
        if (!canNavigateAway())
            return;

        /// QQQ-TODO: new RuntimeException("Redo analysis called").printStackTrace();
        acquireDisplayWait();
        edu.umd.cs.findbugs.util.Util.runInDameonThread(
        new Runnable() {
            public void run() {
                try {
                    updateDesignationDisplay();
                    Project project = getProject();
                    BugCollection bc = BugLoader.redoAnalysisKeepComments(project);
                    updateProjectAndBugCollection(bc);
                    setProjectAndBugCollectionInSwingThread(project, bc);
                } finally {
                    releaseDisplayWait();
                }
            }
        });
    }

    // ================================== misc junk 2
    // ==============================

    void syncBugInformation() {
        boolean prevProjectChanged = projectChanged;
        if (mainFrameTree.getCurrentSelectedBugLeaf() != null) {
            BugInstance bug = mainFrameTree.getCurrentSelectedBugLeaf().getBug();
            displayer.displaySource(bug, bug.getPrimarySourceLineAnnotation());
            updateDesignationDisplay();
            comments.updateCommentsFromLeafInformation(mainFrameTree.getCurrentSelectedBugLeaf());
            updateSummaryTab(mainFrameTree.getCurrentSelectedBugLeaf());
        } else if (currentSelectedBugAspects != null) {
            updateDesignationDisplay();
            comments.updateCommentsFromNonLeafInformation(currentSelectedBugAspects);
            displayer.displaySource(null, null);
            clearSummaryTab();
        } else {
            displayer.displaySource(null, null);
            clearSummaryTab();
        }
        setProjectChanged(prevProjectChanged);
    }

    void clearSourcePane() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                mainFrameComponentFactory.setSourceTab("", null);
                sourceCodeTextPane.setDocument(SourceCodeDisplay.SOURCE_NOT_RELEVANT);
            }
        });
    }

    // =============================== component creation
    // ==================================

    private void initializeGUI() {
        mainFrameComponentFactory.initializeGUI();
    }

    JPanel statusBar() {
        return mainFrameComponentFactory.statusBar();
    }

    JSplitPane summaryTab() {
        return mainFrameComponentFactory.summaryTab();
    }

    JPanel createCommentsInputPanel() {
        return mainFrameComponentFactory.createCommentsInputPanel();
    }

    JPanel createSourceCodePanel() {
        return mainFrameComponentFactory.createSourceCodePanel();
    }

    JPanel createSourceSearchPanel() {
        return mainFrameComponentFactory.createSourceSearchPanel();
    }

    /**
     * Sets the title of the source tabs for either docking or non-docking
     * versions.
     */
    void setSourceTab(String title, @CheckForNull BugInstance bug) {
        mainFrameComponentFactory.setSourceTab(title, bug);
    }

    SorterTableColumnModel getSorter() {
        return mainFrameTree.getSorter();
    }

    void updateDesignationDisplay() {
        comments.refresh();
    }

    private String getActionWithoutSavingMsg(String action) {
        String msg = edu.umd.cs.findbugs.L10N.getLocalString("msg.you_are_" + action + "_without_saving_txt", null);
        if (msg != null)
            return msg;
        return edu.umd.cs.findbugs.L10N.getLocalString("msg.you_are_" + action + "_txt", "You are " + action) + " "
                + edu.umd.cs.findbugs.L10N.getLocalString("msg.without_saving_txt", "without saving. Do you want to save?");
    }

    public void updateBugTree() {
        mainFrameTree.updateBugTree();
        comments.refresh();
    }

    public void resetViewCache() {
        ((BugTreeModel) mainFrameTree.getTree().getModel()).clearViewCache();
    }

    /**
     * Changes the title based on curProject and saveFile.
     */
    public void updateTitle() {
        Project project = getProject();
        String name = project.getProjectName();
        if ((name == null || name.trim().equals("")) && saveFile != null)
            name = saveFile.getAbsolutePath();
        if (name == null)
            name = "";//Project.UNNAMED_PROJECT;
        String oldTitle = this.getTitle();
        String newTitle = TITLE_START_TXT + (name.trim().equals("") ? "" : " - " + name);
        if (oldTitle.equals(newTitle))
            return;
        this.setTitle(newTitle);
    }

    @SuppressWarnings({ "SimplifiableIfStatement" })
    private boolean shouldDisplayIssueIgnoringPackagePrefixes(BugInstance b) {
        Project project = getProject();
        Filter suppressionFilter = project.getSuppressionFilter();
        if (null == getBugCollection() || suppressionFilter.match(b))
            return false;
        return viewFilter.showIgnoringPackagePrefixes(b);
    }

    public void selectPackagePrefixByProject() {
        TreeSet<String> projects = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        Multiset<String> count = new Multiset<String>();
        int total = 0;
        for (BugInstance b : getBugCollection().getCollection())
            if (shouldDisplayIssueIgnoringPackagePrefixes(b)) {
                TreeSet<String> projectsForThisBug = projectPackagePrefixes.getProjects(b.getPrimaryClass().getClassName());
                projects.addAll(projectsForThisBug);
                count.addAll(projectsForThisBug);
                total++;
            }
        if (projects.size() == 0) {
            JOptionPane.showMessageDialog(this, "No issues in current view");
            return;
        }
        ArrayList<ProjectSelector> selectors = new ArrayList<ProjectSelector>(projects.size() + 1);
        ProjectSelector everything = new ProjectSelector("all projects", "", total);
        selectors.add(everything);
        for (String projectName : projects) {
            ProjectPackagePrefixes.PrefixFilter filter = projectPackagePrefixes.getFilter(projectName);
            selectors.add(new ProjectSelector(projectName, filter.toString(), count.getCount(projectName)));
        }
        ProjectSelector choice = (ProjectSelector) JOptionPane.showInputDialog(null,
                "Choose a project to set appropriate package prefix(es)", "Select package prefixes by package",
                JOptionPane.QUESTION_MESSAGE, null, selectors.toArray(), everything);
        if (choice == null)
            return;

        mainFrameTree.setFieldForPackagesToDisplayText(choice.filter);
        viewFilter.setPackagesToDisplay(choice.filter);
        resetViewCache();

    }

    private static String join(String s1, String s2) {
        if (s1 == null || s1.length() == 0)
            return s2;
        if (s2 == null || s2.length() == 0)
            return s1;
        return s1 + "; " + s2;
    }

    private void updateSummaryTab(BugLeafNode node) {
        final BugInstance bug = node.getBug();

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                summaryTopPanel.removeAll();

                summaryTopPanel.add(mainFrameComponentFactory.bugSummaryComponent(bug.getAbridgedMessage(), bug));

                for (BugAnnotation b : bug.getAnnotationsForMessage(true))
                    summaryTopPanel.add(mainFrameComponentFactory.bugSummaryComponent(b, bug));


                BugPattern bugPattern = bug.getBugPattern();
                String detailText =
                        bugPattern.getDetailText()
                        +"<br><p> <b>Bug kind and pattern: " + 
                                bugPattern.getAbbrev() + " - " + bugPattern.getType();
                String txt = bugPattern.getDetailHTML(detailText);
                summaryHtmlArea.setText(txt);

                summaryTopPanel.add(Box.createVerticalGlue());
                summaryTopPanel.revalidate();

                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        summaryHtmlScrollPane.getVerticalScrollBar().setValue(
                                summaryHtmlScrollPane.getVerticalScrollBar().getMinimum());
                    }
                });
            }
        });
    }

    public void clearSummaryTab() {
        summaryHtmlArea.setText("");
        summaryTopPanel.removeAll();
        summaryTopPanel.revalidate();
    }

    public void searchSource(int type) {
        int targetLineNum = -1;
        String targetString = sourceSearchTextField.getText();
        switch (type) {
        case 0:
            targetLineNum = displayer.find(targetString);
            break;
        case 1:
            targetLineNum = displayer.findNext(targetString);
            break;
        case 2:
            targetLineNum = displayer.findPrevious(targetString);
            break;
        }
        if (targetLineNum != -1)
            displayer.foundItem(targetLineNum);
    }

    public boolean canNavigateAway() {
        return comments.canNavigateAway();
    }

    @SuppressWarnings({ "deprecation" })
    public void createProjectSettings() {
        ProjectSettings.newInstance();
    }

    /*
     * If the file already existed, its already in the preferences, as well as
     * the recent projects menu items, only add it if they change the name,
     * otherwise everything we're storing is still accurate since all we're
     * storing is the location of the file.
     */
    public void addFileToRecent(File xmlFile) {
        mainFrameMenu.addFileToRecent(xmlFile);
    }

    public void setSaveType(SaveType saveType) {
        if (GUI2_DEBUG && this.saveType != saveType)
            System.out.println("Changing save type from " + this.saveType + " to " + saveType);
        this.saveType = saveType;
    }

    public SaveType getSaveType() {
        return saveType;
    }

    private Iterable<BugInstance> getDisplayedBugs() {
        return new Iterable<BugInstance>() {
            public Iterator<BugInstance> iterator() {
                return new ShownBugsIterator();
            }
        };
    }

    // =================================== misc accessors for helpers
    // ==========================

    public BugLeafNode getCurrentSelectedBugLeaf() {
        return mainFrameTree.getCurrentSelectedBugLeaf();
    }

    public BugAspects getCurrentSelectedBugAspects() {
        return currentSelectedBugAspects;
    }

    public NavigableTextPane getSourceCodeTextPane() {
        return sourceCodeTextPane;
    }

    public BugCollection getBugCollection() {
        return bugCollection;
    }

    public boolean isProjectChanged() {
        return projectChanged;
    }

    public File getSaveFile() {
        return saveFile;
    }

    public Project getCurrentProject() {
        return curProject;
    }

    public JMenuItem getSaveMenuItem() {
        return mainFrameMenu.getSaveMenuItem();
    }

    public void setSaveFile(File saveFile) {
        this.saveFile = saveFile;
    }

    public ExecutorService getBackgroundExecutor() {
        return backgroundExecutor;
    }

    public CommentsArea getComments() {
        return comments;
    }

    public JMenuItem getReconfigMenuItem() {
        return mainFrameMenu.getReconfigMenuItem();
    }

    public SourceCodeDisplay getSourceCodeDisplayer() {
        return displayer;
    }

    public ProjectPackagePrefixes getProjectPackagePrefixes() {
        return projectPackagePrefixes;
    }

    public void enableRecentMenu(boolean enable) {
        mainFrameMenu.enableRecentMenu(enable);
    }

    public void setCurrentSelectedBugAspects(BugAspects currentSelectedBugAspects) {
        this.currentSelectedBugAspects = currentSelectedBugAspects;
    }

    public ViewFilter getViewFilter() {
        return viewFilter;
    }

    public Project getCurProject() {
        return curProject;
    }

    public MainFrameLoadSaveHelper getMainFrameLoadSaveHelper() {
        return mainFrameLoadSaveHelper;
    }

    public FindBugsLayoutManager getGuiLayout() {
        return guiLayout;
    }

    public MainFrameTree getMainFrameTree() {
        return mainFrameTree;
    }

    public boolean projectChanged() {
        return projectChanged;
    }

    public MainFrameMenu getMainFrameMenu() {
        return mainFrameMenu;
    }

    public JEditorPane getSummaryHtmlArea() {
        return summaryHtmlArea;
    }

    public JLabel getStatusBarLabel() {
        return statusBarLabel;
    }

    public JButton getFindNextButton() {
        return findNextButton;
    }

    public JScrollPane getSummaryHtmlScrollPane() {
        return summaryHtmlScrollPane;
    }

    public JButton getFindPreviousButton() {
        return findPreviousButton;
    }

    public JTextField getSourceSearchTextField() {
        return sourceSearchTextField;
    }

    public JButton getFindButton() {
        return findButton;
    }

    public JPanel getSummaryTopPanel() {
        return summaryTopPanel;
    }

    public void setSummaryTopPanel(JPanel summaryTopPanel) {
        this.summaryTopPanel = summaryTopPanel;
    }

    void waitForMainFrameInitialized() {
        mainFrameInitialized.countDown();
    }

    public void addDesignationItem(JMenu menu, final String key, final String text, int keyEvent) {
        JMenuItem toggleItem = new JMenuItem(text);

        toggleItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                comments.setDesignation(key);
            }
        });
        MainFrameHelper.attachAcceleratorKey(toggleItem, keyEvent);
        menu.add(toggleItem);
    }

    enum BugCard {
        TREECARD, WAITCARD
    }

    private static class ProjectSelector {
        public ProjectSelector(String projectName, String filter, int count) {
            this.projectName = projectName;
            this.filter = filter;
            this.count = count;
        }

        final String projectName;

        final String filter;

        final int count;

        @Override
        public String toString() {
            return String.format("%s -- [%d issues]", projectName, count);
        }
    }

    private class ShownBugsIterator implements Iterator<BugInstance> {
        Iterator<BugInstance> base = getBugCollection().getCollection().iterator();

        boolean nextKnown;

        BugInstance next;

        public boolean hasNext() {
            if (!nextKnown) {
                nextKnown = true;
                while (base.hasNext()) {
                    next = base.next();
                    if (shouldDisplayIssue(next))
                        return true;
                }
                next = null;
                return false;
            }
            return next != null;
        }

        public BugInstance next() {
            if (!hasNext())
                throw new NoSuchElementException();
            BugInstance result = next;
            next = null;
            nextKnown = false;
            return result;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    private class MyGuiCallback extends AbstractSwingGuiCallback {
        private MyGuiCallback() {
            super(MainFrame.this);
        }

        public void registerCloud(Project project, BugCollection collection, Cloud plugin) {
//            assert collection.getCloud() == plugin
//                    : collection.getCloud().getCloudName() + " vs " + plugin.getCloudName();
            if (MainFrame.this.bugCollection == collection) {
                plugin.addListener(userAnnotationListener);
                plugin.addStatusListener(cloudStatusListener);
            }
        }

        public void unregisterCloud(Project project, BugCollection collection, Cloud plugin) {
            assert collection.getCloud() == plugin;
            if (MainFrame.this.bugCollection == collection) {
                plugin.removeListener(userAnnotationListener);
                plugin.removeStatusListener(cloudStatusListener);
            }
        }

        public void setErrorMessage(String errorMsg) {
            MainFrame.this.errorMsg = errorMsg;
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    updateStatusBar();
                }
            });
        }
    }

    private class MyCloudListener implements CloudListener {
        public void issueUpdated(BugInstance bug) {
            if (mainFrameTree.getCurrentSelectedBugLeaf() != null && mainFrameTree.getCurrentSelectedBugLeaf().getBug() == bug)
                comments.updateCommentsFromLeafInformation(mainFrameTree.getCurrentSelectedBugLeaf());
        }

        public void statusUpdated() {
            SwingUtilities.invokeLater(updateStatusBarRunner);
        }

        public void taskStarted(Cloud.CloudTask task) {
        }
    }

    private class MyCloudStatusListener implements Cloud.CloudStatusListener {
        public void handleIssueDataDownloadedEvent() {
            mainFrameTree.rebuildBugTreeIfSortablesDependOnCloud();
        }

        public void handleStateChange(SigninState oldState, SigninState state) {
            Cloud cloud = MainFrame.this.bugCollection.getCloudLazily();
            if (cloud != null && cloud.isInitialized())
              mainFrameTree.rebuildBugTreeIfSortablesDependOnCloud();
        }
    }

    private class StatusBarUpdater implements Runnable {
        public void run() {
            updateStatusBar();
        }
    }

}
