content: package hu.akarnokd.jmh.gui;
  
  import hu.akarnokd.jmh.gui.ComparisonTab.JMHRowModel;
  import hu.akarnokd.utils.lang.StringUtils;
  import hu.akarnokd.utils.xml.XElement;
  
  import java.awt.*;
  import java.awt.event.*;
  import java.io.*;
  import java.net.URI;
  import java.util.function.Consumer;
  
  import javax.swing.*;
  import javax.xml.stream.XMLStreamException;
  
  import say.swing.JFontChooser;
  
  public class JMHCompareGUI extends JFrame {
      private final String VERSION = "1.3.2";
      /** */
      private static final long serialVersionUID = -4168653287697309256L;
      private JTabbedPane tabs;
      final File configFile = new File("./jmh-compare-gui-config.xml");
      Font tableFont;
      int cellPadding;
      String csvSeparator;
      boolean localeDecimalSeparator;
      File workdir = new File(".");
      final DiffConfig diff = new DiffConfig();
  
      void init() {
          setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
          addWindowListener(new WindowAdapter() {
              @Override
              public void windowClosing(WindowEvent e) {
                  saveConfig();
                  quit();
              }
          });
          setSize(1024, 720);
          setLocationRelativeTo(null);
          setTitle("JMH Results Comparison (" + VERSION + ")");
          
          tabs = new JTabbedPane();
          Container c = getContentPane();
          c.setLayout(new BorderLayout());
          c.add(tabs, BorderLayout.CENTER);
          
          tableFont = tabs.getFont().deriveFont(Font.PLAIN);
  
          initMenu();
          
          if (!initConfig()) {
              ComparisonTab ct = new ComparisonTab(tabs, diff);
              tabs.addTab("New tab", ct);
              tabs.addTab("+", new JLabel());
          }
          tabs.addChangeListener(cl -> {
              int idx = tabs.getSelectedIndex();
              if (idx >= 0) {
                  Component tc = tabs.getComponentAt(idx);
                  if (tc instanceof JLabel) {
                      ComparisonTab ct = new ComparisonTab(tabs, diff);
                      ct.setTableFont(tableFont);
                      ct.setPadding(cellPadding);
                      tabs.setComponentAt(idx, ct);
                      tabs.setTitleAt(idx, "New tab");
                      tabs.addTab("+", new JLabel());
                  }
              }
          });
      }
      
      void initMenu() {
          JMenuBar menubar = new JMenuBar();
          setJMenuBar(menubar);
          
          JMenu mnuFile = new JMenu("File");
          menubar.add(mnuFile);
          
          JMenuItem mnuNewTab = new JMenuItem("New tab");
          mnuFile.add(mnuNewTab);
          mnuFile.addSeparator();
          
          JMenuItem mnuOpenTab = new JMenuItem("Open tab...");
          mnuFile.add(mnuOpenTab);
  
          JMenuItem mnuOpenWorkspace = new JMenuItem("Open workspace...");
          mnuFile.add(mnuOpenWorkspace);
  
          mnuFile.addSeparator();
  
          JMenuItem mnuCloseTab = new JMenuItem("Close tab");
          mnuFile.add(mnuCloseTab);
          JMenuItem mnuCloseAllTabs = new JMenuItem("Close all tabs");
          mnuFile.add(mnuCloseAllTabs);
  
          mnuFile.addSeparator();
          
          JMenuItem mnuSaveTab = new JMenuItem("Save tab...");
          mnuFile.add(mnuSaveTab);
          JMenuItem mnuSaveWorkspace = new JMenuItem("Save workspace...");
          mnuFile.add(mnuSaveWorkspace);
          
          mnuFile.addSeparator();
          
          JMenuItem mnuExportSettings = new JMenuItem("Export settings...");
          mnuFile.add(mnuExportSettings);
          
          JMenuItem mnuExportCSV = new JMenuItem("Export into CSV (text)...");
          mnuFile.add(mnuExportCSV);
          
          JMenuItem mnuExportXLS = new JMenuItem("Export into XLS (html)...");
          mnuFile.add(mnuExportXLS);
  
          mnuFile.addSeparator();
  
          JMenuItem mnuExit = new JMenuItem("Exit");
          mnuFile.add(mnuExit);
  
          JMenu mnuView = new JMenu("View");
          menubar.add(mnuView);
          
          JMenuItem mnuFont = new JMenuItem("Font...");
          mnuView.add(mnuFont);
  
          JMenuItem mnuCellPadding = new JMenuItem("Cell padding...");
          mnuView.add(mnuCellPadding);
          
          mnuView.addSeparator();
          
          JMenuItem mnuSmallDiff = new JMenuItem("Small diff percentage...");
          mnuView.add(mnuSmallDiff);
          
          JMenuItem mnuLargeDiff = new JMenuItem("Large diff percentage...");
          mnuView.add(mnuLargeDiff);
  
          mnuView.addSeparator();
          
          JMenuItem mnuSmallPlus = new JMenuItem("Small plus color...");
          mnuView.add(mnuSmallPlus);
          JMenuItem mnuLargePlus = new JMenuItem("Large plus color...");
          mnuView.add(mnuLargePlus);
          JMenuItem mnuSmallMinus = new JMenuItem("Small minus color...");
          mnuView.add(mnuSmallMinus);
          JMenuItem mnuLargeMinus = new JMenuItem("Large minus color...");
          mnuView.add(mnuLargeMinus);
  
          mnuView.addSeparator();
  
          JMenuItem mnuResetDiff = new JMenuItem("Reset percentages");
          mnuView.add(mnuResetDiff);
          JMenuItem mnuResetColors = new JMenuItem("Reset colors");
          mnuView.add(mnuResetColors);
  
          JMenu mnuHelp = new JMenu("Help");
          menubar.add(mnuHelp);
          
          JMenuItem mnuHomepage = new JMenuItem("Homepage...");
          mnuHelp.add(mnuHomepage);
          
          // ------------------------------------------
          
          mnuFont.addActionListener(al -> doChangeFont());
          
          mnuCellPadding.addActionListener(al -> doCellPadding());
          
          mnuHomepage.addActionListener(al -> doHomepage());
          
          mnuNewTab.addActionListener(al -> doNewTab());
          
          mnuCloseAllTabs.addActionListener(al -> doCloseAllTabs());
          
          mnuCloseTab.addActionListener(al -> doCloseTab());
          
          mnuOpenTab.addActionListener(al -> doOpenTab(false, false));
          
          mnuOpenWorkspace.addActionListener(al -> doOpenWorkspace());
          
          mnuSaveTab.addActionListener(al -> doSaveTab());
          
          mnuSaveWorkspace.addActionListener(al -> doSaveWorkspace());
          
          mnuExit.addActionListener(al -> quit());
          
          mnuSmallDiff.addActionListener(al -> doSmallDiff());
  
          mnuLargeDiff.addActionListener(al -> doLargeDiff());
          
          mnuSmallPlus.addActionListener(al -> doChangeColor(diff.smallPlus, "Small plus color", rgb -> diff.smallPlus = rgb));
          mnuSmallMinus.addActionListener(al -> doChangeColor(diff.smallMinus, "Small minus color", rgb -> diff.smallMinus = rgb));
          mnuLargePlus.addActionListener(al -> doChangeColor(diff.largePlus, "Large plus color", rgb -> diff.largePlus = rgb));
          mnuLargeMinus.addActionListener(al -> doChangeColor(diff.largeMinus, "Large minus color", rgb -> diff.largeMinus = rgb));
          
          mnuResetColors.addActionListener(al -> doResetColors());
          mnuResetDiff.addActionListener(al -> doResetDiff());
          
          mnuExportCSV.addActionListener(al -> doExportCSV());
          mnuExportXLS.addActionListener(al -> doExportXLS());
          
          mnuExportSettings.addActionListener(al -> doExportSettings());
      }
      
      void doResetColors() {
          DiffConfig def = new DiffConfig();
          diff.smallPlus = def.smallPlus;
          diff.smallMinus = def.smallMinus;
          diff.largePlus = def.largePlus;
          diff.largeMinus = def.largeMinus;
          repaint();
      }
      void doResetDiff() {
          repaint();
      }
      
      void doChangeColor(Color current, String name, Consumer<Color> newColor) {
          Color c = JColorChooser.showDialog(this, name, current);
          if (c != null) {
              newColor.accept(c);
              repaint();
          }
      }
      
      void doSmallDiff() {
          String value = JOptionPane.showInputDialog(this, "Small diff percentage (float)", "" + diff.smallDiff);
          if (value != null && !value.isEmpty()) {
              value = value.replace(',', '.');
              diff.smallDiff = Double.parseDouble(value);
              repaint();
          }
      }
      void doLargeDiff() {
          String value = JOptionPane.showInputDialog(this, "Large diff percentage (float)", "" + diff.largeDiff);
          if (value != null && !value.isEmpty()) {
              value = value.replace(',', '.');
              diff.largeDiff = Double.parseDouble(value);
              repaint();
          }
      }
      
      void doSaveTab() {
          int idx = tabs.getSelectedIndex();
          if (idx >= 0) {
              Component c = tabs.getComponentAt(idx);
              if (c instanceof ComparisonTab) {
                  ComparisonTab ct = (ComparisonTab) c;
                  JFileChooser fc = new JFileChooser(workdir);
                  if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
                      File f = fc.getSelectedFile();
                      workdir = f.getParentFile();
                      
                      XElement xtab = new XElement("jmh-compare-gui-tabs");
                      saveTab(xtab, ct);
                      
                      try {
                          xtab.save(f);
                      } catch (IOException e) {
                          reportError(e);
                      }
                  }
              }
          }
      }
      
      void doSaveWorkspace() {
          JFileChooser fc = new JFileChooser(workdir);
          if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
              File f = fc.getSelectedFile();
              workdir = f.getParentFile();
              
              XElement xtab = new XElement("jmh-compare-gui-tabs");
              saveAllTabs(xtab);
              
              try {
                  xtab.save(f);
              } catch (IOException e) {
                  reportError(e);
              }
          }
      }
      
      void doOpenWorkspace() {
          doOpenTab(true, true);
      }
      
      void doOpenTab(boolean clear, boolean workspace) {
          JFileChooser fc = new JFileChooser(workdir);
          if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
              File f = fc.getSelectedFile();
              workdir = f.getParentFile();
              
              try {
                  if (clear) {
                      tabs.removeAll();
                  }
                  loadTabs(XElement.parseXML(f), workspace);
              } catch (XMLStreamException ex) {
                  reportError(ex);
              }
          }
      }
      
      void doCloseTab() {
          int idx = tabs.getSelectedIndex();
          if (idx >= 0) {
              Component c = tabs.getComponentAt(idx);
              if (c instanceof ComparisonTab) {
                  ComparisonTab ct = (ComparisonTab) c;
                  ct.close();
              }
          }
      }
      
      void doCloseAllTabs() {
          tabs.removeAll();
          tabs.addTab("+", new JLabel());
      }
      
      void doNewTab() {
          ComparisonTab ct = new ComparisonTab(tabs, diff);
          ct.setTableFont(tableFont);
          ct.setPadding(cellPadding);
          int idx = tabs.getTabCount() - 1;
          tabs.insertTab("New tab", null, ct, null, idx);
          tabs.setSelectedIndex(idx);
      }
      
      void doHomepage() {
          try {
              if (Desktop.isDesktopSupported()) {
                  Desktop desktop = Desktop.getDesktop();
                  URI uri = new URI("https://github.com/akarnokd/jmh-compare-gui");
                  desktop.browse(uri);
                  return;
              }
          } catch (Throwable t) {
              t.printStackTrace();
          }
          reportError("Your platform doesn't seem to support Java Desktop. Please navigate to https://github.com/akarnokd/jmh-compare-gui manually.");
      }
      
      void doCellPadding() {
          String value = JOptionPane.showInputDialog(this, "Enter padding value", "" + cellPadding);
          
          if (value != null) {
              try {
                  cellPadding = Integer.parseInt(value);
                  for (int i = 0; i < tabs.getTabCount(); i++) {
                      Component tc = tabs.getComponentAt(i);
                      if (tc instanceof ComparisonTab) {
                          ComparisonTab ct = (ComparisonTab) tc;
                          ct.setPadding(cellPadding);
                      }
                  }
              } catch (NumberFormatException ex) {
                  reportError(ex);
              }
          }
      }
      
      void doChangeFont() {
          JFontChooser jfc = new JFontChooser();
          jfc.setSelectedFont(tableFont);
          if (jfc.showDialog(this) == JFontChooser.OK_OPTION) {
              tableFont = jfc.getSelectedFont();
              for (int i = 0; i < tabs.getTabCount(); i++) {
                  Component tc = tabs.getComponentAt(i);
                  if (tc instanceof ComparisonTab) {
                      ComparisonTab ct = (ComparisonTab) tc;
                      ct.setTableFont(tableFont);
                      ct.autoSize();
                  }
              }
          }
      }
      
      boolean initConfig() {
          if (configFile.canRead()) {
              try {
                  XElement config = XElement.parseXML(configFile);
                  processConfig(config);
                  return true;
              } catch (XMLStreamException ex) {
                  ex.printStackTrace();
              }
          }
          return false;
      }
      
      void processConfig(XElement config) {
          int state = config.getInt("window-state", getState());
          setState(state);
          if (state != MAXIMIZED_BOTH && state != ICONIFIED) {
              setLocation(config.getInt("window-x", getX()), config.getInt("window-y", getY()));
              setSize(config.getInt("window-width", getWidth()), config.getInt("window-height", getHeight()));
          }
          
          loadTabs(config, true);
          
          if (tabs.getTabCount() > 0) {
              tabs.setSelectedIndex(Math.max(0, config.getInt("selected-tab", 0)));
          }
          
          String workdir = config.get("workdir", null);
          if (workdir != null) {
              this.workdir = new File(workdir);
          }
      }
      void loadTabs(XElement parent, boolean workspace) {
          int c = tabs.getTabCount();
          if (c > 0) {
              if (tabs.getComponentAt(c - 1) instanceof JLabel) {
                  tabs.removeTabAt(c - 1);
              }
          }
          if (workspace) {
              cellPadding = parent.getInt("cell-padding", 0);
              
              XElement xfont = parent.childElement("font");
              if (xfont != null) {
                  tableFont = new Font(xfont.get("name"), xfont.getInt("style"), xfont.getInt("size"));
              }
              localeDecimalSeparator = parent.getBoolean("locale-decimal-separator", false);
              csvSeparator = parent.get("csv-separator", ",");
              
              diff.smallDiff = parent.getDouble("small-diff", diff.smallDiff);
              diff.largeDiff = parent.getDouble("large-diff", diff.largeDiff);
              getColor(parent, "small-plus-color", rgb -> diff.smallPlus = rgb, diff.smallPlus);
              getColor(parent, "small-minus-color", rgb -> diff.smallMinus = rgb, diff.smallMinus);
              getColor(parent, "large-plus-color", rgb -> diff.largePlus = rgb, diff.largePlus);
              getColor(parent, "large-minus-color", rgb -> diff.largeMinus = rgb, diff.largeMinus);
          }
          for (XElement xtab : parent.childrenWithName("tab")) {
              ComparisonTab ct = new ComparisonTab(tabs, diff);
              if (tableFont != null) {
                  ct.setTableFont(tableFont);
              }
              ct.setPadding(cellPadding);
              tabs.add(ct, "New tab");
              ct.load(xtab);
              ct.autoSize();
          }
          tabs.addTab("+", new JLabel());
      }
      
      void saveConfig() {
          XElement xconfig = new XElement("jmh-compare-gui-config");
          
          int state = getState();
          xconfig.set("window-state", state);
          if (state != MAXIMIZED_BOTH && state != ICONIFIED) {
              xconfig.set("window-x", getX());
              xconfig.set("window-y", getY());
              xconfig.set("window-width", getWidth());
              xconfig.set("window-height", getHeight());
          }
          xconfig.set("workdir", this.workdir.getAbsolutePath());
          saveAllTabs(xconfig);
          
          try {
              xconfig.save(configFile);
          } catch (IOException ex) {
              reportError(ex);
          }
      }
      
      void saveTab(XElement parent, ComparisonTab ct) {
          XElement tab = parent.add("tab");
          ct.save(tab);
      }
      
      void addColor(XElement parent, String name, Color c) {
          XElement xcolor = parent.add(name);
          xcolor.set("r", c.getRed());
          xcolor.set("g", c.getGreen());
          xcolor.set("b", c.getBlue());
      }
      void getColor(XElement parent, String name, Consumer<Color> colors, Color def) {
          XElement xcolor = parent.childElement(name);
          if (xcolor != null) {
              int r = xcolor.getInt("r", -1);
              int g = xcolor.getInt("g", -1);
              int b = xcolor.getInt("b", -1);
              if (r < 0 || g < 0 || b < 0) {
                  colors.accept(def);
              } else {
                  colors.accept(new Color(r, g, b));
              }
          }
      }
      
      void saveAllTabs(XElement parent) {
          parent.set("cell-padding", cellPadding);
          
          if (tableFont != null) {
              XElement xfont = parent.add("font");
              xfont.set("name", tableFont.getName());
              xfont.set("style", tableFont.getStyle());
              xfont.set("size", tableFont.getSize());
          }
          
          parent.set("csv-separator", csvSeparator);
          parent.set("locale-decimal-separator", localeDecimalSeparator);
          
          parent.set("small-diff", diff.smallDiff);
          parent.set("large-diff", diff.largeDiff);
          addColor(parent, "small-plus-color", diff.smallPlus);
          addColor(parent, "small-minus-color", diff.smallMinus);
          addColor(parent, "large-plus-color", diff.largePlus);
          addColor(parent, "large-minus-color", diff.largeMinus);
          
          parent.set("selected-tab", tabs.getSelectedIndex());
          
          for (int i = 0; i < tabs.getTabCount(); i++) {
              Component c = tabs.getComponentAt(i);
              if (c instanceof ComparisonTab) {
                  ComparisonTab ct = (ComparisonTab) c;
                  
                  saveTab(parent, ct);
              }
          }
      }
      
      void quit() {
          dispose();
      }
      
      void reportError(String message) {
          JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE);
      }
      void reportError(Throwable ex) {
          ex.printStackTrace();
          JOptionPane.showMessageDialog(this, ex.toString(), "Error", JOptionPane.ERROR_MESSAGE);
      }
      
      public static void main(String[] args) {
          SwingUtilities.invokeLater(() -> {
              JMHCompareGUI gui = new JMHCompareGUI();
              gui.init();
              gui.setVisible(true);
          });
      }
  
      void doExportCSV() {
          ComparisonTab ct = (ComparisonTab)tabs.getSelectedComponent();
          if (ct != null) {
              JFileChooser fc = new JFileChooser(workdir);
              if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
                  File f = fc.getSelectedFile();
                  workdir = f.getParentFile();
                  
                  try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
                      int c = 0;
                      for (String s : ct.model.columnNames) {
                          if (c++ > 0) {
                              out.print(csvSeparator);
                          }
                          printCell(out, s);
                      }
                      out.println();
                      for (JMHRowModel rm : ct.model) {
                          printCell(out, rm.benchmark);
                          int vs = ct.valueStart;
                          int j = 1;
                          for (Double v : rm.values) {
                              out.print(csvSeparator);
                              if (j < vs) {
                                  printCell(out, rm.strings.get(j - 1));
                                  j++;
                                  continue;
                              }
                              if (v != null) {
                                  if (localeDecimalSeparator) {
                                      printCell(out, String.format("%f", v));
                                  } else {
                                      printCell(out, v.toString());
                                  }
                              } else {
                                  printCell(out, "");
                              }
                              j++;
                          }
                          out.println();
                      }
                  } catch (IOException ex) {
                      JOptionPane.showMessageDialog(this, ex.toString(), "Error", JOptionPane.ERROR_MESSAGE);
                  }
              }
          }
      }
      static void printCell(PrintWriter out, String s) {
          out.print("\"");
          if (s != null) {
              s = StringUtils.replaceAll(s, "\"", "\"\"");
              out.print(s);
          }
          out.print("\"");
      }
      void doExportXLS() {
          ComparisonTab ct = (ComparisonTab)tabs.getSelectedComponent();
          if (ct != null) {
              JFileChooser fc = new JFileChooser(workdir);
              if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
                  File f = fc.getSelectedFile();
                  workdir = f.getParentFile();
                  
                  try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
                      out.print("<html><head><title>JMH Benchmark Comparison: ");
                      out.print(XElement.sanitize(tabs.getTitleAt(tabs.getSelectedIndex())));
                      out.println("</title></head>");
                      out.println("<body>");
                      out.println("  <table>");
                      out.println("    <thead>");
                      out.println("      <tr>");
                      for (String s : ct.model.columnNames) {
                          out.print("        <th>");
                          out.print(XElement.sanitize(s));
                          out.println("</th>");
                      }
                      out.println("      </tr>");
                      out.println("    </thead>");
                      out.println("    <tbody>");
                      
                      for (JMHRowModel rm : ct.model) {
                          out.println("      <tr>");
                          out.print("        <td>");
                          out.print(XElement.sanitize(rm.benchmark));
                          out.println("</td>");
                          int vs = ct.valueStart;
                          int j = 1;
                          int col = 0;
                          for (Double v : rm.values) {
                              if (j < vs) {
                                  out.print("        <td>");
                                  out.print(XElement.sanitize(rm.strings.get(j - 1)));
                                  out.println("</td>");
                                  j++;
                                  continue;
                              }
                              
                              out.print("        <td");
                              if (ct.compareIndex >= 0 && ct.compareIndex != col) {
                                  Double v0 = rm.values.get(ct.valueStart + ct.compareIndex - 1);
                                  if (v0 != null) {
                                      if (v != null) {
                                          double ratio = v / v0;
                                          if (ratio >= 1 + diff.largeDiff / 100) {
                                              printColor(out, diff.largePlus);
                                          } else
                                          if (ratio <= 1 - diff.largeDiff / 100) {
                                              printColor(out, diff.largeMinus);
                                          } else
                                          if (ratio >= 1 + diff.smallDiff / 100) {
                                              printColor(out, diff.smallPlus);
                                          } else
                                          if (ratio <= 1 - diff.smallDiff / 100) {
                                              printColor(out, diff.smallMinus);
                                          }
                                      }
                                  }
                              }
                              out.print(">");
                              if (v != null) {
                                  if (localeDecimalSeparator) {
                                      out.print(String.format("%f", v));
                                  } else {
                                      out.print(v.toString());
                                  }
                              }
                              out.println("</td>");
                              col++;
                              j++;
                          }
                          out.println("      </tr>");
                      }
                      out.println("    </tbody>");
                      out.println("  </table>");
                      out.println("</body>");
                      out.println("</html>");
                  } catch (IOException ex) {
                      JOptionPane.showMessageDialog(this, ex.toString(), "Error", JOptionPane.ERROR_MESSAGE);
                  }
              }
          }
      }
      static void printColor(PrintWriter out, Color color) {
          out.print(" style='background-color: rgb(");
          out.print(color.getRed());
          out.print(", ");
          out.print(color.getGreen());
          out.print(", ");
          out.print(color.getBlue());
          out.print(");'");
      }
      void doExportSettings() {
          ExportSettingsDialog dlg = new ExportSettingsDialog(localeDecimalSeparator, csvSeparator);
          dlg.setLocationRelativeTo(this);
          dlg.setVisible(true);
          if (dlg.isApproved()) {
              localeDecimalSeparator = dlg.isLocalDecimalSeparator();
              csvSeparator = dlg.getCsvSeparator();
          }
      }
  }
  