package nxm.sys.lib;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.JPanel;

import nxm.sys.inc.Debug;
import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.ShellParent;

/** A class to run NeXtMidas command(s) from outside of a Midas context
    <br>
    Note: This class should be treated as a singleton, since various underlining
          NeXtMidas framework classes (such as the NeXtMidas Shell) uses
          global variables (such as the PATH, AUXes, etc).
          Otherwise multiple instances can step over each other and cause undefined behavior.
    <pre>
    <b>Example Usage</b>:
    One-shot" mode (do not instantiate this class):
       [prompt]$ java -classpath $NMROOT nxm.sys.lib.NeXtMidas -path="UCL,DSP,SYS" -cmd="home test sys" -script="$NMROOT/nxm/sys/test/test_plot.mm" -prompt

    Instantiate (in java) and run multiple commands:
      Method 1:
        NeXtMidas nxm = NeXtMidas.getGlobalInstance();
        nxm.runCommand("SET AUX.MYDATA /path/to/mydata");  // define MYDATA AUX
        nxm.runCommand("AUX HOME HOME|MYDATA|DAT|RAM");    // setup write &amp; read AUX list
        nxm.runCommand("OPTION DSP CLASSPATH");            // find and define DSP from CLASSPATH
        nxm.runCommand("OPTION MYOPT /path/to/nxm/myopt"); // define MYOPT option tree
        nxm.runCommand("PATH ADD DSP MYOPT");              // add option trees to PATH
        nxm.runCommand("DEMO/BG");                         // run command in background (non-blocking)

      Method 2:
        String[] opts = {"-path=UCL,DSP,SYS", "aux.read=HOME|DAT|"};
        NeXtMidas nxm = new NeXtMidas(opts);  // this calls parseOptions(.) and openShell() for you
        nxm.run();                            // [optional] run commands specified via any -cmd, -command, and -script options
        nxm.runCommand("plot/bg Sunset.jpg"); // run PLOT command in background (non-blocking)
        nxm.runCommand("plot world.shp");     // run another PLOT command in foreground (blocks current thread)

      Method 3:
        NeXtMidas nxm = new NeXtMidas();
        String[] opts = {"-path=UCL,DSP,SYS", "aux.read=HOME|DAT|RAM", "-cmd=plot Sunset.jpg"};  // also gives a command to run later
        nxm.parseOptions(opts);
        nxm.openShell();
        nxm.run();                                    // run commands specified via any -cmd, -command, and -script options
        nxm.runCommand("plot nxm.sys.dat.world.prm"); // run another PLOT command in foreground (blocks)
    </pre>
    @author J. Bauer / Neon Ngo
    @since NeXtMidas 2.9.0
 */
public class NeXtMidas implements ShellParent, MidasReference {

  /** flag to alwaysCallShellOpen always call shell.open(), even is there is an existing global Shell.getCurrent() */
  public static final int ALWAYS_CALL_SHELL_OPEN     = 0x1; // since 3.5.2
  /** flag to restore non-null previous shell in constructor */
  public static final int RESTORE_NONNULL_PREV_SHELL = 0x2; // since 3.5.2
  static final String flagList  = "AlwaysCallShellOpen,RestoreNonNullPrevShell"; // since 3.5.2

  private static final String DEF_WRITE_AUX = "HOME";
  private static final String DEF_READ_AUX  = "HOME|RAM|DAT";

  /** global NeXtMidas session instance (using Java Terminal and default options) */
  private static final NeXtMidas INSTANCE = new NeXtMidas(new Terminal(), RESTORE_NONNULL_PREV_SHELL); // since 3.3.0

  private boolean DEBUG = false;

  /** use Native code? (DEF=off) */
  private boolean noNative      = true;
  private String  path          = "SYS";
  /** The shell driving this NeXtMidas instance */
  private Shell   shell         = null;

  private boolean runInteractiveTextTerminal = false;

  private List<String> executeList = Collections.synchronizedList(new LinkedList<String>());

  /** display panel (when set by user) to embed graphical commands on */
  private JPanel panel; // Change to JPanel from MidasDisplay for Java 11 Modularity since 3.9.1

  /** encountered "--" so stop processing options */
  private boolean haltOptionsCheck = false;

