package nxm.sys.lib;

import static nxm.sys.lib.IOResource.isURLResource;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.ShellParent;
import nxm.sys.lib.FileUtil.FnFilter;

/** This class provides utility methods useful for working with option trees.

    @author  J. Czechowski IV / Neon Ngo
    @since   NeXtMidas 2.3.0
*/
public final class OptionTree {
  private static boolean DEBUG = false; // Turn on debug for getOptionTreeInfo(..), etc.

  /** List of standard option tree sub directories generally desired or needed (value={@value}). */
  public static final String SUBDIRNAMES = "CFG,MCR,INC,PRIM,EXP,DAT,HOST,DOCS,TEST";

  /** The name of the NeXtMidas commands dictionary. */
  public static final String COMMAND_FILE_NAME = "commands.cnf";

  /** Option tree type: Stand alone (Value = <code>"{@value}"</code>).
      <code>commands.cnf=&lt;dir&gt;/nxm/&lt;opt&gt;/cfg/commands.cnf</code>
   */
  public static final String  TYPE_STAND_ALONE  = "STAND_ALONE";

  /** Option tree type: Physically under NMROOT (Value = <code>"{@value}"</code>).
                        <code>commands.cnf=$NMROOT/nxm/&lt;opt&gt;/cfg/commands.cnf</code>
   */
  public static final String  TYPE_UNDER_NMROOT = "UNDER_NMROOT";

  /** Option tree type: Uses a back-link (Value = <code>"{@value}"</code>) [Unix Only]. */
  public static final String  TYPE_BACK_LINK    = "BACK_LINK";

  /** Option tree type: Flat directory structure, requires link in <code>$NMROOT/nxm</code>
                        (Value = <code>"{@value}"</code>) [Unix Only].
   */
  public static final String  TYPE_FLAT         = "FLAT";

  /** Option tree type: JAR file (Value = <code>"{@value}"</code>). */
  public static final String  TYPE_JAR_FILE     = "JAR_FILE";

  /** Option tree type: URL (Value = <code>"{@value}"</code>). */
  public static final String  TYPE_URL          = "URL";

  /** Option tree type: Unknown or invalid option tree definition
                        (Value = <code>"{@value}"</code>).
   */
  public static final String  TYPE_ERROR        = "ERROR";

  /** immutable zero-length array. See Effective Java, Item 27: Return zero-length arrays, not null. */
  static final File[] EMPTY_FILE_ARRAY = new File[0]; // since 3.7.0

//  /** Filter that matches archives the class loader can read from (currently files with the
//      extension ".jar" or ".zip"). This is used when adding an option tree to the classpath
//      (this finds the JARS).
//   */
//  private static final FilenameFilter JAR_FILTER = new FilenameFilter() { // no longer used since 3.7.0 (TODO remove in 3.7.1+ or 3.9.0+)
//    public boolean accept(File dir, String name) {
//      return name.toLowerCase().endsWith(".jar");
//    }
//  };

  /** default version for option tree when it is NOT specified/set in configuration file.
      @since NeXtMidas 3.3.0
   */
  public static final String DEAFULT_VERSION = "0.0.0.version_not_set"; // this MUST be cleaned and normalized!

  /** "fuzzy" version string format REGEX that allows following format:
      major_ver_num['.'minor_ver_num['.'micro_or_update_ver_num[['.'|'-'|'_']qualifier]]]" <br>
      where the *_ver_num are digit(s) and <br>
      the qualifier can be any number of alphanumeric, '-' (dash), or '_' (underscore).
      @since NeXtMidas 3.3.0
   */
  static final Pattern FUZZY_VERSION_REGEX = Pattern.compile("(\\d+)(\\.(\\d+))?(\\.(\\d+))?([\\.\\-\\_]?([a-zA-Z0-9_\\-]+))?");

  /** Internal book-keeping to track option trees that have been added to the PATH (that does NOT
      reside under $NMROOT. This gets wipe out after a JVM restart. Used to try to auto exit JVM
      (or warn) when an option tree is re-defined and that it has been in the PATH (currently or
      previously during this JVM session). <br>
      Key is option tree name (UPPER-CASE) <br>
      Value is LinkedHashSet of paths that the option tree has been defined at (normally one, but could
      be multiple when option trees have been redefined.)
   */
  private static Map<String,Set<String>> optsEverAddedToPath = Collections.synchronizedMap(new HashMap<String, Set<String>>());

  /** Prevent instantiation since all methods are static. */
  private OptionTree() { }

  /** Check if this option tree can be a X-Midas option tree, i.e. it contains <path>/cfg/commands.cfg
      @param midas current Midas context
      @param path path to X-Midas option tree
      @return true if this option tree can be an X-Midas option tree, otherwise false
      @since NeXtMidas 2.9.0
   */
  private static boolean isXMidasOpt (Midas midas, String path) {
    String dir = FileName.terminatePath(path) + "cfg";
    String cfg = FileName.terminatePath(dir)  + "commands.cfg"; // X-Midas Option Tree Command Dictionary
    BaseFile bf = new BaseFile(midas, cfg);
    return bf.exists(); // does X-Midas commands.cfg file exists?
  }

  /** Checks to see if commands.cnf is located at "[path]/cfg/commands.cnf" */
  private static boolean checkOptLevel (Midas midas, String name, String path) {
    String dir = FileName.terminatePath(path) + "cfg";
    String cnf = FileName.terminatePath(dir)  + "commands.cnf"; // NeXtMidas Option Tree
    return (new BaseFile(midas,cnf)).exists()  // NeXtMidas Option Tree
         || isXMidasOpt(midas,path);           // X-Midas Option Tree
  }

