package nxm.sys.libg;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.HeadlessException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;

import org.apache.lucene.document.Document;

import nxm.sys.intr.Help;

/** Help Panel to search help index and display results (hits) to user and allow user
    to view result contents in their browser window.
    User's of this component should {@link #setViewer(nxm.sys.libg.MJHelp.DocViewerIF)} for
    displaying the search result hit's contents in a web browser or html/text viewer such
    as JEditorPane.
    @author  Neon Ngo
    @since NeXtMidas 2.5.0
 */
@SuppressWarnings({"serial"})
public class MJHelp extends javax.swing.JPanel {
  /** Document Viewer interface to display result contents. Implementation of this interface can
      display content in browser or embedded html/text panel/viewer or whatever means desired.
   */
  public interface DocViewerIF {
    /** Display the contents of the specified URL.
        @param url The URL to display the contents of in some type of viewer. This may be
                   ignored by a viewer that does not have a display.
        @return true if able to display contents, otherwise false.
     */
    public boolean showDocument (URL url);
  }

  /** Message to display to user to prompt them to enter search/query string */
  private static final String ENTER_SEARCH_MSG = "Enter your search";

  /** Help index searcher                            */ private        Help.Searcher              searcher;
  /** JList model for search results                 */ private        DefaultListModel<Document> resultsModel;
  /** Document Viewer to display contents of results */ private        DocViewerIF                viewer;
  /** true if want to show file path in results list */ private        boolean                    showPathInResults = false;
  /** turn on to see debug messages                  */ private static boolean                    debug             = false;
  /** max search results to return (defaults to 100) */ private        int                        maxResults        =   100;
  /** waiting cursor                                 */ private        Cursor                     waitCursor;

  /** Constructor - calls super(), create empty resultsModel, then initialize. */
  public MJHelp () throws HeadlessException {
    super();
    resultsModel = new DefaultListModel<>();
    initComponents();
    waitCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
    requestFocusForSearch();
  }

  /** Results Model to be used by results JList during initComponents. */
  private DefaultListModel<Document> getResultsModel () { return resultsModel; }

  /** Set the help index path to specified directory
      @param indexDir path to directory containing the help index
   */
  public void setIndexPath (String indexDir) {
    String emsg = null;
    try {
      searcher = new Help.Searcher(indexDir);
      showMessage(ENTER_SEARCH_MSG);
    } catch (IOException e) {
      // any IOException that happens is probably due to a permission problem or
      // non-existent index path or a corrupt index
      emsg = "Could not initialize help for index path="+ indexDir + " " + e;
    } catch (Exception ex) {
      // any other error is unknown, so print out stack trace (without using Shell)
      ex.printStackTrace();
      emsg = "Could not initialize help! See console for stack trace.";
    }
    if (emsg!=null) showMessage(emsg);
  }

  /** Request focus in query input text field in the window. This should be
      called after this component is visible otherwise for request to go through.
      @return see return value of {@link javax.swing.JTextField#requestFocusInWindow()}.
      @since NeXtMidas 2.7.1
    */
  public boolean requestFocusForSearch () {
    return queryInput.requestFocusInWindow();
  }

  /** Return the document viewer that is being used to display data to user.
   *  @return the document viewer */
  public DocViewerIF getViewer () {
    return viewer;
  }

  /** Set the document viewer to use for displaying the contents of search result files
   *  @param viewer the document viewer */
  public void setViewer (DocViewerIF viewer) {
    this.viewer = viewer;
  }

  /** Set the maximum results to return per search.
      @param maxResults set the max for search results. Use -1 for unlimited.
      @since NeXtMidas 2.7.1
   */
  public void setMaxResults (int maxResults) {
    this.maxResults = maxResults;
  }

  /** Set the maximum results to return per search.
     @param maxResultsStr string containing number to set the max search results to.
                          If an invalid string is given (null, invalid number, etc.),
                          then the maximum search result will not be changed.
     @return Integer of new max results value or null on invalid input.
     @see #setMaxResults(int)
     @since NeXtMidas 2.7.1
   */
  protected Integer setMaxResults (String maxResultsStr) {
    Integer max = null;
    try {
      max = Integer.valueOf(maxResultsStr);
      setMaxResults(max.intValue());
    } catch (Exception ex) { } // ignore exception and leave maxResults alone
    return max;
  }