  /** String Escape mode (enum):
      Percent - translate escape sequences as defined in section 2.1 Percent-Encoding
                of RFC 3986 Uniform Resource Identifier (URI): General Syntax.
                i.e. %hh (here h is hex digit).
   */
  private static enum EscapeCharMode { Percent };
  EscapeCharMode escapeCharMode = null;

  Map<String,String> auxEntries = new LinkedHashMap<>();
  /** terminal to use, or null to use Terminal determined by Shell when it opens */
  private Terminal terminal = null;
  private final Shell prevShell;
  private final boolean alwaysCallShellOpen;

  /** Added for NXM-2900 Issue defining path/options
   * This defines a public static enumeration for the PATH options used by the
   * method addOptionTreeToPathFromClasspath(String optName, enum OptionFlags)
   * @since NeXtMidas 3.7.1
   */
  public static enum OptionFlags {
    /** Puts the option on the front of the existing path */
    ADD,
    /** Puts the option on the end of the existing path but before SYS */
    ADD_PLUS,
    /** If option already exists on the path, does nothing (except notify the
    user). If option is not on path, performs an ADD. 8*/
    PUT;

    @Override
    public String toString() {
      switch (this) {
        case ADD: return "ADD";
        case ADD_PLUS: return "ADD+";
        case PUT: return "PUT";
        default: return "";
      }
    }
  };

  private static boolean shellGuiExists = true;
  static {
    shellGuiExists = Shell.getCurrent().M.dictionary.isDictionaryEntry("SHELLGUI");
  }

  /** Creates a new instance of this class.
      User <b>SHOULD</b> call {@link #parseOptions(String[])} with any desired settings.
      User <b>MUST</b> call {@link #openShell()} prior to running any commands via {@link #runCommand(String)} or {@link #run()}
      Since 3.3.0: this uses the nxm.sys.lib.Terminal (pure Java) vs trying NTerminal (if -nonative=false)
      which will replace SIGINT (Ctrl+C) signal handler.
   */
  public NeXtMidas () {
    this((String[]) null);
  }

  /** Create a new instance of this class, then calls {@link #parseOptions(String[])} and {@link #openShell()}.
      Since 3.3.0: this uses the nxm.sys.lib.Terminal (pure Java) vs trying NTerminal (if -nonative=false)
      which will replace SIGINT (Ctrl+C) signal handler.
      @param options the options to set
  */
  public NeXtMidas (String... options) {
    this(new Terminal(), ALWAYS_CALL_SHELL_OPEN, options);
  }

  /** Create a new instance of this class, then calls {@link #parseOptions(String[])} and {@link #openShell()}.
      @param terminal the terminal to use (set to null to have Shell determine it when it opens)
      @param flags    mask of options (since 3.5.2):
                        0 for none,
                        {@link #ALWAYS_CALL_SHELL_OPEN} ({@value #ALWAYS_CALL_SHELL_OPEN}),
                        {@link #RESTORE_NONNULL_PREV_SHELL} ({@value #RESTORE_NONNULL_PREV_SHELL})
      @param options the options to set (null or empty for none)
      @since NeXtMidas 3.3.0 (public for internal use since 3.5.2)
   */
  @InternalUseOnly
  public NeXtMidas (Terminal terminal, int flags , String... options) {
    DEBUG = Convert.o2z(System.getProperty("nextmidas.debug"));
    noNative = Convert.o2z(System.getProperty("NONATIVE", "TRUE")); // native code is disabled by default until BUG 2124 is fixed
    setNoNative(noNative);
    System.setProperty("NOEXTERNAL", "TRUE"); // ensure OS commands run as internal

    this.alwaysCallShellOpen = (flags&ALWAYS_CALL_SHELL_OPEN) != 0; // since 3.5.2 for BUG 2828
    this.prevShell = Shell.getCurrent();                            // since 3.5.2 for BUG 2828
    this.shell = new Shell(this);

    // set write and read AUX (MUST be after shell instance is created above)
    setWriteAux(DEF_WRITE_AUX);  // set to default
    setReadAux(DEF_READ_AUX);    // set to default

    this.terminal = terminal;    // MUST set terminal before calling openShell()

    if (options != null) {
      parseOptions(options);
    }

    openShell();
    if (prevShell != null && (flags&RESTORE_NONNULL_PREV_SHELL) != 0 ) { // since 3.5.2 for BUG 2828
      Shell.setCurrentShell(prevShell);
    }
  }

