package nxm.sys.intr;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopFilter;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.demo.HTMLDocument;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

import nxm.sys.lib.FileUtil;
import nxm.sys.lib.FileUtil.FnFilter;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Terminal;
import nxm.sys.lib.TextFile;
import nxm.sys.libg.MBox;
import nxm.sys.libg.MJBrowser;
import nxm.sys.libg.MJFrame;
import nxm.sys.libg.MJHelp;
import tools.BuildProps;

/** HelpSwap provides the fields and methods needed to maintain the basic CLI functionality for HELP
    and the Graphics functionalities for HELP (/INDEX, /GUI, and /LiteGUI) which depend on the old lucene jar.

    NOTE: The fields and most of the methods in HelpSwap were originally in Help; they have been extracted into HelpSwap.

    @author Zachary Matthews
    @since NeXtMidas 4.0.0
*/
abstract class HelpSwap extends Intrinsic {
  /** properties from sys/cfg/build.props */
  BuildProps sysBuildProps; // since 3.3.4, delay loading until needed
  boolean debug = false;

  /** Help index searcher instance */
  private Searcher searcher;
  /** pre-compiled pattern for HTML <pre> tag */
  private static final Pattern PRE_OPEN_REGEX  = Pattern.compile("(?i)<pre>");
  /** pre-compiled pattern for HTML </pre> tag */
  private static final Pattern PRE_CLOSE_REGEX  = Pattern.compile("(?i)</pre>");

  /** Swing Frame containing the Help GUI */
  private MJFrame helpGUIFrame;
  /** reference to the Help GUI */
  private MJHelp  mjhelp;
  /** default Close operation for /GUI or /LITEGUI */
  private int defaultCloseOp;
  /** is Shell running under one-shot mode ie. {@link Shell#isOneShot()} */
  boolean isOneShot;

  /** has the Help LiteGUI been started? */
  private boolean liteGUIRunning;

  public int open () {
    helpGUIFrame   = null;
    mjhelp         = null;
    searcher       = null;
    liteGUIRunning = false;
    isOneShot      = Shell.isOneShot();

    if ((MA.getState("/GUI") || MA.getState("/LITEGUI")) && isOneShot) defaultCloseOp = MJFrame.EXIT_ON_CLOSE;
    else defaultCloseOp = MJFrame.DISPOSE_ON_CLOSE;

    // default to a quarter second poll time, since nothing is done in process() for /GUI and /LITEGUI
    if (isOneShot) pollTime = args.getD("/POLL", 0.25);

    return NORMAL;
  }

  int runLuceneIndex () {
    // index help files using Lucene and exit
    String filter   = MA.getCS("/FILTER", null);
    String moreDirs = MA.getCS("/MOREDIRS", null);
    if (!indexFiles(filter, moreDirs)) M.error("Run of Help Indexer failed!");
    return FINISH;
  }

  int runLuceneGUI () {
    if (helpGUIFrame == null) { // Start Interactive HelpGUI (it will stay up even after HELP command
                                // FINISHes). HelpGUI exits when user closes GUI or when Shell exits.
      helpGUI();
    }
    if (isOneShot) return NOOP; // need to return NOOP under one-shot mode otherwise GUI closes immediately
    else return FINISH;
  }

  int runLuceneLiteGUI () {
    if (!liteGUIRunning) {
      // launch the "lite" HelpGUI w/o the MJBrowser (viewer) and exit
      try {
        String indexPath = sysBuildProps.getPath("help.index.dir");
        MJHelp.runAsApplication("NeXtMidas Search", indexPath, 500, 400, Integer.valueOf(defaultCloseOp));
        liteGUIRunning = true;
      } catch (Exception e) {
        M.error("Aborted run of Lite HelpGUI (MJHelp). "+e);
      }
    }
    if (isOneShot) return NOOP; // need to return NOOP under one-shot mode otherwise GUI closes immediately
    else           return FINISH;
  }

  void runLuceneSearchInteraction () {
 // handle interaction with user on terminal (CLI)
    String queryStr = MA.getU("NAME","");
    // handle user's interaction with search results
    doSearchInteraction(queryStr);
  }