  /** Display a message to user via the statusLabel and also updates its tooltip to the same.
      Same as <code>showMessage(msg, msg)</code>.
      @param msg message to display to user
   */
  private void showMessage (String msg) {
    showMessage(msg, msg);
  }

  /** Display a message to user via the statusLabel and also updates its tooltip to the same.
      @param msg message to display to user
      @param tipMsg Tooltip message for status label, if null, then tooltip is not updated.
   */
  private void showMessage (String msg, String tipMsg) {
    if (statusLabel != null) {
      statusLabel.setText(msg);
      statusLabel.moveCaretPosition(0); // move caret to 1st position of text
      if (tipMsg!=null) statusLabel.setToolTipText(tipMsg);
    }
  }

  /** Print debug message to console via System.out.println() only if debug is turned.
      @param msg debug message to display in console
      @since NeXtMidas 2.7.1
   */
  private static void debug (String msg) {
    if (debug) System.out.println(msg);
  }

  /** Print warning message with "WARN:" prepended to console via System.out
      (this is intentional, to help debug any problems when running with
      Java Web Start). If debug is turned on and the provided Throwable is
      not null, then also print out the stack trace to the console for debugging.
      @param msg warning message to display in console
      @param t   Throwable (e.g. Exception) to include stack track if debug is turned on
      @since NeXtMidas 2.7.1
   */
  private static void warn (String msg, Throwable t) {
    System.out.println("WARN: " +msg);
    if (debug && (t != null)) t.printStackTrace(System.out);
  }

  /** 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.
   */
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    popupMenu = new javax.swing.JPopupMenu();
    radioMaxResults100 = new javax.swing.JRadioButtonMenuItem();
    radioMaxResults500 = new javax.swing.JRadioButtonMenuItem();
    radioMaxResultsUnlimited = new javax.swing.JRadioButtonMenuItem();
    javax.swing.ButtonGroup maxResultsButtonGroup = new javax.swing.ButtonGroup();
    statusLabel = new javax.swing.JTextField();
    queryInput = new javax.swing.JTextField();
    executeQuery = new javax.swing.JButton();
    javax.swing.JScrollPane resultScrollPane = new javax.swing.JScrollPane();
    results = new javax.swing.JList<>();

    FormListener formListener = new FormListener();

    popupMenu.setToolTipText("Search Options");

    maxResultsButtonGroup.add(radioMaxResults100);
    radioMaxResults100.setMnemonic('1');
    radioMaxResults100.setSelected(true);
    radioMaxResults100.setText("100 Max Results");
    radioMaxResults100.setToolTipText("Set Max Search Results to 100");
    radioMaxResults100.setActionCommand("100");
    radioMaxResults100.addActionListener(formListener);
    popupMenu.add(radioMaxResults100);

    maxResultsButtonGroup.add(radioMaxResults500);
    radioMaxResults500.setMnemonic('5');
    radioMaxResults500.setText("500 Max Results");
    radioMaxResults500.setToolTipText("Set Max Search Results to 500");
    radioMaxResults500.setActionCommand("500");
    radioMaxResults500.addActionListener(formListener);
    popupMenu.add(radioMaxResults500);

    maxResultsButtonGroup.add(radioMaxResultsUnlimited);
    radioMaxResultsUnlimited.setMnemonic('u');
    radioMaxResultsUnlimited.setText("Unlimited Max Results");
    radioMaxResultsUnlimited.setToolTipText("Set max search results to be unlimited");
    radioMaxResultsUnlimited.setActionCommand("-1");
    radioMaxResultsUnlimited.addActionListener(formListener);
    popupMenu.add(radioMaxResultsUnlimited);

    popupMenu.getAccessibleContext().setAccessibleName("Search Options");

    setBorder(javax.swing.BorderFactory.createEtchedBorder());
    setComponentPopupMenu(popupMenu);