  /** Return global NeXtMidas session instance (using default options).
   *  @return The global NeXtMidas session instance.
   *  @since NeXtMidas 3.3.0
   */
  public static NeXtMidas getGlobalInstance () {
    return INSTANCE;
  }

  /** Run a NeXtMidas command.
      @param args The command to run plus any NeXtMidas parameters to be set
   */
  public static void main (String[] args) {
    NeXtMidas nxm = new NeXtMidas(null, ALWAYS_CALL_SHELL_OPEN, args);
    if (nxm.executeList.isEmpty()) { // if no commands are given, then default to -shell for BUG 2317
      nxm.determineInteractiveShell(false);
    }
    nxm.runShell(); // run with parsed command-line options
  }

  /** run with parsed command-line options (which includes running commands/scripts).
      @see #runCommand(String)
   */
  public void runShell () {
    for (String cmd : executeList ) {
      runCommand(cmd);
    }
    if (runInteractiveTextTerminal) {
      shell.process(); // this will start the interactive text terminal shell
    }
  }

  /** run with parsed command-line options (which includes running commands/scripts).
      @see #runCommand(String)
   */
  public void run () {
    runShell(); // NXM-4030 Jeff's preferred name
  }

  /** open/initialize shell. MUST be called before {@link #run()} or {@link #runCommand(String)} */
  public void openShell () {
    // MUST set Shell Privileges: set to ALL for now
    Shell.setPriv(Privileges.PRIV_ALL);             // TODO: extract setting privileges from Shell.run() so that we can use it here?
    if (prevShell == null || alwaysCallShellOpen) { // since 3.5.2 skip if there previously was a Shell opened unless alwaysCallShellOpen
      shell.open();                                 // initialize shell (this call's shell.boot())
    } else if (prevShell != null) {                 // since 3.5.2
      shell.M.results.setPrevious(prevShell.M.results);    // point to prevShell's results table
      String[] prevDictionaryEntries = Arrays.copyOfRange(prevShell.M.dictionary.entry, 0, prevShell.M.dictionary.size);
      shell.M.dictionary.fromArray(prevDictionaryEntries); // init with a copy of prevShell's command dictionary
    }
    if (auxEntries.size() > 0) updateAuxList();     // re-apply any user-set aux's. (Bug 2325)
  }

  /** Run a NeXtMidas command. <br>
   *  <br>
   *  WARNING: DO NOT run a graphical NeXtMidas command like (LIST2 or PLOT) from the Swing/AWT
   *           Event-Dispatch-Thread (EDT) without the /BG switch otherwise it can lockup the JVM.
   *  @param cmd command line to run
   *  @return reference to the Command that is executed (null if cmdline is empty)
   */
  public Command runCommand (String cmd) {
    return runCommand(cmd, panel);
  }

  /** Run a NeXtMidas command with option to embed a graphical command inside specified panel
   *  (@link MidasDisplay). <br>
   *  <br>
   *  WARNING: DO NOT run a graphical NeXtMidas command like (LIST2 or PLOT) from the Swing/AWT
   *           Event-Dispatch-Thread (EDT) without the /BG switch otherwise it can lockup the JVM.
   *  @param cmd command line to run
   *  @param panel JPanel (panel) to embed graphical command in (update in 3.9.1 to go from
   *               MidasDisplay to JPanel for jdk 11 modularity)
   *  @return reference to the Command that is executed (null if cmdline is empty)
   *  @since NeXtMidas 3.3.0
   */
  public Command runCommand (String cmd, JPanel panel) {
    if (DEBUG) debug("running command: [["+cmd+"]]"+ (panel==null? "" : " on panel = "+panel));
    //shell.run(this, cmd, false);    // NOTE: CANNOT USE THIS since it calls shell's open()/boot() which causes values to get reset
    //Shell.runNew(this, cmd, false); // NOTE: same as above
    Command command = shell.runCommand(cmd, panel);
    return command;
  }