  /** Checks to see if commands.cnf is located at "[path]/nxm/[optname]/cfg/commands.cnf" */
  private static boolean checkNxmLevel (Midas midas, String optname, String path) {
    String nxmPath = FileName.terminatePath(path) + "nxm";
    String optPath = FileName.terminatePath(nxmPath)  + optname;

    return checkOptLevel(midas, optname, optPath);
  }

  /** Defines an option tree, adding an proper entry to the OPT table, as required.
      @param midas The current Midas context (can not be null).
      @param name  The name of the option tree.
      @param path  The path of the option tree.
      @return true if user should restart the JVM since an option tree in PATH is redefined
   */
  public static boolean defineOptionTree (Midas midas, String name, String path) {
    boolean shouldRestartJVM = false;
    Table   optTable         = getOptTable(midas);

    if (path.startsWith("classpath:") || path.endsWith(".jar")) {
      // Either on the classpath or in a JAR, nothing further needed.
    }
    else {
      name = name.toLowerCase();
      path = FileUtil.terminatePath(path);

      if (checkNxmLevel(midas,name,path)) {      // [path]/nxm/[optname]/cfg/commands.cnf
        // nothing to do, already at top
        // this is checked *before* checkOptLevel(..) since we want a "back link" to hit here first
      }
      else if (checkOptLevel(midas,name,path)) { // [optpath]/cfg/commands.cnf
        // need to move down two levels
        String sep  = FileUtil.getPathSeparator(path);
        String nxm  = sep + "nxm" + sep + name + sep;  // "/nxm/<opt>/"
        int    inxm = path.lastIndexOf(nxm);

        if (inxm >= 0) path = path.substring(0,inxm+1); // Bug 1591: +1 to include path separator
        else if (isXMidasOpt(midas,path)) { }           // Bug 2279: do not warn for X-Midas option tree
        else           midas.warning("Missing '"+nxm+"' in option tree path '"+path+"'.");
      }
      else {
        // BEGIN DEPRECATED SECTION
        // need to move down 2+ levels
        String sep  = File.separator;
        String nxm  = sep + "nxm" + sep + name + sep;  // "/nxm/<opt>/"
        int    inxm = path.indexOf(nxm);

        if (inxm >= 0) {
          midas.deprecate("Since NeXtMidas 2.5.3: Support for specifying an OPTION with "+path+" is "
                        + "deprecated, please see the OPTION explain file for details.");
          path = path.substring(0,inxm);
        }
        // END DEPRECATED SECTION
      }

      // Set path=null if path matches $NMROOT
      String canPath   = FileUtil.getCanonicalPath(path);
      String canNmroot = FileUtil.getCanonicalPath(Shell.getNmRoot());

      if (canPath.equals(canNmroot)) path = null;
    }

    if (path != null) {
      String optName = name.toUpperCase();
      String prevOptPath = optTable.getS(optName);
      optTable.put(optName, path, Table.FORCE);

      // if option tree is re-defined (since 2.9.3 for BUG 1795)
      if ((prevOptPath!=null) && !path.equals(prevOptPath)) {
        int removeCount = removeFromOptClasspath(optTable.getTable("CLASSPATH"), optName);
        if (removeCount > 0) {
          // if we removed classpath entries from previous definition of option tree,
          // it means that user SHOULD exit JVM to get correct version loaded.
          Shell.addOptToClasspath(optName, true); // update classpath entries for option tree in PATH
          shouldRestartJVM = true;                // this will require a JVM restart to take effect
        }
      }
      // if defining option tree that had previous definitions that has ever been added to the PATH
      if (hasOtherPaths(optName, path)) {
        shouldRestartJVM = true; // this will require a JVM restart to take effect
      }
    }
    return shouldRestartJVM;
  }

  /** Removes/deletes an option tree definition that resides outside of
      $NMROOT. This removes the proper entry in the OPT table, (i.e. logical
      delete) and NOT the physical path to the option tree.
      @param midas The current Midas context (can not be null).
      @param name  The name of the option tree to remove (cannot be null).
      @return true if the option is removed, otherwise false.
      @since NextMidas 2.7.0
   */
  public static boolean removeOptionTree (Midas midas, String name) {
    boolean removed = false;
    Table optTable  = getOptTable(midas);
    String optName  = name.toUpperCase();
    Table optInfo   = getOption(midas, optName);
    if (optInfo==null) {
      midas.warning("Option tree ["+name+"] not found.");
    } else if (optInfo.get("TYPE").equals("UNDER_NMROOT")) {
      // found under $NMROOT - display warning to user
      midas.warning("Option tree ["+name+"] is under NMROOT at '"
          + optInfo.get("PATH") + "'. Remove this using OS commands.");
    } else { // found it
      optTable.remove(optName, Table.FORCE);
      removed = true;
    }
    return removed;
  }

  /** <b>INTERNAL USE ONLY:</b> DO NOT USE THIS METHOD. This method is used when launching via WebStart
      or from nxm.sys.lib.NeXtMidas (where the NmClassloader is not used so it has to find
      the option tree from the current JVM's class loader).
      This method is here for that purpose ONLY. This method may be changed or removed
      without notice.
      @param opt Option tree.
      @return Option tree root for use via WebStart or nxm.sys.lib.NeXtMidas
      @since NeXtMidas 2.5.0
   */
  @InternalUseOnly
  public static String findOptRoot (String opt) {
    if ((getOptionTreeFinder() != null) && !"sys".equalsIgnoreCase(opt)) {
      // since 3.3.0: to allow finding Option Tree from registered option tree finder (BUG 2484)
      return getOptionTreeFinder().findOptRoot(opt);
    }
    else {
      // since 3.7.2: just use "classpath:" as ClassPathResource will handle the rest.
      return "classpath:";
    }
  }