  public int close () {
    if (helpGUIFrame != null) {
      mjhelp = null;
      helpGUIFrame = null;
    }
    return NORMAL;
  }
  /**
   * Create and Initial HELP GUI graphic components (Frame, JSplitPane, MJHelp, MJBrowser). Does not set the frame to
   * visible.
   */
  private void initHelpGUIComponents (String indexPath) {
    helpGUIFrame = new MJFrame("NeXtMidas Search", new MBox(20, 30, 800, 600));
    javax.swing.JSplitPane jSplitPane = new javax.swing.JSplitPane();
    mjhelp = new MJHelp();
    MJBrowser browser = new MJBrowser();
    mjhelp.setViewer(browser);

    jSplitPane.setDividerLocation(220);
    jSplitPane.setDividerSize(8);

    jSplitPane.setLeftComponent(mjhelp);
    jSplitPane.setRightComponent(browser);

    helpGUIFrame.getContentPane().add(jSplitPane, java.awt.BorderLayout.CENTER);
    helpGUIFrame.pack();
    helpGUIFrame.setDefaultCloseOperation(defaultCloseOp);

    mjhelp.setIndexPath(indexPath);
  }

  /** Run HELP GUI */
  private void helpGUI () {
    try {
      if (helpGUIFrame == null) initHelpGUIComponents(sysBuildProps.getPath("help.index.dir"));
      helpGUIFrame.setVisible(true);
      mjhelp.requestFocusForSearch(); // request focus on search text input field
    } catch (Throwable t) {
      throw new MidasException("Aborted run of HelpGUI.", t);
    }
  }