  /**
   * Method designed to simplify adding an option tree, by using a
   * service provider interfaces. (SPI).
   *
   * This method used a service provider to retrieve all the option
   * name from every jar file that has implemented an optionTreeAutoLoaderInterface
   *
   * Fix 2a. NXM-2900 Issues defining path/options
   * NOTE: This fix is still under development currently because
   * changes are required to the build command in NeXtMidas.
   * @return the current new path
   * @since NeXtMidas 3.7.1
   */
  @InternalUseOnly
  public String addOptionTreePathUsingSPI() {
    OptionTreeAutoLoaderService option = OptionTreeAutoLoaderService.getInstance();
    String optName = option.optionTreePathFromClasspath();
    return optName;
  }

  /**
   * Method designed to simplify adding an option tree, by performing the
   * funtionality of both the OPTION and PATH commands.
   *
   * Fix 1a. NXM-2900 Issues defining path/options
   * @param optName  The option tree name
   * @param optFlag  The enumeration OptionFlags for ADD, ADD+ and PUT
   * @return true if path was updated
   * @since NeXtMidas 3.7.1
   */
  public boolean addOptionTreeToPathClasspath(String optName, OptionFlags optFlag) {
    boolean pathUpdated = false;
    if (optName == null || optName.length() <= 0) shell.M.warning("No Option Tree specified");
    else if (optFlag == null) shell.M.warning("No enumeration option flag specified");
    else if (optFlag.toString() == null) shell.M.warning("The OptionFlag is not a valid option [ADD,ADD+,PUT");
    else {
      String optVal = optName.toUpperCase();
      Parser parse = new Parser(optVal);
      String[] opts = parse.getArray(parse.elements());
      for (String opt: opts) {
        String optCommand = "OPTION,"+opt+",CLASSPATH";
        runCommand(optCommand);
      };
      String flag = optFlag.toString();
      String npath = "PATH,"+flag+",\""+optVal+"\"";
      runCommand(npath);
      pathUpdated = true;
    }
    return pathUpdated;
  }

  /** set if no native code is ro be used (i.e. pure Java)
   *  @param value true if no native code is to be used
   */
  public void setNoNative (boolean value) {
    noNative = value;
    System.setProperty("NONATIVE", Boolean.toString(noNative));
  }

  /** get if no native code is being used (i.e. pure Java)
   *  @return true if no native code is being used
   */
  public boolean getNoNative () {
    return noNative;
  }

  /** Replace any of the standard NeXtMidas separators (',' or '|'), Unix
      separators (':') and DOS separators (';') with the specific separator
      required here. This allows the user to use the separator supported by
      their shell without worrying about NeXtMidas not supporting it.
      @param value The value with separator characters in it
      @param sep The separator uses internally
      @return The value with OS separators replaced with the given separator character
   */
  private static String replaceSeparators (String value, char sep) {
    return value.replace('|',sep).replace(',', sep).replace(':', sep).replace(';', sep);
  }

  /** Parse the input for any NeXtMidas context settings (options) and commands
      @param options string array of options (SHOULD NOT contain a null item)
   */
  public void parseOptions (String... options) {
    int ii = 0; // DEBUG
    for (String option : options) {
      if ("--".equals(option)) {
        haltOptionsCheck = true;
      }
      if (DEBUG) { debug("  parseOptions() checking arg "+ (ii++) + ": [["+option+"]]"); }
      parseOption(option);
    } // end for loop for processing options
    haltOptionsCheck = false;  // reset   TODO: this might not always need/want resetting
    escapeCharMode = null;     // reset   TODO: this might not always need/want resetting

    if (DEBUG) { debug("  Commands to execute list: "+executeList); }
  }

  /** Parse the input for any NeXtMidas context settings (options) and commands
      @param options Table of options (SHOULD NOT contain a null item)
   */
  public void parseOptions (Table options) {
    String[] keys = options.getKeys();
    String[] args = new String[keys.length];
    int index     = 0;

    for (String key : keys) {
      Object value = options.get(key);
      if (value==null) args[index++] = "-"+key;
      else             args[index++] = "-"+key+"="+value;
    }
    parseOptions(args);
  }

  /** Parse a 'single' option or command. Options and commands start with a '-'. <br>
      Example:<br>
      <pre>parseOption("-cmd=plot world")</pre>
      @param option The option
   */
  public void parseOption (String option) {
    if (haltOptionsCheck) {
      // TDB
      // 1. rest of options array (args) would be command and it arguments (for #1aa) ?
      // 2. rest of options array (args) would be arguments (for MyApp.jar for #1b)  ?
    } else {
      String key = option; // name of option
      String value = null; // value (if
      int indexEQ = option.indexOf('=');
      if (indexEQ>0) {                                // have equal sign (get value after it)
        key = option.substring(0,indexEQ).trim();     // trim
        value = option.substring(indexEQ + 1).trim(); // +1 to skip equal sign
      }
      setOption(key, value);
    }
  }