  /** <b>INTERNAL USE ONLY:</b> DO NOT USE THIS METHOD. This method is used when launching via WebStart,
      or from nxm.sys.lib.NeXtMidas (where the NmClassloader is not used)
      and is here for that purpose ONLY. This method may be changed or removed
      without notice. It is here to prevent duplicate code in WebStart.
      @param opts Option trees in the path (comma or pipe separated).
      @param shellParent ShellParent to determine which NMROOT.
      @param needPath Only include opts that have a defined root/path
      @return Root for use via WebStart
      @since NeXtMidas 2.5.0, in 2.9.0 added needPath param
   */
  static String getOptsHelper (String opts, ShellParent shellParent, boolean needPath) {
    StringBuilder str = new StringBuilder(128);

    // JC4: Need to set OPT to point to the specific JAR files being used. If this is not done a
    //      query for nxm.map.dat.world.shp will look in NMROOT (i.e. the nxm-sys.jar JAR file).
    if ((opts != null) && (opts.length() > 0)) {
      String[] options = opts.split("[|,]");

      for (String opt : options) {
        if (!opt.equalsIgnoreCase("SYS")) {
          final String optpath = OptionTree.findOptRoot(opt);
          // only add it to the options mapping if the option tree path is not null
          if (optpath!=null) {
            if (str.length() > 0) str.append(",");
            str.append(opt).append("=").append(optpath);
          }
          else if (!needPath) {    // if no dir needed, just add the option tree name
            if (str.length() > 0) str.append(",");
            str.append(opt);
          }
        }
      }
      if (str.length() > 0) str.append(",");
      str.append("SYS=").append(shellParent.getNmRoot());
    }
    return str.toString();
  }

  /** Get the full path plus the specified area (if not null) to the option tree
      with following precedence:<pre>
        1. as defined in OPT table (which does not include the nxm/optname at end)
        2. if exists under $NMROOT
        3. null for NOT found.
      </pre>
      Note: this method is more intended from NeXtOpt (inside X-Midas) where the
      option tree structure is flat, use {@link #getOptPath(String, String)} for
      normal usages.
      @param midas   The midas context to use.
      @param optname The name of the option tree (cannot be null)
      @param area    The area in the option tree (to append to return value if NOT null)
      @return The path + area to the option tree (1. as defined or 2. if exists under $NMROOT) 3. otherwise null
      @see #getOptPath(String, String)
      @since NeXtMidas 2.9.0
   */
  public static String getOptionPath (Midas midas, String optname, String area) {
    String retPath = null; // initially assume not found (i.e. set to null)
    Table optTable = getOptTable(midas);
    // Look for the path to the option tree as defined in the OPTS table
    String definedOptPath = (String) optTable.getKey(optname.toUpperCase());
    if (definedOptPath == null) {  // If not found (defined), check under NMROOT.
      String pathUnderNmroot = FileName.getOptionPath(Shell.getNmRoot(), optname, area);
      if (CoreIO.fexists(pathUnderNmroot, true)) {
        retPath = pathUnderNmroot;      // found under $NMROOT/nxm/<optname>
      }
    } else {
      retPath = definedOptPath;         // use option tree path as defined (this usually DOES NOT include nxm/<optname> at end
      if (area != null) {               // append area to path (when not null)
        retPath = FileName.addPathDir(definedOptPath, area.toLowerCase());
      }
    }
    return retPath;
  } // end getOptionPath()

  /** Creates an option path in a system independent manner.
      @param opt   The name of the option tree.
      @param area  The area in the option tree.
      @return The path to the given area of the option tree.
   */
  public static String getOptPath (String opt, String area) {
    return Shell.getOptPath(opt,area);
  }

  /** This returns the directory above the <code>cfg</code> area of an option tree.
      @param midas   The midas context to use.
      @param optName The name of the option tree.
      @return The path to the given area of the option tree.
   */
  public static String getCfgRoot (Midas midas, String optName) {
    Table opt = getOption(midas, optName);
    return (opt == null)? null: opt.getS("CFGROOT");
  }

  /** This returns the directory above the <code>nxm</code> used for an option tree.
      @param midas   The midas context to use.
      @param optName The name of the option tree.
      @return The path to the given area of the option tree.
      @since NeXtMidas 2.5.0
   */
  public static String getNxmRoot (Midas midas, String optName) {
    Table opt = getOption(midas, optName);
    return (opt == null)? null: opt.getS("NXMROOT");
  }

  /** List the jar files used by an option tree. This is the list of <code>.jar</code> files in
      the <code>jars</code> area of the given option tree.
      Since NeXtMidas 3.7.0, does so recursively (which follow symbolic links).
      @param opt The name of the option tree.
      @return The list of Jar files. Returns array of length 0 if no Jar files are found, or
              null if unable to search for Jars due to the option tree not being on the local disk.
      @see #getJarFiles(String)
   */
  public static String[] getJars (String opt) {
    File[]   jarFiles = getJarFiles(opt);
    String[] jars     = null;

    if (jarFiles != null) {
      jars = new String[jarFiles.length];
      for (int i = 0; i < jars.length; i++) {
        jars[i] = jarFiles[i].toString();
      }
    }

    return jars;
  }