  /**
   * @param list Numbered list of search entries to the screen
   */
  @SuppressWarnings("deprecation") // for M.terminal
  private void listResults (List<Document> list, String query) {
    boolean showpath = MA.getState("/SHOWPATH");
    int  len  = list.size();
    String statusMessage = len + " result";
    if (len == 0 || len == 1) statusMessage += " found for: ";
    else                      statusMessage += "s found for: ";
    M.more(Terminal.INIT);    // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    M.more(statusMessage + query);
    for (int i=0; i<len && M.more(); i++) {
      Object obj = list.get(i);
      if (obj instanceof Document) {
        Document doc = (Document) obj;
        String doctitle = doc.get("title"); // get its title
        String path     = doc.get("path");  // get its path field
        String filename = path.substring(path.lastIndexOf('/') + 1);
        String pathFromNMROOT = filename;
        String nmroot = Shell.getNmRoot();
        int nmrootIndex = path.indexOf(nmroot);
        if (nmrootIndex != -1) pathFromNMROOT = path.substring(nmroot.length());    // move past NMROOT in path
        if ((doctitle == null) || doctitle.equals("")) { //use the path if it has no title
          doctitle = pathFromNMROOT;
        } else if (showpath) {
          doctitle += " (" + pathFromNMROOT +")";
        } else {
          doctitle += " (" + filename +")";
        }
        String index = " "+i+") "; if (i<10) index = " "+index;
        M.more(index + " " + doctitle);
      }
    } // end for loop
    M.more(Terminal.EXIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
  }

  /** Handle user's interaction with search results from terminal, such as selecting and
      viewing the search result contents.
     @param queryStr search query string
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private void doSearchInteraction (String queryStr) {
    if (queryStr==null || queryStr.trim().length() == 0) {
      queryStr = M.terminal.getInput("Enter search string: ", "").trim();
      if (queryStr.length()==0) return; // nothing to search for
    }

    // search help index for specified query string
    List<Document> results = searchIndex(queryStr);

    int len = results.size();
    if (len > 1) listResults(results, queryStr); // output the list of matches if more than 1 result

    // loop on what to do next
    for (int ilist=0; ilist<len; ilist++) {
      String ans="N";
      if (len>1) ans = M.terminal.getInput("Enter item number or [N]ext/Quit/List ?: ","");
      if (M.terminal.getInterrupt()>0) {               break; }

      if (ans.length()==0) ans="N";
      else ans = ans.trim().toUpperCase();

      if (ans.startsWith("Q")) {
                                                       break;
      }
      else if (ans.startsWith("L")) {
        listResults(results, queryStr);
        ilist--;                                       continue;
      }
      else if (ans.startsWith("N")) {
        if (ilist==len) {                              break; }
      }
      else {
        try {
          ilist = Integer.parseInt(ans);
        } catch (NumberFormatException nfe) {
          ilist--;                                     continue;
        }
      }

      if (ilist >= len) {                              break; }
      Object obj = results.get(ilist);

      // display result content if it is a Document Hit
      if (obj instanceof Document) {
        Document doc   = (Document) obj;
        String docPath = doc.get("path");  // get its path field
        TextFile tf    = new TextFile(M, docPath);

        if (!tf.open(TextFile.NOABORT)) continue;
        M.more(Terminal.INIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
        M.more("Displaying file: " + docPath);
        String text;
        boolean insidePreTag = false;
        while (M.more() && (text=tf.read())!=null) {
          if (!insidePreTag && PRE_OPEN_REGEX.matcher(text).matches())  insidePreTag = true;
          if ( insidePreTag && PRE_CLOSE_REGEX.matcher(text).matches()) insidePreTag = false;
          text = StringUtil.html2Text(text);                      // convert HTML to text
          if (!insidePreTag && text.trim().length()==0) continue; // ignore empty lines inside <pre> tags
          M.more(text);
        }
        M.more(Terminal.EXIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
        tf.close();
        M.println("For better formatting of the contents, view the following in your browser.\n "+docPath);  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      }
    } // end for loop
  }

  /** Search the help index for matches for the specified query string.
      @param queryStr query string to search for (this cannot be null)
      @return Empty List if no results are found, otherwise a List of Hits.
      @since NeXtMidas 2.5.0
   */
  private List<Document> searchIndex (String queryStr) {
    List<Document> results = new ArrayList<>(0);
    if (searcher == null) {
      String indexPath = sysBuildProps.getPath("help.index.dir");
      if (indexPath == null || indexPath.length()==0) M.error("help.index.dir not set in build.props!");
      try {
         searcher = new Searcher(indexPath);
      } catch (IOException ioe) {
        M.warning("Cannot initialize help index search. " + ioe);
      }
    }

    try {
      if (searcher != null) results = searcher.query(queryStr, 100); // execute search (limit to 100 results)
    } catch (ParseException pe) {
      String message = "Invalid search string. " + pe.getMessage();
      M.warning(message);
    }
    return results;
  }

  /** Index files for help and searches
      @param filter Filename filter to use specified the documents to index
      @param additionalDirs '|' separated list of additional directories to
                            include in the list of locations to index documents.
      @return true if indexing is successful, otherwise false
      @since NeXtMidas 2.5.0
   */
  private boolean indexFiles (String filter, String additionalDirs) {
    boolean result         = false;
    String  indexPath      = sysBuildProps.getPath("help.index.dir");
    int     maxFieldLength = Integer.parseInt(sysBuildProps.getProperty("help.maxFieldLength", "100000"));
    int     numDocs        = 0; // total number of documents indexed
    String  locations      = sysBuildProps.getProperty("help.index.locations");
    String  useParentDirs  = sysBuildProps.getProperty("help.index.useparent");
            filter         = sysBuildProps.getProperty("help.index.filter", filter);

    if (indexPath==null || indexPath.length()==0) M.error("help.index.dir not set in build.props!");
    if (additionalDirs!=null) locations += "|" + additionalDirs;

    M.info("Creating help index at " + indexPath + " filter=" + filter +
        "\nThis may take a minute or two...");
    long startTime = System.currentTimeMillis();

    Indexer indexer = null;
    try {
      indexer = new Indexer(indexPath, true, maxFieldLength, filter);
      indexer.debug   = debug; // pass on debug flag to Indexer

      if (locations!=null && locations.length()>0) {
        indexer.setFilter(filter);
        if (debug) M.info("indexing dirs = "+locations+"  -  useparentDirs = "+useParentDirs);
        String[] dirs = locations.split("[|]");
        for (String dir : dirs) {
          dir = dir.trim();
          if (dir.length()>0) {
            boolean useParentDir = (useParentDirs.indexOf(dir) != -1); // false for most cases
            indexer.indexDocs(new File(dir), true, useParentDir);
          }
        }
      }

      numDocs = indexer.getDocCount(); // get total # docs indexed
      result  = true;                  // index is a success
    }
    catch (Exception e) {
      M.warning("Problem trying to index documents for help! " + e);
      if (debug) e.printStackTrace();
    }
    finally {
      if (indexer != null) {
        indexer.close(); // optimize and close indexer
        indexer = null;  // remove reference indexer to help GC free this object
      }
    }
    long endTime = System.currentTimeMillis();
    M.info("Help index created. Indexed "+ numDocs +" files in "+ (endTime - startTime)/1000 +" seconds.");

    return result;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Inner classes
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Help indexer to create lucene index for searching help content.
      This class is only used internally by Help.
      @author Neon Ngo
      @since NeXtMidas 2.5.0
   */
  private static class Indexer {
    private IndexWriter writer;    // Lucene index writer to create and add documents to index
    private FnFilter    fnFilter;  // filename filter for files to index
    private static Analyzer analyzer = new CustomAnalyzer(); // Lucene analyzer for creating/searching help index.
    private boolean debug = false; // enable/disable debugging outputs

    /** Construct a help indexer to create lucene index for searching help content.
       @param indexPath path to directory where index will be stored
       @param overwrite true to create the index or overwrite the existing one; false to append to
                        the existing index
       @param maxFieldLength maximum number of terms that will be indexed for a single field in a document
       @param filter Filename filter for files to index. If null the default filter
                     ("*.html|*.htm|*.txt") will be used. Note: A blank (empty) string, means NONE.
                     See {@link FileUtil#createFnFilter(String, String)} for filename filter format.
       @throws IOException
       @since NeXtMidas 2.5.0
     */
    private Indexer (String indexPath, boolean overwrite, int maxFieldLength, String filter)
      throws IOException {
      Directory directory = FSDirectory.open(new File(indexPath));
      writer = new IndexWriter(directory, analyzer, overwrite, new MaxFieldLength(maxFieldLength));
      fnFilter = FileUtil.createFnFilter(filter,  "*.html|*.htm|*.txt");
    }

    /** Optimize and close index writer (i.e. commit changes to disk) for search to use.
        If index has been closed already, this method will ignore that exception. */
    private void close () {
      if (writer!=null) {
        try {
          writer.optimize();
          writer.close();
        } catch (AlreadyClosedException ace) {
          // It is safe to ignore this exception, writer has been close already
        } catch (Exception e) {
          Shell.printStackTrace("Exception caught in Help.Indexer.close() ",e);
        }
      }
    }

    /** Set the filename filter for files to index.
        @param filter Filename filter for files to index. A blank (empty) string, means NONE.
                      See {@link FileUtil#createFnFilter(String, String)} for filename filter format.
     */
    private void setFilter (String filter) {
      fnFilter = FileUtil.createFnFilter(filter, "");
    }

    /** Return the number of documents currently in the help index writer.
        @return number of documents currently in the help index writer
        @throws AlreadyClosedException if close() has been called before this.
      */
    private int getDocCount () throws AlreadyClosedException {
      int count = 0;
      if (writer!=null) count = writer.maxDoc();
      return count;
    }

    /** Index specified file or files in the specified path
        @param file file or path to directory to index files for help and searches.
        @param recursive set to true if index should goto subdirectories recursively
        @param useParentDir set to true if the stored path in the index for this file or files
               under this directory should point to the parent directory. This is needed for the
               training and user's guides files under the rawfiles directory.
        @throws Exception
     */
    private void indexDocs (File file, boolean recursive, boolean useParentDir) throws Exception {
      if (file.isDirectory()) {                  // if a directory
        String[] files = file.list();            // list its files
        Arrays.sort(files);                      // sort the files
        for (String filestr : files) {           // add files to index
          File afile = new File(file, filestr);
          if (file.isDirectory() && recursive) indexDocs(afile, recursive, useParentDir); // recursively index subdirectory
          else if (fnFilter.accept(afile))     addDocToIndex(afile, useParentDir);
        }
      } else if (fnFilter.accept(file)) {
        addDocToIndex(file, useParentDir);
      }
    }

    /** Add the following file to the help index using the index writer.
     @param file the file to add to the help index
     @param useParentDir set to true if want to point file path to existing file in parent directory.
                         if file does not exist in parent directory, this flag has no effect.
     @throws IOException
     @throws InterruptedException
     @throws CorruptIndexException
     */
    private final void addDocToIndex (File file, boolean useParentDir) throws IOException, InterruptedException {
      if (debug) System.out.println("Help.Indexer: parsing " + file);
      Document doc = HTMLDocument.Document(file); // parse document
      String path  = file.getPath();
      String name  = file.getName();

      if (useParentDir) {
        int pos = path.lastIndexOf(File.separatorChar);
        pos = (pos<=0 ? -1 : path.lastIndexOf(File.separatorChar, pos-1));
        if (pos != -1) {
          String fileInParentDir = path.substring(0, pos+1) + name;
          File fileInParent = new File(fileInParentDir);
          if (fileInParent.exists()) {
            // point file path to file in parent directory (since it exists)
            String newValue = fileInParentDir.replace(File.separatorChar, '/');
            doc.removeField("path"); // remove previously added path
            doc.add(new Field("path", newValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
          }
        }
      }

      // get base filename (which would normally be Javadoc class API or HTML generated version of explain file)
      int dotIndex = name.lastIndexOf('.');
      if (dotIndex != -1) name = name.substring(0, dotIndex); // strip off file extension

      // if we have HTML explain file, strip out option tree and area from it's filename to get command name
      int expIndex = name.indexOf("_exp_");
      if (expIndex != -1) {
        name = name.substring(expIndex + "_exp_".length());
        // add command name in field "explain" to boost the search results for Commands
        doc.add(new Field("explain", name, Field.Store.YES, Field.Index.ANALYZED));
      }

      // add class/command name in field "midas" to boost the search results for Commands & Classes
      doc.add(new Field("midas", name, Field.Store.YES, Field.Index.ANALYZED));

      if (debug) System.out.println("Help.Indexer: adding " + path);
      writer.addDocument(doc);                    // add document to index
    }
  } // end class Indexer

  /** Help index searcher for querying/finding content in the lucene help index.
      This class public because it is used by {@link nxm.sys.libg.MJHelp}.
      @author Neon Ngo
      @since NeXtMidas 2.5.0
   */
  public static class Searcher {
    private IndexSearcher searcher;    // lucene index searcher
    private QueryParser   queryParser; // lucene query parser
    /** search the following fields in the index */
    private static final String[] search_fields = {"midas", "explain", "title", "contents"};

    /** Construct a help searcher for querying the content in the specified lucene help index.
        @param indexPath path to directory to lucene help index
        @throws IOException &nbsp;
        @since NeXtMidas 2.5.0
     */
    public Searcher (String indexPath) throws IOException {
      Map<String,Float> boost = new HashMap<>(); // NTN: I got key/value type from MultiFieldQueryParser source code

      boost.put("explain", Float.valueOf(6)); // boost results for the explain field by 6
      boost.put("midas",   Float.valueOf(4)); // boost results for the midas field by 4
      boost.put("title",   Float.valueOf(2)); // boost results for the title field by 2
      queryParser  = new MultiFieldQueryParser(CustomAnalyzer.LUCENE_VERSION, search_fields, Indexer.analyzer, boost);
      // set default query terms to be ANDed together
      queryParser.setDefaultOperator(QueryParser.Operator.AND);
      queryParser.setAllowLeadingWildcard(true);
      setIndexPath(indexPath);
      BooleanQuery.setMaxClauseCount(2048); // increase from default of 1024 so we can do query for "n*"; TODO: move to config file and/or add a switch to change this
    }

    /** Use Shell to display info/warn messages if Shell.getNmRoot() is set,
        otherwise display to standard output.
        @param msg String to display as an info message
        @param ex  if exception exists then this becomes a warning message
     */
    void displayMessage (String msg, Exception ex) {
      if (Shell.getNmRoot()!=null) {
        if (ex!=null) Shell.printStackTrace(msg, ex);
        else          Shell.info(msg);
      } else {
        String prepend = (ex!=null) ? "WARN: " : "INFO: ";
        System.out.println(prepend+msg);
        if (ex!=null) ex.printStackTrace();
      }
    }

    /** Set the path to help index for searches.
        @param indexDir path to directory containing the help index
    */
   private void setIndexPath (String indexDir) throws IOException {
     displayMessage("HELP: setting indexPath to " + indexDir, null);
     try {
       Directory directory = FSDirectory.open(new File(indexDir));
       searcher = new IndexSearcher(directory, true); // NOTE: this operation is slow for large
                                                      // indices (much slower than the search itself)
     } catch (IOException e) {
       // any error that happens is probably due to a permission problem or
       // non-existent index path or a corrupt index
       String msg = "Could not initialize help for index path='"+ indexDir +
                    "'. Did you run 'HELP/index' or do you have the appropriate file/dir permissions? " + e;
       displayMessage(msg, e);
       throw (e); // re-throw Exception up the chain
     }
   }

   /** Same as {@link #query(String, int)} with a max search result limit of 100.
       @param queryString search string to find in the help index
       @throws ParseException query string is invalid
       @deprecated Since NeXtMidas 2.7.1 use {@link #query(String, int)} instead
       @return List containing {@link Document} of the matching results. Returns an empty list if
               there are no matching results in the help index.
       @throws ParseException query string is invalid
    */
   @Deprecated public List<Document> query (String queryString) throws ParseException {
     return query(queryString, 100);
   }

   /** Query the help index to find matches for the specified query string.
       @param queryString search string to find in the help index
       @param maxResults  max search results to return. Use 0 or -1 for unlimited.
       @return List containing {@link Document} of the matching results. Returns an empty list if
               there are no matching results in the help index.
       @throws ParseException query string is invalid
    */
   public List<Document> query (String queryString, int maxResults) throws ParseException {
     ArrayList<Document> list = new ArrayList<>(); // list of results to return
     final int AUTO_WILDCARD_LEN = 5; // TODO make this configurable
     StringBuilder querySB = new StringBuilder(64);
     if (queryString != null && queryString.length() > 0) {
       // if only one search term, add a wildcard search for this term in the explain and midas fields
       // e.g. exp => exp || explain:exp* || midas:exp*
       if (queryString.indexOf(" ") == -1 && !queryString.endsWith("~") && !queryString.endsWith("^")) {
//       queryString = queryString + " || explain:" + queryString + "* || midas:" + queryString + "*"; // ORIG
         String term1 = (queryString.length() < AUTO_WILDCARD_LEN)? queryString : queryString + "*";
         querySB.append(term1).append(" || explain:").append(queryString).append("* || midas:").append(queryString).append('*');
       }
       else {
         String terms[] = queryString.split("\\s");
         for (String term : terms) {
           if (term.length() >= AUTO_WILDCARD_LEN && !term.endsWith("~") && !term.endsWith("^")) {
             querySB.append(term).append("* "); // auto wildcard terms AUTO_WILDCARD_LEN or longer
           } else {
             querySB.append(term).append(' ');
           }
         }
       }
       try {
         Query query = queryParser.parse(querySB.toString());     // parse the query string using the queryParser
         if (maxResults <= 0) maxResults = Integer.MAX_VALUE;     // effectively unlimited results
         TopDocs topDocHits = searcher.search(query, maxResults); // run the query
         int length = topDocHits.totalHits;
         list.ensureCapacity(length);                  // ensure capacity on results list so it does not have to resize

         for (ScoreDoc hit : topDocHits.scoreDocs) {
           int docId = hit.doc;
           Document doc = searcher.doc(docId);      // get the next document
           float score  = hit.score;                // get the document's hit/search score
           doc.add(new Field("score", Float.toString(score), Field.Store.NO, Field.Index.NOT_ANALYZED));
           list.add(doc);
         } // end for loop
       } catch (Exception ioe) {
         displayMessage("Caught Exception while searching for '" + queryString + "' ", ioe);
       }
     }
     return list;
   }
  } // end class Searcher


  /** CustomerAnalyzer that is the same as the StandardAnalyzer, except it uses single English
      alphabetical letters as stop words (i.e. "a", "b", "c", ..., "z").
      This class is only used internally by {@link Indexer} and {@link Searcher}.
      @since NeXtMidas 2.5.0
    */
  private static class CustomAnalyzer extends StandardAnalyzer {
    static final Version LUCENE_VERSION = Version.LUCENE_30;

    /** A set containing single English letters that are usually not useful for searching. */
    private static final Set<?> stopWords = StopFilter.makeStopSet(new String[] {
      "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
      "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
    });

    public CustomAnalyzer () {
      super(LUCENE_VERSION, stopWords);
    }
  } // end class CustomAnalyzer
}
