/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.cpd;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
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.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;

public class GUI implements CPDListener {

    private static final Object[][] RENDERER_SETS = {
        { "Text", new SimpleRenderer(), },
        { "XML", new XMLRenderer(), },
        { "CSV (comma)", new CSVRenderer(','), },
        { "CSV (tab)", new CSVRenderer('\t'), }, };

    private abstract static class LanguageConfig {
        public abstract Language languageFor(Properties p);

        public boolean canIgnoreIdentifiers() {
            return false;
        }

        public boolean canIgnoreLiterals() {
            return false;
        }

        public boolean canIgnoreAnnotations() {
            return false;
        }

        public boolean canIgnoreUsings() {
            return false;
        }

        public boolean canIgnoreLiteralSequences() {
            return false;
        }

        public boolean canIgnoreIdentifierAndLiteralSequences() {
            return false;
        }

        public abstract String[] extensions();
    }

    private static final Object[][] LANGUAGE_SETS;

    static {
        LANGUAGE_SETS = new Object[LanguageFactory.supportedLanguages.length + 1][2];

        int index;
        for (index = 0; index < LanguageFactory.supportedLanguages.length; index++) {
            final String terseName = LanguageFactory.supportedLanguages[index];
            final Language lang = LanguageFactory.createLanguage(terseName);
            LANGUAGE_SETS[index][0] = lang.getName();
            LANGUAGE_SETS[index][1] = new LanguageConfig() {
                @Override
                public Language languageFor(Properties p) {
                    lang.setProperties(p);
                    return lang;
                }

                @Override
                public String[] extensions() {
                    List<String> exts = lang.getExtensions();
                    return exts.toArray(new String[0]);
                }

                @Override
                public boolean canIgnoreAnnotations() {
                    if (terseName == null) {
                        return false;
                    }
                    switch (terseName) {
                        case "cs":
                        case "java":
                            return true;
                        default:
                            return false;
                    }
                }

                @Override
                public boolean canIgnoreIdentifiers() {
                    return "java".equals(terseName);
                }

                @Override
                public boolean canIgnoreLiterals() {
                    return "java".equals(terseName);
                }

                @Override
                public boolean canIgnoreUsings() {
                    return "cs".equals(terseName);
                }

                @Override
                public boolean canIgnoreLiteralSequences() {
                    if (terseName == null) {
                        return false;
                    }
                    switch (terseName) {
                        case "cpp":
                        case "cs":
                        case "lua":
                            return true;
                        default:
                            return false;
                    }
                }

                @Override
                public boolean canIgnoreIdentifierAndLiteralSequences() {
                    return "cpp".equals(terseName);
                }
            };
        }
        LANGUAGE_SETS[index][0] = "by extension...";
        LANGUAGE_SETS[index][1] = new LanguageConfig() {
            @Override
            public Language languageFor(Properties p) {
                return LanguageFactory.createLanguage(LanguageFactory.BY_EXTENSION, p);
            }

            @Override
            public String[] extensions() {
                return new String[] { "" };
            }
        };
    }