  /** List the jar files used by an option tree. This is the list of <code>.jar</code> files in
      the <code>jars</code> area of the given option tree.
      Since NeXtMidas 3.7.0, does so recursively (which follow symbolic links).
      @param opt The name of the option tree.
      @return The list of Jar files. Returns array of length 0 if no Jar files are found, or
              null if unable to search for Jars due to the option tree not being on the local disk.
      @see #getJars(String)
   */
  public static File[] getJarFiles (String opt) {
    String path = getOptPath(opt,"jars");

    if (FileUtil.isLocalFileName(path)) {
      File jars = new File(FileUtil.toLocalFileName(path));
      if (jars.isDirectory()) {
//      jarFiles = jars.listFiles(JAR_FILTER); // orig prior to 3.7.0
        // begin since 3.7.0: include jar files from any sub-directly of jars area (BUG 2950)
        FnFilter filter = FileUtil.createFnFilter(path, "*.jar|*.JAR", null);
        return FileUtil.findFiles(jars, filter);
        // end since 3.7.0 for BUG 2950
      }
    }
    return EMPTY_FILE_ARRAY;
  }

  /** Gets a list of all available option trees.
      @param midas   The midas context to use.
      @return The list of option tree names.
   */
  public static String[] getOptionNames (Midas midas) {
    return getAllOptions(midas).getKeys();
  }

  /** Gets table with information about a given option tree. The table is the same as one of the
      sub-tables returned from {@link #getAllOptions(Midas)}. <br>
      <br>
      There is a lot of overhead required to identify links and check option tree types. If all you
      need is a path to a given area of an option tree, use {@link #getOptPath(String,String)}
      method, it is much faster.
      @param midas   The midas context to use.
      @param optName The name of the option tree.
      @return A table with the option tree information or null if not found.
  */
  public static Table getOption (Midas midas, String optName) {
    return getAllOptions(midas).getTable(optName.toUpperCase());
  }

  /** Gets table with all available option trees. This creates a table of tables that looks like:
      <pre>
        {SYS={...},
         UCL={...},
          :
         XBC={...}}
      </pre>
      Each of the sub tables contains the following information:
      <pre>
        NAME     - Name of the option tree.
        TYPE     - Type of option tree ({@link #TYPE_STAND_ALONE}, {@link #TYPE_UNDER_NMROOT},
                   {@link #TYPE_BACK_LINK}, {@link #TYPE_FLAT}, {@link #TYPE_JAR_FILE}, {@link #TYPE_URL})
                   or {@link #TYPE_ERROR} if the option tree looks invalid.
        PATH     - This path as provided. This matches what the OPTION command
                   displayed in NeXtMidas 2.2.0. <b>***This location is inconsistent.***</b>
        NXMROOT  - The actual location of the nxm directory used by the option tree
                   (symbolic links and ../, resolved).
        CFGROOT  - The base location of the option tree. This is the directory
                   that cfg/ is in.
      </pre>
      There is a lot of overhead required to identify links and check option tree types. If all you
      need is a path to a given area of an option tree, use {@link #getOptPath(String,String)}
      method, it is much faster.
      @param midas The midas context to use.
      @return A table of all the option trees.
  */
  public static Table getAllOptions (Midas midas) {
    Table options  = new Table();
    Table optTable = getOptTable(midas);

    /* List the ones under $NMROOT/nxm/ */
    String nxmDir = FileName.terminatePath(Shell.getNmRoot() + "nxm");
    String[] files = midas.io.listFiles(nxmDir, "*", "DIR");

    for(int i = 0; (files != null) && (i < files.length); i++) {
      String optDir  = FileName.terminatePath(nxmDir + files[i]);
      String cnfFile = FileName.terminatePath(optDir + "cfg") + COMMAND_FILE_NAME;

      if (midas.io.find(cnfFile)) {
        File dir = new File(optDir);
        String name = files[i].toUpperCase();

        if (!dir.exists() || !dir.isDirectory()) {
          // skip
        } else if (options.containsKey(name)) {
          Shell.warning("OPTION multiple definitions for OPT="+name+" found.");
        } else {
          options.put(name, getOptionTreeInfo(midas, files[i], optDir, true));
        }
      }
    }

    /* List the others defined in OPT */
    String[] keys = optTable.getKeys();
    for (String key : keys) {
      if (key.equals("PATH") || key.equals("CLASSPATH") || key.equals("CP")) {
        // Ignore
      } else if (options.containsKey(key)) {
        Shell.warning("OPTION multiple definitions for OPT="+key+" found.");
      } else {
        options.put(key, getOptionTreeInfo(midas, key, optTable.getS(key), false));
      }
    }

    return options;
  }

  /** Get the OPT results table where logical option trees are defined.
     @param midas Midas Context
     @return OPT results Table
     @since NeXtMidas 2.7.0
   */
  private static Table getOptTable (Midas midas) {
    return midas.getResults().getTable("OPT");
  }

  /** Gets information about an unconfigured option tree. This method is used by the <code>OPTION</code>
      command to verify option tree attributes when a new option tree is defined. <br>
      <br>
      *** This is a utility method used by <code>OPTION</code>, no one else should use this method. ***
      @param midas   The midas context to use.
      @param optName The name of the option tree.
      @param path    The path to use.
      @return A table with the option tree information or null if not found.
   */
  @InternalUseOnly("since initial version")
  public static Table getUnconfiguredOption (Midas midas, String optName, String path) {
    return getOptionTreeInfo(midas, optName, path, true);
  }