  /** Set a single option or command.
      @param key The supported option or command
      @param value The value to set it to
   */
  public void setOption (String key, String value) {
    if (key.charAt(0)!='-') key = "-"+key;  // input key can use '-' or not

    // decode any escapes in the parameter's value
    if (escapeCharMode == EscapeCharMode.Percent) {
      value = StringUtil.decodePercentEncoding(value);
    }

    // process the parameters
    if      ("-aux.read".equalsIgnoreCase(key)) {
      setReadAux(value);
    }
    else if ("-aux.write".equalsIgnoreCase(key)) {
      setWriteAux(value);
    }
    else if ((key.length() > 5) && "-aux.".equalsIgnoreCase(key.substring(0,5))) {
      String auxName = key.substring(5).toUpperCase();
      auxEntries.put(auxName, value);
    }
    else if ("-nonative".equalsIgnoreCase(key)) {
      setNoNative( (value==null) || Convert.o2z(value) );
    }
    else if ("-headless".equalsIgnoreCase(key)) {
      Boolean headless = ( (value==null) || Convert.o2z(value) );
      try {
        System.setProperty("HEADLESS", headless.toString());
      } catch (Exception e) {
        // NOTE: This is an error instead of a warning because we need to fail-fast
        // if we cannot set headless. This avoids situations later on where the graphics
        // init can hang for 30+ seconds which causes other problems
        throw new RuntimeException("Cannot set HEADLESS value", e);
      }
    }
    else if ("-path".equalsIgnoreCase(key)) {
      path = value;
    }
    else if ("-command".equalsIgnoreCase(key) || "-cmd".equalsIgnoreCase(key)) {
      executeList.add(value);
    }
    else if ("-shell".equalsIgnoreCase(key)) {
      determineInteractiveShell(false);
    }
    else if ("-prompt".equalsIgnoreCase(key)) {
      determineInteractiveShell(true);                  // force text terminal
    }
    else if ("-script".equalsIgnoreCase(key)) {
      if (value != null)  executeList.add(getMacroCmd(value));
    }
    else if ("-debug".equalsIgnoreCase(key)) {
      executeList.add("debug/quiet off all");           // clear any debug from any previous cmds
      String debugCmd;;
      if (value==null) debugCmd = "debug on deprecate"; // default value
      else debugCmd = "debug/quiet on "+value;
      executeList.add(debugCmd);
    }
    else if ("-encoding".equalsIgnoreCase(key)) {
      if      (value==null || value.length() == 0) escapeCharMode = null;
      else if (value.equalsIgnoreCase("None"))     escapeCharMode = null;
      else if (value.equalsIgnoreCase("Percent"))  escapeCharMode = EscapeCharMode.Percent;
      else if (value.equalsIgnoreCase("%"))        escapeCharMode = EscapeCharMode.Percent;
      else throw new IllegalArgumentException("Illegal parameter " +key+"="+value);
    }
    else if ("-version".equalsIgnoreCase(key) || "-v".equalsIgnoreCase(key)) {
      // Print out NXM version to System.out (similar to "java -version")
      executeList.add("VERSION SYS"); // since 3.5.2 as part of BUG 2828
    }
    else if ("-verboseversion".equalsIgnoreCase(key) || "-vv".equalsIgnoreCase(key)) { // since 3.5.2 as part of BUG 2828
      executeList.add("VERSION/ALL");
    }
    else if ("-help".equalsIgnoreCase(key) || "-h".equalsIgnoreCase(key) || "-?".equals(key)) {
      // print usage to System.out (similar to "java -help")
      executeList.add(""); // blank command to skip running interactive shell, if user does not specify it
    }
    else {
      throw new IllegalArgumentException("Illegal parameter " +key+"="+value);
    }
  }

