package nxm.sys.intr;

import java.io.File;             // Needed for File.separator
import java.util.ArrayList;
import java.util.Arrays;         // Needed to sort List
import java.util.List;
import java.util.regex.Pattern;

import nxm.sys.inc.Installation;
import nxm.sys.inc.LegacyMethod;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Build;        // Needed to distinguish from using nxm.sys.intr.Build
import nxm.sys.lib.Convert;
import nxm.sys.lib.CoreIO;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.IOResource;
import nxm.sys.lib.OptionTree;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Terminal;
import nxm.sys.lib.TextFile;
import tools.BuildProps;         /* Used to access build properties. */

/** Displays HELP for a given keyword or query string. HELP can create the help index via the /INDEX
    switch. HELP displays a graphical user interface to query the help index via the /GUI switch.
    Otherwise HELP is an interactive command that users can list the search results and the result
    contents in a NeXtMidas shell.

    @author unknown / J. Czechowski IV / Jay Hallam / Neon Ngo
 */
public class Help extends HelpSwap {
  /** NMROOT when this class was loaded */
  static final String NMROOT = Shell.getNmRoot();
  /** The location of the javadoc directory. */
  static final String JAVADOC_PATH = NMROOT + "htdocs" + File.separator + "api" + File.separator;
  /** The location of the help directory. */
  static final String HELP_PATH = NMROOT + "htdocs" + File.separator + "help" + File.separator;

  /** This is the default href for the style sheet used by help files.  */
  private static final String STYLE_SHEET_HREF = "../../nxm/sys/docs/style.css";

  private static final String mainFileName   = "mainhelp.hlp";
  private static final String configFileName = "mainhelp.cnf";

  /** main help config file */
  private TextFile fcnf;
  /** main help search file */
  private TextFile fmhs;
  /** all help/commands/documents page (HTML) for a particular option tree */
  private TextFile fall;
  /** The help directory ($NMROOT/htdocs/help) */
  private String   jhdir;

  private boolean inTable;

  /** pre-compiled pattern to test if valid command - alpha + (alphanumeric or ;) */
  private static final Pattern VALID_COMMAND_REGEX  = Pattern.compile("\\p{Alpha}[\\w;]*");

  /** Option tree containing help item */
  private String opt;
  /** option trees to build help files or for interactive CLI searching */
  private String[] opts;

  /** execution mode enum */
  private enum RunMode { INDEX, BUILD, GUI, LiteGUI, CLI, LegacyCLI };
  private RunMode runMode;

  /** @since NeXtMidas 3.3.4 */
  private void initBuildProps () {
    if (sysBuildProps == null) {
      sysBuildProps = BuildProps.getPropsFor("SYS");
    }
  }

  /** deault constructor */
  public Help () {}

  public int open () {
    super.open();
    // get parameters
    sysBuildProps  = null;
    inTable        = false;
    opt            = MA.getU("OPT", "ALL");
    debug          = MA.getState("/DEBUG");

    // determine option tree search path
    if (opt.equals("ALL")) {
      String path = (String)M.results.get("OPT.PATH");
      opts = StringUtil.cleanSplit(path,",");
    } else {
      opts = new String[] { opt };
    }

    if      (MA.getState("/INDEX"))        runMode = RunMode.INDEX;     // build help index for searching
    else if (MA.getState("/BUILD"))        runMode = RunMode.BUILD;
    else if (MA.getState("/GUI"))          runMode = RunMode.GUI;       // Interactive HelpGUI
    else if (MA.getState("/LITEGUI"))      runMode = RunMode.LiteGUI;   // Lite interactive HelpGUI
    else if (MA.getState("/LEGACY", true)) runMode = RunMode.LegacyCLI;
    else                                   runMode = RunMode.CLI;

    switch (runMode) { // since 3.3.4 (BUG 2763)
      case INDEX: case GUI: case LiteGUI: case CLI:
        initBuildProps(); break;
      default: break;
    }

    return NORMAL;
  }

  public int process () {
    if (runMode == RunMode.INDEX) return runLuceneIndex();
    else if (runMode == RunMode.GUI) return runLuceneGUI();
    else if (runMode == RunMode.LiteGUI) return runLuceneLiteGUI();
    else if (runMode == RunMode.BUILD) {
      // build the help system for the option tree (or ALL)
      String jddir = null;
      String[] pathList = { Shell.getNmRoot(),"htdocs","api" };

      if (MA.find("/LOCAL")) {
        if (opt.equals("ALL")) {
          M.error("Option name must accompany /LOCAL");
        }

        pathList[0] = Shell.getOptPath(opt,"");
        pathList[1]="docs";
        FileName fn = new FileName (pathList,"","");
        jddir = fn.getFullName();
      }

      if (jddir == null) {
        M.info("Building JavaDocs for opt=" + opt + " in " + JAVADOC_PATH);
      } else {
        M.info("Building JavaDocs for opt=" + opt + " in " + jddir);
      }
      Build.setMidasContext(M);
      Build.compileDocs(opt, jddir, debug);

      pathList[2] = "help";
      FileName fn = new FileName (pathList,"","");
      jhdir = fn.getFullName();
      M.info("Building help for opt=" + opt + " in " + jhdir);

      int status = 0;
      for (String opt : opts) {
        status += buildMainHelp(opt); // group this separate from below
      }
      for (String opt : opts) {
        status += buildHelp(opt);     // group this separate from mainHelp above
      }

      status += buildMasterIndex();
      if (status<0) {
        M.warning("Encountered " + (-status) + " problems while building Help.");
      }

      return FINISH;
    }
    else if (runMode == RunMode.LegacyCLI) {
      // handle interaction with user on terminal (CLI)
      String helpItemName = MA.getU("NAME","MAINHELP");
      doLegacySearch(helpItemName, opt, opts);
    }
    else runLuceneSearchInteraction();

    return FINISH;
  }

  public int close () {
    return super.close();
  }

