content: /*
   * Copyright 2016 the original author or authors.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.github.alexfalappa.nbspringboot.projects.customizer;
  
  import java.awt.Component;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.KeyAdapter;
  import java.awt.event.KeyEvent;
  import java.util.Comparator;
  import java.util.TreeSet;
  import java.util.prefs.Preferences;
  
  import javax.swing.DefaultListCellRenderer;
  import javax.swing.DefaultListModel;
  import javax.swing.JComponent;
  import javax.swing.JList;
  import javax.swing.JTextField;
  import javax.swing.KeyStroke;
  import javax.swing.UIManager;
  import javax.swing.event.DocumentEvent;
  import javax.swing.event.DocumentListener;
  import javax.swing.event.ListSelectionEvent;
  import javax.swing.event.ListSelectionListener;
  import javax.swing.text.html.HTMLEditorKit;
  
  import org.openide.util.NbPreferences;
  import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
  
  import com.github.alexfalappa.nbspringboot.PrefConstants;
  import com.github.alexfalappa.nbspringboot.Utils;
  import com.github.alexfalappa.nbspringboot.projects.service.api.SpringBootService;
  
  import static com.github.alexfalappa.nbspringboot.PrefConstants.PREF_DEPR_ERROR_SHOW;
  import static com.github.alexfalappa.nbspringboot.PrefConstants.PREF_DEPR_SORT_LAST;
  import static java.awt.event.MouseEvent.BUTTON1;
  
  /**
   * Dialog for choosing one Spring Boot configuration property in an ordered list of those available in the project.
   * <p>
   * Queries a {@link SpringBootService} for the configuration properties names.
   *
   * @author Alessandro Falappa
   */
  public class CfgPropsDialog extends javax.swing.JDialog {
  
      private boolean okPressed = false;
      private final TreeSet<ConfigurationMetadataProperty> sortedProps;
      private final boolean bDeprErrorShow;
  
      /**
       * Creates new dialog CfgPropsDialog.
       *
       * @param parent the parent dialog.
       */
      public CfgPropsDialog(java.awt.Dialog parent) {
          super(parent, true);
          initComponents();
          // retrieve some flads from prefs
          final Preferences prefs = NbPreferences.forModule(PrefConstants.class);
          final boolean bDeprLast = prefs.getBoolean(PREF_DEPR_SORT_LAST, true);
          bDeprErrorShow = prefs.getBoolean(PREF_DEPR_ERROR_SHOW, true);
          // setup props sorting
          this.sortedProps = new TreeSet<>(new ConfigurationMetadataComparator(bDeprLast));
          // setup property list
          lCfgProps.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
              @Override
              public void valueChanged(ListSelectionEvent e) {
                  if (!e.getValueIsAdjusting()) {
                      final ConfigurationMetadataProperty selectedValue = lCfgProps.getSelectedValue();
                      if (selectedValue != null) {
                          tpDetails.setText(Utils.cfgPropDetailsHtml(selectedValue));
                          tpDetails.setCaretPosition(0);
                      }
                  }
              }
          });
          // set default button
          rootPane.setDefaultButton(bOk);
          // close dialog with ESC key
          final ActionListener escAction = new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  CfgPropsDialog.this.setVisible(false);
              }
          };
          rootPane.registerKeyboardAction(escAction, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
                  JComponent.WHEN_IN_FOCUSED_WINDOW);
      }
  
      public void loadCfgProps(SpringBootService bootService) {
          if (bootService == null) {
              return;
          }
          sortedProps.addAll(bootService.queryPropertyMetadata(null));
          filterProps(null);
          // setup filtering
          new ListFilterer(txFilter);
          pack();
      }
  
      public boolean okPressed() {
          return okPressed;
      }
  
      public String getSelectedPropName() {
          return lCfgProps.getSelectedValue().getId();
      }
  
      /** This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of
       * this method is always regenerated by the Form Editor.
       */
      @SuppressWarnings("unchecked")
      // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
      private void initComponents() {
  
          bCancel = new javax.swing.JButton();
          bOk = new javax.swing.JButton();
          splitter = new javax.swing.JSplitPane();
          scroller1 = new javax.swing.JScrollPane();
          lCfgProps = new javax.swing.JList<>();
          scroller2 = new javax.swing.JScrollPane();
          tpDetails = new javax.swing.JTextPane();
          txFilter = new javax.swing.JTextField();
          lFilter = new javax.swing.JLabel();
  
          setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
          setTitle(org.openide.util.NbBundle.getBundle(CfgPropsDialog.class).getString("CfgPropsDialog.title")); // NOI18N
  
          org.openide.awt.Mnemonics.setLocalizedText(bCancel, org.openide.util.NbBundle.getBundle(CfgPropsDialog.class).getString("CfgPropsDialog.bCancel.text")); // NOI18N
          bCancel.addActionListener(new java.awt.event.ActionListener() {
              public void actionPerformed(java.awt.event.ActionEvent evt) {
                  bCancelActionPerformed(evt);
              }
          });
  
          org.openide.awt.Mnemonics.setLocalizedText(bOk, org.openide.util.NbBundle.getBundle(CfgPropsDialog.class).getString("CfgPropsDialog.bOk.text")); // NOI18N
          bOk.addActionListener(new java.awt.event.ActionListener() {
              public void actionPerformed(java.awt.event.ActionEvent evt) {
                  bOkActionPerformed(evt);
              }
          });
  
          splitter.setBorder(null);
          splitter.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);
          splitter.setContinuousLayout(true);
  
          lCfgProps.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
          lCfgProps.setCellRenderer(new ConfigurationMetadataCellRenderer());
          lCfgProps.setVisibleRowCount(16);
          lCfgProps.addMouseListener(new java.awt.event.MouseAdapter() {
              public void mouseClicked(java.awt.event.MouseEvent evt) {
                  lCfgPropsMouseClicked(evt);
              }
          });
          scroller1.setViewportView(lCfgProps);
  
          splitter.setTopComponent(scroller1);
  
          tpDetails.setEditorKit(new HTMLEditorKit());
          tpDetails.setCursor(new java.awt.Cursor(java.awt.Cursor.TEXT_CURSOR));
          tpDetails.setMinimumSize(new java.awt.Dimension(42, 63));
          tpDetails.setPreferredSize(new java.awt.Dimension(42, 63));
          scroller2.setViewportView(tpDetails);
  
          splitter.setRightComponent(scroller2);
  
          txFilter.setColumns(25);
          txFilter.setText(org.openide.util.NbBundle.getMessage(CfgPropsDialog.class, "CfgPropsDialog.txFilter.text")); // NOI18N
          txFilter.setToolTipText(org.openide.util.NbBundle.getMessage(CfgPropsDialog.class, "CfgPropsDialog.txFilter.toolTipText")); // NOI18N
  
          org.openide.awt.Mnemonics.setLocalizedText(lFilter, org.openide.util.NbBundle.getMessage(CfgPropsDialog.class, "CfgPropsDialog.lFilter.text")); // NOI18N
  
          javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
          getContentPane().setLayout(layout);
          layout.setHorizontalGroup(
              layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
              .addGroup(layout.createSequentialGroup()
                  .addContainerGap()
                  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                      .addComponent(splitter, javax.swing.GroupLayout.Alignment.TRAILING)
                      .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                          .addGap(0, 0, Short.MAX_VALUE)
                          .addComponent(bCancel)
                          .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                          .addComponent(bOk))
                      .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                          .addComponent(lFilter)
                          .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                          .addComponent(txFilter)))
                  .addContainerGap())
          );
  
          layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bCancel, bOk});
  
          layout.setVerticalGroup(
              layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
              .addGroup(layout.createSequentialGroup()
                  .addContainerGap()
                  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                      .addComponent(txFilter, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                      .addComponent(lFilter))
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(splitter)
                  .addGap(18, 18, 18)
                  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                      .addComponent(bOk)
                      .addComponent(bCancel))
                  .addContainerGap())
          );
  
          pack();
      }// </editor-fold>//GEN-END:initComponents
  
      private void bOkActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bOkActionPerformed
          okPressed = true;
          bCancelActionPerformed(null);
      }//GEN-LAST:event_bOkActionPerformed
  
      private void bCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bCancelActionPerformed
          setVisible(false);
      }//GEN-LAST:event_bCancelActionPerformed
  
      private void lCfgPropsMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_lCfgPropsMouseClicked
          if (evt.getButton() == BUTTON1 && evt.getClickCount() > 1) {
              bOkActionPerformed(null);
          }
      }//GEN-LAST:event_lCfgPropsMouseClicked
  
      // Variables declaration - do not modify//GEN-BEGIN:variables
      private javax.swing.JButton bCancel;
      private javax.swing.JButton bOk;
      private javax.swing.JList<ConfigurationMetadataProperty> lCfgProps;
      private javax.swing.JLabel lFilter;
      private javax.swing.JScrollPane scroller1;
      private javax.swing.JScrollPane scroller2;
      private javax.swing.JSplitPane splitter;
      private javax.swing.JTextPane tpDetails;
      private javax.swing.JTextField txFilter;
      // End of variables declaration//GEN-END:variables
  
      private void filterProps(String filter) {
          DefaultListModel<ConfigurationMetadataProperty> dlmCfgProps = new DefaultListModel<>();
          for (ConfigurationMetadataProperty item : sortedProps) {
              if (filter == null || item.getId().contains(filter)) {
                  if (Utils.isErrorDeprecated(item)) {
                      if (bDeprErrorShow) {
                          dlmCfgProps.addElement(item);
                      }
                  } else {
                      dlmCfgProps.addElement(item);
                  }
              }
          }
          lCfgProps.setModel(dlmCfgProps);
          if (!dlmCfgProps.isEmpty()) {
              lCfgProps.setSelectedIndex(0);
          }
      }
  
      private static class ConfigurationMetadataCellRenderer extends DefaultListCellRenderer {
  
          @Override
          public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                  boolean cellHasFocus) {
              super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
              if (value instanceof ConfigurationMetadataProperty) {
                  ConfigurationMetadataProperty prop = (ConfigurationMetadataProperty) value;
                  if (prop.isDeprecated()) {
                      setText(String.format("<html><s>%s", prop.getId()));
                      if (Utils.isErrorDeprecated(prop)) {
                          setForeground(UIManager.getColor("nb.errorForeground"));
                      }
                  } else {
                      setText(prop.getId());
                  }
              }
              return this;
          }
  
      }
  
      private static class ConfigurationMetadataComparator implements Comparator<ConfigurationMetadataProperty> {
  
          private final boolean sortDeprLast;
  
          public ConfigurationMetadataComparator(boolean sortDeprLast) {
              this.sortDeprLast = sortDeprLast;
          }
  
          @Override
          public int compare(ConfigurationMetadataProperty p1, ConfigurationMetadataProperty p2) {
              if (!sortDeprLast) {
                  return p1.getId().compareTo(p2.getId());
              } else {
                  boolean d1 = p1.isDeprecated();
                  boolean d2 = p2.isDeprecated();
                  if (d1 && !d2) {
                      return 1;
                  } else if (d2 && !d1) {
                      return -1;
                  } else {
                      return p1.getId().compareTo(p2.getId());
                  }
              }
          }
  
      }
  
      private class ListFilterer extends KeyAdapter implements DocumentListener {
  
          private final JTextField txtField;
  
          public ListFilterer(JTextField txtField) {
              this.txtField = txtField;
              txtField.getDocument().addDocumentListener(this);
              txtField.addKeyListener(this);
          }
  
          @Override
          public void changedUpdate(DocumentEvent e) {
              doFilter();
          }
  
          @Override
          public void insertUpdate(DocumentEvent e) {
              doFilter();
          }
  
          @Override
          public void removeUpdate(DocumentEvent e) {
              doFilter();
          }
  
          @Override
          public void keyPressed(KeyEvent e) {
              final int keyCode = e.getKeyCode();
              if (keyCode == KeyEvent.VK_ESCAPE) {
                  final String filter = txtField.getText();
                  if (filter == null || filter.isEmpty()) {
                      // close dialog if filter textfield is empty
                      setVisible(false);
                  } else {
                      // clear textfield
                      txtField.setText(null);
                      e.consume();
                  }
              } else {
                  final int size = lCfgProps.getModel().getSize();
                  // short circuit if empty size
                  if (size == 0) {
                      return;
                  }
                  int selIdx = lCfgProps.getSelectedIndex();
                  final int pageStep = lCfgProps.getVisibleRowCount();
                  // calculate new index
                  switch (keyCode) {
                      case KeyEvent.VK_PAGE_UP:
                          selIdx -= pageStep;
                          // clamp to [0 : size-1] range
                          if (selIdx < 0) {
                              selIdx = 0;
                          } else if (selIdx >= size) {
                              selIdx = size - 1;
                          }
                          break;
                      case KeyEvent.VK_UP:
                          selIdx -= 1;
                          // wrap if needed
                          if (selIdx < 0) {
                              selIdx += size;
                          } else if (selIdx >= size) {
                              selIdx -= size;
                          }
                          break;
                      case KeyEvent.VK_DOWN:
                          selIdx += 1;
                          // wrap if needed
                          if (selIdx < 0) {
                              selIdx += size;
                          } else if (selIdx >= size) {
                              selIdx -= size;
                          }
                          break;
                      case KeyEvent.VK_PAGE_DOWN:
                          selIdx += pageStep;
                          // clamp to [0 : size-1] range
                          if (selIdx < 0) {
                              selIdx = 0;
                          } else if (selIdx >= size) {
                              selIdx = size - 1;
                          }
                          break;
                  }
                  // move properties list selection and scroll to it
                  lCfgProps.setSelectedIndex(selIdx);
                  lCfgProps.scrollRectToVisible(lCfgProps.getCellBounds(selIdx, selIdx));
              }
          }
  
          private void doFilter() {
              String text = txFilter.getText().toLowerCase();
              if (!text.isEmpty()) {
                  filterProps(text);
              } else {
                  filterProps(null);
              }
          }
  
      }
  }
  