  /** Gets a table with the information about an option tree. If you just need the path to an
      option tree or an area in an option tree, use the {@link Shell#getOptPath(String,String)}
      method. Since this method needs to resolve links, trace back-links, etc. it has a
      significantly higher overhead than the {@link Shell#getOptPath(String,String)} method.
      @param midas The midas context to use.
      @param optName  The name of the option tree (e.g. "MYOPT" or "myopt").
      @param path     The path to the option tree. If inNmroot is true then this is the directory
                      under $NMROOT/nxm/ (e.g. $NMROOT/nxm/myopt/). If inNmroot is false then this
                      is the directory as defined in OPT.MYOPT; that includes the nxm directory.
      @param inNmroot Is the path under $NMROOT/nxm/
      @return A table with the necessary information (see getAllOptions(..) for details).
  */
  private static Table getOptionTreeInfo (Midas midas, String optName, String path, boolean inNmroot) {
    optName = optName.toLowerCase();

    String  type;
    String  root;
    String  file = path;
    String  base = null;
    boolean link = false;
    if (file.startsWith("file:")) { file = file.substring(5); }

    if (isURLResource(file)) {   // option tree defined from a URL
      if (path.startsWith("jar:")) {
        type = TYPE_JAR_FILE;
      } else {
        type = TYPE_URL;         // since 3.3.0 for BUG 2484
      }
      root = path;
      base = path+"nxm/"+optName+"/"; // where cfg area is a child directory
    }
    else {                       // option tree defined from a local file system
      File dir = new File(path);
      if (dir.isFile()) {
        if (dir.getName().endsWith(".jar")) {
          type = TYPE_JAR_FILE;
          root = dir.toString();
          base = path+"!/nxm/"+optName+"/"; // since 3.3.0
        } else {
          // JC4: Unknown type... should check for this in the future! (WAR/ZIP file?)
          Shell.warning("OPTION: Can not determine type of option "+optName.toUpperCase()+"="+path);
          type = TYPE_JAR_FILE;
          root = dir.toString();
        }
      } else {
        path = FileName.terminatePath(path);

        File nmroot    = new File(Shell.getNmRoot());
        File nmrootCan = FileUtil.getCanonicalFile(nmroot);
        File nmrootNxm = new File(nmroot,    "nxm");
        File nmrootOpt = new File(nmrootNxm, optName);
        File nmrootCfg = new File(nmrootOpt, "cfg");
        File nmrootCC  = FileUtil.getCanonicalFile(nmrootCfg);
        File canonical = FileUtil.getCanonicalFile(dir);
        File top;
        File nxm;
        File opt;
        File cfg;

        if (inNmroot) {
          File canNxm = new File(canonical, "nxm");
          File canOpt = new File(canNxm,    optName);
          File canCfg = new File(canOpt,    "cfg");

          opt = canonical;
          nxm = canonical.getParentFile();
          top = nxm.getParentFile();
          cfg = canCfg;
        } else {
          top = canonical;
          nxm = new File(top, "nxm");
          opt = new File(nxm, optName);
          cfg = new File(opt, "cfg");
        }
        File cfgFlat = new File(canonical, "cfg");
        File optCan  = FileUtil.getCanonicalFile(opt);

        boolean isStandalone = (top.exists() && top.isDirectory()
                             && nxm.exists() && nxm.isDirectory() && nxm.getName().equals("nxm")
                             && opt.exists() && opt.isDirectory() && opt.getName().equals(optName));
        if (inNmroot) {
          link = nmrootCfg.exists() && nmrootCC.equals(FileUtil.getCanonicalFile(cfgFlat));
          base = canonical.toString();
        }
        else {
          link = nmrootCfg.exists() && nmrootCC.equals(FileUtil.getCanonicalFile(cfg));
          base = dir.toString();
          if (isStandalone) { // Add in the nxm/<opt>/ for those that need it
            File tempNxm = new File(base,    "nxm");
            File tempOpt = new File(tempNxm, optName);
            base = tempOpt.toString();
          }
        }

        if (top.equals(nmrootCan)) {
          root = nmroot.toString();
          type = TYPE_UNDER_NMROOT;
          base = nmrootOpt.toString(); // Override - do not resolve any symlinks here!
          link = false;                // Override - not a link!
        }
        else if (top.equals(optCan)) {
          root = top.toString();
          type = TYPE_BACK_LINK;
        }
        else if (isStandalone) {
          root = FileUtil.getCanonicalFile(top).toString();
          type = TYPE_STAND_ALONE;
        }
        else if (cfgFlat.exists() && inNmroot) {
          if (FileUtil.getCanonicalFile(cfgFlat).equals(FileUtil.getCanonicalFile(cfg))) {
            root = nmroot.toString();
            type = TYPE_BACK_LINK;
          } else {
            root = nmroot.toString();
            type = TYPE_FLAT;
          }
        }
        else if (cfgFlat.exists() && inNmroot) {
          root = nmroot.toString();
          type = TYPE_FLAT;
        }
        else {
          root = dir.toString();
          type = TYPE_ERROR;
        }

        if (!link && (type == TYPE_BACK_LINK)) {
          base = path; // Special case for those with a back link.
        }

        if (DEBUG) {
          midas.type(">>> -------------------------------------");
          midas.type(">>> OPT       = "+optName);
          midas.type(">>> CANONICAL = "+canonical);
          midas.type(">>> TOP       = "+top);
          midas.type(">>> NXM       = "+nxm);
          midas.type(">>> OPT       = "+opt);
          midas.type(">>> CFG       = "+cfg);
          midas.type(">>> CFGFLAT   = "+cfgFlat);
          midas.type(">>> NMROOTCFG = "+nmrootCfg);
          midas.type(">>>");
          midas.type(">>> NXMROOT   = "+root);
          midas.type(">>> CFGROOT   = "+base);
          midas.type(">>> PATH      = "+path);
          midas.type(">>> TYPE      = "+type);
        }
        path = FileName.terminatePath(path);
        base = FileName.terminatePath(base);
        root = FileName.terminatePath(root);
      }
    }
    Table tbl = new Table();
    tbl.put("NAME", optName.toUpperCase());
    tbl.put("LINK", link);
    tbl.put("TYPE", type);
    tbl.put("PATH", path);
    tbl.put("CFGROOT", base);
    tbl.put("NXMROOT", root);

    if (DEBUG) {
      midas.type("getOptionTreeInfo ==> "+tbl);
    }
    return tbl;
  }