  /** Handle terminal search functionality prior to moving to searching Lucene Help index.
      @param name Name of help item to search for
      @param opt Option tree containing help item
      @param opts
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private void doLegacySearch (String name, String opt, String[] opts) {
    ArrayList<Key> list = new ArrayList<>(128); // init List for search list

    // search for an explain file
    Args args = Shell.parseCommand (M,name);
    if (!args.name.equals("") && (opt.equals("ALL") || args.option.equals(opt))) {
      Key key = new Key();
      key.type = "EXPLAIN";
      key.label = args.name;
      key.opt = args.option;
      list.add(key);
    }

    // if this is MAINHELP splash screen reset default opts list
    if (name.equals("MAINHELP") && opt.equals("ALL")) opts = new String[] { "SYS" };

    // search the MainHelp files
    for (String optionTree : opts) {
      searchMainHelp(optionTree, name, list);
    }

    // output the list of matches
    M.println("Search for key="+name+" in opt="+opt+" yields "+list.size()+" entries:");  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
    if (list.size()>1) listEntries(list);

    // loop on what to do next
    for (int ilist=0; list.size()>0; ilist++) {
      String ans="N";
      if (list.size()>1)
        ans = M.terminal.getInput("Enter item number or [N]ext/Quit/List ?: ","");
      if (ans.length()==0) ans="N";
      else ans = ans.trim().toUpperCase();

      if (ans.startsWith("Q")) break;
      else if (ans.startsWith("L")) { listEntries(list); continue; }
      else if (ans.startsWith("N")) { if (ilist==list.size()) break; }
      else ilist = Convert.s2l(ans);
      ilist %= list.size();
      Key key = list.get(ilist);
      if (key.type.equals("EXPLAIN")) doExplainFileEntry (key.label,key.opt);
      else doHelpTreeEntry (key.file,key.label,key.opt);
    }
  }

  // look for a keyword match in the main help file
  private void searchMainHelp (String opt, String keyword, List<Key> list) {
    String fn = M.io.name("HLP","MAINHELP","HLP",opt);
    TextFile tf = new TextFile(M, fn);
    if (!tf.find(IOResource.DIR_INPUT)) return;
    if (!tf.open(TextFile.NOABORT)) return;
    String text,filename=null;
    while ((text=tf.read())!=null) {
      Key key = parseKey(text);
      if (key==null) continue;
      else if (key.type.equals("FILE")) {
        filename=key.label;
        key.file="MAINHELP"; key.opt=opt;
        if (key.label.indexOf(keyword)>=0) {
          // Added to prevent duplicate entries like ~File FLAGS in MAINHELP of ICE tree
          int iDupMain = mainHelpInList(list);
          if (iDupMain>=0) {
//System.out.println("MAINHELP ALREADY IN, NOT ADDING key->"+key);
          } else {
            list.add(key);
          }
        }
      }
      else if (key.type.equals("ENTRY")) {
        key.file=filename; key.opt=opt;
        if (key.label.indexOf(keyword)>=0) {
          // Added to prevent duplicate entries like ~File FLAGS in MAINHELP
          // of ICE tree
          if (key.file.equals("MAINHELP") && mainHelpInList(list) >=0) {
//System.out.println("MAINHELP ALREADY IN, NOT ADDING key->"+key);
          } else {
            list.add(key);
          }
        }
      }
    }
    tf.close();
  }

// START - Added to prevent duplicate entries like ~File FLAGS in MAINHELP of ICE tree
  /** Determine if MAINHELP is already in the list */
  private int mainHelpInList (List<Key> list) {
    int index = -1;
    for (int i=0; i<list.size(); i++) {
      Key key = list.get(i);
      if (key.file != null && key.file.equals("MAINHELP")) {
        index = i;
        break;
      }
    }
    return index;
  }
// END - Added to prevent duplicate entries like ~File FLAGS in MAINHELP of ICE tree

  /**
   * @param list Numbered list of search entries to the screen
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private void listEntries (List<Key> list) {
    M.more(Terminal.INIT);      // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    for (int i=0; i<list.size(); i++) {
      Key key = list.get(i);
      String type = "Help Item  ";
      if (key.type.equals("FILE")) type = "Help File  ";
      if (key.type.equals("EXPLAIN")) type = "Explain On ";
      String file = (key.file==null)? "": " file="+key.file;
      String index = " "+i+") "; if (i<10) index = " "+index;
      M.more(index+type+key.label+"   in"+file+" opt="+key.opt);  // since 3.7.1 - NXM-1813
    }
    M.more(Terminal.EXIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
  }

  /** Display an explain file entry */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private void doExplainFileEntry (String name, String opt) {
    String text, fname=M.io.name("EXP",name,"EXP",opt);
    TextFile tf = new TextFile(M, fname);
    if (!tf.open(TextFile.NOABORT)) return;
    M.more(Terminal.INIT);   // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    while (M.more() && (text=tf.read())!=null) M.more(text);
    M.more(Terminal.EXIT);
    tf.close();
  }

  /** Display a help tree entry */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private int doHelpTreeEntry (String filename, String keyword, String opt) {
    String fname, text=null, infname=filename;
    ArrayList<Key> list = new ArrayList<>(16);

    // construct the help file name
    if (filename.equals("DOCUMENTS")) {
      fname = M.io.name("HLP",keyword,"DOC",opt);
      if (!M.io.find(fname)) fname = M.io.name("DOCS",keyword,"DOC",opt);
    }
    else if (filename.equals("INCLUDES"))
      fname = M.io.name("INC",keyword,"INC",opt);
    else
      fname = M.io.name("HLP",filename,"HLP",opt);

    // open the designated help file
    TextFile tf = new TextFile(M,fname);
    if (!tf.open(BaseFile.INPUT|BaseFile.NOABORT)) {
      M.warning("HELP: Could not find "+tf.getName());
      return 0;
    }

    // advance to section line of interest
    if (keyword.equals("")) text = tf.read();
    else if (filename.equals("INCLUDES")) text = "Include File: "+fname;
    else if (filename.equals("DOCUMENTS")) text = "Document File: "+fname;
    else {  // build list of keys while looking for the section of interest
      while ((text=tf.read())!=null) {
        Key key = parseKey(text);
        if (key==null) continue;
        else if (key.type.equals("FILE") && infname.equals("MAINHELP")) {
          filename = key.label;
          if (key.label.indexOf(keyword)>=0)  {
            text="Help Category: "+opt+"_"+key.label; break; }
        }
        else if (key.type.equals("SECTION") || key.type.equals("FUNCTION")) {
          if (key.label.indexOf(keyword)>=0)  {
            text="Help Entry: "+opt+"_"+filename+"_"+key.label; break; }
        }
      }
    }

    // section not found - malformed help file
    if (text==null) M.error("Help Format Err: Section "+keyword+" not found in "+infname);

    // print this entry to screen
    M.more(Terminal.INIT);    // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    for ( M.more(text); M.more(); ) {
      text=tf.read();
      if (text==null) break;
      Key key = parseKey(text);
      if (key==null);
      // build new sub-level search list from this section while printing
      else if (key.type.equals("ENTRY")) { key.file=filename; list.add(key); }
      else if (key.type.equals("INCLUDE")) { doHelpTreeEntry(key.file, "", opt);  } // Added for BUG 2147
      else break;
      M.more(text);
    }
    M.more(Terminal.EXIT);
    if (list.size()>0) {   // query for further probing ?
      text = M.terminal.getInput("Item in "+keyword+" to explore ?: ","");
      text = text.toUpperCase();
    }
    tf.close();

    if (text==null || text.length()==0) return 0;  // no interest in continuing