  /** Get help on usage of this class (showing available options). Similar to java -help.
      @return help string on how to use this class with all available options
      @since NeXtMidas 3.1.3
   */
  public static String getUsage () {
    // NOTE1: wrap output text at or before 80th character ---------------------------------V
    // NOTE2: keep option list alphabetical
    //     "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
    return "Usage: [-options]*\n"
         + "\n"
         + "where options include:\n"
         + "  -aux.<auxname>=<value>  Define a disk AUX entry to a path or URL\n"
         + "  -aux.read=<auxlist>     Set the read AUX list\n"
         + "  -aux.write=<aux>        Set the write AUX\n"
         + "  -cmd=<command>          Alias for -command\n"
         + "  -command=<command>      A command with any necessary arguments to execute.\n"
         + "  -debug=<value>          Set debug to masked value (deprecate, trace, etc.)\n"
         + "  -encoding=<type>        Set the type of escape char enocding (\"NONE\", \n"
         + "                          \"Percent\", or \"%\")\n"
         + "  -headless=[true|false]  Set HEADLESS mode\n"
         + "  -help, -h, -?           Print this usage help message\n"
         + "  -nonative=[true|false]  Disable(true)/enable(false) native code\n"
         + "  -path=<path>            Set the PATH\n"
         + "  -prompt                 Enter the NeXtMidas 'nM>' terminal shell prompt\n"
         + "                          (executed after all commands and/or scripts)\n"
         + "  -script=<macro>         Run a NeXtMidas macro from HOMEPATH, unless using an\n"
         + "                          absolute path, \"nxm\" dot syntax, or a URL\n"
         + "  -shell                  Open a NeXtMidas SHELLGUI (or -prompt if\n"
         + "                          graphics is not available)\n"
         + "  -verboseversion, -vv    Print out the verbose version of NeXtMidas, Java, etc.\n"
         + "  -version, -v            Print out the NeXtMidas version\n"
         + "\n"
         + "Note: Multiple -command and -script options can be specified to run a series of\n"
         + "      commands. They will be executed in the order specified (left to right).\n"
         ;
    //     "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
  }

  /** Gets the NeXtMidas version as a display string.
   *  @return NeXtMidas version
   *  @since NeXtMidas 3.5.2
   */
  public static String getVersion () {
    return "NeXtMidas version " + Installation.NM_VERSION;
  }

  /** Get verbose version information.
   *  @return verbose version of NeXtMidas, java, OS, etc.
   *  @since NeXtMidas 3.5.2
   */
  public static String getVerboseVersion () {
    StringBuilder sb = new StringBuilder(256);
    // NeXtMidas version and NMROOT
    sb.append(getVersion()).append("\nNMROOT: ").append(INSTANCE.getNmRoot());

    // the below are Secure System Properties Accessible by All RIAs (Web Start, etc.)
    sb.append("\nJava version: ").append(Shell.getProperty("java.version"))
      .append(", class version: ").append(Shell.getProperty("java.class.version"))
      .append(", vendor: ").append(Shell.getProperty("java.vendor"));

    sb.append("\nOS name: ").append(Shell.getProperty("os.name"))
      .append(", version: ").append(Shell.getProperty("os.version"))
      .append(", arch: ").append(Shell.getProperty("os.arch"));
    return sb.toString();
  }

  /** determine which interactive shell to use. If not in headless mode, then use
      SHELLGUI otherwise use OS text terminal shell.
      @param forceTextTerminal true to force using text terminal, false to auto-detect
   */
  private void determineInteractiveShell (boolean forceTextTerminal) {
    if ((forceTextTerminal || Shell.isHeadless()) || !(shellGuiExists)) {
      runInteractiveTextTerminal = true;
      executeList.add(""); // since 3.1.3: need to give blank command otherwise -prompt will run SHELLGUI first, then interactive text shell
    }
    else {
      executeList.add("SHELLGUI;SYS");
    }
  }

  /** Set READ AUX list. Requires Shell instance!
      See {@link #addAuxEntry(String, String)} and {@link #removeAuxEntry(String)} for defining and removing AUX entries.
      See AUXILIARY explain file for more details.
      @param readAuxList list of AUXes (pipe, comma, etc. separated) for READ AUX list
   */
  public void setReadAux (String readAuxList) {
    auxEntries.put("READ", replaceSeparators(readAuxList, '|'));
    updateAuxList();
  }