    statusLabel.setBackground(javax.swing.UIManager.getDefaults().getColor("Label.background"));
    statusLabel.setEditable(false);
    statusLabel.setFont(new java.awt.Font("Dialog", 0, 11));
    statusLabel.setText("status label");
    statusLabel.setToolTipText("status/informational messages");
    statusLabel.setAlignmentX(1.0F);
    statusLabel.setBorder(null);
    statusLabel.setDragEnabled(true);
    statusLabel.setInheritsPopupMenu(true);
    statusLabel.setOpaque(false);
    statusLabel.setRequestFocusEnabled(false);

    queryInput.setToolTipText("Enter your search here. Boolean operators (AND, OR, +, -, NOT), groupings using parentheses, and wildcards (*, ?) can be used.");
    queryInput.setDragEnabled(true);
    queryInput.setFocusCycleRoot(true);
    queryInput.setInheritsPopupMenu(true);
    queryInput.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
      public void changedUpdate(javax.swing.event.DocumentEvent evt) { } // ignore changed update messages
      public void insertUpdate(javax.swing.event.DocumentEvent evt)  { performSearch(queryInput.getText()); }
      public void removeUpdate(javax.swing.event.DocumentEvent evt)  { performSearch(queryInput.getText()); }
    });
    queryInput.addActionListener(formListener);

    executeQuery.setFont(new java.awt.Font("Dialog", 1, 10));
    executeQuery.setText("search");
    executeQuery.setToolTipText("");
    executeQuery.setMargin(new java.awt.Insets(2, 5, 2, 5));
    executeQuery.addActionListener(formListener);

    resultScrollPane.setInheritsPopupMenu(true);

    results.setModel(getResultsModel());
    results.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
    results.setCellRenderer(new ResultsRenderer());
    results.setInheritsPopupMenu(true);
    results.addMouseListener(formListener);
    results.addKeyListener(formListener);
    resultScrollPane.setViewportView(results);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addComponent(statusLabel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 266, Short.MAX_VALUE)
          .addComponent(resultScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 266, Short.MAX_VALUE)
          .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
            .addComponent(queryInput, javax.swing.GroupLayout.DEFAULT_SIZE, 203, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(executeQuery)))
        .addContainerGap())
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addComponent(statusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
          .addComponent(executeQuery, 0, 0, Short.MAX_VALUE)
          .addComponent(queryInput, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(resultScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 344, Short.MAX_VALUE)
        .addContainerGap())
    );
  }

  // Code for dispatching events from components to event handlers.

  private class FormListener implements java.awt.event.ActionListener, java.awt.event.KeyListener, java.awt.event.MouseListener {
    FormListener() {}
    public void actionPerformed(java.awt.event.ActionEvent evt) {
      if (evt.getSource() == queryInput) {
        MJHelp.this.queryInputActionPerformed(evt);
      }
      else if (evt.getSource() == executeQuery) {
        MJHelp.this.executeQueryActionPerformed(evt);
      }
      else if (evt.getSource() == radioMaxResults100) {
        MJHelp.this.setMaxResultsActionPerformed(evt);
      }
      else if (evt.getSource() == radioMaxResults500) {
        MJHelp.this.setMaxResultsActionPerformed(evt);
      }
      else if (evt.getSource() == radioMaxResultsUnlimited) {
        MJHelp.this.setMaxResultsActionPerformed(evt);
      }
    }

    public void keyPressed(java.awt.event.KeyEvent evt) {
      if (evt.getSource() == results) {
        MJHelp.this.resultsKeyPressed(evt);
      }
    }

    public void keyReleased(java.awt.event.KeyEvent evt) {
    }

    public void keyTyped(java.awt.event.KeyEvent evt) {
    }

    public void mouseClicked(java.awt.event.MouseEvent evt) {
      if (evt.getSource() == results) {
        MJHelp.this.resultsMouseClicked(evt);
      }
    }

    public void mouseEntered(java.awt.event.MouseEvent evt) {
    }

    public void mouseExited(java.awt.event.MouseEvent evt) {
    }

    public void mousePressed(java.awt.event.MouseEvent evt) {
    }

    public void mouseReleased(java.awt.event.MouseEvent evt) {
    }
  }// </editor-fold>//GEN-END:initComponents

  private void executeQueryActionPerformed(ActionEvent evt) {//GEN-FIRST:event_executeQueryActionPerformed
    performSearch(queryInput.getText());
  }//GEN-LAST:event_executeQueryActionPerformed

  private void queryInputActionPerformed(ActionEvent evt) {//GEN-FIRST:event_queryInputActionPerformed
    performSearch(queryInput.getText());
  }//GEN-LAST:event_queryInputActionPerformed

  private void setMaxResultsActionPerformed(ActionEvent evt) {//GEN-FIRST:event_setMaxResultsActionPerformed
    setMaxResults(evt.getActionCommand());
  }//GEN-LAST:event_setMaxResultsActionPerformed

  private void resultsKeyPressed(KeyEvent evt) {//GEN-FIRST:event_resultsKeyPressed
    if (evt.getKeyCode()==KeyEvent.VK_ENTER) {
      MJHelp.this.showResultHit();
    }
  }//GEN-LAST:event_resultsKeyPressed

  private void resultsMouseClicked(MouseEvent evt) {//GEN-FIRST:event_resultsMouseClicked
    MJHelp.this.showResultHit();
  }//GEN-LAST:event_resultsMouseClicked

  /** Perform actual search of query string and update results list by updating the model.
      @param queryString query string to search for (this cannot be null)
      @throws NullPointerException if queryString is null
   */
  private void performSearch (String queryString) {
    if (searcher==null) return; // cannot perform search without a Searcher instance

    resultsModel.clear();

    queryString = queryString.trim();
    if (queryString.length() == 0) { // empty query request
      showMessage(ENTER_SEARCH_MSG);
      return;
    }

    // these boolean terms require additional query terms after them, so wait for more input from user
    if ( queryString.endsWith("AND") || queryString.endsWith("OR") || queryString.endsWith("+") ||
         queryString.endsWith("NOT") || queryString.endsWith("&&") || queryString.endsWith("^") ||
         queryString.endsWith("||")  || queryString.endsWith("(")  || queryString.endsWith("-") ) {
      showMessage("Waiting for additional search terms...");
      return;
    }

    Cursor prevCursor      = null;
    Cursor prevInputCursor = null;
    try {
      // save previous cursor and change to busy cursor for long queries
      prevCursor        = getCursor();
      prevInputCursor   = queryInput.getCursor();
//      Cursor waitCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
      setCursor(waitCursor);
      queryInput.setCursor(waitCursor);

      showMessage("Searching for " + queryString + " ...");
      List<Document> hits = searcher.query(queryString, maxResults+1); // execute search
      int len = hits.size();
      String statusMessage;
      if (len == 0 || len == 1)   { statusMessage = len + " result found for: "  + queryString; }
      else if (len <= maxResults || maxResults < 0) { statusMessage = len + " results found for: " + queryString; }
      else { // else number results > maxResult (limiting result list)
        len = maxResults; // show only maxResults
        statusMessage = len +"+ results found for: " + queryString;
      }
      showMessage(statusMessage);

      for (int ii=0; ii<len; ii++) {
        resultsModel.addElement(hits.get(ii));
      } // end for loop
    }
    catch (Exception e) {
      String message = "Invalid search string";
      String emsg    = e.getMessage();
      int index      = emsg.indexOf("column");
      if (index != -1) {
        int endIndex = emsg.indexOf('.', index);
        // append column where input is invalid to message
        if (endIndex != -1) message += " at column " + emsg.substring(index+7, endIndex);
      }
      message += " - " + emsg;
      showMessage(message);
      if (statusLabel!=null) statusLabel.setToolTipText(message);
      warn("Help - caught exception. " + message, e);
    } catch (Throwable t) {
      String emsg = "Reload this page/GUI OR restart browser to recover from: " + t.getMessage();
      showMessage(emsg);
      if (statusLabel!=null) statusLabel.setToolTipText(emsg);
      warn("Help ERROR: " + emsg, t);
    } finally {
      // restore previous cursor for this and the queryInput field if it was changed
      if (prevCursor!=null) setCursor(prevCursor);
      else                  setCursor(Cursor.getDefaultCursor()); // use default cursor if prevCursor is null

      if (prevInputCursor!=null) queryInput.setCursor(prevInputCursor);
    }
  }

  /** show contents of selected result's hits using the registered document viewer
     (via {@link #setViewer(nxm.sys.libg.MJHelp.DocViewerIF)}. e.g. show it in
     the browser.
   */
  private void showResultHit () {
    int i = results.getSelectedIndex();
    String path = "";
    try {
      Document doc = resultsModel.get(i);
      // display document in viewer if it exists
      if (doc != null) {
        path = doc.get("path");
        viewer.showDocument(new File(path).toURI().toURL());
      }
    }  catch(MalformedURLException e) {
      showMessage("URL not found: " + path + " - "+ e);
    } catch (ArrayIndexOutOfBoundsException e) {
      // ignore this as another query may have changed the index
      if (debug) warn("showResultHit Exception: "+e, e);
    }
  }


  // Variables declaration - do not modify//GEN-BEGIN:variables
  /** execute query button */         protected javax.swing.JButton executeQuery;
  /** popup menu */                   protected javax.swing.JPopupMenu popupMenu;
  /** query input */                  protected javax.swing.JTextField queryInput;
  /** 100 results radio item */       private javax.swing.JRadioButtonMenuItem radioMaxResults100;
  /** 500 results radio item */       private javax.swing.JRadioButtonMenuItem radioMaxResults500;
  /** unlimited results radio item */ private javax.swing.JRadioButtonMenuItem radioMaxResultsUnlimited;
  /** results list */                 protected javax.swing.JList<Document> results;
  /** status label */                 protected javax.swing.JTextField statusLabel;
  // End of variables declaration//GEN-END:variables

  /** WARNING: Under penalty of public humiliation, do not call this method.  It is only public to
      be used by the help command. Violators will be pointed to and laughed at.

      Run the Help inside a JFrame as a Swing Application.
      @param title Frame title to use
      @param indexPath path to the help index directory
      @param width width of the window
      @param height height of the window
      @param defaultCloseOperation override the JFrame's default close operation, see {@link JFrame#setDefaultCloseOperation(int)}
  */
  public static void runAsApplication (final String title, final String indexPath, final int width,
                                       final int height, final Integer defaultCloseOperation) {

    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        JFrame frame     = new JFrame(title);
        if (defaultCloseOperation != null) frame.setDefaultCloseOperation(defaultCloseOperation.intValue());

        final MJHelp help = new MJHelp();
        help.setIndexPath(indexPath);
        help.setViewer(new DocViewerIF() {
          public boolean showDocument (URL url) {
            String urlStr = url.toString();
            help.showMessage(urlStr, "Open this URL: " + urlStr);
            MJBrowser.browse(url);
            return true;
          }
        });
        frame.getContentPane().add(help);
        frame.pack();
        frame.setSize(width, height);
        frame.setVisible(true);
      }
    });
  }

  /** Simple test for MJHelp
   *  @param args indexPath in 0 index */
  public static void main (String[] args) {
    String indexPath = args[0];
    runAsApplication("NeXtMidas Help", indexPath, 300, 400, Integer.valueOf(JFrame.EXIT_ON_CLOSE));
  }

  /** Customer Cell Renderer for List to display search results stored in {@link Document} class. */
  private class ResultsRenderer extends DefaultListCellRenderer {

    /** Display value as "index. title" when showPathInResult is false (default), otherwise
        use "index. title - path". Also add the following tooltip "index. path (score)".
      */
    public Component getListCellRendererComponent (JList<?> list, Object value, int index,
        boolean isSelected, boolean cellHasFocus) {
      Component comp = super.getListCellRendererComponent(list,value,index,isSelected,cellHasFocus);
      int hitNumber = index + 1; // add one to make it 1s base for display to user
      if (value instanceof Document) {
        Document doc = (Document) value;
        String score = doc.get("score");    // get its score
        String doctitle = doc.get("title"); // get its title
        String path     = doc.get("path");  // get its path field
        if ((doctitle == null) || doctitle.equals("")) { // use the filename if it has no title (BUG 1331)
          File file = new File(path);
          doctitle = file.getName();
        } else if (showPathInResults && path!=null){
          doctitle += " - " + path;
        }
        JLabel label = ((JLabel)comp);
        label.setText(hitNumber + ". " + doctitle);
        label.setToolTipText(hitNumber + ". " + path + " (" + score + ")");
      }
      return comp;
    }
  } // end class ResultsRenderer

}