    // we have a list of keys, check for interest and search
    int stat=0;
    for (Key key : list) {
      if (key.label.indexOf(text)>=0) {
        String ans = M.terminal.getInput("Explore "+key.label+" in "+
           opt+"_"+key.file+" [Y]/N/Q ?: ","");
        ans = ans.toUpperCase();
        if (ans.startsWith("N")) continue;
        if (ans.startsWith("Q")) return -1;
        stat = doHelpTreeEntry(key.file,key.label,opt);
      }
    }
    return stat;
  }

  // parse a line of help text for possible special keys
  private Key parseKey (String text) {
    text = text.replaceAll("\\s+", " "); // JC4: Replace all blocks of whitespace with a single space

    int i=0, j, len=text.length();
    for (; i<len && text.charAt(i)<=' '; i++);
    if (i==len) return null;
    Key key=null;
    if (text.charAt(i)=='~') {      // tilde section header
      key = new Key();
      for (j=++i; i<len && text.charAt(i)>' '; i++);
      key.type = text.substring(j,i).toUpperCase();
      for (; i<len && text.charAt(i)==' '; i++);
      for (j=i; j<len && text.charAt(j)!=' '; j++);
      if (key.type.equals("INCLUDE")) { key.file=text.substring(i,j); }  // JWB- ADDED FOR BUG 2147
      key.label = text.substring(i,j).toUpperCase();
      for (; j<len && (text.charAt(j)==' ' || text.charAt(j)=='-'); j++);
      key.text = (j<len)? text.substring(j):"";

      if(key.type.trim().length() == 0) {
        key = null;
      }
    } else if ((i=text.indexOf('-'))>0) {    // dash separated keyword entry
      key = new Key();
      key.type = "ENTRY";
      key.label = text.substring(0,i).trim().toUpperCase();
      for (j=0; j<key.label.length(); j++) // multiple words before dash ?
        if (key.label.charAt(j) <= ' ') return null;
      key.text = text.substring(i+1).trim();
    }
    return key;
  }

  private static class Key {
    String type;  // type of key (FILE,SECTION,ENTRY,EXPLAIN...)
    String file;  // filename of help file containing info
    String label;  // section header or other keyword of interest
    String text;  // additional text for label description
    String opt;    // Option tree this was found in

    public String toString() {
      return "Key: type="+type+" file="+file+" label="+label+" text="+text+" opt="+opt;
    }
  }

   /** Build the Help support files for a given option tree. The result of
      this method is the generation of a number of files into the hlp
      directory of the option tree.

      In addition, a topic directory will be created in the hlp area which
      contains the HTML files for the individual help topics.  These are
      referenced by the nxmMaster files.

      @param opt The name of a single option tree to build the Java Help files for.
  */
  private int buildMainHelp (String opt) {

    String path = M.io.name("HLP","","",opt);
    ArrayList<Key> list = new ArrayList<>(16);
    String text;

    fmhs = new TextFile(M, path+mainFileName );   // no risk of filename being all UC
    fcnf = new TextFile(M, path+configFileName );

    if (fcnf.find(IOResource.DIR_INPUT)) {
      M.info("File mainhelp.cnf found, generating mainhelp.hlp for "+opt);
      if (!fcnf.open(BaseFile.INPUT|BaseFile.NOABORT)) return -1;
      if (!fmhs.open(BaseFile.OUTPUT|BaseFile.NOABORT)) return -1;
      fmhs.writeln("~File MAINHELP");
      while ((text=fcnf.read())!=null) {
        fmhs.writeln(text);
        Key key = parseKey(text);
        if (key==null) continue;
        list.add(key);
      }
      for (Key key : list) {
        buildMainHelpFile(opt,key.label);
      }
    }
    else if (fmhs.find(IOResource.DIR_INPUT)) {
      M.info("File mainhelp.cnf not found, using mainhelp.hlp for "+opt);
    }
    else {
      M.info("No mainhelp.cnf or mainhelp.hlp found for "+opt);
    }
    fmhs.close();
    fcnf.close();
    return 0;
  }

  // process one hlp file for buildMainHelp
  private int buildMainHelpFile (String opt, String name) {
    String text; boolean header=true, needOneLiner=false;

    String fn = M.io.name("HLP",name,"HLP",opt);
    TextFile tf = new TextFile(M, fn);
    if (!tf.open(TextFile.NOABORT)) return -1;
    tf.setIncludeString("~Include");

    fmhs.writeln("~File "+name);
    fmhs.writeln("");

    while ((text=tf.read())!=null) {
      Key key = parseKey(text);
      if (key==null) {
        if (needOneLiner && text.length()>0 && text.charAt(0)!=' ') {
          int i=text.indexOf(". ");  if (i>0) text = text.substring(0,i);
          fmhs.writeln(text); needOneLiner=false;
        }
      }
      else if (key.type.equals("MAIN")) {
        header=true; text=null;
      }
      else if (key.type.equals("SECTION") || key.type.equals("FUNCTION")) {
        if (needOneLiner) fmhs.writeln("unknown");
        needOneLiner = (key.text.length()==0);
        if (needOneLiner) fmhs.write("  "+key.label+" - ");
        else fmhs.writeln("  "+key.label+" - "+key.text);
        header = false;
      }
      else header = false;
      if (header && text!=null) fmhs.writeln(text);
    }
    fmhs.writeln("");
    tf.close();
    return 0;
  }

  /** Build all of the Help and Html pages for specified option tree. */
  private int buildHelp (String opt) {

    M.info("Building Help/Explain/Documents for "+opt);

    String path = M.io.name("HLP","","",opt);
    String optLC = opt.toLowerCase();
    ArrayList<Key> list = new ArrayList<>(16);
    String text, target;

    // TODO: NTN: I don't think we need the next two lines of Gordon code
    // if main help file exists, then make sure we can open it for read, otherwise continue processing
    fmhs = new TextFile(M, path+mainFileName );
    if (fmhs.find(IOResource.DIR_INPUT) && !fmhs.open(TextFile.INPUT|TextFile.NOABORT)) return -1;

    // build the top level splash screen
    target = optLC+"_tree_index";
    fall = getHelpFile(target);
    writeHTMLHeader (fall,opt+" Top Help",opt+" Option Tree",null);
    TextFile fver = new TextFile(M, M.io.name(".","version","txt",opt) );
    if (fver.open(TextFile.INPUT|TextFile.NOABORT)) {
      fall.writeln("<p align='center'>");
      while ((text=fver.read())!=null) {
        if(!text.startsWith("#")) {
          fall.writeln("  <code>" + text.replaceAll("  ", " &nbsp;") + "</code><br>");
        }
      }
      fall.writeln("</p>");
      fall.writeln("");
      fver.close();
    }
    writeHTMLTableHeader (fall);
    // help areas
    writeHTMLBullet (fall,"CONCEPTS",optLC+"_hlp_all.html","Operational Concepts - Help Tree");
    writeHTMLBullet (fall,"COMMANDS",optLC+"_exp_all.html","Configured Commands - Explain Tree");
    writeHTMLBullet (fall,"DOCUMENTS",optLC+"_doc_all.html","Supporting Documents - Docs Tree");
    // javadoc areas
    text="../api/nxm/"+optLC;
    if (CoreIO.fexists(jhdir+"../api/nxm/"+optLC+"/lib",true))
      writeHTMLBullet (fall,"LIBRARIES",text+"/lib/package-summary.html","Libraries for Basic Functions");
    if (CoreIO.fexists(jhdir+"../api/nxm/"+optLC+"/libg",true))
      writeHTMLBullet (fall,"GRAPHICS",text+"/libg/package-summary.html","Libraries for Graphics Functions");
    if (CoreIO.fexists(jhdir+"../api/nxm/"+optLC+"/libm",true))
      writeHTMLBullet (fall,"MATH",text+"/libm/package-summary.html","Libraries for Mathematical Functions");
    if (CoreIO.fexists(jhdir+"../api/nxm/"+optLC+"/lib",true))
      writeHTMLBullet (fall,"LIBRARIES",text+"/lib/package-summary.html","Libraries for Basic Functions");
    // release notes
    if (M.io.fexists(M.io.name("hlp","release","hlp",opt)))
      writeHTMLBullet (fall,"RELEASE",optLC+"_hlp_release.html","Release Notes for Current Version");
    // homepage bullet
    String homepage = M.io.name("docs","index","html",opt);
    if (M.io.fexists(homepage)) homepage="../../nxm/"+optLC+"/docs/index.html"; else homepage="";
    homepage = MA.getCS("/HOMEPAGE",homepage);
    if (homepage.length()>0) writeHTMLBullet (fall,"HOMEPAGE",homepage,"Option Tree Home Page");
    writeHTMLTableFooter (fall);
    // copyright/licensing notice
    TextFile fin,fout; int ok=0;
    fin = new TextFile(M, M.io.name(".","copyright","txt",opt) );
    fout = new TextFile(M, jhdir+optLC+"_copyright.txt");
    if (fin.find(IOResource.DIR_INPUT)) ok += fin.copy(fout);
    fin = new TextFile(M, M.io.name(".","license","txt",opt) );
    fout = new TextFile(M, jhdir+optLC+"_license.txt");
    if (fin.find(IOResource.DIR_INPUT)) ok += fin.copy(fout);
    if (ok>0) {
      fall.writeln("<br><div style=\"text-align:center;\"><a href='"+optLC+"_copyright.txt'>Copyright</a>");
      fall.writeln(" / <a href='"+optLC+"_license.txt'>License</a></div>");
    }
    writeHTMLFooter (fall);
    fall.close();

    // build list of help files
    buildMainHelpFiles(opt,list);

    // build help for all help files
    target = optLC+"_hlp_all";
    fall = getHelpFile(target);
    writeHTMLHeader (fall,"All Concepts",opt+" Concept Index",null);
    writeHTMLTableHeader (fall);
    for (Key key : list) {
      buildHelpFile(opt,key);
    }
    writeHTMLTableFooter (fall);
    writeHTMLFooter (fall);
    fall.close();

    // build help for all commands
    target = optLC+"_exp_all";
    fall = getHelpFile(target);
    writeHTMLHeader (fall,"All Commands",opt+" Command Index",null);
    writeHTMLTableHeader (fall);
    writeHTMLCommandTableTitle(fall);
    for (int i=0; i<M.dictionary.size; i++) {
      Parser command = new Parser ( M.dictionary.get(i) );
      buildExplainFile(opt,command);
    }
    writeHTMLTableFooter (fall);
    writeHTMLFooter (fall);
    fall.close();

    // build help for all documents
    String[] files; String lpath;
    target = optLC+"_doc_all";
    fall = getHelpFile(target);
    writeHTMLHeader (fall,"All Documents",opt+" Document Index",null);
    writeHTMLTableHeader (fall);
    path = M.io.name("HLP","","",opt);
    files = M.io.listFiles(path,"*","doc");
    lpath = "../../nxm/"+optLC+"/hlp";
    if (!CoreIO.fexists(jhdir+lpath,true)) lpath = null;
    if (files != null) {
      writeHTMLBullet (fall, "|-- hlp/", lpath, "files under HLP area"); // since 3.3.4 (BUG 2765)
      Arrays.sort(files, String.CASE_INSENSITIVE_ORDER); // since 3.3.4 (BUG 2765)
      for (String file : files) {
        buildDocumentFile(opt,path,file,lpath);
      }
    }
    path = M.io.name("DOCS","","",opt);
    files = M.io.listFiles(path,"*","*");
    lpath = "../../nxm/"+optLC+"/docs";
    if (!CoreIO.fexists(jhdir+lpath,true)) lpath = null;
    if (files != null) {
      writeHTMLBullet (fall, "|-- docs/", lpath, "files under DOCS area"); // since 3.3.4 (BUG 2765)
      Arrays.sort(files, String.CASE_INSENSITIVE_ORDER);  // since 3.3.4 (BUG 2765)
      for (String file : files) {
        buildDocumentFile(opt, path, file, lpath);
      }
    }
    writeHTMLTableFooter (fall);
    writeHTMLFooter (fall);
    fall.close();

    fmhs.close();
    return 0;
  }

  // process the mainhelp.hlp file for top level screen
  private int buildMainHelpFiles (String opt, List<Key> list) {

    String path = M.io.name("HLP","MAINHELP","HLP",opt);
    fmhs = new TextFile(M, path);
    if (!fmhs.find(IOResource.DIR_INPUT)) return 0;
    if (!fmhs.open(TextFile.NOABORT)) return -1;

    String optLC = opt.toLowerCase();
    String target = optLC+"_hlp_main";
    TextFile fhtm = getHelpFile(target);
    writeHTMLHeader (fhtm,null,"Help: "+opt+"_MAIN",null);
    fhtm.writeln("<pre>");
    String text, section=null;

    fmhs.read(); // read past ~File MAINHELP
    while ((text=fmhs.read())!=null) {
      Key key = parseKey(text);
      if (key==null) fhtm.writeln(textToHtml(text,opt,"HLP"));
      else if (key.type.equals("FILE")) {
        section = key.label;
        fhtm.writeln("</pre>");
        writeHTMLFooter(fhtm);
        fhtm.close();
        target = optLC+"_hlp_"+key.label.toLowerCase();
        fhtm = getHelpFile(target);
        writeHTMLHeader (fhtm,null,"Help: "+opt+"_"+key.label,key.text);
        fhtm.writeln("<pre>");
      }
      else if (key.type.equals("ENTRY")) {
        if (section==null) list.add(key);
        String starget = target+"_"+key.label.toLowerCase();
        writeHTMLBullet (fhtm,key.label,starget+".html",key.text);
      }
    }
    fhtm.writeln("</pre>");
    writeHTMLFooter(fhtm);
    fhtm.close();
    fmhs.close();
    return 0;
  }

  // process one hlp file for buildHelp
  private int buildHelpFile (String opt, Key key) {

    String text,starget="";
    boolean section=false;
    String name = key.label;
    String optLC = opt.toLowerCase();
    TextFile fhtms;

    String fn = M.io.name("HLP",name,"HLP",opt);
    TextFile tf = new TextFile(M, fn);
    if (!tf.open(TextFile.NOABORT)) return -1;
    tf.setIncludeString("~Include");

    // tag the file created in buildMainHelpFiles
    String target = optLC+"_hlp_"+name.toLowerCase();
    writeHTMLBullet (fall,name,target+".html",key.text);
    TextFile fhtm = getHelpFile(target, false);

    while ((text=tf.read())!=null) {
      key = parseKey(text);
      if (key==null);
      else if (key.type.equals("ENTRY"));
      else if (key.type.equals("END")) text=null;
      else if (key.type.equals("COMMENT")) text=null;
      else if (key.type.equals("FILE")) text="";
      else if (key.type.equals("MAIN")) {
        if (section) buildHelpFileCloseSection(fhtm,/*ftoc*/null);
        section = false;
      }
      else if (key.type.equals("SUBSECTION")) {
        fhtm.writeln("<a id=\""+key.label+"\">");
        text = key.label + key.text;
      }
      else {
        if (section) buildHelpFileCloseSection(fhtm,/*ftoc*/null);
        starget = target+"_"+key.label.toLowerCase();
        fhtm = getHelpFile(starget);
        if (key.text.length()==0) key.text = getHelpKeyText(opt,name,key.label);
        writeHTMLHeader (fhtm,null,"Help: "+opt+"_"+name+"_"+key.label,key.text);
        fhtm.writeln("<pre>");
        if (key.type.equals("FUNCTION")) text = text.substring(1);
        else text = null;
        section = true;
      }
      if (section && text!=null) fhtm.writeln(textToHtml(text,opt,"HLP"));
    }
    if (section) buildHelpFileCloseSection(fhtm,/*ftoc*/null);
    tf.close();
    return 0;
  }

  // helper repeat function for buildHelpFile
  private void buildHelpFileCloseSection (TextFile fhtm, TextFile ftoc) {
    fhtm.writeln("</pre>");
    writeHTMLFooter(fhtm);
    fhtm.close();
  }

  // build HTML version of an explain file
  private int buildExplainFile (String opt, Parser command) {

    String name = command.get(1); // name looks like COMMAND;OPT:n
    char support = command.get(2).charAt(0);
    int lname = name.indexOf(';');
    int lendName = name.indexOf(':');
    if (lendName < 0) lendName = name.length();
    String option = name.substring(lname+1,lendName);
    if (!option.equals(opt)) return 0;

    name = name.substring(0,lname);
    TextFile fexp = new TextFile(M, M.io.name("EXP",name,"exp",opt) ); // UC cmd names -> LC argument to TextFile
    if (!fexp.open(BaseFile.INPUT|BaseFile.NOABORT)) return 0;
    fexp.setIncludeString("~Include");
    String optLC = opt.toLowerCase();
    String nameLC = name.toLowerCase();
    String target = optLC+"_exp_"+nameLC;
    TextFile fhtm = getHelpFile(target);

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //String text = fexp.read();
    //Key key = parseKey(text);
    //writeHTMLHeader (fhtm,"Command Help -"+name,name,key.text);
    //fhtm.writeln("<pre>");
    //fhtm.writeln(textToHtml(text,opt,"EXP"));
    //while ( (text=fexp.read()) != null) {
    //  fhtm.writeln(textToHtml(text,opt,"EXP"));
    //}
    ArrayList<String> fileText = new ArrayList<>();
    String text;
    while ((text=fexp.read()) != null) {
      fileText.add(text);
    }

    Key key = null;
    try {
      key = parseKey(fileText.get(0));
      /* Try to catch multi-line headers, e.g.:
           APPSERVER - Server macro allowing clients to start/stop/monitor Midas macros
                       on this node.
       */
      final String leadingSpace = StringUtil.padRight("", key.label.length());
      for (int i = 1; i < fileText.size(); i++) {
        final String txt = fileText.get(i);

        if (txt.startsWith(leadingSpace) && (!txt.trim().startsWith("<"))
                                        && (!txt.trim().startsWith("["))) {
          key.text += " " + txt.trim();
        } else {
          break;
        }
      }
      /* Make it a proper sentence: first letter capitalized, ends with a period. */
      key.text = key.text.trim();
      if (key.text.length() > 1) {
        key.text = key.text.substring(0,1).toUpperCase() + key.text.substring(1)
                 + ((key.text.endsWith(".")? "": "."));
      }
    } catch (Exception ex) {
      M.warning("Unable to build html from explain file for "+name+" command");
      //ex.printStackTrace();
    }
    String fileTextString = "Unknown - invalid or missing explain file";
    if (key != null) {
      fileTextString = key.text;
    }
    /* Write the header. */
    writeHTMLHeader (fhtm,"Command Help -"+name,name,fileTextString);
    /* Write the text. */
    fhtm.writeln("<pre>");
    for (String str : fileText) {
      fhtm.writeln(textToHtml(str,opt,"EXP"));
    }
    ////////////////////////////////////////////////////////////////////////////////////////////////
    String srcCodeName   = null;            // Name of the source file (e.g. "temp.java").
    String testMacroName = "test_"+nameLC;  // Name of the test macro - NO EXTENSION (e.g. "test_temp").
    String className     = null;            // The name of the class (null for non-Java).
    String area          = null;            // The option tree area for the command.
    boolean noTest = false;

    switch (support) {
      case 'M': area = "mcr";  srcCodeName = nameLC+".mm";  break;
      case 'H': area = "host"; srcCodeName = nameLC+".for"; break;
      case 'I': area = "intr"; className = name.charAt(0)+nameLC.substring(1); break;
      case 'P': area = "prim"; className = nameLC; break;
      default:  area = "prim"; className = nameLC; break;
    }

    /////////// CHECK FOR TEST MACRO
    TextFile configFile = new TextFile(M, "nxm."+opt+".test.test_config.tbl");
    String   testConfig = null;
    String   optOfTestMacro = opt;
    if (configFile.exists()) {
      nxm.sys.lib.Table configTbl = new nxm.sys.lib.Table(configFile);
      testConfig = configTbl.getS(name);
    }
    if (testConfig != null) {
      if (testConfig.equals("NOTEST")) {
        noTest = true;
      } else if (testConfig.equals("WEB") || testConfig.equals("BROKEN")) {
        // Test is fine, just not normally run.
      } else {
        testMacroName = "test_" + testConfig.toLowerCase(); // Tested by other macro.
        int iSemicolon = testMacroName.lastIndexOf(';');    // since 3.1.1 to support COMMAND;OTHER_OPT syntax
        if (iSemicolon > 0) {
          int optIndex = iSemicolon + 1;
          if (testMacroName.length() > optIndex) optOfTestMacro = testMacroName.substring(optIndex);
          testMacroName  = testMacroName.substring(0, iSemicolon);
        }
      }
    }

    /////////// SET PATH VARIABLES
    if (className != null) { srcCodeName = className+".java"; } // File name based on class name
    String srcPath  = relativeToDocDir(OptionTree.getOptPath(opt,area),   srcCodeName);
    String testPath = OptionTree.getOptPath(optOfTestMacro,"test");
    String relativeToTestPath = relativeToDocDir(testPath, testMacroName+".mm");
    String expPath  = opt.toLowerCase()+"_exp_"+name.toLowerCase()+".html";
    String docPath  = null;

    if (className != null) { // If Java, set names based on name of class.
      String dir = getJavadocDir(opt) + File.separator + "nxm"
                                      + File.separator + opt.toLowerCase()
                                      + File.separator + area;
      srcCodeName = className+".java";
      docPath = relativeToDocDir(dir, className+".html");
    }
    if (!noTest && !M.io.exists(testPath + testMacroName+".mm")) {
      relativeToTestPath = null; // No test macro found
    }

    fhtm.writeln("</pre>");
    writeHTMLFooter(fhtm);
    fall.writeln("<tr>");
    fall.writeln("  <td><a href='"+expPath+"'>"+name+"</a></td>");
    fall.writeln("  <td>"+fileTextString+"</td>");
    fall.writeln("  <td align='center'>"+support+"</td>");
    if (docPath != null) {
      fall.writeln("  <td align='center'><a href='"+docPath+"'>doc</a></td>");
    } else {
      fall.writeln("  <td align='center'><a href='"+expPath+"'>doc</a></td>"); // Default to exp file.
    }
    fall.writeln("  <td align='center'><a href='"+srcPath+"'>src</a></td>");
    if (noTest) {
      fall.writeln("  <td align='center'>n/a</td>");
    } else if (relativeToTestPath != null) {
      fall.writeln("  <td align='center'><a href='"+relativeToTestPath+"'>"+testMacroName+"</a></td>");
    } else {
      fall.writeln("  <td align='center'><i>missing</i></td>");
    }
    fall.writeln("</tr>");
    fhtm.close();
    fexp.close();
    return 0;
  }

  /** Gets a relative link to destDir from the documentation dir. */
  private String relativeToDocDir (String destDir, String fname) {
    java.io.File dest   = new java.io.File(destDir);
    java.io.File docDir = new java.io.File(getHelpDir());
    String dir = FileUtil.getRelativeDir(docDir, dest);

    dir = dir.replace(java.io.File.separatorChar, '/'); // Convert to URL syntax

    return dir + fname;
  }

  // build Help links for the document files
  private int buildDocumentFile (String opt, String path, String dfname, String lpath) {
    FileName fn = new FileName(path+dfname,true);
    String name = fn.getRoot()+"."+fn.getExt();
    String url = fn.getFullName();
    if (lpath!=null) url = lpath + "/" + name;
    writeHTMLBullet (fall,name,url,"document file");
    return 0;
  }

  /** Find the help file reference for an embedded see also help entry. */
  private String getHelpTarget (String name) {
    String retValue = "sys_hlp_help_notfound";
    String text, section=null;
    String path = (String)M.results.get("OPT.PATH");
    String[] opts = StringUtil.cleanSplit(path,",");
    for (int i=opts.length-1; i>=0; i--) {
      String opt = opts[i];
      String fn = M.io.name("HLP","MAINHELP","HLP",opt);
      TextFile tf = new TextFile(M, fn);
      if (!tf.find(IOResource.DIR_INPUT)) continue;
      if (!tf.open(TextFile.NOABORT)) continue;
      while ((text=tf.read())!=null) {
        Key key = parseKey(text);
        if (key==null);
        else if (key.type.equals("FILE")) {
          section = key.label;
          if (name.equalsIgnoreCase(key.label))
            retValue = opt+"_hlp_"+section;
        }
        else if (key.type.equals("ENTRY")) {
          if (name.equalsIgnoreCase(key.label))
            retValue = opt+"_hlp_"+section+"_"+key.label;
        }
      }
      tf.close();
    }
    return retValue;
  }

  /** Find the help file reference for a Help Key without text.

      @param opt  The NeXtMidas option.
      @param file The file name.
      @param name The the label in the file.

      @todo Better explain this method.
   */
  private String getHelpKeyText (String opt, String file, String name) {
    String text; boolean section=false;
    String fn = M.io.name("HLP","MAINHELP","HLP",opt);
    TextFile tf = new TextFile(M, fn);
    if (!tf.open(TextFile.NOABORT)) return "noMainHelp";
    while ((text=tf.read())!=null) {
      Key key = parseKey(text);
      if (key==null);
      else if (key.type.equals("FILE")) {
        section = file.equalsIgnoreCase(key.label);
      }
      else if (key.type.equals("ENTRY")) {
        if (section && name.equalsIgnoreCase(key.label)) return key.text;
      }
    }
    tf.close();
    return "unknown";
  }

  /** Build a master index pointing to all configured option trees.

      @return Always returns <code>0</code>.
   */
  private int buildMasterIndex () {
    String[] options = Shell.getKnownOptionTrees();
    fall = getHelpFile("index");
    writeHTMLHeader (fall,null,"NeXtMidas Master Help Index","Configured Option Trees");
    fall.writeln("<div style=\"text-align:center;\"><a href='../search.html' target='_top'>Show Search Frame</a> &nbsp; " +
        "        <a href='index.html' target='_top'>Hide Search Frame</a></div>");

    writeHTMLTableHeader (fall);
    for (String opt : options) {
      String fileName = opt.toLowerCase() + "_tree_index.html";
      writeHTMLBullet (fall, opt, fileName, "The " + opt + " option tree");
    }
    writeHTMLTableFooter (fall);

    // JC4: START HACK: Add in documentation links.
    final String verClass    = Installation.NM_VERSION.substring(0,
                                Installation.NM_VERSION.lastIndexOf('.')+1)+"X";
    final String releaseFile = "ReleaseNotes_" + verClass.replace('.','_') + ".html";
    final String releaseName = "Release Notes " + verClass;

    fall.writeln("<h3 align='center'>General NeXtMidas Documentation</h3>");
    fall.writeln("<table border='1' cellpadding='2' cellspacing='0' style=\"margin-left:auto;margin-right:auto;\">");
    fall.writeln("  <tr>");
    fall.writeln("    <td><a href='../../nxm/sys/docs/" + releaseFile + "'>RN</a></td>");
    fall.writeln("    <td>" + releaseName + "</td>");
    fall.writeln("  </tr>");
    fall.writeln("  <tr>");
    fall.writeln("    <td><a href='../api/index.html'>API</a></td>");
    fall.writeln("    <td>Source Code Documentation</td>");
    fall.writeln("  </tr>");
    fall.writeln("  <tr>");
    fall.writeln("    <td><a href='../usersguide/index.html'>USER GUIDE</a></td>");
    fall.writeln("    <td><i>NeXtMidas User's Guide</i></td>");
    fall.writeln("  </tr>");
    fall.writeln("  <tr>");
    fall.writeln("    <td><a href='../training/index.html'>TRAINING</a></td>");
    fall.writeln("    <td><i>NeXtMidas Training Class</i></td>");
    fall.writeln("  </tr>");
    fall.writeln("  <tr>");
    fall.writeln("    <td><a href='../../nxm/sys/docs/InstallationGuide.html'>INSTALL</a></td>");
    fall.writeln("    <td><i>NeXtMidas Installation Guide</i></td>");
    fall.writeln("  </tr>");
    fall.writeln("</table>");
    // JC4: END HACK

    writeHTMLFooter (fall);

    fall.close();
    return 0;
  }

  /** Open a file in the JavaHelp area for writing.
      @param targetName The name of the file to open.
      @param open       If it should be opened.
      @return The text file.
   */
  private TextFile getHelpFile (String targetName, boolean open) {
    TextFile tf = new TextFile(M, jhdir+targetName+".html");

    if (open) tf.open(BaseFile.OUTPUT|BaseFile.MKDIR|BaseFile.FORCEABORT);
    return tf;
  }

  /** Open a file in the JavaHelp area for writing. This is the same as
      <code>getHelpFile(target,true)</code>.

      @param target The name of the file to open.
      @return The text file.
      @see #getHelpFile(String,boolean)
   */
  private TextFile getHelpFile (String target) {
    return getHelpFile(target, true);
  }

  /** Translate a line of text to html.

      @param text The initial text.
      @param opt  The NeXtMidas option area.
      @param area The NeXtMidas area.

      @return The translated string.
   */
  private String textToHtml (String text, String opt, String area) {
    try {
      text = StringUtil.toHTML(text);
      text = translateSeeAlso(text,opt,area);
    } catch (Exception e) {
      // ignore and just use the inputted text
      M.warning("Help.textToHtml - Cannot translate the following text: " + text);
      if (debug) M.warning(e);
    }
    return text;
  }

  /** Search for a "See Also" and its synonyms and make the appropriate translation.
      @param text The text to search.
      @param opt  <i>This is not used as this time.</i>
      @param area The area this is in.
      @return The results.
      @todo Better explain this method.
   */
  private String translateSeeAlso (String text, String opt, String area) {
    //JC4 - Next line removed to maintain consistency with raw explain file
    //text = text.trim();
    int i,j,k=text.length();
    i=text.toUpperCase().indexOf("SEE");
    if (i<0) return text;
    i+=3;                                              // past "SEE"
    if (text.indexOf(':',i)==i) i++;                   // past ':' for for "SEE:"
    while(text.indexOf(' ',i)==i) i++;                 // past spaces
    if (text.toUpperCase().indexOf("ALSO",i)==i) i+=4; // past ALSO
    if (text.indexOf("the ",i)==i) i+=4;               // past "the"
    if (i>=k) return text;                             // nothing after "see also" (return)
    if (text.indexOf(':',i)==i) i++;                   // past ':' again for "SEE ALSO:"
    if (i>=k) return text;                             // nothing after "see also:" (return)

    j=text.indexOf(". ",i); if (j<i) j = text.length();
    for (;text.charAt(j-1)==' '|| text.charAt(j-1)=='.'; j--);
    String stext = text.substring(0,i);
    for (k=j; i<k; i=j) {
      // find the end of this parse
      for (;i<k && text.charAt(i)==' '; i++,stext+=" "); // past spaces
      if (i>=k) break;  // nothing else after see also
      j=text.indexOf(',',i); if (j<0||j>k) j=k;  // to comma or end of line

      String[] tokens = StringUtil.cleanSplit(text.substring(i,j)," ");

      // handle special cases: see command explain file, or see entry help file
      if (tokens.length>=3 && tokens[2].equalsIgnoreCase("FILE")) { //Made >= instead of >
        if (tokens[1].equalsIgnoreCase("EXPLAIN")) area = "EXP";
        else if (tokens[1].equalsIgnoreCase("EXP")) area = "EXP";
        else if (tokens[1].equalsIgnoreCase("HELP")) area = "HLP";
        else break;
        name = tokens[0];
        j=k=i+tokens[0].length()+tokens[1].length()+tokens[2].length()+2;
      }
      // handle special cases: see explain on command, or see help on entry
      else if (tokens.length>=3 && tokens[1].equalsIgnoreCase("ON")) { //Made >= instead of >
        if (tokens[0].equalsIgnoreCase("EXPLAIN")) area = "EXP";
        else if (tokens[0].equalsIgnoreCase("EXP")) area = "EXP";
        else if (tokens[0].equalsIgnoreCase("HELP")) area = "HLP";
        else break;
        name = tokens[2];
        j=k=i+tokens[0].length()+tokens[1].length()+tokens[2].length()+2;
      }
      else if (tokens.length==3) {
        if (tokens[1].equalsIgnoreCase("EXPLAIN")
            || tokens[1].equalsIgnoreCase("EXP")) {
          area = "EXP";
        }
        else if (tokens[1].equalsIgnoreCase("HELP")){
          area = "HLP";
        }
        else break;
        name=tokens[2]; opt=tokens[0];
        j=k=i+tokens[0].length()+tokens[1].length()+tokens[2].length()+2;
      }
      else if (tokens.length==2) {
        if (tokens[0].equalsIgnoreCase("EXPLAIN")
            || tokens[0].equalsIgnoreCase("EXP")) {
          area = "EXP";
        }
        else if (tokens[0].equalsIgnoreCase("HELP")){
          area = "HLP";
        }
        else break;
        name=tokens[1];
        j=k=i+tokens[0].length()+tokens[1].length()+1;
      }
      else {
        boolean resolved = false;
        if (tokens.length>3) {
          if (tokens[0].equalsIgnoreCase("EXPLAIN")
              || tokens[0].equalsIgnoreCase("EXP")) {
            area = "EXP";
            resolved = true;
          }
          else if (tokens[0].equalsIgnoreCase("HELP")){
            area = "HLP";
            resolved = true;
          }
          if (resolved)
          {
            name=tokens[1];
            j=k=i+tokens[0].length()+tokens[1].length()+1;
          }
        }
        if (tokens.length>=1 && !resolved) {
          name=tokens[0];
          if (tokens.length>1) j=k=i+tokens[0].length();
          resolved = true;
          //Moved this code down after stripping punctuation
          //if (name.toUpperCase().indexOf("NXM.") >= 0) {
          // Take care of nxm.<opt>.<area>.<name>.[<ext>]
          //  subtokens = StringUtil.cleanSplit(text.substring(i,j),".");
          //}
        }//try to resolve see also when many tokens follow
        if (!resolved)
        {
          break;  // wrong number of tokens, ignore
        }
      }

      //Finally, remove trailing periods and spaces from name
      name = name.trim();
      //while (name.endsWith(".")){
      //  name = name.substring(0,name.length()-1);
      //}

      //remove trailing punctuation from name--1
      String[] parts = name.split("\\p{Punct}");

      int punct = 0;
      if((parts.length > 0) && (parts[0].length() != name.length())) {
        int end = name.lastIndexOf(parts[parts.length-1]) + parts[parts.length-1].length();
        punct = name.length() - end;
        name = name.substring(0,end);
        j=j-punct;
      }

      String[] subtokens = null; // Tokens for nxm.<opt>.<area>.<name>.[<ext] syntax
      if (name.toUpperCase().indexOf("NXM.") >= 0) {
        // Take care of nxm.<opt>.<area>.<name>.[<ext>]
        subtokens = StringUtil.cleanSplit(text.substring(i,j),".");
      }

      if (area.equalsIgnoreCase("EXPL")) area = "EXP";
      if (area.equalsIgnoreCase("HELP")) area = "HLP";
      if (area.equalsIgnoreCase("EXP")) {   // expand possible abbreviation
        if (VALID_COMMAND_REGEX.matcher(name).matches()) { // if name is a valid command
          Args args = Shell.parseCommand (M,name);         // lookup the command
          name=args.name; opt= args.option;
        }
      }

      String target = opt+"_"+area+"_"+name;
      if (area.equalsIgnoreCase("HLP")) {   // find help target
        target = getHelpTarget (name);
      }

      if (subtokens != null) {
        if (subtokens.length > 3 ) {
          // Take care of nxm.<opt>.<area>.<name>.[<ext>]
          String subExt  = "html";
          String subOpt  = subtokens[1].toLowerCase();
          String subArea =  subtokens[2].toLowerCase();
          String subName =  subtokens[3];
          String subPath = "../api/"+subOpt+"/nxm/";
          if (subtokens.length > 4 ) {
            // When an extension is included a source file is assumed and
            // we build a source URL
            subExt = subtokens[4];
            subPath = "../../nxm/";
          }
          target = subPath+subOpt+"/"+subArea+"/"+subName+"."+subExt;
        } else {
          M.warning("Unknown See Reference string = "+text.substring(i,j));
        }

      } else {
        target = target.toLowerCase()+".html";
      }
      //if (opt.equals("") && subtokens != null && subtokens.length <= 3)
      if(target.endsWith("_"+area.toLowerCase()+"_.html") || target.endsWith("notfound.html"))
      {
        break;
      }
      stext += "<a href='"+target+"'>"+text.substring(i,j)+"</a>";
      if (j<k) stext += text.charAt(j++);
    } // end for loop
    if (i<text.length()) stext += text.substring(i);
    return stext;
  }

  /** Writes the header of a HTML file to a text file.
      @param tf    The file to write the tags to.
      @param title The title of the screen.
      @param head1 The first heading.
      @param head2 The second heading.
      @see #writeHTMLFooter
   */
  private void writeHTMLHeader (TextFile tf, String title, String head1, String head2) {
    if (title==null) title=head1;
    tf.writeln("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
    tf.writeln("<html>");
    tf.writeln("<!-- ************************************************************************************* -->");
    tf.writeln("<!-- ** WARNING: This file is autogenerated any modifications to this file may be lost. ** -->");
    tf.writeln("<!-- ************************************************************************************* -->");
    tf.writeln("<head>");
    tf.writeln("  <title>"+title+"</title>");
    tf.writeln("  <link href='" + STYLE_SHEET_HREF + "' type='text/css' rel='stylesheet'>");
    tf.writeln("  <link href='../NXM.ico' rel='shortcut icon'>");
    tf.writeln("</head>");
    tf.writeln("<body><div>");
    if (head1!=null) tf.writeln("<h2 align='center'>"+head1+"</h2>");
    if (head2!=null) tf.writeln("<h3 align='center'>"+head2+"</h3>");
  }
  /** Writes the HTML for the Command table titles
      @param tf    The file to write the tags to.
      @see #writeHTMLHeader
   */
  private void writeHTMLCommandTableTitle (TextFile tf) {
    tf.writeln("<tr>");
    tf.writeln("  <td>Command/Explain</td>");
    tf.writeln("  <td>Description</td>");
    tf.writeln("  <td align='center'>SUP</td>");
    tf.writeln("  <td align='center'>API</td>");
    tf.writeln("  <td align='center'>Source</td>");
    tf.writeln("  <td align='center'>Test Macro</td>");
    tf.writeln("</tr>");
  }

  /** Writes the header of a HTML table to a text file.
      @param tf The file to write the tags to.
      @see #writeHTMLTableFooter
   */
  private void writeHTMLTableHeader (TextFile tf) {
    tf.writeln("<table border='1' cellpadding='2' cellspacing='0' style=\"margin-left:auto;margin-right:auto;\">");
    inTable=true;
  }

  /** Writes the footer of a HTML table to a text file.
      @param tf The file to write the tags to.
      @see #writeHTMLTableHeader
   */
  private void writeHTMLTableFooter (TextFile tf) {
    tf.writeln("</table>");
    inTable=false;
  }

  /** Writes a HTML table row with a link to a text file.
      @param tf   The file to write the tags to.
      @param name The name of the entry.
      @param ref  The link/URL reference.
      @param text The description of the entry.
      @see #writeHTMLTableHeader
   */
  private void writeHTMLBullet (TextFile tf, String name, String ref, String text) {
    if (inTable) {
      tf.writeln("<tr><td><a href='"+ref+"'>"+name+"</a></td>"
               + "<td>"+text+"</td></tr>");
    } else {
      tf.writeln("  <a href='"+ref+"'>"+name+"</a> - "+text);
    }
  }

  /** Writes the footer of an HTML file to a text file.
      @param tf The file to write the tags to.
      @see #writeHTMLHeader
   */
  private void writeHTMLFooter (TextFile tf) {
    tf.writeln("</div></body>");
    tf.writeln("</html>");
  }

  /** Determines where the help directory is. This will <b>not</b> include
      any trailing path separator.

      @return The location of the help directory.
      @since NeXtMidas 1.8.0
      @see #getJavadocDir(String)
      @todo Move the definition for the path to the build.props file.
   */
  public String getHelpDir () {
    final int delimLength = FileName.LOCAL_PATH_DELIM.length();

    return HELP_PATH.substring(0, (HELP_PATH.length() - delimLength));
  }

  /** Determines where the javadoc directory is. This is the directory in which
      javadoc places the generated HTML files. This will <b>not</b> include any
      trailing path separator.

      @param opt The option tree to get the javadoc directory for.
      @return The location of the javadoc directory.
      @since NeXtMidas 1.8.0
      @see #getHelpDir()
   */
  public String getJavadocDir (String opt) {
    return JAVADOC_PATH + opt.toLowerCase();
  }

  /** This will get a list of all the option trees that have had their documents
      compiled at some point in the past. <br>
      <br>
      This works by listing all of the directories (each representing an option
      tree) that are in the <code>$NMROOT/htdocs/api</code> directory.
      @return The list of option trees that have had their documents compiled.
              All values in this array will be capitalized.
      @since NeXtMidas 1.8.0
   */
  @LegacyMethod("use method in library Class: nxm.sys.lib.Shell.getKnownOptionTrees()}")
  public static String[] getKnownOptionTrees () {
    return Shell.getKnownOptionTrees(); // logic moved to allow separation of graphics / non-graphical libraries into JDK Modules - 3.9.2
  }



} // end class Help