  /** Set WRITE AUX. Requires Shell instance!
      See {@link #addAuxEntry(String, String)} and {@link #removeAuxEntry(String)} for defining and removing AUX entries.
      See AUXILIARY explain file for more details.
      @param writeAuxName name of write AUX
      @see #addAuxEntry(String, String)
   */
  public void setWriteAux (String writeAuxName) {
    auxEntries.put("WRITE", writeAuxName);
    updateAuxList();
  }

  /** Add (i.e. define) AUX entry. Requires Shell instance!
   *  If the AUX entry exists, this will overwrite it.
   *  See AUXILIARY explain file for more details.
   *  @param auxName AUX Name (cannot be null, will be capitalized)
   *  @param path    the path (to a directory) or URL associated with the AUX entry
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public void addAuxEntry (String auxName, String path) {
    auxName = auxName.toUpperCase();
    shell.M.results.put("AUX."+auxName, path, Table.ADDROOT|Table.FORCE);
    boolean exists = shell.M.io.dexists(path);
    if (!exists) {
      shell.M.warning("Specified path for AUX entry [" + auxName + "] does not exist at " + path);
    }
  }

  /** Remove AUX Entry. Requires Shell instance!
   *  @param auxName Remove AUX entry with this name (cannot be null, will be capitalized)
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public void removeAuxEntry (String auxName) {
    auxName = auxName.toUpperCase();
    shell.M.results.remove("AUX."+auxName, Table.ADDROOT|Table.FORCE);
  }

  /** Get the path associated with this AUX.  Requires Shell instance!
   *  @param auxName - Name of AUX entry to look up (cannot be null, will be capitalized)
   *  @return path associated with this AUX name
   *  @since NeXtMidas 3.5.2  (bug 2828)
   */
  public String getAuxPath (String auxName) {
    auxName = auxName.toUpperCase();
    return shell.M.results.getS("AUX."+auxName);
  }

  /** Update AUX entries. Requires this.shell instance! */
  private void updateAuxList () {
    for (Map.Entry<String, String> e : auxEntries.entrySet() ) {
      // The next line follows what is done for CWD within Shell.configure()
      shell.M.results.put("AUX."+e.getKey(), e.getValue(), Table.ADDROOT|Table.FORCE);
    }
  }

  private String getMacroCmd (String url) {
//  FileName fn = new FileName(url); // prior to 3.1.2 (does not work for -script=nxm.sys.test... when Shell has not opened.)
//  String macroName = fn.getRoot(); // so do not use FileName.getRoot()
    String macroName = FileName.getRoot(url); // since 3.1.2: fix issue with NXM dot syntax before Shell has opened
    String cmd="%"+macroName+" /MFN="+url;
    return cmd;
  }

  /** Set Debug Option Mask
   *  @param optionMask debug mask in String form
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public void setDebug (String optionMask) {
    // Convert the String Mask to an Integer Mask, with the default being deprecate
    int debugIntMask = Parser.mask(Debug.debugList, optionMask, Debug.DEPRECATE);
    // Set debug mask for this Midas instance
    shell.M.setDebug(debugIntMask);
    // Save in Global Debug Settings, this makes the behavior the same as -debug=...
    Debug.saveGlobalDebugSettings(shell.M.results, debugIntMask);
  }

  /** Get the Debug Mask
   *  @return debug mask in String form
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public String getDebug () {
    return shell.M.getDebug();
  }

  /** Set HEADLESS property
   *  @param isHeadless true if headless, false if not
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public void setHeadless (boolean isHeadless) {
    try {
      Boolean headless = isHeadless;
      System.setProperty("HEADLESS", headless.toString());
    } catch (Exception e) {
      // NOTE: This is an error instead of a warning because we need to fail-fast
      // if we cannot set headless. This avoids situations later on where the graphics
      // init can hang for 30+ seconds which causes other problems
      throw new RuntimeException("Cannot set HEADLESS value", e);
    }
  }

  /** Get HEADLESS as boolean
   *  @return true if headless, false if not
   *  @since NeXtMidas 3.5.2 (bug 2828)
   */
  public boolean isHeadless () {
    String headlessString = System.getProperty("HEADLESS","true");
    Boolean isHeadless = Boolean.valueOf(headlessString);
    return isHeadless;
  }

  /** Set the NeXtMidas path.
   *  @param newPath new path of option trees (comma separated)
   *  @since NeXtMidas 3.5.2
   */
  public void setPath (String newPath) {
    path = newPath;
    runCommand("PATH SET " + newPath);
  }