  /** <b>INTERNAL USE ONLY:</b> Remove specified option tree names (startsWithName) from OPT.CLASSPATH Table.
      Key entries starting with underscores are ignored.
      @param cpTable OPT.CLASSPATH Table to remove entries from
      @param optName name of option tree to remove entries or "*" to remove all
      @return count of entries removed from OPT.CLASSPATH
      @since 2.9.3
   */
  @InternalUseOnly("This method should be package-private, but nxm.sys.intr.Configure needs access it it, hence it is public")
  public static int removeFromOptClasspath (Table cpTable, String optName) {
    int count = 0;
    boolean removeAll = "*".equals(optName); // allow removing all entries
    String optKeyPrefix = optName.toUpperCase() + "_";
    String[] keys = cpTable.getKeys();
    for (String key : keys) {
      boolean reservedKey = (key.indexOf('_') == 0); // reserved keys starts with underscore
      if ( !reservedKey && (removeAll || key.startsWith(optKeyPrefix)) ) {
        cpTable.remove(key, Table.FORCE);
        count++;
      }
    }
    return count;
  }

  /** <b>Internal use only:</b> Track the specified option tree added to the PATH.
      @param optName name of option tree (UPPER-CASE!)
      @param optPath path where where option tree is defined
      @since NeXtMidas 2.9.3
   */
  @InternalUseOnly("since initial version")
  static void addedToPath (String optName, String optPath) {
    Set<String> optPathSet = optsEverAddedToPath.get(optName);
    if (optPathSet == null) {
      optPathSet = Collections.synchronizedSet(new LinkedHashSet<String>(16));
      optsEverAddedToPath.put(optName, optPathSet);
    }
    optPathSet.add(optPath);
  }

  /** <b>Internal use only:</b> Check if there are other definitions of specified option tree that
      is different from given one that had every been added to the PATH.
      @param optName name of option tree
      @param optPath path to option tree
      @return true if specified option tree has ever been added to the PATH
              with a different option path definition, otherwise false.
      @since NeXtMidas 2.9.3
   */
  @InternalUseOnly("since initial version")
  static boolean hasOtherPaths (String optName, String optPath) {
    boolean foundOtherPathDef = false;
    Set<String> optPathSet = optsEverAddedToPath.get(optName);
    if (optPathSet != null) {
      int size = optPathSet.size();
      if (size > 1) foundOtherPathDef = true; // more than one entry always means another opt path exists
      else if (size == 1 && !optPathSet.contains(optPath)) foundOtherPathDef = true; // one entry, but with different opt path
      // there SHOULD NOT be a zero entry optPathSet, but that scenario is ok
    }
    return foundOtherPathDef;
  }