    private static final int DEFAULT_CPD_MINIMUM_LENGTH = 75;
    private static final Map<String, LanguageConfig> LANGUAGE_CONFIGS_BY_LABEL = new HashMap<>(LANGUAGE_SETS.length);
    private static final KeyStroke COPY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK,
            false);
    private static final KeyStroke DELETE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);

    private class ColumnSpec {
        private String label;
        private int alignment;
        private int width;
        private Comparator<Match> sorter;

        ColumnSpec(String aLabel, int anAlignment, int aWidth, Comparator<Match> aSorter) {
            label = aLabel;
            alignment = anAlignment;
            width = aWidth;
            sorter = aSorter;
        }

        public String label() {
            return label;
        }

        public int alignment() {
            return alignment;
        }

        public int width() {
            return width;
        }

        public Comparator<Match> sorter() {
            return sorter;
        }
    }

    private final ColumnSpec[] matchColumns = new ColumnSpec[] {
        new ColumnSpec("Source", SwingConstants.LEFT, -1, Match.LABEL_COMPARATOR),
        new ColumnSpec("Matches", SwingConstants.RIGHT, 60, Match.MATCHES_COMPARATOR),
        new ColumnSpec("Lines", SwingConstants.RIGHT, 45, Match.LINES_COMPARATOR), };

    static {
        for (int i = 0; i < LANGUAGE_SETS.length; i++) {
            LANGUAGE_CONFIGS_BY_LABEL.put((String) LANGUAGE_SETS[i][0], (LanguageConfig) LANGUAGE_SETS[i][1]);
        }
    }

    private static LanguageConfig languageConfigFor(String label) {
        return LANGUAGE_CONFIGS_BY_LABEL.get(label);
    }

    private static final class CancelListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }

    private final class GoListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    tokenizingFilesBar.setValue(0);
                    tokenizingFilesBar.setString("");
                    resultsTextArea.setText("");
                    phaseLabel.setText("");
                    timeField.setText("");
                    go();
                }
            }).start();
        }
    }

    private class SaveListener implements ActionListener {

        final CPDReportRenderer renderer;

        SaveListener(CPDReportRenderer theRenderer) {
            renderer = theRenderer;
        }

        @Override
        public void actionPerformed(ActionEvent evt) {
            JFileChooser fcSave = new JFileChooser();
            int ret = fcSave.showSaveDialog(GUI.this.frame);
            File f = fcSave.getSelectedFile();
            if (f == null || ret != JFileChooser.APPROVE_OPTION) {
                return;
            }

            if (!f.canWrite()) {
                final CPDReport report = new CPDReport(matches, numberOfTokensPerFile);
                try (PrintWriter pw = new PrintWriter(Files.newOutputStream(f.toPath()))) {
                    renderer.render(report, pw);
                    pw.flush();
                    JOptionPane.showMessageDialog(frame, "Saved " + matches.size() + " matches");
                } catch (IOException e) {
                    error("Couldn't save file" + f.getAbsolutePath(), e);
                }
            } else {
                error("Could not write to file " + f.getAbsolutePath(), null);
            }
        }

        private void error(String message, Exception e) {
            if (e != null) {
                e.printStackTrace();
            }
            JOptionPane.showMessageDialog(GUI.this.frame, message);
        }

    }

    private final class BrowseListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            JFileChooser fc = new JFileChooser(rootDirectoryField.getText());
            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
            fc.showDialog(frame, "Select");
            if (fc.getSelectedFile() != null) {
                rootDirectoryField.setText(fc.getSelectedFile().getAbsolutePath());
            }
        }
    }

    private class AlignmentRenderer extends DefaultTableCellRenderer {
        private static final long serialVersionUID = -2190382865483285032L;
        private int[] alignments;

        AlignmentRenderer(int[] theAlignments) {
            alignments = theAlignments;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
                int row, int column) {
            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            setHorizontalAlignment(alignments[column]);

            return this;
        }
    }

    private JTextField rootDirectoryField = new JTextField(System.getProperty("user.home"));
    private JTextField minimumLengthField = new JTextField(Integer.toString(DEFAULT_CPD_MINIMUM_LENGTH));
    private JTextField encodingField = new JTextField(System.getProperty("file.encoding"));
    private JTextField timeField = new JTextField(6);
    private JLabel phaseLabel = new JLabel();
    private JProgressBar tokenizingFilesBar = new JProgressBar();
    private JTextArea resultsTextArea = new JTextArea();
    private JCheckBox recurseCheckbox = new JCheckBox("", true);
    private JCheckBox ignoreIdentifiersCheckbox = new JCheckBox("", false);
    private JCheckBox ignoreLiteralsCheckbox = new JCheckBox("", false);
    private JCheckBox ignoreAnnotationsCheckbox = new JCheckBox("", false);
    private JCheckBox ignoreUsingsCheckbox = new JCheckBox("", false);
    private JCheckBox ignoreLiteralSequencesCheckbox = new JCheckBox("", false);
    private JCheckBox ignoreIdentifierAndLiteralSequencesCheckbox = new JCheckBox("", false);
    private JComboBox<String> languageBox = new JComboBox<>();
    private JTextField extensionField = new JTextField();
    private JLabel extensionLabel = new JLabel("Extension:", SwingConstants.RIGHT);
    private JTable resultsTable = new JTable();
    private JButton goButton;
    private JButton cancelButton;
    private JPanel progressPanel;
    private JFrame frame;
    private boolean trimLeadingWhitespace;

    private List<Match> matches = new ArrayList<>();
    private Map<String, Integer> numberOfTokensPerFile;

    private void addSaveOptionsTo(JMenu menu) {

        JMenuItem saveItem;

        for (final Object[] rendererSet : RENDERER_SETS) {
            saveItem = new JMenuItem("Save as " + rendererSet[0]);
            saveItem.addActionListener(new SaveListener((CPDReportRenderer) rendererSet[1]));
            menu.add(saveItem);
        }
    }

    public GUI() {
        frame = new JFrame("PMD Duplicate Code Detector (v " + PMDVersion.VERSION + ')');

        timeField.setEditable(false);

        JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic('f');

        addSaveOptionsTo(fileMenu);

        JMenuItem exitItem = new JMenuItem("Exit");
        exitItem.setMnemonic('x');
        exitItem.addActionListener(new CancelListener());
        fileMenu.add(exitItem);
        JMenu viewMenu = new JMenu("View");
        fileMenu.setMnemonic('v');
        JMenuItem trimItem = new JCheckBoxMenuItem("Trim leading whitespace");
        trimItem.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                AbstractButton button = (AbstractButton) e.getItem();
                GUI.this.trimLeadingWhitespace = button.isSelected();
            }
        });
        viewMenu.add(trimItem);
        JMenuBar menuBar = new JMenuBar();
        menuBar.add(fileMenu);
        menuBar.add(viewMenu);
        frame.setJMenuBar(menuBar);

        // first make all the buttons
        JButton browseButton = new JButton("Browse");
        browseButton.setMnemonic('b');
        browseButton.addActionListener(new BrowseListener());
        goButton = new JButton("Go");
        goButton.setMnemonic('g');
        goButton.addActionListener(new GoListener());
        cancelButton = new JButton("Cancel");
        cancelButton.addActionListener(new CancelListener());

        JPanel settingsPanel = makeSettingsPanel(browseButton, goButton, cancelButton);
        progressPanel = makeProgressPanel();
        JPanel resultsPanel = makeResultsPanel();

        adjustLanguageControlsFor((LanguageConfig) LANGUAGE_SETS[0][1]);

        frame.getContentPane().setLayout(new BorderLayout());
        JPanel topPanel = new JPanel();
        topPanel.setLayout(new BorderLayout());
        topPanel.add(settingsPanel, BorderLayout.NORTH);
        topPanel.add(progressPanel, BorderLayout.CENTER);
        setProgressControls(false); // not running now
        frame.getContentPane().add(topPanel, BorderLayout.NORTH);
        frame.getContentPane().add(resultsPanel, BorderLayout.CENTER);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    private void adjustLanguageControlsFor(LanguageConfig current) {
        ignoreIdentifiersCheckbox.setEnabled(current.canIgnoreIdentifiers());
        ignoreLiteralsCheckbox.setEnabled(current.canIgnoreLiterals());
        ignoreAnnotationsCheckbox.setEnabled(current.canIgnoreAnnotations());
        ignoreUsingsCheckbox.setEnabled(current.canIgnoreUsings());
        ignoreLiteralSequencesCheckbox.setEnabled(current.canIgnoreLiteralSequences());
        ignoreIdentifierAndLiteralSequencesCheckbox.setEnabled(current.canIgnoreIdentifierAndLiteralSequences());
        extensionField.setText(current.extensions()[0]);
        boolean enableExtension = current.extensions()[0].isEmpty();
        extensionField.setEnabled(enableExtension);
        extensionLabel.setEnabled(enableExtension);
    }

    private JPanel makeSettingsPanel(JButton browseButton, JButton goButton, JButton cxButton) {
        JPanel settingsPanel = new JPanel();
        GridBagHelper helper = new GridBagHelper(settingsPanel, new double[] { 0.2, 0.7, 0.1, 0.1 });
        helper.addLabel("Root source directory:");
        helper.add(rootDirectoryField);
        helper.add(browseButton, 2);
        helper.nextRow();
        helper.addLabel("Report duplicate chunks larger than:");
        minimumLengthField.setColumns(4);
        helper.add(minimumLengthField);
        helper.addLabel("Language:");
        for (int i = 0; i < LANGUAGE_SETS.length; i++) {
            languageBox.addItem(String.valueOf(LANGUAGE_SETS[i][0]));
        }
        languageBox.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                adjustLanguageControlsFor(languageConfigFor((String) languageBox.getSelectedItem()));
            }
        });
        helper.add(languageBox);
        helper.nextRow();
        helper.addLabel("Also scan subdirectories?");
        helper.add(recurseCheckbox);

        helper.add(extensionLabel);
        helper.add(extensionField);

        helper.nextRow();
        helper.addLabel("Ignore literals?");
        helper.add(ignoreLiteralsCheckbox);
        helper.addLabel("");
        helper.addLabel("");
        helper.nextRow();

        helper.nextRow();
        helper.addLabel("Ignore identifiers?");
        helper.add(ignoreIdentifiersCheckbox);
        helper.addLabel("");
        helper.addLabel("");
        helper.nextRow();

        helper.nextRow();
        helper.addLabel("Ignore annotations?");
        helper.add(ignoreAnnotationsCheckbox);
        helper.addLabel("");
        helper.addLabel("");
        helper.nextRow();

        helper.nextRow();
        helper.addLabel("Ignore usings?");
        helper.add(ignoreUsingsCheckbox);
        helper.addLabel("");
        helper.addLabel("");
        helper.nextRow();

        helper.nextRow();
        helper.addLabel("Ignore literal sequences?");
        helper.add(ignoreLiteralSequencesCheckbox);
        helper.add(goButton);
        helper.add(cxButton);
        helper.nextRow();

        helper.nextRow();
        helper.addLabel("Ignore identifier and literal sequences?");
        helper.add(ignoreIdentifierAndLiteralSequencesCheckbox);
        helper.add(goButton);
        helper.add(cxButton);
        helper.nextRow();

        helper.addLabel("File encoding (defaults based upon locale):");
        encodingField.setColumns(1);
        helper.add(encodingField);
        helper.addLabel("");
        helper.addLabel("");
        helper.nextRow();
        // settingsPanel.setBorder(BorderFactory.createTitledBorder("Settings"));
        return settingsPanel;
    }

    private JPanel makeProgressPanel() {
        JPanel progressPanel = new JPanel();
        final double[] weights = { 0.0, 0.8, 0.4, 0.2 };
        GridBagHelper helper = new GridBagHelper(progressPanel, weights);
        helper.addLabel("Tokenizing files:");
        helper.add(tokenizingFilesBar, 3);
        helper.nextRow();
        helper.addLabel("Phase:");
        helper.add(phaseLabel);
        helper.addLabel("Time elapsed:");
        helper.add(timeField);
        helper.nextRow();
        progressPanel.setBorder(BorderFactory.createTitledBorder("Progress"));
        return progressPanel;
    }

    private JPanel makeResultsPanel() {
        JPanel resultsPanel = new JPanel();
        resultsPanel.setLayout(new BorderLayout());
        JScrollPane areaScrollPane = new JScrollPane(resultsTextArea);
        resultsTextArea.setEditable(false);
        areaScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        areaScrollPane.setPreferredSize(new Dimension(600, 300));

        resultsPanel.add(makeMatchList(), BorderLayout.WEST);
        resultsPanel.add(areaScrollPane, BorderLayout.CENTER);
        return resultsPanel;
    }

    private void populateResultArea() {
        int[] selectionIndices = resultsTable.getSelectedRows();
        TableModel model = resultsTable.getModel();
        List<Match> selections = new ArrayList<>(selectionIndices.length);
        for (int selectionIndex : selectionIndices) {
            selections.add((Match) model.getValueAt(selectionIndex, 99));
        }
        CPDReport toRender = new CPDReport(selections, Collections.emptyMap());
        String report = new SimpleRenderer(trimLeadingWhitespace).renderToString(toRender);
        resultsTextArea.setText(report);
        resultsTextArea.setCaretPosition(0); // move to the top
    }

    private void copyMatchListSelectionsToClipboard() {

        int[] selectionIndices = resultsTable.getSelectedRows();
        int colCount = resultsTable.getColumnCount();

        StringBuilder sb = new StringBuilder();

        for (int r = 0; r < selectionIndices.length; r++) {
            if (r > 0) {
                sb.append('\n');
            }
            sb.append(resultsTable.getValueAt(selectionIndices[r], 0));
            for (int c = 1; c < colCount; c++) {
                sb.append('\t');
                sb.append(resultsTable.getValueAt(selectionIndices[r], c));
            }
        }

        StringSelection ss = new StringSelection(sb.toString());
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
    }

    private void deleteMatchlistSelections() {

        int[] selectionIndices = resultsTable.getSelectedRows();

        for (int i = selectionIndices.length - 1; i >= 0; i--) {
            matches.remove(selectionIndices[i]);
        }

        resultsTable.getSelectionModel().clearSelection();
        resultsTable.addNotify();
    }

    private JComponent makeMatchList() {

        resultsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                populateResultArea();
            }
        });

        resultsTable.registerKeyboardAction(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                copyMatchListSelectionsToClipboard();
            }
        }, "Copy", COPY_KEY_STROKE, JComponent.WHEN_FOCUSED);

        resultsTable.registerKeyboardAction(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                deleteMatchlistSelections();
            }
        }, "Del", DELETE_KEY_STROKE, JComponent.WHEN_FOCUSED);

        int[] alignments = new int[matchColumns.length];
        for (int i = 0; i < alignments.length; i++) {
            alignments[i] = matchColumns[i].alignment();
        }

        resultsTable.setDefaultRenderer(Object.class, new AlignmentRenderer(alignments));

        final JTableHeader header = resultsTable.getTableHeader();
        header.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                sortOnColumn(header.columnAtPoint(new Point(e.getX(), e.getY())));
            }
        });

        return new JScrollPane(resultsTable);
    }

    private boolean isLegalPath(String path, LanguageConfig config) {
        String[] extensions = config.extensions();
        for (int i = 0; i < extensions.length; i++) {
            if (path.endsWith(extensions[i]) && !extensions[i].isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private String setLabelFor(Match match) {

        Set<String> sourceIDs = new HashSet<>(match.getMarkCount());
        for (Iterator<Mark> occurrences = match.iterator(); occurrences.hasNext();) {
            sourceIDs.add(occurrences.next().getFilename());
        }
        String label;

        if (sourceIDs.size() == 1) {
            String sourceId = sourceIDs.iterator().next();
            int separatorPos = sourceId.lastIndexOf(File.separatorChar);
            label = "..." + sourceId.substring(separatorPos);
        } else {
            label = String.format("(%d separate files)", sourceIDs.size());
        }

        match.setLabel(label);
        return label;
    }

    private void setProgressControls(boolean isRunning) {
        progressPanel.setVisible(isRunning);
        goButton.setEnabled(!isRunning);
        cancelButton.setEnabled(isRunning);
    }

    private void go() {
        try {
            File dirPath = new File(rootDirectoryField.getText());
            if (!dirPath.exists()) {
                JOptionPane.showMessageDialog(frame, "Can't read from that root source directory", "Error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            setProgressControls(true);

            Properties p = new Properties();
            CPDConfiguration config = new CPDConfiguration();
            config.setMinimumTileSize(Integer.parseInt(minimumLengthField.getText()));
            config.setSourceEncoding(encodingField.getText());
            config.setIgnoreIdentifiers(ignoreIdentifiersCheckbox.isSelected());
            config.setIgnoreLiterals(ignoreLiteralsCheckbox.isSelected());
            config.setIgnoreAnnotations(ignoreAnnotationsCheckbox.isSelected());
            config.setIgnoreUsings(ignoreUsingsCheckbox.isSelected());
            config.setIgnoreLiteralSequences(ignoreLiteralSequencesCheckbox.isSelected());
            config.setIgnoreIdentifierAndLiteralSequences(ignoreIdentifierAndLiteralSequencesCheckbox.isSelected());
            p.setProperty(LanguageFactory.EXTENSION, extensionField.getText());

            LanguageConfig conf = languageConfigFor((String) languageBox.getSelectedItem());
            Language language = conf.languageFor(p);
            config.setLanguage(language);

            CPDConfiguration.setSystemProperties(config);

            CPD cpd = new CPD(config);
            cpd.setCpdListener(this);
            tokenizingFilesBar.setMinimum(0);
            phaseLabel.setText("");
            if (isLegalPath(dirPath.getPath(), conf)) { // should use the
                // language file filter
                // instead?
                cpd.add(dirPath);
            } else {
                if (recurseCheckbox.isSelected()) {
                    cpd.addRecursively(dirPath);
                } else {
                    cpd.addAllInDirectory(dirPath);
                }
            }
            Timer t = createTimer();
            t.start();
            cpd.go();
            t.stop();

            CPDReport cpdReport = cpd.toReport();
            numberOfTokensPerFile = cpdReport.getNumberOfTokensPerFile();
            matches = new ArrayList<>();
            for (Match match : cpdReport.getMatches()) {
                setLabelFor(match);
                matches.add(match);
            }

            setListDataFrom(matches);
            String report = new SimpleRenderer().renderToString(cpdReport);
            if (report.length() == 0) {
                JOptionPane.showMessageDialog(frame,
                        "Done. Couldn't find any duplicates longer than " + minimumLengthField.getText() + " tokens");
            } else {
                resultsTextArea.setText(report);
            }
        } catch (IOException | RuntimeException t) {
            t.printStackTrace();
            JOptionPane.showMessageDialog(frame, "Halted due to " + t.getClass().getName() + "; " + t.getMessage());
        }
        setProgressControls(false);
    }

    private Timer createTimer() {

        final long start = System.currentTimeMillis();

        return new Timer(1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                long now = System.currentTimeMillis();
                long elapsedMillis = now - start;
                long elapsedSeconds = elapsedMillis / 1000;
                long minutes = (long) Math.floor(elapsedSeconds / 60);
                long seconds = elapsedSeconds - minutes * 60;
                timeField.setText(formatTime(minutes, seconds));
            }
        });
    }

    private static String formatTime(long minutes, long seconds) {

        StringBuilder sb = new StringBuilder(5);
        if (minutes < 10) {
            sb.append('0');
        }
        sb.append(minutes).append(':');
        if (seconds < 10) {
            sb.append('0');
        }
        sb.append(seconds);
        return sb.toString();
    }

    private abstract class SortingTableModel<E> extends AbstractTableModel {
        abstract int sortColumn();

        abstract void sortColumn(int column);

        abstract boolean sortDescending();

        abstract void sortDescending(boolean flag);

        abstract void sort(Comparator<E> comparator);
    }

    private TableModel tableModelFrom(final List<Match> items) {

        return new SortingTableModel<Match>() {

            private int sortColumn;
            private boolean sortDescending;

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                Match match = items.get(rowIndex);
                switch (columnIndex) {
                case 0:
                    return match.getLabel();
                case 2:
                    return Integer.toString(match.getLineCount());
                case 1:
                    return match.getMarkCount() > 2 ? Integer.toString(match.getMarkCount()) : "";
                case 99:
                    return match;
                default:
                    return "";
                }
            }

            @Override
            public int getColumnCount() {
                return matchColumns.length;
            }

            @Override
            public int getRowCount() {
                return items.size();
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return false;
            }

            @Override
            public Class<?> getColumnClass(int columnIndex) {
                return Object.class;
            }

            @Override
            public String getColumnName(int i) {
                return matchColumns[i].label();
            }

            @Override
            public int sortColumn() {
                return sortColumn;
            }

            @Override
            public void sortColumn(int column) {
                sortColumn = column;
            }

            @Override
            public boolean sortDescending() {
                return sortDescending;
            }

            @Override
            public void sortDescending(boolean flag) {
                sortDescending = flag;
            }

            @Override
            public void sort(Comparator<Match> comparator) {
                Collections.sort(items, comparator);
                if (sortDescending) {
                    Collections.reverse(items);
                }
            }
        };
    }

    private void sortOnColumn(int columnIndex) {
        Comparator<Match> comparator = matchColumns[columnIndex].sorter();
        SortingTableModel<Match> model = (SortingTableModel<Match>) resultsTable.getModel();
        if (model.sortColumn() == columnIndex) {
            model.sortDescending(!model.sortDescending());
        }
        model.sortColumn(columnIndex);
        model.sort(comparator);

        resultsTable.getSelectionModel().clearSelection();
        resultsTable.repaint();
    }

    private void setListDataFrom(List<Match> matches) {

        resultsTable.setModel(tableModelFrom(matches));

        TableColumnModel colModel = resultsTable.getColumnModel();
        TableColumn column;
        int width;

        for (int i = 0; i < matchColumns.length; i++) {
            if (matchColumns[i].width() > 0) {
                column = colModel.getColumn(i);
                width = matchColumns[i].width();
                column.setPreferredWidth(width);
                column.setMinWidth(width);
                column.setMaxWidth(width);
            }
        }
    }

    // CPDListener
    @Override
    public void phaseUpdate(int phase) {
        phaseLabel.setText(getPhaseText(phase));
    }

    public String getPhaseText(int phase) {
        switch (phase) {
        case CPDListener.INIT:
            return "Initializing";
        case CPDListener.HASH:
            return "Hashing";
        case CPDListener.MATCH:
            return "Matching";
        case CPDListener.GROUPING:
            return "Grouping";
        case CPDListener.DONE:
            return "Done";
        default:
            return "Unknown";
        }
    }

    @Override
    public void addedFile(int fileCount, File file) {
        tokenizingFilesBar.setMaximum(fileCount);
        tokenizingFilesBar.setValue(tokenizingFilesBar.getValue() + 1);
    }
    // CPDListener

    public static void main(String[] args) {
        // this should prevent the disk not found popup
        // System.setSecurityManager(null);
        new GUI();
    }

}