  /** Get the NeXtMidas path.
   *  @return path
   *  @since NeXtMidas 3.5.2
   */
  public String getPath () {
    String globalPath = Shell.options.getS("PATH");
    if (globalPath == null) {
      return path;
    }
    else if (!globalPath.equals(path)) {
      // path = globalPath; TODO: consider if we want to update path to be same as globalPath?
      return globalPath;
    }
    return path;
  }

  //////////////////////////////////////////////////////////////////////////////
  // Begin implement ShellParent interface
  //////////////////////////////////////////////////////////////////////////////
  @Override
  public String getNmRoot () {
    String nmroot = System.getProperty("NMROOT",OptionTree.findOptRoot("SYS"));
    if (nmroot.startsWith("file:") && !(nmroot.contains(".jar"))) nmroot = nmroot.substring(5,nmroot.length());
    return nmroot;
    //return "classpath:";
  }

  @Override
  public String getAux () {
    String write = Convert.o2s(shell.M.results.get("AUX.WRITE"));
    String read  = Convert.o2s(shell.M.results.get("AUX.READ"));
    return write+" "+read;
  }

  @Override
  public String getOpts () {
    return OptionTree.getOptsHelper(path, this, false);
  }

  @Override
  public String getCWD () {
    String cwd = System.getProperty("user.dir"); // NOTE: Sandboxed RIAs cannot access this
    if (cwd != null) cwd= FileUtil.terminatePath(cwd);
    return cwd;
  }

  @Override
  public String getNMS () {
    return Shell.getProperty("NMSTARTUP.USER"); // since 3.1.3 as part of BUG 1447
  }

  @Override
  public String getHome () {
    String home = System.getenv("HOME");                     // 1. try HOME environment variable
    if (home == null) home = Shell.getProperty("user.home"); // 2. try system property (since 3.5.2) NOTE: Sandboxed RIAs cannot access this
    if (home != null) home = FileUtil.terminatePath(home);
    return home;
  }

  @Override
  public String getServer () {
    return null;
  }

  /** Gets theme based on "ENV.THEME" system property, otherwise returns null
   *  to use system defaults.
   *  @since NeXtMidas 2.9.3
   */
  @Override
  public String getTheme () {
    return Shell.getProperty("ENV.THEME");
  }

  /** Gets style based on "ENV.STYLE" system property, otherwise returns null
   *  to use system defaults.
   *  @since NeXtMidas 2.9.3
   */
  @Override
  public String getStyle () {
    return Shell.getProperty("ENV.STYLE");
  }

  /** Returns a terminal that NeXtMidas should use, null if the Shell should pick
   *  an appropriate terminal.
   *  @since NeXtMidas 3.3.0
   */
  @Override
  public Terminal getTerminal () {
    return terminal;
  }
  //////////////////////////////////////////////////////////////////////////////
  // End implement ShellParent interface
  //////////////////////////////////////////////////////////////////////////////

  /** Prints an internal debug message. */
  private final void debug (String msg) {
    System.out.println("DEBUG: "+msg);
  }

//  /** Set the graphics window panel that graphical commands will use (to embed inside of).
//   *  @param panel The panel.
//   *  @deprecated Since 3.3.0: Use {@link #runCommand(String, JPanel)} instead.
//   */
//  @Deprecated
//  public void setPanel (nxm.sys.libg.MidasDisplay panel) { // TODO: remove in 3.9+
//    this.panel = panel;
//  }

//  /** @deprecated Since NeXtMidas 3.5.4, use {@link #runCommand(String, JPanel)} instead.
//   *  @return the panel.
//   */
//  @Deprecated
//  public javax.swing.JComponent getPanel () { return panel; } // TODO: remove in 3.9+

  /** Get current shell driving this instance.
   *  @return shell driving this instance
   *  @since NeXtMidas 3.3.0
   */
  public Shell getShell () {
    return shell;
  }

  /** Get current Midas context for this instance.
   *  @return Midas context for current shell on this instance
   *  @since NeXtMidas 3.3.0
   */
  public Midas getMidasContext () {
    return shell.M;
  }

  /** @since NeXtMidas 3.5.1 */
  @Override // implement MidasReference interface
  public Midas getMidas () {
    return shell.M;
  }
}