  /** Gets a table with the configuration information about an option tree.
      The returned tables contains the following information:
      <pre>
        NAME          - Name of the option tree.
        PATH          - The path to this option tree (as defined), null if NOT defined or under $NMROOT/nxm.
        --- option tree's version configurations from &lt;option_tree_dir&gt;/version.txt ---
        VERSION       - The version of this option tree (e.g. 1.2.3[.qualifier]) gleaned from MANIFEST.MF or parsed from version.txt
                        It has the following precedence:
          VERSION_BUNDLE         - [optional] (1st precedence) from "Bundle-Version" key in MANIFEST.MF (if it exists)
          VERSION_IMPLEMENTATION - [optional] (2nd precedence) from "Implementation-Version" key in MANIFEST.MF (if it exists)
          VERSION_TXT            - [optional] (3rd precedence) parsed from version.txt (if it exists)
                                              (4rd/last precedence) none specified (defaults to {@link OptionTree#DEAFULT_VERSION})
        DESCRIPTION   - Human readable description of this option tree.
        REQUIRED_OPTS - Table of REQUIRED option trees, where keys are option tree names
                        that this option tree depends on and the values are additional
                        constraints (such as version constraints).
      </pre>
      @param optName The name of the option tree (e.g. "MYOPT" or "myopt") (cannot be null).
      @param M       The midas context to use.
      @param verbose Display version information as version config file is loaded, etc.
      @param warn    Display warning message if version can not be determined
      @return A table of the configuration info for specified option tree.
      @since NeXtMidas 3.3.0
  */
  public static Table getInfo (String optName, Midas M, boolean verbose, boolean warn) {
    optName = optName.trim().toLowerCase();      // lower case optName
    String OPTNAME = optName.toUpperCase();      // upper case version of optName
    String FROM_OPT = " [" + OPTNAME + "]";      // for verbose/warn messages
    Table optConfigTbl = new Table();
    Table requiredOpts = new Table();
    String optpath = getOptPath(optName, null);
    optConfigTbl.put("NAME", OPTNAME);
    optConfigTbl.put("PATH", optpath);
    if (optpath != null) {
      // check if manifest has version(s) specified in its meta-data
      FileName manifestFileName = new FileName("nxm."+optName+".cfg.manifest.mf");
      BaseFile mf = new BaseFile(M, manifestFileName);
      if (mf.exists()) {
        if (mf.open()) { // open manifest file
          try {
            Manifest manifest = new Manifest(mf.getResource().getInputStream());
            Attributes mfMainAttributes = manifest.getMainAttributes();
            String bundleVersion = mfMainAttributes.getValue("Bundle-Version");
            String implVersion   = mfMainAttributes.getValue("Implementation-Version");
            if (bundleVersion != null) {
              optConfigTbl.put("VERSION", cleanVersion(bundleVersion));           // 1st precedence for version
              optConfigTbl.put("VERSION_BUNDLE", bundleVersion);
            }
            if (implVersion != null) {
              optConfigTbl.addIfNotPresent("VERSION", cleanVersion(implVersion)); // 2nd precedence for version
              optConfigTbl.put("VERSION_IMPLEMENTATION", implVersion);
            }
          } catch (Exception e) {
            throw new MidasException("Unable to load "+manifestFileName, e);
          } finally {
            mf.close();    // close manifest file
          }
        }
      } // end if manifest.mf exists

      FileName  configFileName = new FileName(optpath + "version.txt");
      TextFile  textFile = new TextFile(configFileName);
      if (textFile.open(BaseFile.NOABORT)) {     // if able to open file for reading
        if (verbose) M.info("Parsing"+FROM_OPT+" option tree's info/version from " + configFileName.getFullName());
        String version     = null;
        String description = null;
        String line;
        while ((line=textFile.read()) != null) { // parse file for version and required option tree(s)
          line = line.trim();
          if (version == null) {                 // look for version string since we haven't found it.
            version = parseForVersion(line);
            if (version != null) {
              optConfigTbl.addIfNotPresent("VERSION", version);     // 3rd precedence for version
              optConfigTbl.put("VERSION_TXT", version);
            }
          }
          if (description == null) {             // use first line that does not start with "****" as description
            if (!line.startsWith("****")) {      // ignore lines starting with four stars
              description = line;                // since they are commonly the header banner
              optConfigTbl.put("DESCRIPTION", description);
            }
          }
          Table reqOpt = parseForRequiredOpt(line);
          if (reqOpt != null) {
            requiredOpts.merge(reqOpt);
          }
        } // end while loop to read text file
      }
      textFile.close();
    }
    if (optConfigTbl.addIfNotPresent("VERSION", DEAFULT_VERSION)) { // 4th precedence for version (none specified)
      if (warn) M.warning("VERSION not specified in"+FROM_OPT+"/version.txt or in manifest.mf, defaulting to "+DEAFULT_VERSION);
    }

    optConfigTbl.put("REQUIRED_OPTS", requiredOpts);

    return optConfigTbl;
  }

  /** remove leading zeroes from input string. since 3.3.0 */
  private static String removeLeadingZeroes (String numstr) {
    if (numstr == null) return null;
    int n = 0; // index of last leading zero
    int checkLen = numstr.length() - 1; // minus one to always keep one digit
    while ((n < checkLen) && (numstr.charAt(n) == '0')) {
      n++;
    }
    if (n == 0) return numstr;

    return numstr.substring(n);
  }

  /** Normalize/Clean specified version string so that it is a valid normalized version string
      of the format: major_version.minor_version.micro_version[.qualifier].
      <br>
      The major version number part MUST be specified in the input version string
      otherwise it is invalid and a null will be returned.
      <br>
      If the minor and/or micro/update version numbers are not specified, they will be set to 0 (zero).
      @param version input version string to clean to a proper version string (cannot be null)
      @return null if input version string is not a valid version string,
              otherwise a valid version string (with any necessary cleansing)
      @since NeXtMidas 3.3.0
   */
  public static String cleanVersion (String version) {
    // do a fuzzy version string match and cleaning
    Matcher m = FUZZY_VERSION_REGEX.matcher(version);
    if (m.matches()) {
      // for (int ii=0; ii <= m.groupCount(); ii++) System.out.println("DEBUG:  matcher.group("+ii+") = "+m.group(ii));
      String major = removeLeadingZeroes(m.group(1)); // group 1 from regex is major version
      String minor = removeLeadingZeroes(m.group(3)); // group 3 from regex is minor version
      String micro = removeLeadingZeroes(m.group(5)); // group 5 from regex is micro/update version
      String qualifier = m.group(7);                  // group 7 from regex is version qualifier

      StringBuilder sb = new StringBuilder(major);
      if (minor == null) minor = "0"; // use 0 unspecified minor version
      sb.append('.').append(minor);
      if (micro == null) micro = "0"; // use 0 unspecified micro/update version
      sb.append('.').append(micro);
      if (qualifier != null) {
        sb.append('.').append(qualifier);
      }

      return sb.toString();
    }
    return null; // no match
  }

  /** Replace qualifier part of version from a clean/normalized version input string.
      @param version clean and normalized version string (should not be null)
      @param customQualifier string to replace the qualifier part of in the input version string
      @return original version string if customQualifier is null, otherwise a updated version string
              with the customQualifier in qualifier portion of the version.
      @since NeXtMidas 3.5.1
   */
  @InternalUseOnly("since initial version")
  public static String replaceVersionQualifier (String version, final String customQualifier) {
    Matcher m = FUZZY_VERSION_REGEX.matcher(version);
    if ((customQualifier != null) && m.matches()) { // verify that version str is valid (matches)
      int index = m.start(6);                       // group 6 is dot/dash before version qualifier
      if (index != -1) {
        version = version.substring(0, index);      // remove existing qualifier from version (including dot or dash)
      }
      return version + "." + customQualifier;       // append custom version qualifier
    }
    return version;
  }

  /** parse for/find version string in specified line.
      This will also convert versions that does not a dot qualifier (fourth/last) version component,
      but ends with (alphanum | '_' | '-')+ to be the qualifier. Also any non-specified
      minor or mirco version numbers will be set to zero, e.g.<pre>
        Version  1.2.3-04  becomes 1.2.3.04
        Version  1.2.3_05  becomes 1.2.3.05
        Version: 1.2.4rc1  becomes 1.2.4.rc1
        Version: 1b23      becomes 1.0.0.b23
      </pre>
      @param text line of to scan for version string (cannot be null)
      @return version string or null if NOT found
      @since NeXtMidas 3.3.0
   */
  public static String parseForVersion (String text) {
    int startIndex = text.toUpperCase().indexOf("VERSION");
    if (startIndex >= 0) {     // found version string specifier
      startIndex += 7;         // move index past "Version"
      if (text.length() > 7) { // must be 8+ chars to have "Version:" specifier
        if (text.charAt(7) == ':') {
          startIndex++;        // move index past ':'
        }
      }
      text = text.substring(startIndex); // trim any text prior to version specifier
    } // else parse whole text line

    String version = null;
    StringTokenizer st = new StringTokenizer(text," \t"); // tokenize on space or tabs
    while ((version == null) && st.hasMoreTokens()) {
      String token = st.nextToken();
      version = cleanVersion(token); // check for and clean version string
    }
    return version;
  }

  /** Parse for required option tree specified via "<code>#REQUIREDOPT: option_tree_name[; constraint]*</code>".
      The constraint is zero or more semi-colon separated list of (key=value) constraints.
      The following constraints are currently supported:<pre>
        VERSION:
          "version"=(minimal_version | version_range)
          min_version   := version
          max_version   := version
          version       := (major_ver_#[.minor_ver_#[.update_ver_#[.qualifier]]])
          version_range := ("[" | "(") min_version ',' max_version ("]" | ")")
            use a "[" and "]" for inclusive and "(" and ")" for exclusive versions to allow.
      </pre>
      The returned Table is in the following format:<pre>
        OPTNAME={[VERSION=versionContraints]}
        e.g.
        SYS={VERSION="3.3.0"}       ! requires SYS version 3.3.0 or later
        DSP={}                      ! requires DSP any version
        MYOPT={VERSION="[1.0,2.0)"} ! requires MYOPT version 1.0.0 up to be not including 2.0.0
      </pre>
      @param text line of to scan for version string (cannot be null)
      @return Table of required option tree and any specified constraints, otherwise null if none specified
      @since NeXtMidas 3.3.0
   */
  public static Table parseForRequiredOpt (String text) {
    Table reqOpt = null;
    if (text.startsWith("#REQUIREDOPT:")) {
      text = text.substring(13); // trim any text prior to "#REQUIREDOPT:" specifier
      String reqOptName = null;
      StringTokenizer st = new StringTokenizer(text," \t;"); // tokenize on space, tab, and semi-colon
      Table constraints = new Table();
      while (st.hasMoreTokens()) {
        String token = st.nextToken();
        if (reqOptName == null) {  // first token is required option tree name
          reqOptName = token.toUpperCase();
          reqOpt = new Table();
          reqOpt.put(reqOptName, constraints);
        }
        else {                     // all subsequent tokens are (key=value) constraints
          int iEq = token.indexOf('=');
          if (iEq > 0) { // index of equal sign must be 1+
            String key = token.substring(0,iEq);
            String val = token.substring(iEq+1);
            constraints.put(key.toUpperCase(), val);
          }
          // else non key=value tokens are ignored
        }
      } // end while loop for text tokens
    }

    return reqOpt;
  }

  /** Set debug mode
   *  @param state true means in DEBUG mode
   *  @since NeXtMidas 3.3.0 */
  public static void setDebug (boolean state) { DEBUG = state; }
  /** Is this in debug mode?
   *  @return true if in DEBUG mode
   *  @since NeXtMidas 3.3.0 */
  public static boolean isDebug () { return DEBUG; }

  /** the registered option tree root finder (null if none)
      @since NeXtMidas 3.3.0
   */
  private static OptionTreeFinder optRootFinder = null;

  /** Register an Option Tree finder {@link OptionTreeFinder} to use
      to find option trees verses from ClassLoader in {@link #findOptRoot(String)}.
      @param optTreeFinder option tree root finder to use (null to set to none)
      @return previous registered option tree root finder (null if none was set)
      @since NeXtMidas 3.3.0
   */
  public static OptionTreeFinder registerOptionTreeFinder (OptionTreeFinder optTreeFinder) {
    OptionTreeFinder prev = optRootFinder;
    optRootFinder = optTreeFinder;
    return prev;
  }

  /** Get the registered Option Tree finder {@link OptionTreeFinder} to use
      to find option trees verses from ClassLoader in {@link #findOptRoot(String)}.
      @return registered option tree root finder (null if none is set)
      @since NeXtMidas 3.3.0
   */
  public static OptionTreeFinder getOptionTreeFinder () {
    return optRootFinder;
  }

  /** Interface to find an option tree root URL/path string for specialized execution environments
      such as in the OSGi framework.
      @since NeXtMidas 3.3.0
   */
  public static interface OptionTreeFinder {
    /** Get the URL string of the specified option tree's root path from current running environment.
      @param opt name of option tree (should be lower case to be optimal)
      @return null if not found, otherwise URL string to option tree's root path
     */
    public String findOptRoot (String opt);
  }
}
